
Using STM32 USART with ChibiOS Serial Driver
Important notice
Please note that the information contained in this article may no longer be accurate or useful due to changes in the ChibiOS codebase and ChibiStudio.
Unfortunately, at the moment there is no direct replacement for this article. However, we are in the process of rewriting the series with updated information, a better writing style, and improved ease of use. The new series will also be completed with a larger set of examples and exercises to help you better understand how to use ChibiOS and ChibiStudio.
We welcome any feedback or suggestions you may have, so please don’t hesitate to get in touch with us.
Thank you for your interest in our content.
Universal Synchronous/Asynchronous Receiver Transmitter
The serial communication in asynchronous mode is one of the simplest and most used methods to exchange data between a 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.
Parallel communication

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 transmitted 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 the transmitter and the receiver are sharing the same reference ground.
Let us consider an 8-bit parallel communication where data is sampled on the 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 impractical as the number of wires required to carry out the transmission is almost proportional to the word size.
Serial communication
Nowadays, almost all communications are Serial. In such kind of communication, the bits are sequentially transmitted over a single line which usually is known as BUS. The idea itself is quite simple but such kind of communication requires a more complex synchronization between communication parties.
From synchronization point of view, serial protocols can be split into 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 part which generates the clock is named Master while other Slaves. Examples of such kind of communication are the SPI, the I2C or the USB.
- Asynchronous Serial, whereas there is not a common clock signal, but the synchronization 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 split into three types:
- Simplex Communication, whereas 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 is the I2C as well as the 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 directions at the same moment. Examples of such kind of communication are the SPI and the UART.
Even if synchronous buses can reach higher baud-rate in comparison to asynchronous one, they require an additional line for clock and the whole communication is strictly related to the reliability of that signal. Clock lines (especially at high frequency) can be affected by disturbances as line load effect which depends also by the bus length.
Asynchronous protocols like the RS-232 are still widely used because of their simplicity and because they actually require just a couple o wire (RX and TX) plus reference ground to put two endpoints in touch communicating over relatively long distances.
The RS-232
In this article, we will give 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 the connection between a Data Terminal Equipment (or DTE) and a Data Communication Equipment (DCE). Such standard has been used for a long time to connect PC to 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 an embedded system where is usually implemented through the USART peripheral.
Communication frame
In the beginning, the DTEs were electromechanical teletypewriters and DCEs were usually modems. The standard was based on the idea of transmitting characters and this explains 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, the 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 the case of PC COM ports, VDD is usually 9V or 5V but the circuitry is able to operate up to 25V as this voltage have been often adopted in a 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, the data line has to implement a synchronization mechanism: each data frame begins with a Start Bit which actually is a transition from a high logical state to a low logical state. This allows sender and receiver to synchronize each other (assuming the baud-rate and data frame format known). The length of the start bit is perfectly known and is equal to the time required to transmit one bit at a given 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 known 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 has 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 the parity bit or the check bit and is the simplest form of error detecting code. Consequently, there are two variants of parity bits: the even and odd parity bit.
To conclude RS-232 allows also flow control using up to six extra signals 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 encapsulated in the USB protocol using a couple of USB Endpoints and the operating system on the USB side makes the USB device appears 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 kits. 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. For 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 you cannot use the ST-Link and your device does not have the USB peripheral you may think to use an FTDI chip which is easy to find on the internet in form of break-out boards.
STM32 USART
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 a UART.
The STM32’s UART is designed to implement many serial protocols: for example, it implements two different kinds 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 is encoded in the raising and falling edges.

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 applications 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 has its own address and master can send two kinds of data: slave addresses and data. To distinguish between those two types of data protocol accept a ninth bit. Thus we have that
- addresses have the 9th bit as ‘1’;
- data have the 9th bit as ‘0’.
To implement such kind of communication, the STM32 UART allows also to select data size choosing between 8-bit and 9-bit payload length.
One of the problems of asynchronous communication is a lack of clock accuracy: as timing is generated internally and synchronization 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 the clock to every STM32’s peripheral.
Each STM32’s UART peripherals come 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 shortened as SD).
The Serial Driver buffers input and output streams using I/O Queues and this offers a big benefit: the user application has not to handle Interrupt Requests because the driver does it internally 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 a 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 at 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 have 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.
/** * @brief Enables the SERIAL subsystem. */ #if !defined(HAL_USE_SERIAL) || defined(__DOXYGEN__) #define HAL_USE_SERIAL TRUE #endif
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.
/* * SERIAL driver system settings. */ #define STM32_SERIAL_USE_USART1 FALSE #define STM32_SERIAL_USE_USART2 TRUE #define STM32_SERIAL_USE_USART6 FALSE #define STM32_SERIAL_USART1_PRIORITY 12 #define STM32_SERIAL_USART2_PRIORITY 12 #define STM32_SERIAL_USART6_PRIORITY 12

Note that is not possible to assign the same peripheral to a different driver because this would generate conflict on IRQ management and the project would not compile.
For 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 window 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.
/** @brief USART1 serial driver identifier.*/ #if STM32_SERIAL_USE_USART1 || defined(__DOXYGEN__) SerialDriver SD1; #endif /** @brief USART2 serial driver identifier.*/ #if STM32_SERIAL_USE_USART2 || defined(__DOXYGEN__) SerialDriver SD2; #endif /** @brief USART3 serial driver identifier.*/ #if STM32_SERIAL_USE_USART3 || defined(__DOXYGEN__) SerialDriver SD3; #endif /** @brief UART4 serial driver identifier.*/ #if STM32_SERIAL_USE_UART4 || defined(__DOXYGEN__) SerialDriver SD4; #endif /** @brief UART5 serial driver identifier.*/ #if STM32_SERIAL_USE_UART5 || defined(__DOXYGEN__) SerialDriver SD5; #endif /** @brief USART6 serial driver identifier.*/ #if STM32_SERIAL_USE_USART6 || defined(__DOXYGEN__) SerialDriver SD6; #endif /** @brief UART7 serial driver identifier.*/ #if STM32_SERIAL_USE_UART7 || defined(__DOXYGEN__) SerialDriver SD7; #endif /** @brief UART8 serial driver identifier.*/ #if STM32_SERIAL_USE_UART8 || defined(__DOXYGEN__) SerialDriver SD8; #endif /** @brief LPUART1 serial driver identifier.*/ #if STM32_SERIAL_USE_LPUART1 || defined(__DOXYGEN__) SerialDriver LPSD1; #endif
As example assigning the STM32 USART 1 to the serial driver, the SD1 object would become available, SD2 assigning USART 2, SD3 assigning UART 3 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 get familiar with ChibiOS approach. All the drivers have an initialization function which for the Serial Driver is
/** * @brief Serial Driver initialization. * @note This function is implicitly invoked by @p halInit(), there is * no need to explicitly initialize the driver. * * @init */ void sdInit(void) { ... }
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.
/** * @brief Configures and starts the driver. * * @param[in] sdp pointer to a @p SerialDriver object * @param[in] config the architecture-dependent serial driver configuration. * If this parameter is set to @p NULL then a default * configuration is used. * * @api */ void sdStart(SerialDriver *sdp, const SerialConfig *config) { ... }
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 features we have introduced: the baud rate, the parity bit, the 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 represents the related configuration. This structure contains all the dependencies which are strictly related to the underlying hardware. This means that moving from an STM32 family to another which has 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 the peripheral clock. In certain applications (especially those addressed to low power) it is undesired 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.
/** * @brief Stops the driver. * @details Any thread waiting on the driver's queues will be awakened with * the message @p MSG_RESET. * * @param[in] sdp pointer to a @p SerialDriver object * * @api */ void sdStop(SerialDriver *sdp) { ... }
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.
/* Starting Serial Driver 2 with my configuration. */ sdStart(&SD2, &my_sd_configuration); /* Doing some operation on Serial Driver 2. */ /* Stopping. */ sdStop(&SD2); ... /* Starting again. */ sdStart(&SD2, &my_sd_configuration); /* Carrying out other operation. */ /* Stopping. */ sdStop(&SD2);
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 a scenario, we can start peripheral multiple time with a different configuration. Note that stop is not required, rather, it is discouraged because in case of multiple subsequent sdStart() calls certain internal operations would be 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
/** * @brief STM32 Serial Driver configuration structure. * @details An instance of this structure must be passed to @p sdStart() * in order to configure and start a serial driver operations. * @note This structure content is architecture dependent, each driver * implementation defines its own version and the custom static * initializers. */ typedef struct { /** * @brief Bit rate. */ uint32_t speed; /* End of the mandatory fields.*/ /** * @brief Initialization value for the CR1 register. */ uint32_t cr1; /** * @brief Initialization value for the CR2 register. */ uint32_t cr2; /** * @brief Initialization value for the CR3 register. */ uint32_t cr3; } SerialConfig;
Considering SerialConfig, speed represents the independent part and is the baud rate expressed as an unsigned integer. The other three values represent the value which will be used to initialize 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 a 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 you 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. For example, the following snippet of code has been extracted from the file chibios182\os\common\ext\ST\STM32F4xx\stm32f401xe.h and represent the bitmasks related to USART Control Registers
/****************** Bit definition for USART_CR1 register *******************/ #define USART_CR1_SBK_Pos (0U) #define USART_CR1_SBK_Msk (0x1U << USART_CR1_SBK_Pos) #define USART_CR1_SBK USART_CR1_SBK_Msk #define USART_CR1_RWU_Pos (1U) #define USART_CR1_RWU_Msk (0x1U << USART_CR1_RWU_Pos) #define USART_CR1_RWU USART_CR1_RWU_Msk #define USART_CR1_RE_Pos (2U) #define USART_CR1_RE_Msk (0x1U << USART_CR1_RE_Pos) #define USART_CR1_RE USART_CR1_RE_Msk #define USART_CR1_TE_Pos (3U) #define USART_CR1_TE_Msk (0x1U << USART_CR1_TE_Pos) #define USART_CR1_TE USART_CR1_TE_Msk #define USART_CR1_IDLEIE_Pos (4U) #define USART_CR1_IDLEIE_Msk (0x1U << USART_CR1_IDLEIE_Pos) #define USART_CR1_IDLEIE USART_CR1_IDLEIE_Msk #define USART_CR1_RXNEIE_Pos (5U) #define USART_CR1_RXNEIE_Msk (0x1U << USART_CR1_RXNEIE_Pos) #define USART_CR1_RXNEIE USART_CR1_RXNEIE_Msk #define USART_CR1_TCIE_Pos (6U) #define USART_CR1_TCIE_Msk (0x1U << USART_CR1_TCIE_Pos) #define USART_CR1_TCIE USART_CR1_TCIE_Msk #define USART_CR1_TXEIE_Pos (7U) #define USART_CR1_TXEIE_Msk (0x1U << USART_CR1_TXEIE_Pos) #define USART_CR1_TXEIE USART_CR1_TXEIE_Msk #define USART_CR1_PEIE_Pos (8U) #define USART_CR1_PEIE_Msk (0x1U << USART_CR1_PEIE_Pos) #define USART_CR1_PEIE USART_CR1_PEIE_Msk #define USART_CR1_PS_Pos (9U) #define USART_CR1_PS_Msk (0x1U << USART_CR1_PS_Pos) #define USART_CR1_PS USART_CR1_PS_Msk #define USART_CR1_PCE_Pos (10U) #define USART_CR1_PCE_Msk (0x1U << USART_CR1_PCE_Pos) #define USART_CR1_PCE USART_CR1_PCE_Msk #define USART_CR1_WAKE_Pos (11U) #define USART_CR1_WAKE_Msk (0x1U << USART_CR1_WAKE_Pos) #define USART_CR1_WAKE USART_CR1_WAKE_Msk #define USART_CR1_M_Pos (12U) #define USART_CR1_M_Msk (0x1U << USART_CR1_M_Pos) #define USART_CR1_M USART_CR1_M_Msk #define USART_CR1_UE_Pos (13U) #define USART_CR1_UE_Msk (0x1U << USART_CR1_UE_Pos) #define USART_CR1_UE USART_CR1_UE_Msk #define USART_CR1_OVER8_Pos (15U) #define USART_CR1_OVER8_Msk (0x1U << USART_CR1_OVER8_Pos) #define USART_CR1_OVER8 USART_CR1_OVER8_Msk /****************** Bit definition for USART_CR2 register *******************/ #define USART_CR2_ADD_Pos (0U) #define USART_CR2_ADD_Msk (0xFU << USART_CR2_ADD_Pos) #define USART_CR2_ADD USART_CR2_ADD_Msk #define USART_CR2_LBDL_Pos (5U) #define USART_CR2_LBDL_Msk (0x1U << USART_CR2_LBDL_Pos) #define USART_CR2_LBDL USART_CR2_LBDL_Msk #define USART_CR2_LBDIE_Pos (6U) #define USART_CR2_LBDIE_Msk (0x1U << USART_CR2_LBDIE_Pos) #define USART_CR2_LBDIE USART_CR2_LBDIE_Msk #define USART_CR2_LBCL_Pos (8U) #define USART_CR2_LBCL_Msk (0x1U << USART_CR2_LBCL_Pos) #define USART_CR2_LBCL USART_CR2_LBCL_Msk #define USART_CR2_CPHA_Pos (9U) #define USART_CR2_CPHA_Msk (0x1U << USART_CR2_CPHA_Pos) #define USART_CR2_CPHA USART_CR2_CPHA_Msk #define USART_CR2_CPOL_Pos (10U) #define USART_CR2_CPOL_Msk (0x1U << USART_CR2_CPOL_Pos) #define USART_CR2_CPOL USART_CR2_CPOL_Msk #define USART_CR2_CLKEN_Pos (11U) #define USART_CR2_CLKEN_Msk (0x1U << USART_CR2_CLKEN_Pos) #define USART_CR2_CLKEN USART_CR2_CLKEN_Msk #define USART_CR2_STOP_Pos (12U) #define USART_CR2_STOP_Msk (0x3U << USART_CR2_STOP_Pos) #define USART_CR2_STOP USART_CR2_STOP_Msk #define USART_CR2_STOP_0 (0x1U << USART_CR2_STOP_Pos) #define USART_CR2_STOP_1 (0x2U << USART_CR2_STOP_Pos) #define USART_CR2_LINEN_Pos (14U) #define USART_CR2_LINEN_Msk (0x1U << USART_CR2_LINEN_Pos) #define USART_CR2_LINEN USART_CR2_LINEN_Msk /****************** Bit definition for USART_CR3 register *******************/ #define USART_CR3_EIE_Pos (0U) #define USART_CR3_EIE_Msk (0x1U << USART_CR3_EIE_Pos) #define USART_CR3_EIE USART_CR3_EIE_Msk #define USART_CR3_IREN_Pos (1U) #define USART_CR3_IREN_Msk (0x1U << USART_CR3_IREN_Pos) #define USART_CR3_IREN USART_CR3_IREN_Msk #define USART_CR3_IRLP_Pos (2U) #define USART_CR3_IRLP_Msk (0x1U << USART_CR3_IRLP_Pos) #define USART_CR3_IRLP USART_CR3_IRLP_Msk #define USART_CR3_HDSEL_Pos (3U) #define USART_CR3_HDSEL_Msk (0x1U << USART_CR3_HDSEL_Pos) #define USART_CR3_HDSEL USART_CR3_HDSEL_Msk #define USART_CR3_NACK_Pos (4U) #define USART_CR3_NACK_Msk (0x1U << USART_CR3_NACK_Pos) #define USART_CR3_NACK USART_CR3_NACK_Msk #define USART_CR3_SCEN_Pos (5U) #define USART_CR3_SCEN_Msk (0x1U << USART_CR3_SCEN_Pos) #define USART_CR3_SCEN USART_CR3_SCEN_Msk #define USART_CR3_DMAR_Pos (6U) #define USART_CR3_DMAR_Msk (0x1U << USART_CR3_DMAR_Pos) #define USART_CR3_DMAR USART_CR3_DMAR_Msk #define USART_CR3_DMAT_Pos (7U) #define USART_CR3_DMAT_Msk (0x1U << USART_CR3_DMAT_Pos) #define USART_CR3_DMAT USART_CR3_DMAT_Msk #define USART_CR3_RTSE_Pos (8U) #define USART_CR3_RTSE_Msk (0x1U << USART_CR3_RTSE_Pos) #define USART_CR3_RTSE USART_CR3_RTSE_Msk #define USART_CR3_CTSE_Pos (9U) #define USART_CR3_CTSE_Msk (0x1U << USART_CR3_CTSE_Pos) #define USART_CR3_CTSE USART_CR3_CTSE_Msk #define USART_CR3_CTSIE_Pos (10U) #define USART_CR3_CTSIE_Msk (0x1U << USART_CR3_CTSIE_Pos) #define USART_CR3_CTSIE USART_CR3_CTSIE_Msk #define USART_CR3_ONEBIT_Pos (11U) #define USART_CR3_ONEBIT_Msk (0x1U << USART_CR3_ONEBIT_Pos) #define USART_CR3_ONEBIT USART_CR3_ONEBIT_Msk
Starting Serial Driver with default configuration
In the default demo, we already used the Serial Driver 2 to print test suite results. Looking back at that article we can notice that we start the driver with no configuration.
/* * Activates the serial driver 2 using the driver default configuration. */ sdStart(&SD2, NULL);
This is a peculiarity of Serial Driver which, when receives a NULL configuration, it starts with a default configuration defined for STM32 as
/** @brief Driver default configuration.*/ static const SerialConfig default_config = { SERIAL_DEFAULT_BITRATE, 0, USART_CR2_STOP1_BITS, 0 };
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.
/** * @brief Default bit rate. * @details Configuration parameter, this is the baud rate selected for the * default configuration. */ #if !defined(SERIAL_DEFAULT_BITRATE) || defined(__DOXYGEN__) #define SERIAL_DEFAULT_BITRATE 38400 #endif
Such kind of configuration matches the default of Eclipse Terminal window.
Serial operations
Put, Get, Read and Write
Operation related to Serial Driver are those easily associable to characters handling:
sdPut(sdp, token); uint8_t token = sdGet(sdp); sdWrite(sdp, string, size); sdRead(sdp, buffer, size);
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 the buffer to read/write. For example
sdPut(&SD2, (int8_t)'a'); uint8_t token = sdGet(&SD1); sdWrite(&SD6, (int8_t*)"Hohoho\r\n", 8); sdRead(&SD3, buffer, 5);
Note that these functions work with an 8-bit unsigned integer instead of relying on a primitive type like the char. This makes the API compiler independent. Sometimes casts are required to avoid a warning at compile time.
IO Queues
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 separately 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
- reducing the sleeping time in its loop
- 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 fewer 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. A 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 for an asynchronous event like, for 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 known 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 the background, usually in hardware, launching an interrupt on operation complete. Such IRQ usually is exposed by driver API in form of a callback. Such functions are also known as asynchronous functions.
If you are used to JavaScript you can see a non-blocking function like an asynchronous script. So far the Serial Driver does not implement non-blocking function thus we will deepen this concept in a later article.
Put, Get, Read and Write with a timeout
In certain application, blocking functions are not suitable. Let’s consider for instance a condition in which the RX line is broken. If we try to do a sdGet() our thread will be stuck there indefinitely. To manage such kind of scenario the same functions are available with a timeout.
sdPutTimeout(sdp, char, timeout); char token = sdGetTimeout(sdp, timeout); sdWriteTimeout(sdp, string, size, timeout); sdReadTimeout(sdp, buffer, size, timeout);
The timeout shall be expressed in system ticks. As this conversion is not trivial, here are some macros which convert milliseconds to system ticks
TIME_S2I(secs) TIME_MS2I(msecs) TIME_US2I(usecs)
For example, if we want to read 4 bytes with a 50ms timeout we should write
sdReadTimeout(&SD2, buffer, 4, TIME_MS2I(50));
Note also that TIME_IMMEDIATE and TIME_INFINITE are accepted as well. In the first case, the function will return immediately; in the second it will behave like functions without a timeout.
GPIO related configuration
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). For 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.
/* Configuring PC6 as AF8 assigning it to USART6_TX. */ palSetPadMode(GPIOC, 6, PAL_MODE_ALTERNATE(8)); /* Configuring PC7 as AF8 assigning it to USART6_RX. */ palSetPadMode(GPIOC, 7, PAL_MODE_ALTERNATE(8));
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 pins are already configured in at board level for this purpose. Looking at STM32 Nucleo F401RE board.h we can notice that GPIOA setup is:
/* * GPIOA setup: * * PA0 - ARD_A0 ADC1_IN0 (input pullup). * PA1 - ARD_A1 ADC1_IN1 (input pullup). * PA2 - ARD_D1 USART2_TX (alternate 7). * PA3 - ARD_D0 USART2_RX (alternate 7). * PA4 - ARD_A2 ADC1_IN4 (input pullup). * PA5 - LED_GREEN ARD_D13 (output pushpull high). * PA6 - ARD_D12 (input pullup). * PA7 - ARD_D11 (input pullup). * PA8 - ARD_D7 (input pullup). * PA9 - ARD_D8 (input pullup). * PA10 - ARD_D2 (input pullup). * PA11 - OTG_FS_DM (alternate 10). * PA12 - OTG_FS_DP (alternate 10). * PA13 - SWDIO (alternate 0). * PA14 - SWCLK (alternate 0). * PA15 - PIN15 (input pullup). */
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 the serial driver 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 printing 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:
/** * @brief BaseSequentialStream specific methods. */ #define _base_sequential_stream_methods \ _base_object_methods \ /* Stream write buffer method.*/ \ size_t (*write)(void *instance, const uint8_t *bp, size_t n); \ /* Stream read buffer method.*/ \ size_t (*read)(void *instance, uint8_t *bp, size_t n); \ /* Channel put method, blocking.*/ \ msg_t (*put)(void *instance, uint8_t b); \ /* Channel get method, blocking.*/ \ msg_t (*get)(void *instance);
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 a way, it is possible to implement methods which work on many different drivers.
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 the 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 edit the makefile and adding the inclusion of the file chibios182/os/hal/lib/streams/streams.mk. This can be done at the section named “Other files (optional)” (see the last line of next code box).
# Licensing files. include $(CHIBIOS)/os/license/license.mk # Startup files. include $(CHIBIOS)/os/common/startup/ARMCMx/compilers/GCC/mk/startup_stm32f4xx.mk # HAL-OSAL files (optional). include $(CHIBIOS)/os/hal/hal.mk include $(CHIBIOS)/os/hal/ports/STM32/STM32F4xx/platform.mk include $(CHIBIOS)/os/hal/boards/ST_NUCLEO64_F401RE/board.mk include $(CHIBIOS)/os/hal/osal/rt/osal.mk # RTOS files (optional). include $(CHIBIOS)/os/rt/rt.mk include $(CHIBIOS)/os/common/ports/ARMCMx/compilers/GCC/mk/port_v7m.mk # Other files (optional). include $(CHIBIOS)/test/lib/test.mk include $(CHIBIOS)/test/rt/rt_test.mk include $(CHIBIOS)/test/oslib/oslib_test.mk include $(CHIBIOS)/os/hal/lib/streams/streams.mk
At this point, we can include the chprintf.h in the main file and use it. The function chprintf is defined as
/** * @brief System formatted output function. * @details This function implements a minimal @p printf() like functionality * with output on a @p BaseSequentialStream. * The general parameters format is: %[-][width|*][.precision|*][l|L]p. * The following parameter types (p) are supported: * - <b>x</b> hexadecimal integer. * - <b>X</b> hexadecimal long. * - <b>o</b> octal integer. * - <b>O</b> octal long. * - <b>d</b> decimal signed integer. * - <b>D</b> decimal signed long. * - <b>u</b> decimal unsigned integer. * - <b>U</b> decimal unsigned long. * - <b>c</b> character. * - <b>s</b> string. * . * * @param[in] chp pointer to a @p BaseSequentialStream implementing object * @param[in] fmt formatting string * @return The number of bytes that would have been * written to @p chp if no stream error occurs * * @api */ int chprintf(BaseSequentialStream *chp, const char *fmt, ...)
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 the first parameter and print a formatted string on it
/* This would print "Hello World 1st test" */ chprintf(&SD2, "Hello World %dst test!\r\n", 1); /* The previous line will give a warning at compile time resolved using this instead. */ chprintf((BaseSequentialStream*)&SD2, "Hello World %dst test!\r\n", 1); /* If you have to use many chprintf and do not want to cast every time you can use this trick. */ BaseSequentialStream* bsp = (BaseSequentialStream*)&SD2; chprintf(bsp, "Hello "); chprintf(bsp, "World "); chprintf(bsp, "%dst ", 1); chprintf(bsp, "test\r\n");
Using the previous code 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.
Further readings and Hands-on
You can find a collection of example and use case for the Serial Driver here:
Printing strings on a Virtual COM port with an STM32 and ChibiOS
More is coming and we suggest you follow us on Facebook to be up to date. In case you are using the STM32 Nucleo-64 F401RE development board some time ago, I have crafted a demo which shows how to use the ChibiOS Shell over Serial Driver.
RT-STM32F401RE-NUCLEO64-Shell-182.zip
A shell is basically a textual interface which relies on an I/O stream, in this case, offered by the serial driver. This demo has been copied by another ChibiOS demo about USB CDC (already mentioned above).
Another interesting source of information is some demos under testex which uses chprintf to print sensor data. Those demos can be very interesting if you are using an STM32F3 Discovery or an STM32F4 Discovery or an STM32 Nucleo + a MEMS x-Nucleo like the IKS01A1 or the IKS01A2.
Previous and next
This article is part of a series of articles which are meant to be tutorials. I have composed them to be read in sequence. Here the previous and next article of this series:
Hi, I want to print String from stm32f407 discovery to eclipse when I run or debug program. May we use USB-CDC for that? if possible, how can we do that?
Thank you so much.
Van Quang: The demo program wasnt printing to my serial, looking at the manual for my stm32f4 discovery It was clear that they didnt actually physically connect the PA2,3 to st-link pins used as uart for the virtual com port, so instead I used an ftdi cable directly on PA2,3 on stm32f407 which worked fine
Yes, only the ST-Link v2-1 offers the Virtual COM port. This was introduced with the STM32 Nucleos. The previous boards (F3 discovery and F4 discovery) did not support this feature.
Lately they reworked these board: F3 Discovery supports the feature starting from revision C.
I don’t remember if they reworked the F4 Discovery too and starting from which revision.
If the F4 discovery offers at least the VCP you can manually connect the UART of the MCU with the STLink using a couple of duponts.
Hi Rocco, thanks for you great article.
My OS is Ubuntu, I imported a demo project to ChibiStudio and it is compiled successfully. But as I want to check the source of the hal -e.g. the “sduStart” funciton- I can’t get to the fucntions’ definition.
So would you please tell me how can I check their source through the project directly?
Heh Phoenix,
you have to build the project at least once to open the definitions. If this doesn’t work, right click on the project and rebuild the index
Thanks Rocco, but I did it, but by Ctrl+Click on a function name, it only directs me to its declaration -not definition-.