Discovery: UART Input
Welcome back. Previously we were talking about what UARTs are, how they work,and how to set them up in Cube with the little piece of code that you need to go between the C printing library and the UART output HAL subroutine calls.
This time we are going to investigate the input functions of UARTs.
Getting Into It
In a recent blog post, Elecia White presented some command console code that we can all use. In her function called ConsoleIoReceive she uses the command:
ch = getchar();
to retrieve a single character from the serial port. A bunch of characters are bundled together to make a command. The program then figures out what command was entered and runs a piece of code in response. But how did we get the characters into the processor to form the command? They came in one of the serial communication ports (UART), and was redirected to the getchar routine through the I/O library calls.
With input from the UART, we have the same issue as we did with printf. Since there is more than one UART on the processor, we have to route the library calls to our choice of ports. We have to add a helper function to get our data from the device registers and send it to getchar.
If you are using the AC6/SW4STM32/openSTM32 toolset (wow I wish they would just pick one name) with GCC, getchar uses a I/O library helper function called _read. The library code doesn’t know anything about your system, so it uses _read as the interface function to your hardware. By default _read doesn’t do anything, you need to fill it in. The _read function is tasked with acquiring the characters from the UART, using whatever means you wish, and return them so they can be presented by getchar.
Instead of digging around in the device registers to get a character from the UART, we will use the HAL_UART_Receive function. This function checks the UART status register to see if any characters have been received and, one by one, retrieves however many you ask for. Since computers are fast compared to humans, there is a high probability that there won’t be anything to retrieve, so HAL_UART_Receive provides a parameter where we can specify the number of milliseconds to hang around and check for a character. If all of our data comes in, the function returns a code of HAL_OK, but if too few come in we get a return code of HAL_TIMEOUT.
To show you how this works, start a new project in Cube and put this code in main.c:
/* USER CODE BEGIN 1 */
char ch;
/* USER CODE END 1 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
ch = getchar();
printf("%c", ch);
}
/* USER CODE END 3 */
This is a simple, infinite loop, grabbing a single character and printing it back out. This is also known as an echo program, since it just echoes back what you type.
Get that code running and convince yourself that it doesn’t work.
Making It Work
Next, we need to put in our implementation of GCC’s helper routine _read which connects getchar() to HAL_UART_Receive.
The _read function prototype looks like:
int _read(int file, char *result, size_t len);
Some notes on the prototype:
The first parameter is a file number that we will ignore for now. In the future, you could use this parameter to decide which port to read from.
The second parameter is the memory location of where our data should be placed.
The third parameter is the number of characters that we should read.
The function returns; the number of bytes read, or 0 when we are told to read 0 bytes, or -1 on error.
From my experiments, the library code really wants _read to wait (block) until all of the requested data is retrieved, not just return after some of it comes in.
The HAL_UART_Receive routine takes four parameters: the control structure for UART3 (which is defined in usart.h), the memory location of where the data will be returned, the number of characters to read, and a timeout time in milliseconds.
If we specify a timeout of HAL_MAX_DELAY then HAL_UART_Receive will block until it receives all of the requested data.
So, now that you understand all that, paste this code into the USER CODE 0 area of main.c, so between:
/* USER CODE BEGIN 0 */
and
/* USER CODE END 0 */
You’ll insert:
int _read(int file, char *result, size_t len) {
HAL_StatusTypeDef status;
int retcode = 0;
if (len != 0) {
status = HAL_UART_Receive( &huart3, (uint8_t *) result, len, HAL_MAX_DELAY);
if (status == HAL_OK) {
retcode = len;
} else {
retcode = -1;
}
}
return( retcode);
}
Next, we will need the output helper routine from last time. You can put it after _read.
int _write(int file, char *outgoing, int len) {
HAL_UART_Transmit(&huart3, (uint8_t *) outgoing, len, 100);
return len;
}
IAR and Keil users will be given a similar routine that need to be altered to suit. It should be called fgetc. I don’t have an example for you, perhaps an IAR user could post a working version in the comments.
Hold On! It STILL Doesn’t Work!
When you run your program, you will probably get nothing. Why? The compiler and IO library create a 1024 byte buffer for you. This is a holdover from the UNIX heritage of the library code. The read routine was used to read UARTs as well as disk files. You really don’t want to read a huge file one byte at a time, it would be slow, so the library code would read a 1K block into memory and feed it back to you in the size you want.
You can typey-type for a while and it should echo back eventually. Or if you were to jam a pencil into your keyboard, that works too. But that’s not what we want. Let’s put in an extra command and turn off the buffering.
In main.c, before your while(1) loop put in this command:
setvbuf(stdin, NULL, _IONBF, 0);
This magic (but standard C) code turns off the 1K buffer. Now we can type away, and the characters get echoed back to us immediately.
Go on, try it. It really should work this time. Well, it should work if you type at it.
STRESS!
The first one or two pastes may work properly, but after a short time your program gets stuck. You can hit the black reset button to try again. What’s going on now?
When you type on a keyboard you get, at most, 10 characters per second. That’s incredibly slow. But when you paste into your terminal program window, the characters get transmitted at about 11,520 per second (given a baud rate of 115,200 bits/s).
HAL_UART_Receive picks the characters off one at a time by checking the data ready flag repeatedly, and when the flag indicated that a character has been received, the code retrieves the character, checks to see if a timeout has happened, and starts again. If there is a character in the receive data register that hasn’t been picked up (because we’re busy checking for timeouts) and another comes in, the UART gets an overrun error and shuts down. ARRRGGG!
If you go back into Cube and switch to the external crystal to crank up the processor speed to 168MHz, the response is much better, but there are still characters getting dropped.
This is the great failing of using polling, it only works well if the data is coming in slowly. Our program really cannot handle power typing because the UARTs on our processor only have a single character buffer.
Relief
If you are doing something slightly more aggressive, like receiving data from a GPS (or some form of computer to computer communication) you can run into overrun problems. The better way to handle the UART would be to use interrupts.
Interrupt UART support is beyond what I’m going to cover this time. However, Cube and the HAL support interrupts: you would use HAL_UART_Receive_IT to set up the processor to receive your data, and your interrupt routine gets called when all of your data has been received. Nice and clean. Not even too tough if you want to give it a shot.
In the meantime your program does not have to poll and wait to see if a character comes in. The timeout processing and retrying code can go away.
Unfortunately, your system can still get swamped by the incoming interrupts. Your processor still has to handle 11,520 interrupts per second. If your incoming data packet is small, the processor could probably keep up. If you have large amounts of data coming in, very quickly, this can overload the processor to the point that it cannot keep up.
When interrupts can’t keep up or your system is really busy, we have to bring out the big tools. Next time, I’ll introduce you to the sledge hammer known as Direct Memory Access or DMA.
This post is part of a series. Please see the other posts here.
Music to program by - Test Shot Starfish. This is the music that SpaceX plays during their rocket launch webcasts. You can find their music at https://soundcloud.com/testshotstarfish.