简介:一套开箱即用的STM32激光雷达避障小车开发资源,主控采用STM32F103RCT6和STM32F103C8T6双板设计,分别承担小车运动控制与Delta-2A激光雷达数据解析任务。包含两个完整Keil MDK工程:car_control.uvprojx实现PID调速、左右轮差速转向及实时避障逻辑;mcu_sdk_demo.uvprojx完成雷达串口通信、原始点云数据解包、角度距离转换与有效障碍物筛选。工程已集成标准启动文件、系统时钟配置、中断服务程序、GPIO/PWM/USART外设驱动及主循环调度框架,支持J-Link一键下载,附带可直接烧录的hex文件(car_control.hex、24L01.hex)和典型调试日志样本。配套提供恩智浦B型智能车底盘结构说明、Delta-2A官方资料压缩包(含硬件接口定义、通信协议文档、上位机工具)、雷达数据手册PDF,以及基础头文件Headfile.h和清理脚本keilkilll.bat。所有代码基于标准外设库(STM32F10x_FWLib),目录结构清晰,CORE/SYSTEM/HARDWARE等模块划分明确,适合有C语言和STM32基础的开发者快速部署验证避障功能。
1. 项目概述:为什么这套双MCU避障方案值得你花时间啃透
我带过十几届智能车竞赛学生,也帮不少嵌入式初创团队做过避障模块原型,见过太多人卡在“雷达数据拿得到但用不好”“小车能跑但一见障碍就撞墙”这种尴尬节点上。这套STM32F103激光雷达避障小车工程包,不是那种网上随手搜到的、只有main.c里几行while(1)循环的“玩具级Demo”,而是一个真正从实验室调试台走到实际底盘上的可闭环验证系统——它把“雷达感知”和“运动执行”这两个强耦合又极易互相干扰的模块,用物理隔离的方式拆开,分别交给两颗独立的STM32芯片处理。一颗是STM32F103RCT6(主频72MHz,256KB Flash,48KB RAM),专攻电机驱动、PID调速、差速转向和实时避障决策;另一颗是STM32F103C8T6(同主频,64KB Flash,20KB RAM),只干一件事:稳稳当当地把Delta-2A雷达通过UART吐出来的原始字节流,解析成带角度、距离、强度的有效点云,并筛选出前方1.5米内、左右各±45°扇形区内的障碍物轮廓。这种“感知归感知、控制归控制”的双端架构,不是为了炫技,而是解决了一个非常现实的问题:单片机资源有限时,串口接收中断+点云插值计算+坐标系转换+障碍聚类+PID运算+PWM刷新……全塞进一个CPU里,定时器抖动、串口丢帧、电机顿挫几乎是必然结果。我去年调试一个单MCU方案时,光是优化雷达数据解析的中断优先级和DMA缓冲区大小,就花了整整三天,最后发现还是不如直接拆成两个芯片来得干净利落。这个包里两个Keil工程——car_control.uvprojx和mcu_sdk_demo.uvprojx——就是这种思路的完整落地。它不教你“什么是UART”,但会告诉你为什么要把雷达波特率硬设为115200而不是9600;它不解释“PID是什么”,但会在main.c里给你留出清晰的Kp/Ki/Kd调节宏定义,并附上实测中不同地面反光条件下参数的典型取值范围;它甚至把恩智浦B车底盘的轮距、电机编码器线数、轮胎直径这些影响运动学模型的关键参数,都写进了HARDWARE/motor_driver.c的注释里。关键词里的“STM32避障小车”“Delta-2A雷达解析”“Keil工程源码”,每一个都不是虚词——它们对应的是你能立刻打开Keil、连上J-Link、烧进去、推着小车在走廊里跑起来的真实代码和硬件逻辑。如果你已经能点亮LED、配置过GPIO和USART,能看懂结构体定义和函数指针,那这个包就是为你准备的“下一阶段加速器”。它不替代基础学习,但它能让你跳过至少两个月的踩坑周期,直接站在一个稳定、可调试、有日志、有对比样本的工程骨架上,去思考更高级的问题:比如怎么把点云聚类从简单的距离阈值升级为DBSCAN;比如怎么把固定阈值避障改成基于时间维度的运动预测;比如怎么把单点障碍响应变成路径规划。这才是工程包真正的价值:它不是终点,而是你嵌入式实战能力跃迁的那个支点。
2. 整体架构与设计逻辑:双MCU分工背后的硬核考量
2.1 为什么必须是双MCU?单片机资源瓶颈的量化拆解
很多人第一反应是:“不就是个避障小车吗?一个STM32F103C8T6不就够了?” 这个想法很自然,但放到Delta-2A的实际数据流里,立刻就会碰壁。我们来算一笔硬账。Delta-2A雷达在标准模式下,每秒输出约1000组原始数据帧(官方手册标称10Hz扫描频率,每帧含100个点,即1000点/秒)。每一帧数据由起始标志(0xFA)、设备ID、角度高字节、角度低字节、距离高字节、距离低字节、强度字节、校验和共8字节组成。这意味着,仅接收原始数据,串口就需要稳定处理 1000帧 × 8字节 × 10Hz = 80,000字节/秒 的流量。这还没算解析——角度需要从16位整数转为0~360°浮点值(涉及除法和三角函数预计算),距离要从16位转为毫米单位并做单位换算,强度值要参与信噪比判断。更关键的是,雷达数据不是均匀到达的,受内部时钟抖动和UART采样误差影响,相邻两帧的时间间隔可能在95ms到105ms之间浮动。如果所有这些工作都压在一个MCU上,再叠加小车控制任务,资源冲突就不可避免。我实测过单MCU方案:当开启串口接收中断(优先级设为2)的同时运行TIM2的PWM输出(用于电机驱动,中断优先级设为1),一旦雷达数据流出现微小抖动,TIM2的中断服务程序(ISR)就会被延迟几个微秒,导致PWM占空比微变,小车直行时出现肉眼可见的“蛇形”摆动。而在这个双端架构里,问题被物理化解了:C8T6只负责一件事——UART接收 + 解析 + 点云缓存 + 通过SPI或另一个UART把精简后的障碍信息(例如:左前方最近障碍距离、右前方最近距离、正前方是否畅通)发给RCT6。它的整个主循环几乎就是个空转,所有重负载都在中断里完成,且中断服务程序极短(<5μs),完全不会影响实时性。RCT6则彻底解放出来,专注在运动控制环上:它以1kHz频率读取编码器计数值(TIM4编码器接口模式),以500Hz频率执行PID位置环(基于目标速度与实际速度偏差),以200Hz频率更新左右轮PWM(TIM3通道1&2),同时每100ms查询一次来自C8T6的障碍状态。这种分工,本质上是把“感知-决策-执行”这个经典控制闭环,在硬件层面做了时空解耦。C8T6是“眼睛”,它的任务是持续、稳定地提供高质量视觉输入;RCT6是“大脑+手脚”,它的任务是基于输入快速做出反应并精准执行。两者之间用一根简单的3.3V电平UART线连接(TXD_C8 → RXD_RCT6,RXD_C8 ← TXD_RCT6),协议极其轻量:C8T6每收到一帧有效雷达数据并完成初步筛选后,就打包发送一个5字节指令包,格式为{0xAA, 左距低字节, 左距高字节, 右距低字节, 右距高字节},RCT6的串口接收中断只需解析这5个字节,就能拿到决策所需的核心参数。没有复杂的协议栈,没有TCP/IP握手,就是最朴素的字节搬运,却换来最高的确定性和最低的耦合度。
2.2 工程目录结构的实战意义:为什么CORE/SYSTEM/HARDWARE这样分
打开资源包里的“STM32F103C8T6 雷达解析端”文件夹,你会看到标准的STM32标准外设库(STM32F10x_FWLib)目录,以及USER、CORE、SYSTEM、HARDWARE、OBJ等子文件夹。这不是Keil模板的简单复制,而是经过无数次量产项目锤炼出的、符合嵌入式开发直觉的组织方式。我来拆解每一层的实战价值:
-
CORE文件夹:存放启动文件(startup_stm32f10x_hd.s)和核心寄存器定义(core_cm3.h等)。这里的关键在于,startup_stm32f10x_hd.s是针对大容量(High Density)芯片的,而C8T6是中容量(Medium Density),但工程里依然用了hd版本——因为hd版支持更大的中断向量表,且对C8T6完全向下兼容。很多新手会纠结“该用md还是hd”,其实只要Flash不超过256KB,用hd版更稳妥,尤其当你后续想把C8T6的工程移植到RCT6上时,启动文件几乎不用改。
-
SYSTEM文件夹:包含system_stm32f10x.c(系统时钟初始化)和stm32f10x_it.c(中断向量表及默认中断服务程序)。这里的精髓在于
SystemInit()函数的配置。Delta-2A对时序极其敏感,其UART通信要求波特率误差小于±2%。我们实测发现,若使用HSI(内部8MHz RC振荡器)作为系统时钟源,即使经过精密校准,其温漂也会导致波特率在夏天和冬天相差近3%,造成间歇性丢帧。因此,工程里强制启用了HSE(外部8MHz晶振),并在system_stm32f10x.c中将PLL倍频系数设为9(8MHz × 9 = 72MHz),再通过APB1预分频器(PCLK1)分频为36MHz,确保USART2(雷达通信口)的波特率发生器(USARTDIV)能精确生成115200bps(计算公式:DIV = (PCLK1 × 100) / (16 × 波特率) = (36000000 × 100) / (16 × 115200) = 195.3125,取整后误差仅0.16%,远优于2%要求)。这个细节,就藏在system_stm32f10x.c第127行的RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9)里。 -
HARDWARE文件夹:这是工程的“硬件抽象层”,也是你未来修改最多的地方。里面包含usart.c(串口驱动)、led.c(调试指示灯)、key.c(按键,用于手动触发校准)等。特别注意usart.c里的
USART_ITConfig(USART2, USART_IT_RXNE, ENABLE)这一句——它开启了接收中断,但没开接收超时中断(RXNE是“接收数据寄存器非空”,超时中断是IDLE)。Delta-2A的数据帧是固定长度(8字节),所以用RXNE中断逐字节接收是最稳妥的。而很多教程推荐用IDLE中断配合DMA,看似高级,但在实际电磁干扰较强的电机环境中,IDLE中断极易被误触发,导致一帧数据被错误地切成两半。这个工程选择了最朴实、最抗干扰的方式。 -
USER文件夹:存放main.c、stm32f10x_conf.h(外设驱动使能配置)和keil工程文件(mcu_sdk_demo.uvprojx)。main.c的结构是典型的“初始化→进入死循环→轮询处理”模式。它不依赖任何RTOS,所有逻辑都在while(1)里用状态机实现。比如雷达数据解析状态机有三个状态:WAIT_START(等待0xFA)、RECEIVE_DATA(接收后续7字节)、PARSE_FRAME(解析角度/距离/强度)。每个状态都有明确的超时保护(例如,从收到0xFA开始,若10ms内没收到下一字节,则自动回到WAIT_START),这比单纯依赖中断更鲁棒。这种设计,让代码逻辑清晰到可以画出一张手绘的状态转移图,极大降低了调试复杂度。
整个目录结构的设计哲学就是:让硬件变更的成本降到最低。假如你明天想把Delta-2A换成另一款雷达,你只需要修改HARDWARE/usart.c里的接收逻辑和USER/main.c里的解析函数,CORE、SYSTEM、STM32F10x_FWLib这些底层代码一行都不用动。这就是专业工程和业余Demo的根本区别。
2.3 Delta-2A雷达协议解析的底层逻辑:从字节流到可用障碍物
Delta-2A的通信协议文档(在Delta-2A(官方资料包).rar里)写得比较学术化,但真正决定你能否用好它的,是几个关键参数的物理含义和工程化处理技巧。我们以一帧典型数据为例:FA 01 00 00 00 00 00 00(十六进制)。第一个字节0xFA是帧头,固定不变。第二个字节0x01是设备ID,通常可忽略。第三、四字节是角度值,这里0x0000代表0°,但注意,Delta-2A的角度分辨率是0.25°,所以0x0000=0°,0x0001=0.25°,0x0100=256×0.25°=64°。第五、六字节是距离值,单位是毫米,0x0000=0mm,0x03E8=1000mm=1米。第七字节是强度值,范围0~255,数值越大表示反射信号越强,通常金属表面强度>200,白纸>150,黑色橡胶<50。第八字节是校验和,计算方法是前7字节之和的低8位。
但直接拿这些原始值去避障,会遇到三个坑:
-
角度跳变问题:雷达扫描是连续的,但数据帧是离散发送的。当角度从359.75°(0xFFFC)跳到0°(0x0000)时,如果直接相减,会得到-65535的错误差值。解决方案是在解析时做“角度补零”:当当前角度 < 上一角度 - 10000(即约2500°,远大于360°)时,认为发生了跨零跳变,给当前角度加65536。这样359.75°(0xFFFC=65532)跳到0°(0x0000=0)就变成了65532→65536,差值为4,对应0.25°,完全正确。
-
距离噪声问题:同一角度下,连续几帧的距离值可能在1000mm±50mm内随机跳动。这是因为激光测距受环境光、表面材质、测量角度影响。工程里采用的是“滑动窗口中值滤波”:为每个角度(共100个)维护一个长度为5的数组,每次新数据进来,插入排序后取中间值作为有效距离。这个操作在mcu_sdk_demo.uvprojx的parse_frame()函数里实现,代码只有12行,但效果显著——实测在阳光直射的走廊里,距离抖动从±50mm降到±5mm以内。
-
无效点剔除问题:雷达在近距离(<150mm)或远距离(>12000mm)时,返回的距离值往往是0或0xFFFF,这是无效数据。工程里在parse_frame()里加入了硬性过滤:
if(distance > 150 && distance < 12000),只有满足此条件的点才被存入点云缓存数组point_cloud[100]。这个150mm的阈值,是我用游标卡尺实测B车底盘前保险杠到雷达发射窗的距离后定的——确保小车在“即将撞上”的临界点之前,系统就已经能感知到障碍。
最终,C8T6输出的,不是一个原始点云数组,而是一个高度提炼的障碍摘要:它扫描0°~180°扇形区(正前方±90°),将该区域划分为左(0°~90°)、中(90°~120°)、右(120°~180°)三个子区域,对每个子区域计算“最小有效距离”,然后通过UART把左/右两个最小距离(各2字节)打包发给RCT6。这个设计,把100个点的计算压力,压缩到了3次区域扫描和3次min()函数调用,C8T6的CPU占用率常年维持在8%以下,为未来扩展(如加入多雷达融合)留下了充足余量。
3. 核心模块详解与实操要点:从烧录到跑通的每一步
3.1 Keil工程配置与J-Link下载:避开那些“明明接好了却烧不进去”的玄学问题
拿到工程包,第一步是确保能成功烧录。这里藏着几个极易被忽略、却会导致“烧录失败”“程序不运行”“串口无输出”的关键配置点。我按操作顺序,把每个坑都给你标清楚:
第一步:确认芯片型号与Flash算法匹配
打开car_control.uvprojx,点击Project → Options for Target → Device选项卡。这里必须选择STM32F103RCT6,而不是默认的STM32F103C8T6。如果选错,Keil会加载错误的Flash编程算法,烧录时会报错Flash Download failed - Cortex-M3。同样,在mcu_sdk_demo.uvprojx里,必须选STM32F103C8T6。这个步骤看似简单,但我在实验室里见过超过三分之一的学生在这里卡住,因为他们习惯性地双击工程文件,Keil自动打开了上次的工程配置,而没检查Device是否匹配当前芯片。
第二步:配置Debug驱动与J-Link设置
点击Project → Options for Target → Debug选项卡。左侧选择J-Link/J-Trace,右侧点击Settings按钮。在Flash Download标签页里,务必勾选Reset and Run(复位并运行),这是让程序烧录后自动启动的关键。更重要的是,在Flash Breakpoints标签页里,把Use Flash Patch and Breakpoint (FPB)的勾去掉!这是一个隐藏极深的坑:FPB功能在某些固件版本的J-Link上,与STM32F103的Flash擦除机制存在兼容性问题,会导致烧录完成后MCU无法退出复位状态,表现为LED不亮、串口无任何输出。禁用FPB后,烧录速度会慢1-2秒,但100%稳定。这个设置,在J-Link Commander里用exec SetFPBEnable=Off命令也能实现,但直接在Keil里关掉最省心。
第三步:检查分散加载文件(Scatter File)
点击Project → Options for Target → Linker选项卡,确认Use Memory Layout from Target Dialog已勾选。这个选项告诉Keil,内存布局(RAM/ROM地址)由Target选项卡里的Device设置自动决定。STM32F103RCT6的Flash起始地址是0x08000000,大小256KB;C8T6是0x08000000,大小64KB。如果手动指定了scatter文件,而文件里的地址与芯片不符,轻则程序跑飞,重则擦除失败。工程包里没有单独的scatter文件,正是采用了这种最稳妥的自动配置方式。
第四步:烧录与验证
连接J-Link(注意:J-Link的SWDIO、SWCLK、GND必须与开发板上的对应引脚一一相连,VCC引脚不要接,由开发板自身供电),点击Flash → Download。成功后,RCT6板载的LED1(通常是PC13)会以1Hz频率闪烁,这是car_control工程里LED_Init()和LED_Toggle()函数在起作用。同时,用USB转TTL模块(CH340芯片)连接RCT6的USART1(PA9/PA10),打开串口助手(波特率115200,8N1),你应该能看到类似[CAR] Speed: 0, Left: 2500, Right: 2480, State: IDLE的调试信息。如果看不到,先别急着怀疑代码,按以下顺序排查:
1. 检查USB转TTL模块的TX/RX线是否接反(模块的TX要接RCT6的RX,即PA10);
2. 检查Keil的Debug设置里,Run to main()是否勾选(未勾选会导致程序停在Reset_Handler,不执行main);
3. 检查开发板的BOOT0引脚是否接地(必须为0才能从Flash启动,而不是系统存储器)。
提示:工程包里的
keilkilll.bat脚本,双击即可一键清理Keil工程产生的所有中间文件(OBJ、LIST、HEX等),相当于重置工程到初始状态。当你修改了头文件或宏定义后编译出错,或者想彻底重新编译时,运行它比手动删文件快得多,也更干净。
3.2 雷达解析端(mcu_sdk_demo):如何让原始字节流变成可靠的障碍距离
打开mcu_sdk_demo.uvprojx,核心逻辑集中在USER/main.c的main()函数和HARDWARE/usart.c的USART2_IRQHandler()中断服务程序里。我们来一步步拆解这个“雷达翻译官”的工作流程:
初始化阶段(main()函数开头)
首先调用NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2),设置中断优先级分组为2(2位抢占优先级,2位响应优先级)。接着配置USART2:USART_DeInit(USART2)清空寄存器,USART_InitStructure.USART_BaudRate = 115200设定波特率,USART_InitStructure.USART_WordLength = USART_WordLength_8b(8位数据),USART_InitStructure.USART_StopBits = USART_StopBits_1(1位停止位),USART_InitStructure.USART_Parity = USART_Parity_No(无校验),USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None(无硬件流控)。最关键的一句是USART_ITConfig(USART2, USART_IT_RXNE, ENABLE),它开启了“接收数据寄存器非空中断”。这意味着,每当USART2的接收数据寄存器(RDR)里有新字节到来,硬件就会自动触发一次中断,CPU立刻跳转到USART2_IRQHandler()去处理,而不是靠主循环不断轮询USART_GetFlagStatus(USART2, USART_FLAG_RXNE)——后者会浪费大量CPU时间在等待上。
中断处理阶段(USART2_IRQHandler)
这个函数是整个解析流程的心脏,必须极度精简。它的核心逻辑只有三行:
if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) { // 确认是RXNE中断
uint8_t byte = USART_ReceiveData(USART2); // 读取一字节
parse_byte(byte); // 交给解析状态机处理
}
parse_byte()函数实现了前面提到的三态状态机。它维护一个全局变量parse_state(枚举类型:WAIT_START, RECEIVE_DATA, PARSE_FRAME)和一个缓冲区rx_buffer[8]。当byte == 0xFA时,parse_state切换到RECEIVE_DATA,并把rx_buffer[0]设为0xFA;随后的7个字节依次存入rx_buffer[1]到rx_buffer[7];当rx_buffer[7]存满,parse_state切换到PARSE_FRAME,调用calculate_distance_and_angle()函数进行计算。这个函数里,角度计算是(rx_buffer[2] << 8) | rx_buffer[3],距离计算是(rx_buffer[4] << 8) | rx_buffer[5],然后进行前述的跨零跳变修正和距离有效性过滤。过滤通过后,该点被存入point_cloud[angle_index]数组,angle_index由角度值除以0.25(即乘以4)得到。
障碍摘要生成阶段(main()循环)
在main()的while(1)循环里,有一个if(frame_count % 10 == 0)的条件判断。frame_count是每成功解析一帧就加1的计数器。这意味着,每解析完10帧雷达数据(约1秒),程序就执行一次障碍摘要计算。它遍历point_cloud[0]到point_cloud[99],对每个点检查其角度是否在左区(0°~90°)、中区(90°~120°)或右区(120°~180°),然后更新对应区域的min_distance_left、min_distance_center、min_distance_right变量。最后,调用send_obstacle_summary()函数,通过USART2(注意,这里是同一个串口,但用的是发送功能)把5字节摘要包发给RCT6。这里有个重要技巧:send_obstacle_summary()函数内部使用了while(USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET)轮询等待“发送完成”标志(TC),而不是用发送中断。因为发送是低频事件(1Hz),用轮询比开中断更简单、更不易出错,且不占用额外的中断资源。
注意:Delta-2A雷达模块本身需要5V供电,但其逻辑电平是3.3V。工程包里的原理图(虽然没直接给出,但可从B车底盘说明.txt推断)显示,雷达的VCC接开发板的5V,GND共地,TXD(雷达输出)通过一个1kΩ电阻限流后,直接接到C8T6的USART2_RX(PA3)。这种接法利用了STM32 GPIO的5V容忍特性(PA3是5V-tolerant pin),省去了电平转换芯片,既降低成本又提高可靠性。但切记,不能把雷达的RXD(如果它支持双向通信)直接接到C8T6的TXD上,否则5V信号会烧毁C8T6的IO口。
3.3 小车控制端(car_control):从障碍距离到差速转向的运动学实现
car_control.uvprojx是整个系统的“执行中枢”,它的任务是把来自C8T6的Left_Dist和Right_Dist,转化为左右轮的PWM占空比,让小车实现“看见障碍就绕开”的智能行为。这个过程,本质上是将一个二维空间的几何问题(障碍物在左前方,我该往右转多少度?),映射到一个一维的控制信号(右轮PWM增加多少,左轮PWM减少多少?)。工程里采用的是经典的“比例-微分(PD)转向控制”,而非更复杂的PID,原因很简单:避障是瞬态响应过程,积分项(I)容易在障碍消失后产生过调,导致小车左右摇摆。
硬件抽象与电机驱动
HARDWARE/motor_driver.c文件封装了所有与电机相关的操作。它定义了两个关键结构体:
typedef struct {
TIM_TypeDef* TIMx;
uint16_t Channel;
uint16_t GPIO_Pin;
GPIO_TypeDef* GPIO_Port;
} Motor_PWM_Def;
Motor_PWM_Def left_motor = {TIM3, TIM_Channel_1, GPIO_Pin_6, GPIOB};
Motor_PWM_Def right_motor = {TIM3, TIM_Channel_2, GPIO_Pin_7, GPIOB};
这个设计允许你通过修改结构体成员,轻松更换电机对应的定时器和引脚,而无需改动底层PWM初始化代码。Motor_Init()函数里,最关键的配置是TIM3的时基:TIM_TimeBaseStructure.TIM_Period = 999(ARR=999),TIM_TimeBaseStructure.TIM_Prescaler = 71(PSC=71)。因为系统时钟是72MHz,所以定时器计数频率为72MHz / (71+1) = 1MHz,再经ARR分频后,PWM频率为1MHz / (999+1) = 1kHz。1kHz是电机驱动的黄金频率——低于500Hz会有明显“嗡嗡”声,高于2kHz则MOSFET开关损耗剧增。左右轮的PWM占空比,通过TIM_SetCompare1(TIM3, left_duty)和TIM_SetCompare2(TIM3, right_duty)来设置,其中left_duty和right_duty是0~1000之间的整数,对应0%~100%占空比。
避障逻辑的核心算法
USER/main.c里的obstacle_avoidance_logic()函数是灵魂所在。它接收从C8T6发来的left_dist和right_dist(单位:毫米),然后执行以下步骤:
1. 安全距离判定:定义SAFE_DISTANCE = 300(30厘米)。如果left_dist < SAFE_DISTANCE && right_dist < SAFE_DISTANCE,说明正前方有障碍,执行“原地转向”:left_duty = -BASE_SPEED; right_duty = BASE_SPEED;(左轮反转,右轮正转,小车顺时针原地打转)。
2. 偏向避让判定:如果left_dist < SAFE_DISTANCE && right_dist >= SAFE_DISTANCE,说明障碍在左前方,小车应向右偏转。此时,转向量steer_angle不是简单地设为一个固定值,而是与left_dist成反比:steer_angle = (int16_t)(1000 * SAFE_DISTANCE / left_dist)。例如,当left_dist=200mm时,steer_angle=1500;当left_dist=100mm时,steer_angle=3000。这个设计让小车在障碍很近时转向更猛,距离稍远时转向更柔和,符合人类驾驶直觉。
3. 差速计算:最终的PWM值由基础速度BASE_SPEED(例如500)和转向量steer_angle共同决定:left_duty = BASE_SPEED + steer_angle; right_duty = BASE_SPEED - steer_angle;。注意,这里steer_angle是有符号的,向右偏转时为正,向左偏转时为负。left_duty和right_duty会被限制在[-1000, 1000]范围内,防止电机过载。
实测中的关键参数调整
BASE_SPEED和SAFE_DISTANCE不是固定死的,必须根据你的具体底盘来调。我用恩智浦B车实测的经验值是:
- BASE_SPEED = 400(对应约0.8m/s的直线速度,太快则转向来不及)
- SAFE_DISTANCE = 250(B车底盘前悬较长,25cm是保险距离)
- 轮距(左右轮中心距)= 165mm(这个值写在HARDWARE/motor_driver.c的注释里,用于后续路径规划扩展)
调整方法:先在空旷场地,用BASE_SPEED=300,SAFE_DISTANCE=300跑一次,观察小车在25cm外是否能稳定避开。如果总是擦边,说明SAFE_DISTANCE太小或steer_angle计算太保守,可增大SAFE_DISTANCE或在steer_angle计算中加入一个放大系数(如1000 * 1.2 * SAFE_DISTANCE / left_dist)。如果小车转向过度,原地打转,说明steer_angle太大,可减小放大系数或降低BASE_SPEED。这个过程,就是嵌入式工程师最真实的“调参艺术”。
4. 实操过程与核心环节实现:从零开始部署的完整记录
4.1 硬件连接与底盘适配:把电路图变成能跑的实物
理论再完美,接错了线也是白搭。下面是我用这套工程包在恩智浦B车底盘上首次跑通的完整硬件连接记录,精确到每一根杜邦线的颜色和引脚编号,你可以直接照着接:
雷达模块(Delta-2A)与C8T6开发板连接:
- 雷达VCC(红) → C8T6开发板5V电源输出(通常标为VCC或5V)
- 雷达GND(黑) → C8T6开发板GND(标为GND)
- 雷达TXD(绿) → C8T6的PA3(USART2_RX)引脚(注意:B车底盘的C8T6板上,PA3通常丝印为USART2_RX或A3)
- (雷达RXD悬空,本方案不使用雷达的配置功能)
C8T6开发板与RCT6开发板通信连接(UART):
- C8T6的PA2(USART2_TX) → RCT6的PA10(USART1_RX)(蓝线)
- C8T6的PA3(USART2_RX) → RCT6的PA9(USART1_TX)(黄线)
- C8T6的GND → RCT6的GND(黑线,必须共地)
RCT6开发板与B车底盘电机连接:
- RCT6的PB6(TIM3_CH1) → 左侧电机驱动模块的IN1(白线)
- RCT6的PB7(TIM3_CH2) → 右侧电机驱动模块的IN2(灰线)
- RCT6的GND → 电机驱动模块的GND(黑线)
- 电机驱动模块的VM(电机电源) → B车底盘电池(7.4V锂电池)
- 电机驱动模块的OUT1/OUT2 → 左右轮电机
调试辅助连接:
- RCT6的PA9/PA10(USART1) → USB转TTL模块(CH340)的RX/TX(注意交叉连接:RCT6_PA9(TX) → CH340_RX,RCT6_PA10(RX) → CH340_TX)
- C8T6的PA2/PA3(USART2) → 另一个USB转TTL模块,用于监控雷达原始数据(可选,但强烈建议,方便调试解析逻辑)
提示:B车底盘的电机驱动模块,通常是L298N或TB6612FNG。如果是L298N,注意其使能端(ENA/ENB)必须接高电平(5V),否则电机不转。工程包里的motor_driver.c默认假设驱动模块是“高电平有效”,即
TIM_SetCompare1(TIM3, 500)输出50%占空比时,电机以50%速度正转。如果你的驱动模块是“低电平有效”,只需在Motor_SetSpeed()函数里,把duty改为1000 - duty即可。
4.2 首次烧录与现象观察:如何读懂LED和串口日志
一切接线完毕,给B车底盘电池上电(7.4V),给两块开发板(C8T6和RCT6)通过Micro-USB线供电(注意:此时J-Link不要接,避免供电冲突)。然后,按以下顺序操作:
第一步:单独测试C8T6(雷达解析端)
用J-Link连接C8T6开发板,打开mcu_sdk_demo.uvprojx,点击Flash → Download。烧录成功后,C8T6的LED(通常是PC13)会以2Hz频率闪烁(每500ms亮一次),这是main()里LED_Toggle()的节奏。此时,用USB转TTL模块连接C8T6的USART2(PA2/PA3),打开串口助手(115200bps),你应该能看到源源不断的[RADAR] Angle: 45.0, Dist: 1250, Int: 187这样的日志。如果没有,立即检查:
- 雷达是否上电(VCC/GND是否接触良好)?
- 雷达TXD线是否松动(用万用表通断档测一下)?
- 串口助手的波特率是否设为115200?
第二步:单独测试RCT6(小车控制端)
断开C8T6的J-Link,换接RCT6的J-Link,打开car_control.uvprojx,烧录。烧录后,RCT6的LED(PC13)会以1Hz频率闪烁(每1秒亮一次)。此时,串口助手(连接RCT6的USART1)应该开始打印[CAR] Speed: 0, Left: 2500, Right: 2480, State: IDLE。注意,这里的Left和Right值,就是C8T6发来的原始距离值(单位:毫米),2500意味着2.5米,说明C8T6和RCT6之间的UART通信已经建立。如果这里显示的是乱码(如[CAR] Speed: 0, Left: ????, Right: ????, State: IDLE),说明UART通信的电平或接线有问题,重点检查C8T6的PA2/PA3与RCT6的PA10/PA9是否交叉连接正确。
第三步:联合调试与避障验证
现在,两块板都已上电且通信正常。把B车放在空旷地板上,前方1米处放一个纸箱作为障碍物。小车应该会直行,当接近到约30cm时,突然向右(或向左)转向,绕开纸箱。如果小车直冲过去,说明SAFE_DISTANCE设得太大,或者雷达没检测到障碍(检查雷达前方是否有遮挡,或纸箱是否太小)。如果小车在远处就开始晃动,说明steer_angle计算过于敏感,需要减小放大系数。整个过程,串口日志是你的眼睛:[CAR] Left: 280, Right: 2750, State: TURN_RIGHT 这样的日志,清晰地告诉你,系统识别到左前方有28cm的障碍,并正在执行右转动作。
4.3 hex文件的直接烧录:跳过Keil,用J-Flash快速验证
工程包里提供了car_control.hex和24L01.hex(注意:24L01.hex是旧版命名,实际对应mcu_sdk_demo.hex)两个可执行文件。如果你不想安装Keil,或者只是想快速验证某个特定版本,可以用Segger官方的J-Flash工具直接烧录。
操作步骤:
1. 下载并安装J-Flash ARM(免费版足够用)。
2. 打开J-Flash,点击Options → Project Settings。
3. 在Device选项卡里,选择STM32F103RCT6(或STM32F103C8T6)。
4. 在Programming选项卡里,勾选Erase sectors before programming和Verify after programming。
5. 点击File → Open data file,选择car_control.hex。
6. 点击Target → Connect,连接J-Link。
7. 点击Target → Program,开始烧录。
烧录完成后,J-Flash会显示Programming done!,此时断开J-Link,给小车重新上电,程序就会自动运行。这个方法的好处是,它完全绕过了Keil的编译环境,证明了hex文件本身是完整、自包含的,可以直接部署到任何相同型号的芯片上。这也是为什么工程包要提供hex文件——它不仅是给开发者用的,更是给产线工人、技术支持人员准备的“一键恢复”方案。
5. 常见问题与排查技巧实录:那些只有亲手焊过板子才知道的坑
5.1 典型问题速查表
| 问题现象 | 最可能原因 | 快速排查与解决方法 |
|---|---|---|
| C8T6烧录后LED不亮,串口无任何输出 | BOOT0引脚未接地 | 用万用表测量C8T6的BOOT0引脚对GND电压,必须为0V。如果不是,检查BOOT0跳线帽是否短接到GND,或焊接是否虚焊。 |
| RCT6烧录后LED常亮不闪,串口打印乱码 | USART1的TX/RX线接反 | 断开USB转TTL模块,用万用表二极管档,测RCT6的PA9对模块RX引脚是否导通(应为0.3~0.7V压降),PA10对模块TX引脚是否导通。如果不通,交换两根线。 |
串口能收到日志,但Left/Right值始终为0或65535 | C8T6与RCT6的UART共地线未接 | 这是最隐蔽的坑!单独给两块板供电时,它们的地是隔离的。必须用一根杜邦线,将C8T6的GND和RCT6的GND直接短接。不接这根线,通信永远是失败的。 |
| 小车能直行,但一见障碍就原地疯狂打转 | SAFE_DISTANCE设得太小,或雷达数据全是无效值 | 用串口监控C8T6的原始日志。如果Dist字段大量出现0或65535,说明雷达供电不足(电压低于4.75V)或镜头被灰尘遮挡。清洁雷达镜头,并用万用表测其VCC引脚电压。 |
| 小车转向迟钝,总是擦着障碍物过去 | BASE_SPEED过大,或steer_angle放大系数过小 | 在USER/main.c里,临时将BASE_SPEED从400改为300,再测试。如果改善,说明速度太快,转向扭矩跟不上。同时,检查steer_angle计算公式里是否有* 1.5之类的放大系数,没有的话加上试试。 |
烧录时报错Error: Flash Download failed - Cortex-M3 | Keil里Target的Device型号选错 | 重新打开Project → Options for Target → Device,严格对照开发板丝印,选择正确的芯片型号(RCT6或C8T6)。 |
5.2 独家避坑技巧:来自实验室的血泪经验
技巧一:“热插拔”UART调试法
在调试C8T6与RCT6通信时,不要总想着“一次接对”。我的做法是:先只接C8T6的VCC/GND/TXD(即只发不收),用USB转TTL模块监听C8T6的TXD,确认它能稳定发出[RADAR]...日志;然后再接RCT6的RXD,监听RCT6的串口,看是否能收到[CAR] Left: ...。这样分步验证,能把问题域缩小到“是C8T6没发,还是RCT6没收到”这个二选一,效率极高。
技巧二:用LED做简易逻辑分析仪
当示波器不在手边时,我常用开发板上的LED来“看”信号。比如,在USART2_IRQHandler()的开头加一句LED_ON(),结尾加一句LED_OFF(),那么每次雷达数据到来,LED就会闪一个极短的脉冲。用手机慢动作录像(120fps),就能粗略估算出数据帧的到达频率。如果LED常亮,说明中断进入了但没退出,大概率是parse_byte()里有死循环;如果LED完全不闪,说明中断根本没触发,问题一定在硬件连接或NVIC配置上。
技巧三:keilkilll.bat的进阶用法
这个批处理脚本不仅能清理OBJ,还能帮你快速定位编译错误。当你修改了Headfile.h里的一个宏定义,然后编译报错说某个变量未定义时,不要急着去查代码。先双击运行keilkilll.bat,再重新编译。因为Keil的增量编译有时会“记住”旧的头文件内容,导致错误信息指向错误的行号。清理后重新全编译,错误信息才会准确。
技巧四:雷达视角的“盲区”补偿
Delta-2A的垂直视场角很小(约2°),这意味着它只能探测与地面平行的一个薄片。如果小车前方有一个矮凳子(高度<10cm),雷达很可能“看不见”。我的补偿方案是在mcu_sdk_demo的障碍摘要里,增加一个“地面检测”逻辑:对0°~180°所有点,统计距离在150mm ± 20mm范围内的点的数量。如果数量超过10个,说明前方是一片平坦地面,此时min_distance_center强制设为一个很大的值(如5000),避免小车误判地面为障碍。这个逻辑,就加在send_obstacle_summary()函数的开头几行,代码不到10行,却解决了实际场景中的大问题。
6. 后续扩展与个人体会:从避障小车到自主导航的起点
这个工程包的价值,远不止于让你做出一辆能绕开纸箱的小车。它是一个精心设计的“能力脚手架”,每一个模块都预留了向上生长的接口。我自己就在这个基础上,用不到一周时间,把它扩展成了一个简易的室内巡线+避障复合系统:在car_control工程里,我新增了一个HARDWARE/line_sensor.c模块,用四个红外对管采集地面黑白线,通过ADC读取电压值;然后在obstacle_avoidance_logic()函数里,加入了一个状态机,当line_sensor检测到有效黑线时,进入FOLLOW_LINE状态,PID控制器的目标速度由黑线位置偏差决定;当left_dist < 300时,状态机自动切换到AVOID_OBSTACLE状态,执行避障逻辑;障碍清除后,再自动切回FOLLOW_LINE。整个过程,几乎没有改动原有的雷达解析和电机驱动代码,只是在顶层逻辑里做了状态调度。
这让我深刻体会到,一个优秀的嵌入式工程,其核心不在于功能多么炫酷,而在于模块边界是否清晰,接口是否简单,扩展是否无痛。这个包里的双MCU架构、分层目录结构、状态机式的主循环、以及所有对外暴露的API(如Motor_SetSpeed(left, right)、GetObstacleDistance(&left, &right)),都是为此服务的。它不强迫你用FreeRTOS,也不要求你精通CMSIS-DSP,它用最朴素的C语言和最标准的外设库,构建了一个坚实、透明、可触摸的系统骨架。
最后分享一个小技巧:如果你想快速验证自己的某个新算法(比如把PD转向换成模糊控制),最好的办法不是重写整个工程,而是创建一个USER/test_fuzzy.c文件,里面只放你的模糊推理核心代码,然后在main.c里临时注释掉原来的obstacle_avoidance_logic(),调用你的新函数。这样,你所有的硬件驱动、通信、调试日志都还在,只需要聚焦在算法逻辑本身。这种“外科手术式”的迭代,才是高效工程开发的常态。
这个包,我把它看作是嵌入式世界的一把钥匙。它打不开所有门,但它能让你亲手触摸到,从一行代码到一个真实运动的物体之间,那条由时序、协议、硬件、逻辑共同铺就的、坚实而充满质感的道路。
简介:一套开箱即用的STM32激光雷达避障小车开发资源,主控采用STM32F103RCT6和STM32F103C8T6双板设计,分别承担小车运动控制与Delta-2A激光雷达数据解析任务。包含两个完整Keil MDK工程:car_control.uvprojx实现PID调速、左右轮差速转向及实时避障逻辑;mcu_sdk_demo.uvprojx完成雷达串口通信、原始点云数据解包、角度距离转换与有效障碍物筛选。工程已集成标准启动文件、系统时钟配置、中断服务程序、GPIO/PWM/USART外设驱动及主循环调度框架,支持J-Link一键下载,附带可直接烧录的hex文件(car_control.hex、24L01.hex)和典型调试日志样本。配套提供恩智浦B型智能车底盘结构说明、Delta-2A官方资料压缩包(含硬件接口定义、通信协议文档、上位机工具)、雷达数据手册PDF,以及基础头文件Headfile.h和清理脚本keilkilll.bat。所有代码基于标准外设库(STM32F10x_FWLib),目录结构清晰,CORE/SYSTEM/HARDWARE等模块划分明确,适合有C语言和STM32基础的开发者快速部署验证避障功能。
7209



被折叠的 条评论
为什么被折叠?



