Porting FreeModbus RTU Slave to STM32F103 (Keil) with USART1 and TIM6 Integration
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.