
简介本资源是一套面向嵌入式开发工程师与工业自动化项目实践者的STM32单片机MODBUS RTU从站完整软件例程解决基于RS485物理层的工业现场设备接入Modbus主站网络的核心通信问题适用于传感器数据采集、执行器控制等典型工控场景要求使用者具备STM32基础编程能力及Modbus协议理解能力。压缩包共669个文件涵盖90个C源文件含串口驱动、Modbus帧解析、寄存器映射等核心逻辑、115个头文件定义功能码、数据结构及硬件抽象层、216个HTML文档含详细API说明与流程图解、129张PNG图表展示协议时序、内存布局与调试界面以及配套的编译脚本.bat、工程配置.uvproj/.ewp和固件镜像.hex整体大小为2.6MB。已有123人学习下载。读者可直接部署运行快速实现符合Modbus RTU规范的从站响应包含完整的异常处理机制校验失败、超时重传、地址非法等、可配置的从站地址与波特率参数并支持主流STM32F1系列芯片代码模块清晰、注释详尽便于二次开发与协议扩展。1. 项目概述与核心价值最近在整理资料时翻出了一个压箱底的STM32 RS485 MODBUS从站软件例程源码。这个项目是我几年前为一个工业数据采集终端做的核心通信模块当时为了满足客户对稳定性和实时性的苛刻要求前后调试了将近两个月踩了不少坑也积累了不少实战经验。今天把它拿出来结合最新的开发环境比如STM32CubeIDE和HAL库重新梳理一遍希望能给正在或即将涉足工业通信领域的工程师朋友们提供一个清晰、可靠、可直接复用的参考方案。简单来说这个项目实现了一个运行在STM32单片机上的MODBUS RTU从站。它通过RS485物理接口响应来自上位机如PC上的MODBUS Poll软件、PLC或触摸屏的读写请求操作单片机内部的寄存器可以映射到实际的内存变量、ADC采样值、GPIO状态等。对于嵌入式开发者而言掌握MODBUS从站开发是进入工业自动化、智能楼宇、环境监控等领域的敲门砖。这个源码包的价值在于它不仅仅是一堆代码更是一个经过实际项目验证的、包含了硬件设计考量、协议栈实现、数据帧处理、异常处理等完整环节的解决方案。无论你是想快速实现产品原型还是希望深入理解MODBUS协议在单片机上的落地细节这份例程都能提供一个坚实的起点。2. 整体设计与思路拆解2.1 为什么选择MODBUS RTU over RS485在工业现场通信方案的选择首要考虑的是可靠、简单和通用。MODBUS协议以其开源、标准、易于实现的特性成为了事实上的工业通信标准。而RTU远程终端单元模式采用二进制编码在同样的波特率下比ASCII模式传输效率更高更节省带宽。RS485则是一种差分信号传输标准具有抗干扰能力强、传输距离远可达千米级、支持多点通信一主多从的优点。将MODBUS RTU运行在RS485物理层上构成了工业领域最常见、最经典的“黄金组合”。我们的STM32作为从站设备就是挂接在这样一条RS485总线上等待主站的轮询。2.2 软件架构核心思想整个软件例程的设计围绕“事件驱动”和“状态机”两个核心思想展开。这并不是一个庞大复杂的操作系统而是在裸机或配合简单的调度器环境下构建的一个高效、清晰的程序结构。硬件抽象层HAL驱动利用STM32CubeMX生成的HAL库代码初始化USART用于RS485收发、定时器用于MODBUS帧超时判断、GPIO用于控制RS485收发器的方向引脚DE/RE。这是所有功能的基础。数据链路层串口驱动与帧处理USART配置为异步模式开启接收中断和空闲中断。这是整个通信的“发动机”。空闲中断是MODBUS RTU帧分割的关键当总线空闲时间超过3.5个字符时间时认为一帧数据接收完毕触发中断进行后续处理。协议解析层MODBUS状态机这是代码的核心。它从接收缓冲区中取出完整的一帧数据进行CRC校验。校验通过后解析功能码如0x03读保持寄存器、0x06写单个寄存器和地址信息。然后根据功能码跳转到对应的处理函数访问事先定义好的“寄存器映射表”。应用层寄存器映射这是用户最需要关心的部分。我们会在内存中定义一个结构体数组或一片连续的区域作为MODBUS的保持寄存器Holding Register。例如holding_reg[0]映射到设备温度holding_reg[1]映射到设备状态。协议解析层读写的就是这片内存而用户的应用程序如ADC采样、逻辑控制则负责更新这片内存的值。注意这里有一个关键设计取舍——是否使用DMA。对于高波特率如115200以上或频繁通信的场景使用DMA空闲中断可以极大减轻CPU负担避免数据覆盖。但对于初学者或通信量不大的应用使用中断缓冲区的方式更为直观也足以应对大多数9600bps的场合。本例程将先阐述中断方案并在后续补充DMA方案的升级要点。2.3 关键外设与引脚规划以常见的STM32F103系列为例我们需要规划以下硬件资源USART选择其中一个支持硬件流控的串口如USART2、USART3用于RS485的数据收发RX/TX。定时器选择一个基本定时器如TIM6/TIM7用于精确计算3.5个字符的超时时间。这是实现RTU帧分割的“第二道保险”比单纯依赖空闲中断更稳健。GPIO一个普通的输出引脚用于控制RS485收发器如MAX3485的使能端DE和RE通常短接。高电平时单片机处于发送模式驱动总线低电平时处于接收模式监听总线。缓冲区在内存中定义两个环形缓冲区或线性缓冲区加索引一个用于串口接收原始数据一个用于存放已解析的完整MODBUS帧。3. 核心模块实现与代码解析3.1 硬件驱动初始化使用STM32CubeMX可以快速完成图形化配置。这里强调几个关键配置点USART配置模式异步Asynchronous。波特率9600工业常用可根据需要调整。数据位8。停止位1。校验位无MODBUS RTU协议层自带CRC校验。高级配置必须使能“串口空闲中断”Idle Interrupt和“接收寄存器非空中断”RXNE Interrupt。这是实现帧接收的硬件基础。定时器配置选择一个基本定时器TIM6。预分频器PSC和自动重载值ARR需要根据系统主频计算以产生一个精确的计时基准例如1ms中断一次。我们将用它来累加计算总线空闲时间。控制引脚配置将一个GPIO如PA8配置为推挽输出模式初始化为低电平接收状态。我们将其命名为RS485_DE_RE_PIN。生成的代码骨架已经有了我们需要在合适的地方添加自己的逻辑。3.2 MODBUS从站状态机实现这是整个例程的“大脑”。我们将其实现为一个被主循环或定时器中断周期性调用的函数MODBUS_Handler()。其内部是一个大的switch-case状态机。typedef enum { MB_STATE_IDLE, // 空闲状态等待帧开始 MB_STATE_RECEIVING, // 正在接收数据 MB_STATE_PROCESSING, // 帧接收完成正在处理 MB_STATE_RESPONDING // 处理完毕正在组织并发送响应帧 } MB_StateTypeDef; MB_StateTypeDef mbState MB_STATE_IDLE; uint8_t rxBuffer[256]; // 串口接收原始缓冲区 uint16_t rxIndex 0; uint32_t lastCharTime 0; // 上次收到字符的时间戳以定时器tick计 uint8_t frameBuffer[256]; // 完整帧缓冲区 uint16_t frameLength 0;状态迁移逻辑MB_STATE_IDLE当串口收到第一个字节触发RXNE中断时将字节存入rxBuffer[0]rxIndex设为1lastCharTime更新为当前时间状态跳转到MB_STATE_RECEIVING。同时启动或重置那个用于计算3.5字符超时的定时器。MB_STATE_RECEIVING后续每个字节通过RXNE中断存入rxBuffer[rxIndex]并不断更新lastCharTime。关键事件1串口空闲中断触发。这表明总线空闲已超过1个字符时间是一个强烈的“帧结束”信号。在空闲中断服务函数中我们可以直接将状态从 RECEIVING 改为MB_STATE_PROCESSING。关键事件2定时器超时3.5字符时间到。定时器中断服务函数中检查当前状态是否为RECEIVING并且(当前时间 - lastCharTime) 3.5字符时间。如果成立也认为一帧结束状态改为MB_STATE_PROCESSING。这是防止空闲中断未正确触发的后备机制。MB_STATE_PROCESSING在主循环中状态机发现状态变为PROCESSING开始处理。将rxBuffer中长度为rxIndex的数据复制到frameBuffer。CRC校验计算frameBuffer中除最后两个CRC字节外的所有数据的CRC16值与帧中自带的CRC进行比较。校验失败则丢弃该帧状态重置为MB_STATE_IDLE并清空缓冲区。解析功能码与地址校验通过后解析从站地址第一个字节、功能码第二个字节。检查从站地址是否与本机地址匹配或广播地址0x00。调用功能处理函数根据功能码调用对应的函数如MODBUS_ReadHoldingRegistersMODBUS_WriteSingleRegister。这些函数会操作我们定义的寄存器映射数组。组织响应数据对于读请求需要读取数据对于写请求需要回显写入的值。将响应数据、从站地址、功能码重新计算CRC组成响应帧放入发送缓冲区。状态跳转到MB_STATE_RESPONDING。MB_STATE_RESPONDING将RS485控制引脚拉高切换到发送模式。通过HAL库的发送函数如HAL_UART_Transmit或发送中断将响应帧发送出去。发送完成后在发送完成中断或回调函数中将RS485控制引脚拉低切换回接收模式。状态重置为MB_STATE_IDLE清空所有缓冲区和索引准备接收下一帧。实操心得RS485收发切换的时序。这是一个经典的坑点。必须在开始发送第一个字节之前就将DE/RE引脚拉高确保发送器有效。同时必须在最后一个字节的发送完成中断TC触发后再延时一小段时间例如发送1-2个字节的时间再将引脚拉低。如果拉低过早最后一个字节的停止位可能还未完全送出导致波形畸变。最稳妥的方法是在TC中断中启动一个短延时定时器如1ms在定时器回调中拉低引脚。3.3 寄存器映射与功能处理函数这是用户业务逻辑与MODBUS协议的接口。我们定义一个全局数组作为保持寄存器#define HOLDING_REG_NUM 100 uint16_t holdingRegisters[HOLDING_REG_NUM] {0};然后实现几个最常用的功能码处理函数示例处理读保持寄存器功能码0x03请求帧格式[从站地址][0x03][起始地址高][起始地址低][寄存器数量高][寄存器数量低][CRC低][CRC高]MB_ErrorTypeDef MODBUS_ReadHoldingRegisters(uint8_t *pFrame, uint16_t *len) { uint16_t startAddr (pFrame[2] 8) | pFrame[3]; // 合并高8位和低8位 uint16_t regNum (pFrame[4] 8) | pFrame[5]; uint8_t response[256]; uint16_t respIndex 0; // 1. 参数检查 if ((startAddr regNum) HOLDING_REG_NUM) { return MB_ERROR_ILLEGAL_DATA_ADDRESS; // 返回非法数据地址错误 } if (regNum 0 || regNum 125) { // MODBUS协议规定一次最多读125个寄存器 return MB_ERROR_ILLEGAL_DATA_VALUE; } // 2. 组织响应数据 response[respIndex] slaveAddress; // 从站地址 response[respIndex] 0x03; // 功能码 response[respIndex] regNum * 2; // 字节数 寄存器数 * 2 for (int i 0; i regNum; i) { response[respIndex] (holdingRegisters[startAddr i] 8) 0xFF; // 高字节 response[respIndex] holdingRegisters[startAddr i] 0xFF; // 低字节 } // 3. 计算CRC并附加到响应帧末尾 uint16_t crc CalculateCRC16(response, respIndex); response[respIndex] crc 0xFF; response[respIndex] (crc 8) 0xFF; // 4. 将响应帧拷贝到发送缓冲区并设置长度 memcpy(txBuffer, response, respIndex); *len respIndex; return MB_ERROR_NONE; }示例处理写单个寄存器功能码0x06请求帧格式[从站地址][0x06][寄存器地址高][寄存器地址低][寄存器值高][寄存器值低][CRC低][CRC高]这个功能码的响应就是原样回显请求帧所以处理起来更简单检查地址是否合法如果合法则将请求帧中的值写入holdingRegisters数组对应位置然后将整个请求帧作为响应帧发回即可。4. 关键问题排查与实战经验在实际调试中通信不通是最常见的问题。下面是一个系统性的排查清单和我的实战经验。4.1 通信完全无响应硬件检查第一接线RS485是A/B差分线。确保所有设备的A接AB接B。通常A接“”B接“-”但不同厂家标识可能相反如果不对调换一下试试。一定要有终端电阻120Ω接在总线最远两端的A-B之间消除信号反射。电源与共地确保所有设备共地这是差分信号参考的基础。检查MCU和RS485收发器的供电是否稳定。收发器方向控制用逻辑分析仪或示波器看控制引脚DE/RE的波形。发送数据前是否为高电平发送完成后是否延时拉低这是最易出错的地方。软件配置检查波特率、数据位、停止位必须与主站严格一致。用示波器测量一个字节的时长反算波特率是否准确。中断是否开启确认USART的RXNE和Idle中断在NVIC中已使能。从站地址用MODBUS Poll等工具测试时设置的从站地址是否与代码中slaveAddress一致尝试使用地址0x00广播地址从站不应响应广播或0xFF测试。4.2 能收到请求但CRC错误或响应异常帧结构错乱3.5字符超时不准这是RTU模式的核心。计算3.5个字符时间T 3.5 * (1 8 1) / BaudRate。以9600波特率为例一个字符11位1起始8数据1停止1校验无校验则10位T 3.5 * 11 / 9600 ≈ 4ms。你的定时器精度是否足够超时时间设置是否大于这个值缓冲区溢出如果主站发送过快或从站处理太慢可能导致接收缓冲区被新数据覆盖。增大缓冲区或在处理期间暂时关闭接收中断。CRC计算错误确保CRC计算的初始值、多项式与标准MODBUS0xFFFF 多项式0xA001一致。确保计算范围正确对于接收计算除最后两个字节外的所有字节对于发送计算整个响应帧包括地址、功能码、数据后再附加CRC。响应数据错误字节序问题MODBUS协议规定寄存器数据是大端序Big-Endian即高字节在前。而STM32内存是小端序。在组织响应帧时必须手动将uint16_t类型的数据拆成高8位和低8位按顺序放入缓冲区。地址映射错误MODBUS的寄存器地址通常从0开始而你的数组索引也从0开始。但有些主站软件或协议规定地址从1开始如地址40001对应寄存器0。需要在代码中做好偏移转换。4.3 稳定性与抗干扰优化增加软件超时除了帧间超时3.5字符还应增加一个“帧内超时”。如果在接收一个帧的过程中两个字符之间的间隔超过1.5个字符时间则认为帧错误丢弃当前数据并复位状态机。这可以应对总线上的干扰毛刺。发送失败重试机制在发送响应后可以短暂切换到接收模式尝试接收自己发出的数据自发自收进行比对。如果比对失败可以考虑在应用层协议上约定重发机制虽然标准MODBUS没有但可在私有协议中扩展。看门狗与状态监控将MODBUS状态机的主处理函数放在主循环中并确保循环能够定期执行。如果因为某种原因卡死在某个状态独立看门狗IWDG可以复位系统。同时可以定义一个“通信健康”计数器每次成功完成一次请求-响应交互就清零否则累加。当该计数器超过阈值时可以触发一个故障标志供上位机查询。5. 从例程到产品进阶优化建议当你掌握了基础例程后可以考虑以下优化让代码更健壮、更高效、更易于维护。5.1 引入DMA进行数据搬运对于高波特率或需要同时处理其他繁重任务的系统使用DMA是必由之路。配置思路如下接收将USART的RX引脚配置为DMA模式循环Circular模式。DMA会自动将接收到的数据搬运到你指定的大环形缓冲区中。你只需要在串口空闲中断中计算本次空闲中断与上次之间DMA搬运了多少个数据到缓冲区这段数据就是一帧。这种方式完全解放了CPU且不易丢数据。发送将USART的TX引脚配置为DMA模式普通Normal模式。发送时启动DMA传输并在DMA传输完成中断中拉低RS485方向引脚并清理状态。踩坑记录使用DMA接收时务必注意缓冲区的对齐和长度。如果DMA目标缓冲区长度不是4字节对齐在某些型号的STM32上可能会出现奇怪的问题。另外在空闲中断中计算接收长度时需要读取DMA的剩余数据寄存器__HAL_DMA_GET_COUNTER来反推已接收数据量这个计算要小心处理边界条件环形缓冲区的头尾。5.2 设计优雅的寄存器映射接口直接操作全局数组不够灵活。可以设计一个寄存器映射表每个条目包含寄存器地址、数据类型16位、32位浮点数、字符串等、读写权限、以及指向实际变量的指针或回调函数。typedef enum { REG_U16, REG_S32, REG_FLOAT } RegDataType; typedef enum { REG_RO, REG_WO, REG_RW } RegAccessType; typedef struct { uint16_t addr; RegDataType dataType; RegAccessType access; void *pData; // 指向实际数据的指针 int32_t (*readCallback)(uint16_t addr); // 读回调用于动态数据 bool (*writeCallback)(uint16_t addr, int32_t value); // 写回调用于执行动作 } ModbusRegEntry_t; ModbusRegEntry_t regMap[] { {0, REG_U16, REG_RW, g_systemVoltage, NULL, NULL}, {1, REG_S32, REG_RO, NULL, readTemperature, NULL}, {2, REG_U16, REG_WO, NULL, NULL, setRelayOutput}, // ... 更多寄存器 };这样协议解析层只需要遍历这个表找到匹配的地址然后根据数据类型和访问权限调用相应的回调函数或直接操作内存代码的扩展性和可维护性会大大提高。5.3 集成到RTOS中如果项目使用了FreeRTOS或类似的实时操作系统可以将MODBUS任务化。创建一个独立任务MODBUS_Task其优先级设置为中等。使用消息队列串口空闲中断或DMA完成中断向任务发送一个消息例如一个信号量或直接发送帧数据指针通知任务有新的数据帧需要处理。任务阻塞在接收消息的队列上。状态机放在任务中任务被唤醒后执行之前在主循环中的状态机逻辑处理帧并发送响应。这样通信处理变成了一个并发的、可阻塞的任务不影响其他关键任务如运动控制、高速采样的执行。注意资源共享寄存器映射表是共享资源如果被多个任务访问需要使用互斥锁Mutex进行保护。这份“STM32单片机-RS485 MODBUS从站软件例程源码”的核心骨架和思想已经基本阐述完毕。从硬件初始化到协议解析从状态机设计到问题排查我希望它不仅仅是一份可以编译通过的代码更是一份能让你理解工业通信底层逻辑的设计文档。在实际项目中你可能还会遇到多从站地址管理、协议扩展如支持03/04/06/16等更多功能码、与实时传感器数据同步等问题但只要你掌握了这里提到的核心框架和调试方法所有这些问题都将有迹可循。最后建议你亲手用开发板和USB转485适配器搭建一个最简单的测试环境用MODBUS Poll作为主站进行测试从点亮第一个寄存器开始逐步构建起自己的稳定通信系统。本文还有配套的精品资源点击获取