Writing functions to manage specific tasks can make your code more modular, more readable, and easier to maintain. In particular, when writing code to manage hardware resources, functions can offer higher-level interfaces that abstract away hardware-specific information like register addresses and bit-field definitions.
A good candidate for such a function is UART configuration – the sample code shown earlier can be used as a UART configuration function at start-up. After the UART has been configured, it’s ready to send and receive data without the CPU playing any role. To productively access UART data, the CPU must know certain details, like when newly received data is available, whether all data has been removed from the FIFO, whether received data must be read to clear room in the FIFO to prevent loss of new data, etc. The Interrupt Status Register (ISR) contains the needed information, and simple functions can be written to check the appropriate bits.
For example, to check whether the transmit FIFO is full, a function can read the appropriate status register bit and return a “flag” to show the status (a flag is typically just a ‘1’ or a ‘0’ written to the LSB of a register to indicate some status). The fourth bit of the UART status register shows whether the TXD FIFO is full - if this bit is set, no more entries can be accepted into the transmit FIFO. The code below shows an example “FIFO full” function that returns a flag in R0.
.equ UART1_SR, 0xE000102C
.equ UART1_DATA, 0xE0001030
@ Checks status of uart1 TX FIFO
@ Inputs: none
@ Returns r0, status of tx fifo 1:full, 0:not full
uart1_tx_empty:
mov r0,#0 @init r0
ldr r2, =UART1_SR
ldr r1, [r2] @get status flags
ands r1, #16 @check if 'bit 4' is set
movne r0,#1 @return 1 if set
bx lr @return
A similar function can be written to check whether the RXD FIFO is empty - bit 1 of the same status register indicates whether the RXD FIFO is empty. If this bit is clear, the FIFO has entries that are ready to read. Prior to accessing either of the FIFOs, it’s a good idea to check the status first.
A function that sends a character to the UART is shown below. The function receives the character to be sent as a parameter loaded into R0 – defining simple functions like this, and using parameters (in registers) to send information to functions generally makes for more readable and maintainable code. The function uses the stack to backup R4 and LR, and then loads information from R0 into R4 (if you are not sure what’s going on, it might be a good time to brush up on ARM calling conventions). Note the function also uses the previous function to get the fill status of the TX FIFO before trying to write data.
@ Places the low byte of R0 into UART TX FIFO; if FIFO full, blocks until FIFO has room
@ Inputs: r0 <- data to be sent
@ Returns: none
uart1_put_char:
push {r4,lr} @ backup the registers used
and r4,r0,#0xFF @ save bottom byte R0 (input) to r4
check_full_loop:
bl uart1_tx_empty @ check if uart1 tx has room
cmp r0,#1 @ check the return value
beq check_full_loop @ keep checking until room in FIFO
ldr r1, =UART1_DATA
str r4,[r1] @write to tx fifo
pop {r4,lr} @restore registers
bx lr
The code below shows an example that uses the function above.
.equ F_ASCII_CONST, 70 @constant holding the ascii value for capital F
main:
bl configure_uart1 @configure the uart module
mov r0, #F_ASCII_CONST @load 'F' as parameter to send
bl uart1_put_char @send 'F'
b. @halt program here
Functions written in C can implement the same behavior and use the same addresses. If you are new to writing C, you can refer to the Embedded C Primer.
#define UART1_SR *((uint32_t *) 0xE000102C) //UART1 status reg
#define UART1_DATA *((uint32_t *) 0xE0001030) //UART1 TX/RX FIFO DATA
//check if room for bytes in tx fifo
int uart1_tx_full()
{
return (UART1_SR & 16)!=0; //Check SR register bit4, return 1 if fifo full
}
void uart1_put_char(char dat)
{
while(uart1_tx_full()); //wait for room in FIFO
UART1_DATA = dat; //write to TX FIFO
}
Dissassembly
Compiling a C program produces machine code, just like assembling an assembly program does. Since Assembly instructions have a one-to-one mapping to machine code, machine code can be disassembled into assembly language. Looking at disassembled C code can provide insight into how C programs work on the hardware. Disassembled code can be found in the “.elf” file produced by the complier (you can open and examine the .elf file in the SDK tool). The code below is copied from the elf file produced by compiling the C code above (some notes have been added). The first column is the address of the instruction, followed by the opcode found at that address. The disassembled code for each instruction is shown to the right of the machine codes.
int uart1_tx_full()
{
Note: Frame pointer operations, used in C ABI
100910: e52db004 push {fp} ; (str fp, [sp, #-4]!)
100914: e28db000 add fp, sp, #0
Note:These two instructions load the Status Register's address
100918: e301302c movw r3, #4140 ; 0x102c
10091c: e34e3000 movt r3, #57344 ; 0xe000
Note: Loads the status flag and check if 'bit 4' is clear
100920: e5933000 ldr r3, [r3]
100924: e2033010 and r3, r3, #16
100928: e3530000 cmp r3, #0
Note: set if bit 4 is clear
10092c: 13a03001 movne r3, #1
100930: 03a03000 moveq r3, #0
Note: zero-extension
100934: e6ef3073 uxtb r3, r3
}
Note: Copy to return parameter
100938: e1a00003 mov r0, r3
Note: Return Frame pointer operations
10093c: e24bd000 sub sp, fp, #0
100940: e49db004 pop {fp} ; (ldr fp, [sp], #4)
100944: e12fff1e bx lr
The disassembled code above is identical to the assembly version, with the addition of some code to manage the stack. In the relevant code, the address of the status register is loaded, the data from that address is loaded, and then then the specific bit is checked: if it is set, a return value of ‘1’ is loaded, else a ‘0’ is loaded.