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

STM32省IO设计:旋转开关编码与Modbus浮点传输实战

  • 首页
  • 资讯中心
  • /
  • STM32省IO设计:旋转开关编码与Modbus浮点传输实战

相关资讯

基于nRF52832的BLE透传模块E104-BT02应用与调试全攻略 2026/9/5 11:20:18
从零部署Codex:构建统一大语言模型API网关的实践指南 2026/9/5 11:20:18
Python爬虫实战:从微信读书API导出个人笔记与书架数据 2026/9/5 11:20:18

最新资讯

MATLAB股票价格预测:可验证建模工作流与工程化实践
从PyPI包到AI代理:codex-0.6.5.tar.gz的三种场景与实战部署
Footprint Tool:快速分析代码依赖与架构健康的开发利器
川大教务课程表安全获取与离线同步实践
SolidWorks国标焊件型材库安装配置与效率提升指南
Python+Flask+ECharts数据大屏实战:打通生产级可视化全链路

今日推荐

流式背压机制:避免前端渲染卡死与内存暴涨的滑动窗口限流
幂等性设计:在 Agent 自动重试与工具执行中的防重复扣费实战
向量检索与标量过滤混合查询:PostgreSQL pgvector 与 Milvus 的过滤下推实操

本周热门

备战数据库管理工程师校招:索引、事务、备份恢复核心考点解析
数字电路时序基石:深入理解建立时间与保持时间
蓝桥杯国赛超声波测距机:从单片机原理到嵌入式系统实战

本月精选

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

STM32省IO设计:旋转开关编码与Modbus浮点传输实战

发布时间:2026/9/5 11:20:18
STM32省IO设计:旋转开关编码与Modbus浮点传输实战 解决这种问题的思路其实挺直接的既然IO不够用那就想办法让一个IO干更多活要么编码、要么复用。我这次用的就是编码的思路把4个档位编码成2路IO的4种组合状态硬件上省事代码上也就一张查表的事。至于Modbus那个float拆分说白了就是字节序的问题但这里面坑不少尤其是大小端排列组合起来有好几种搞错了设备之间通讯就是乱码。这篇文章就把这两个事从头到尾捋一遍从硬件接线到代码实现再到调试时遇到的各种奇葩问题都记下来。项目本身是从一块基于STM32F103的板子开始的标准库环境需要采集一个4档旋钮开关的状态同时通过RS485走Modbus RTU协议把数据上报给上位机中间涉及float温湿度数据的打包传输。1. 旋转开关省IO采集的设计思路1.1 为什么要想办法省IO做嵌入式项目的朋友应该都有这种经历选型的时候觉得这颗MCU的IO够用了等真正把外设都接上去发现剩不了几个引脚。我这次遇到的就是这种情况板子上除了旋转开关还有两路继电器输出、两路光耦输入、一个DHT22温湿度传感器、一个OLED显示屏另外还预留了一个外部扩展接口算来算去可用IO就剩3个了。旋转开关如果按常规做法每个档位对应一个IO口4档就得用4个IO这显然不现实。有人说用ADC采样行不行当然也行但问题在于ADC采样的电压阈值受电源纹波影响容易误判如果档位开关老化接触不良中间态电压飘忽不定ADC判断很容易卡在阈值附近反复跳变普通档位旋钮是机械开关不带电阻网络的话ADC方案还得外接精密电阻成本并不低所以综合考虑我还是选了2路IO编码的方式4个档位刚好对应2的2次方每种组合对应一个档位硬件和代码都不复杂还省了2个IO出来。1.2 编码方案对比编码思路其实有好几种我先列个表格对比一下方案所需IO数优点缺点直接IO采集4个IO逻辑简单代码最简占用IO太多不划算ADC采样1个IO最省IO需要精密电阻分压易受干扰二进制编码加下拉2个IO省IO、速度快、判断稳定需要外部上下拉电阻二进制编码花式接法2个IO省IO且不用额外电阻接线方式要有讲究我最终采用的是第二种把旋转开关内部的4个触电重新接线让不同档位对应两路IO的“0/1”组合。这种方式在按键扫描、档位识别、拨码开关读取中都很常见本质就是一个2bit的编码器。理论上还有一种串行移位的方式通过一个IO配合时钟信号去读但那个需要额外芯片或者软件模拟协议对旋转开关来说有点杀鸡用牛刀了。2. 硬件搭建与软件实现细节2.1 接线方式旋转开关的引脚定义一般是一个公共端COM其余是各个档位的独立端。我的做法是把公共端接GND然后两个IO分别接在不同档位的触点上这样当开关旋到某个档位时公共端就会和对应的触点连通IO读到低电平其余IO读到高电平这里需要把IO配置为内部上拉输入。举个例子假设开关有4个档位A、B、C、D对应引脚分别为A、B、C、D公共端为COM。我这样接IO1 接到档位A和档位B的触点上IO2 接到档位A和档位C的触点上档位D就让它对应 1 1也就是IO1和IO2都为高电平这样档位IO1IO2A00B01C10D11逻辑上完全可行但有个细节要注意同一个IO接了两个触点会不会互相影响如果开关质量不好内部绝缘性差或者PCB走线距离太近确实可能有轻微漏电但实际测试下来问题不大。因为IO配置的是内部上拉输入模式是高阻态泄漏电流很小不至于引起逻辑翻转。2.2 GPIO初始化与读取代码上我用的是STM32F103标准外设库GPIO配置为输入上拉模式GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; // 上拉输入 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure);读取档位也很简单直接把两路IO的状态拼成一个字节然后查表uint8_t read_switch_position(void) { uint8_t io1 GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) ? 1 : 0; uint8_t io2 GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) ? 1 : 0; uint8_t code (io1 1) | io2; switch (code) { case 0x00: return 1; // 档位1 case 0x01: return 2; // 档位2 case 0x02: return 3; // 档位3 case 0x03: return 4; // 档位4 default: return 0; // 错误状态 } }这段代码看起来平平无奇但真到调试的时候你要是直接把IO状态读上来就判断大概率会被机械开关的抖动折磨疯。别问我怎么知道的踩过坑才长记性。2.3 防抖处理——比想象中重要机械旋转开关和按键一样在档位切换的瞬间触点会经历一个“先断开、再弹跳、最后稳定”的过程。如果主循环查询的速度足够快会读到很多中间状态从而导致业务逻辑误判。比如我在测试档位切换时就遇到过明明是1档切到2档程序却瞬间读到了0x03编码差点被当成4档触发了一次数据上报。防抖处理我总结了三板斧第一板斧连续采样。每次检测都连续读几次取中间值或者占多数的那一个。我一般是连续读5次每次间隔2ms如果5次读到的编码都一样才认为档位稳定。第二板斧状态保持。用一个变量记录上一次的有效档位只有当这次的档位和上次不同并且已经连续5次都稳定才更新内部状态。这样可以避免在切换过程中反复横跳。第三板斧延时确认。机械开关通常抖动时间在5~10ms所以我第一次检测到档位变化后不立即处理而是延时15~20ms再确认一次。确认无误后再执行对应的业务逻辑。三个方案可以叠加使用也可以根据实际情况只选一个。对于大多数场景第一板斧加大大约10ms的延时就够了再多就有点浪费CPU时间了。2.4 一个隐藏Bug上拉还是下拉我在调试过程中还遇到过一个莫名其妙的现象板子一上电旋转开关明明在1档程序却报0也就是读到了0x03。排查了大半天最后发现是GPIO配置成了浮空输入而不是上拉输入。如果把STM32的GPIO配置成浮空输入引脚外部又没有接上拉或下拉电阻那么IO会呈现高阻态这个时候机械开关的触点本身如果存在微弱的漏电流或者线上有感应噪声读到的电平就是随机的。所以要强调一下提示机械开关接入MCU时输入模式一定要设置内部上拉或外部上拉否则上电瞬间极易读到随机电平导致档位误判。3. Modbus中float的拆分与还原3.1 问题背景项目里有温湿度数据是float类型的DHT22读出来直接就是温度和湿度的浮点值。上报给上位机时要走Modbus RTU协议而Modbus协议本身是以16位的寄存器为基本单位的。一个float占32位所以一个float必须拆成两个16位寄存器来传输。这看起来是个非常基础的协议处理问题但实际项目中多少人栽在字节序上我都不用去统计。Modbus从站调试工具一接读上来的数值不是天价就是负数十有八九就是字节顺序搞反了。3.2 IEEE 754浮点格式快速回顾在动手拆分之前有必要把float的存储格式再复习一遍。IEEE 754标准规定单精度浮点数用32位表示位数含义第31位符号位0正1负第30~23位指数部分8位偏移量为127第22~0位尾数部分23位比如12.34这个数转换成十六进制的存储形式是0x414570A4。如果用联合体看一下这4个字节在内存里怎么排不同平台会得到不一样的结果这就引出了字节序问题。3.3 字节序模型Modbus协议规范里没有强制规定float的字节序这就尴尬了。业界通常按字的顺序和字节的顺序分成了4种排列方式大端字序 大端字节序ABCD大端字序 小端字节序BADC小端字序 大端字节序CDAB小端字序 小端字节序DCBA其中ABCD表示4个字节按从高到低的顺序存放BADC表示16位字内部字节交换CDAB表示两个16位字的顺序交换而DCBA则是最彻底的小端模式。绝大多数工业现场用的是ABCD也就是“大端模式”Modbus Poll这种上位机工具默认也支持切换不同的字序/字节序调试的时候只要多看几种组合很快就能确定对端采用的是什么排列。3.4 用联合体做拆分还原最直观的拆分还原方式就是联合体。C语言里float型变量和uint8_t数组共用一块内存直接读数组内容就是float的原始字节typedef union { float f; uint8_t bytes[4]; } FLOAT_BYTE_UNION;发送的时候FLOAT_BYTE_UNION fbu; fbu.f temperature; // 赋值浮点数 // 取出 fbu.bytes[0] ~ fbu.bytes[3] 发送接收的时候fbu.bytes[0] reg_buf[0]; fbu.bytes[1] reg_buf[1]; fbu.bytes[2] reg_buf[2]; fbu.bytes[3] reg_buf[3]; temperature fbu.f;注意联合体的本质是“复用内存”所以这种方式依赖目标平台的字节序。如果MCU是小端模式STM32就是那么bytes[0]存的是float的最低字节bytes[3]是最高字节。如果协议要求的是大端传输那就要做一个字节顺序调整。3.5 用位运算做拆分还原联合体方式代码简洁但有个隐患它依赖编译器和硬件平台的字节序换了平台代码可能就不对了。所以在写跨平台代码或者要交给客户二次开发的时候我更推荐用位运算把字节序的处理显式地写出来。拆分float转两个寄存器采用大端顺序void float_to_registers(float val, uint16_t *reg_hi, uint16_t *reg_lo) { uint32_t raw; memcpy(raw, val, 4); // 把float的bit pattern复制到uint32_t uint8_t b0 (uint8_t)(raw 24); // 最高字节 uint8_t b1 (uint8_t)(raw 16); uint8_t b2 (uint8_t)(raw 8); uint8_t b3 (uint8_t)(raw); *reg_hi (uint16_t)((b0 8) | b1); // 高16位 *reg_lo (uint16_t)((b2 8) | b3); // 低16位 }还原两个寄存器转floatfloat registers_to_float(uint16_t reg_hi, uint16_t reg_lo) { uint8_t b0 (uint8_t)(reg_hi 8); uint8_t b1 (uint8_t)(reg_hi); uint8_t b2 (uint8_t)(reg_lo 8); uint8_t b3 (uint8_t)(reg_lo); uint32_t raw ((uint32_t)b0 24) | ((uint32_t)b1 16) | ((uint32_t)b2 8) | b3; uint32_t bits raw; float val 0.0f; memcpy(val, bits, 4); return val; }这里的memcpy不是多余的在C语言里直接对一个uint32_t变量做强制类型转换到float指针是很危险的行为除非你明确知道编译器是怎么处理这种alias的。用memcpy把同样的二进制位重新解释成float符合C标准的type punning规则尽量避免未定义行为。3.6 什么是AB/CD/CDAB/DCBA模式这里展开说一下排列组合的问题因为很多初学Modbus的朋友就是在这块迷路的。假设float的原始字节序列内存顺序是byte0 —— 最低字节LSBbyte1byte2byte3 —— 最高字节MSB对于STM32这种小端MCU内存顺序是 byte0、byte1、byte2、byte3。Modbus的寄存器一次传16位。假设协议要求第一个寄存器地址N传高16位第二个寄存器地址N1传低16位这是最常见的“高字在前”方式寄存器N (byte2 8) | byte3寄存器N1 (byte0 8) | byte1这就是ABCD模式也是大多数Modbus主站默认的模式。CDAB模式则是低字在前寄存器N (byte0 8) | byte1寄存器N1 (byte2 8) | byte3不管是哪种模式只要主站和从站约定一致通讯就不会有问题。怕就怕一个用了ABCD另一个用了CDAB读出来的数据就变成天价或负值了。4. 工程实操把两个问题组合在一起4.1 整体数据流设计回到我的项目两件事其实是串在一起的旋转开关负责设定设备的工作模式温湿度传感器负责采集环境数据Modbus负责把模式和温湿度数据都上报给上位机。数据流大致是这样的主循环检测旋转开关档位得到mod字段DHT22定时采集温湿度得到float数据将档位写入保持寄存器将温湿度拆分写入两个或四个保持寄存器上位机通过Modbus RTU请求这些寄存器从站应答时按协议顺序把float拆好的字节放回去寄存器地址规划寄存器地址内容数据类型0x0000设备模式档位uint16_t0x0001温度高16位uint16_t0x0002温度低16位uint16_t0x0003湿度高16位uint16_t0x0004湿度低16位uint16_t这里要注意寄存器地址是按0开始还是按1开始取决于用的是什么Modbus库。像FreeModbus默认是0基地址而上位机端很多组态软件习惯1基地址对应关系要提前确认好不然后面调试地址错位非常头疼。4.2 与FreeModbus库的结合STM32标准库环境我移植的是FreeModbus v1.6通过RS232转RS485接到上位机。协议栈本身处理了CRC校验、地址匹配和功能码分发我们需要关心的只是寄存器回调函数里怎么填数据。在写保持寄存器回调时eMBErrorCode eMBRegHoldingCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode) { USHORT usRegIndex usAddress - MB_REG_HOLDING_START; if (eMode MB_REG_WRITE) { // 写寄存器一般用于修改配置这里我们暂时不支持 } else { // 读寄存器从我们的寄存器数组里拷贝数据 USHORT *pRegs (USHORT *)holding_regs[usRegIndex]; for (USHORT i 0; i usNRegs; i) { *((USHORT *)pucRegBuffer i) pRegs[i]; } } return MB_ENOERR; }然后主循环里更新寄存器数组FLOAT_BYTE_UNION t_un; FLOAT_BYTE_UNION h_un; t_un.f temp_value; h_un.f humi_value; holding_regs[0] switch_mode; holding_regs[1] (t_un.bytes[3] 8) | t_un.bytes[2]; holding_regs[2] (t_un.bytes[1] 8) | t_un.bytes[0]; holding_regs[3] (h_un.bytes[3] 8) | h_un.bytes[2]; holding_regs[4] (h_un.bytes[1] 8) | h_un.bytes[0];我这个写法是基于联合体把float的4个字节直接取出来拼寄存器。如果你的MCU是大端模式那高低字节的排列要反过来这个要注意。4.3 上位机验证调试Modbus通讯时手里必备的工具除了串口助手还有就是Modbus Poll和Modbus Slave这两个软件。我用Modbus Poll当主站去读从站的数据用来验证从站返回的寄存器值是否正确。输入寄存器地址和功能码之后关键是设置数据格式。Modbus Poll里在Display菜单下可以选择数据格式如果你把两个寄存器组合成Float注意它默认的字节序是ABCD如果你的从站实际发的是CDAB模式就要在设置里切换一下或者干脆在从站端改代码统一成ABCD。这里给一个我在现场调试时验证float拆分的土办法把温湿度传感器放到已知的环境中比如室温应该是25.3℃然后用Modbus Poll读寄存器把读到的4个字节手动拼一下用计算器转成float看看是不是25.3。虽然土但好用百试百灵。5. 调试实录踩过的坑与排查技巧5.1 典型问题速查调试过程中遇到的最多的几类问题及其原因我整理成了表格方便大家对照排查现象可能原因解决办法旋转开关读出的档位随机跳变IO配置成浮空输入没有上拉配置内部上拉或外部加上拉电阻档位切换瞬间触发多次业务动作机械触点抖动未做防抖连续采样 延时确认Modbus读上来的温度是超大值字节顺序不匹配尝试AB/CD/CDAB/DCBA四种组合Modbus读上来的温度是0.0或接近0float未正确写入保持寄存器或地址偏移错误检查寄存器地址映射用仿真器看内存485总线挂多设备无法通讯终端电阻缺失或地址冲突检查120欧终端匹配电阻确认从站地址唯一从站能收到请求但不回复CRC计算错误或波特率不一致用逻辑分析仪抓串口数据人工校验CRC5.2 一个让我纠结了一晚上的问题这次调试中最纠缠我的一个问题出在Modbus上。热电偶的数据读出来是正常的但温度float一上Modbus就变成了“-8569.4”这种离谱的负数。最开始我以为是符号位出问题了但后来用串口抓包看原始字节发现数据本身是好的问题出在我把float拆成寄存器时的字节顺序搞错了。具体来说我板子上的STM32是小端存储我习惯性地认为协议传输也用同样顺序然后直接把内存里的字节按顺序拆开填到寄存器里。结果变量名看得好好的实际发出去的顺序却是反的。上位机按大端解析自然就读出了怪异的数据。所以我在这里再次强调一下注意MCU内存的字节序和Modbus协议传输的字节序是两回事不能默认相同。写代码的时候一定要明确自己最终采用的是哪种字节序排列并且在协议文档中注明否则后续维护的人会骂街的。5.3 排查技巧用好串口抓包和Wireshark排查Modbus问题光靠肉眼看数据是不够的我推荐几个组合拳USB转485模块 串口调试助手最基础的排查工具。看从站有没有回复、CRC是否正确、返回的寄存器字节是什么。串口助手最好带HEX显示功能。Modbus Poll Modbus Slave联调一个当主站一个当从站隔离问题。如果你是做从站直接用Modbus Slave模拟一下确认Wireshark之类工具抓包时发送和接收是否符合预期。逻辑分析仪抓串口波形如果波特率设置不对、收发时序有问题逻辑分析仪可以一目了然。能看到起始位、停止位自己数一下位宽对不对。我之前排查过一次485通讯问题主机和从机单独测试都正常接到一起就通讯不上。后来用示波器看A/B线波形发现是地电位不一致主从两边没共地导致485电平乱飞。加上一根地线之后问题立刻消失了。像这种玄学问题没有示波器的话真的很难定位。5.4 关于省IO的扩展思路这次用2路IO编码读4档旋钮属于一种通用的“IO扩展”思路。如果之后遇到更多档位或者更多按键还可以继续扩展3路IO可以编码8种状态用74HC165这种移位寄存器一个IO也能读8个甚至16个开关如果只是做按键矩阵可以用行列扫描不过这里要提醒一点编码方案适合开关状态变化不频繁的场景如果是旋转编码器那种连续增量输出就不适合用这个方案了应该用正交解码或者外部计数中断。在嵌入式项目里IO省下来可以做什么比如我这次省下两个IO一个用于外接一个LED状态灯一个用于串口下载模式切换都是很实际的需求。6. 一些实战中的个人体会说了这么多最后聊聊我在这些调试中的一些真实感受。IO编码这件事很多人第一反应是“程序里一个switch就搞定了”但真正到现场机械开关的抖动、上电瞬间的毛刺、PCB走线的耦合噪音会让你觉得世界充满了恶意。有一次我测试旋转开关发现偶尔档位识别错误抓了半天最后发现是板子上一个继电器动作时产生的电磁干扰通过空间耦合到了开关的线上。后来我在IO线上各加了一个100nF的电容到地情况才彻底消失。以后凡是接机械开关的IO我都会预留一个电容的焊盘位这个小习惯在EMC测试中帮了我好几次。Modbus float这块我最大的体会是协议本身不复杂复杂的是一堆设备厂商各自为政的字节序定义。我去现场调试的时候绝对不会只看对方提供的协议文档就算完了一定会用Modbus Poll实测一遍把所有可能的字节序排列都试一遍以实际读到的数值为准。因为文档更新不及时、代码注释和实际不符的事情太常见了。如果你也在做类似的项目我的建议是先花半天时间把硬件的接线图和GPIO分配画清楚再花半天时间把Modbus协议的数据帧格式和字节序约定写明白这两个工作做完后边的软件调试会顺畅非常多。嵌入式调试说到底是慢工出细活急不来的。

关于恒美微站

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

快速链接

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

服务项目

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

联系方式

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

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