简介:基于STM32F10x与CAN总线的多节点温湿度数据采集系统设计资料,面向毕业设计、课程设计及工程实训场景。项目通过CAN请求/应答机制与两个从机通信,将温湿度实时显示在LCD上并经串口打印,完整演示了CAN总线多节点组网、数据帧收发与显示处理的开发流程。压缩包共463个文件,以99个h头文件和95个c源文件为主,便于阅读与二次开发;另含MDK工程文件(uvproj)、hex烧录文件及编译中间文件,解压后可直接打开或烧录验证。整体约6.57MB,结构紧凑,适合快速上手复刻。目前已有343人学习使用。资料内包含源码、工程与引脚连接说明,即便不自行绘制PCB,也可用面包板配合杜邦线搭建外设完成验证。所有代码经过测试可直接运行,适合希望深入理解CAN协议并完成课设、毕设或竞赛练手的开发者。
1. 为什么多节点温湿度采集要选STM32F10+CAN总线
多节点温湿度采集,用 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 位 ID,CAN 2.0B 扩展帧是 29 位 ID;温湿度采集这类节点数不超过 32 个的系统,标准帧完全够用,帧体更短,相同波特率下能多传不少数据。IDE 位为 0 表示标准帧,RTR 位为 0 表示这就是数据帧而不是远程帧,DLC 用 4 位表达,协议上最大是 15,但一条报文的数据段最多只有 8 字节,这是设计应用层协议时绕不开的硬约束。
| 段名 | 关键字段 | 位长 | 作用 |
|---|---|---|---|
| 帧起始 | SOF | 1 | 显性电平,宣告总线从空闲变忙 |
| 仲裁段 | ID + RTR | 12 / 32 | 11 位或 29 位标识符,决定优先级 |
| 控制段 | IDE + DLC | 6 | 区分标准/扩展帧,声明数据长度 |
| 数据段 | Data | 0~8 字节 | 温湿度数据实际存放位置 |
| CRC 段 | CRC + 界定符 | 16 / 17 | 校验错误、错误帧判定的关键区域 |
| ACK 段 | 应答位 + 界定符 | 2 | 接收节点必须回显性应答 |
| 帧结束 | EOF | 7 | 隐性电平,结束帧 |
写代码时最容易漏的是 ACK 段:发送节点把报文发完后,必须有一个接收节点在 ACK 槽回一个显性位,否则发送节点按错误处理流程重发。课程设计里最常见的"发不出去"现象,就是总线上只挂了单板,没有第二个节点回 ACK,发送邮箱一直显示 pending。
2.2 仲裁机制:多节点同时发送时谁赢,要不要上CAN FD
CAN 的物理层只区分显性(逻辑 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 / ((BRP+1) × (1 + BS1 + BS2))以 36MHz 出 500kbps 为例,BRP 取 3,BS1 取 10Tq,BS2 取 7Tq,加上 SYNC_SEG 的 1Tq 一共 18Tq,代入公式正好是 500k。采样点位置则是 (SYNC_SEG + BS1) / 位时间 = 11/18,约 61%。BS1 必须比 BS2 大,就是为了让采样点落在位的后半段,避开跳变沿附近的不稳定区间;总线越长、节点越多,采样点越往后放越稳。
| 目标波特率 | BRP | BS1 | BS2 | SJW | 采样点 |
|---|---|---|---|---|---|
| 1Mbps | 1 | 9Tq | 8Tq | 4Tq | 约 63% |
| 500kbps | 3 | 10Tq | 7Tq | 4Tq | 约 61% |
| 250kbps | 7 | 10Tq | 7Tq | 4Tq | 约 61% |
SJW 是重同步时的最大跳变宽度,取值不能超过 BS2。各节点时钟存在微小偏差,收到显性位跳变时会动态调整位时序对齐,SJW 越大纠偏越快,但太大反而容易误同步,500kbps 下取 4Tq 是常用值。全总线节点必须用同一套位时序参数,一个节点配错,初期看不出问题,跑一段时间错误计数器涨上去,整段总线都会瘫痪在错误帧里。
3. 基于STM32F103的bxCAN初始化与温湿度节点实现
3.1 硬件链路:STM32F103C8T6、TJA1050与DHT11怎么接
一个采集节点由三部分组成:STM32F103C8T6 最小系统板承担控制逻辑,TJA1050 负责把控制器的 TTL 电平转成 CAN 总线差分电平,DHT11 负责采温湿度。控制器的 CAN_RX/CAN_TX 引脚固定在 PA11/PA12,TJA1050 的 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 对应上节表格里的 BRP,CAN_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 是逻辑 0,70us 左右是逻辑 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~28us,40us 时已经回落;逻辑 1 的高电平约 70us,40us 时还维持在高位。这段代码要求 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; // 传感器类型:0x01=DHT11 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 区间 | 用途 |
|---|---|---|---|
| 报警帧 | 0 | 0x001~0x01F | 温湿度越限立即上报,优先级最高 |
| 数据帧 | 1 | 0x101~0x11F | 周期上报温湿度 |
| 心跳帧 | 2 | 0x201~0x21F | 每秒一次,判断节点在线状态 |
| 配置帧 | 3 | 0x301~0x31F | 修改上报周期、阈值参数 |
这样划分后,任何报警帧的 ID 都小于数据帧,数据帧小于心跳帧。总线拥塞时高优先级报文先抢到发送权,低优先级自动退避,不会因为某个节点频繁上报导致其他节点的报警发不出去。接收端用掩码滤波器只放行某一类报文,比如只收类型号为 1 的数据帧:
CAN_FilterInitStructure.CAN_FilterIdHigh = 0x100 << 5; // 期望 ID 0x100,高半字 CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000; // 低半字,IDE=0 表示标准帧 CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x700 << 5; // 只比较 ID[10:8] 类型段 CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x04; // 强制匹配 IDE=0这里的 <<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 bit,250kbps 下 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_ratio=0.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 帧心跳,DLC=6,250kbps rate = can_load_rate(250_000, frame_count=16, dlc=6, period_s=1.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% 时,先去查节点时钟误差和错误重传次数,而不是先怀疑计算公式。
本文还有配套的精品资源,点击获取