
简介这是一份基于STM32的BNO085姿态传感器数据读取与CAN总线发送的嵌入式实战项目适合单片机初学者、毕业设计/课程设计/工程实训等场景。资源包含完整可编译工程与烧录固件主控以STM32F1系列HAL库为基础实现I2C读取BNO085姿态数据并通过CAN外设将状态信息发送到总线覆盖传感器驱动、协议解析、CAN通信等关键环节。压缩包共225个文件含63个h头文件、35个c源文件、工程配置文件uvprojx/ioc、编译中间文件与hex/axf烧录文件整体约9.06MB目录结构清晰便于直接导入Keil查看或二次开发。已有679人学习下载。资料附带使用说明开发者可快速复现也可基于现有驱动扩展更多传感器或通信功能适合作为项目基础框架参考。1. 用CAN总线把BNO085的姿态数据送出去比想象中多两个坑BNO085 内部自带一颗协处理器加速度计、陀螺仪、磁力计的数据会先在传感器内部融合成四元数再输出所以在 STM32 上读它并不需要自己写姿态解算。真正出问题的往往在两个地方一个是对 SH-2 协议包的字节序和通道号理解不到位不同固件版本会对不上另一个是一旦接上 CAN波特率、位时间段、终端电阻、ID 冲突会同时冒出来。这个标题对应的实现简单说就是 STM32 用 UART 读 BNO085 的姿态状态数据解析成欧拉角和状态字后打包成 CAN 数据帧按固定周期向总线上广播。对课设、毕设和竞赛来说这条链路覆盖了传感器驱动、串口协议、总线通信三个必查的验收点顺着本文的接线和代码可以直接跑通再按自己的报告格式改字段就行。2. 硬件怎么接、CubeMX参数怎么填先把链路基础钉死把传感器和 CAN 收发器接好是后面所有调试的前提。BNO085 的 UART 接口是 3.3V 电平STM32F103 的引脚也是 3.3V两者可以直接连不建议中间再串 5V 电平转换或分压电阻。CAN 收发器这里按最常见的 TJA1050 或板载收发器模块处理如果你的板子已经集成了收发器TXD/RXD 引脚直接接到 STM32 即可。2.1 接线表BNO085 与 STM32F103C8 的 UART 连接下表默认使用 USART1这是 F103 上最容易配、也最不容易与其他调试口冲突的一组引脚。BNO085 引脚STM32F103C8 引脚方向说明VIN3.3V电源输入BNO085 的 VIN 不是 5V tolerant接错容易烧模块GNDGND电源地必须与 STM32 共地TXPA10 (USART1_RX)BNO085 - STM32传感器数据输出脚RXPA9 (USART1_TX)STM32 - BNO085传感器数据输入脚也用来发使能命令CAN 收发器接线如下CAN 收发器引脚STM32F103C8 引脚说明TXDPA12 (CAN1_TX)CAN1 默认映射RXDPA11 (CAN1_RX)CAN1 默认映射CANH / CANL总线差分线总线两端各接 120Ω 终端电阻注意F103C8 的 CAN 引脚可以通过重映射换到 PB8/PB9但默认映射 PA11/PA12 最省事。如果 PA11/PA12 被其他外设占用再考虑重映射同一时间只能启用一组映射。2.2 CubeMX 关键配置时钟树决定 CAN 波特率是否准确在 STM32CubeMX 里新建 F103C8 工程后先照下面这张表把系统时钟和外设状态配置好。配置项值说明SYS DebugSerial Wire不要选 JTAG否则会占用 PB3/PB4RCC HSECrystal/Ceramic Resonator按板载 8MHz 晶振设置主频72MHzAPB1 总线时钟 36MHz挂 CAN1USART1 ModeAsynchronous波特率 1152008N1CAN1 ModeActivated波特率填 500000 Bits/s这里最容易算错的就是 APB1。CAN1 在 F103 上挂在 APB1 总线上APB1 时钟是 36MHzCAN 外设的位时间参数也基于这个时钟计算。CubeMX 里直接填 500kbps还需要检查生成的 CAN 初始化代码中时序参数是否合理。常见的一组参数是预分频 Prescaler4BS113tqBS24tqSJW1tq。计算如下tq 时间 1 / (36MHz / 4) 0.111μs一个位时间 1 13 4 18tq即 2μs波特率 1 / 2μs 500kbps如果你用的是 Keil MDK 而不是 CubeIDE先去 Pack Installer 安装 STM32F1xx_DFP 芯片包否则器件列表里选不到 STM32F103CB。很多新手在第一步就卡在这里编译时提示找不到目标芯片头文件。2.3 生成工程后先做一次烧录验证代码生成后先不急着接 BNO085写一个点灯程序确认 ST-Link 烧录链路是通的。这个是整个方案的兜底如果烧录都不通后面所有调试都没有意义。提示烧录时如果报No STM32 target found!优先检查 ST-Link 的 SWD 四根线有没有接反、目标板是否有 3.3V 供电、BOOT0 是否拉低。还有一部分是板载 ST-Link 固件版本过旧用 STM32CubeProgrammer 升级一下固件再试。Windows 下如果 USB 转串口设备出现黄色感叹号驱动也要一并处理否则后面 printf 输出根本看不到。烧录通过后建议先做两项快速检查用万用表确认 PA9/PA10 对地电压约为 0V 到 3.3V 之间的逻辑电平量一下 CANH 和 CANL 之间是否能看到约 60Ω 的终端电阻值。两项都正常再往下走。3. 读 BNO085 前先把 SH-2 协议包拆明白BNO085 的 UART 输出不是简单的“一帧数据”而是带通道、序列号、校验和的 SHTP 包。很多人在这一步直接把收到的字节流按固定长度截断结果姿态数据总是乱跳问题就出在没按协议边界解析。3.1 SHTP 包的最小结构一个标准的 SHTP 包由以下几部分组成。字段长度说明包起始1 字节固定 0x50payload 长度2 字节小端从 channel 字节开始计数channel1 字节命令通道和传感器报告通道是分开的sequence1 字节每发一包加 1用于对包序payload变长命令参数或传感器报告数据checksum1 字节对包起始到 payload 末尾所有字节的补码和校验和的具体算法是从 0x50 开始把包括 payload 在内的所有字节求和取低 8 位再取反加一。也就是说把校验和字节也加进去之后整个包所有字节的和对 256 取模必须等于 0。这个校验在解析端非常有用能过滤掉大部分串口电平抖动产生的错包。一个完整包的十六进制示意 50 08 00 02 0A 00 00 F9 0B 14 00 00 00 00 84其中08 00表示 payload 长度 802是命令通道0A是序列号F9 0B 14 00 00 00 00是 SET_REPORT 命令数据84是校验和。你不需要背这个结构但解析代码必须按照这个边界来做不能用固定字节偏移。3.2 让 BNO085 持续上报旋转向量上电后 BNO085 不会默认输出姿态数据需要先通过命令通道发一条 SET_REPORT 请求。下面这段代码把旋转向量报告使能命令封装成一个函数间隔参数单位是毫秒。void bno085_enable_rotation_vector(uint8_t seq, uint16_t interval_ms) { uint8_t pkt[15] { 0x50, 0x08, 0x00, // SHTP 起始 payload 长度 8 0x02, // 命令通道 seq, // 序列号每次自增 0x00, 0x00, // 最大响应时间填 0 表示不限制 0xF9, // SET_REPORT 0x0B, // 旋转向量 feature ID interval_ms 0xFF, // 报告间隔低字节 interval_ms 8, // 报告间隔高字节 0x00, 0x00, 0x00, // 后续参数按 feature 定义补齐 }; uint8_t sum 0; for (uint8_t i 0; i 14; i) { sum pkt[i]; } pkt[14] (uint8_t)(~sum 1u); // 补码和校验 HAL_UART_Transmit(huart1, pkt, sizeof(pkt), 20); }调用时 seq 建议用一个全局变量递增避免连续发多条命令时序列号相同导致传感器忽略请求。interval_ms填 20 表示每 20ms 上报一次姿态对应 50Hz够一般运动控制用。如果后续 CAN 发送周期也是 20ms那传感器每次上报的数据都能对应上一帧 CAN 报文。3.3 一个字节一个字节喂出来的解析状态机UART 中断每收到一个字节就交给下面的状态机函数处理。状态机的作用是先在字节流里找到 0x50再根据长度字段确定这一包什么时候结束最后用校验和决定这包数据是否有效。static uint8_t rx_buf[128]; static uint16_t rx_len, target_len; static uint8_t rx_sum, rx_state; void bno085_feed_byte(uint8_t c) { switch (rx_state) { case 0: // 等待包起始 if (c 0x50) { rx_buf[0] c; rx_sum c; rx_len 1; rx_state 1; } break; case 1: // 收到长度低字节 rx_buf[1] c; rx_sum c; rx_len 2; rx_state 2; break; case 2: // 收到长度高字节 rx_buf[2] c; rx_sum c; rx_len 3; target_len rx_buf[1] | (rx_buf[2] 8); if (target_len 4 sizeof(rx_buf)) { rx_state 0; // 长度异常重新同步 } else { rx_state 3; } break; default: // 连续接收剩余字节和校验和 rx_buf[rx_len] c; rx_sum c; if (rx_len target_len 4) { if ((uint8_t)rx_sum 0) { // 补码和校验通过 bno085_parse_packet(rx_buf, rx_len); } rx_state 0; } break; } }这里有几个参数要解释清楚。target_len是 SHTP 头里的 payload 长度它从 channel 字节开始计数。包的总字节数等于target_len 4其中 4 是指包起始 1 字节、长度 2 字节、校验和 1 字节。状态机收到全部字节后用rx_sum 0做校验因为补码和的特性就是整个包所有字节相加低 8 位为 0。接下来在解析函数里把四元数转成欧拉角并记录精度状态字static int16_t rd_i16(uint8_t *p) { return (int16_t)(p[0] | (p[1] 8)); // SHTP 里多字节为小端 } static void bno085_parse_packet(uint8_t *buf, uint16_t len) { if (buf[3] ! SENSOR_CHANNEL) { // 只处理传感器报告通道 return; } uint8_t *d buf[5]; // 跳过 channel 和 seq if (d[0] 0x0B) { // 旋转向量报告 ID float qr rd_i16(d[1]) / 16384.0f; // 实数部分按 14 位定点 float qi rd_i16(d[3]) / 16384.0f; float qj rd_i16(d[5]) / 16384.0f; float qk rd_i16(d[7]) / 16384.0f; att.roll atan2f(2.0f * (qr * qi qj * qk), 1.0f - 2.0f * (qi * qi qj * qj)); att.pitch asinf(2.0f * (qr * qj - qk * qi)); att.yaw atan2f(2.0f * (qr * qk qi * qj), 1.0f - 2.0f * (qj * qj qk * qk)); att.status d[9] 0x03; // 精度状态0 到 3 att_frame_valid 1; } }注意旋转向量报告里四元数的排列顺序不同固件版本的字段定义可能不一样。调试时先让模块静止看解析出的 roll 和 pitch 是否接近 0yaw 是否随转动单调变化。如果发现 yaw 不动、roll 乱跳大概率是 qr 和 qi 的顺序反了调换字段位置即可。3.4 串口调试输出先确认解析结果再连 CAN不要直接跳到 CAN 发送。先用串口把 yaw、pitch、roll 打出来观察数据是否符合物理转动。这一步可以省掉后面两倍的排错时间。printf(yaw%.2f pitch%.2f roll%.2f st%d\r\n, att.yaw * 57.3f, att.pitch * 57.3f, att.roll * 57.3f, att.status);把模块平放yaw 会在当前朝向附近小幅波动roll 和 pitch 应该在 0 附近小幅跳动status 字段从 0 慢慢变到 3 说明传感器内部已经完成初始化校准。如果 yaw 一直在跳且没有规律检查模块周围有没有强磁干扰以及 BNO085 的安装方向是否和预期一致。4. 报文设计与 bxCAN 发送状态数据按帧广播CAN 侧的任务比 UART 简单难点在设计数据帧的字段分配和发送周期。BNO085 已经给出了四元数、欧拉角、精度状态CAN 报文只需要把这些数据压缩成 8 字节再周期性调用发送函数。4.1 数据帧字段设计我一般把 CAN 的标准帧 ID 定为 0x1818 字节数据段按下面的格式填充。字节字段格式说明Byte 0-1Yawint160.01°/位小端序Byte 2-3Pitchint160.01°/位小端序Byte 4-5Rollint160.01°/位小端序Byte 6状态字bit0 数据有效bit1-2 精度等级0 到 3Byte 7帧计数uint80-255 循环接收端检测丢帧这里的角度精度按 0.01° 量化能满足大多数运动控制和车辆姿态监控场景。如果你需要更高精度可以把 Yaw 改成 float32 的弧度值但那样 8 字节只能装下 2 个浮点数需要拆成两帧。毕设和课设场景里0.01°的 int16 量化已经足够还省了总线占用。滤波器配置也要在启动时做好。下面的配置把接收过滤器放空也就是不过滤任何接收帧方便调试CAN_FilterTypeDef filter {0}; filter.FilterActivation ENABLE; filter.FilterBank 0; filter.FilterFIFOAssignment CAN_RX_FIFO0; filter.FilterIdHigh 0x0000; filter.FilterIdLow 0x0000; filter.FilterMaskIdHigh 0x0000; filter.FilterMaskIdLow 0x0000; filter.FilterMode CAN_FILTERMODE_IDMASK; filter.FilterScale CAN_FILTERSCALE_32BIT; HAL_CAN_ConfigFilter(hcan1, filter); HAL_CAN_Start(hcan1);如果只允许接收 ID 0x181 的报文把FilterIdLow填成(0x181 5)FilterMaskIdLow填成0x7FF 5掩码位为 1 表示必须匹配。4.2 发送函数与邮箱状态检查下面这个函数把欧拉角转成 int16 后塞进 8 字节帧并通过 bxCAN 的发送邮箱发出。发送前先查邮箱空闲数量避免在邮箱满时调用 HAL_CAN_AddTxMessage 造成阻塞。static uint8_t can_seq; void can_send_attitude(void) { uint8_t data[8]; int16_t yaw (int16_t)(att.yaw * 5729.58f); // 弧度转 0.01° int16_t pitch (int16_t)(att.pitch * 5729.58f); int16_t roll (int16_t)(att.roll * 5729.58f); data[0] (uint8_t)(yaw 8); data[1] (uint8_t)(yaw 0xFF); data[2] (uint8_t)(pitch 8); data[3] (uint8_t)(pitch 0xFF); data[4] (uint8_t)(roll 8); data[5] (uint8_t)(roll 0xFF); data[6] (uint8_t)(0x01 | ((att.status 0x03) 1)); data[7] can_seq; uint32_t mailbox; if (HAL_CAN_GetTxMailboxesFreeLevel(hcan1) 0) { HAL_CAN_AddTxMessage(hcan1, tx_header, data, mailbox); } }tx_header的配置在前面第 2 章的 CubeMX 生成代码中已经定义显式指定一下更稳妥CAN_TxHeaderTypeDef tx_header { .StdId 0x181, .ExtId 0, .IDE CAN_ID_STD, .RTR CAN_RTR_DATA, .DLC 8, .TransmitGlobalTime DISABLE, };发送时机放在主循环的 tick 判断里不放在 BNO085 的 UART 中断回调里直接发。原因是 SH-2 上报频率和 CAN 发送频率未必一致串口中断里做 CAN 发送会让优先级关系变得复杂。4.3 发送周期与总线负载怎么权衡假设 BNO085 每 20ms 出一包姿态数据CAN 发送周期最好也设为 20ms 或 10ms。如果设 1ms 发送总线上一半以上的报文都在重复同一份姿态数据负载率会被白白抬高。按 500kbps 计算一个 8 字节标准数据帧包含帧头和填充后大约占用 220μs 到 250μs。发送周期单节点负载率5 节点同频率发送1ms约 25%超过 100%总线不可用5ms约 5%25%10ms约 2.5%12.5%20ms约 1.25%6.25%CAN 总线的仲裁机制是 ID 越小优先级越高。0x181 属于自定义地址在它上面可以再挂更高优先级的节点用 0x100 之类的 ID确保紧急报文能先发出去。如果你的项目中还有一个控制器接收这条姿态报文务必让接收端只按 ID 过滤不要依赖字节里的帧计数来判断报文先后顺序。4.4 关于 CAN FD 的一个提醒如果用的是带 FDCAN 的 STM32G4 或 H7且总线上的其他节点也支持 CAN FD可以把四元数、欧拉角、时间戳一次塞进 64 字节数据段里甚至做速率切换加快传输。但真正的工程选型里CAN FD 的兼容性问题远多于标准 CAN。毕设和课设的外设节点往往是 USB-CAN 分析仪很多低价分析仪只支持标准 CAN不支持 CAN FD如果一开始就上 FD到最后验证环节会发现自己抓不了包。建议默认用标准 CAN 把链路跑通再考虑要不要升级 FD。5. 用回环和波形把整条 CAN 链路验收掉到了最后阶段数据已经从 BNO085 读到 STM32也调用了 CAN 发送函数。这时最大的风险不是发送代码而是总线上根本没有人确认报文。下面这套验证路径我每次都会走一遍能把问题范围快速缩小。5.1 回环模式不接外部设备先自测先把 CAN1 的工作模式改成回环模式这一步不需要外部 CAN 收发器和终端电阻数据会从内部直接回到接收 FIFO。hcan1.Init.Mode CAN_MODE_LOOPBACK; HAL_CAN_Start(hcan1);在发送函数被调用后打开 CAN1 接收中断在HAL_CAN_RxFifo0MsgPendingCallback回调里打印收到的 ID 和数据。如果收到的 ID 是 0x181且数据里的 yaw 和本地发送前的 yaw 一致说明 CAN 外设的配置和发送流程是正确的问题只剩外部物理链路。5.2 用示波器看 CAN 波形判断链路好坏接上 TJA1050 和外部总线后用示波器探头分别点 CANH 和 CANL重点看差分电压。隐性电平时 CANH 和 CANL 都在 2.5V 附近差分接近 0V显性电平时 CANH 升到约 3.5VCANL 降到约 1.5V差分约 2V。如果波形幅值明显偏低或者显性电平持续时间不稳定先检查终端电阻。现象可能原因波形幅值只有 1V 左右缺少终端电阻或某端 120Ω 脱落帧和帧之间毛刺多节点供电不稳或收发器电源滤波不够一直发错误帧波特率不匹配或总线上某个节点未上电发送后无 ACK 时隙总线只有一个节点收发器无 ACK 回应这里有一个很容易被忽略的坑如果总线上只有 STM32 一个节点且没有接 USB-CAN 分析仪发送的帧会因为没人回复 ACK 而导致发送邮箱一直被占住现象是 Can 发送失败或初始化后无法继续发送。这不是代码问题接一个能回 ACK 的节点就能解决。5.3 用 USB-CAN 抓包做端到端一致性校验最后一步是把 USB-CAN 分析仪挂到总线上波特率设为 500kbps抓取 0x181 的报文。让 BNO085 在水平桌面上缓慢旋转观察分析软件里的 yaw 值是否从 0 到 360 平滑变化帧计数器是否连续递增。如果帧计数器出现跳号说明某一次发送被总线错误打断需要回到第 5.2 节查波形。还建议做一次“数据有效性”的确认把模块断电再上电观察状态字从 0 到 3 的过程。精度状态 0 表示传感器还在初始化此时 CAN 帧里的欧拉角不可信状态变成 3 之后姿态数据才能被接收端当作有效值。把这一条逻辑直接写进接收端比在发送端过滤更可靠。本文还有配套的精品资源点击获取