Writing UART Routines

Universal Asynchronous Serial Transmission (UART) is one of the simplest ways to allow your microcontroller to communicate with a computer or another chip. Implementing UART in assembly is quite simple and can be done with the instructions we've seen so far. Here we will look at writing UART routines for the ATmega328P. The proccess is the same for all AVR chips, but some of the register names may need to be modified.

Initialization

A simple UART initialization subroutine is shown below

;**************************************************************
;* subroutine: initUART
;*
;* inputs: r17:r16 - baud rate prescale
;*
;* enables UART transmission with 8 data, 1 parity, no stop bit
;* at input baudrate
;*
;* registers modified: r16
;**************************************************************

initUART:
	sts	UBRR0L,r16			; load baud prescale
	sts	UBRR0H,r17			; to UBRR0

	ldi	r16,(1<<RXEN0)|(1<<TXEN0)	; enable transmitter
	sts	UCSR0B,r16			; and receiver

	ret					; return from subroutine

The desired baud prescale must be loaded into registers 16 and 17 before calling this subroutine. For example

	.equ	baud	= 9600			; baudrate
	.equ	bps	= (F_CPU/16/baud) - 1	; baud prescale

	...

	ldi	r16,LOW(bps)			; load baud prescale
	ldi	r17,HIGH(bps)			; into r17:r16

	rcall	initUART			; call initUART subroutine

All constants are treated as 32-bit numbers by the assembler, so we do not have to worry about truncation with the above bps calculation.

Transmitting Characters

A subroutine for transmitting a single character is shown below

;**************************************************************
;* subroutine: putc
;*
;* inputs: r16 - character to transmit
;*
;* transmits single ASCII character via UART
;*
;* registers modified: r17
;**************************************************************

putc:	lds	r17,UCSR0A			; load UCSR0A into r17
	sbrs	r17,UDRE0			; wait for empty transmit buffer
	rjmp	putc				; repeat loop

	sts	UDR0,r16			; transmit character

	ret					; return from subroutine

Using this subroutine simply requires loading a character into r16 before calling it. For example

	ldi	r16,'a'				; load char 'a' into r16
	rcall	putc				; transmit character

Transmitting Strings

More often than not, you will want to transmit an entire string rather than a single character. The easiest way to do this is with a subroutine that accepts a Program Memory address stored in the Z pointer that points to a null terminated string.

;**************************************************************
;* subroutine: puts
;*
;* inputs: ZH:ZL - Program Memory address of string to transmit
;*
;* transmits null terminated string via UART
;*
;* registers modified: r16,r17,r30,r31
;**************************************************************

puts:	lpm	r16,Z+				; load character from pmem
	cpi	r16,$00				; check if null
	breq	puts_end			; branch if null

puts_wait:
	lds	r17,UCSR0A			; load UCSR0A into r17
	sbrs	r17,UDRE0			; wait for empty transmit buffer
	rjmp	puts_wait			; repeat loop

	sts	UDR0,r16			; transmit character
	rjmp	puts				; repeat loop

puts_end:
	ret					; return from subroutine

You should notice that this subroutine uses the labels puts_wait and puts_end. It is good practice to include the subroutine name in labels that appear within the subroutine. This helps prevent them from conflicting with labels in the rest of your program.

To use this subroutine, you must define a null terminated string in Program Memory and load its address into the Z pointer. For example

	ldi	ZL,LOW(2*myStr)			; load Z pointer with
	ldi	ZH,HIGH(2*myStr)		; myStr address
	rcall	puts				; transmit string
	...
myStr:	.db	"Hello",$00

Receiving Characters

Implementing a routine to receive a character over UART can be done as

;**************************************************************
;* subroutine: getc
;*
;* inputs: none
;*
;* outputs:	r16 - character received
;*
;* receives single ASCII character via UART
;*
;* registers modified: r16, r17
;**************************************************************

getc:	lds	r17,UCSR0A			; load UCSR0A into r17
	sbrs	r17,UDRE0			; wait for empty transmit buffer
	rjmp	getc				; repeat loop

	lds	UDR0,r16			; get received character

	ret					; return from subroutine

This subroutine has no inputs so it can be called without any setup. It will put the microcontroller in a loop that will continue until a character is received.

Receiving Strings

Implementing a subroutine to receive a string over UART can be done in many different ways. Here is an example of a subroutine that will receive characters and store them in a buffer until a carriage return is received.

;**************************************************************
;* subroutine: gets
;*
;* inputs: XH:XL - SRAM buffer address for rcv'd string
;*
;* outputs: none
;*
;* receives characters via UART and stores in data memory
;* until carriage return received
;*
;* registers modified: r16, r17, XL, XH
;**************************************************************

gets:	lds	r17,UCSR0A			; load UCSR0A into r17
	sbrs	r17,UDRE0			; wait for empty transmit buffer
	rjmp	putc				; repeat loop

	lds	UDR0,r16			; get received character

	cpi	r16,$0D				; check if rcv'd char is CR
	breq	gets_end			; branch if CR rcv'd

	st	X+,r16				; store character to buffer
	rjmp	gets				; get another character

gets_end:
	ret					; return from subroutine
rjhcoding.com 2018