简介:STM32F103主机从机源代码例程,面向嵌入式开发者和自动化工程人员,解决RS485总线上的Modbus RTU主从机通信设计与实现问题。包内提供主机与从机两套完整工程,基于STM32标准外设库,涉及定时器、串口、RCC、Flash等驱动模块,可直接在Keil中编译下载,便于理解Modbus报文解析、CRC校验、地址映射及RS485收发切换等关键环节。资源共157个文件,其中包含37个头文件与36个C源文件,另有工程配置文件、编译中间文件、说明文档和烧录hex,压缩包整体4.57MB,文件类型涵盖C源码、头文件、工程配置、编译中间产物与输出镜像,便于完整了解工程构建过程。目录按主机、从机分列,源码结构清晰,适合入门到进阶的嵌入式学习者参考。目前已有1014人学习下载,对于需要快速搭建RS485 Modbus验证平台的读者来说,是一份可直接改用的模板与范例。 拿到这套“STM32F103主机从机源代码例程序范例RS485 ModbusRTU通讯协议资料.zip”的时候,我其实挺意外的。挂在网盘里看着像是个普普通通的例程包,解压完才发现内容比标题实在得多:主机工程、从机工程、协议封装、通讯调试说明全给齐了。如果你正准备在项目里上ModbusRTU,手头又是F103这颗芯片,这套资料能帮你省掉至少一周的摸索时间。这篇文章我打算从实际工程角度,把这套资料的价值拆透,把主从机代码的结构、485硬件要点和联调时最容易踩的坑一并讲清楚。
网上很多类似的“例程包”只是把官方库函数堆在一起,告诉你“能跑就行”。但这套不一样,它是能直接往项目里搬的——主机怎么发请求、从机怎么解析帧、CRC怎么算、方向脚怎么切,全部是完整的工程逻辑。为了让你看得更明白,我会把主机和从机之间的关系、代码里每个模块的作用、485电路的设计思路,以及联调时常见的“分别测试都正常,连起来就不通”这类玄学问题,一层一层剥开讲。
1. 玩转这套资料前,先把ModbusRTU主从关系理清楚
1.1 主机从机不是强弱关系,是“一问一答”
很多第一次接触Modbus的人会把“主机”和“从机”理解成上位机和下位机的关系,以为主机就一定更高级。实际上在ModbusRTU体系里,主机(Master)和从机(Slave)的区别只是“谁先开口说话”。
主机拥有总线的发起权,它主动发送请求帧,总线上的从机根据帧里的地址字段判断这条请求是不是发给自己的。如果不是,忽略;如果是,执行操作并把响应帧发回总线上。主机和从机之间是严格的“一问一答”模式,主机不发话,从机绝对不能主动往总线上丢数据。这就是为什么ModbusRTU能在两根线上挂几十个设备而不乱套——它们靠“令牌式”的轮流发言保持秩序。
这套资料里,主机源代码是一个完整的轮询控制器,从机源代码则是带寄存器映射的响应器。你不需要改动太多,就能把它们分别烧进两块STM32F103核心板,组成一个最小通讯系统。
1.2 RS485为什么是工业现场的首选物理层
ModbusRTU是应用层协议,RS485是物理层传输标准。这两者经常成对出现,是因为RS485的特性太适合工业现场了:
- 差分传输,抗共模干扰能力强。两根线(A和B)上的电压差表示逻辑0和1,外部干扰同时叠加到两根线上时,差值为零,不产生误码。
- 支持多点组网。标准RS485收发器(比如SP3485、MAX485)能挂32个节点,配上高输入阻抗的芯片可以挂更多。
- 传输距离远。9600波特率下最远能到1200米左右,如果用专用电缆和隔离,距离和稳定性还能再往上走。
但RS485有一个关键特性:半双工。同一时刻只能有一个设备在发送,其他设备都在接收。这就要求发送节点必须能控制自己的收发器方向,而这恰恰是“分别测试正常、连起来不正常”的头号元凶。
| 参数 | RS485典型值 | 说明 |
|---|---|---|
| 传输方式 | 差分、半双工(也可以配置为全双工,但ModbusRTU用半双工) | 2线制:A、B |
| 节点容量 | 标准32节点/总线 | 高阻抗芯片可到256节点 |
| 最大速率 | 10Mbps(短距离) | 实际工控常用9600/19200bps |
| 最大距离 | 1200米@9600bps | 超过后需加中继或降低波特率 |
| 终端电阻 | 120欧姆(总线两端各一个) | 匹配特征阻抗,减少反射 |
2. 主机程序和从机程序怎么组织,关键代码怎么调
2.1 从机工程:把“回答”做成状态机
从机程序的核心是“收到请求 - 校验合法性 - 执行操作 - 返回响应”。这套资料的从机代码把整个流程做成了串口中断 + 状态机,没有用阻塞式等待,CPU占用极低。
具体结构大概是这样的:
- 串口1接收中断:把每个字节丢进接收缓冲区,同时重启一个字节超时定时器。
- 定时器中断:如果在3.5个字符时间内没有新的字节到来,认为一帧数据接收完毕,置位“帧完成”标志。
- 主循环或后台任务:检测到帧完成标志后,调用协议解析函数。
- 协议解析:先从缓冲区取出帧,计算CRC16校验,比对地址域,确认是本机地址后执行功能码对应的操作。
串口初始化的关键点是波特率、数据位、校验位、停止位必须和主机保持一致。这套源码默认是9600波特率,8数据位,无校验,1停止位(9600,8,N,1),这也是ModbusRTU最通用的配置。如果你要改成19200或者偶校验,注意ModbusRTU在带校验时,数据位要设置为9位(校验位占1位),否则双方对不上。
CRC16是ModbusRTU的帧尾,由从第一个地址字节到最后一个数据字节的所有内容计算得出。计算用的多项式是0x8005,初值为0xFFFF,结果低字节在前。工程里提供了查表法和逐位计算两种实现,查表法快,逐位计算省空间,实际项目中我建议直接用查表法,速度优势明显,代码也简单。
// CRC16-MODBUS查表法的核心计算逻辑 uint16_t ModbusCRC16(uint8_t *data, uint16_t len) { uint16_t crc = 0xFFFF; uint16_t i; uint8_t j; for (i = 0; i < len; i++) { crc ^= data[i]; for (j = 0; j < 8; j++) { if (crc & 0x0001) crc = (crc >> 1) ^ 0xA001; else crc >>= 1; } } return crc; }从机的寄存器映射也是这套源码的亮点。它把保持寄存器(Holding Register)定义成了一个数组,通过索引读写。比如保持寄存器0x0000存设备地址、0x0001存运行状态、0x0002存转速给定值。配合功能码03(读保持寄存器)和06(写单个寄存器),上位机或主机就可以远程读写这些变量了。
从机代码里值得注意的还有异常处理。如果收到功能码不支持、寄存器地址越界、写入值非法,从机会返回异常帧,异常码分别为01、02、03。可能你初学时会觉得“返回异常帧好像不必要”,但实际上这是调试的大利器——主机收到异常帧,就能精确定位问题出在哪个环节。
2.2 主机工程:一次完整读写的调用链
主机就是主动方。这套资料的主机代码里封装了几个API:读保持寄存器、写单个寄存器、写多个寄存器。函数的传入参数是从机地址、起始寄存器地址、寄存器数量,传出参数是读取回来的数据。
一次完整的读操作调用链是这样的:
// 示例:读取从机1,起始寄存器地址0x0000,读取2个寄存器 uint16_t regs[2]; int8_t ret = Master_ReadHoldingRegisters(0x01, 0x0000, 2, regs); if (ret == 0) { // 读取成功,regs[0]是从机地址,regs[1]是运行状态 } else { // 读取失败,根据错误码定位原因 }主机内部会做这几件事:
把请求参数组帧:从机地址 + 功能码 + 起始地址(高字节在前)+ 数量(高字节在前)+ CRC16。
切换RS485收发器方向为发送,通过串口把帧发出去。
发送完毕后切换方向为接收,等待从机响应。
如果收到响应,校验CRC、地址、功能码。如果CRC错误或功能码异常,进入错误处理。
设置超时时间。如果在超时时间内没收到完整响应帧,返回超时错误,主机可以重发或报告故障。
这个过程里最容易被忽略的是超时计算。ModbusRTU标准规定,从机接收到请求后必须在不超过1秒的时间内开始响应,但我们在工程上不能只依赖这个标准,还要考虑串口波特率带来的传输时间。比如9600波特率下,一帧20字节的数据传输时间大约是23ms,超时设置至少要大于这个数值,工程上通常设定为50ms到200ms。
3. 一次把联调常见坑踩明白
3.1 分别测都通过,连起来就不通——先查方向控制
这是搜索热词里最扎心的一个问题:单独测主机,它能正常发数据;单独测从机,上位机软件发指令它也能正常回。但把主机和从机用485短接,通讯就失败。
我调试过的项目里,十有八九是RS485的方向切换时序出了问题。
RS485收发器(如MAX485、SP3485)的DE和RE引脚控制方向:DE为高时发送数据,RE为低时接收数据。一般会把DE和RE并到一起,用一个GPIO控制。问题往往出在“发送完毕后立刻切换方向”这一句上:
USART_SendData(USART1, data); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); // 这里是发送寄存器空 // 但此时数据可能还在移位寄存器里!立刻切方向会把最后一个字节的尾巴砍掉正确做法是等发送完成标志TC位置位,或者干脆延时一个字节时间再切换方向:
USART_SendData(USART1, data); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); // 等待发送完成 DE_CTRL_LOW(); // 切换到接收这个细节在很多教程里都不会特意讲,但丢尾巴字节导致的通讯失败占了联调问题的一半以上。这套资料的主机代码里方向切换是放在串口发送中断或DMA完成中断里做的,思路没问题,但你复制到自己项目里时一定要确认GPIO切换的时机。
3.2 CRC校验、寄存器地址、波特率类的低级错误
方向控制排除了以后,接下来按概率排优先级:
设备地址对不上。主机发的是从机地址1,从机程序里初始化地址却是2,能通才怪。这种情况在调试时最容易忽略,因为两边的地址定义往往不在同一个文件里。
CRC高低字节顺序反了。ModbusRTU规定CRC低字节在前、高字节在后。不少人按习惯先发高字节,结果对端校验永远失败。从机收到帧后直接丢弃,表现为主机一直超时。
波特率不一致。两台设备都是“9600”,但一个的晶振实际频率偏差大,时间长了累积下来就会产生错位。最好用示波器看一下信号,或者两边都换成外部晶振。
终端电阻和地线问题。短距离测试可以不接终端电阻,但距离超过几十米后,不接120欧终端电阻就会出现信号反射,时好时坏。还有一种情况是通讯地没有共地,特别在两个设备各自用开关电源供电时,A/B线之间的共模电压可能超出发送器承受范围。
| 现象 | 优先排查项 | 排查方法 |
|---|---|---|
| 主机一直超时 | 地址不匹配、CRC错误 | 串口助手抓从机收到的帧,对比CRC |
| 通讯时好时坏 | 方向切换时序、接触不良 | 示波器看波形,降低波特率测试 |
| 从机能收到但不回复 | 地址不符、功能码不支持 | 从机调试串口打印收到的帧内容 |
| 上电后偶尔乱码 | 共地问题、终端电阻未接 | 检查GND连接,加终端电阻,缩短线长 |
3.3 排查工具的实战用法
我得特别强调串口助手在485联调里的价值。很多朋友直接在主机和从机中间接逻辑分析仪,波形看半天分析不出来。其实最朴素的办法是:把主机的TXD信号(TTL电平)单独拉出来接到一个USB转TTL串口上,在电脑串口助手里看到主机实际发出的帧。再把从机的RXD和GND也拉出来接另一个USB转TTL,看从机实际收到的帧。两边一对比,问题立刻现形。
比如主机明明发的是01 03 00 00 00 02 C4 0B,从机那边收到的却是01 03 00 00 00 02 0B C4,那肯定是CRC高低字节顺序错了。如果从机那边根本收不到任何数据,说明方向切换把最后一个字节吃掉了。如果收到的帧里有乱码,检查波特率是否一致、线是否太长、地线是否共地。
逻辑分析仪用来看波形细节是后面的事,比如确认起始位宽度、检验位、停止位是否符合配置。但绝大多数情况下,串口助手的双向对比足够解决问题了。
4. 向工程代码之外再迈一步:稳定性设计
4.1 硬件上隔离与保护
很多项目在实验室跑得好好的,一到现场就各种抽风,多半是485电路没有做足够的保护。这套资料提供的原理图默认是基础版:一颗SP3485、两个上下拉电阻、一个120欧终端电阻、一个TVS管。如果你只是做课设或室内小规模通讯,这样绰绰有余。
但如果你要把设备放到工业现场或者室外,强烈建议加隔离和更完善的保护:
- 电源隔离:用B0505S之类的DC-DC隔离模块给485收发器单独供电,避免现场强电干扰通过电源窜入MCU系统。
- 信号隔离:光耦或数字隔离器(如ADUM1201)把MCU侧的UART信号和485侧的收发器信号隔离开,实现电气隔离。
- TVS保护:在A/B线上对地加TVS管,吸收ESD和浪涌;如果环境恶劣,还可以考虑加气体放电管或PTC自恢复保险丝。
其实还可以考虑用带自动方向控制的RS485收发器,省掉方向切换GPIO。但这类芯片在ModbusRTU下有隐患,自动换向电路靠检测无数据时的总线空闲来切换方向,在连续数据帧间隔极短的情况下可能误判,反而丢数据。所以工控产品里还是普遍采用GPIO手动切换方向。
4.2 软件上超时重发和错误码设计
在主机代码里,超时重发是可靠性的核心。实测下来,单次请求超时后立即重发往往还会失败,因为现场干扰是持续性的,立刻重发大概率继续撞枪口。更好的策略是:第一次超时后延时50~100ms,第二次再超时后延时200ms,第三次超时后报错并停止发送。
关于错误码,我的经验是宁可多定义几个也不要嫌麻烦。比如:
- 0:成功
- -1:超时无响应
- -2:CRC错误
- -3:地址不匹配
- -4:从机返回异常帧(再细分异常码)
有了这些错误码,主机轮询到某个从机失败时,你至少能知道是“物理层不通”还是“从机在处理上出差错”,而不是一个笼统的“通讯失败”。
| 节点数 | 波特率 | 总线长度 | 终端电阻 |
|---|---|---|---|
| 2-10 | 9600 | <200m | 建议接 |
| 10-32 | 9600 | 200m-800m | 必须接 |
| 2-5 | 115200 | <10m | 可以不接(但要测试) |
5. 我这套资料实际用下来的几个体会
如果你是从零开始接触ModbusRTU,我建议的练习顺序是:先烧从机程序,用电脑上的Modbus调试助手当主机,看能不能读到从机的寄存器。能读到了,再烧主机程序,用另一个串口调试助手抓主机发出的帧内容。两侧都验证通过后,再把两侧接在同一根RS485总线上联调。
这一步一步验证的好处是,问题能被限制在最小范围内。直接在两个单片机之间联调,一旦不通讯,你至少有三个嫌疑方向:主机程序问题、从机程序问题、硬件接线问题。而用调试助手分步验证,至少能把“程序能独立工作”这个问题先排除掉。
这套资料里还有一个我比较喜欢的地方,是从机代码里预留了多个功能码的实现框架——除了03和06,还有01(读线圈)、05(写单线圈)、16(写多个寄存器)的接口。即使当前项目用不到,后续要扩展功能也很方便。我实际做过几个用STM32做从机ModbusRTU通讯的项目,DTU数据采集器和智能电表类似的应用,都是在这套框架上扩展出来的。把寄存器映射表定义好以后,上位机对接只是填表的事。
最后分享一个小技巧:从机的寄存器映射表最好在设计初期就按模块划分好区域,比如0x0000-0x001F放设备信息,0x0020-0x003F放运行参数,0x0040-0x005F放设置项。这样客户端上位机开发不会因为寄存器地址乱跳而晕头转向,后续增加新功能也有充足的空余地址可用。就算项目做完不再动,过半年回头维护,看地址就知道当初想干什么,比翻注释快多了。
本文还有配套的精品资源,点击获取