UART Controller

Working with ZYNQ's UARTs



A “Universal Asynchronous Receiver Transmitter” (UART) is a computer hardware circuit that is used to send serialized data from one device to another. UARTs use a single wire to move a data, without sending any timing signal to synchronize the data (that’s where the word Asynchronous in the title comes from). UART signals are unidirectional, meaning they always move data in one direction. To move in both directions, two wires are needed – one to transmit data (TXD), and one to receive data (RXD). Both the sender and receiver must agree on the data rate (in bits per second, or “baud”), so the receiver can read the data bits at the proper time.

UARTs were used in the original “serial port” communications systems, and they have been in widespread use for more than 60 years. In more recent times, other serial bus systems like USB (for computer-to-device communications) and SPI (for on-board communications) have displaced UARTs in most applications, but they are still commonly encountered. You can read more about UART communication circuits here: UARTs and Serial ports

Blackboard’s UARTs

The ZYNQ device includes two full-duplex UART controllers that are accessed through registers available at specific addresses on the ARM’s bus (see below). UART0 (base address 0xE0000000) is attached to the onboard ESP32 WIFI/Bluetooth radio, and UART1 (base address 0xE0001000) is attached to the FTDI 2232 USB controller. UART0 is used to configure the radio and to move transmit and receive radio data, and UART1 creates a serial “COM” port to the PC. For information on UART0 and its use with the ESP32 radio, see the radio document on this webpage.

The UART1 RXD and TXD signals are “passed through” the USB controller so that the PC’s COM port appears to be directly connected to ZYNQ’s UART1. This COM port can be used with a Windows (or Linux) terminal emulator program to easily communicate with the Blackboard.

Figure 1. Blackboard's UARTs
Figure 1. Blackboard’s UARTs
ZYNQs UART Controller

ZYNQ’s UART controllers are documented in chapter 19 of the ZYNQ TRM. Several of the most useful features and register definitions are also described here.

ZYNQ’s UART controllers include a programmable clock generator that can produce many different baud rates, a managed 64-byte FIFO, as well as a parity generator and tester. The UART controller also includes flow control functions that are useful with older modems. The flow control functions will not be discussed here (see the Xilinx TRM page 1175 for a more complete UART description). The UART clock, parity generator, FIFO management, and other control parameters must be properly setup before the UART can be used.

Clock Management

Each UART controller uses a UART_Clk that can either be the 100MHz master input clock, or a 12.5MHz clock (master clock divided by 8). The UART_Clk is then used to produce two different internal clocks using simple counter dividers. The faster of the two clocks, here called the “baud_sample_clock” or “RXD_clock”, is used to sample the incoming RXD data signal. The second lower frequency clock, here called the “bit_period_clock" or “TXD_clock”, is used by the transmitter to shift data out the TXD pin. The TXD_clock frequency defines the rate at which the TXD pin can change state, and this is also known as the “baud rate”. A typical clock setup will define 16, 32, or more RXD_clocks per TXD_clock.

The RXD_clock uses a higher frequency so it can sample the RXD input signal several times during each bit period. By taking several samples, the receiver can do a better job of sampling the RXD pin in the middle of the bit period, and it can do a better job of filtering and rejecting noise. ZYNQ’s UART controllers divide the UART_Clk clock input by a user-set 16-bit value to produce the RXD_clock. Subsequently, this RXD_clock is divided by a user-set 8-bit number to define the TXD_clock. The 16-bit divider value is written through the BAUDGEN register, and the 8-bit divider value is set through the BAUD_RATE_D register (note: prior to doing the divide, a “1” is added to the value stored in the BAUD_RATE_D register).

Figure 2. ZYNQ's UART Clock control
Figure 2. ZYNQ’s UART Clock control


Both the transmit and receive data paths in ZYNQ include a 64-byte managed FIFO, and both FIFOs are accessed using the same 8-bit data port (the FIFO register at offset 0x00000030). Several status bits are produced automatically, and those bits can be read (polled) or they can drive interrupts. The topic document provides background information on managed hardware FIFOs: Managed hardware FIFOs

Transmit data written to the FIFO will automatically be serialized and sent out the TXD pin. Data is sent as 8-bit bytes, wrapped inside a 10, 11, or 12 bit packet depending on options. A start bit is always shifted out first (the start bit is ‘0’ since the TXD signal idles at ‘1’), then the 8 data bits, then an optional parity bit (if parity was enabled), and then one or two stop bits. These 10, 11, or 12 bit packets will be sent continuously as long as data remains in the transmit FIFO. Depending on the defined bit-clock, each packet could take 40us to several milliseconds to send. Data is shifted out on the falling edge of the TXD_clock.

If the processor writes data into the FIFO faster than it can be sent out the TXD pin, the FIFO will fill and once the FIFO is full, new data will not be enqueued. It is up to the user program to ensure the FIFO doesn’t fill, either by checking the transmit FIFO status bits, or by setting interrupts (including possibly setting a “trigger point" to indicate when FIFO is nearing full status).

The receive module constantly oversamples the RXD data pin using the RXD_clock. When a 1-0 transition is first detected, a start bit may be present. The receiver waits for one-half of a bit time (based on the baud rate), and then samples the RXD signal three more times using the baud_sample clock. If all three samples are “0”, a start bit is assumed. The receiver then waits one bit-clock period, then samples RXD again in the theoretical center of the LSB, and then waits another bit-clock period and samples bit1, and so on for all 8 bits.

The sampled data from the RXD pin is loaded into the receive shift register. The receive module checks the parity bit (if enabled), and checks for the selected number of stop bits (parity or framing errors are generated if these bits are not correct). If a parity or framing error is detected, it is up to the user program to deal with the issue (usually by invalidating the received data, and communicating to the sender that data must be resent).

Received data must be read by the processor at least as fast as it arrives, or the FIFO will fill and new data will be lost. When parallelized data enters the receive FIFO, the RxFIFO empty flag is set to “0”, and it remains low until all data has been read from the FIFO (reading from an empty FIFO returns all zeros). If the read FIFO is filled, new data cannot be written into the FIFO. A threshold trigger level can be set to issue an interrupt when the receive FIFO reaches a certain fill level. Readable status bits are available to indicate when the receive FIFO has at least one data point, and when it is full, empty, or nearly full. Interrupts can also be set to indicate these conditions, or when the FIFO data has reached an user-settable trigger point.

Figure 3. UART FIFOs
Figure 3. UART FIFOs


The table below shows the relevant registers available in ZYNQ’s UART controllers (some registers that are used to connect old external modems are not shown).

Figure 4. ZYNQ's UART Registers
Figure 4. ZYNQ’s UART Registers

UART_CR (0x00000000): Configuration register for overall configuration settings (read/write).

TB: Stop transmitter break (1 to stop transmission of the break after one character)
SB: Start transmitter break (1 to start transmission of break after FIFO empty)
RT: Restart transceiver timeout counter (1 to restart counter)
TD: Transmit Disable (1 to disable)
TE: Transmit Enable (1 to enable)
RD: Receive Disable (1 to disable)
RE: Receive Enable (1 to enable)
SRT: Software Reset for TXD (1 to reset transmitter and delete all TXD FIFO data)
SRR: Software Reset for RXD (1 to reset receiver and delete all RXD FIFO data)

UART_MR (0x00000004): Mode register to define data formats (read/write).

CM: Channel Mode (00: Normal; 01: automatic echo; 10: local loopback; 11: remote loopback)
SBITS: Number of start bits (00: 1 stop bit; 01: 1.5 stop bits; 10: two stop bits; 11: reserve red)
PARITY: Parity (000: even; 001: odd; 010: forced to 0/space; 011: forced to 1/mark; 1xx: No parity)
CHLN: Character Length (0x: 8 bits; 10: 7 bits; 11: 6 bits)
CL: Master/input clock select (0: 100MHz; 1: 12.5MHz)

UART_IER (0x00000008): Enable Interrupts. Writing a ‘1’ enables corresponding interrupt (write only).

TO: Transmitter FIFO overflow (1 to enable)
NF: Transmitter FIFO nearly full (1 to enable)
TI: Transmitter FIFO trigger interrupt (1 to enable)
MS: Modem status indicator (1 to enable)
RT: Receiver timeout error (1 to enable)
RP: Receiver parity error (1 to enable)
RFR: Receiver framing error (1 to enable)
RO: Receiver FIFO overflow (1 to enable)
TF: Transmitter FIFO full (1 to enable)
TE: Transmitter FIFO empty (1 to enable)
RF: Receiver FIFO full (1 to enable)
RE: Receiver FIFO empty (1 to enable)
RTR: Receiver FIFO trigger interrupt (1 to enable)

UART_IDR (0x0000000C): Disable Interrupts. Writing a ‘1’ disables corresponding interrupt (write only).

TO: Transmitter FIFO overflow (1 to disable)
NF: Transmitter FIFO nearly full (1 to disable)
TI: Transmitter FIFO trigger interrupt (1 to disable)
MS: Modem status indicator (1 to disable)
RT: Receiver timeout error (1 to disable)
RP: Receiver parity error (1 to disable)
RFR: Receiver framing error (1 to disable)
RO: Receiver FIFO overflow (1 to disable)
TF: Transmitter FIFO full (1 to disable)
TE: Transmitter FIFO empty (1 to disable)
RF: Receiver FIFO full (1 to disable)
RE: Receiver FIFO empty (1 to disable)
RTR: Receiver FIFO trigger interrupt (1 to disable)

UART_IMR (0x00000010): Mask bits for Interrupts. Reading a ‘1’ bit indicates interrupt is enabled; reading a ‘0’ indicates corresponding interrupt is masked/disabled (read only).

TO: Transmitter FIFO overflow (1 for enabled)
NF: Transmitter FIFO nearly full (1 for enabled)
TI: Transmitter FIFO trigger interrupt (1 for enabled)
MS: Modem status indicator (1 for enabled)
RT: Receiver timeout error (1 for enabled)
RP: Receiver parity error (1 for enabled)
RFR: Receiver framing error (1 for enabled)
RO: Receiver FIFO overflow (1 for enabled)
TF: Transmitter FIFO full (1 for enabled)
TE: Transmitter FIFO empty (1 for enabled)
RF: Receiver FIFO full (1 for enabled)
RE: Receiver FIFO empty (1 for enabled)
RTR: Receiver FIFO trigger interrupt (1 for enabled)

UART_ISR (0x00000014): Shows any interrupts that have occurred since this register was last cleared. This register can be read for polling. Writing will clear (write to clear).

TO: Transmitter FIFO overflow (1 indicates FIFO is full)
NF: Transmitter FIFO nearly full (1 indicates FIFO nearly full)
TI: Transmitter FIFO trigger interrupt (1 indicates FIFO fill trigger level reached)
MS: Modem status indicator (1 indicates a modem status flag has been set)
RT: Receiver timeout error (1 indicates receiver timeout counter expired)
RP: Receiver parity error (1 indicates parity bit does not match expected value)
RFR: Receiver framing error (1 indicates stop bit not detected)
RO: Receiver FIFO overflow (1 indicates received byte not yet transferred to FIFO/FIFO full)
TF: Transmitter FIFO full (1 indicates last written byte took last available FIFO spot)
TE: Transmitter FIFO empty (1 indicates final byte in FIFO has been sent)
RF: Receiver FIFO full (1 indicates last received byte took final FIFO spot)
RE: Receiver FIFO empty (1 indicates final FIFO byte has been read)
RTR: Receiver FIFO trigger interrupt (1 indicates FIFO fill trigger lever reached)

UART_BAUDGEN (0x00000018): Divider value to create RXD_clock (read/write).

BAUDGEN: 16-bit divider applied to master clock input to define RXD_clock value

UART_RXTOUT (0x0000001C): Receiver timeout value. Sets max idle time (in RXD-clocks) on receiver data line before status bit set and/or interrupt generated (read/write).

RT: 8-bit value (in RXD-clocks) to set max idle time before status bit set and/or interrupt generated

UART_RXWM (0x00000020): Receiver fill trigger level. Sets receive FIFO fill level at which status bit set and/or interrupt generated (read/write).

RFIFOLVL: 6-bit RXD FIFO trigger value determines at what level status bit set and/or interrupt generated

UART_SR (0x0000002C): Status register. Allows “raw” FIFO status bits to be read (read only).

FS: FIFO status (1 indicates FIFO is nearly full – see TFM for details)
FTS: Transmitter FIFO trigger status (1 indicates FIFO fill level is greater than trigger value)
RDS: Receiver FIFO flow delay trigger (1 indicates FIFO fill level greater than FDEL)
TSS: Transmitter state machine status (1 indicates transmitter active)
RSS: Receiver state machine status (1 indicates transmitter active)
TFS: Transmitter FIFO full status (1 indicates FIFO full)
TES: Transmitter FIFO empty status (1 indicates FIFO empty)
RFS: Receiver FIFO full status (1 indicates FIFO full)
RES: Receiver FIFO empty status (1 indicates FIFO empty)
RTS: Receiver FIFO trigger status (1 indicates FIFO fill level is greater than trigger value)

UART_FIFO (0x00000030): FIFO port register. Read/write data port for FIFO (read/write).

RWFIFO: Read and write FIFO data port

UART_BAUD_RATE_D (0x00000034): Baud rate divider applied to RXD_clock to produce TXD_clock (read/write). TXD_clock is the same as baud rate.

BAUDDIV: 8-bit divider value. Values 3-0 are treated as ‘1’; values from 4-255 are used as divider.

UART_TX_Trigger (0x00000044): Transmit trigger level. Sets level at which TXD trigger level status bit set and/or interrupt generated (read/write).

TFIFOLVL: 6-bit TXD FIFO trigger value determines at what level status bit set and/or interrupt generated

Configuring ZYNQ’s UART Controller

Configuring ZYNQ’s UART Controller Several steps are required to configure ZYNQ’s UART controllers. First, it’s a good idea to reset the UART module by setting the SRT and SRR reset bits in the control register. Writing a ‘1’ to both bits to a ‘1’ starts the reset process, and the UART controller will reset them both to ‘0’ when the reset process is complete. Resetting the Uart controller will discard any data from transmissions in progress, as well as clear the RX and TX FIFOs.

After the reset, the appropriate registers can be programmed. The following steps can occur in any order:

  • Configure the mode register for the desired mode (for example, set the Channel Mode to normal operation; one stop bit; 8 data bits; and the main 100MHz clock);
  • Enable the transmitter and receiver by setting the enable bits in the control register;
  • And configure the baudrate by writing to to the baudgen and bauddiv registers. Some typical/standard baud rates are 9600, 14400, 19200, 115200, 128000 and 256000 bits per second. You don’t need to hit these clock rates exactly, but you should get as close as possible. Several choices of BAUDGEN and BAUD_RATE_D are possible, but whatever values you choose, you should attempt to define an RXD_clock that has at least 16 or more cycles per TXD_clock.

Note that both ends of the UART channel must use all the same settings, or communications will not be possible. Typical settings might be no echo or loopback, 8 data bits, 1 stop bit, no parity, and 115200 baud.

After configuration, the TXD and RXD FIFOs are ready to use, and UART communications can commence. Any data you write to the TXD FIFO will be transmitted automatically on the TXD signal, and any data received on the RXD signal will be placed in the RXD FIFO where it can be read in the order received. The following ARM assembly code example resets the UART and sets a standard configuration.

#uart1 control register
.equ UART1_CR, 0xE0001000
#uart1 mode register
.equ UART1_MR, 0xE0001004

	ldr r1,=UART1_CR

	mov r0, #3	@assert reset
	str r0,[r1]

		ldr r2,[r1]	@get current CR val
		teq r2,r0 	@see if bits are still set
		bne reset_loop	@branch if either set

	bx lr

	push {lr} @push lr for subroutine call

	bl reset_uart1 @reset the module
	ldr r1, =UART1_MR

	#configure the mode register
	#normal mode bits [9:8] = 2b00
	#one stop bit, bits [7:6] = 2b00
	#8 data bits, bits [2:1] = 2b0x
	#use reference clock, bit0 = 0
	#disable parity, bits[5:3] = 3b1xx
	#6b100000 == 0x20
	mov r0, #0x20

	str r0,[r1]

	ldr r1, =UART1_CR

	mov r0,#4 @set bit 2 to enable tx
	orr r0,r0,#16 @set bit 4 to enable rx
	str r0,[r1] @write back to CR
	pop {lr} @restore return address
	bx lr @return

All that’s left to do is configure the baud rate by programming the appropriate regsiters. For example, if you want a baudrate of 115200, you could write 0x7C to the BAUDGEN register and 6 to the BAUD_RATE_D register.

UART1_BAUDGEN	0xE0001018
UART1_BAUDDIV	0xE0001034