Universal Synchronous/Asynchronous Receiver Transmitter
The serial communication in asynchronous mode is one of the simplest and most used method to exchange data between microcontroller and other devices. Such kind of communication can be achieved through a Universal Synchronous/Asynchronous Receiver Transmitter (or USART) as well as UART peripheral which actually is a subset of USART. Each STM32 microcontroller is equipped with multiple instances of these peripherals (from 2 up to 8) depending on the microcontroller model.
In this article we are going to take an overview of serial communication protocols and peripherals putting some focus on UART and how to use it with the Serial Driver of ChibiOS/HAL. This driver offers an easy way to use USART providing buffer mechanisms as I/O queue and capability to print formatted strings.
Since the outset of computer science, exchange data between computers has always been a need. In the beginning all the communication were Parallel.
In a parallel communication each bit is transmitter over a dedicated line: this means that such kind of communication requires a line per each bit plus a synchronization line to carry out the operation. Of course we are intending that transmitter and receiver are sharing the same reference ground.
Let us consider an 8-bit parallel communication where data is sampled on positive edge. In such kind of communication the transmitter properly changes the status of the data lines (D0 to D7) and toggles the synchronization line (TRG). On positive edge of TRG, the receiver samples data lines and operation completes.
The parallel communication is reliable and fast but quite unpractical as the number of wires required to carry out the transmission is almost proportional to the word size.
Nowadays, almost all communications are Serial. In a serial communication bits are sequentially transmitted over a single line which usually is know as BUS. The idea itself is quite simple but such kind of communication requires a more complex synchronisation between communication parties.
From synchronisation point of view, serial protocols can be splitted in two types:
- Synchronous Serial, whereas a clock signal is generated by one of the endpoint’s interface and provided to the others through a specific clock line. The communication party which generates the clock is named Master while other Slaves. Example of such kind of communication are SPI, I2C or USB.
- Asynchronous Serial, whereas there is not a common clock signal, but the synchronisation is performed sending additional bits over the data line (like start and stop condition), and baud-rate is known to all the parties. Example of such kind of communication is Asynchronous RS-232 which can be implemented through the UART.
From the communication point of view, protocols can be splitted in three types:
- Simplex Communication, whereas the communication is one way like in figure 2.
- Half-duplex Communication, whereas the communication is bidirectional on a single wire. In this case it is not possible to send and receive data at the same moment. Example of such kind of communication are I2C as well as three-wires SPI.
- Full-duplex Communication, whereas the communication is bidirectional on two separate wires. In this case it is possible to exchange data in the two direction at the same moment. Example of such kind of communication are SPI and UART.
Even if Synchronous buses can reach higher baud-rate in comparison to Asynchronous one, they requires and additional line for clock and the whole communication is strictly related to to reliability of that signal. Clock lines (especially at high frequency) can be affected by disturbances as line load effect which depends also by bus length.
Asynchronous protocols like the RS-232 are still widely used because of their simplicity and because they actually requires just a couple o wire (RX and TX) plus reference ground to put two endpoint in touch communicating over relatively long distances.
In this article we will give a particular emphasis to Serial communication and in particular to the Recommended Standard 232 (or RS-232). This standard has been introduced in 1960 and formally defines connection between a Data Terminal Equipment (or DTE) and a Data Communication Equipment (DCE). Such standard have been used for a long time to connect PC peripherals (like modems, printers and mice) using computer serial ports. It has been gradually replaced by more functional Universal Serial Bus and TCP/IP standards but it remains still largely used in embedded system where is usually implemented through the USART peripheral.
In the beginning the DTEs were electromechanical teletypewrites and DCEs were usually modems. The standard was based on the idea of transmitting characters and this explain why each RS-232 transfer operation has a data size 8-bit.
Bits are encoded as Bipolar Non Return to Zero Level also known as Bipolar NRZL. In this binary code ‘ones‘ are represented by the high logic level (VDD) and ‘zeros‘ by low logic level (-VDD). In NRZ, idle condition is usually associated to the high logic level and logic levels are bipolar (+/-VDD with VDD from 3V to 25V) and this means that signal as not to return to zero before a new bit transmission. The actual VDD value depends on required disturb immunity. In case of PC COM ports usually VDD is 9 or 5V but circuitry is able to operate up to 25V as usually this voltage have been adopted in noisy environment like industry.
The RS-232 protocol can be implemented through the USART peripheral but microcontroller are not able to manage bipolar signals. Instead, signals generated by STM32′ USART (but more in general from microcontrollers) are encoded as Unipolar NRZL: ‘ones‘ are represented by a positive voltage (VDD) while ‘zeros‘ are represented by reference ground voltage (GND). This is why Logic-Level Shifter like the ST3232 or the MAX3232 is required connecting a microcontroller to a PC COM Port through USART.
As the communication is asynchronous, data line has to implement a synchronization mechanism: each data frame begins with a Start Bit which actually is a transition from high logical state to low logical state. This allows sender and receiver to synchronize each other (assuming the baud-rate and data frame format known). The length of start bit is perfectly known and is equal to the time required to transmit one bit at give baud rate. Each data frame ends with a Stop Bit which is a transition low to high. The duration of Stop Bit is configurable as 0.5, 1, 1.5 or 2 bit times: this because the stop bit actually can be intended as an idle time to allow the entire system to remain in sync.
As said the synchronization can be achieved because baud-rate is know by both receiver and transmitter. The RS-232 baud-rate is based on multiples of the rates for electromechanical teleprinters. Bit rates commonly supported include 75, 110, 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600 and 115200 bit/s. A funny note is that some specific Crystal oscillator with a frequency of 1.843200 MHz have been sold specifically for this purpose for a long time.
To reduce problems related to noise disturbance or clock tolerance there is the chance to add an additional bit to payload transmission to ensure that the total number of 1-bits in the string is always even or odd: this is known as parity bit or check bit, and is the simplest form of error detecting code. Consequently there are two variants of parity bits: even parity bit and odd parity bit.
To conclude RS-232 allows also flow control using up to six extra signal to implement hardware handshake. However, manufacturers have over the years built many devices that implemented non-standard variations on the standard, for example, printers that use DTR as flow control.
From RS-232 to USB CDC
Nowadays is almost rare to see a COM Port on the rear of a PC case: this because the USB has proven to be perfectly able to supersede the RS-232. To do that the USB provides also a specific profile known as Communication Device Class (or CDC). In USB CDC RX and TX streams are in-capsuled in the USB protocol using a couple of USB Endpoints and the operating system on the USB side makes the USB device appear as a traditional RS-232 port usually named as Virtual COM Port.
Such device class is implemented also by the ST-Link V2-1 debugger available on many STM32 development kit. Connecting this debugger on Windows the CDC will appear in device manager as ST-Link Virtual COM port . From the device side the ST-Link is physically connected to a couple of pin of the STM32 which can be rerouted as UART TX and UART RX via GPIO Alternate Functions. The information about this connection is on the board schematic and thus reported in the board user manual. As example looking at the STM32 Nucleo-64 User Manual you will find out that ST-Link is connected to USART2 through the pin PA2 (Arduino connector D1) and PA3 (Arduino connector D0).
As we will see in a later article in ChibiOS there are also some examples to implement a CDC inside our code and use the STM32 USB as a Virtual COM Port. IF you are in that annoying case where your cannot use the ST-Link and your device does not have the USB peripheral you may think to use a FTDI chip which is easy to find on the internet in form of break-out boards.
STM32 is equipped both with UART and USART: the last one is actually a super-set of the first one which is provided with a clock line and is able to perform full-duplex and half-duplex synchronous communication. In such case the USART actually acts like an SPI. Anyway, here we will focus on asynchronous communication and we will use always USART like an UART.
The STM32’s UART is designed to implement many serial protocols: as example iit implements two different kind of binary encoding which are the Unipolar NRZL and the Manchester Code. In the first case a ‘1’ is represented by a VDD and a ‘0’ by a GND, in the second case a data signal rising edge represent a ‘1’ while a falling edge a ‘0’.
The Manchester code is designed to avoid long permanency of a certain logical state: indeed in such code a level state can persist half or one bit times in comparison to NZRL. This is very useful in those application where communication channel reliability is non-guaranteed like the IrDA.
In serial protocol like the Recommended Standard 485 there is room for an architecture single master multiple slaves: in such network, each slave as its own address and master can send two kind of data: slave addresses and data. To distinguish between those two type of data protocol accept a 9-th bit. Thus we have that
- addresses have the 9th bit as ‘1’;
- data have the 9th bit as ‘0’.
To implement also such kind of communication the STM32 UART allows also to choose data size choosing between 8-bit and 9-bit payload length.
One of the problem of asynchronous communication is a lack of clock accuracy: as timing is generated internally and synchronisation happens through the start bit a misalignment is possible. This condition is more critical the higher is baud-rate. To mitigate this effect, UART peripherals usually oversample data signal. STM32’s UART is able to perform configurable 8/16 bit oversampling: this means that each bit is sampled 8/16 times to reduce error and mitigate noise effects.
The baud-rate generator is fractional and it is able to generate any transmit and receive baud-rate starting from the ARM Peripheral Bus (or APB) which supplies clock to every STM32’s peripheral.
Each STM32’s UART peripherals comes with some dedicated I/O which can be rerouted using GPIO Alternate Function. Those IO are:
- the transmission line (UART_TX)
- the receiver line (UART_RX)
- Clear to Send (UART_CTS)
- Request to Send (UART_RTS)
It is possible to control the serial data flow between two devices using two additional I/O which are the Clear To Send (or CTS) and the Request To Send (or RTS): in such scenario receiver’s RTS is connected to the transmitter CTS; when the receiver lowers the line the transmitter sends a byte.
The ChibiOS Serial Driver
ChibiOS/HAL offers a quick and easy way to use the UART through a software driver known as Serial Driver (often shorten as SD).
The Serial Driver buffers input and output streams using I/O Queues and this offers a main advantage: the user application has not to continuously serve Interrupt Requests because the driver does this internally on data exchange storing data in these buffers. This mechanism allows to easily implement a producer-consumer pattern without any effort from the application side: in this scenario the driver fills a buffer and the user application consumes data. Such pattern allows to not lose any byte if the application consumes data faster than production rate and this condition is easily achievable considering serial baud rates and typical STM32 core speed.
As side note, ChibiOS/HAL offers another driver to deal with UART which is the UART Driver: this driver exposes IRQ to the application thus the user has to properly fill some driver callback to handle character reception, transmission and eventually operation errors. This driver will be presented in a later moment and we will focus on Serial Driver only.
Each API of the Serial Driver starts with the prefix “sd”. Function names are camel-case, pre-processor constants uppercase and variables lowercase.
Driver enabling and peripheral allocation
To use the SD it shall be enabled in our project halconf.h file and we should assign at least a peripheral to it acting on mcuconf.h. We have already introduced this concept but we will recall this quickly here (if you want to read more maybe you should give a look here).
Each ChibiOS driver can be enabled or disabled in the halconf.h file and each project has is own HAL configuration header. In the original demos all unused drivers are usually disabled. If you have launched the default demo you should have noticed that we use Serial Driver to print test suite results, and us most likely your project will be a copy of the default one most-likely you will find the SD already enabled.
To use the driver we have then to assign a USART/UART peripheral to the Serial Driver. This can be done acting on mcuconf.h. The next code has been copied from the MCU configuration header of the original demo for STM32 Nucleo-64 F401RE. In this case the USART2 is assigned to SD.
Note that is not possible to assign the same peripheral to different driver because this would generate conflict on IRQ management and the project would not compile.
As instance, it is not possible to assign USART 2 both to the Serial Driver and UART Driver and trying to do will result in a compile error. In figure 4 the problem windows reports the compile error derived by such operation. The error is due to the tentative to assign the same IRQ line (VectorD8) to two different drivers.
The Serial Driver object
ChibiOS/HAL is designed following an object oriented approach and each driver is represented by a structure. This implementation allows multiple instances of the same driver and so is for the SD. There is a Serial Driver instance for each available USART but each instance becomes available only when a peripheral is assigned to the driver.
As example assigning the STM32 USART1 to serial driver the SD1 object would become available. Note that, the driver implementation inumbering is aligned to peripheral numbering:USART peripheral 1 is associated to SD1 , USART peripheral 2 to SD2, UART peripheral 3 to SD3 and so on.
Each driver in ChibiOS\HAL implements a Finite State Machine and so it is for the Serial Driver. The current state of the driver is stored inside the object in a field named state (you can access it using structure syntax as example SD2.state). The following image has been grabbed from the ChibiOS\HAL documentation and illustrates the finite state machine of the SD
Serial driver initialization
Comparing different ChibiOS\HAL drivers we will find some similitudes which will help us getting familiar with ChibiOS approach. All the drivers have an initialization function which for the Serial Driver is
This function is automatically called on HAL initialization if the driver is enabled in the HAL configuration file. The HAL initialization happens in the main of our application on halInit call. We have noticed a similar approach in the PAL driver and actually this approach is adopted in every ChibiOS/HAL driver. The sdInit function initializes objects and variables moving the state of the driver from SD_UNINIT to SD_STOP.
Note that sdInit only initializes variables and objects related to the Serial Driver. User shall not confuse initialization with configuration.
If a ChibiOS driver is enabled in the HAL configuration file, it is automatically initialized on HAL initialization. Initialization is related to variable initialization more than on hardware configuration.
Configuring serial driver
Before to use it, the serial driver shall be properly initialized and configured. This operation is carried out by another function: the start.
This function shall be called at least once by the user application before to start the serial communion. Its purpose is to configure the peripheral and this involves setup of all that configurability we have introduced: baud rate, parity bit, stop bit length, encoding and so on.
In ChibiOS/HAL every driver except PAL shall be started before to be used.
The sdStart function receives two parameters which are a pointer to the serial driver object we want to start (e.g. &SD1, &SD2 or whatever is the USART we are going to use) and a pointer to a structure which represent the related configuration. This structure contains all the dependencies which are strictly related to the underlying hardware. This means that moving from a STM32 family to another which has a different underlying hardware we most likely have to apply some changes to these configuration structures.
Starting a driver we need a pointer to it and a pointer to its configuration structure. This structure contains all the HW dependencies and has to been reviewed if we port our application on a different microcontroller.
The start function usually enables peripheral clock. In certain application (especially those addressed to low power) it is undesidered to keep a peripheral clocked when it is not used. Because of that there is another function we could use to stop the driver and stop the peripheral clock: the stop.
This function can be called by the user application when the peripheral is unnecessary. It is almost intuitive that after a stop we need a new start to be able to use again the driver.
It is possible to stop a driver to reduce hardware power consumption when peripheral is used for a small percentage of execution time. After being stopped a the driver shall be re-started to be used again.
A most common paradigm is to start the driver when needed and stop it when the operation has been completed.
This offers advantage especially if peripheral is actually used in a small slice of the whole time amount of execution time.
A similar approach is used when we need to change driver configuration on-the-fly. In such scenario we can start peripheral multiple time with different configuration. Note that stop is not required rather is discouraged as in case of subsequent start operation certain operation are skipped.
It is possible to start driver more than once with different configuration in case we need to change driver configuration on-the-fly.
The configuration structure
The configuration structure is composed by two separated parts: the first part remains unchanged across all kind of hardware, the second is strictly hardware dependent
Considering SerialConfig, speed represent the independent part and is an the baud rate expressed as unsigned integer. The other three values represent the value which will be used to stored in the Control Register 1, Control Register 2 and Control Register 3 of USART driver on start. The meaning of each bit of these register is described inside the reference manual of the STM32 currently in use.
Dealing with configuration there are some hints that can help you to figure out how to correctly compose them:
- take a look to demos under the testhal folder and testex folders: in these demos you can find some precomposed configuration that you can copy and use in your application. As the configuration depends on hardware be sure to pick a demo for the subfamily you are currently using.
- those fields which are related to hardware are described in the reference manual, use it to spot the meaning of each bit.
- remember that the start function usually does some internal obvious modification to register values passed through the configuration. As instance bit 13 of CR1 of UART for STM32F401RE is an enable and shall be set as 1 to enable the peripheral: you can take for granted that sdStart force this bit to 1.
- There are some simple techniques to change certain bit of the register leaving others unchanged. Such operation takes the name of bit-masking and the adopted paradigm to deal with registers read-modify-write. This is a knowledge you really should do have. If don’t I highly suggest to read this article (even at later moment).
- You do not have to define register bitmasks as they are already defined and available in CMSIS header files. You can find these files under the folder chibios182\os\common\ext\ST. As example the following snippet of code has been extracted from the filechibios182\os\common\ext\ST\STM32F4xx\stm32f401xe.h and represent the bitmasks related to USART Control Registers
Starting Serial Driver with default configuration
In the default demo, we already used the Serial Driver 2 to print test suite results. Looking back a that article we can notice that we start the driver with no configuration.
This is a peculiarity of Serial Driver which, when receives a NULL configuration, it starts with a default configuration defined for STM32 as
Such configuration means no parity, 1-bit stop, no hardware flow control, 8-bit data size and baud rate equal to SERIAL_DEFAULT_BITRATE which is defined inside the project HAL configuration file as 38400bps.
Such kind of configuration matches the default of Eclipse Terminal window.
Put, Get, Read and Write
Operation related to Serial Driver are those easily associable to characters handling:
In these functions sdp is the pointer to the Serial Driver we are operating on. Remember that to use this functions the driver shall be started. Get and put works on a single character, read an write on strings but expect you specify the size of buffer to read/write. As example
The I/O functions just presented interact with the I/O queue as schematized in the next diagram
Queue size can be configured in your project’s HAL Configuration file and the default value is 16 bytes or 16 characters. In asynchronous mode the input and the output are completely independent.
For instance, let us consider the RX side and the TX side sepately focusing on the RX side only. The paradigm of this implementation is the Producer-Consumer. STM32 USART RX fills the Input Queue while the user application consumes data emptying the buffer. If data is not consumed at a faster rate than production the queue will be filled and characters arriving from the RX will be lost.
With a proper application design, data is consumed faster than production rate and there is no data loss. This condition can be achieved increasing priority of thread which consumes the data or reducing sleeps in its loop or reducing the UART baud rate. Anyway, if such condition is matched, it could happen that a thread calls sdGet or sdRead when the input queue is empty or has less elements than the quantity we want to read. In such case the thread which has called the function is suspended indefinitely until new data coming from the RX line fills the input queue. Function like the sdPut, sdGet, sdWrite and sdRead are thus defined as blocking functions.
A blocking function blocks the execution of the calling thread until the operation is completed. Such functions are executed in a smart way: if to complete the operation the function has to wait an asynchronous event like, as example, the reception of a new character from the USART peripheral, the function suspends the calling thread waiting for an interrupt from USART. This means, if we are trying to read a character using sdGet when the input queue is empty, the thread is suspended and will be resumed when the input queue becomes not empty. In this way, RTOS executes other threads while this one is suspended. Such functions are also know as synchronous functions as the calling thread remains in sync with the operation
A non-blocking function launches the operation and returns to the calling thread which continues to be executed. Such operation is executed in background, usually in hardware, launching a interrupt on operation complete. Such IRQ usually is exposed by driver API in form of a callback. Such function are also know as asynchronous functions.
Put, Get, Read and Write with timeout
In certain application blocking function are not suitable. Lets consider for instance a condition in which RX line is broken. If we try to do a sdGet our thread will stuck there indefinitely. To manage such kind of scenario the same functions are available with timeout.
Timeout shall be expressed as system ticks. As this is not immediate there are some macros which converts milliseconds to systemtick
As example if we want to read 4 bytes with a 50ms timeout we should write
Note also that TIME_IMMEDIATE and TIME_INFINITE are accepted as well. In the first case function will return immediately in second it will behave like functions without timeout.
To use Serial Driver we need to reroute GPIO connections assigning them to UART through PAL driver (take a look to GPIO article if you are not familiar with). As example in case we decide to use SD6 on STM32 Nucleo-64 F401 we need to check on STM32F401xE datasheet which pin can be rerouted on USART6. On the Alternate Function map we can read that PC6 and PC7 can be remapped as USART6_TX and USART6_RX using Alternate Function 8.
Thus we can use this code to change GPIO configuration right before to use SD functions. I use to configure pins right before the first sdStart.
Alternatively it is possible to generate a custom board file which pre-configure those pins on halInit() call.
On STM32 Nucleo-64 boards USART2 is connected to ST-Link Virtual COM port. Note that related pin are already configured in board for this use. Looking at STM32 Nucleo F401RE board.h we can notice that GPIOA setup is:
PA2 and PA3 are the pins actually connected to the ST-Link and are properly configured. Indeed, looking back to previous articles we already used serial in default demo with no call to palSetPadMode.
The Serial Driver as BaseSequentialStream
ChibiOS/HAL offers many abstraction interfaces: one of these could be interesting because it allows to print formatted strings. The interface we are talking about is the BaseSequentialStream.
In object-oriented programming an Interface is just an abstract API which is a behavioural description more than a code implementation. In the case of BaseSequentialStream the specified behaviour is that this interface has 4 methods:
In other words we are stating that a driver which implement this interface shall provide 4 methods (write, read, put and get). We are not quibbling about how those methods will be implemented but we are defining related parameters and return types.
In ChibiOS the main purpose of interfaces is to generalize drivers. In such way it is possible implement methods which works on many different driver.
Serial Driver implements a BaseSequentialStream interface. As we will see later Serial Driver Over USB (also known as USB CDC) implements the same interface as well. It is possible to use BaseSequentialStream functions on both Serial and Serial Over USB driver indistinctly.
Printing formatted strings using chprintf
ChibiOS offers an optional module named streams which offers some feature included the API chprintf: this API is the ChibiOS version of printf and prints formatted strings over BaseSequentialStream like printf does on output stream.The stream module is located at chibios182/os/hal/lib/streams and, as it is considered an optional module, it is not included by default.
To use it we have to editi the makefile and adding the inclusion of the file chibios182/os/hal/lib/streams/streams.mk. This can be do at the section named “Other files (optional)” (see the last line of next codebox).
At this point we can include the chprintf.h in the main file and use it. The function chprintf is defined as
it is completely identical to printf with the difference that the first parameter which receives is a pointer to the BaseSequentialStream. Thus we can pass to the chprintf a pointer to a serial driver as first parameter and print formatted string on it
Using the previous code we we will see the string “Hello World 1st test” with a carriage return and newline printed three times. Note that the first line will return a warning at compile time because &SD2 is actually a pointer to SerialDriver. This warning is easy to be solved with an explicit cast.
- Universal Synchronous/Asynchronous Receiver Transmitter
- Parallel communication
- Serial communication
- STM32 USART
- The ChibiOS Serial Driver
- Driver enabling and peripheral allocation
- The Serial Driver object
- Serial driver initialization
- Configuring serial driver
- Serial operations
- GPIO related configuration
- The Serial Driver as BaseSequentialStream
- Using STM32’s GPIO with ChibiOS’ PAL Driver (previous article)
- Using STM32’s ADC with ChibiOS ADC Driver (next article)