Project 4 UARTs

Communicate with the Blackboard using a PC's COM port

4002

Introduction

This Project introduces Universal Asynchronous Receive/Transmit (UART) ports and protocols, and uses one of ZYNQ’s UART ports to transfer data between the Blackboard and a PC.

UART ports were among the first data ports ever used in computer systems, and they haven’t changed much in more than 50 years. They are simple and efficient, and widely available and inexpensive, but they are also relatively low performance. From the 1970’s until the early 2000’s, they were built in to virtually every computer system, and they were used for connecting external devices like mice, keyboards, modems, scanners, printers, and many other after-market devices. They were also widely used inside the computer to connect time-of-day clocks, temperature sensors, small displays, and other devices to the CPU. In the last 20 years, they have been superseded by USB (or wireless) ports for inter-device communications, and by I2C or SPI ports for on-board communications. Nevertheless, because they are simple and low cost, many devices still support them, and many systems still include them. The Blackboard uses one UART port to move data between the Bluetooth/WIFI ESP32 radio and the ZYNQ chip, and a second UART port to move data between the ZYNQ and a USB-connected PC (using “UART over USB”).

In this project, you will write an assembly language program to transfer data between the Blackboard and a PC using one of ZYNQ’s UART ports. On the PC side, you can use a “terminal program" to communicate with the UART channel (a suitable terminal program is built in to Vitis). Terminal programs let you interact in real-time with a PC’s serial/UART port, and they have been a part of PC operating systems from the very beginning. Characters typed into the terminal window are immediately sent out the UART port, and characters received from the UART port are immediately displayed in the same window.

After your basic UART communications software is working, you will write a simple C program to call your assembly-language subroutines. Once you can successfully send and receive data from your C program, you can easily use the UART in future projects to move data between your Blackboard programs and a PC.

Before you begin, you should:

  • Be comfortable working with assembly in Vitis;
  • Have a basic familarity with the C language;
  • Be comfortable using the reference pages to learn how to program/access Blackboard’s peripherals;
  • Be familiar with the Xilinx TRM.

After you’re done, you should:

  • Understand how UARTs work;
  • Know how to configure ZYNQ’s UARTs;
  • Be able to move data to an from the PC using a UART.

Background

Almost all microcontrollers include at least one UART controller, and many of them include several (ZYNQ includes two). ZYNQ’s UART1 port is connected to Blackboard’s main USB port (the same port used for programming the board) using an FTDI USB controller chip. The FTDI chip (and its PC-side driver) create a UART port between the ARM and the PC that operates over USB. On the PC side, the UART will appear as a COM port, and a terminal program can connect to the COM port to send and receive data over the UART. The topic documents contain more information.

Requirements

1. Send a character from the Blackboard to a PC

Write a function that configures the Zynq’s UART 1 module for 115200 baud, 8 data bits, one stop bit, and no parity (refer to the reference manual page to learn which registers must be programmed). After configuring the UART, send a character to your PC by writing an ASCII character to the UART data FIFO. After sending the character, enter an endless loop. When you run the program, the sent character should appear on the Vitis terminal.

To send the character, write a function that takes in a one-byte ASCII character as a parameter. Make sure you check the UART FIFO transmit status before placing the data in the FIFO, and wait for it to be ‘not full’ before writing to the UART FIFO.

Here is an example program structure:

main:
	bl configure_uart1
	
	mov r0,#70	@copy ascii 'F' to r0
	bl uart1_send_char	@send the data in r0 over uart

	b .	@endless loop

2. Receive a character over UART

Write an assembly subroutine to read a character from the UART port. Once called, the subroutine can check the FIFO status, and wait until there is at least one entry in the receive FIFO. When data is available, read it, and pass it back to the calling program.

Write a program that uses your read subroutine to receive a character from the UART port. If an ‘a’ is received, toggle LED0 to its opposite state. If a ‘b’ is received, toggle LED1, if a ‘c’, toggle LED2, and if a ‘d’ toggle LED3. Ignore any other characters. When an LEDs state is changed, send the number of the LED that was toggled to the PC’s terminal.

3. Use null-terminated strings to send a sequence of characters

Write a program capable of transmitting null-terminated strings (also known as c strings). A null-terminated string is simply a character string with the last character being zero.

Using your name, create a string of characters that your program can transmit. Find the ASCII values for the letters in your name, create a data section in your program, and add the ASCII values to the data section as a sequence of consecutive bytes (be sure to include an all-zero byte at the end). Add a label to the start of the string that your program can access. See the topic documents for more information on data segements.

Write a function to send your name string from the Blackboard to the PC. The function should take the first address of the string as a parameter, and then send the characters in the data segment string over the UART, byte-by-byte, until the null character is encountered (the zero byte). Make sure you use byte-wise access instructions (ldrb/strb) to move the string data, and increment your addresses by 1 (byte alignment) rather than 4 (word alignment).

Call this function from a program that sends the string once and then enters an endless loop.

4. Receive strings over UART

Make a subroutine that receives a string from the UART port, and saves it as a null-terminated string in memory. The subroutine should receive a parameter that defines the address where the string should be saved, and another parameter to define the maximum permitted length of the string. The function should read UART characters until a newline character is received, or the maximum string length is exceeded. After the string is read, insert a null character (zero value) byte at the end, even if the maximum length for the string has been reached.

In your main source file, use the .comm directive to define a 128-byte data section to store the string (see the topic documents for an example of how to use .comm directive).

Use the receive function to read a string from the UART, and then send the received string back to the terminal using the send function you wrote earlier. When your program runs, the received characters should not be echoed back to the terminal until you press enter.

5. Write a main program using the C programming language

This requirement uses the C programming language – you may want to read through the C primer topic document before continuing (the primer presents how to deal with memory-mapped I/O in C, as well as other techniques that might be useful).

Write a main program in C which functions the same as the program in requirement 4. You can call your assembly subroutines in C code, but you must declare them as ‘external’.

The following code block gives an example of declaring an external function and calling it within main. It also declares a global array for the string data, this is allocated globally, similar to the array allocated using .comm in requirement 4. For the requirement a string array can be made global or it can be placed in the scope of the main function.


//declare function get_str as external
extern get_str(char dat[]);

char str_data[128];

int main(void)
{
	//get string and put in array
	get_str(str_data);

	//infinite loop
	for(;;);
}

Calling Assembly Functions from C

Be careful when calling assembly functions from C. How you declare the external function will decide how the compiler will pass arguments to it. The first 4 arguments will be passed in registers and functions using more than 4 variables will use the stack to pass parameters.

Look at your assembly function then decide how to write the C extern declaration of it.

6. Write C Versions of your UART Assembly Subroutines

Create equivalent C functions for UART routines you wrote in the first 3 requirements. You need to write C functions that can do the following:

  • configure the UART module;
  • Send and receive single characters
  • Send a C string;
  • Receive a line over UART.

Use separate header and source files for your UART functions. In general, creating header/source pairs for each peripheral results in well-defined interfaces that are clear, organized and reusable. Example prototypes for C functions are given below.

//configures UART1 for 115200 baud, 8-data bits, no parity, 1 stop bit.
void configure_uart1();

//returns next received character from uart.
//Blocks until character availible.
char uart1_getchar();

//Sends 'data' over uart.
//Blocks until data is placed in transmit FIFO.
void uart1_sendchar(char data);

//sends the null-terminated string in 'buffer' through uart1
void uart1_sendstr(char buffer[]);

/***
* Receives characters from uart and copies into 'buffer'.
* Returns when a newline is encountered or the max
* number of characters, 'max', have been received. 
* Newline is not copied and string is always 
* terminated with a null character.
* Function returns the number of characters copied
***/
int uart1_getln(char buffer[], int max);

After you’ve written the functions, test them by placing them in your main function from Requirement 5.

Challenges

1. Write a function that can send integer values via the UART

Write a function that takes a decimal integer as a parameter, and sends the number over the UART port (send the characters representing the number, not the actual value). You can also make a version that sends hex numbers. Converting integers to a string of hex characters might be easier. Why? How do computers represent integers?

2. Read XADC voltage and display the result on the terminal

Read data from ZYNQs analog-to-digtal converter (called the XADC), convert the data so it appears as a 4-digit value between 0.000 and 1.000 volts, and send the value to the PC over the UART port.

To do this, you will need to read the Xilinx TRM to learn where the XADC regsiters are, and also how the data is represented in the XADC regsiters. You will need to convert the “raw” XADC value to an integer, locate the decimal point correctly, build a string, and then send the string to the PC. Note the XADC input is connected to the large potentiometer on the Blackboard. If your program is working correctly, you should be able to move the pot, and then read a different number from the XADC.

Note that since little infomation is provided to help you complete this challenge, it is weighted more heavily than the previous challenges.