
In Part 1 we configured the timer and the transmit pin; in Part 2 we put that time base to work and actually send data. The interrupt-driven transmitter queues a buffer of bytes, shifts them out back-to-back at 9600 baud, and notifies you through a completion callback once the last stop bit has left the pin.
In this guide, we shall cover the following:
- Create a source and header file.
- Implementing the header file.
- Implementing the source file.
- Testing our firmware.
1. Create a Source and Header File:
We start by creating new header and source file with name of soft_uart.h and soft_uart.c respectively.
To create the header file, right click on inc folder, and select new, header file as follows:

Give it a name soft_uart.h and click on Finish.
To create new source file, right click on src folder, new and Source file as follows:

Give it a name soft_uart.h and click on Finish.
2. Implementing the Header File:
Open soft_uart.h header file.
We start by including the following header file:
#include "stm32f4xx_hal.h" #include <stdint.h>
Next, declare the following macros:
extern TIM_HandleTypeDef htim2; /* generated by CubeMX (MX_TIM2_Init) */ #define SOFTUART_TIM_HANDLE htim2 #define SOFTUART_TIM_INSTANCE TIM2 #define SOFTUART_TIM_IRQn TIM2_IRQn /* --- Pins (must match the CubeMX GPIO configuration) --- * PA0 : GPIO_Output, Push-Pull, No pull, High Speed, initial level = High * PA1 : GPIO_Input, Pull-up */ #define SOFTUART_TX_PORT GPIOA #define SOFTUART_TX_PIN GPIO_PIN_0 #define SOFTUART_RX_PORT GPIOA #define SOFTUART_RX_PIN GPIO_PIN_1 /* --- Line format / timing --- * OVERSAMPLING must equal: timer_clock / ((PSC+1)*(ARR+1)*BAUD) * here : 100e6 / (651 * 2 * 9600) = 8 */ #define SOFTUART_BAUD 9600u /* bits per second */ #define SOFTUART_OVERSAMPLING 8u /* timer ticks per bit (>= 4) */ /* RX IDLE detection threshold, in timer ticks. The line must remain high for * at least this long after the last stop bit before IDLE is reported (~1 * frame time). Must be > one bit so back-to-back bytes are not split. */ #define SOFTUART_IDLE_TICKS (10u * SOFTUART_OVERSAMPLING) /* 80 ticks */
These are constant that can changes according to your setup such as GPIO pins, timer etc
Next, declare the initialization function:
/* Reset state, drive TX idle high and start the TIM2 update interrupt. * Call once after MX_GPIO_Init() and MX_TIM2_Init(). */ void SoftUART_Init(void);
Next, blocking and non blocking sending functions:
/* Non-blocking send of a whole buffer. * Returns HAL_OK, HAL_BUSY (a transfer is already running) or HAL_ERROR. * The buffer must stay valid until soft_uart_tx_cplt_callback() fires. */ HAL_StatusTypeDef SoftUART_Transmit_IT(const uint8_t *data, uint16_t len); /* Blocking wrappers (also trigger soft_uart_tx_cplt_callback()). */ HAL_StatusTypeDef SoftUART_Transmit(const uint8_t *data, uint16_t len); HAL_StatusTypeDef SoftUART_TransmitByte(uint8_t byte); uint8_t SoftUART_TransmitBusy(void); /* 1 = transmitting */ void SoftUART_AbortTransmit(void); /* stop and force idle high */
Next, function to be called in the timer interrupt to handle timing of TX/RX:
/* Called from the TIM2 update interrupt (via HAL_TIM_PeriodElapsedCallback * at the bottom of soft_uart.c). */ void SoftUART_Tick(void);
And finally, transmit completed callback function:
/* Weak callback, override in your application. * Runs in the timer ISR context, once per buffer, after the last stop bit. */ void soft_uart_tx_cplt_callback(void);
Hence, the header file as follows:
/**
******************************************************************************
* @file soft_uart.h
* @brief Interrupt-driven software UART TRANSMITTER (single timer)
* STM32F411RE @ 100 MHz, 9600 baud, 8N1
*
* TX = PA0 (output, push-pull)
*
* TIM2 is configured by STM32CubeMX and its update interrupt runs at
* BAUD * OVERSAMPLING = 9600 * 8 = 76.8 kHz (one tick = 1/8 bit).
*
* SoftUART_Transmit_IT() queues a whole buffer. The bytes are sent
* back-to-back. When the LAST stop bit has left the pin,
* soft_uart_tx_cplt_callback() is called from the timer ISR.
******************************************************************************
*/
#ifndef SOFT_UART_H
#define SOFT_UART_H
#ifdef __cplusplus
extern "C" {
#endif
#include "stm32f4xx_hal.h"
#include <stdint.h>
/* ======================= Configuration (match CubeMX) ===================== */
/* --- Timer: configured by CubeMX, handle generated in main.c ---
*
* Required TIM2 settings in STM32CubeMX:
* Clock Source : Internal Clock
* Prescaler (PSC) : 650
* Counter Mode : Up
* Counter Period(ARR) : 1
* Clock Division : No Division
* auto-reload preload : Disable
* NVIC : TIM2 global interrupt = Enabled
*
* -> update interrupt = 100 MHz / ((650+1) * (1+1)) = 76805 Hz
* = 8 x 9600 baud
*/
extern TIM_HandleTypeDef htim2; /* generated by CubeMX (MX_TIM2_Init) */
#define SOFTUART_TIM_HANDLE htim2
#define SOFTUART_TIM_INSTANCE TIM2
#define SOFTUART_TIM_IRQn TIM2_IRQn
/* --- TX pin (must match the CubeMX GPIO configuration) ---
* PA0 : GPIO_Output, Push-Pull, No pull, High Speed, initial level = High
*/
#define SOFTUART_TX_PORT GPIOA
#define SOFTUART_TX_PIN GPIO_PIN_0
/* --- Line format / timing ---
* OVERSAMPLING must equal: timer_clock / ((PSC+1)*(ARR+1)*BAUD)
* here : 100e6 / (651 * 2 * 9600) = 8
*/
#define SOFTUART_BAUD 9600u
#define SOFTUART_OVERSAMPLING 8u
/* ================================== API =================================== */
/* Reset state, drive TX idle high and start the TIM2 update interrupt.
* Call once after MX_GPIO_Init() and MX_TIM2_Init(). */
void SoftUART_Init(void);
/* Non-blocking send of a whole buffer.
* Returns HAL_OK, HAL_BUSY (a transfer is already running) or HAL_ERROR.
* The buffer must stay valid until soft_uart_tx_cplt_callback() fires. */
HAL_StatusTypeDef SoftUART_Transmit_IT(const uint8_t *data, uint16_t len);
/* Blocking wrappers (also trigger soft_uart_tx_cplt_callback()). */
HAL_StatusTypeDef SoftUART_Transmit(const uint8_t *data, uint16_t len);
HAL_StatusTypeDef SoftUART_TransmitByte(uint8_t byte);
uint8_t SoftUART_TransmitBusy(void); /* 1 = transmitting */
void SoftUART_AbortTransmit(void); /* stop and force idle high */
/* Called from the TIM2 update interrupt (via HAL_TIM_PeriodElapsedCallback
* at the bottom of soft_uart.c). */
void SoftUART_Tick(void);
/* Weak callback, override in your application.
* Runs in the timer ISR context, once per buffer, after the last stop bit. */
void soft_uart_tx_cplt_callback(void);
#ifdef __cplusplus
}
#endif
#endif /* SOFT_UART_H */3. Source File Implantation:
Open soft_uart.c source file.
We start by including soft_uart.h header file as follows:
#include "soft_uart.h"
Next, define number of bit for each character:
#define SOFTUART_BIT_TICKS (SOFTUART_OVERSAMPLING) /* 8 ticks / bit */
Next, declare variables to hold state of TX as follows:
/* ------------------------------- TX state --------------------------------- */ static uint8_t tx_active; static const uint8_t *tx_ptr; /* buffer being sent */ static uint16_t tx_len; static uint16_t tx_idx; static volatile uint16_t tx_frame; /* 10-bit frame, LSB transmitted first */ static volatile uint8_t tx_bit; /* 0..9 */ static volatile uint8_t tx_tick; /* within-bit tick counter */
Next, define a function to send the start bit as follows:
static void SoftUART_StartByte(uint8_t byte)
{
/* bit0 = START(0), bit1..8 = data LSB first, bit9 = STOP(1) */
tx_frame = (uint16_t)(((uint16_t)byte << 1) | 0x0200u);
tx_bit = 0u;
tx_tick = 0u;
HAL_GPIO_WritePin(SOFTUART_TX_PORT, SOFTUART_TX_PIN, GPIO_PIN_RESET);
tx_frame >>= 1u; /* bit0 is now data bit 0 */
}Next, function to initialize the soft uart:
void SoftUART_Init(void)
{
/* GPIO is configured by CubeMX; just enforce the idle level. */
HAL_GPIO_WritePin(SOFTUART_TX_PORT, SOFTUART_TX_PIN, GPIO_PIN_SET);
tx_active = 0u;
tx_ptr = 0;
tx_len = 0u;
tx_idx = 0u;
tx_bit = 0u;
tx_tick = 0u;
HAL_NVIC_EnableIRQ(SOFTUART_TIM_IRQn); /* safe if CubeMX did it too */
HAL_TIM_Base_Start_IT(&SOFTUART_TIM_HANDLE);
}A function to indicate if the transmission is busy or not:
uint8_t SoftUART_TransmitBusy(void)
{
return tx_active;
}Next, a function to send data using interrupt method:
HAL_StatusTypeDef SoftUART_Transmit_IT(const uint8_t *data, uint16_t len)
{
if ((data == 0) || (len == 0u))
{
return HAL_ERROR;
}
if (tx_active)
{
return HAL_BUSY;
}
uint32_t primask = __get_PRIMASK();
__disable_irq();
tx_ptr = data;
tx_len = len;
tx_idx = 0u;
tx_active = 1u;
SoftUART_StartByte(data[0]);
__set_PRIMASK(primask);
return HAL_OK;
}Transmit using blocking mode:
HAL_StatusTypeDef SoftUART_Transmit(const uint8_t *data, uint16_t len)
{
if ((data == 0) || (len == 0u))
{
return HAL_ERROR;
}
while (tx_active) { } /* wait for the line to be free */
if (SoftUART_Transmit_IT(data, len) != HAL_OK)
{
return HAL_ERROR;
}
while (tx_active) { }
return HAL_OK;
}To send a single byte:
HAL_StatusTypeDef SoftUART_TransmitByte(uint8_t byte)
{
return SoftUART_Transmit(&byte, 1u);
}Abort transmission function:
void SoftUART_AbortTransmit(void)
{
uint32_t primask = __get_PRIMASK();
__disable_irq();
tx_active = 0u;
tx_idx = tx_len;
HAL_GPIO_WritePin(SOFTUART_TX_PORT, SOFTUART_TX_PIN, GPIO_PIN_SET);
__set_PRIMASK(primask);
}Tick for TX:
oid SoftUART_Tick(void)
{
if (!tx_active)
{
return;
}
if (++tx_tick >= SOFTUART_BIT_TICKS)
{
tx_tick = 0u;
tx_bit++;
if (tx_bit < 10u)
{
/* Still inside this byte: output the next bit. */
HAL_GPIO_WritePin(SOFTUART_TX_PORT, SOFTUART_TX_PIN,
(tx_frame & 1u) ? GPIO_PIN_SET : GPIO_PIN_RESET);
tx_frame >>= 1u;
}
else
{
/* Byte finished (its STOP bit has just completed). */
tx_idx++;
if (tx_idx < tx_len)
{
SoftUART_StartByte(tx_ptr[tx_idx]); /* next byte, no gap */
}
else
{
HAL_GPIO_WritePin(SOFTUART_TX_PORT, SOFTUART_TX_PIN, GPIO_PIN_SET);
tx_active = 0u;
soft_uart_tx_cplt_callback(); /* whole buffer sent */
}
}
}
}Timer interrupt handler:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == SOFTUART_TIM_INSTANCE)
{
SoftUART_Tick();
}
}Finally, the weak function to handle TX completed as follows:
__weak void soft_uart_tx_cplt_callback(void)
{
/* Override in your application code. */
}4. Testing Our Firmware:
Open the main.c source file.
In user code begin include, include the software uart header file and stdio header file as follows:
#include "soft_uart.h" #include "stdio.h"
Next, in user code begin PV, declare the following variables:
uint8_t txBuff[100]; uint8_t counter; volatile uint8_t tx_completed;
- Buffer to hold the data to be transmitted.
- Counter value to be increment later during the demo.
- tx_completed as flag to indicate that uart has finished transmitting the data.
In user code begin 0, declare the following function:
void soft_uart_tx_cplt_callback(void)
{
tx_completed=1;
}
The function shall only set the flag to 1.
Next, in user code begin 2 in main function:
Initialize the software uart as follows:
SoftUART_Init();
Next, in user code begin 3 in while 1 loop:
uint16_t len=snprintf(txBuff,100,"Counter Value =%d \r\n",counter++); SoftUART_Transmit_IT(txBuff, len); while(tx_completed==0); tx_completed=0; HAL_Delay(10);
Thats all for the firmware.
Save, build the project and run it as follows:

Using TTL-USB converter, connect the RX pin to PA0 of STM32F411, open your serial terminal and set the baudrate to 9600, you should get the following:

Here is a screenshot from oscilloscope:

In next part, we shall implement the reception part.
Stay tuned.
Happy coding š


Discussion