简介:这是面向毕业设计或课程作业的STM32与CAN总线多节点温湿度数据采集完整工程包,适合正在学习嵌入式开发、需要快速搭建数据采集与总线通信原型的学生,可用于理解实际项目中多节点实时通信的完整链路。资源包内含461个文件,压缩后约9.11MB,以C源码(95个.c)、头文件(99个.h)、Keil工程文件(.uvproj)、编译输出(.o/.axf/.hex)及说明文档(.md/.txt)为主,同时保留备份文件与编译中间产物,便于对照源码逆向分析工程配置和构建流程。已有167人学习下载。工程覆盖STM32CubeMX初始化、CAN控制器与收发器配置、多节点仲裁通信、温湿度传感器驱动以及实时操作系统任务调度等关键实现,硬件电路与PCB设计思路也可从源码中反推;借助源码、烧录文件与备份文件,读者可快速复用节点组网方案,迁移到自己的数据采集项目中,也可作为毕设答辩或实验报告的参照。
1. 为什么多节点温湿度采集会选 CAN 总线
在一间几十平米的温室大棚布 5 个温湿度测点,或者给机房做环境监测,拉 RS485 总线过去会发现:主站轮询一圈有延迟,某个节点异常挂死还会把整条链路卡住。用 CAN 总线重做一版,所有节点共享一对差分线,按帧 ID 仲裁自动错开发送,任何节点掉线都不影响其余节点继续上报,这正是这套 STM32 + CAN 多节点温湿度采集工程最有价值的地方。它把毕业设计和课程作业里最容易被追问的三个点串成一条完整链路:Cortex-M 外设编程、CAN 2.0 协议栈、温湿度传感器驱动时序。适合正在做毕设选题、想补一个完整嵌入式通信链路的学生,也适合需要快速搭一套机房或仓储环境监测原型的工程师。下面按从协议到代码的路径,把每个环节的参数选择和踩坑点展开。
2. CAN 多节点通信架构与 STM32 bxCAN 位时序配置
2.1 为什么多节点采集优先考虑 CAN 而不是 RS485 或 I2C
RS485 配合 Modbus 协议是工业现场最常见的组合,但它是严格的主从轮询模型:主站逐个询地址、等应答、再询下一个。节点一多,最慢节点的响应时间直接拉长整个采集周期,而且某节点应答超时还要等超时定时器走完。I2C 本身是板上总线,地址空间有限、抗干扰弱,拖几十米线基本不可用。CAN 总线是真正的多主网络,任何节点有数据随时可以发,总线仲裁由硬件完成,低 ID 帧先传输,高 ID 帧自动退避重发,不需要主站调度。
从协议角度再看一层:CAN 2.0B 标准帧有 11 位 ID、扩展帧有 29 位 ID,单帧数据最多 8 字节。这个数据场大小对温湿度这种两个 float 的载荷刚好合适,一帧装温度一帧装湿度,不浪费带宽。另一个隐藏优势是 CAN 的错误处理机制:位错误、填充错误、CRC 错误、ACK 错误都会被硬件检测,出错帧自动丢弃并要求重发,比 RS485 裸收发可靠得多。下表是三种总线在同类场景下的直观对比。
| 对比项 | RS485 + Modbus | I2C | CAN 2.0B |
|---|---|---|---|
| 典型节点数 | 32(加中继可扩展) | 同线地址有限 | 理论上千,实用几十 |
| 主从关系 | 严格主从轮询 | 主从 | 多主,非破坏性仲裁 |
| 实时性 | 轮询周期随节点累加 | 依赖主设备调度 | 事件触发,高优先级先发 |
| 抗干扰能力 | 差分,较好 | 差 | 差分 + CRC + 错误重发 |
| 软件实现成本 | 收发状态机 | 简单 | 需 CAN 控制器外设支持 |
STM32 全系基本都内置 bxCAN 控制器,也就是说仲裁、填充、CRC 这些链路层工作全部由硬件完成,工程师只需要配置位时序、填充数据、调用发送接收函数,不需要写协议栈本身。这也是这个项目能在一门课程作业周期内做完的原因。
2.2 STM32 bxCAN 外设与最小硬件连接
以最常见的 STM32F103C8T6 为例,片上集成一个 bxCAN 控制器,挂在 APB1 总线上。注意:MCU 的 CAN 控制器引脚输出的是逻辑电平,不是差分电平,不能直接接总线。必须外接 CAN 收发器,常用型号是 TJA1050 或 SN65HVD230,把 MCU 的 TX/RX 转成 CANH、CANL 差分信号。典型接法是 PA11 复用为 CAN_RX,PA12 复用为 CAN_TX,收发器的 STBY 或 RS 引脚接地或接 GPIO 控制进入高速模式。
接线有一个容易忽略的细节:总线两端各需要一只 120Ω 终端电阻,不是每个节点都加。终端电阻的作用是匹配传输线阻抗、吸收反射波,位置不对或者阻值偏了,高速率下会出现位错误。如果是自己画板验证,电阻选 120Ω/0.25W 以上,放在总线的物理两端,也就是最左边节点和最右边节点的 CANH 与 CANL 之间。
2.3 位时序计算与 CubeMX 参数配置
CAN 一个位的时间由四段组成:同步段(固定 1 TQ)、传播段、相位段 1、相位段 2,其中传播段和相位段 1 在 STM32 HAL 库中合并为 TimeSeg1,相位段 2 对应 TimeSeg2。波特率计算公式为:
波特率 = CAN时钟频率 / Prescaler / (1 + TimeSeg1 + TimeSeg2)以 F103 的 APB1 时钟 36 MHz 为例,目标是 500 kbps。把 Prescaler 设为 8,CAN 时钟降到 4.5 MHz,位时间取 9 TQ,即 1 + 6 + 2,得到 4.5 MHz / 9 = 500 kbps。采样点位置是决定总线稳定性的关键参数,计算公式为 (1 + TimeSeg1) / (1 + TimeSeg1 + TimeSeg2),这里为 (1+6)/9 ≈ 77.8%,落在推荐的 75% 到 85% 区间内,50 米以内的总线长度足够稳定。
| 参数 | 推荐值 | 说明 |
|---|---|---|
| Prescaler | 8 | APB1 = 36 MHz 时,CAN 时钟 = 4.5 MHz |
| SyncJumpWidth | 1 TQ | 同步跳转宽度,常规选 1 |
| TimeSeg1 | 6 TQ | 包含传播段和相位段 1 |
| TimeSeg2 | 2 TQ | 相位段 2 |
| 采样点 | 77.8% | 位于位时间的 3/4 处 |
CubeMX 生成初始化代码后,HAL 层关键配置项如下:
hcan.Instance = CAN1; hcan.Init.Prescaler = 8; hcan.Init.SyncJumpWidth = CAN_SJW_1TQ; hcan.Init.TimeSeg1 = CAN_BS1_6TQ; hcan.Init.TimeSeg2 = CAN_BS2_2TQ; hcan.Init.Mode = CAN_MODE_NORMAL; hcan.Init.AutoBusOff = DISABLE; hcan.Init.AutoWakeUp = DISABLE; hcan.Init.AutoRetransmission = ENABLE; if (HAL_CAN_Init(&hcan) != HAL_OK) { Error_Handler(); }AutoRetransmission 置 ENABLE 表示仲裁失败或发送出错时,硬件自动重发当前帧。多节点场景下这个选项非常重要——两个节点同时抢占总线时,低 ID 帧先走,高 ID 帧自动重发,保证了数据不丢。AutoBusOff 置 DISABLE 是为了调试期手动观察总线关闭状态,量产环境一般也保持 DISABLE,由软件处理恢复逻辑,避免硬件自动恢复导致错误状态不可控。
3. DHT22 温湿度数据采集与单总线驱动实现
3.1 传感器选型与单总线电气连接
温湿度传感器常用 DHT11 和 DHT22。DHT11 精度较差,湿度误差 ±5%,温度 ±2℃,只适合演示效果;DHT22(又称 AM2302)湿度误差 ±2%,温度 ±0.5℃,分辨率 0.1℃,采集到的数据更有分析价值。两者协议都是单总线(1-Wire 风格),使用同一个 GPIO 脚完成输入输出双向通信,外部必须接一个 4.7kΩ 上拉电阻到 3.3V 或 5V。4.7k 是兼顾上升沿时间和驱动能力的常见值,换成 10k 在短线上也能工作,但线长或寄生电容大时边沿变缓,容易采位出错。
传感器 DATA 脚接 STM32 的一个普通推挽引脚,比如 PC13 或 PB5,需要在发送起始信号时把引脚配置为输出、读取响应时切换为输入。HAL 库下可以用 GPIO_InitStruct 反复修改 Mode 字段,也可以在初始化时直接配置为开漏输出带上拉,靠外部上拉电阻实现输入读取。前者逻辑更清晰,适合课程作业讲解。
3.2 单总线时序与位读取实现
DHT22 的通信时序分三个阶段。主机先把总线拉低,持续至少 18 微秒,再释放,传感器检测到起始信号后回传 80 微秒低电平加 80 微秒高电平。之后每个数据位以 50 微秒低电平开始,高电平持续 26 到 28 微秒表示逻辑 0,持续约 70 微秒表示逻辑 1。区分 0 和 1 的常用做法是:等低电平结束,延时 40 微秒后读取引脚电平,读到高就是 1,读到低就是 0,因为 1 的高电平宽度必然覆盖这个采样点,而 0 的高电平短于 40 微秒。
uint8_t DHT22_ReadBit(GPIO_TypeDef *port, uint16_t pin) { while (HAL_GPIO_ReadPin(port, pin) == GPIO_PIN_RESET) { } // 跳过50us起始低电平 delay_us(40); // 在40us处采样 if (HAL_GPIO_ReadPin(port, pin) == GPIO_PIN_SET) { while (HAL_GPIO_ReadPin(port, pin) == GPIO_PIN_SET) { } // 等待高电平结束 return 1; } return 0; }这段逻辑的精髓在于采样点选在 40 微秒。DHT22 输出“0”时高电平只有 26 微秒左右,40 微秒时已经回到低电平;输出“1”时高电平持续 70 微秒,40 微秒时仍维持高电平。用这种中心采样法比测量高电平宽度更简单,也不依赖定时器输入捕获。注意 while 等待语句要加超时保护,否则传感器没接或损坏时程序会卡死在循环里,初始化阶段建议加一个 200 微秒超时变量,超时直接返回错误。
3.3 数据帧结构与校验
DHT22 一次完整传输返回 40 位数据,分成 5 个字节:湿度高字节、湿度低字节、温度高字节、温度低字节、校验字节。校验字节等于前四个字节累加和的低 8 位,这是单总线协议少有的带校验场景,必须在驱动层校验,否则个别位翻转会直接产生离谱的温度读数。
uint8_t DHT22_Read(float *humidity, float *temperature) { uint8_t buf[5]; // 发送起始信号:拉低至少18us,释放 HAL_GPIO_WritePin(DHT_PORT, DHT_PIN, GPIO_PIN_RESET); delay_us(2000); HAL_GPIO_WritePin(DHT_PORT, DHT_PIN, GPIO_PIN_SET); delay_us(30); // 等待传感器响应:80us低 + 80us高 if (HAL_GPIO_ReadPin(DHT_PORT, DHT_PIN) == GPIO_PIN_RESET) { while (HAL_GPIO_ReadPin(DHT_PORT, DHT_PIN) == GPIO_PIN_SET) { } } for (int i = 0; i < 5; i++) { for (int j = 7; j >= 0; j--) { buf[i] |= DHT22_ReadBit(DHT_PORT, DHT_PIN) << j; } } if ((uint8_t)(buf[0] + buf[1] + buf[2] + buf[3]) != buf[4]) { return 1; // 校验失败 } *humidity = ((uint16_t)buf[0] << 8 | buf[1]) / 10.0f; *temperature = ((uint16_t)(buf[2] & 0x7F) << 8 | buf[3]) / 10.0f; if (buf[2] & 0x80) { *temperature = -*temperature; // 最高位为符号位,1表示零下 } return 0; }读取时高位在前,所以内层循环从 7 到 0 移位。温度和湿度都扩大了 10 倍,比如 buf[0]=25、buf[1]=6,实际湿度是 25.6%。DHT22 的温度符号位在温度高字节的最高位,这一位不参与数值计算,只在组合完成后判断正负。这个细节在答辩时经常被问到,能讲清楚说明对数据手册读得够细。
3.4 驱动层常见故障与排查
第一次跑这个驱动最容易遇到三个问题。第一是卡死在起始信号后的 while 循环,原因是 DHT22 上电后需要 1 秒稳定时间,刚复位立刻读会拿不到响应,正确做法是主程序启动后延时 1.5 秒再开始采集。第二是读出来的数据总是 0xFF 或 0x00,通常不是协议问题而是 GPIO 模式没有从输出切回输入,输出模式下读引脚只能读到引脚寄存器自身的状态。第三是数据偶尔跳变,检查延时函数是否被编译器优化,for(i=0;i<100;i++);这种空循环在 -O2 优化下可能直接被优化掉,改用 SysTick 计数的 delay_us 才可靠。
4. HAL 库 CAN 收发实现与节点 ID 分配策略
4.1 帧格式选择与节点编址方式
CAN 2.0B 支持两种帧格式:标准帧 11 位 ID,扩展帧 29 位 ID。11 位标准帧能表示 2048 个不同 ID,对本项目十几个节点绰绰有余,而且短 ID 意味着仲裁场更短,总线利用率更高,所以优先用标准帧。每个节点采集两类数据,温度和湿度,可以合并成一帧 8 字节发送,也可以拆成两帧。拆帧的好处是接收方能独立处理,温度刷新率和湿度刷新率可以不同;合并帧则减少总线占用,适合节点多的情况。
ID 规划要考虑两点:一是优先级,ID 数值越小仲裁优先级越高,所以重要数据用低 ID;二是解析方便,让 ID 直接携带节点和数据类型信息。推荐按高字节表示类型、低字节表示节点号的方式编码:
| 帧 ID(标准帧) | 发送内容 | DLC |
|---|---|---|
| 0x1A1 ~ 0x1AF | 节点 1 ~ 15 的温度 | 4 |
| 0x2A1 ~ 0x2AF | 节点 1 ~ 15 的湿度 | 4 |
温度帧和湿度帧的 ID 区间错开,接收端只需要掩码判断高字节就能区分数据类型,低字节直接作为节点数组下标,不用查表映射。0x1A1 低 4 位是 0x01,所以节点号取StdId & 0x0F即可。
4.2 滤波器配置:接收全部节点还是单节点
网关节点需要接收所有从节点的数据,所以滤波器要配成掩码全 0 的接收所有模式。bxCAN 的滤波器工作原理是:接收到的 ID 与 FilterId 做异或,再与 MaskId 做与运算,结果为 0 才接收。MaskId 某位为 0 表示该位不关心。
CAN_FilterTypeDef filter = {0}; filter.FilterIdHigh = 0x0000; filter.FilterIdLow = 0x0000; filter.FilterMaskIdHigh = 0x0000; filter.FilterMaskIdLow = 0x0000; filter.FilterFIFOAssignment = CAN_RX_FIFO0; filter.FilterBank = 0; filter.FilterMode = CAN_FILTERMODE_IDMASK; filter.FilterScale = CAN_FILTERSCALE_32BIT; filter.FilterActivation = ENABLE; HAL_CAN_ConfigFilter(&hcan, &filter);掩码全 0 等同于不过滤,所有消息都进 FIFO0。如果某个从节点只想接收主站下发的配置帧,可以把 FilterId 设为配置帧 ID,掩码设为 0x7FF,这样只有完全匹配的 ID 才能通过。注意 Mask 位宽是 32 位,标准帧的 ID 放在高 16 位,所以 FilterIdHigh 才需要填 ID 值。
4.3 发送接口与接收中断回调
发送端核心是三个要素:帧头结构体、数据缓冲、发送邮箱号。HAL 库的HAL_CAN_AddTxMessage会把待发帧放入三个硬件发送邮箱之一,立即返回,真正发送在硬件层面异步完成。邮箱满时函数返回HAL_BUSY,需要手动重试或等待发送完成中断。
CAN_TxHeaderTypeDef txHeader; uint8_t txData[8]; uint32_t txMailbox; txHeader.IDE = CAN_ID_STD; // 标准帧 txHeader.StdId = 0x1A1 + node_id; // 温度帧ID txHeader.RTR = CAN_RTR_DATA; // 数据帧 txHeader.DLC = 4; // 4字节 txData[0] = (uint8_t)(temp_raw >> 8); txData[1] = (uint8_t)(temp_raw & 0xFF); txData[2] = 0; txData[3] = sensor_status; // 传感器健康状态 if (HAL_CAN_AddTxMessage(&hcan, &txHeader, txData, &txMailbox) != HAL_OK) { // 邮箱满或总线忙,可在这里记录发送失败次数 }温度数据是 16 位整数,放大 10 倍后拆成高低字节。DLC 固定写成 4 而不是 8,减少无效字节传输,CAN 控制器会在数据场结束后自动计算并填充 CRC。txData 里的 sensor_status 是驱动层返回的传感器状态码,0 表示正常,1 表示校验失败,2 表示无响应,接收方即使收到错误状态也能区分“没有数据”和“传感器坏了”。
接收侧最省事的方案是开启 FIFO0 消息挂起中断,在回调函数里取数据并解析:
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rxHeader; uint8_t rxData[8]; HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rxHeader, rxData); if ((rxHeader.StdId & 0xF00) == 0x100) { // 温度帧 uint8_t node = rxHeader.StdId & 0x0F; node_temp[node] = ((rxData[0] << 8) | rxData[1]) / 10.0f; } else if ((rxHeader.StdId & 0xF00) == 0x200) { // 湿度帧 uint8_t node = rxHeader.StdId & 0x0F; node_hum[node] = ((rxData[0] << 8) | rxData[1]) / 10.0f; } }回调函数里只做数据拷贝和简单解析,不调用 HAL_Delay、printf 或复杂的浮点运算。浮点除法放在回调里其实可以接受,因为温湿度数据规模很小,但更规范的做法是把原始 uint16_t 存入全局数组,解析和显示交给主循环。中断里一旦调用阻塞函数,其他优先级的实时任务会被拖累,这是 CAN 接收中断的通用优化原则。
4.4 多节点发送节奏与总线冲突规避
所有节点都周期性发送,比如每 2 秒上报一次,如果它们同时到达总线,CAN 仲裁机制会让低 ID 帧先发,高 ID 帧自动退避。硬件自动重发开启后,退避帧会在总线空闲后立即补发,所以从数据完整性看不会丢帧。但总线利用率会周期性冲高,尤其多节点同时到期的场景,可以用一个简单的时间片偏移来平滑:
uint32_t send_delay = 50 + node_id * 50; // 每个节点错开50ms起始相位节点 1 在 100ms 时发,节点 2 在 150ms 时发,节点 3 在 200ms 时发,配合 2 秒的采集周期,每帧之间隔了足够空隙。这种错峰策略在总线负载高的场景收益明显,能减少仲裁重发的次数,降低隐性故障概率。
5. 多节点组网调试:终端电阻、差分电压与故障排查清单
5.1 总线拓扑与终端电阻的正确位置
CAN 总线的物理拓扑必须是直线型,也就是从一个节点引出到下一个节点,不能在某个位置分叉成星型。星型拓扑在支线末端产生反射,反射波叠加在有效信号上,轻则提高误码率,重则直接产生位错误帧。每个节点到总线的引出线(stub)尽量短,控制在 30 厘米以内,长引出线相当于一个小的开路分支,影响更明显。实际项目中如果节点分散,用 T 型连接器或者端子排串接是最稳的做法。
终端电阻的检查可以用万用表:系统断电状态下,在总线任意位置量 CANH 和 CANL 之间的阻值,正常应该接近 60Ω,因为两端各 120Ω 并联。如果量到 120Ω,说明只接了一端;如果量到接近 0Ω,说明 CANH 和 CANL 短接了;如果量到几百千欧,说明两个终端电阻都没接。这个测量方法在验收时很实用,比用示波器看波形更快定位物理层问题。
5.2 差分电压变化与波形观察
CAN 总线收发器工作在两种状态:隐性(recessive)和显性(dominant)。隐性时收发器内部将 CANH 和 CANL 都偏置到 2.5V 左右,差分电压接近 0V,总线表现为逻辑 1;显性时 CANH 被拉高到约 3.5V,CANL 被拉低到约 1.5V,差分电压约 2V,总线表现为逻辑 0。差分电压的变化完全由收发器驱动,MCU 侧的 TX 引脚输出高电平时对应隐性,输出低电平时对应显性。
用示波器测 CANH 和 CANL 的波形,用数学通道做 CANH-CANL 差分,总线空闲时波形是 0V,数据传输时出现幅度约 2V 的脉冲串。一个完整标准帧的波形特征是:帧起始是一个显性位(SOF),接着是仲裁场(ID + RTR),控制场(IDE + DLC),数据场,CRC 场,ACK 槽最后是 7 个隐性位的帧结束。抖动明显的波形说明终端电阻没配好或线缆过长,数据位宽度不一致则指向波特率偏差。
5.3 用 HAL 错误寄存器定位问题
多节点联调最常见的现象是:单个节点自发自收正常,接入第二个节点后报错。这时直接看 HAL 库的HAL_CAN_GetError返回值,不同错误位直接对应不同物理层故障。
uint32_t err = HAL_CAN_GetError(&hcan); if (err & HAL_CAN_ERROR_BUSOFF) { // 总线关闭:连续错误过多,控制器进入离线状态,需要恢复 HAL_CAN_Stop(&hcan); HAL_CAN_Start(&hcan); } if (err & HAL_CAN_ERROR_ACK) { // ACK错误:总线上只有自己,没有其他节点应答 } if (err & HAL_CAN_ERROR_STUFF) { // 填充错误:大多是波特率不一致或信号质量差 }| 现象 | 排查动作 |
|---|---|
| 单个节点发送报 ACK 错误 | 确认总线上有至少两个节点;终端电阻是否两端都接 |
| 接入第二个节点后连续报 STUFF 错误 | 两端配置的波特率是否一致;用示波器对比位宽度 |
| 发送几帧后总线关闭 | 检查 CANH 和 CANL 是否接反,或收发器供电不稳 |
| 接收不到特定节点的数据 | 检查该节点 ID 和接收端滤波器掩码是否匹配 |
总线关闭是 CAN 控制器在错误计数超过 256 次后进入的状态,此时控制器既不发送也不接收。课程设计阶段经常遇到,恢复逻辑可以直接调用HAL_CAN_Stop再HAL_CAN_Start,量产场景建议配合看门狗做分级恢复,先软复位,多次失败再硬复位。
6. 用 USB-CAN 分析仪验证完整数据链路并压缩总线占用
把 USB-CAN 分析仪并接到总线上,上位机软件设置 500 kbps 波特率,能直接看到所有节点上报的帧。这里有一个更高效的验证技巧:不要只看 ID 和原始值,而是在每个节点的数据帧里加入递增序列号。接收端每收到一帧,对比上一个序号,序号不连续说明中间有丢帧,也说明总线确实出现过仲裁重发或错误帧。把序号放在 DLC 的第 4 个字节,代价只有 1 字节,换来的却是整条链路可靠性的可视监控。
总线占用率也可以通过分析仪软件统计。假设 5 个节点、每个节点 2 秒上报 1 帧、每帧 4 字节数据,可以估算出理论占用率:
每帧实际占线时间 ≈ 位时间 * 帧总位数 帧总位数 ≈ (1 + 11 + 1 + 1 + 4 * 8 + 15 + 7 + 3) = 67 位 位时间 = 1 / 500000 = 2 us 单帧耗时 ≈ 134 us 5 节点 2 秒周期占用率 ≈ 5 * 134us / 2s ≈ 0.03%这个结果说明 500 kbps 的带宽对温湿度采集绰绰有余。真正影响占用率的不是采样频率,而是每个节点为了等 DHT22 稳定而空转的时间。实际测量如果发现占用率高于 5%,大概率是某个节点在忙等待传感器时把发送节奏打乱了,或者在中断里做了过长处理。把 DHT22 读取和 CAN 发送解耦:定时器中断只维护一个采样子系统状态机,CAN 发送在主循环里根据采集完成标志触发,这样上报节奏更均匀,也更容易通过分析仪做周期验证。
本文还有配套的精品资源,点击获取