ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32 ADC电压采集通过RS485发送到PC上位机的完整方案

STM32 ADC电压采集通过RS485发送到PC上位机的完整方案 简介面向嵌入式开发者和STM32初学者的ADC采集与RS485通信示例工程帮助解决工业现场模拟量采集后长距离传输至PC的需求重点演示ADC多通道采样、串口发送及485接口时序控制也适合作为物联网数据采集节点的学习原型。资源为完整MDK工程共185个文件包含32个C源文件、32个头文件以及编译生成的axf、hex、map等二进制与烧录文件还有Keil工程配置文件整体仅4.4MB打开即可查看工程结构和直接编译下载。项目基于标准外设库覆盖ADC、定时器、Flash、I2C、CAN等多个驱动模块用户可对照学习外设初始化流程和寄存器配置思路尤其可参考UART与485方向控制引脚的处理方式在此基础上扩展Modbus协议或自定义帧格式快速搭建采集上报系统。目前已有669人学习下载适合课程设计、毕业设计或实际项目预研参考。 做一个STM32的ADC电压采集通过RS485总线发给PC上位机显示这个需求在工业现场和实验室设备里太常见了。很多刚接触这块的朋友以为难点在ADC的采样精度上结果真正做起来反而被RS485的半双工通信折腾得够呛。这篇文章把我自己完整跑通的一套做法写出来从硬件接线、CubeMX配置、HAL库代码、数据帧设计到上位机收包排查一条链路走通适合刚入手STM32的开发者也适合想快速搭一套遥测demo的工程师。1. 方案选型为什么这套组合适合工程现场数据采集1.1 为什么是RS485总线而不是TTL串口或无线先解决一个很基本的问题STM32本身有USART串口直接引出TTL电平也能发数据给PC为什么非要加一颗RS485收发芯片因为TTL串口在工程现场基本没法用。TTL串口的0和1是相对于参考地的单端电平距离稍微拉长一点线缆上的压降、电机的电磁干扰、地线电位差都会让信号彻底变形。而且TTL是点对点通信一台PC只能对应一个设备扩展性为零。RS485采用的是差分信号传输A和B两根线之间的电压差来表示逻辑电平天然具备很强的共模干扰抑制能力。在9600波特率这种比较保守的速率下传输距离可以到1200米左右普通工业环境完全够用。再加上RS485总线是半双工共享总线结构一条总线上最多可以挂32个节点做一主多从的分布式采集特别顺手。至于无线方案2.4G、LoRa、WiFi在特定场景下确实有优势但工业现场里无线链路的稳定性和确定性始终不如有线总线而且很多老旧厂区对无线信号并不友好。所以从可靠性和后期维护角度出发RS485依然是工程现场数据采集的主流选择。1.2 这套方案的边界什么时候能用什么时候得换方案用STM32内置ADC加RS485这套组合最适合的是实时性要求不是特别极端的中低速采集场景。比如温度传感器电压输出、电位器位置反馈、电流采样电阻两端电压、电池电压监测这类信号的带宽本身很低采样率几百赫兹以内就绰绰有余。如果项目要求每通道几十kSPS以上的连续高速采样那就要慎重了。一是STM32内置12位ADC的采样率上限摆在那里二是RS485在115200波特率下每秒也就能传大约11KB的数据算上帧头帧尾和校验位实际有效吞吐还要打折。这种情况通常要考虑改用局域网传输或者直接把采集系统做成一个独立的板卡。另外ADC精度也要有预期。STM32内置ADC是12位分辨率在3.3V参考电压下理论最小分辨力大约是0.8mV实际做到稳定读数的精度在十几毫伏级别就不错了。如果项目要求16位甚至24位的高精度采集得外挂ADS1115、ADS1256这类专用ADC芯片不是单纯靠软件滤波能解决的。2. 硬件连接RS485收发器与ADC引脚的几个关键细节2.1 硬件清单与引脚分配以我常用的STM32F103C8T6最小系统板为例整套方案需要的硬件很少STM32F103C8T6最小系统板一块485收发器模块一个推荐SP3485或MAX34853.3V供电版本一个模拟电压源比如精密电位器分压电路或者传感器的电压输出PC端需要一个USB转485模块市面上常见的CH340加MAX485方案就可以引脚分配方面我的习惯用法是这样的功能STM32引脚对端ADC采集通道0PA0模拟电压输入ADC采集通道1PA1模拟电压输入USART1发送PA9485模块DIUSART1接收PA10485模块RO485方向控制PB1485模块DE/REPA0和PA1是ADC1的常规通道PA9和PA10是USART1的默认引脚PB1随便挑了一个普通GPIO控制方向。如果你手头的板子引脚被占用可以选其他ADC通道和USART但要注意有些引脚是JTAG复用口比如PA13到PA15、PB3和PB4默认情况下用作调试接口要当普通GPIO用必须先关掉JTAG功能这个细节容易被人忽略。2.2 分压电路ADC输入电压范围别超限STM32的ADC输入电压范围是0到VREF开发板上通常就是0到3.3V。如果你的被采集信号超过了3.3V必须得分压。以采集0到10V为例两个电阻分压就行一个10k一个4.7k分压系数大概是4.7/(104.7)约等于0.3210V输入被分到大约3.2V留了一点余量。但分压不是简单把电压变小就完事了。ADC内部有一个采样保持电容每次采样前都要通过模拟开关把外部信号源接入给这个电容充电。电阻分压后的等效源阻抗如果太高充电时间不够采样结果就会偏低而且这个偏差和信号幅度有关系不是简单乘个系数能校准的。所以分压电阻的取值不能太大。经验做法是源阻抗尽量控制在10k以下。如果必须用大电阻分压可以去CubeMX里把ADC的采样时间拉到最大比如239.5周期给采样电容足够长的充电时间。采样时间和源阻抗的关系在ST的参考手册里有专门表格说明实际调试时可以逐个采样时间档位对比读数变化。2.3 RS485接线里最常见的三个失误RS485的硬件接线看着简单就A和B两根线但我见过太多人在这里翻车。第一个是A/B接反。485收发器的差分输入是有极性的A对应同相端B对应反相端接反之后收端完全解调不出数据。排查方法也很简单拿一个USB转485模块和你的板卡直连如果收不到数据先把A/B对调试试这是最快的验证方式。第二个是漏接参考地。很多人以为RS485是差分信号就不需要共地了其实这是误解。差分信号能抑制的是共模干扰但收发双方的GND还是需要连在一起的否则总线上的共模电压会超出收发器的输入范围。尤其是非隔离方案GND必须通。只有用了带隔离的收发器比如加了数字隔离芯片的方案才可以不共地。第三个是终端匹配电阻的问题。RS485标准要求在总线两端各并一个120欧电阻用来吸收信号反射。如果只是桌面调试、线长不超过一两米不加匹配电阻也能工作但距离超过几十米或者波特率上到115200之后反射信号会导致误码。另外要注意的是匹配电阻只加在总线物理最远端不是每个节点都加节点加多了反而会把总线压死。3. STM32CubeMX里的ADC与串口配置别漏看采样时间这一项3.1 ADC配置扫描模式、采样时间与校准用CubeMX生成工程是最省事的几步就能把外设配置完。以HAL库为例在Pinout页面把PA0和PA1配置为ADC1的IN0和IN1然后到Analog选项卡里打开ADC1。在ADC1的Configuration界面里有几个参数值得注意。Resolution保持12位就行这是STM32F1的上限。Scan Conversion Mode要打开这样可以把IN0和IN1放在一组规则序列里依次转换。Number Of Conversion根据你实际用了几个通道填2。Continuous Conversion Mode这里有个选择如果取一个通道就跑一次可以不开连续模式如果打算一次扫完两个通道建议打开连续转换模式配合定时器触发或者主循环里手动读取。External Trigger可以选择软件触发简单可靠缺点是要在代码里显式调用启动函数。Sampling Time是很多人容易忽略的一项。在上面的章节里已经提到采样时间越长前级等效源阻抗的容错越大。我的习惯是如果信号源阻抗不确定直接保守一点选55.5周期以上如果前面接了大的分压电阻直接选239.5周期。采样时间变长带来的唯一代价是单次转换时间增加但对于这个项目里几百赫兹的采样频率来说根本感觉不到。CubeMX生成代码后在main函数开头不要忘记调用校准函数。STM32F1的ADC有一个上电校准流程校准不执行虽然也能转换但结果可能会有零点漂移。校准代码就是HAL_ADCEx_Calibration_Start(hadc1);这行必须在ADC启动之前调用而且每次上电后只需要执行一次。3.2 串口与方向控制引脚配置USART1的配置就简单了Mode选择Asynchronous波特率先设9600。前面说过9600在RS485现场是最保守的选择长距离稳定性最好。如果只是桌面短距离测试想快一点也可以设成115200但正式用于工业现场还是建议从9600起步。参数里8位数据位、无校验、1位停止位也就是常说的8N1是最通用的配置。Word Length选8 BitsParity选NoneStop Bits选1。如果总线上以后要挂多个厂家设备除了常见的Modbus协议外多数设备默认就是8N1兼容性最好。方向控制引脚PB1在CubeMX里配为GPIO_Output初始电平尽量设为低也就是默认让485收发器处于接收状态。这个细节比较重要如果上电瞬间DE引脚处于不确定状态收发器可能误入发送模式把总线拉死。4. 代码实现滤波、组帧与半双工方向切换4.1 用HAL库读取ADC并做滑动滤波CubeMX生成好工程后核心逻辑都在main.c里。读取ADC的代码不复杂但有一个容易踩的坑HAL库的读取流程是启动转换、等待完成、读取结果单次模式下每次读一个通道都要重新Start一次。我以前见过有人写代码只Start了一次然后在主循环里只读GetValue读到的一直是第一次的转换结果。正确做法是这样uint16_t adc_read(uint32_t channel) { ADC_ChannelConfTypeDef sConfig {0}; sConfig.Channel channel; sConfig.Rank 1; sConfig.SamplingTime ADC_SAMPLETIME_55CYCLES_5; HAL_ADC_ConfigChannel(hadc1, sConfig); HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); return HAL_ADC_GetValue(hadc1); }这个函数每次调用时重新配置通道和采样时间然后以阻塞方式等待转换完成。对于低速采集来说完全够用逻辑也清晰。原始ADC值是0到4095之间的整数要转成电压值需要乘以参考电压再除以4096。这里有个细节虽然开发板上VREF接的是3.3V但这个3.3V往往不是标准值。板上的3.3V通常由1117这类LDO从5V转出来不同板子、不同负载电流下实际电压可能有几十毫伏的偏差换算出来的电压值就会整体偏移。解决方式有两种。要求不高的话直接用3.3V算简单要求高一点就拿万用表实测一下板上3.3V引脚的实际电压比如测到3.28V就把换算公式里的3.3改成3.28。这个替换对精度提升非常明显。电压值本身还有噪声尤其是直接从电源取电的情况下ADC读数会上下跳动好几个LSB。我习惯在软件里加一个简单滑动滤波取最近N次采样求平均。N取多少合适对于工频干扰占比大的场合N取64左右效果比较好但会增加响应延迟如果实时性要求高取16到32也可以。代码实现#define FILTER_N 32 uint16_t adc_filtered(uint32_t channel) { uint32_t sum 0; for (int i 0; i FILTER_N; i) { sum adc_read(channel); } return (uint16_t)(sum / FILTER_N); }把多次采样结果累加再平均效果立竿见影。需要注意的是累加变量sum一定要用uint32_t哪怕是32次乘以4095也超过了uint16_t的范围。4.2 数据帧结构设计与校验把电压值直接通过串口发出去确实能让PC端收到数据但会面临几个问题一是PC端无法确认数据边界流里可能丢字节数据就对不齐了二是如果以后总线上挂多个设备PC端无法区分数据来自哪个节点。所以必须设计一个简单的数据帧。我常用的帧结构如下字节含义值0帧头10xAA1帧头20x552通道号0x01或0x023电压值高字节放大100倍后的整数4电压值低字节同上5校验字节前5字节异或结果用连续两个帧头0xAA 0x55来同步基本能避免数据错位。电压值放大100倍是为了保留两位小数比如3.28V发出去就是328用两个字节表示高字节是0x01低字节是0x48这样整数范围可以覆盖0到65535对应0到655.35V完全够用。校验用最简单的异或校验把所有字节按位异或得到一个字节作为校验位加在帧尾。PC端收到一帧后做同样的异或运算比对结果不一致就丢弃这一帧。对于单字节错乱的场景异或校验的检出概率还是很高的。以后如果想更稳妥可以换成CRC16但复杂度就上去了。组帧和发送的逻辑可以单独写一个函数void send_voltage_frame(uint8_t ch, uint16_t voltage_x100) { uint8_t frame[6]; frame[0] 0xAA; frame[1] 0x55; frame[2] ch; frame[3] (voltage_x100 8) 0xFF; frame[4] voltage_x100 0xFF; frame[5] frame[0] ^ frame[1] ^ frame[2] ^ frame[3] ^ frame[4]; rs485_send(frame, 6); }4.3 半双工方向切换的时序陷阱这是整个项目里最容易翻车的地方。RS485收发器是半双工结构同一时刻要么发送要么接收。MAX3485这类芯片的DE和RE通常并在一起用一个GPIO来控制引脚拉高进入发送模式拉低回到接收模式。发送一帧数据的流程是先把方向引脚拉高然后通过UART发送发完后再把方向引脚拉低。问题就出在“发完后再拉低”这个时机上。UART发送寄存器和移位寄存器是两级结构你往数据寄存器里写一个字节数据要等移位寄存器把当前位全部移完才开始移出。如果发了最后一个字节后立刻拉低方向引脚最后一个字节很可能还没完全从TXD引脚上输出出去总线上的数据就被截断了PC端收到的就是丢尾的坏帧。在HAL库的阻塞式发送函数里情况会好一些void rs485_send(uint8_t *data, uint16_t len) { HAL_GPIO_WritePin(RS485_DIR_GPIO_Port, RS485_DIR_Pin, GPIO_PIN_SET); HAL_UART_Transmit(huart1, data, len, 1000); HAL_GPIO_WritePin(RS485_DIR_GPIO_Port, RS485_DIR_Pin, GPIO_PIN_RESET); }HAL_UART_Transmit在阻塞模式下发送完所有字节后会等待TCTransmission Complete发送完成标志置位才返回。也就是说函数返回时最后一个字节已经完整地从移位寄存器移出了这时再拉低方向引脚是安全的。但如果你的工程用的不是阻塞模式而是中断模式或者DMA模式这个隐藏的坑就非常明显了。中断模式下HAL_UART_Transmit_IT是立即返回的在一个空循环里就是设置方向为发送、调用Transmit_IT、然后马上下一次循环又把方向拉低了数据根本没发出去。DMA模式同理。正确做法是在发送完成回调函数里切换方向void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { HAL_GPIO_WritePin(RS485_DIR_GPIO_Port, RS485_DIR_Pin, GPIO_PIN_RESET); } }这个回调是HAL库在DMA或中断发送完成后自动调用的在这个时机拉低方向引脚时序上才是正确的。这个细节可以说是我在这个项目里踩得最深的一个坑。5. 上位机收包与实测中的故障排查5.1 PC端接收串口工具和Python脚本PC端接收需要两个部分一个USB转485硬件模块和一个能解析数据的工具。硬件上我用的是一块带CH340和MAX485的模块USB口插电脑A和B端子分别接到STM32一端的A和BGND也一并接上。这里有个容易忽略的点USB转485模块的GND和板卡的GND必须连通否则即使A/B接对了也可能收不到数据。调试初期直接用串口调试助手看原始字节是最快的。设置串口参数为波特率9600、8位数据、无校验、1位停止位打开串口后应该能看到一行行以AA 55开头的6字节数据流。如果看到的数据里AA 55反复出现说明链路已经通了剩下的就是解析问题。如果要做一个正式的PC上位机用Python写一个pyserial解析脚本是最快捷的方式import serial ser serial.Serial(COM3, 9600, timeout1) def parse_frame(data): if len(data) 6: return None if data[0] ! 0xAA or data[1] ! 0x55: return None checksum 0 for b in data[:5]: checksum ^ b if checksum ! data[5]: return None ch data[2] voltage (data[3] 8 | data[4]) / 100.0 return ch, voltage while True: data ser.read(6) result parse_frame(data) if result: ch, voltage result print(f通道{ch}: {voltage:.2f}V)这段脚本从串口读取固定长度6字节然后验证帧头、校验字节通过校验后把放大100倍的电压值还原成真正的电压值打印出来。把其中打印部分替换成曲线绘制就变成了简易的示波器界面。5.2 实测故障排查清单这个项目从零开始到跑通可能遇到的故障基本都能归到下面几类里。我把排查思路整理成表格按顺序排查是最省时间的现象可能原因排查方法PC端完全收不到数据串口号选错、A/B反接、共地缺失、波特率不一致先用USB转485模块单独短接收发测试收到的全是乱码波特率不一致、A/B极性接反、接地不可靠确认两端都是9600/8N1对调A/B检查GND数据缺尾或丢帧DE/RE切换时序太早确认收到的帧是否总是缺最后一字节改用Txcplt回调切方向电压值偏小或跳变源阻抗太大、采样时间太短、参考电压不准采样时间调大用万用表实测VREF电压每次上电前几帧异常上电瞬间DE未初始化导致总线冲突将PB1初值设为低电平跑一段时间后通信中断总线缺终端电阻引起反射、供电不稳两端加120欧匹配电阻检查电源其中“收不到数据”很多人一上来就怀疑是485电路坏了其实第一步应该先绕过485把STM32的PA9直接接一个USB转TTL模块看看串口能不能收到数据。如果TTL直连能收到而485不行问题基本锁定在485收发器那一侧如果TTL直连本身就收不到那问题在STM32的串口配置或代码逻辑上。这样做能快速缩小问题范围比自己拿着万用表瞎猜高效得多。6. 扩展思路这套设计还能怎么升级6.1 从点对点到一主多从的组网改造目前的方案是单块STM32对着PC端如果想挂多个采集节点RS485总线天生就支持。需要做的改动有两处一是帧结构里加上地址字段每个从机拥有唯一地址二是PC端作为主机下发带地址的查询指令各从机收到指令后只有地址匹配才发送数据。物理层到应用层的思路都变了PCB部分不需要任何改动只需要在总线上再并接几个节点。这样做要注意总线的节点容量问题。标准RS485收发器芯片每个节点负载约1个单位一条总线最多支持32个节点。如果节点数量超过32要换用低负载型收发器比如单位负载1/4甚至1/8的型号。6.2 采样策略和上位机显示的进阶方向如果觉得主循环里用HAL_Delay做定时采样不够精确可以改成定时器触发ADC采样。CubeMX里把ADC触发源从软件触发改为定时器的TRGO事件比如用TIM2更新事件触发ADC采样采样周期完全由定时器的更新中断决定稳定性远好于软件延时方式。把采样结果用DMA搬运到内存主循环只负责解析和发送CPU负载还能再降一截。PC端显示方面从打印文本变成绘制实时曲线能直观看到电压波形变化。Python生态里matplotlib的动态绘图或者pyqtgraph都可以做到把每一帧解析出来的电压值追加到缓冲区定时刷新绘图。如果以后数据量大了可以用InfluxDB加Grafana做成历史趋势记录。6.3 写在最后的个人经验整套方案做下来我最想强调的还是RS485方向切换这个问题。半双工通信的时序问题不像编译报错那么显眼它更隐蔽——看起来程序逻辑都对但你观察波形就会发现最后一个字节往往被截断。调试时拿一个示波器同时看DI引脚和A/B之间的电压差能很直观地看到发送和方向切换的时序关系。另一个体会是嵌入式项目里软件逻辑往往不是瓶颈硬件的电气细节才是。A/B极性、共地、终端电阻、采样时间这些参数单个拿出来都很不起眼但叠在一起就会造就“为什么我的数据老是丢一两个字节”“为什么我测的电压偏了0.2V”这种困惑。把每个环节验证清楚了整套系统才能稳定跑起来。本文还有配套的精品资源点击获取
返回列表