Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Porting FreeModbus RTU Slave to STM32F103 (Keil) with USART1 and TIM6 Integration

Tech 1

Porting FreeModbus RTU Slave to STM32F103 (Keil MDK) using USART1 and TIM6 on the Wildfire board requires wiring the protocol stack to the MCU’s UART, timer, and basic interrupt primitives. The following steps integrate FreeModbus v1.6 with the STM32F103VET6 Standard Peripheral Library (SPL), targeting an RTU slave running on USART1 and a 50 µs timebase via TIM6.

Hardware

  • STM32F103VET6 board (Wildfire Guide series)

Software

  • Keil MDK
  • FreeModbus v1.6
  • STM32F10x SPL (or equivalent CMSIS + peripheral layer)

Project layout

  • Add freemodbus/modbus/*.c to your project
  • Add port/port.h, port/portevent.c, port/portseiral.c, port/porttimer.c (new or adapted) to you're project
  • Ensure include paths cover FreeModbus headers and your port folder

File changes for the port layer

port.h

  • Provide critical section macros expected by the stack. On Cortex‑M3, CMSIS intrinsics can be used.
#ifndef _PORT_H
#define _PORT_H

#include "stm32f10x.h"
#include <stdint.h>

#define ENTER_CRITICAL_SECTION() __disable_irq()
#define EXIT_CRITICAL_SECTION()  __enable_irq()

#endif

If __disable_irq/__enable_irq are not available, map to __set_PRIMASK(1) and __set_PRIMASK(0) respectively.

portserial.c

  • Bind FreeModbus serial hooks to USART1. The example uses RS‑485 friandly logic and uses TXE (transmit data register empty) for non-blocking transmit.
#include "stm32f10x.h"
#include "port.h"
#include "mb.h"
#include "mbport.h"

/* Change these macros as needed for a different UART or RS485-DE/RE pin */
#define MB_UART                 USART1
#define MB_UART_GPIO_PORT       GPIOA
#define MB_UART_TX_PIN          GPIO_Pin_9
#define MB_UART_RX_PIN          GPIO_Pin_10
#define MB_UART_IRQn            USART1_IRQn

/* Optional: RS485 driver-enable pin (active-high). Comment out if unused. */
/* #define MB_RS485_GPIO_PORT   GPIOB
   #define MB_RS485_GPIO_PIN    GPIO_Pin_1 */

static void prvTxISR(void);
static void prvRxISR(void);

BOOL xMBPortSerialInit(UCHAR ucPort, ULONG ulBaud, UCHAR ucDataBits, eMBParity eParity)
{
    GPIO_InitTypeDef  gpio;
    USART_InitTypeDef uart;
    NVIC_InitTypeDef  nvic;

    /* GPIO clocks */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    /* UART clock */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);

    /* TX: AF push-pull */
    gpio.GPIO_Pin   = MB_UART_TX_PIN;
    gpio.GPIO_Mode  = GPIO_Mode_AF_PP;
    gpio.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(MB_UART_GPIO_PORT, &gpio);

    /* RX: floating input */
    gpio.GPIO_Pin  = MB_UART_RX_PIN;
    gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(MB_UART_GPIO_PORT, &gpio);

    /* Optional: RS485 DE/RE control pin setup */
#ifdef MB_RS485_GPIO_PORT
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    gpio.GPIO_Pin   = MB_RS485_GPIO_PIN;
    gpio.GPIO_Mode  = GPIO_Mode_Out_PP;
    gpio.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(MB_RS485_GPIO_PORT, &gpio);
    /* Default to receive */
    GPIO_ResetBits(MB_RS485_GPIO_PORT, MB_RS485_GPIO_PIN);
#endif

    /* Map parity/data bits */
    uart.USART_BaudRate            = (uint32_t)ulBaud;
    uart.USART_StopBits            = USART_StopBits_1;
    uart.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    uart.USART_Mode                = USART_Mode_Rx | USART_Mode_Tx;

    switch (eParity) {
    case MB_PAR_EVEN:
        uart.USART_Parity    = USART_Parity_Even;
        uart.USART_WordLength = (ucDataBits == 8) ? USART_WordLength_9b : USART_WordLength_9b;
        break;
    case MB_PAR_ODD:
        uart.USART_Parity    = USART_Parity_Odd;
        uart.USART_WordLength = (ucDataBits == 8) ? USART_WordLength_9b : USART_WordLength_9b;
        break;
    case MB_PAR_NONE:
    default:
        uart.USART_Parity    = USART_Parity_No;
        uart.USART_WordLength = (ucDataBits == 9) ? USART_WordLength_9b : USART_WordLength_8b;
        break;
    }

    USART_Init(MB_UART, &uart);
    USART_Cmd(MB_UART, ENABLE);

    /* NVIC for USART */
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    nvic.NVIC_IRQChannel = MB_UART_IRQn;
    nvic.NVIC_IRQChannelPreemptionPriority = 1;
    nvic.NVIC_IRQChannelSubPriority        = 1;
    nvic.NVIC_IRQChannelCmd                = ENABLE;
    NVIC_Init(&nvic);

    /* Enable RXNE by default; TXE will be enabled when sending */
    USART_ITConfig(MB_UART, USART_IT_RXNE, ENABLE);

    return TRUE;
}

void vMBPortSerialEnable(BOOL xRxEnable, BOOL xTxEnable)
{
    if (xRxEnable) {
        /* Switch RS485 to receive */
#ifdef MB_RS485_GPIO_PORT
        GPIO_ResetBits(MB_RS485_GPIO_PORT, MB_RS485_GPIO_PIN);
#endif
        USART_ClearITPendingBit(MB_UART, USART_IT_RXNE);
        USART_ITConfig(MB_UART, USART_IT_RXNE, ENABLE);
    } else {
        USART_ITConfig(MB_UART, USART_IT_RXNE, DISABLE);
    }

    if (xTxEnable) {
        /* Switch RS485 to transmit */
#ifdef MB_RS485_GPIO_PORT
        GPIO_SetBits(MB_RS485_GPIO_PORT, MB_RS485_GPIO_PIN);
#endif
        /* Use TXE interrupt to stream bytes from the stack */
        USART_ClearITPendingBit(MB_UART, USART_IT_TXE);
        USART_ITConfig(MB_UART, USART_IT_TXE, ENABLE);
    } else {
        USART_ITConfig(MB_UART, USART_IT_TXE, DISABLE);
        /* Optionally also disable TC */
        USART_ITConfig(MB_UART, USART_IT_TC, DISABLE);
#ifdef MB_RS485_GPIO_PORT
        /* Back to receive after TX completed; when using TC path, do it in ISR */
        GPIO_ResetBits(MB_RS485_GPIO_PORT, MB_RS485_GPIO_PIN);
#endif
    }
}

BOOL xMBPortSerialPutByte(CHAR ch)
{
    USART_SendData(MB_UART, (uint16_t)ch);
    return TRUE;
}

BOOL xMBPortSerialGetByte(CHAR *pch)
{
    *pch = (CHAR)USART_ReceiveData(MB_UART);
    return TRUE;
}

static void prvTxISR(void)
{
    /* Notify stack that a byte can be queued */
    pxMBFrameCBTransmitterEmpty();
}

static void prvRxISR(void)
{
    /* Notify stack that a byte arrived */
    pxMBFrameCBByteReceived();
}

void USART1_IRQHandler(void)
{
    if (USART_GetITStatus(MB_UART, USART_IT_RXNE) != RESET) {
        (void)USART_ReceiveData(MB_UART); /* read DR promptly or leave for xMBPortSerialGetByte */
        prvRxISR();
        USART_ClearITPendingBit(MB_UART, USART_IT_RXNE);
    }

    /* TXE-driven transmit */
    if (USART_GetITStatus(MB_UART, USART_IT_TXE) != RESET) {
        prvTxISR();
        USART_ClearITPendingBit(MB_UART, USART_IT_TXE);
    }

    /* If you prefer TC-driven flow, enable TC and call prvTxISR() here. */
    if (USART_GetITStatus(MB_UART, USART_IT_TC) != RESET) {
        /* Optional: end of frame handling when using TC */
        USART_ClearITPendingBit(MB_UART, USART_IT_TC);
#ifdef MB_RS485_GPIO_PORT
        GPIO_ResetBits(MB_RS485_GPIO_PORT, MB_RS485_GPIO_PIN);
#endif
    }
}

Notes

  • Using TXE avoids patching the RTU core and commonly aligns with FreeModbus’s expectations: TXE triggers when DR is empty and requests the next byte via pxMBFrameCBTransmitterEmpty().
  • If you decide to drive sending with TC instead, seed the first byte manually (see the optional mbrtu.c tweak below) and enable TC interrupts.

porttimer.c

  • Provide a 50 µs timebase for the RTU inter-character and frame timeouts. With a 72 MHz system clock, prescale to 20 kHz (50 µs) and use the argument as the auto-reload value in "units of 50 µs".
#include "stm32f10x.h"
#include "port.h"
#include "mb.h"
#include "mbport.h"

#define MB_TIMx              TIM6
#define MB_TIMx_IRQn         TIM6_IRQn

static void prvTimerISR(void)
{
    (void)pxMBPortCBTimerExpired();
}

BOOL xMBPortTimersInit(USHORT usTicks50us)
{
    TIM_TimeBaseInitTypeDef tim;
    NVIC_InitTypeDef        nvic;

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE);

    /* 72 MHz / 3600 = 20 kHz => 50 µs per tick */
    tim.TIM_Prescaler     = 3600 - 1;
    tim.TIM_Period        = (uint16_t)usTicks50us; /* in 50 µs units */
    tim.TIM_ClockDivision = TIM_CKD_DIV1;
    tim.TIM_CounterMode   = TIM_CounterMode_Up;
    TIM_TimeBaseInit(MB_TIMx, &tim);

    TIM_ClearFlag(MB_TIMx, TIM_FLAG_Update);
    TIM_ITConfig(MB_TIMx, TIM_IT_Update, ENABLE);

    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    nvic.NVIC_IRQChannel = MB_TIMx_IRQn;
    nvic.NVIC_IRQChannelPreemptionPriority = 1;
    nvic.NVIC_IRQChannelSubPriority        = 2;
    nvic.NVIC_IRQChannelCmd                = ENABLE;
    NVIC_Init(&nvic);

    return TRUE;
}

void vMBPortTimersEnable(void)
{
    TIM_SetCounter(MB_TIMx, 0);
    TIM_ClearITPendingBit(MB_TIMx, TIM_IT_Update);
    TIM_ITConfig(MB_TIMx, TIM_IT_Update, ENABLE);
    TIM_Cmd(MB_TIMx, ENABLE);
}

void vMBPortTimersDisable(void)
{
    TIM_ITConfig(MB_TIMx, TIM_IT_Update, DISABLE);
    TIM_ClearITPendingBit(MB_TIMx, TIM_IT_Update);
    TIM_Cmd(MB_TIMx, DISABLE);
}

void TIM6_IRQHandler(void)
{
    if (TIM_GetITStatus(MB_TIMx, TIM_IT_Update) != RESET) {
        TIM_ClearITPendingBit(MB_TIMx, TIM_IT_Update);
        prvTimerISR();
    }
}

portevent.c

  • The default FreeModbus portevent.c typically needs no modification; keep it as provided.

Optional: mbrtu.c first-byte seeding for TC-driven TX

  • When using the "transmission complete" (TC) interrupt instead of TXE, seed the first byte and enable TC so the ISR can continue sending via pxMBFrameCBTransmitterEmpty(). With TXE this is unnecessary.
/* In mbrtu.c, inside eMBRTUSend(...) before enabling transmitter */
/* ... existing buffer preparation ... */

eSndState = STATE_TX_XMIT;

/* Seed first byte so TC (or TXE) can chain the rest */
xMBPortSerialPutByte((CHAR)*pucSndBufferCur);
pucSndBufferCur++;
usSndBufferCount--;

vMBPortSerialEnable(FALSE, TRUE);

Main entry point

  • Initialize the stack, enable it, and poll in the main loop. UART and timer are owned by the port layer and do not require separate handling here.
#include "mb.h"

int main(void)
{
    /* Slave ID = 0x01, port index is ignored by this port (hard-wired to USART1) */
    eMBInit(MB_RTU, 0x01, 1, 9600, MB_PAR_NONE);
    eMBEnable();

    for (;;) {
        (void)eMBPoll();
    }
}

Modbus data model callbacks

  • Implement the four standard areas: coils, discrete inputs, holding registers, and input registers. The examples below use small in-memory buffers; adapt sizes and addresses to your application.

Coils (read/write bits)

#define COIL_ADDR_BASE   0
#define COIL_COUNT       100

static USHORT g_coilStart = COIL_ADDR_BASE;
static UCHAR  g_coilBits[COIL_COUNT] = {
    0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x10
};

eMBErrorCode eMBRegCoilsCB(UCHAR *pBuf, USHORT addr, USHORT numCoils, eMBRegisterMode mode)
{
    eMBErrorCode status = MB_ENOERR;
    USHORT byteIndex, bitOffset, nBytes;

    addr--; /* Modbus addresses are 1-based in function handler */

    if ((addr >= g_coilStart) && (addr + numCoils <= g_coilStart + COIL_COUNT)) {
        byteIndex = (addr - g_coilStart) / 8;
        bitOffset = (addr - g_coilStart) % 8;
        nBytes    = (numCoils + 7) / 8;

        if (mode == MB_REG_READ) {
            while (nBytes--) {
                *pBuf++ = xMBUtilGetBits(&g_coilBits[byteIndex++], bitOffset, 8);
            }
            /* Trim high bits of the last byte if coil count not multiple of 8 */
            if (numCoils % 8) {
                pBuf--;
                *pBuf &= (UCHAR)((1u << (numCoils % 8)) - 1u);
            }
        } else { /* MB_REG_WRITE */
            while (numCoils > 8) {
                xMBUtilSetBits(&g_coilBits[byteIndex++], bitOffset, 8, *pBuf++);
                numCoils -= 8;
            }
            if (numCoils) {
                xMBUtilSetBits(&g_coilBits[byteIndex], bitOffset, numCoils, *pBuf);
            }
        }
    } else {
        status = MB_ENOREG;
    }

    return status;
}

Discrete inputs (read-only bits)

#define DISC_ADDR_BASE   0
#define DISC_COUNT       100

static USHORT g_discStart = DISC_ADDR_BASE;
static UCHAR  g_discBits[DISC_COUNT] = {
    0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88,0x99,0xAA
};

eMBErrorCode eMBRegDiscreteCB(UCHAR *pBuf, USHORT addr, USHORT nDiscrete)
{
    eMBErrorCode status = MB_ENOERR;
    USHORT byteIndex, bitOffset, nBytes;

    addr--; /* 1-based to 0-based */

    if ((addr >= g_discStart) && (addr + nDiscrete <= g_discStart + DISC_COUNT)) {
        byteIndex = (addr - g_discStart) / 8;
        bitOffset = (addr - g_discStart) % 8;
        nBytes    = (nDiscrete + 7) / 8;

        while (nBytes--) {
            *pBuf++ = xMBUtilGetBits(&g_discBits[byteIndex++], bitOffset, 8);
        }
        if (nDiscrete % 8) {
            pBuf--;
            *pBuf &= (UCHAR)((1u << (nDiscrete % 8)) - 1u);
        }
    } else {
        status = MB_ENOREG;
    }

    return status;
}

Holding registers (read/write 16-bit)

#define HOLD_ADDR_BASE   0
#define HOLD_COUNT       100

static USHORT g_holdStart = HOLD_ADDR_BASE;
static USHORT g_holdBuf[HOLD_COUNT] = {0,1,2,3,4,5,6,7,8,9,10};

eMBErrorCode eMBRegHoldingCB(UCHAR *pBuf, USHORT addr, USHORT nRegs, eMBRegisterMode mode)
{
    eMBErrorCode status = MB_ENOERR;
    USHORT idx;

    addr--; /* already adjusted by stack code path; kept for clarity */

    if ((addr >= g_holdStart) && (addr + nRegs <= g_holdStart + HOLD_COUNT)) {
        idx = addr - g_holdStart;
        if (mode == MB_REG_READ) {
            while (nRegs--) {
                *pBuf++ = (UCHAR)(g_holdBuf[idx] >> 8);
                *pBuf++ = (UCHAR)(g_holdBuf[idx] & 0xFF);
                idx++;
            }
        } else { /* MB_REG_WRITE */
            while (nRegs--) {
                g_holdBuf[idx]  = ((USHORT)(*pBuf++) << 8);
                g_holdBuf[idx] |= ((USHORT)(*pBuf++));
                idx++;
            }
        }
    } else {
        status = MB_ENOREG;
    }

    return status;
}

Input registers (read-only 16-bit)

#define IN_ADDR_BASE   0
#define IN_COUNT       100

static USHORT g_inStart = IN_ADDR_BASE;
static USHORT g_inBuf[IN_COUNT] = {0,1,2,3,4,5,6,7,8,9,10};

eMBErrorCode eMBRegInputCB(UCHAR *pBuf, USHORT addr, USHORT nRegs)
{
    eMBErrorCode status = MB_ENOERR;
    USHORT idx;

    if ((addr >= g_inStart) && (addr + nRegs <= g_inStart + IN_COUNT)) {
        idx = addr - g_inStart;
        while (nRegs--) {
            *pBuf++ = (UCHAR)(g_inBuf[idx] >> 8);
            *pBuf++ = (UCHAR)(g_inBuf[idx] & 0xFF);
            idx++;
        }
    } else {
        status = MB_ENOREG;
    }

    return status;
}

Testing

  • Connect the board via the onboard USB–UART (USART1 on PA9/PA10) and use a PC Modbus master tool (e.g., Modbus Poll) to issue function codes 01/02/03/04.
  • Verify UART frames and timing with a logic analyzer or serial monitor.
  • Adjust baud rate, parity, or the TIM6 prescaler/ARR if the master reports timeouts.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.