恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

STM32F103 MODBUS RTU从站控制6路继电器实践详解

  • 首页
  • 资讯中心
  • /
  • STM32F103 MODBUS RTU从站控制6路继电器实践详解

相关资讯

审批模板复杂嵌套表单状态管理:从Schema驱动到集中状态树 2026/9/9 0:07:56
企业级数据可视化大屏HTML工程骨架解析 2026/9/9 0:07:56
STM32流量监测与水泵控制系统:从Proteus仿真到实物联调全解析 2026/9/9 0:07:56

最新资讯

酒吧点餐小程序系统开发实战:从需求分析到上线部署指南
智能体工作流工具深度对比:11款AI Agent编排平台选型指南
蚊子目标检测数据集详解:YOLO与VOC格式训练全流程
全民健身解决方案小程序系统开发实战:从架构设计到上线指南
全民健身解决方案系统源码实战指南:从架构设计到部署全流程解析
Google SEO排名提升实战:避开关键词与技术优化的关键误区

今日推荐

基于MongoDB的图书管理系统:数据建模与Spring Boot+Vue实战
Claude Code安装配置全攻略:从零开始用上终端AI编程助手
tmux 会话管理与终端复用:AI 编程工作流的调度中枢实战

本周热门

超人会飞不算本事:系统稳定依赖清晰规则与边界设计
超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论
基于CNN的调制信号识别:MATLAB实现时频图分类实战

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

STM32F103 MODBUS RTU从站控制6路继电器实践详解

发布时间:2026/9/9 0:07:56
STM32F103 MODBUS RTU从站控制6路继电器实践详解 简介基于STM32F103的串口MODBUS RTU 6通道继电器切换资源是一套可直接落地的工业控制方案。面向嵌入式开发、PLC通信及物联网设备调试人员解决多路继电器远程通断控制与上位机协议对接问题。资源已在实际项目中验证程序源码基于Keil平台编写配合Cadence设计的原理图与PCB从硬件到固件形成完整闭环适合需要快速集成或参考硬件设计的工程师。包体共246个文件约7.24MB。其中包含52个C源码与49个头文件构成STM32固件主体35个crf、34个o等为编译中间产物便于排查构建过程原理图dsn与PCB brd文件为Cadence工程可直接编辑另有hex烧写文件、uvprojx工程文件及bat脚本方便直接使用和二次开发。已有875人学习下载。资料价值在于提供经过量产验证的完整工程不仅包含MODBUS RTU协议栈实现、继电器驱动电路还有USB转TTL接口设计省去协议调试与硬件打样弯路拿来即可用。1. 项目概述与核心需求这次做的是一个 STM32F103 跑 MODBUS RTU 从站控制 6 路继电器通断的小项目。需求本身不复杂上位机通过串口RS485 或直连 TTL发 MODBUS RTU 协议帧单片机解析后控制 6 路继电器支持单路控制、多路同时控制、状态读取。但实际做下来从协议移植到硬件接线有一堆细节值得记录这也是我写这篇文章的初衷。先说为什么选 STM32F103。作为经典的 M3 内核单片机它主频 72MHz、资源丰富标准外设库和 HAL 库都很成熟网上资料多到爆炸遇到问题基本都能搜到答案。而且对继电器这种低速设备来说F103 的性能绰绰有余项目成本也能压得很低。6 路继电器切换本身不涉及高实时性要求MODBUS RTU 的波特率通常在 9600 或 19200一帧数据也就几十个字节串口中断完全够用。再聊 MODBUS RTU 本身。MODBUS 协议在工业控制领域几乎是事实标准RTU 模式是二进制传输每帧数据有 CRC 校验可靠性和实时性都不错。这里用的是从站模式上位机是主站发命令帧给单片机单片机收到后解析地址、功能码、数据段执行对应操作后回复响应帧。最常用的功能码就两个0x01 读线圈状态、0x05 写单线圈、0x0F 写多线圈。本项目中 6 路继电器对应 6 个线圈地址功能码 0x01 用于查询状态0x05 和 0x0F 用于控制。整个项目我拆成了几个模块串口驱动USART1 接收发送、MODBUS RTU 协议解析、继电器控制逻辑、定时器超时判断用于 RTU 帧间隔检测。协议部分我原本打算用 FreeMODBUS后来觉得项目简单直接手写解析反而更灵活。后面我会详细对比这两种方式的取舍。2. 整体方案设计与硬件选型2.1 系统框架系统结构很简单上位机PC 或触摸屏通过 USB 转 TTL 或 RS485 转接板连接到 STM32F103 的 USART1单片机根据收到的 MODBUS RTU 帧控制 GPIO 引脚进而驱动光耦隔离继电器模块。继电器输出端接负载比如灯具、电磁阀、小功率电机等。在实际工业场景里RS485 更常见。因为 RS485 是差分信号抗干扰能力强传输距离能到上百米而且支持多设备组网同一总线上挂多个从站每个从站分配不同地址。如果只是桌面上调试直接用 USB 转 TTL 接到单片机的 PA9TX、PA10RX就行。这里有个坑USB 转 TTL 模块的地线必须和单片机共地否则串口通信会不稳定大概率收到乱码。2.2 选用自由协议还是 FreeMODBUS最初我考虑过 FreeMODBUS它是一套开源的 MODBUS 协议栈实现移植到 STM32 标准库有现成教程配 v3.5 库很常见。FreeMODBUS 的优势是协议栈健壮支持 RTU、ASCII、TCP 多种模式而且对帧间隔、异常处理都考虑得很周全缺点是代码量大配置繁琐对 6 路继电器这种简单控制有点杀鸡用牛刀。后来我决定手写一套精简版 MODBUS RTU 协议解析。原因有三一是项目功能单一只需要 0x01、0x05、0x0F 三个功能码二是手写代码可以完全掌控解析逻辑出问题好排查三是可以严格控制代码体积和 RAM 占用。当然自由协议也有代价比如 CRC 校验、超时处理这些都得自己写但对动手实践来说反而是提升的机会。如果你做的是比较复杂的产品比如支持多个功能码、多寄存器读写、带广播帧、需要异常响应那么直接用 FreeMODBUS 或 libmodbus 移植会更安全。如果只是像我这样的中小型控制项目手写解析完全够用而且能让你对 MODBUS 协议的细节理解得更透。2.3 继电器驱动电路设计继电器模块我用的是市售光耦隔离继电器模块低电平触发带 LED 指示。每个模块输入侧有三个引脚VCC接 3.3V 或 5V、GND接单片机 GND、IN接单片机 GPIO。注意虽然模块标注支持 3.3V 驱动但实际测试中部分模块在 3.3V 下可靠吸合的效果一般所以建议如果模块是 5V 供电的输入侧高电平至少要在 4V 以上。STM32 的 GPIO 输出 3.3V直接接上去可能不够稳定。解决办法是要么选 3.3V 兼容的继电器模块带光耦的通常可以要么加一级 NPN 三极管比如 SS9013或 N-MOS 管做电平转换和驱动增强。网上有好多基于 SS9013 的 12V 继电器驱动电路原理就是在基极串一个电阻接单片机 GPIO发射极接地集电极接继电器线圈和续流二极管。继电器线圈是感性负载断电瞬间会产生反向电动势必须并联续流二极管1N4007 或 1N4148来吸收否则容易击穿三极管或单片机引脚。这在选型时特别重要额定电压、线圈电流、触点容量都要看数据手册。我用的是 5V 光耦继电器模块输入侧 IN 引脚直接接 STM32 的 GPIOPC0、PC1、PC2、PC3、PC4、PC5 依次对应 6 路继电器。实测下来3.3V 高电平对部分模块的可控性不稳所以我加了 3.3V 转 5V 的逻辑电平转换模块确保继电器驱动可靠。这一步建议不要省否则现场经常会遇到“有时候能吸合有时候不能”的灵异问题。3. 核心细节解析与实操要点3.1 USART1 串口配置STM32 中串口配置说难不难说简单也有不少细节。我用的是标准库 v3.5USART1 的引脚是 PA9TX和 PA10RX复用推挽输出和浮空输入。波特率选 9600 还是 19200MODBUS RTU 标准里没有强制但一般从站都会配置为 9600 8 N 19600 波特率、8 位数据位、无校验、1 位停止位。9600 是工业现场最常用的选择稳定性最好19200 傳輸更快但对线材和干扰更敏感。我的建议有噪声干扰的环境优先 9600短距离桌面调试可以用 19200 或 38400。串口初始化代码要点如下void USART1_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); // TX PA9 GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_Init(GPIOA, GPIO_InitStructure); // RX PA10 GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); USART_InitStructure.USART_BaudRate 9600; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_RX | USART_Mode_TX; USART_Init(USART1, USART_InitStructure); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); USART_Cmd(USART1, ENABLE); }关于中断接收我用的是 RXNE读数据寄存器非空中断。每收到一个字节就进一次中断把数据扔进接收缓冲区。接收完一帧后需要判断帧间隔MODBUS RTU 规定一帧内字节间间隔不能超过 1.5 个字符时间帧与帧之间至少 3.5 个字符时间。最简单可靠的方式是用两个定时器一个负责字节超时3.5 字符时间一个负责帧处理。如果项目资源紧张也可以用 SysTick 或单个 TIM 做超时判断。实际使用中更稳妥的做法是不用固定延时而是记录每字节到达时刻判断当前字节与上一字节的时间差是否超过 3.5 字符时间。9600 波特率下一个字符约 1ms3.5 字符就是 3.5ms用定时器计时完全没有压力。我用 TIM2 做了一个 100us 的时基在处理完每字节后清零计时中断每次进来加一超过 35 就认为帧超时并复位帧索引。这种方法对任意波特率通用代码也直观。3.2 MODBUS RTU 协议帧格式MODBUS RTU 帧格式是固定的四段地址码1 字节、功能码1 字节、数据段N 字节、CRC 校验2 字节低字节在前。最常见的读取线圈状态请求如下地址 功能码 起始地址高 起始地址低 线圈数量高 线圈数量低 CRC低 CRC高 0x01 0x01 0x00 0x00 0x00 0x06 0x?? 0x??这个帧的意思是向地址 0x01 的从站读取从寄存器地址 0x0000 开始的 6 个线圈状态。从站响应帧是地址 功能码 字节数 数据(位打包) CRC低 CRC高 0x01 0x01 0x01 0x2D 0x?? 0x??数据的每一位对应一个线圈状态bit0 对应起始地址的第一个线圈。比如 6 路继电器都闭合那么对应字节的 bit0~bit5 都是 1这个字节就是 0x3F。写单线圈0x05的帧如下地址 功能码 线圈地址高 线圈地址低 数据高 数据低 CRC低 CRC高 0x01 0x05 0x00 0x00 0xFF 0x00 0x?? 0x??这里有个 MODBUS 协议规定的特殊点写单线圈时数据段必须是 0xFF00 表示闭合0x0000 表示断开其他值非法。这和其他寄存器写功能直接写 0x0001不同别搞混了。写多线圈0x0F的帧稍微复杂一点地址 功能码 起始地址高 起始地址低 线圈数量高 线圈数量低 字节数 数据 CRC 0x01 0x0F 0x00 0x00 0x00 0x06 0x01 0x2D 0x?? 0x??3.3 CRC 校验实现MODBUS RTU 的 CRC 是 CRC16-IBM/MODBUS多项式是 0xA001即常规 CRC16 多项式 0x8005 反射后得到。注意低字节在前发送也就是先发 CRC 低 8 位再发 CRC 高 8 位。CRC 计算函数如下uint16_t ModbusCRC16(uint8_t *pBuffer, uint16_t len) { uint16_t crc 0xFFFF; uint16_t i; uint8_t j; for (i 0; i len; i) { crc ^ pBuffer[i]; for (j 0; j 8; j) { if (crc 0x0001) { crc (crc 1) ^ 0xA001; } else { crc 1; } } } return crc; }如果你想要性能更好可以用查表法预先计算 0~255 的 CRC 表之后每个字节只需一次查表和异或速度提升明显。但对于 9600 波特率逐位法完全没压力毕竟处理器是 72MHz一帧数据也就十几个字节逐位计算也就几十微秒的事。发送响应帧时添加 CRC 的代码注意高低字节顺序frame[frameLen] crc 0xFF; // 低字节在前 frame[frameLen] (crc 8) 0xFF;接收端校验时要单独验证 CRC 是否正确。如果 CRC 校验失败最简单策略是直接丢弃不回任何响应等待主站超时重试。这是 MODBUS 标准的推荐做法避免坏帧继续传递。4. 实操过程与核心环节实现4.1 硬件连接我用的是 STM32F103C8T6 最小系统板蓝色板那种加一块 6 路光耦隔离继电器模块。硬件连接如下STM32F103C8T6继电器模块说明PA9 (USART1_TX)-接 USB 转 TTL 的 RXPA10 (USART1_RX)-接 USB 转 TTL 的 TXGNDGND必须共地PC0IN1继电器 1PC1IN2继电器 2PC2IN3继电器 3PC3IN4继电器 4PC4IN5继电器 5PC5IN6继电器 63.3V 或外接 5VVCC按模块要求供电注意单独给继电器模块供电是很有必要的。如果直接用电脑 USB 给单片机供电同时继电器模块也从同一个 5V 取电那么继电器吸合的瞬时电流可能拉低电压导致单片机复位。实际测试中吸合 3 路以上时电压跌落已经相当可观。所以我用了一个外置 5V 适配器给继电器模块单独供电单片机用 USB 供电两者共地。这个方案跑起来非常稳定。4.2 接收缓冲区和状态机我定义了一个接收缓冲区采用简单的环形队列或者直接线性数组。由于 MODBUS RTU 是半双工协议同一时间只会有一个帧在传输所以用线性数组加帧索引就够了#define RX_BUFFER_SIZE 64 uint8_t rxBuffer[RX_BUFFER_SIZE]; uint8_t rxIndex 0; uint8_t frameReady 0;在串口中断中每收到一个字节就存入 rxBuffer并清零超时计时器。超时计时器累加到阈值后认为当前帧接收完毕置位 frameReady。这种做法的优点是逻辑简单对时序要求不高几乎不会出错。帧处理函数放在主循环中轮询或者放到超时中断里执行。我放在主循环里只在 frameReady 置位后处理if (frameReady) { frameReady 0; ProcessModbusFrame(rxBuffer, rxIndex); rxIndex 0; }注意处理完帧后必须将 rxIndex 归零否则会导致错误拼接。4.3 主站地址和功能码处理MODBUS 从站必须有个地址我的地址是 0x01。帧首字节如果不是 0x01直接丢弃。另外还支持广播地址 0x00广播帧不需要回应这个可以根据实际需求加。如果你做的是多从站组网每个从站的地址可以通过拨码开关设置而不是写死在代码里。功能码处理逻辑如下void ProcessModbusFrame(uint8_t *buffer, uint8_t len) { uint16_t crcReceived buffer[len - 2] | (buffer[len - 1] 8); uint16_t crcCalc ModbusCRC16(buffer, len - 2); if (crcReceived ! crcCalc) { return; // CRC error, ignore frame } uint8_t slaveAddr buffer[0]; if (slaveAddr ! SLAVE_ADDR) { return; } uint8_t funcCode buffer[1]; switch (funcCode) { case 0x01: // Read coils HandleReadCoils(buffer, len); break; case 0x05: // Write single coil HandleWriteSingleCoil(buffer, len); break; case 0x0F: // Write multiple coils HandleWriteMultipleCoils(buffer, len); break; default: // Send exception response (illegal function) // 0x80 to func code break; } }非法功能码时要发送异常响应帧功能码加 0x80异常码 0x01非法功能。这个细节很多初学者会忽略但 MODBUS 协议里明确要求从站对非法功能码回复异常帧而不是静默。如果在工业现场对接触摸屏或组态软件静默会导致上位机报错“设备无应答”排查起来很费劲。4.4 读线圈和写线圈实现6 路继电器的线圈地址映射为 0x0000~0x0005。这里推荐用数组变量保存继电器状态而不是每次读 GPIO 状态因为 GPIO 引脚可能被复用或上下拉影响。#define RELAY_COUNT 6 uint8_t relayStates 0x00; // bit0-5 对应6路继电器 void HandleReadCoils(uint8_t *buffer, uint8_t len) { if (len ! 8) return; uint16_t startAddr (buffer[2] 8) | buffer[3]; uint16_t quantity (buffer[4] 8) | buffer[5]; if (quantity 16) quantity 16; uint8_t byteCount (quantity 7) / 8; uint8_t statusByte[2] {0, 0}; for (uint8_t i 0; i quantity; i) { uint16_t coilAddr startAddr i; if (coilAddr RELAY_COUNT) { if (relayStates (1 coilAddr)) { statusByte[i / 8] | (1 (i % 8)); } } } uint8_t resp[32]; resp[0] SLAVE_ADDR; resp[1] 0x01; resp[2] byteCount; resp[3] statusByte[0]; if (byteCount 1) { resp[4] statusByte[1]; } uint16_t respLen 4 (byteCount - 1); uint16_t crc ModbusCRC16(resp, respLen); resp[respLen] crc 0xFF; resp[respLen] (crc 8) 0xFF; SendResponse(resp, respLen); }写单线圈0x05实现void HandleWriteSingleCoil(uint8_t *buffer, uint8_t len) { if (len ! 8) return; uint16_t coilAddr (buffer[2] 8) | buffer[3]; uint16_t value (buffer[4] 8) | buffer[5]; if (coilAddr RELAY_COUNT) return; if (value 0xFF00) { relayStates | (1 coilAddr); } else if (value 0x0000) { relayStates ~(1 coilAddr); } else { // invalid value, send exception 0x03 return; } UpdateRelayGPIO(); // response is echo of request SendResponse(buffer, len); }写多线圈0x0F实现类似只是解析数据段不是单个 0xFF00/0x0000而是按位取数据字节。注意如果数据字节数不够就必须丢弃并考虑异常响应。实际调试中上位机软件特别是 ModbusPoll对异常响应很敏感格式不对会导致通信报错所以解析函数必须严格处理长度和数量。4.5 继电器 GPIO 驱动GPIO 初始化的关键是将 PC0~PC5 配置为推挽输出默认电平拉高继电器模块低电平触发时默认断开void Relay_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOC, GPIO_InitStructure); GPIO_SetBits(GPIOC, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5); }更新继电器状态函数void UpdateRelayGPIO(void) { if (relayStates 0x01) GPIO_ResetBits(GPIOC, GPIO_Pin_0); // 低电平触发复位为吸合 else GPIO_SetBits(GPIOC, GPIO_Pin_0); if (relayStates 0x02) GPIO_ResetBits(GPIOC, GPIO_Pin_1); else GPIO_SetBits(GPIOC, GPIO_Pin_1); // ... 以此类推 PC2~PC5 }如果你的继电器模块是高电平触发的那逻辑正好反过来GPIO_SetBits 表示吸合GPIO_ResetBits 表示断开。看模块说明书或模块上的丝印就可以确定实测时也可以通过万用表测量输入引脚电压来判断。4.6 调试工具与踩坑调试 MODBUS RTU 必不可少的工具是串口调试助手和 ModbusPoll。左边发 MODBUS 请求帧右边观察响应帧方便比对。推荐几个常用工具串口调试助手用来裸看串口收发数据发十六进制帧ModbusPoll作为模拟主站自动发送读写请求验证从站响应是否符合协议虚拟串口工具如果你没有真实主站设备可以用虚拟串口对VSPD 一类配合 ModbusPoll 调试。如果你手头没有 USB 转 TTL推荐 CH340 或 FTDI 芯片的模块。CH340 便宜几块钱但 Windows 驱动最好去官网下载别用系统自动安装的旧驱动可能不稳定。FTDI 芯片的模块贵一些但兼容性更好尤其在 Mac 和 Linux 下很友好。实际项目里我还遇到过 CH340 模块跑 115200 高波特率丢字节的问题降级到 9600 就正常而 FTDI 没问题这也是老生常谈了。USB 转 TTL 模块的 TX 和 RX 需要交叉连接模块 TX 接单片机 RXPA10模块 RX 接单片机 TXPA9。别接成同向否则数据自说自话啥也收不到。接完后先打开串口调试助手发一个读取寄存器命令看看能不能收到响应。比如发送报文01 01 00 00 00 06 3C 0B这条帧是地址 01功能码 01起始地址 0000数量 0006CRC 为 3C 0B计算出来就是这样的。如果一切正常从站应回类似下面这条01 01 01 3F 90 0C其中 0x3F 是 6 路继电器状态位打包后的结果0000 0011 1111表示 6 路全部闭合。如果返回的不是预期值先在电脑上用硬件串口检测每个字节的波形确认波特率、数据位等是否匹配。5. 常见问题与排查技巧实录5.1 串口收不到数据排查步骤比较简单先确认 USB 转 TTL 是否安装驱动设备管理器里有没有 COM 口号再确认串口调试助手波特率是否与程序一致然后用示波器或万用表量一下模块 TX 引脚有没有波形。如果你用的是 STM32 最小系统板很多板子的 PA9/PA10 已经被板载 USB 相关电路占用或者需要短路帽插上才引出这个细节非常容易踩坑。最好先看一下板子原理图确认引脚引出来了再接线。如果发送帧单片机一直没有反应先用串口调试助手自发自测验一下把 USB 转 TTL 模块的 TX 和 RX 短接发送的数据如果能原样收回来说明模块没问题。然后接上单片机打开调试助手给单片机发一个读状态帧看调试助手能否收到响应。如果收不到检查单片机程序是否跑起来了点个 LED 看。5.2 CRC 校验错误CRC 错误常见原因有两个一个是上位机软件计算的 CRC 和你单片机计算的 CRC 算法不一致另一个是在调试助手手动输入帧时CRC 填的是错的或填反了高低字节。建议用 ModbusPoll 这类自动计算 CRC 的工具验证不要手动输 CRC。手动输的话也要记住 RTU 模式下 CRC 是低字节在前例如 CRC 是 0x3C0B帧里先填 0x0B 再填 0x3C很多人在这里搞反。另一个容易忽略的点计算 CRC 的范围是从地址码到数据段结束不包括 CRC 本身。计算长度 len 是帧总长减 2我前面代码里的 ModbusCRC16(buffer, len - 2) 就是干这个的。如果长度传错了CRC 必然校验不过。5.3 帧超时导致数据黏包如果你之前用的是固定延时来判断帧结束比如接收完先延时 10ms 再处理在波特率变化或上位机连续快速发送时容易出问题。RTU 协议的要求是 3.5 字符时间9600 波特率下约 4ms19200 下约 2ms。固定延时取值不好就可能导致一帧被拆成两帧或者两帧粘成一帧。我解决的办法是使用定时器超时判断前面已经说过。如果不用定时器也可以用另一个思路接收中断里直接判断当前帧的第一个字节地址对不对不对就直接丢弃减少无用帧占缓存。但最好的方案仍然是定时器超时。5.4 继电器抖动或误动作继电器模块在上电瞬间或者复位瞬间GPIO 引脚可能是高阻输入态外部干扰或内部上拉会导致电平不确定继电器可能瞬间误吸合。解决方法是在 GPIO 引脚外部加一个 10K 下拉电阻到地低电平触发模块确保上电默认断开或者在代码初始化 GPIO 时先设置寄存器输出低电平再配置复用模式。建议硬件上就加上下拉软件也要保证 Init 顺序。另一个常见问题是继电器吸合时电源跌落。多路同时吸合时电流冲击很大如果电源功率不足会直接导致单片机复位。前面提到过解决办法是独立电源给继电器模块供电并共地。如果负载是交流 220V 设备注意触点容量和工作电流匹配建议留一定余量。5.5 波特率混乱和数据错位如果你接收到的数据是完整帧但内容明显不对比如地址不是 0x01或者功能码乱跳首先怀疑波特率不匹配。用逻辑分析仪或示波器抓串口波形能直接看到每一位的宽度是否匹配。也可以写一个小程序收到任意字节后直接回显上位机发什么收什么来判断底层串口是否正常。另外一个不太容易想到的原因是上位机软件把 MODBUS ASCII 模式和 RTU 模式搞混了。ASCII 模式下会有冒号、十六进制 ASCII 字符、CR/LF 等跟 RTU 的二进制帧完全不同。在 ModbusPoll 等工具里一定要选 RTU别选 ASCII。6. 扩展与升级思路6 路继电器只是最基础的形态做完之后可以直接朝几个方向扩展成本增加很少但能适用的场景一下子广了不少。6.1 接入 RS485 组网当前我调的是 USB 转 TTL 直连如果想真正拿到工业现场用建议加一个 TTL 转 RS485 模块把串口信号转成差分信号。RS485 组网时每个从站地址不同上位机轮询所有从站即可。STM32F103 的 USART1 TX/RX 接 MAX485 一类芯片注意 DE 方向控制引脚接线发送时拉高 DE接收时拉低。半双工切换时机要处理好否则会出现发送完还停留的收尾字节产生冲突。6.2 蓝牙 BLE 无线控制如果你不想拉线可以用蓝牙模块比如 HC-08 或 JDY-31替代串口线与手机端 BLE 透传 APP 配合。MODBUS RTU 帧原样通过蓝牙透传软件协议栈不用改只换物理传输层。不过蓝牙和 485 的时序不一样串口透传的延迟和丢包需要测试尤其当通信频率高时要注意。蓝牙 BLE 的优势是调试方便手机当主站随时连缺点是距离有限约 10 米且信道干扰时可能有丢包。6.3 增加 Modbus TCP 网关如果你后面想接入 PLC 或上位机组态软件可以考虑用 W5500 或 ENC28J60 以太网模块做 MODBUS TCP 网关。TCP 和 RTU 的协议差异主要在帧头MBAP 头和 CRC 上把 RTU CRC 去掉加上事务标识、协议标识、长度字段就变成 TCP 帧了。网上有现成的移植方案可以直接参考。6.4 增加 IO 扩展和 ADC 采集只有 6 路继电器显然不够做完整的 I/O 系统。你可以用 74HC595 或 MCP23017 扩展更多输出通道或者用 ADC 采集模拟量比如温度、电流采集并把数据放到 MODBUS 保持寄存器中供主站读取。这样从一台简单的“6 路继电器控制器”就变成了一个功能完整的远端 I/ORemote I/O设备。这类设备在工业现场是标配自己做一台出来就会对 MODBUS 的理解深很多。7. 最后分享一些实操心得写到这里整个项目已经讲得比较透了。最后再说几个我实际踩过的坑希望对你有帮助。第一正在调试时千万不要盲改波特率。之前我有一次现场调高波特率到 115200结果串口助手和程序那边都改到了但 CRC 一直出错排查了半天才发现 CH340 在高波特率下丢字节。工业调试长时间挂在 9600 不是没道理的速度没那么重要稳定才重要。第二继电器线圈一定要加续流二极管。这个我刚开始做的时候忽略过后来继电器关断瞬间把三极管击穿了一次换上续流二极管后问题消失。感性负载关断时产生的反向电动势是真实存在且破坏力不小的选模块或者自己做驱动电路时千万别省。第三手写 MODBUS 协议时不要着急先拿 ModbusPoll 把所有功能码、异常帧测一遍再用真实上位机测一遍。ModbusPoll 的日志功能会把每一帧收发都记录下来排查通信问题比盲打盲试效率高太多了。如果这篇文章对你有帮助你可以照着跑一边遇到问题欢迎在评论区留言交流。后面我可能会再写一篇把 STM32F103 和 RS485、云端远程控制接起来的案例感兴趣的朋友可以先关注着。本文还有配套的精品资源点击获取

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号