[Revised] Emulating UART Part 2: Sending Data

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 šŸ˜‰

EmbeddedExpertIO courses

Explore the course catalogue

Find a course for your next embedded project.

Browse coursesstudy.embeddedexpert.ioOpens in a new tab

Discussion

Join the discussion

Your email address will not be published. Required fields are marked *