UART Functions

Creating functions to support UART operations

2231

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.