STM32F103+CAN总线多节点温湿度采集系统设计与实现 简介基于STM32F10x与CAN总线的多节点温湿度数据采集系统设计资料面向毕业设计、课程设计及工程实训场景。项目通过CAN请求/应答机制与两个从机通信将温湿度实时显示在LCD上并经串口打印完整演示了CAN总线多节点组网、数据帧收发与显示处理的开发流程。压缩包共463个文件以99个h头文件和95个c源文件为主便于阅读与二次开发另含MDK工程文件uvproj、hex烧录文件及编译中间文件解压后可直接打开或烧录验证。整体约6.57MB结构紧凑适合快速上手复刻。目前已有343人学习使用。资料内包含源码、工程与引脚连接说明即便不自行绘制PCB也可用面包板配合杜邦线搭建外设完成验证。所有代码经过测试可直接运行适合希望深入理解CAN协议并完成课设、毕设或竞赛练手的开发者。1. 为什么多节点温湿度采集要选STM32F10CAN总线多节点温湿度采集用 CAN 总线配 STM32F103是课程设计和毕业设计里常见的选择。测点往往分散在几十米甚至上百米范围内RS-485 轮询要按地址逐个点名节点一多延迟线性上升某个节点掉线还会拖住整条链路CAN 总线为汽车布线而设计差分信号抗干扰强多主结构下任何节点都能主动上报仲裁机制保证高优先级报文抢占总线更适合点位多、上报随机、有电气干扰的采集场景。STM32F103 内置 bxCAN 外设不需要外扩控制器配一颗 TJA1050 收发器就能搭出完整节点。下文按协议、硬件、代码、组网、调试五个环节推进读完能独立搭建 4 到 8 个节点的完整系统。2. CAN总线协议核心帧格式、仲裁机制与位时序参数2.1 CAN 2.0B数据帧结构从SOF到EOF逐段拆解一帧完整的 CAN 数据报文由七个段拼成帧起始、仲裁段、控制段、数据段、CRC 段、ACK 段和帧结束。对写固件的工程师来说真正需要关心的是仲裁段里的标识符、控制段里的 DLC 以及数据段的 8 字节上限。CAN 2.0A 标准帧是 11 位 IDCAN 2.0B 扩展帧是 29 位 ID温湿度采集这类节点数不超过 32 个的系统标准帧完全够用帧体更短相同波特率下能多传不少数据。IDE 位为 0 表示标准帧RTR 位为 0 表示这就是数据帧而不是远程帧DLC 用 4 位表达协议上最大是 15但一条报文的数据段最多只有 8 字节这是设计应用层协议时绕不开的硬约束。段名关键字段位长作用帧起始SOF1显性电平宣告总线从空闲变忙仲裁段ID RTR12 / 3211 位或 29 位标识符决定优先级控制段IDE DLC6区分标准/扩展帧声明数据长度数据段Data0~8 字节温湿度数据实际存放位置CRC 段CRC 界定符16 / 17校验错误、错误帧判定的关键区域ACK 段应答位 界定符2接收节点必须回显性应答帧结束EOF7隐性电平结束帧写代码时最容易漏的是 ACK 段发送节点把报文发完后必须有一个接收节点在 ACK 槽回一个显性位否则发送节点按错误处理流程重发。课程设计里最常见的发不出去现象就是总线上只挂了单板没有第二个节点回 ACK发送邮箱一直显示 pending。2.2 仲裁机制多节点同时发送时谁赢要不要上CAN FDCAN 的物理层只区分显性逻辑 0和隐性逻辑 1两种电平多个节点同时往总线发数据时显性电平覆盖隐性电平而每个发送节点都在边发边读。某个节点在仲裁段发出隐性位、却从总线上读回显性位时它立即知道自己输了仲裁转入接收状态把总线让给赢家。这套边发边听、输了就退的机制让 CAN 成为少数不需要主站调度就能多点同时上行的总线这也是多节点温湿度系统敢让所有节点自由上报的原因。仲裁的规则决定了 ID 数值越小优先级越高。所以多节点系统的 ID 规划不能把节点号直接当地址用而要把报文类型放在 ID 的高位报警帧用最小的 ID 区间数据帧居中心跳帧放最后。总线拥挤时低优先级帧自动退避重发高优先级报警却能实时抢通这个特性在温湿度越限场景里比 RS-485 的排队查询可靠得多。至于 CAN FD它把数据段扩展到最多 64 字节、数据段还能提速但 STM32F103 内置的 bxCAN 控制器只支持经典 CAN 2.0要用 CAN FD 只能外扩 MCP2518FD 这类独立控制器。毕业设计阶段把经典 CAN 的仲裁、错误处理、负载率这些概念吃透比追 CAN FD 的新特性更有性价比。2.3 位时序与采样点BS1、BS2、SJW参数怎么定CAN 是同步串行协议每个位时间由若干个时间量子 Tq 组成顺序是 SYNC_SEG 在最前接着是 BS1、BS2。STM32F103 的 bxCAN 挂在 APB1 总线上最大 36MHz波特率由分频值和每位的 Tq 数共同决定波特率 f_APB1 / ((BRP1) × (1 BS1 BS2))以 36MHz 出 500kbps 为例BRP 取 3BS1 取 10TqBS2 取 7Tq加上 SYNC_SEG 的 1Tq 一共 18Tq代入公式正好是 500k。采样点位置则是 (SYNC_SEG BS1) / 位时间 11/18约 61%。BS1 必须比 BS2 大就是为了让采样点落在位的后半段避开跳变沿附近的不稳定区间总线越长、节点越多采样点越往后放越稳。目标波特率BRPBS1BS2SJW采样点1Mbps19Tq8Tq4Tq约 63%500kbps310Tq7Tq4Tq约 61%250kbps710Tq7Tq4Tq约 61%SJW 是重同步时的最大跳变宽度取值不能超过 BS2。各节点时钟存在微小偏差收到显性位跳变时会动态调整位时序对齐SJW 越大纠偏越快但太大反而容易误同步500kbps 下取 4Tq 是常用值。全总线节点必须用同一套位时序参数一个节点配错初期看不出问题跑一段时间错误计数器涨上去整段总线都会瘫痪在错误帧里。3. 基于STM32F103的bxCAN初始化与温湿度节点实现3.1 硬件链路STM32F103C8T6、TJA1050与DHT11怎么接一个采集节点由三部分组成STM32F103C8T6 最小系统板承担控制逻辑TJA1050 负责把控制器的 TTL 电平转成 CAN 总线差分电平DHT11 负责采温湿度。控制器的 CAN_RX/CAN_TX 引脚固定在 PA11/PA12TJA1050 的 RXD/TXD 与之对应相连单片机 3.3V 逻辑和 TJA1050 的 5V 输入之间建议串一个 33Ω 限流电阻防止收发器输入端的钳位二极管把 5V 倒灌给 MCU。总线侧CANH 和 CANL 之间要接 120Ω 终端电阻而且只在总线物理上最远的两端各接一个。中间节点如果也接了终端电阻会改变总线特性阻抗波形反射直接表现为偶发错误帧。DHT11 的数据脚接任意 GPIO比如 PB0供电用 3.3V数据线与 VCC 之间加一个 5kΩ 左右的上拉电阻单总线协议靠电平释放后的上拉过程区分信号。3.2 bxCAN初始化GPIO复用、500kbps位时序与滤波器配置下面这段基于 STM32 标准外设库 V3.5完成 CAN1 的初始化配置为 500kbps、正常模式并开启接收中断void CAN_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; CAN_InitTypeDef CAN_InitStructure; CAN_FilterInitTypeDef CAN_FilterInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); // PA11 CAN_RX输入上拉PA12 CAN_TX复用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_12; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); CAN_DeInit(CAN1); CAN_StructInit(CAN_InitStructure); CAN_InitStructure.CAN_TTCM DISABLE; // 关闭时间触发模式 CAN_InitStructure.CAN_ABOM ENABLE; // Bus Off 自动恢复 CAN_InitStructure.CAN_AWUM ENABLE; // 自动唤醒 CAN_InitStructure.CAN_NART DISABLE; // 发送失败自动重传 CAN_InitStructure.CAN_RFLM DISABLE; // 接收 FIFO 不锁定 CAN_InitStructure.CAN_TXFP DISABLE; // 发送优先级由 ID 决定 CAN_InitStructure.CAN_Mode CAN_Mode_Normal; CAN_InitStructure.CAN_SJW CAN_SJW_4tq; CAN_InitStructure.CAN_BS1 CAN_BS1_10tq; CAN_InitStructure.CAN_BS2 CAN_BS2_7tq; CAN_InitStructure.CAN_Prescaler 3; // 36MHz / 4 / 18 500kbps CAN_Init(CAN1, CAN_InitStructure); // 滤波器 0 掩码模式掩码全 0 放行所有报文先调试后收紧 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; CAN_FilterInit(CAN1, CAN_FilterInitStructure); CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE); // FIFO0 挂起中断 }参数说明CAN_Prescaler 3 对应上节表格里的 BRPCAN_BS1_10tq / CAN_BS2_7tq 把采样点放在约 61% 位置CAN_ABOM 置位后控制器进入 Bus Off 不需要应用干预就能回到总线多节点环境里单节点异常不至于永久掉线CAN_NART 保持 DISABLE 让发送失败自动重发报警帧靠这个机制保证不丢CAN_TXFP 用 DISABLE这样三个发送邮箱按报文 ID 的优先级排队符合 2.2 节的仲裁思路。提示初始化顺序不能乱必须先配置 GPIO 再配置 CAN 外设最后建滤波器。滤波器在 CAN 外设复位后默认是关闭的漏掉 CAN_FilterActivation ENABLE 会导致收不到任何报文。3.3 DHT11读取与CAN报文封装8字节数据段怎么排DHT11 一次读取返回 40 位湿度整数、湿度小数、温度整数、温度小数、校验和。主机先拉低总线 18ms 再释放DHT11 回一个 80us 低电平加 80us 高电平作为应答然后连续输出 40 个 bit。每个 bit 都是 50us 低电平开头区别在高电平持续时间26~28us 是逻辑 070us 左右是逻辑 1所以读位的核心是卡高电平时长。uint8_t DHT11_ReadByte(void) { uint8_t i, ret 0; for (i 0; i 8; i) { while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) 0); // 等 50us 低电平结束 delay_us(40); // 0/1 的分界点40us 后仍是高就是 1 if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) 1) { ret | (0x80 i); } while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) 1); // 等高电平结束 } return ret; }delay_us(40) 是读位能否成功的核心逻辑 0 的高电平只有 26~28us40us 时已经回落逻辑 1 的高电平约 70us40us 时还维持在高位。这段代码要求 delay_us 用 SysTick 实现并且编译优化级别别开太高-O0 或 -O1否则延时抖动直接导致读出的字节错位。采集结果按温度 ×10、湿度 ×10存成无符号整数拼进 CAN 数据段发送uint16_t temp 265; // 26.5℃ uint16_t humi 582; // 58.2%RH CanTxMsg tx_msg; tx_msg.StdId 0x100 NODE_ADDR; // 标准帧 ID数据帧类型 节点号 tx_msg.ExtId 0; tx_msg.RTR CAN_RTR_DATA; // 数据帧 tx_msg.DLC 6; tx_msg.Data[0] NODE_ADDR; // 节点号 1~8 tx_msg.Data[1] 0x01; // 传感器类型0x01DHT11 tx_msg.Data[2] temp 8; // 温度高字节 tx_msg.Data[3] temp 0xFF; // 温度低字节 tx_msg.Data[4] humi 8; // 湿度高字节 tx_msg.Data[5] humi 0xFF; // 湿度低字节 CAN_Transmit(CAN1, tx_msg);DLC 固定为 6接收端解析时不依赖 ID 判断长度协议更简单直接。CAN_Transmit 只是把报文塞进发送邮箱真正发完要轮询 CAN_TransmitStatus(CAN1, 邮箱号) 等于 CAN_TxStatus_Ok或者用发送完成中断数据段留出的第 6、7 字节可以给后续扩展用比如电池电量或信号强度。4. 多节点组网CAN ID规划、报文格式与负载率计算4.1 节点ID怎么分配报文类型优先级高于节点号多节点系统最忌讳直接拿节点号当 CAN ID。仲裁按 ID 数值分胜负如果节点 1 用 0x001、节点 2 用 0x002报警帧和数据帧混在同一层优先级完全无法区分。常见做法是把 11 位标准帧 ID 按位段拆开高位 3 位表示报文类型中间 4 位留作区域低位 4 位放节点号。报文类型类型号ID 区间用途报警帧00x001~0x01F温湿度越限立即上报优先级最高数据帧10x101~0x11F周期上报温湿度心跳帧20x201~0x21F每秒一次判断节点在线状态配置帧30x301~0x31F修改上报周期、阈值参数这样划分后任何报警帧的 ID 都小于数据帧数据帧小于心跳帧。总线拥塞时高优先级报文先抢到发送权低优先级自动退避不会因为某个节点频繁上报导致其他节点的报警发不出去。接收端用掩码滤波器只放行某一类报文比如只收类型号为 1 的数据帧CAN_FilterInitStructure.CAN_FilterIdHigh 0x100 5; // 期望 ID 0x100高半字 CAN_FilterInitStructure.CAN_FilterIdLow 0x0000; // 低半字IDE0 表示标准帧 CAN_FilterInitStructure.CAN_FilterMaskIdHigh 0x700 5; // 只比较 ID[10:8] 类型段 CAN_FilterInitStructure.CAN_FilterMaskIdLow 0x04; // 强制匹配 IDE0这里的 5 是 STM32 滤波器寄存器特有的对齐11 位标准 ID 存放在 32 位滤波寄存器的 bit31~21高 16 位寄存器里恰好是 ID 左移 5 位。掩码为 1 的位必须匹配为 0 的位不关心0x700 5 0xE000 锁定了类型号三段。这个移位是滤波器配置里最容易写错的地方建议先在纸上把 ID 的 11 个 bit 画出来再对着寄存器位填。4.2 数据帧协议设计一帧装两组温湿度还是拆帧温湿度采集的典型配置是每个节点带 2~4 个传感器分布在节点周边几米内。8 字节数据段可以装下两组温度 湿度每组占 4 字节温度整字节、温度小数字节、湿度整字节、湿度小数字节。8 个节点、每节点 2 组数据一个完整周期就是 8 帧总共约 900 bit250kbps 下 4ms 就能传完负载率很低。如果要求 0.1℃ 分辨率温度和湿度各用 2 字节一组就要 4 字节8 字节刚好装两组要 0.01℃ 精度一帧只能装一组数据帧数翻倍负载率跟着翻倍。分辨率与帧容量的权衡就体现在这里。协议里还应该在数据段末尾留一个校验字节采用累加和或 CRC8避免接收端把损坏的数据当真实采样值用。4.3 CAN总线负载率计算公式与Python实测脚本负载率定义为单位时间内总线上被占用的时间比例计算核心是每帧的总位长负载率 Σ(每帧总位长) / (波特率 × 上报周期)每帧总位长除了协议本身的帧结构还要算上填充位。CAN 规定连续 5 个相同电平后必须插入一个反向填充位填充位数量取决于数据内容随机数据平均约 20% 的位会触发填充工程估算按 1.2 倍系数。def can_load_rate(bit_rate_hz, frame_count, dlc, period_s, stuff_ratio0.2): # 标准帧数据帧固定开销SOF1 仲裁12 控制6 CRC16 ACK2 EOF7 IFS3 47 bit overhead 47 data_bits dlc * 8 base_bits overhead data_bits bits_per_frame base_bits * (1 stuff_ratio) return frame_count * bits_per_frame / period_s / bit_rate_hz * 100 # 8 节点每节点每秒 1 帧数据 1 帧心跳DLC6250kbps rate can_load_rate(250_000, frame_count16, dlc6, period_s1.0) print(f理论负载率: {rate:.2f}%) # 输出约 0.73%算出来的 0.73% 说明余量非常大。CAN 总线的工程经验是长期负载率控制在 30% 以下短时峰值不超过 50%超过后低优先级帧的延迟会明显变大因为 CAN 没有以太网那样的队列缓冲发不出去的帧只能反复重试或丢弃。负载率上去了先怀疑上报周期是否被中断优先级拖慢再检查有没有节点在疯狂重发错误帧。注意实测负载率通常比理论值高。DHT11 的读取时序抖动会让实际上报周期比配置值略短错误重传也会额外占用总线。两者偏差在 20% 以内属于正常偏差过大时要先排查错误帧而不是改公式系数。5. CAN总线测试实操回环模式、波形验证与Bus Off恢复5.1 回环模式先通软件链路再接收发器板子焊好先别接 TJA1050把 CAN_Mode 改成 CAN_Mode_LoopBack 回环模式。回环模式下报文在控制器内部自发自收不经过引脚也不占用总线专门用来验证初始化参数、滤波器配置和接收中断是否正常。测试步骤是调用一次发送再到 FIFO0 接收中断里比对收到的帧内容逐字节一致说明软件链路通。CAN_InitStructure.CAN_Mode CAN_Mode_LoopBack; // 只改这一行做自测 // 发送 0x101 帧后检查 CAN_TransmitStatus(CAN1, 邮箱号) // 返回 CAN_TxStatus_Ok且接收中断收到同样一帧即为通过回环通过后切回 CAN_Mode_Normal 再挂收发器。回环正常、正常模式却收不到时按顺序查三处TJA1050 的 TXD/RXD 是否误接到了其他引脚总线两端是否各有一个 120Ω 终端电阻对端设备的波特率是否一致。这三个问题占正常模式联调失败原因的八成以上。5.2 示波器看差分波形与Bus Off恢复策略注意抓波形用示波器的差分模式两个探头分别夹 CANH 和 CANL不要对地测单端电平单端波形受共模干扰影响容易误判。空闲状态下 CANH 和 CANL 都稳定在 2.5V 附近出现显性位时 CANH 抬到约 3.5V、CANL 拉低到约 1.5V差分幅度 2V。空载时如果线上不是 2.5V优先检查收发器供电和终端电阻波形能抓到但帧发不出去多半是 ACK 错误——总线上只有发方自己没有节点应答。Bus Off 之后控制器脱离总线配置了 CAN_ABOM ENABLE 会自动恢复但恢复时机不受应用控制。工程做法是把周期性温湿度帧直接丢弃不加重发逻辑错过一个周期下一个周期自然补上对必须送达的配置帧则放进环形缓冲等发送邮箱空闲了再补发。最后用 USB-CAN 分析仪对比实测负载率和 4.3 节脚本算出的理论值偏差超过 20% 时先去查节点时钟误差和错误重传次数而不是先怀疑计算公式。本文还有配套的精品资源点击获取