STM32 FreeModbus 移植(最详细)

FreeModbus 移植说明文档

1. 概述

本文档详细说明如何将 FreeModbus 库移植到基于 STM32F4 系列微控制器的嵌入式系统中。本移植基于 STM32CubeMX 生成的 HAL 库代码,并使用 FreeRTOS 作为操作系统。

移植环境:

  • 硬件平台:STM32F407xx
  • 开发环境:STM32CubeMX + Keil MDK / IAR / GCC
  • 操作系统:FreeRTOS
  • 通信接口:USART6 (RS485)
  • 定时器:TIM13

2. 硬件资源配置

2.1 UART 配置 (USART6)

使用 STM32CubeMX 配置 USART6 用于 Modbus RTU 通信:

CubeMX 配置参数:

  • Mode: Asynchronous
  • Baud Rate: 115200
  • Word Length: 8 bits
  • Stop Bits: 1
  • Parity: None (Modbus RTU 通常使用偶校验,但此处配置为 None,可在代码中调整)
  • Flow Control: None
  • DMA: 未使用 (使用中断方式)
  • 中断: 使能

引脚配置:

  • TX: PG14 (USART6_TX)
  • RX: PG9 (USART6_RX)
  • RTS/DE: PG8 (用于 RS485 收发控制)

代码生成:

CubeMX 会生成 MX_USART6_UART_Init() 函数,初始化 huart6 句柄。

2.2 定时器配置 (TIM13)

使用 STM32CubeMX 配置 TIM13 作为 Modbus 3.5 字符超时定时器:

CubeMX 配置参数:

  • Prescaler: 42 (得到 1MHz 计数频率,假设系统时钟 168MHz APB1 42MHz)
  • Counter Mode: Up
  • Period: 5000 (对应 5ms 超时,适用于 115200 波特率)
  • Clock Division: DIV1
  • Auto-reload Preload: Disable

超时计算:

对于 115200 波特率,1 位时间 = 1/115200 ≈ 8.68μs。

3.5 字符时间 ≈ 3.5 * 10 * 8.68μs ≈ 304μs。

此处设置 5ms 超时,留有足够余量(实际只要1.8ms足够)。

代码生成:

CubeMX 会生成 MX_TIM13_Init() 函数,初始化 htim13 句柄。

2.3 GPIO 配置

RS485 收发控制引脚:

  • PG8: 输出,推挽,上拉,用于控制 RS485 收发器的 DE/RE 引脚。
    • 高电平:发送模式
    • 低电平:接收模式

CubeMX 配置:

在 GPIO 配置中,将 PG8 设置为 GPIO_Output,并配置为推挽输出、上拉。

3. 软件架构

3.1 文件结构

复制代码
Core/
├── Src/
│   ├── main.c                    # 主程序,包含 Modbus 任务和回调函数
│   └── ...
├── freemodbus/
│   └── modbus/                   # FreeModbus 核心源码
│       ├── mb.c
│       ├── rtu/mbrtu.c
│       └── ...
Middlewares/
└── freemodbus/
    └── port/                     # 移植层代码
        ├── port.h
        ├── portserial.c
        ├── porttimer.c
        └── portevent.c

3.2 移植层实现

3.2.1 port.h - 移植头文件

定义数据类型和宏,适配 STM32 环境。

c 复制代码
#ifndef _PORT_H
#define _PORT_H

#include <assert.h>
#include <inttypes.h>

#define	INLINE                      inline
#define PR_BEGIN_EXTERN_C           extern "C" {
#define	PR_END_EXTERN_C             }

#define ENTER_CRITICAL_SECTION( )   // 临界区保护,可根据需要实现
#define EXIT_CRITICAL_SECTION( )    

typedef uint8_t BOOL;

typedef unsigned char UCHAR;
typedef char CHAR;

typedef uint16_t USHORT;
typedef int16_t SHORT;

typedef uint32_t ULONG;
typedef int32_t LONG;

#ifndef TRUE
#define TRUE            1
#endif

#ifndef FALSE
#define FALSE           0
#endif

#endif
3.2.2 portserial.c - 串口移植

实现串口初始化、发送、接收及中断处理。

关键函数:

  1. xMBPortSerialInit: 初始化串口,启动接收中断。
  2. vMBPortSerialEnable: 使能/禁用发送和接收。
  3. xMBPortSerialPutByte: 发送单个字节。
  4. xMBPortSerialGetByte: 接收单个字节。
  5. 中断回调函数 : 在 main.c 中实现 HAL_UART_RxCpltCallbackHAL_UART_TxCpltCallback

代码示例 (portserial.c):

c 复制代码
#include "cmsis_os2.h"
#include "port.h"
#include "mb.h"
#include "mbport.h"
#include "stm32f4xx_hal_gpio.h"
#include "stm32f4xx_hal_uart.h"
#include <stdint.h>

extern UART_HandleTypeDef huart6;

#define MBPORT_BUFFER_SIZE 128
CHAR ucBuffer[MBPORT_BUFFER_SIZE];
uint32_t bufReadPos = 0;
uint32_t bufWritePos = 0;

static volatile UCHAR * pucSndBufferCur;
static volatile USHORT usSndBufferCount;

void vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{
    if (xTxEnable) {
        HAL_GPIO_WritePin(GPIOG, GPIO_PIN_8, GPIO_PIN_SET);   // 发送使能
        osDelay(2);  // 等待 485 收发器稳定
        if (pxMBFrameCBTransmitterEmpty != NULL) {
            pxMBFrameCBTransmitterEmpty();
        }
    } else {
        HAL_GPIO_WritePin(GPIOG, GPIO_PIN_8, GPIO_PIN_RESET); // 接收使能
        osDelay(1);  // 等待 485 收发器稳定
        HAL_UART_AbortTransmit_IT(&huart6);
    }

    if (xRxEnable) {
        if (HAL_UART_GetState(&huart6) != HAL_UART_STATE_BUSY_RX) {
            HAL_UART_Receive_IT(&huart6, (uint8_t *)&ucBuffer[bufWritePos], 1);
        }
    } else {
        HAL_UART_AbortReceive(&huart6);
    }
}

BOOL xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )
{
    // UART 初始化由 CubeMX 生成的 MX_USART6_UART_Init() 完成
    // 此处只需启动接收中断
    HAL_UART_Receive_IT(&huart6, (uint8_t *)&ucBuffer[bufWritePos], 1);
    return TRUE;
}

BOOL xMBPortSerialPutByte( CHAR ucByte )
{
    pucSndBufferCur = (UCHAR *)&ucByte;
    usSndBufferCount = 1;
    HAL_UART_Transmit_IT(&huart6, (uint8_t *)pucSndBufferCur, 1);
    return TRUE;
}

BOOL xMBPortSerialGetByte( CHAR * pucByte )
{
    *pucByte = ucBuffer[bufReadPos];
    bufReadPos++;
    if (bufReadPos >= MBPORT_BUFFER_SIZE) {
        bufReadPos = 0;
    }
    return TRUE;
}

中断回调函数 (main.c):

c 复制代码
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART6) {
        // 增加缓冲区写指针
        bufWritePos++;
        if (bufWritePos >= MBPORT_BUFFER_SIZE) {
            bufWritePos = 0;
        }
        
        // 调用 Modbus 回调
        if (pxMBFrameCBByteReceived != NULL) {
            pxMBFrameCBByteReceived();
        }
        
        // 重启接收下一个字节
        HAL_UART_Receive_IT(&huart6, (uint8_t *)&ucBuffer[bufWritePos], 1);
    }
}

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART6) {
        // 通知 Modbus 栈发送器已空
        if (pxMBFrameCBTransmitterEmpty != NULL) {
            pxMBFrameCBTransmitterEmpty();
        }
    }
}
3.2.3 porttimer.c - 定时器移植

实现 Modbus 3.5 字符超时定时器。

关键函数:

  1. xMBPortTimersInit: 初始化定时器(此处由 CubeMX 完成,返回 TRUE)。
  2. vMBPortTimersEnable: 启动定时器。
  3. vMBPortTimersDisable: 停止定时器。
  4. 中断回调函数 : 在 main.c 中实现 HAL_TIM_PeriodElapsedCallback

代码示例 (porttimer.c):

c 复制代码
#include "port.h"
#include "mb.h"
#include "mbport.h"
#include "stm32f4xx_hal_tim.h"

extern TIM_HandleTypeDef htim13;

BOOL xMBPortTimersInit( USHORT usTim1Timerout50us )
{
    // 定时器初始化由 CubeMX 生成的 MX_TIM13_Init() 完成
    return TRUE;
}

inline void vMBPortTimersEnable(  )
{
    HAL_TIM_Base_Start_IT(&htim13);
}

inline void vMBPortTimersDisable(  )
{
    HAL_TIM_Base_Stop_IT(&htim13);
}

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

中断回调函数 (main.c):

c 复制代码
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM6)
    {
        HAL_IncTick();
    }
    else if (htim->Instance == TIM13) {
        if (pxMBPortCBTimerExpired != NULL) {
            pxMBPortCBTimerExpired(  );
        }
    }
}
3.2.4 portevent.c - 事件移植

实现 Modbus 事件队列(用于 RTU 模式)。

代码示例 (portevent.c):

c 复制代码
#include "mb.h"
#include "mbport.h"

static eMBEventType eQueuedEvent;
static BOOL     xEventInQueue;

BOOL xMBPortEventInit( void )
{
    xEventInQueue = FALSE;
    return TRUE;
}

BOOL xMBPortEventPost( eMBEventType eEvent )
{
    xEventInQueue = TRUE;
    eQueuedEvent = eEvent;
    return TRUE;
}

BOOL xMBPortEventGet( eMBEventType * eEvent )
{
    BOOL            xEventHappened = FALSE;

    if( xEventInQueue )
    {
        *eEvent = eQueuedEvent;
        xEventInQueue = FALSE;
        xEventHappened = TRUE;
    }
    return xEventHappened;
}

4. 主程序集成

4.1 Modbus 任务创建

main.c 中创建 Modbus 任务,并初始化 Modbus 栈。

c 复制代码
void Modbus_task(void *argument)
{
  // 初始化 Modbus 协议栈 (RTU 模式)
  // 从站地址: 0x01, 端口: 1 (USART6), 波特率: 115200, 校验: 偶校验
  eMBErrorCode eStatus = eMBInit(MB_RTU, 0x01, 6, 115200, MB_PAR_EVEN);
  
  if (eStatus != MB_ENOERR) {
      printf("Modbus initialization failed with error: %d\n", eStatus);
      Error_Handler();
  }
  
  // 使能 Modbus 协议栈
  eStatus = eMBEnable();
  if (eStatus != MB_ENOERR) {
      printf("Modbus enable failed with error: %d\n", eStatus);
      Error_Handler();
  }
  
  // 初始化 Modbus 寄存器缓冲区
  memset(usRegInputBuf, 0, sizeof(usRegInputBuf));
  memset(usRegHoldingBuf, 0, sizeof(usRegHoldingBuf));
  memset(ucCoilBuf, 0, sizeof(ucCoilBuf));
  memset(ucDiscreteInputBuf, 0, sizeof(ucDiscreteInputBuf));
  
  printf("Modbus RTU slave initialized with address 0x01\n");
  
  /* 无限循环 */
  for(;;)
  {
    // 调用 Modbus 协议栈的主轮询函数
    eMBPoll();
    
    // 延迟 1ms 以允许其他任务运行
    osDelay(1);
  }
}

注意: eMBInit 的第三个参数是端口号,此处应为 6(对应 USART6),而不是 1。

4.2 寄存器回调函数

实现 Modbus 寄存器读写回调函数。

输入寄存器 (30001-30004):

c 复制代码
eMBErrorCode eMBRegInputCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs )
{
    // 实现读取输入寄存器的逻辑
    // ...
}

保持寄存器 (40001-40016):

c 复制代码
eMBErrorCode eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode )
{
    // 实现读写保持寄存器的逻辑
    // ...
}

线圈 (00001-00016):

c 复制代码
eMBErrorCode eMBRegCoilsCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNCoils, eMBRegisterMode eMode )
{
    // 实现读写线圈的逻辑
    // ...
}

离散输入 (10001-10013):

c 复制代码
eMBErrorCode eMBRegDiscreteCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNDiscrete )
{
    // 实现读取离散输入的逻辑
    // ...
}

5. 编译与调试

5.1 编译配置

确保在 Makefile 或项目配置中包含以下路径:

复制代码
C_INCLUDES += -ICore/freemodbus/modbus/include
-IMiddlewares/freemodbus/port/
-ICore/freemodbus/modbus/rtu
-ICore/freemodbus/modbus/ascii



C_COURCES += Core/freemodbus/modbus/mb.c \
Core/freemodbus/modbus/rtu/mbrtu.c \
Core/freemodbus/modbus/rtu/mbcrc.c \
Core/freemodbus/modbus/ascii/mbascii.c \
Core/freemodbus/modbus/functions/mbfunccoils.c \
Core/freemodbus/modbus/functions/mbfuncdiag.c \
Core/freemodbus/modbus/functions/mbfuncholding.c \
Core/freemodbus/modbus/functions/mbfuncinput.c \
Core/freemodbus/modbus/functions/mbfuncother.c \
Core/freemodbus/modbus/functions/mbfuncdisc.c \
Core/freemodbus/modbus/functions/mbutils.c \
Middlewares/freemodbus/port/portevent.c \
Middlewares/freemodbus/port/porttimer.c \
Middlewares/freemodbus/port/portserial.c \

5.2 调试技巧

  1. 检查中断回调: 确保 HAL_UART_RxCpltCallbackHAL_TIM_PeriodElapsedCallback 被正确调用。
  2. 缓冲区溢出: 监控 bufWritePosbufReadPos,确保缓冲区大小足够。
  3. 超时定时器: 使用逻辑分析仪或示波器检查 TIM13 的输出,确保超时时间正确。
  4. RS485 控制: 检查 PG8 引脚电平,确保收发切换正常。

6. 常见问题

6.1 无法接收数据

  • 检查 UART 中断是否使能。
  • 检查 RS485 收发控制引脚是否正确配置。
  • 检查缓冲区大小是否足够。

6.2 超时不准确

  • 检查 TIM13 的时钟配置和预分频器。
  • 调整 Period 值以匹配波特率。

6.3 Modbus 无响应

  • 检查从站地址是否正确。
  • 检查寄存器地址范围是否匹配。
  • 检查回调函数是否正确实现。

7. 总结

本文档详细说明了 FreeModbus 在 STM32F4 平台上的移植过程,包括硬件配置、软件架构、代码实现和调试技巧。通过遵循本文档,开发者可以快速将 FreeModbus 移植到新的 STM32 设备上。

相关推荐
weiyvyy2 小时前
嵌入式硬件接口开发的流程
人工智能·驱动开发·单片机·嵌入式硬件·硬件架构·硬件工程
weiyvyy2 小时前
嵌入式硬件接口开发的核心原则
驱动开发·单片机·嵌入式硬件·fpga开发·硬件架构·硬件工程
Heartache boy2 小时前
野火STM32_HAL库版课程笔记-TIM通用定时器基础中断应用
笔记·stm32·单片机·嵌入式硬件
Vinegar �2 小时前
Cmos sensor的一些定义
单片机·嵌入式硬件
CODE_RabbitV3 小时前
【保姆级实操版 - STM32 系列笔记】新手入门STM32第一课:CubeMX+Keil MDK实现LED点灯
笔记·stm32·嵌入式硬件
是翔仔呐3 小时前
第6章 UART串口通信!掌握单片机与外界的双向数据通道,实现跨设备交互
c语言·开发语言·单片机·嵌入式硬件·gitee
Nice__J3 小时前
Mcu架构以及原理——4.时钟系统
单片机·嵌入式硬件·架构
慕诗客3 小时前
VSCODE+EIDE编译和下载单片机程序
ide·vscode·单片机
我不吃西红柿k3 小时前
stm32新建项目编译即出现大量报错,如何解决
stm32·单片机·嵌入式硬件