简介基于STC32G12单片机的Modbus-RTU主机例程资源包面向嵌入式开发者和工业自动化爱好者解决在8051内核单片机上实现Modbus主机通信的问题。压缩包共140个文件类型以72个C源文件与62个头文件为主另有工程配置、HEX固件、汇编启动文件及清理脚本等整体仅224KB便于查看和移植。目前已有503人学习下载适合具备基础单片机知识、希望掌握Modbus协议栈的读者参考。代码完整覆盖主机侧关键环节UART串口参数配置、RTU报文帧的生成与解析、CRC校验计算、主从请求响应流程、超时重试与错误处理等并提供了可编译的Keil工程与HEX文件方便直接烧录验证。通过阅读注释和调试示例读者可以较快理解Modbus-RTU主机的工作原理并应用到PLC通信、传感器数据采集或物联网网关等实际项目中。1. 基于stc32g12单片机写Modbus-RTU主机不是读例程是拿例程做事单片机 Modbus-RTU 主机例程这东西网上能搜到一大把但多数是演示版真接到变频器、电表、温湿度传感器上就容易卡壳。这份基于 stc32g12 单片机的 Modbus-RTU 主机例程解决的是现场最磨人的两个问题一是发送完请求后如何在 3.5T 字符间隔内精准切到接收态二是从机不响应时主机怎样超时重发而不把自己卡死。它适合做小型数据采集网关、设备控制面板的开发者新手照着接线能把 03 功能码读回来熟手可以直接改状态机去轮询多台从机。下面是我把这套例程拆开、跑通、踩坑后的完整笔记包含硬件连接、代码结构、参数设置和几个特别容易翻车的地方。2. 硬件与协议选型为什么stc32g12做Modbus-RTU主机不吃力2.1 stc32g12 的串口资源与 RS485 收发控制做 Modbus-RTU 主机首先得有 UART其次要控制 RS485 的收发方向。stc32g12 这类增强型 8051 内核单片机跑 24MHz 以上主频是常态串口资源通常不止一个内部波特率发生器也比老 51 灵活很多。我一般会把 UART1 留给调试打印UART2 做 Modbus 总线通信这样抓问题时能分开看。方向控制最简单的方式是拿一个普通 GPIO 去接 RS485 芯片的 DE/RE发送时拉高接收时拉低。不要用 TX 使能这种旁路方案因为 Modbus 要求严格的帧间隙GPIO 翻转时机必须在代码里显式控制。硬件连接需要注意共地。很多新手只接 A、B 两根线跑几米没问题距离一长或者从机一多就偶发乱码。正确做法是主机、从机的工作地GND必须连在一起RS485 是差分信号但收发器芯片的电平参考还是依赖共地。接线顺序建议先接 GND再接 A/B最后上电。热插拔 A/B 线时容易在总线上产生毛刺可能把从机打挂。下面是典型的端子接线表连接对象主机侧引脚从机侧引脚备注差分正RS485 A或 DA或 D双绞线不要交叉差分负RS485 B或 D-B或 D-双绞线不要交叉信号地GNDGND必须共地屏蔽层保护地保护地单端接地别两头都接2.2 终端电阻与波特率的选择逻辑总线末端要接 120Ω 终端电阻这是老生常谈但做主机时经常被忽略。主机一般处于总线一端所以主机侧的 RS485 收发器 A-B 之间要并一个 120Ω 电阻。如果总线只接了主机和一个从机从机那端也接一个相当于两端各接一个匹配特性阻抗。距离在 10 米以内、波特率 9600 时有人不接也能跑但我不建议赌这个工业现场干扰无处不在一个电阻的电感效应比丢一帧数据便宜多了。波特率的选择直接影响 3.5T 间隔计算。Modbus RTU 规定帧间隔是 3.5 个字符时间对应到串口一个字符包含起始位、8 个数据位、校验位和停止位通常按 11 位算。9600bps 下 1 位时间约 104.17μs3.5T 就是约 3.65ms19200bps 下约 1.82ms。例程里的定时器参数一般按波特率换算拿到代码后要检查定时器初值是否和实际波特率匹配否则帧间隔不对从机就会把一帧拆成两帧导致主机永远收不到正常响应。2.3 串口参数配置8N1、校验位与方向切换的时隙Modbus-RTU 最常用的是 8 数据位、无校验、1 停止位即 8N1。有的设备默认是偶校验8E1主机必须跟着改否则校验字段对不上。例程里通常用一个结构体保存串口参数包括波特率、数据位、校验位、停止位实际接线前先看从机手册别上来就照搬例程。方向切换的时隙是这个项目里最容易翻车的地方。发送完最后一个字节后RS485 驱动器还需要一点时间把数据彻底推上总线如果立刻拉低 DE最后一个字节可能被截断。我一般会在发送完中断里加一个延时或者用一个短定时器延迟 20μs 再切接收态。这个值因 RS485 芯片而异常见的 SP485、MAX485 芯片在 9600 波特率下取 50μs 左右都行但我习惯按字节时间的一半来估算宁可多等一点也别切太早。3. 例程代码结构状态机、超时与CRC校验3.1 例程的工程文件与模块划分这套例程拿到手先别急着编译看一眼文件结构。通常会有 main.c、modbus_master.c、modbus_master.h、crc16.c、crc16.h还可能带一个 timer.c 或 delay.c。main.c 只管初始化主循环modbus_master.c 是核心逻辑CRC 单独成文件是因为它会被频繁调用独立出来方便测试。例程的模块划分比较合理协议栈和硬件操作分离串口发送接收只留接口这样移植到其他单片机时只需要改底层串口函数。典型的文件清单文件名职责依赖main.c系统初始化、主循环调度各模块头文件modbus_master.c请求帧构建、响应解析、状态机modbus_master.hmodbus_master.h宏定义、数据结构、函数声明无crc16.cCRC16-Modbus 计算crc16.htimer.c3.5T 超时定时、轮询定时平台寄存器3.2 构建请求帧功能码、起始地址与CRC16Modbus 请求帧的格式很固定从机地址1字节 功能码1字节 起始地址2字节 数据N字节 CRC162字节低字节在前。例程里一般有个函数封装这个过程比如modbus_master_build_request。我先把它简化为一个发送函数uint16_t modbus_read_holding_regs(uint8_t slave_addr, uint16_t start_reg, uint16_t reg_count) { uint8_t frame[8]; uint8_t i; uint16_t crc; frame[0] slave_addr; // 从机地址 1 frame[1] 0x03; // 功能码读保持寄存器 frame[2] start_reg 8; // 起始地址高字节 frame[3] start_reg 0xFF; // 起始地址低字节 frame[4] reg_count 8; // 寄存器数量高字节 frame[5] reg_count 0xFF; // 寄存器数量低字节 crc calc_crc16(frame, 6); // 前6字节参与CRC frame[6] crc 0xFF; // CRC低字节在前 frame[7] crc 8; // CRC高字节在后 // 翻转DE为发送模式然后逐个发送 rs485_de_set(1); for (i 0; i 8; i) { uart_send_byte(frame[i]); } // 发送完成后再等一个短延时然后翻转DE为接收 delay_us(50); rs485_de_set(0); return 0; }注意frame数组大小要覆盖最大帧长我这里按 8 字节写死如果后续要写多个寄存器需要扩到 256。calc_crc16是标准 Modbus CRC16 算法查表法还是位运算法都行但要注意初始值是 0xFFFF最后结果低字节在前发送。rs485_de_set是 GPIO 翻转的封装例程里一般放在底层硬件文件里。3.3 接收状态机解析响应和超时控制硬件方面接收处理如果只靠串口中断一字节一字节地拼很容易被打断。例程通常的做法是建立一个状态机空闲态、等待态、接收态、完成态。我简化成一个接收处理函数void modbus_master_on_uart_byte(uint8_t byte) { static uint8_t rx_buffer[256]; static uint8_t rx_len 0; static uint8_t state MB_IDLE; static uint8_t slave_addr; switch (state) { case MB_IDLE: // 第一个字节必须先和当前轮询的从机地址比对 if (byte current_slave_addr) { rx_buffer[0] byte; rx_len 1; state MB_RECEIVING; // 启动3.5T超时定时器 timer_start(3_5T_TIMEOUT); } break; case MB_RECEIVING: rx_buffer[rx_len] byte; timer_restart(3_5T_TIMEOUT); if (rx_len 3) { // 根据功能码判断期望的响应长度 uint8_t fn rx_buffer[1]; if (fn 0x83 || fn 0x86 || fn 0x90) { state MB_ERROR_RECEIVED; // 异常响应 } else if (fn 0x03 rx_len 3) { // 广播帧长度处理 } } break; default: break; } }逻辑说明接收状态机以第一个字节是否为当前从机地址作为过滤条件这能挡掉总线上发给其他从机的响应。timer_restart每次收到字节都重置 3.5T只有超过 3.5T 没有新字节到来才认为一帧结束了。这个定时器是切换判断的关键必须用硬件定时器实现别用软件延时否则发数据时中断被关掉超时时间就不准。3.4 轮询调度多个从机的循环机制单从机很简单多从机就得靠轮询。例程里一般用current_slave_addr和current_reg_addr两个变量跟踪进度每轮完成后切到下一台从机。核心是一次只发一个请求收到响应或超时后再发下一个不能连续发两个请求因为总线是半双工的。我通常把轮询周期分为两个阶段请求阶段构建帧、翻转 DE、发送、等待响应或超时空闲阶段留出足够间隙给从机处理一般等 10ms 以上。这样就算某台从机掉线超时重发不会堵塞整个总线其他从机还能正常轮询。4. 功能码与寄存器配置03、06与16怎么落到具体设备4.1 03 读保持寄存器地址映射与浮点处理绝大多数传感器和仪表支持 03 功能码读取保持寄存器。实际操作时先看从机手册里的寄存器地址表注意地址有两种表示方式一种是协议地址如 0x0000另一种是数据地址如 40001。Modbus 报文里发送的是协议地址也就是 40001 要减去 40001 再变成 0x0000。例程里start_reg参数应该直接填协议地址如果手册给的是数据地址记得转换。响应帧格式是从机地址 功能码 0x03 字节数 数据 CRC。寄存器值是 16 位大端序高字节在前但很多设备会把浮点数拆成两个寄存器这时候字节序就乱了。比如一个 32 位浮点数占两个寄存器有的设备是高字在前有的是低字在前例程里如果用int16_t直接拼接读出来的数完全不对。我一般会在应用层单独写一个modbus_parse_float函数先按字节数组收进来再根据设备的字节序规则做重排不要照搬例程的memcpy。4.2 06 写单个寄存器执行确认和写后回读06 功能码用于写单个保持寄存器请求帧是地址 功能码 寄存器地址 寄存器值 CRC。正常响应是回显请求帧也就是从机把发送的内容原样返回。如果响应的寄存器值和你写的不一致说明写入失败。例程里通常只判断功能码是否为 0x06不够严谨建议再比对寄存器值和返回值不一致就报错重试。写操作和设备内部处理时间有关有些设备写 EEPROM 需要几十毫秒主机必须等响应。例程里的超时时间如果是 200ms写慢设备容易超时。我遇到过一个设备写保持寄存器后要 300ms 才返回响应后来把超时时间改成 800ms 才稳定。建议超时时间做成宏定义不同设备用不同值。4.3 16 写多个寄存器批量下发的长度边界16 功能码0x10用于写多个寄存器请求帧里多了字节数和你实际想写的寄存器个数。这地方最容易出错的是字节数和寄存器个数的关系一个寄存器 2 字节写 10 个寄存器就是 20 字节字节数填 20寄存器数量填 10。例程里如果只做了 06 功能码想扩展成 16要自己改构帧函数尤其注意帧长度数组要放大。另一个坑是寄存器数量上限。Modbus 标准规定 16 功能码一次最多写 123 个寄存器也就是 246 字节数据。但很多设备实际能力更小比如某电表只允许一次写 64 个寄存器写多了会返回异常码 0x03非法数据值。所以我一般会在例程里加一个max_reg_count配置按最小从机限制来切分批量写入。4.4 异常响应与错误码解析从机收到非法请求会返回异常帧从机地址 功能码最高位置1 异常码 CRC。例程里如果只在等到数据就返回 OK不判断功能码会把异常响应当成有效数据。异常码含义如下异常码含义常见原因0x01非法功能码从机不支持该功能0x02非法数据地址寄存器地址越界0x03非法数据值写入值超出范围或长度越界0x04从机设备故障设备内部错误0x05确认已接受但处理中稍后需重读0x06从机忙设备忙需稍后重试解析异常帧时建议把所有错误码映射成枚举方便上层直接判断。例程里可能只做了 03/06 的响应解析拿到 0x83 这种异常码要单独走异常处理分支不能当作正常数据进通讯录否则地址对不上就卡死。5. 避坑、常见问题与排查四条血泪记录5.1 从机总是没响应逻辑分析仪却看到数据发出去了现象主机串口明明发了一帧完整请求抓波形也正常但收不到任何响应超时后重发还是没响应。原因最常见的是收发的 DE/RE 切换时序不对发送完后没有等驱动器把最后一个字节推完就切到了接收态导致从机收到的帧末尾被截断从机认为帧不完整自然不会回复。另一个高频原因是共地问题两边 GND 没接总线噪声大从机收到的请求 CRC 校验错。解决先把 DE/RE 的翻转延时长到 100μs 试试同时确认 RS485 的 A/B 不要接反。然后在从机端用串口工具直接回环测试确认从机本身能正常响应。例程里如果超时时间太短比如只有 50ms碰到某些处理慢的从机也会误判先把超时放到 500ms。5.2 发送和接收切换时第一个响应字节被丢现象逻辑分析仪明明看到从机回的帧第一个字节正常但主机收到的数据里第一个字节就是 0或者直接少一个字节。原因RS485 收发器从发送模式切到接收模式时总线状态需要稳定时间如果主机切得太早驱动芯片还没有释放总线从机的起始位被拉偏主机采样到乱码。另一个原因是串口接收中断在切换瞬间还未使能第一个字节到达时被丢失。解决DE 拉低后不要立刻使能接收中断加一个 10μs 左右的延时再使能。另外检查串口接收中断使能是否在主循环里被误关例程里常见做法是在发送完后恢复接收中断要确保这行代码在 DE 切换之后执行。5.3 波特率 9600 能通19200 就乱码现象同样的代码波特率调到 19200 后偶发乱码一帧里总有几个字节 CRC 错。原因stc32g12 的串口波特率由定时器产生如果用的是定时器 1 的 8 位自动重装模式波特率误差在高波特率时会变大。另外RS485 总线在 19200 下对线缆分布电容更敏感双绞线过长或者分支过多信号边沿变差。解决先把波特率发生器切到定时器 2或者使用内部高精度时钟源误差能压到 0.1% 以内。硬件方面缩短总线分支分支线不超过 20cm终端电阻也要接好。例程里如果只在初始化时设了波特率没有检查误差率可以用示波器量一下 TX 引脚的实际位宽对照理论值调整。5.4 从机返回数据对但CRC一直报错现象用逻辑分析仪抓到的响应帧数据和从机手册一致CRC 看起来也对但主机解析时 CRC 校验失败程序卡在重发逻辑里。原因多数是 CRC 计算的多项式或字节序弄反了。Modbus CRC16 是低字节在前但有的例程里用了标准的 CRC16-CCITT多项式不对。另外计算 CRC 时把起始地址当成从机地址一起算或者把 CRC 自身加入计算都会导致错位。解决用已知的测试向量校验算法。例如 01 03 00 00 00 0A 的 CRC 应该是 C5 CD先跑一个纯算法测试如果结果不对就调整初始值和多项式方向。例程里通常提供calc_crc16函数直接在 main 里加一个断言判断固定向量能快速定位是不是 CRC 函数的问题。6. 从单从机到多从机轮询验证方法与进阶技巧6.1 用逻辑分析仪验证 3.5T 间隔单从机跑通后别急着加多从机先验证帧间隔。把逻辑分析仪挂在 RS485 的 A、B 差分线上波特率设置成和主机一致抓一帧请求和一帧响应。重点看两帧之间的静默时间也就是最后一个字节的停止位到下一帧第一个字节起始位之间的低电平时间。这个时间应该大于 3.5T。我常用的办法是直接在逻辑分析仪软件里测量相邻两帧的间隔9600 波特率下间隔必须大于 3.5ms。如果实测只有 1ms 左右说明主机在发送完请求后没有等够 3.5T 就开始准备下一轮从机会把请求拆成两帧处理。6.2 多从机轮询的时序规划多从机轮询的逻辑要点是同一时刻只处理一台从机不能因为某台从机超时就把整个总线堵住。我给出一段轮询调度伪代码void poll_all_slaves(void) { static uint8_t idx 0; // 当前从机还在等待响应不发起新的请求 if (mb_busy) { return; } // 如果当前从机超时记录错误并切换到下一台 if (mb_timeout_flag) { set_slave_error(slave_table[idx].addr); mb_timeout_flag 0; } idx (idx 1) % slave_count; mb_busy 1; modbus_read_regs(slave_table[idx].addr, slave_table[idx].start_reg, slave_table[idx].reg_count); }逻辑说明mb_busy标志表示当前已有请求在总线上必须等响应完成或超时才能清掉这是防止总线冲突的关键。mb_timeout_flag由定时器在 3.5T 超时后置位超时后先记录错误再轮询下一台不能让掉线的从机一直占据总线。slave_count和slave_table在例程里通常作为一个数组配置增删从机不需要改业务代码。6.3 把主机状态机嵌入 RTOS 或主循环很多开发者想把 Modbus 主机跑在 RTOS 里其实没必要开一个独立任务除非你有多个串口同时做主机。最简单的方式是把 Modbus 状态机和主循环融合每 10ms 调一次modbus_poll利用定时器中断驱动 3.5T 间隔主循环里只检查标志位。这样代码结构清晰不容易出现任务间共享串口资源的竞态问题。我最初也犯过把发送和接收拆成两个任务的错发送任务不停往串口写接收任务等待响应结果一个字节没发完就被接收任务打断整个总线直接废掉。后来改成单任务状态机把发送、等待、接收、超时全部放进同一套流程这个问题才消失。从那以后我每次拿到新的 Modbus 主机例程都先检查是不是用状态机而不是中断里乱发强制走一遍这个检查再动手移植。希望这篇实战拆解能帮你把这份 stc32g12 Modbus-RTU 主机例程真正用在项目里少走我走过的弯路。本文还有配套的精品资源点击获取