做嵌入式这些年,ADC采样和CAN通信通常是被分开对待的:ADC负责把物理世界变成数字,CAN负责把数字搬到另一个节点。但真正到了“双结点控制”这种项目里,这两件事必须放在一起设计——采样节奏、报文格式、节点间的数据同步、甚至一块PCB上的布局,全都互相牵扯。我最近完成的一个P3项目就是这样一个典型的ADC/CAN双结点控制系统,过程中把采样周期、CAN时序、报文ID分配、PCB布局这些环节逐个抠了一遍。这篇文章完整记录整个设计过程,适合正在做STM32或类似平台双机通信项目的朋友参考,尤其是那些ADC数值不稳定、CAN通信偶发错误、两个节点数据对不上的情况。
1. 双结点系统的全貌:先搞清楚数据往哪流、谁说了算
做这类项目最容易犯的错是一上来就写代码,其实最该先做的是把两个节点之间的“职责边界”画清楚。双结点控制不是简单的一发一收,而是两个节点各自拥有传感器和执行器,它们需要通过CAN总线交换数据、协同动作。设计的第一步就是定义清楚谁是主节点、谁是从节点,以及每个节点上ADC采集的数据到底要在哪里被使用。
1.1 两个节点的数据流与控制职责划分
在我这个P3项目里,节点A负责采集两路模拟信号(一路温度传感器、一路电位器给定值),节点B负责采集一路电流信号,同时控制一个PWM执行机构。控制策略放在节点B执行,但节点A的温度数据需要通过CAN实时传给节点B参与运算。这就形成了一个典型的不对称双节点结构:节点A是“采集密集型”,节点B是“控制密集型”。
这种划分直接决定了后续的硬件选型和软件架构。节点A的MCU主频可以低一些,但ADC的采样精度和稳定性要求更高;节点B的MCU要留足CPU资源给控制算法,CAN中断处理不能占用太多时间。我用的两片都是STM32F103系列——成本低、资料多、CAN控制器和ADC外设都有,用来验证整个闭环系统足够了。
1.2 资源分配与引脚规划表
开始画板之前,我把两个节点的外设资源全部列成了表格。这一点强烈建议照做,因为ADC输入引脚和CAN引脚的复用关系很容易在设计后期发现问题。
| 资源项 | 节点A | 节点B |
|---|---|---|
| MCU | STM32F103C8T6 | STM32F103RCT6 |
| ADC通道 | PA0(温度)、PA1(给定值) | PA4(电流) |
| ADC位数/模式 | 12位,单次转换+扫描 | 12位,单次转换 |
| CAN外设 | bxCAN1,PA11/PA12 | bxCAN1,PA11/PA12 |
| PWM输出 | 无 | PA8(TIM1_CH1) |
| 与外部交互 | RS232调试口 | 按键+OLED |
这样一张表看着简单,但能规避一个非常隐蔽的坑:STM32F103的PA11和PA12默认是USB引脚,同时也是CAN_RX和CAN_TX。如果你开了USB功能又想用CAN,引脚就会打架。我早期调试时曾经因为初始化了USB库导致CAN完全收不到数据,后来把USB相关代码全部关掉才正常。
1.3 为什么一定要先把系统架构定下来再动手
双结点控制项目的复杂度不在于单个结点,而在于两个结点“如何对齐”。如果先各写各的代码,等联调时再统一报文格式和采样触发时刻,往往要返工。正确的顺序是:先定好双方的报文结构、ID分配方案和ADC采样触发方式,再分别写代码。我在这个项目中写的第一份文档不是代码,而是一份一页纸的“结点间通信协议”,上面写清了每条报文的ID、发送周期、数据字节含义、字节序定义。后面所有软件都严格对照这份协议写,联调阶段几乎没有因为“格式不一致”的问题折腾过。
2. ADC采样链路设计:从传感器信号到可靠数值的每一步
ADC是整个系统的眼睛。很多项目看起来是CAN通信的问题,追根溯源其实是ADC采出来的数据本身就不准——数据不准后面再努力也是白搭。这一节把ADC采样电路从原理到代码完整过一遍,重点说清楚采样周期怎么定、采样值怎么算、数据漂移怎么治。
2.1 STM32 ADC的时钟、采样周期与转换时间计算
ADC的采样精度和采样周期是直接相关的。STM32F103的ADC是逐次逼近型(SAR)结构,内部有一个采样保持电容,采样时间越长,外部信号源对电容的充电越充分,采样结果越精确。ADC时钟来自APB2总线(最高72MHz),经过ADC预分频器分频,要求ADC时钟最高不超过14MHz。
我的配置是:APB2=72MHz,ADC预分频为6分频,得到ADC时钟12MHz。然后选择采样周期为13.5个ADC时钟周期。这样单个通道的转换时间计算公式是:
转换时间 = 采样周期 + 12.5个比较周期 = 13.5 + 12.5 = 26个ADC时钟周期
换算成时间就是 26 / 12MHz ≈ 2.17μs。如果我要扫描两个通道,总时间为 2 × 2.17 ≈ 4.34μs,加上软件开销,实际能做到连续采样速率大约每秒20万次以上,对这个项目来说完全够用。
这里有一个常见的理解偏差需要注意:STM32的ADC转换时间不是简单的“采样周期”,而是“采样周期+固定比较周期”。很多人设置采样周期为1.5周期,觉得省时间,结果高阻抗信号源下的采样值跳动非常厉害。如果传感器输出阻抗较高,或者信号线上串联了滤波电阻,建议采样周期至少选13.5周期以上,宁可牺牲一点速度也要保证精度。
2.2 ADC采样值如何换算成真实电压
热词里有一句“如果单片机adc输入口电压为1v,则采样得到的值是多少”,这里统一回答一下。对STM32F103的12位ADC,参考电压通常接3.3V,计算公式是:
采样值 = 输入电压 / 参考电压 × 4095
所以1V输入对应的理论采样值是 1 / 3.3 × 4095 ≈ 1241。反过来,如果读到的ADC值是2048,对应的电压是 2048 / 4095 × 3.3 ≈ 1.65V。
实际项目中我习惯把电压值换算成定点数而不是浮点数,避免在中断里做浮点运算拖慢速度。比如把电压放大1000倍,用uint32_t保存毫伏值,整数运算不仅快,而且打印调试时更直观。
2.3 数据漂移的处理:硬件与软件的双管齐下
ADC数据漂移是热词中出现频率非常高的问题,实际排查起来也最让人头疼。最常见的漂移来源有三个:参考电压Vref波动、温度漂移和地线噪声。对这个双结点项目,我做了两件事来解决:
第一,硬件上把MCU的VREF+引脚单独走线,并且就近放了1μF和100nF两颗去耦电容。VREF+是ADC的基准,它不干净,采样数据一定不干净。如果你用的芯片没有独立的VREF+引脚(F103的LQFP48封装就经常被省掉),那至少要在VDDA引脚上下足功夫。
第二,软件上利用STM32内部自带的VREFINT通道做校准。F103内部有一个稳定的1.2V参考电压,可以通过ADC的通道17测量它。因为VREFINT的电压是稳定的,实测ADC值会随着VREF的波动反向变化,利用这个关系可以反推出当前真实的VREF电压,进而修正其他通道的转换结果。校准公式我实测下来是:
实际电压 = 1.2 / (VREFINT采样值 / 4095 × 3.3) × 采样值 / 4095 × 3.3
简化之后就是:
实际电压 = 1.2 × 4095 × 采样值 / (VREFINT采样值 × 4095) = 1.2 × 采样值 / VREFINT采样值
这个校准方法可以让温度通道的数值在24小时内的漂移从±15个LSB降到±3个LSB左右,效果非常明显。
2.4 软件滤波:中值+滑动平均的工程组合
硬件调理做完之后,数据还有残余噪声。此时软件滤波是最后一道防线。我在ADC中断里先做一次中值滤波(取5个样本的中间值,剔除偶然尖峰),然后累加到一个长度为16的滑动窗口中取平均。
#define FILTER_WINDOW_SIZE 16 uint16_t adc_filtered_sample(uint16_t new_sample) { static uint16_t buffer[FILTER_WINDOW_SIZE] = {0}; static uint8_t index = 0; static uint32_t sum = 0; static uint8_t filled = 0; if (filled == FILTER_WINDOW_SIZE) { sum -= buffer[index]; } buffer[index] = new_sample; sum += new_sample; index = (index + 1) % FILTER_WINDOW_SIZE; if (index == 0) filled = 1; if (filled) { return (uint16_t)(sum / FILTER_WINDOW_SIZE); } return new_sample; }中值滤波负责杀掉偶尔出现的尖峰(比如电机启动瞬间的干扰),滑动平均负责平滑高频噪声。这个组合做下来,ADC原始数据的峰峰值从±10个LSB降到了±2个LSB以内。特别提醒一下,滑动窗口大小不是越大越好。窗口太大,会对真实信号变化产生明显的滞后,在控制环路里可能引起震荡。我这个项目里温度信号变化慢,16个窗口没问题;如果你采集的是快速变化的电流,窗口改到8就差不多了。
3. CAN通信设计:报文ID、仲裁机制与时序计算的完整推演
CAN总线是这个系统的神经网络。从应用层看,CAN最难理解但最核心的三个点是:报文ID与优先级的关系、位时序的精确计算、以及标准帧和扩展帧的抉择。这一节逐个说透,最后给出可复制的配置代码。
3.1 CAN报文ID怎么分配才能保证实时性
CAN总线仲裁依靠报文ID:ID数值越小,优先级越高。在总线上,显性电平(逻辑0)可以覆盖隐性电平(逻辑1),所以帧起始后ID位先发送,谁先出现显性位谁就赢得仲裁。这意味着在一个已经设计好的系统中,最重要的数据必须放在最小的ID上。
我的双结点项目里报文ID分配如下:
| 报文ID(标准帧) | 发送方 | 内容 | 发送周期 |
|---|---|---|---|
| 0x100 | 节点B | 控制指令 | 10ms |
| 0x200 | 节点A | 温度+给定值 | 20ms |
| 0x300 | 节点B | 电流值+状态字 | 50ms |
节点B的控制指令是闭环系统最重要的报文,周期最短、ID最小。如果总线上同时有多帧报文要发送,0x100总是第一个抢到总线。这个方案简单且验证有效,不需要用复杂的CANopen或J1939协议。项目里如果只有两三个节点,自己定义ID表完全够用,反而更透明。
3.2 CAN位时序计算:BS1/BS2和采样点到底怎么配
CAN通信里最经典的坑就是波特率配置不对导致的通信错误。STM32F103的bxCAN位时间由三部分组成:同步段(SYNC_SEG)固定为1个时间量子Tq,传播段+相位缓冲段1合并为BS1,相位缓冲段2为BS2。波特率计算公式是:
波特率 = APB1时钟 / (预分频值 × (1 + BS1 + BS2))
APB1时钟在F103上典型为36MHz。我的目标是500kbps,这样算:
预分频值 = 4,则总Tq数 = 36MHz / (500kbps × 4) = 18个Tq
取BS1=13个Tq、BS2=4个Tq,加上同步段1个Tq,正好18个Tq。采样点位置为:
采样点 = (1 + BS1) / (1 + BS1 + BS2) = (1+13)/18 = 77.8%
这个采样位置比较合理。网线长度较短时采样点靠近80%左右是比较稳的,过长或过短都会降低对线缆延迟、温漂的容忍度。如果总线长度超过50米,适当增大BS1、减小BS2,让采样点后移,可以弥补信号在长线上传播导致的边沿偏移。
3.3 CAN和CAN FD的选择:什么时候需要升级
这两个节点之间的数据量不大,控制指令10ms一帧、每帧8字节以内,标准CAN 2.0B的带宽完全够用,所以我最终选了经典CAN。CAN FD真正的优势在于数据场变长(最多64字节)和数据段速率提升(最高到8Mbps甚至更高),适合固件升级、大数据量诊断这类场景。
如果你的项目涉及多个传感器节点需要同时上传大量波形数据,那FD值得考虑;但如果只是周期性的几十字节控制数据,用CAN FD反而在稳定性上没有优势,还增加了总线的物理层挑战——数据段速率上来之后,终端电阻匹配、线缆质量、连接器阻抗都得更讲究。
3.4 CAN字节序的大端小端:一个让联调翻车的细节
热词里有“CAN 大端小端”,这个坑真的非常典型。CAN协议规定:报文在字节内的位序是MSB先发(大端位序),但字节本身的发送顺序是从第0字节到第7字节,也就是字节间的顺序是顺序的。真正的问题出在MCU的内存存储上,不同芯片、不同编译器对标量类型的内存布局不同。
我在协议里做了一个绝对稳妥的约定:所有多字节数据在CAN报文里一律采用小端字节序存储,且由发送方软件负责显式拆分,不依赖编译器的结构体对齐。举个例子,节点A要把一个16位的温度值发给节点B:
uint16_t temperature; uint8_t can_data[8]; can_data[0] = (uint8_t)(temperature & 0xFF); // 低字节 can_data[1] = (uint8_t)((temperature >> 8) & 0xFF); // 高字节接收方做反向组合。这种方式看起来多写几行代码,但避免了结构体打包、位域在不同编译器和优化等级下的兼容性问题。双结点项目一旦两边用的IDE版本不一样,结构体对齐方式很容易悄悄变化,这是“偶发乱码”最常见的来源之一。
3.5 CAN过滤器配置
STM32的bxCAN有过滤器机制,但很多新手在这里困惑。双结点系统中,如果不配置过滤器,CAN外设默认接收所有报文(F103的bxCAN在初始化后配置过滤器组为全部屏蔽模式,可以收到总线上的所有报文)。
我的做法是:节点A只接收ID=0x100的报文,节点B只接收ID=0x200和0x300。配置过滤器的核心是设置CAN_FilterIdHigh/Low和CAN_FilterMaskIdHigh/Low。对标准帧,32位过滤器寄存器的用法是:ID的高16位存的是标准ID左移21位后的部分,低16位存的是RTR、IDE位等。这里非常容易配错,建议直接用寄存器位操作而不是眼睛算。
CAN_FilterInitTypeDef CAN_FilterInitStructure; CAN_FilterInitStructure.CAN_FilterNumber = 0; CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh = 0x0000; CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000; CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;注意上面这个配置是全部屏蔽,也就是接收所有报文。实际项目里,你需要在接收到报文后做一次软件ID判断,或者在初始化时精确配置ID掩码。我的经验是先用全部屏蔽跑通通信,最后再来精配过滤器,分阶段调试的效率更高。过滤器掩码的原理是:掩码位为0时要求ID相应位必须匹配,为1时该位不关心。如果只想接收ID=0x100,掩码要为0;如果想接收一组ID(比如0x100、0x104、0x108这种后两位可变的),掩码的低两位设为1就行。这种灵活度在实际项目中非常有用,可以用一个过滤器同时接收多个功能相关的报文,减少FIFO溢出风险。
4. 双结点联调的真实排错过程:从各亮各灯到完整闭环
这一节直接复盘我在联调时遇到的两个难题。这两个问题耗时最长、也最典型,一个是CAN通信的物理层问题,一个是ADC数据与CAN报文时间的耦合问题。把它们写出来,是希望读者下次遇到类似现象时可以直接对照排查。
4.1 症状一:波特率配置正确但通信偶发失败
现象描述:两个节点单独跑都正常,CAN收发器用两个便宜的分析仪接上去,数据偶尔能收到、偶尔报错,错误计数器一直增长。
排查链路:
第一反应是检查波特率寄存器配置。对比两个节点的CAN_BTR寄存器,发现一边是APB1=36MHz配置的500kbps,另一边在初始化时先开了PLL再把系统时钟改成了72MHz,导致APB1实际变成了36MHz但代码里仍按旧的RCC配置计算预分频值。这个问题通过打印RCC_GetClocksFreq()很快就发现了。
第二个问题是终端电阻。两个节点距离不足30cm,我用的是面包板跳线连接,省掉了120Ω终端电阻。在低速短距离场景下,没有终端电阻系统往往也能跑,但CAN收发器的显性电平下降沿反射会在接收端产生振铃,导致后一位采样错误。后来在总线两端各接了一个120Ω电阻,帧错误率立刻降了下来。
第三个点比较隐蔽:CAN_H和CAN_L接反了。接反时总线完全静默,但有的收发器芯片会把它当成单线模式工作,出现极其不稳定的“时好时坏”。检查时不要只看线色,要用万用表量收发器输出端的电平,正常显性时CAN_H约3.5V、CAN_L约1.5V,差分约2V。
4.2 症状二:节点B收到的ADC数据总是滞后一拍
现象描述:节点A每20ms发送一次温度数据。节点B收到的温度和本地实测温度总是有明显的滞后,在温度变化时尤其明显,像是有平滑滤波一样。
排查链路:
- 先用CAN分析仪抓取总线报文,发现节点A确实是每20ms发一帧,周期准确。
- 然后用示波器对比节点A的ADC采样点与CAN发送时刻。结果发现节点A是在主循环中“有空才采样、采样完顺带发送”,而不是严格定时。当主循环被其他任务(比如LED刷新)卡住时,采样的实际间隔会从20ms变成30ms甚至50ms,接收端平均下来就感觉滞后了。
- 修复方式:把ADC采样和CAN发送全部挪到定时器中断里,主循环只做数据显示和按键扫描。定时器设为20ms周期,中断里启动一次ADC转换,转换完成后在中断回调里把数据填入CAN发送邮箱。这样采样时刻严格固定在20ms,不再随主循环负载漂移。
这个问题反映了一个很核心的工程原则:在双结点控制系统中,数据采集和发送的“时基”必须由硬件定时器驱动,而不能依赖主循环的软件延时。只要两个节点的时基都稳定,后续做数据融合或者同步控制才有基础。
4.3 症状三:VREFINT校准之后温度数据依然缓慢漂移
这个症状排查到最后问题居然出在PCB上。当时用的是手工焊接的洞洞板,VREF+引脚的去耦电容距离MCU引脚超过1cm,走线细长且靠近一个开关电源模块。后来按照一个原则重画了板子——所有去耦电容必须放在MCU电源引脚的背面(LQFP封装一般在引脚对面),走线尽量短粗直接,模拟供电和数字供电之间用磁珠串接,地平面在ADC采样区域保持完整不分割。重做之后漂移问题大幅缓解。
5. 从能跑到稳定的工程化打磨:PCB布局与CAN收发电路细节
程序层面跑通只是第一步,真正让双结点系统长期稳定运行的关键在硬件设计。这一节集中讲PCB布局上的三个要点,以及CAN收发器外围电路的选型与布线。
5.1 PCB布局的三个关键点:时钟、电源噪声与地
热词里提到“规避时钟抖动与电源噪声的3个PCB布局要点”,这正是我在这个项目里用上的方法:
第一点,晶振电路要远离CAN收发器和高频数字走线。晶振是MCU的时钟源,一旦被CAN总线上的共模噪声干扰,时钟抖动会直接体现在ADC采样周期上。我把8MHz晶振放在MCU一侧,周围用铺地铜箔包围,并且保证晶振下方没有其他信号走线穿过。
第二点,ADC参考电压和模拟供电要独立走线。哪怕是两层板,也要把VREF+走线从MCU引脚单独拉出来,中间不要和数字信号平行长距离走线。可以在VREF+上串一个10Ω电阻再接一个10μF电解电容,构成一个低通滤波器,进一步隔离数字噪声。
第三点,CAN收发器要靠近接线端子放,且它的电源引脚要单独退耦。CAN收发器是总线上电流变化最剧烈的器件,它的电源噪声会通过地平面传导到MCU的模拟电路。我习惯在收发器VCC引脚放一个100nF电容,同时把收发器的地和MCU地在MCU电源地引脚处单点汇合,不要让收发器的地电流流过ADC模拟地回路。
5.2 CAN收发器选型与外围电路配置
最常用的收发器是TJA1050、SN65HVD230和ISO1050。其中SN65HVD230是3.3V供电的,可以直接和STM32连接,不需要电平转换,我这个项目用的就是它。TJA1050是5V供电,如果MCU是3.3V逻辑,需要在CAN_TX和CAN_RX上串接电阻或用电平转换芯片。
CAN收发器的外围电路要注意:
- 终端电阻:短距离(<1m)点对点可以不接也能通,但可靠起见还是两端各接一个120Ω电阻。注意是总线两端各一个,不是每个节点都接。如果每个节点都接了120Ω,总线上等效阻抗会变成60Ω以下,收发器可能因为负载过重发热甚至损坏。
- 共模电感:如果节点要通过长线缆连接,或者工作环境有电机、变频器等干扰源,CAN_H和CAN_L线上串联一个共模电感可以有效抑制共模干扰。有的收发器内部已经做了共模抑制,外部就不需要额外加了。
- ESD保护:总线是暴露在外的,插拔连接器瞬间的静电放电可能直接击穿收发器。我在模块上预留了PESD1CAN的焊盘位置,如果使用环境比较恶劣,贴上一颗就能提供±30kV的ESD防护。
5.3 采样数值的归一化处理与数据校验
当两个节点的ADC数值通过CAN传出去之后,接收方不能直接拿过来就用。我在协议里加了一个简单的归一化步骤:发送方先把原始ADC值减去零点偏移量,再乘上增益系数,最后以整型数值发送;接收方拿到后直接当作工程量使用。这样做的意义是:如果以后传感器更换了,只需要在发送方改校准参数,接收方完全不用动。
数据校验采用CRC8。CAN协议本身有CRC15校验,但那只是保证物理层的传输无误,不能防止应用层的逻辑错误。我在8字节报文里,前4字节放数据,第5字节放序列号,第6字节放CRC8,最后两字节保留。序列号的作用是让接收方可以检测到报文丢失或乱序,这在控制类系统里非常重要。如果发现序列号跳变超过阈值,接收方可以主动进入安全状态(比如停止PWM输出),避免在数据不完整的情况下继续执行错误的控制动作。
CRC8代码用查表法实现,开销极低:
uint8_t crc8_update(uint8_t crc, uint8_t data) { uint8_t i; crc = crc ^ data; for (i = 0; i < 8; i++) { if (crc & 0x80) { crc = (crc << 1) ^ 0x07; } else { crc <<= 1; } } return crc; }这个CRC8的多项式0x07是通用的CRC-8/ATM,两边保持一致即可。如果不做校验,遇到偶发的噪声导致数据位翻转,接收方可能把错误的值当成正常控制量执行,后果可能很严重。加校验这件事成本极低,收益却巨大,强烈建议任何CAN项目都加上。
6. 双结点系统调试中的关键测量工具与实测数据记录
调试工具这个话题值得单独讲一讲。做CAN总线项目,一个好的分析工具能省下大量时间;而ADC采样部分的测量工具选择和测量方法也直接影响你对系统是否稳定的判断。
6.1 CAN总线的调试工具选择与使用方法
我手头常用的工具是两个:一个USB-CAN分析仪(这类工具很多,选择支持PCAN-View协议栈或兼容格式的即可),一个逻辑分析仪。USB-CAN分析仪用来抓应用层报文、看周期和内容非常方便;逻辑分析仪的采样率足够高(我用的是100MHz),用来排查物理层波形问题很关键。
调试CAN物理层时,我习惯用逻辑分析仪挂在CAN_RX引脚上,观察波形而不是直接看协议解码。正常的CAN显性位波形应该是干净的方波,如果有明显的振铃、圆角或者幅值不足,说明终端电阻、线缆或收发器供电可能有问题。如果波形正常但依然通信失败,才进一步去查CAN控制器的寄存器配置。
实际使用中有一个小技巧:分析仪和系统共地问题。很多CAN分析仪通过USB供电,PC的地线和目标板的参考地之间如果有电位差,容易造成测量干扰甚至损坏收发器。我在连接时始终确保目标板和分析仪通过USB口共地,不会只连CAN_H/CAN_L两根线悬空操作。
6.2 ADC采样精度的验证方法与实测数据对比
验证ADC精度不能只看几次采样,要有统计意识。我在节点A上跑了一段测试代码:连续采样1024次,记录最大值、最小值、平均值和标准差。用这个方法对比了三种情况:
| 测试条件 | 最大值-最小值(峰峰值) | 标准差 |
|---|---|---|
| 仅软件滤波,无PCB优化 | ±12 LSB | 4.5 |
| 加VREFINT校准 | ±7 LSB | 2.8 |
| PCB重做且滤波+校准全开 | ±2 LSB | 0.9 |
从这个表可以清楚看到,PCB布局对ADC稳定性的影响甚至比软件校准还大。很多人在洞洞板上调了很久软件都压不下来的噪声,其实根因在板子布局上。
验证CAN通信误码率的方法是:让节点A每10ms发一帧带递增序列号的报文,连续跑24小时,节点B接收后统计收到的帧数、丢帧数和CRC错误数。实测结果在500kbps、短距离无干扰环境下,24小时丢帧数为0,CRC错误数为0。这个实验项目完全够用。如果把线缆加长到20米并靠近变频器运行,数据会变得难堪一些,但这也是验证你的终端电阻和共模抑制设计是否到位的好方法。
6.3 双结点时间同步的一个简单有效方法
双结点控制如果涉及两边的数据需要做时间对齐(比如节点A的温度数据和节点B的电流数据要合成一个控制决策),那就需要考虑时间同步。最朴素的做法是用周期同步帧:节点B每100ms发一帧SYNC报文(ID最小),节点A收到SYNC后清零本地时间戳,然后每条数据报文都带一个相对SYNC的时刻偏移。这样两边就能把数据对齐到同一个时间轴上。
实测下来,这种同步方式的误差大约在一个CAN帧的传输时间内(500kbps下典型为0.2ms以下),对温度控制这种大惯性对象完全够用。如果要求更高精度的同步,就得用硬件方案(比如IEEE 1588),但在大多数双结点项目里属于过度设计。
7. 写在最后:一次完整的ADC/CAN双结点设计复盘
整个项目从画板到闭环跑通,花了大约三周。如果让我重新再来一次,我会把更多时间花在前面两件事:一是协议设计,二是PCB布局仿真验证。协议设计时把ID分配、字节序、超时策略都白纸黑字定清楚,后面写代码就会非常顺手;PCB布局则决定了整个系统的噪声底线,这个底线一旦定低了,软件再怎么写也救不回来。
这个项目里最值得自夸的操作是把ADC采样触发完全交给硬件定时器,主循环不再参与时序控制。这让系统在多任务交错时仍然能保持20ms的严格采样周期,也让CAN报文里的时间戳有了实际意义。如果你在调类似的双结点项目,不妨先问自己一句:我的数据采集和发送真的是按“时基”在走吗?
另外,在处理CAN大端小端问题时,我试验了用结构体位域打包和解包的方案,最后放弃了,因为两个节点的代码由不同人维护,编译器版本和环境变量不一致,结构体对齐在这些场景下太不可控。显式字节拆分虽然代码看着啰嗦,但可读性最强、兼容性最好,这也是我在多个项目中总结出来的经验。
最后分享一个调试利器:我在节点A和节点B的固件里都保留了一段“测试模式”代码。上电时如果检测到某个按键按下,就进入测试模式,固定发送一组已知数据帧,同时循环打印ADC原始值和滤波值的对比。这个功能看起来是调试代码,实际上在产线检验和现场排障时非常好用。做双结点项目时,尽量让每个节点都能独立验证自身硬件,再谈联调,效率会高很多。