STM32 Programming. Part 17: UART Driver
Posted: 17 Oct 2023, 07:22
In the last article we got acquainted with such an interesting block in STM32 as UART. In this article we will not deal with simple examples (but that's for now), but we will immediately get acquainted with the library that allows you to conveniently interact with any UART in STM32F103xx microcontrollers.
Brief description
This library was created as a result of professional activities aimed at developing software for another project on a microcontroller It consists of 2 header files and 2 C files:
With the help of these defines we connect the pieces of code that are responsible for working with the corresponding UART module. Here you should pay attention to the fact that if the selected MC does not have this UART, you must comment out the corresponding define, otherwise the code will not compile.
Next comes the following:
There's a bunch of define-ons that are responsible for fine-tuning the functionality of the library.
If we want to use the ring buffer, we need to uncomment #define UARTn_USE_RING_BUFF, n is the number of UART-a. Further, you can configure the length of the ring buffer for the receiver and transmitter through #define UARTn_TXBUFF_LENGHT and #define UARTn_RXBUFF_LENGHT, in this example, the lengths of the buffers are 16 bytes.
Then comes the initialization structure:
and prototypes of library functions:
UART_Init() is used for initialization, it should be called before you start working with UART. UART_PutC() - send byte to UART, UART_GetC() - read.
Next are the functions of working with ring buffers.
UART_BytesToRead() - returns the number of bytes in the receiver buffer, which can be read by the function UART_GetC().
UART_BytesToWrite() - with this function you can get the number of bytes in the UART transmitter buffer.
UART_ReadBuffClear() and UART_WriteBuffClear() - clear the buffer of the receiver and transmitter respectively.
Internal device
Full description of the library I will not spend a full description of the library, I will stop only on the function of UART initialization. It is organized as follows:
For an example, let's look at the initialization process of UART1:
The first instruction in the condition if(id == 1) { ... } is to enable clocking of the UART1 module:
After that we proceed to initialize the pins to which the RX and TX lines are connected:
Using the TxPinInit() function we initialize the TX pin and RxPinInit() the RX pin. These functions are not very convenient, but I have some ideas how to improve it. By default, non-remap pins are used, if there is a need to use remap pins, all the configuration should be done in this code block. Of course, it's not very convenient, but for now it's just the way it is.
Next is the initialization of the UART:
The first thing we do is to reset the UART module through the RCC registers. This is done for "clean" reinitialization of the UART module. A bunch of asm("nop") just in case, that the periphery to keep up with the rapid execution of the program in the CPU. We do not use different HALs, we have a program is executed quickly?
After that we call the _uart_init() function, and in case of failure we exit with the return code -1.
Then we initialize the ring buffer, if it is used for this UART:
And the final touch: enable the UART and exit with return code 0:
Example of working with the library
Initially this library was written to work on stm32f103c8 microcontroller, then it was finalized on stm32f103ve, and for writing this article the project for stm32f103c8 was created again, and it didn't cause any problems. Most likely, there will be no problems when porting this library to any stm32f103 microcontroller.
So, let's move on to the code. Here is main.c:
The first thing we declare the structure, which contains information for the initialization of UART-a, and I declared it as const, so that it was placed in flash memory and did not take up space in the RAM of the microcontroller. The value of the parameters of the structure is as follows:
Let's go to main(). First initialize the clock generator with ClockInit() to the maximum frequency of operation. After that initialize UART1:
UART_Init(1, &UARTInitStr);
As arguments we pass the number of the UART and the initialization structure.
After that, we clear the buffers of the receiver and transmitter (it is not necessary), and in an infinite loop poll the receiver UART with the function UART_GetC(). If any character arrives, the function will return a value other than -1, which we immediately send back using UART_PutC(). Here is the whole example.
Brief description
This library was created as a result of professional activities aimed at developing software for another project on a microcontroller It consists of 2 header files and 2 C files:
- uart.h
- uart.c
- RingFIFO.h
- RingFIFO.c
- Runs on stm32f103xx microcontrollers;
- Allows data exchange via UART1, 2, 3, 4. Data exchange through UART5 is not yet implemented (was not necessary, but this will be corrected );
- Ability to reinitialize UART directly during code execution;
- The ability to engage and customize the length of the FIFO-buffer on the receiver and transmitter;
- Code sections not used in this project will not be compiled into the project (implemented via #ifdef);
- Uses only CMSIS for operation;
- Not overloaded with unnecessary code (if possible) and requires not very many resources (reasonable price for usability).
Code: Select all
#define UART1_ENABLE
#define UART2_ENABLE
#define UART3_ENABLE
//#define UART4_ENABLE
Next comes the following:
Code: Select all
#ifdef UART1_ENABLE
#define UART1_USE_RING_BUFF
#define UART1_TXBUFF_LENGHT 16
#define UART1_RXBUFF_LENGHT 16
#endif
#ifdef UART2_ENABLE
#define UART2_USE_RING_BUFF
#define UART2_TXBUFF_LENGHT 16
#define UART2_RXBUFF_LENGHT 16
#endif
#ifdef UART3_ENABLE
#define UART3_USE_RING_BUFF
#define UART3_TXBUFF_LENGHT 16
#define UART3_RXBUFF_LENGHT 16
#endif
#ifdef UART4_ENABLE
#define UART4_USE_RING_BUFF
#define UART4_TXBUFF_LENGHT 16
#define UART4_RXBUFF_LENGHT 16
#endif
If we want to use the ring buffer, we need to uncomment #define UARTn_USE_RING_BUFF, n is the number of UART-a. Further, you can configure the length of the ring buffer for the receiver and transmitter through #define UARTn_TXBUFF_LENGHT and #define UARTn_RXBUFF_LENGHT, in this example, the lengths of the buffers are 16 bytes.
Then comes the initialization structure:
Code: Select all
typedef struct
{
uint32_t bus_freq; // uart bus frequency
uint32_t baud; //baud rate
uint8_t data_bits; //number of data bits (8, 9)
uint8_t stop_bits; //number of stop bits (1 or 2)
uint8_t parity; // parity check (0 - none, 1 - even, 2 - odd)
} UARTInitStructure_t;
Code: Select all
//UART initialization
//
//id - port number
//init - UART initialization structure
//
//Return: 0 - success, -1 - initialization error
int16_t UART_Init(uint8_t id, const UARTInitStructure_t *init);
//Write a character into the transmitter buffer
//
//id - port number
//s - symbol to be sent
//
//Retrun: s - success, -1 - error, buffer overflowed
int16_t UART_PutC(uint8_t id, char c);
//Read the symbol from the receiver buffer
//
//id - port number
//
//Return: -1 - no data to read, 0..255 - read character
int16_t UART_GetC(uint8_t id);
//Get the number of unread bytes in the buffer of the receiver
int16_t UART_BytesToRead(uint8_t id);
//Get the number of bytes not yet sent in the transmitter buffer
int16_t UART_BytesToWrite(uint8_t id);
//Clear the receiver buffer
void UART_ReadBuffClear(uint8_t id);
//Clear the transmitter buffer
void UART_WriteBuffClear(uint8_t id);
Next are the functions of working with ring buffers.
UART_BytesToRead() - returns the number of bytes in the receiver buffer, which can be read by the function UART_GetC().
UART_BytesToWrite() - with this function you can get the number of bytes in the UART transmitter buffer.
UART_ReadBuffClear() and UART_WriteBuffClear() - clear the buffer of the receiver and transmitter respectively.
Internal device
Full description of the library I will not spend a full description of the library, I will stop only on the function of UART initialization. It is organized as follows:
Code: Select all
int16_t UART_Init(uint8_t id, const UARTInitStructure_t *init)
{
if(id == 1)
{
....
return 0;
}
if(id == 2)
{
....
return 0;
}
if(id == 3)
{
....
return 0;
}
....
return -1;
}
Code: Select all
#ifdef UART1_ENABLE
/*
PORTS:
DEFAULT REMAP.
TX1 PA9 PB6
RX1 PA10 PB7
BUS:
APB2
*/
if(id == 1)
{
//Turn on UART module clocking
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
//Customize ports
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
//configure TX (PA9)
TxPinInit(&GPIOA->CRH,
GPIO_CRH_MODE9_Pos,
GPIO_CRH_CNF9_Pos);
//setup RX (PA10)
RxPinInit(&GPIOA->CRH,
&GPIOA->BSRR,
GPIO_CRH_MODE10_Pos,
GPIO_CRH_CNF10_Pos,
10);
//Module reset
RCC->APB2RSTR |= RCC_APB2RSTR_USART1RST;
asm("nop");
asm("nop");
asm("nop");
RCC->APB2RSTR &= ~RCC_APB2RSTR_USART1RST;
asm("nop");
asm("nop");
asm("nop");
//Initialization of the main registers of UART
if(_uart_init(USART1, init) < 0)
return -1;
#ifdef UART1_USE_RING_BUFF
RingBuffInit(&tx_fifo1, tx_buff1, UART1_TXBUFF_LENGHT);
RingBuffInit(&rx_fifo1, rx_buff1, UART1_RXBUFF_LENGHT);
RXNEIEnable(USART1);
NVIC_EnableIRQ(USART1_IRQn);
#endif
//Start UART
_uart_en(USART1);
return 0;
}
#endif
Code: Select all
//Turn on the clocking of the UART module
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
Code: Select all
//Set up the ports
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
//configure TX (PA9)
TxPinInit(&GPIOA->CRH,
GPIO_CRH_MODE9_Pos,
GPIO_CRH_CNF9_Pos);
//setup RX (PA10)
RxPinInit(&GPIOA->CRH,
&GPIOA->BSRR,
GPIO_CRH_MODE10_Pos,
GPIO_CRH_CNF10_Pos,
10);
Next is the initialization of the UART:
Code: Select all
//Reset the module
RCC->APB2RSR |= RCC_APB2RSTR_USART1RST;
asm("nop");
asm("nop");
asm("nop");
RCC->APB2RSTR &= ~RCC_APB2RSTR_USART1RST;
asm("nop");
asm("nop");
asm("nop");
//Initialization of the main registers of UART
if(_uart_init(USART1, init) < 0)
return -1;
After that we call the _uart_init() function, and in case of failure we exit with the return code -1.
Then we initialize the ring buffer, if it is used for this UART:
Code: Select all
#ifdef UART1_USE_RING_BUFF
RingBuffInit(&tx_fifo1, tx_buff1, UART1_TXBUFF_LENGHT);
RingBuffInit(&rx_fifo1, rx_buff1, UART1_RXBUFF_LENGHT);
RXNEIEnable(USART1);
NVIC_EnableIRQ(USART1_IRQn);
#endif
Code: Select all
//Start UART
_uart_en(USART1);
return 0;
Initially this library was written to work on stm32f103c8 microcontroller, then it was finalized on stm32f103ve, and for writing this article the project for stm32f103c8 was created again, and it didn't cause any problems. Most likely, there will be no problems when porting this library to any stm32f103 microcontroller.
So, let's move on to the code. Here is main.c:
Code: Select all
#include <stdint.h>
#include "clock.h"
#include "uart.h"
static const UARTInitStructure_t UARTInitStr =
{
.bus_freq = 36000000,
.baud = 19200,
.data_bits = 8,
.stop_bits = 1,
.parity = 0,
};
void main()
{
int16_t c;
ClockInit();
UART_Init(1, &UARTInitStr);
UART_ReadBuffClear(1);
UART_WriteBuffClear(1);
for(;;)
{
c = UART_GetC(1);
if(c != -1)
UART_PutC(1, (char)c);
}
}
- bus_freq - the frequency of the bus, which is connected to the module UART, in this case 36MHz
- baud - data transfer rate
- data_bits - the number of data bits, 8 or 9
- stop_bits - number of stop bits, 1 or 2
- parity - parity control, 0 - none, 1 - even, 2 - odd.
Let's go to main(). First initialize the clock generator with ClockInit() to the maximum frequency of operation. After that initialize UART1:
UART_Init(1, &UARTInitStr);
As arguments we pass the number of the UART and the initialization structure.
After that, we clear the buffers of the receiver and transmitter (it is not necessary), and in an infinite loop poll the receiver UART with the function UART_GetC(). If any character arrives, the function will return a value other than -1, which we immediately send back using UART_PutC(). Here is the whole example.