简介:本资源是一套基于STM32平台实现毫米波雷达CAN数据实时采集与解析的完整嵌入式工程,面向嵌入式开发工程师、智能感知系统学习者及自动驾驶/工业传感方向实践者,解决毫米波雷达原始CAN报文接收难、协议解析无参考、STM32 CAN外设配置调试复杂等实际问题。压缩包共168个文件,含48个头文件(.h)定义寄存器与协议结构、47个C源文件(.c)覆盖CAN初始化、中断接收、帧解析、LCD显示等核心模块,另有编译输出文件(.axf/.hex/.map)、Keil工程配置(.uvprojx/.uvoptx)及构建脚本(.bat),总大小4.84MB。已有1946人学习下载,工程基于STM32F4系列,代码结构清晰分层,包含stm32f4xx_can.c等底层驱动、lcd.c等外设适配及完整CAN滤波与ID匹配逻辑,可直接编译烧录运行,附带实测数据格式说明与解析映射关系,大幅降低毫米波雷达接入门槛。 前阵子有个做无人车底盘的朋友问我:手头有一块毫米波雷达,支持CAN输出,怎么在STM32上把目标数据读出来?我直接丢给他一份例程让他跑通了再回来聊。他折腾了一个周末,回来第一句话就是:“网上的资料太散了,又是配置寄存器又是协议手册,半天拼不出一个能跑的东西。”
其实这个事没那么玄乎。毫米波雷达通过CAN总线往外发数据,STM32这边只需要做三件事:让CAN外设正常收发、把报文按协议字段切开、再按缩放系数换算成物理量。这三步里,前两步是通用技能,第三步完全取决于你手上雷达的协议手册。今天的博文就围绕“毫米波雷达CAN数据解析”这条主线,把从硬件接线、CubeMX配置、代码实现到常见排障的完整过程拆开讲一遍。适合正在用STM32做雷达数据对接、或者准备把毫米波雷达接入自己项目的朋友参考,内容我尽量控制在一个下午能复现的颗粒度。
1. 项目整体设计与思路拆解
1.1 毫米波雷达与CAN总线:为什么这个组合是主流
先把应用场景摆出来。毫米波雷达在辅助驾驶、机器人避障、安防监控里已经非常常见,输出的是目标级数据,比如某个目标的距离、速度、角度、反射强度,甚至微多普勒特征。这类数据更新频率一般在10Hz到30Hz,单条报文的数据量并不大,长度基本在8个字节以内。所以很多人会纠结一个问题:雷达数据为什么不用串口或者以太网,偏偏用CAN?
答案是可靠性和确定性。CAN总线是差分信号传输,抗干扰能力比单端串口好得多,在电机、电调等强干扰环境下不容易丢帧。同时CAN的报文优先级机制很实用——雷达检测到紧急目标时可以把对应报文ID的优先级调高,保证重要的目标信息不会被拥堵的总线拖延。从车规级应用沿用到机器人、工业场景,CAN成为毫米波雷达最主流的输出接口一点都不意外。
我在实际项目里接触过两种CAN输出形态的雷达:一种是标准CAN 2.0,波特率常见500kbps,报文ID和字节布局完全由雷达厂商自定义;另一种是CAN FD,现在一些新型4D毫米波雷达会上这玩意儿,数据场可以到64字节,能同时把目标列表、点云信息一起塞进去。本篇重点讲CAN 2.0的场景,因为目前市面上量最大的毫米波雷达仍然以这个为主。
1.2 数据链路总览:从雷达信号到STM32可读信息
把整条数据链路画在脑子里,总共四段:雷达通过内部DSP完成目标检测,把目标参数封装成CAN报文,通过物理层CAN收发器发送到总线上;STM32这边用CAN外设接收报文,完成验收滤波、去填充、错误检测后,把有效数据存进邮箱或FIFO;然后由应用程序读取这些报文;最后一步是按照雷达的通信协议解析出实际物理量。
听起来环节多,但对你写代码而言真正要关心的只有两个部分:STM32的CAN外设寄存器和雷达协议文档。前者是通用的,任何MCU厂商的CAN模块实现逻辑都差不多;后者则完全是依赖雷达型号的。我记得第一次调试某款24GHz雷达时,看到协议文档里写“目标距离字段偏移量为10,缩放因子为0.1m”,就知道距离值要从报文数据段的某个字节取出来,再乘0.1,否则你读到的只是一个原码数据。
这里就引出一个常见误区:很多人拿到例程后发现“读到的数值和雷达屏幕上看到的不一样”,其实不是通信坏了,而是忘了做缩放。比如速度字段可能是补码表示,负号代表目标远离,如果协议文档没有写清楚是有符号数,你按无符号数解析出来就是十几万这样的巨大数。这种坑后面我会专门展开。
2. 核心细节解析与实操要点
2.1 读懂雷达CAN报文协议:ID、DLC与数据段布局
拿到任何一款毫米波雷达,第一件事不是接杜邦线,而是看协议文档里那几张报文定义表。CAN报文本身只有三件事值得关心:ID、数据长度(DLC)、8个字节的数据段。绝大多数雷达协议会把目标数据按“帧类型”划分,比如0x501是目标1的状态报文、0x502是目标1的距离速度报文、0x600开头的多帧用于点云等等。
以我常用的某款77GHz雷达为例,它的协议表长这样:
| 报文ID | 报文名称 | 数据长度 | 典型内容 |
|---|---|---|---|
| 0x501 | 目标状态 | 8 | 目标存活标识、ID、量测状态 |
| 0x502 | 目标运动 | 8 | 纵向距离、横向距离 |
| 0x503 | 目标速度 | 8 | 纵向速度、横向速度 |
| 0x504 | 目标属性 | 8 | RCS、角度、加速度 |
每个字段在数据段里占据的位宽、偏移量、缩放因子,协议文档里都有详细表格。你需要做的是把这些信息“翻译”成结构体。这里我的习惯是,先把每一个报文ID定义好一个typedef结构体,然后把接收到的原始字节按偏移填进去,再用统一的解析函数换算物理量。这样一来,后续换雷达型号时,只需要改结构体定义和换算系数,主流程基本不用动。
一个值得注意的细节:CAN报文的数据段是高字节在前还是低字节在前,每个厂商习惯不同。有的雷达直接用大端排列,有的则是小端,还有的干脆每个字段按位偏移手工摆。一定要逐个字段比对,不要想当然地认为第0字节就是协议表里第一个字段。我自己就吃过这个亏,某雷达把速度字段放在Byte3的bit4到bit11,直接按字节切就错了。
2.2 硬件连接与接口电路:别让电平把你坑了
硬件部分可能是整个项目里最“隐蔽”的坑。很多人直接用STM32开发板上的CAN_TX和CAN_RX引脚去接雷达的CAN_H和CAN_L,结果发现无论如何都收不到数据。原因是STM32的CAN控制器只输出逻辑电平,真正在物理总线上传输差分信号的是外部的CAN收发器。开发板上常见的TJA1050、SN65HVD230、MCP2562就是干这个活的。
一个典型接线图是:STM32的PA11接到收发器的RXD,PA12接到TXD,收发器的CANH、CANL分别接雷达的CANH、CANL。要注意,两个设备之间一定要共地,也就是把收发器的GND和雷达的GND接在一起,很多调试不出数据的问题都出在地没共好。我做过一个小实验,雷达和STM32分别用两个独立开关电源供电,地线没接时偶尔能收到一帧,接了之后数据稳定得一批。
终端电阻也是很容易被忽略的点。CAN总线规范要求两端各接一个120欧姆终端电阻,用来匹配总线阻抗、消除信号反射。如果你的总线上只有雷达和STM32两块设备,那就是两端,各加一个120欧姆。如果实际系统里还有其他CAN节点,要注意不要重复接太多终端电阻,否则总线负载会变重,极端情况下会导致通信错误率上升。判断总线状态有个懒人方法:在静默状态下用万用表量CANH到CANL之间的电阻,如果接近60欧姆,说明两个120欧姆到位了;如果只有120欧姆,说明只接了一端;如果是0或者无穷大,那就要重新查了。
还有一点,有些雷达模块本身端口已经内置了终端电阻,你再在STM32这边接一个120欧姆,就变成双重终端了。这个没法从外观判断,最靠谱的是翻雷达硬件手册,确认“内置终端电阻”这项描述。之前在调试一款工业雷达时,对方硬件工程师明确说模块内部有120欧姆,所以我STM32这边就没再接,通信也很顺利。
3. 实操过程与核心环节实现
3.1 工程搭建:STM32CubeMX配置CAN外设
如果你还在用寄存器方式撸CAN,工作量会很大。现在STM32CubeMX已经把CAN外设的初始化封装得很好了,重点配几个参数就行。我以STM32F103系列为例,这类芯片内置的是bxCAN外设,一个CAN节点,两个可用的FIFO。
CubeMX里的关键配置项如下:
- 波特率:需要通过Time Quanta和Bit Segment配置。常用的500kbps,在36MHz APB1时钟下,分频为4,同步段1Tq,传播段1Tq,相位段1Tq、2Tq,就能凑出500kbps。
- 工作模式:Normal,如果要先调试可用Loopback模式,不需要真实总线就能自发自收。
- 自动重传:建议开启,在总线上有偶发错误时可以自动重新发送,对雷达这种周期性上报场景更友好。
- FIFO接收中断:勾选FIFO0消息挂起中断,这样有报文到达时触发中断回调,不占用主循环轮询资源。
具体到FDCAN——比如STM32G4、H7系列上的FDCAN外设——配置逻辑类似,但要额外设置仲裁段波特率和数据段波特率。如果雷达还处于CAN 2.0模式,建议把FDCAN的协议模式改成经典CAN模式,或者把仲裁波特率和数据波特率设成一致,否则可能收不到数据或大量错误帧。
初始化代码生成之后,还要手动打开CAN中断的NVIC通道。这个在很多CubeMX版本里默认不帮你勾选,漏了的话中断永远不会触发。我习惯在生成工程后先检查一下HAL_CAN_Start和HAL_CAN_ActivateNotification这两个函数是否在main里被调用了,因为默认生成的代码有时候只初始化外设,并没有真正启动接收。
3.2 接收中断与环形缓冲:不丢帧的接收方案
CAN报文来的时候是突发性的,尤其是多目标雷达,一个周期内可能连续发好几条报文。如果每条报文都当场解析、当场处理,主程序压力会很大。我的做法是只把原始报文塞进一个环形缓冲区,然后由主循环取出来做解析和业务处理。
中断回调里做的事情很少:
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rxHeader; uint8_t rxData[8]; HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rxHeader, rxData); ring_buffer_write(&can_rb, rxHeader.IDE, rxHeader.StdId, rxData); }注意,回调函数里尽量不要做耗时操作,比如浮点运算、打印、内存分配,这些全放到主循环外。我见过有人直接在中断回调里做速度换算,结果波特率一高就偶发性丢帧,原因就是中断处理时间太长,下一个报文到来时FIFO溢出。
环形缓冲区不需要多复杂,我一般用结构体数组加两个读写指针就够了。容量设为64或128条报文,按雷达10Hz到30Hz的发送频率,完全够用。如果项目里还有别的中断在跑,可以给读写指针加临界区保护,STM32上用临界区或暂停中断即可,成本很低。
3.3 数据解析实战:从原始字节到目标信息
这节直接用代码演示怎么解析。假设我手里的雷达协议定义:ID为0x502的报文,Byte0~1是纵向距离,单位0.1m,大端模式;Byte2~3是横向距离,单位0.1m;Byte4是纵向速度,有符号,单位0.25m/s;Byte5~6是横向速度,有符号,单位0.25m/s;Byte7是目标状态。
对应的解析代码可以这样写:
typedef struct { float long_distance_m; float lat_distance_m; float long_velocity_ms; float lat_velocity_ms; uint8_t target_status; } RadarTarget_t; void parse_target_msg(uint32_t std_id, uint8_t *data, RadarTarget_t *target) { if (std_id == 0x502) { /* 纵向距离:Byte0-1,大端,0.1m单位 */ uint16_t raw_long_dist = (data[0] << 8) | data[1]; target->long_distance_m = raw_long_dist * 0.1f; /* 横向距离:Byte2-3,大端,0.1m单位 */ uint16_t raw_lat_dist = (data[2] << 8) | data[3]; target->lat_distance_m = raw_lat_dist * 0.1f; /* 纵向速度:Byte4,有符号,0.25m/s单位 */ int8_t raw_long_vel = (int8_t)data[4]; target->long_velocity_ms = raw_long_vel * 0.25f; /* 横向速度:Byte5-6,有符号,0.25m/s单位,先拼成16位再转有符号 */ int16_t raw_lat_vel = (int16_t)((data[5] << 8) | data[6]); target->lat_velocity_ms = raw_lat_vel * 0.25f; target->target_status = data[7]; } }这里有一个写代码时容易忽略的点:带符号字段转换时,要么用类型明确的int8_t/int16_t强转,要么自己手动处理符号扩展。如果你写成uint8_t去接收再强制转float,负数就会被当成大于127的正数来处理,速度方向瞬间变成反的,甚至数值大得离谱。
我还会在解析函数里做一层有效性判空,因为CAN报文并不保证每次都是8字节满数据,有些短报文DLC只有4字节,直接越界取data[7]会读到脏数据。稳妥的做法是先看rxHeader.DLC的值,再决定解析哪些字节,不足的字段直接清零。
实测下来,这类解析代码放在主循环里跑,对STM32F103这种72MHz主频的芯片来说毫无压力。真正有性能压力的是解析之后的点位计算、跟踪滤波、UI刷新,这些逻辑另算,不属于本文讨论范围。
4. 常见问题与排查技巧实录
4.1 收不到数据?先按这个顺序检查
这是被问得最多的问题。CAN通信收不到数据,原因往往不是代码,而是硬件或物理层配置。我建议按照下面的顺序排查,每一步都做确认,不要跳过。
第一,确认波特率是否完全一致。雷达和STM32的CAN波特率哪怕差1%,都会导致持续错误帧。用示波器看CANH和CANL的波特率最直观,没有示波器就试着多设几个常见值:125k、250k、500k、1M。注意有些雷达出厂默认不是500k,而是250k,我就是靠逐个尝试发现某款雷达实际工作在250k。
第二,用Loopback模式验证STM32侧的CAN收发链路。把CAN外设设为Loopback,自发自收,如果能收到自己发的报文,说明MCU和收发器这一侧基本没问题,问题大概率在雷达端、线缆或接线。
第三,检查CAN_H和CAN_L是否接反。这个低级错误相当常见。CAN总线是差分信号,两根线拧在一起传输,一旦接反,接收端看到的差分信号会完全反转,报文头都找不到。如果拆开线缆发现红色和绿色对接,那就要认真看定义了。
第四,检查报文ID是否被接收滤波器屏蔽了。STM32的bxCAN默认滤波器如果配置不当,会丢掉所有报文。我在调试初期干脆先把滤波器设为接收全部,等能收到数据了,再按雷达的报文ID范围优化滤波器。CubeMX里默认生成的滤波器配置有时候是屏蔽模式,需要仔细检查FilterID和FilterMask的值,别让它把0x502这种ID给过滤掉了。
4.2 解析出来的数据像乱码?字节序和缩放因子是重点
如果你能收到报文,但解析出来的数值完全不符合预期,比如距离是负数、速度超过100m/s、角度在0~360度来回跳,那八成不是通信问题,而是解析逻辑和协议不匹配。
先把字节序查清楚。同样是0x502报文的两个字节,如果雷达定义是大端,你的代码却按小端解析,得到的数值是大错特错的。这时候有个闷头查的办法:把收到的原始字节直接打印出来,和雷达配套工具或上位机显示的十六进制数据对比。如果原始字节一致,那就是解析逻辑的问题;如果原始字节都不一样,那通信链路还有问题。
再检查缩放因子。CAN报文的字段基本都是整数编码,比如距离字段数值248,不代表距离是248米,而是2480.1=24.8米。忽略缩放因子是新手最容易犯的错。另外,有些协议不仅用缩放因子,还带有偏移量,比如温度字段是“原始值0.1-40”,这种别漏了偏移。
符号字段的处理也值得单独提一下。协议文档里如果写“速度字段为有符号数”,你就在转换时用int8_t/int16_t,别用uint。负速度在CAN数据里可能是0xFE,如果你按无符号解析成254,然后乘以0.25得到63.5m/s,显然是不会得到正确结果的。我习惯在做任何物理量之前,先把数据转成有符号整数,再统一乘缩放因子,这样逻辑清晰,也方便以后移植。
4.3 多目标跟踪场景下的工程化建议
我去年接的一个项目里,雷达最多同时输出64个目标,每秒30帧,意味着每秒要处理1920条目标报文。这时候如果每一条报文都做浮点运算、写日志、推送给上层算法,单片机性能就会吃紧。分享几个工程化建议。
第一个建议是“只提取你关心的目标”。毫米波雷达输出的目标列表里面,很多目标其实是静止的护栏、路灯、墙面,对你的避障或跟踪逻辑没用。可以在解析完成后,先按距离、速度阈值做粗过滤,只把符合条件的目标放行到上层。我这边的经验是,过滤掉速度接近于0且距离大于某阈值的静止目标,能显著降低上层算法的干扰。
第二个建议是给目标ID建立小型的跟踪表。雷达每次上报的目标ID是同一目标短时间内保持不变的,但长时间可能会漂移。你可以用一个数组记录{目标ID, 最后更新时间},在每帧中断或主循环里把超过一定时间没更新的目标标记为丢失。这样比每次从头到尾扫描全部目标要高效得多。
第三个建议是关于DMA和CAN外设的联动。有很多开发板支持从CAN控制器直接通过DMA搬运数据到内存,确实能降低CPU开销,但代价是代码复杂度上升。对于一般项目,中断加环形缓冲已经够用了,只有在目标数量特别大且还要做复杂决策的时候,才建议上DMA甚至换用CAN FD加多FIFO的方案。
我在实际调试中还发现一个小经验:很多人只盯着CAN数据接收,却忽略了雷达本身有诊断报文和周期状态报文。有些雷达协议要求主机周期发送一条心跳报文,雷达才会持续输出目标数据;如果收不到心跳,雷达会主动进入低功耗或者静默模式。所以遇到“雷达明明上电了,但总线上一点报文都没有”的情况,不妨先查一下雷达是否处于需要主机激活的状态。这种“需要主动拉一把”的设计不太常见,但我在某款工业雷达上真实遇到过,折腾了快半天才搞明白。
还有一个小技巧,调试时别急着上高波特率。先用125k或者250k打通链路,确认收发正常后再按协议设定提高波特率,能减少很多排查成本。我自己每次新接一款雷达,都会做一个最小验证程序:主循环每500ms发一帧带递增计数的报文,CANalyzer或者另一个STM32的调试板能收到就算打通了“MCU->总线”方向;再反向验证“总线->MCU”。两个方向都通了,才会继续做解析,这个习惯帮我省了大量时间。
结尾
写到这里,核心的内容都讲完了。如果你已经按前面几节的步骤把毫米波雷达的CAN数据在STM32上跑通了,那恭喜你,整套流程里最基础也最关键的一环算是拿下了。我个人在实际操作中的体会是,CAN数据解析本身并不难,难的是对协议细节的耐心和一条靠谱的排查思路。遇到数据不对的时候,别急着改代码,先把原始字节打出来和上位机对比,往往能迅速定位问题。另外,如果后续想把这套能力扩展到CAN FD、多雷达组网、甚至和ROS2对接,思路和今天讲的完全一致,只是把数据帧的定义换一换而已。希望这篇分享能帮你少踩几个我踩过的坑,也欢迎你在真正动手之后总结出自己的经验,再回来互相补充。
本文还有配套的精品资源,点击获取