
In Part 2 we pushed bytes out onto the wire; in Part 3 we turn the same timer interrupt around and listen. We will detect the start bit, sample each data bit at its centre using the oversampled tick, and report a completed line through an idle-line callback once the transmitter stops.
In this guide, we shall cover the following:
- Firmware Development.
- Flowchart of the soft uart.
- Results.
1. Firmware Development:
Open the soft_uart.h header file.
Within the header file, declare the following functions:
HAL_StatusTypeDef SoftUART_ReceiveToIdle_IT(uint8_t *buf, uint16_t len);
This function shall receive data using interrupt mode and trigger the function soft uart rx completed if either the buffer has been filled or IDLE line is detected. This is fully interrupt non-blocking mode.
Next, declare the following function:
HAL_StatusTypeDef SoftUART_ReceiveToIdle(uint8_t *buf, uint16_t len,
uint16_t *recv_len, uint32_t timeout_ms);This is similar to the previous function, however, it is in blocking mode and it will timeout after a while.
Next, declare the following two functions:
uint8_t SoftUART_ReceiveBusy(void); /* 1 = reception armed */ void SoftUART_AbortReceive(void);
The first one to determine if the reception is busy or not and the second one to abort the current transmission.
Finally, rx completed callback function:
void soft_uart_rx_cplt_callback(uint8_t *buf, uint16_t size);
This will be called when reception using interrupt has been called.
Hence, the full header file as follows:
#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 (OVERSAMPLING below)
*/
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 */
/* ================================== API =================================== */
/* Initialise state and start the single timer interrupt.
* Call after MX_GPIO_Init() and MX_TIM2_Init(). */
void SoftUART_Init(void);
/* ------------------------------- Transmit --------------------------------- */
/* Non-blocking. Queues the whole buffer; soft_uart_tx_cplt_callback() is
* invoked when the last character has been shifted out. The buffer must stay
* valid until the callback fires. */
HAL_StatusTypeDef SoftUART_Transmit_IT(const uint8_t *data, uint16_t len);
/* Blocking convenience 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);
/* ------------------------------- Receive ---------------------------------- */
/* Non-blocking. Fill buf[0..len-1] until IDLE is detected or the buffer is
* full, then call soft_uart_rx_cplt_callback(buf, size). */
HAL_StatusTypeDef SoftUART_ReceiveToIdle_IT(uint8_t *buf, uint16_t len);
/* Blocking variant; *recv_len receives the number of bytes collected. */
HAL_StatusTypeDef SoftUART_ReceiveToIdle(uint8_t *buf, uint16_t len,
uint16_t *recv_len, uint32_t timeout_ms);
uint8_t SoftUART_ReceiveBusy(void); /* 1 = reception armed */
void SoftUART_AbortReceive(void);
/* ------------------------- User callbacks (weak) -------------------------- */
/* Override these in your application; both run in the timer ISR context. */
void soft_uart_tx_cplt_callback(void);
void soft_uart_rx_cplt_callback(uint8_t *buf, uint16_t size);
/* Internal: called from the TIM2 update interrupt. Do not call from main. */
void SoftUART_Tick(void);
#ifdef __cplusplus
}
#endif
#endif /* SOFT_UART_H */Next, open soft_uart.c source file.
In soft_uart.c, declare the following variables:
/* ------------------------------- RX state --------------------------------- */ static uint8_t rx_active; static uint8_t *rx_buf; /* user buffer */ static uint16_t rx_len; static volatile uint16_t rx_idx; static volatile uint16_t rx_idle; /* consecutive high ticks */ static volatile uint8_t rx_done; static volatile uint16_t rx_last_size; static volatile uint8_t rx_busy; /* mid-frame */ static volatile uint8_t rx_tick; static volatile uint8_t rx_bit; static volatile uint8_t rx_shift;
These are related to rx status.
Next, for the RX reception functions, we shall start by reporting function:
static void SoftUART_RxReport(uint16_t size)
{
rx_active = 0u;
rx_done = 1u;
rx_last_size = size;
soft_uart_rx_cplt_callback(rx_buf, size);
}Next, a function to return the status of the RX:
uint8_t SoftUART_ReceiveBusy(void)
{
return rx_active;
}Next, a function to start the reception until idle in interrupt mode:
HAL_StatusTypeDef SoftUART_ReceiveToIdle_IT(uint8_t *buf, uint16_t len)
{
if ((buf == 0) || (len == 0u))
{
return HAL_ERROR;
}
if (rx_active)
{
return HAL_BUSY;
}
uint32_t primask = __get_PRIMASK();
__disable_irq();
rx_buf = buf;
rx_len = len;
rx_idx = 0u;
rx_idle = 0u;
rx_done = 0u;
rx_active = 1u;
__set_PRIMASK(primask);
return HAL_OK;
}Next, reception in polling mode:
HAL_StatusTypeDef SoftUART_ReceiveToIdle(uint8_t *buf, uint16_t len,
uint16_t *recv_len, uint32_t timeout_ms)
{
if (SoftUART_ReceiveToIdle_IT(buf, len) != HAL_OK)
{
return HAL_BUSY;
}
uint32_t start = HAL_GetTick();
while (!rx_done)
{
if ((timeout_ms != 0u) && ((HAL_GetTick() - start) >= timeout_ms))
{
SoftUART_AbortReceive();
return HAL_TIMEOUT;
}
}
if (recv_len != 0)
{
*recv_len = rx_last_size;
}
rx_done = 0u;
return HAL_OK;
}Next, the function to abort the reception:
void SoftUART_AbortReceive(void)
{
uint32_t primask = __get_PRIMASK();
__disable_irq();
rx_active = 0u;
rx_busy = 0u;
rx_done = 0u;
__set_PRIMASK(primask);
}
Next, within the softUART_Tick function, add the following block of code:
if (!rx_busy)
{
if (HAL_GPIO_ReadPin(SOFTUART_RX_PORT, SOFTUART_RX_PIN) == GPIO_PIN_RESET)
{
/* Falling edge = potential start bit. */
rx_idle = 0u;
rx_busy = 1u;
rx_tick = 0u;
rx_bit = 0u;
rx_shift = 0u;
}
else
{
/* Line idle: count consecutive high ticks for IDLE detection. */
if (rx_idle < 0xFFFFu)
{
rx_idle++;
}
if (rx_active && (rx_idx > 0u) && (rx_idle >= SOFTUART_IDLE_TICKS))
{
SoftUART_RxReport((uint16_t)rx_idx);
}
}
}
else
{
rx_tick++;
if (rx_tick > SOFTUART_HALF_TICKS)
{
uint8_t d = (uint8_t)(rx_tick - SOFTUART_HALF_TICKS);
if ((d % SOFTUART_BIT_TICKS) == 0u)
{
uint8_t centre = (uint8_t)(d / SOFTUART_BIT_TICKS); /* 1..9 */
if (centre <= 8u) /* data bit centre */
{
rx_shift >>= 1u;
if (HAL_GPIO_ReadPin(SOFTUART_RX_PORT, SOFTUART_RX_PIN) != GPIO_PIN_RESET)
{
rx_shift |= 0x80u;
}
rx_bit++;
}
else /* stop bit centre (9) */
{
if ((HAL_GPIO_ReadPin(SOFTUART_RX_PORT, SOFTUART_RX_PIN) != GPIO_PIN_RESET)
&& (rx_bit == 8u))
{
if (rx_active)
{
rx_buf[rx_idx++] = rx_shift;
if (rx_idx >= rx_len) /* buffer full */
{
SoftUART_RxReport(rx_len);
}
}
}
rx_busy = 0u;
}
}
}
else if (rx_tick == SOFTUART_HALF_TICKS) /* verify start bit */
{
if (HAL_GPIO_ReadPin(SOFTUART_RX_PORT, SOFTUART_RX_PIN) != GPIO_PIN_RESET)
{
rx_busy = 0u; /* false start / noise */
}
}
}This will be called each uart tick to read the rx pin and determine the state etc.
Finally, the rx completed function:
__weak void soft_uart_rx_cplt_callback(uint8_t *buf, uint16_t size)
{
(void)buf;
(void)size;
/* Override in your application code. */
}Hence, the full source code as follows:
/**
******************************************************************************
* @file soft_uart.c
* @brief Timer-driven (single timer) software UART for STM32F411RE @ 100 MHz
*
* TIM2 and the pins (PA0 = TX, PA1 = RX) are configured by STM32CubeMX. This
* driver uses the generated htim2 handle and its update interrupt, which
* fires at BAUD * OVERSAMPLING = 9600 * 8 = 76.8 kHz (one tick per 1/8 bit).
*
* TX (SoftUART_Transmit_IT):
* A buffer is sent byte by byte, back-to-back. Each byte is a 10-bit
* frame (start, 8 data LSB-first, stop) shifted out one bit every
* OVERSAMPLING ticks. When the last stop bit has completed,
* soft_uart_tx_cplt_callback() is called (from the ISR).
*
* RX (SoftUART_ReceiveToIdle_IT):
* The user supplies a buffer. Each received byte is stored in it. When
* the line stays idle (high) for SOFTUART_IDLE_TICKS after the last stop
* bit, or the buffer becomes full, soft_uart_rx_cplt_callback(buf,size)
* is called (from the ISR).
*
* Full duplex with a single timer.
******************************************************************************
*/
#include "soft_uart.h"
#define SOFTUART_BIT_TICKS (SOFTUART_OVERSAMPLING) /* ticks / bit */
#define SOFTUART_HALF_TICKS (SOFTUART_OVERSAMPLING / 2u) /* to the centre */
/* ------------------------------- 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 first */
static volatile uint8_t tx_bit; /* 0..9 */
static volatile uint8_t tx_tick; /* within-bit tick counter */
/* ------------------------------- RX state --------------------------------- */
static uint8_t rx_active;
static uint8_t *rx_buf; /* user buffer */
static uint16_t rx_len;
static volatile uint16_t rx_idx;
static volatile uint16_t rx_idle; /* consecutive high ticks */
static volatile uint8_t rx_done;
static volatile uint16_t rx_last_size;
static volatile uint8_t rx_busy; /* mid-frame */
static volatile uint8_t rx_tick;
static volatile uint8_t rx_bit;
static volatile uint8_t rx_shift;
/* ========================================================================== */
/* Init */
/* ========================================================================== */
void SoftUART_Init(void)
{
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;
rx_active = 0u;
rx_buf = 0;
rx_len = 0u;
rx_idx = 0u;
rx_idle = 0u;
rx_done = 0u;
rx_busy = 0u;
rx_tick = 0u;
rx_bit = 0u;
rx_shift = 0u;
HAL_NVIC_EnableIRQ(SOFTUART_TIM_IRQn); /* safe if CubeMX did it too */
HAL_TIM_Base_Start_IT(&SOFTUART_TIM_HANDLE);
}
/* ========================================================================== */
/* TX */
/* ========================================================================== */
/* Build frame and drive the start bit (called with the line at rest, either
* from main under lock or from the ISR between frames). */
static void SoftUART_StartByte(uint8_t byte)
{
tx_frame = (uint16_t)(((uint16_t)byte << 1) | 0x0200u); /* start=0, stop=1 */
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 */
}
uint8_t SoftUART_TransmitBusy(void)
{
return tx_active;
}
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;
}
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;
}
HAL_StatusTypeDef SoftUART_TransmitByte(uint8_t byte)
{
return SoftUART_Transmit(&byte, 1u);
}
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);
}
/* ========================================================================== */
/* RX */
/* ========================================================================== */
/* Reporting helper: end the current reception and notify the user. Runs in
* the timer ISR context. */
static void SoftUART_RxReport(uint16_t size)
{
rx_active = 0u;
rx_done = 1u;
rx_last_size = size;
soft_uart_rx_cplt_callback(rx_buf, size);
}
uint8_t SoftUART_ReceiveBusy(void)
{
return rx_active;
}
HAL_StatusTypeDef SoftUART_ReceiveToIdle_IT(uint8_t *buf, uint16_t len)
{
if ((buf == 0) || (len == 0u))
{
return HAL_ERROR;
}
if (rx_active)
{
return HAL_BUSY;
}
uint32_t primask = __get_PRIMASK();
__disable_irq();
rx_buf = buf;
rx_len = len;
rx_idx = 0u;
rx_idle = 0u;
rx_done = 0u;
rx_active = 1u;
__set_PRIMASK(primask);
return HAL_OK;
}
HAL_StatusTypeDef SoftUART_ReceiveToIdle(uint8_t *buf, uint16_t len,
uint16_t *recv_len, uint32_t timeout_ms)
{
if (SoftUART_ReceiveToIdle_IT(buf, len) != HAL_OK)
{
return HAL_BUSY;
}
uint32_t start = HAL_GetTick();
while (!rx_done)
{
if ((timeout_ms != 0u) && ((HAL_GetTick() - start) >= timeout_ms))
{
SoftUART_AbortReceive();
return HAL_TIMEOUT;
}
}
if (recv_len != 0)
{
*recv_len = rx_last_size;
}
rx_done = 0u;
return HAL_OK;
}
void SoftUART_AbortReceive(void)
{
uint32_t primask = __get_PRIMASK();
__disable_irq();
rx_active = 0u;
rx_busy = 0u;
rx_done = 0u;
__set_PRIMASK(primask);
}
/* ========================================================================== */
/* Tick (called from ISR) */
/* ========================================================================== */
void SoftUART_Tick(void)
{
/* -------------------------------- TX --------------------------------- */
if (tx_active)
{
if (++tx_tick >= SOFTUART_BIT_TICKS)
{
tx_tick = 0u;
tx_bit++;
if (tx_bit < 10u)
{
HAL_GPIO_WritePin(SOFTUART_TX_PORT, SOFTUART_TX_PIN,
(tx_frame & 1u) ? GPIO_PIN_SET : GPIO_PIN_RESET);
tx_frame >>= 1u;
}
else
{
/* Byte finished -> chain the next one or report completion. */
tx_idx++;
if (tx_idx < tx_len)
{
SoftUART_StartByte(tx_ptr[tx_idx]);
}
else
{
HAL_GPIO_WritePin(SOFTUART_TX_PORT, SOFTUART_TX_PIN, GPIO_PIN_SET);
tx_active = 0u;
soft_uart_tx_cplt_callback();
}
}
}
}
/* -------------------------------- RX --------------------------------- */
if (!rx_busy)
{
if (HAL_GPIO_ReadPin(SOFTUART_RX_PORT, SOFTUART_RX_PIN) == GPIO_PIN_RESET)
{
/* Falling edge = potential start bit. */
rx_idle = 0u;
rx_busy = 1u;
rx_tick = 0u;
rx_bit = 0u;
rx_shift = 0u;
}
else
{
/* Line idle: count consecutive high ticks for IDLE detection. */
if (rx_idle < 0xFFFFu)
{
rx_idle++;
}
if (rx_active && (rx_idx > 0u) && (rx_idle >= SOFTUART_IDLE_TICKS))
{
SoftUART_RxReport((uint16_t)rx_idx);
}
}
}
else
{
rx_tick++;
if (rx_tick > SOFTUART_HALF_TICKS)
{
uint8_t d = (uint8_t)(rx_tick - SOFTUART_HALF_TICKS);
if ((d % SOFTUART_BIT_TICKS) == 0u)
{
uint8_t centre = (uint8_t)(d / SOFTUART_BIT_TICKS); /* 1..9 */
if (centre <= 8u) /* data bit centre */
{
rx_shift >>= 1u;
if (HAL_GPIO_ReadPin(SOFTUART_RX_PORT, SOFTUART_RX_PIN) != GPIO_PIN_RESET)
{
rx_shift |= 0x80u;
}
rx_bit++;
}
else /* stop bit centre (9) */
{
if ((HAL_GPIO_ReadPin(SOFTUART_RX_PORT, SOFTUART_RX_PIN) != GPIO_PIN_RESET)
&& (rx_bit == 8u))
{
if (rx_active)
{
rx_buf[rx_idx++] = rx_shift;
if (rx_idx >= rx_len) /* buffer full */
{
SoftUART_RxReport(rx_len);
}
}
}
rx_busy = 0u;
}
}
}
else if (rx_tick == SOFTUART_HALF_TICKS) /* verify start bit */
{
if (HAL_GPIO_ReadPin(SOFTUART_RX_PORT, SOFTUART_RX_PIN) != GPIO_PIN_RESET)
{
rx_busy = 0u; /* false start / noise */
}
}
}
}
/* ========================================================================== */
/* HAL glue: route the timer IRQ */
/* ========================================================================== */
/* NOTE: if you already define HAL_TIM_PeriodElapsedCallback() elsewhere,
* delete this one and call SoftUART_Tick() for SOFTUART_TIM_INSTANCE there. */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == SOFTUART_TIM_INSTANCE)
{
SoftUART_Tick();
}
}
/* ========================================================================== */
/* Default (weak) callbacks */
/* ========================================================================== */
__weak void soft_uart_tx_cplt_callback(void)
{
/* Override in your application code. */
}
__weak void soft_uart_rx_cplt_callback(uint8_t *buf, uint16_t size)
{
(void)buf;
(void)size;
/* Override in your application code. */
}
Next, open main.c source file.
In user code begin PV, declare the following:
uint8_t rxBuff[100]; volatile uint8_t rx_completed;
Next, in user code begin 2 after starting the soft uart, start the reception in interrupt as follows:
SoftUART_ReceiveToIdle_IT(rxBuff,100);
Since this will call the rx completed, in user code begin 0, declare the following function:
void soft_uart_rx_cplt_callback(uint8_t *buf, uint16_t size)
{
rx_completed=1;
SoftUART_ReceiveToIdle_IT(rxBuff,100);
}Thats all for the firmware.
Save, build the project and run it as follows:

You may download the project from here.
2. Flow Chart of Soft UART:

3. Results.
Using TTL-USB converter, connect the RX pin to PA0 of STM32F411, open your serial terminal and set the baudrate to 9600, you can send data and display the rx buffer in the debugging session, you should get the following:

Thats all for the guide.
Stay tuned.
Happy coding š


Discussion