基于STM32与Rd-03雷达模组实现人体感应灯:嵌入式开发实战
1. 项目概述:从想法到实现
最近在捣鼓一些智能家居的小玩意儿,发现人体感应是个挺有意思的方向。传统的红外传感器(PIR)虽然便宜,但容易受温度、遮挡物影响,而且探测范围是个扇形,有盲区。正好看到安信可新出的Rd-03雷达模组,主打高精度、抗干扰,还能测距,就想着用它搭配手头闲置的STM32开发板,做个反应更灵敏、可玩性更高的雷达感应灯。
这个项目本质上是一个微控制器(MCU)与专用传感器模组的典型结合案例。STM32负责逻辑控制、状态判断和输出驱动,而Rd-03则是一个“黑盒”,专门负责发射微波并解析回波,通过串口把处理好的目标信息(比如有无目标、距离、运动状态)吐给STM32。这样一来,我们就不用去啃复杂的雷达信号处理算法,专注于应用层逻辑就行,非常适合嵌入式入门和快速原型开发。
最终实现的效果是:当Rd-03检测到设定范围内有人体移动时,STM32会收到信号并点亮LED灯(模拟灯具);人离开后,灯延迟一段时间熄灭。你可以轻松地把这个LED替换成继电器,去控制真正的台灯、走廊灯甚至智能插座,实现自动照明。下面,我就把从硬件连接到软件调试的完整过程,以及踩过的坑和心得,详细拆解一遍。
2. 核心硬件选型与连接解析
2.1 为什么是Rd-03和STM32?
安信可Rd-03雷达模组:这是一款基于调频连续波(FMCW)原理的毫米波雷达模组。和PIR相比,它的优势很明显:
- 穿透性强:可以穿透亚克力、塑料外壳等非金属材料,方便隐藏式安装,外观更整洁。
- 不受环境影响:对温度、光照、气流不敏感,稳定性好。
- 提供距离信息:不仅能检测有无,还能知道目标离雷达有多远,这为实现“靠近亮灯”、“区域触发”等复杂逻辑提供了可能。
- 输出信息丰富:通过串口输出的是已经处理好的结构化数据包,包含目标状态、距离、能量值等,MCU解析起来非常方便。
STM32微控制器:这里我选用的是最常见的STM32F103C8T6(俗称“蓝莓派”或最小系统板)。选择它是因为:
- 资源足够:拥有USART串口用于与Rd-03通信,足够的GPIO控制LED,定时器用于实现延时。
- 生态完善:资料多,社区活跃,用STM32CubeMX工具配置起来非常快。
- 成本低廉:核心板价格亲民,适合学习和DIY。
当然,你也可以用STM32F0、G0系列,或者ESP32、Arduino等,只要带串口就行。原理是相通的。
2.2 硬件连接清单与原理
你需要准备以下材料:
- 安信可Rd-03雷达模组 x1
- STM32F103C8T6最小系统板 x1
- USB转TTL串口模块(用于调试和供电) x1
- LED灯及220Ω限流电阻 x1
- 杜邦线若干
- 电脑一台(安装Keil MDK或STM32CubeIDE)
连接关系是项目的物理基础,务必理解:
- Rd-03与STM32:这是数据通道。Rd-03的
TX引脚接STM32的某个串口的RX引脚(如PA10),Rd-03的RX接STM32的TX(如PA9)。这样Rd-03才能把检测数据发送给STM32,STM32也能发送配置指令给Rd-03(如果需要修改参数)。 - STM32与LED:这是执行通道。STM32的一个GPIO口(如
PC13)通过一个220Ω的限流电阻连接到LED的正极,LED的负极接GND。STM32通过控制这个GPIO输出高电平或低电平来点亮或熄灭LED。 - 供电:这是能量通道。Rd-03和STM32的
VCC都接3.3V,GND都共地。可以用STM32板上的3.3V输出给Rd-03供电,但要注意STM32板载LDO的带载能力。更稳妥的做法是,使用USB转TTL模块的3.3V输出,或者一个独立的3.3V稳压模块,同时给两者供电。
具体的接线表示例:
| 元件 | 引脚 | 连接至 | 引脚 | 说明 |
|---|---|---|---|---|
| Rd-03 | VCC | 电源 | 3.3V | 务必是3.3V,5V会烧毁模组! |
| Rd-03 | GND | 电源/STM32 | GND | 共地 |
| Rd-03 | TX | STM32 | PA10(USART1_RX) | 雷达数据输出 |
| Rd-03 | RX | STM32 | PA9(USART1_TX) | 雷达指令输入 |
| STM32 | PC13 | LED正极 | 通过220Ω电阻 | 控制输出 |
| LED | 负极 | 电源/STM32 | GND | 回路 |
| STM32 | 3.3V | 电源 | 3.3V | 如果独立供电,此线可不接 |
| STM32 | GND | 电源 | GND | 共地 |
注意:Rd-03的工作电压是3.3V,且其串口电平也是3.3V TTL,与STM32完全兼容。切勿接入5V!首次上电前,务必再三检查VCC电压。
3. 开发环境搭建与STM32基础工程配置
3.1 工具链准备:STM32CubeMX + Keil MDK
我习惯使用STM32CubeMX进行图形化引脚和时钟配置,然后用Keil MDK-ARM进行代码编写和调试。这是目前STM32开发非常主流和高效的一套组合。
- 安装STM32CubeMX:从ST官网下载安装。它集成了HAL库,能自动生成初始化代码。
- 安装Keil MDK:并安装对应的STM32F1xx Device Family Pack。
- 安装串口调试助手:如
SSCOM、XCOM或Putty,用于观察Rd-03的原始数据输出和调试。
3.2 使用CubeMX配置工程骨架
- 新建项目:打开CubeMX,选择
STM32F103C8Tx。 - 系统核心(SYS):在
SYS选项卡下,将Debug改为Serial Wire。这样才能用ST-LINK进行调试和下载。 - 时钟(RCC):在
RCC选项卡下,将High Speed Clock (HSE)选择为Crystal/Ceramic Resonator,为外部8MHz晶振。 - 时钟树配置:进入
Clock Configuration标签页。通常将HSE输入后,经过PLL倍频,将系统时钟(SYSCLK)设置为72MHz。这是F103的典型最高主频,能提升处理性能。 - 串口配置(USART1):
- 在
Pinout & Configuration标签页,找到USART1。 - 将模式设置为
Asynchronous(异步通信)。 - 参数设置:波特率
256000(这是Rd-03出厂默认波特率),字长8 Bits,停止位1 Stop Bit,无校验位。这些参数必须与雷达模组一致,否则收不到正确数据。 - 使能串口全局中断(NVIC Settings中勾选
USART1 global interrupt)。这样当串口收到数据时,CPU会暂停当前任务去处理数据,保证实时性。
- 在
- GPIO配置(PC13):
- 找到
PC13引脚,将其设置为GPIO_Output。 - 在
System Core->GPIO中,可以设置上电后的默认输出电平为低(LED灭),输出模式为推挽输出,速度低速即可。
- 找到
- 生成工程:
- 进入
Project Manager标签页,设置项目名称和路径。 Toolchain / IDE选择MDK-ARM V5。- 在
Code Generator中,选择Copy all used libraries into the project folder,这样工程更独立。 - 点击
GENERATE CODE,生成Keil工程。
- 进入
3.3 理解生成的代码结构
打开生成的Keil工程,你会看到CubeMX已经为我们做好了所有底层硬件的初始化(main.c中的SystemClock_Config,MX_GPIO_Init,MX_USART1_UART_Init等函数)。我们的主要工作是在/* USER CODE BEGIN */和/* USER CODE END */之间添加应用逻辑。
重点关注的函数是串口中断回调函数HAL_UART_RxCpltCallback。当串口接收完一个字节的数据后,会自动进入这个函数,我们可以在这里把数据存入缓冲区。
4. Rd-03雷达模组数据协议解析与驱动编写
这是整个项目的核心难点,也是区分“简单接线”和“真正理解”的关键。
4.1 Rd-03的数据输出格式
Rd-03会通过串口持续发送数据帧。根据安信可的官方资料,其数据帧格式通常如下(具体需以最新手册为准):
| 字节序号 | 内容 | 说明 |
|---|---|---|
| 0 | 0x53 | 帧头,固定 |
| 1 | 0x59 | 帧头,固定 |
| 2 | 0x02或0x01 | 数据长度(后续有效数据字节数) |
| 3 | 0x80 | 命令字(表示这是雷达数据) |
| 4 | DATA1 | 数据1(例如:目标状态,0x00=无目标,0x01=有目标) |
| 5 | DATA2 | 数据2(例如:距离信息高字节) |
| 6 | DATA3 | 数据3(例如:距离信息低字节) |
| 7 | DATA4 | 数据4(例如:信号能量值) |
| 8 | SUM | 校验和(通常为前面所有字节的和,取低8位) |
一帧数据共9个字节。我们的任务就是编写程序,从串口数据流中准确地识别并提取出每一帧,然后解析出我们关心的信息——主要是目标状态和距离。
4.2 编写数据解析状态机
在串口中断中一个字节一个字节地接收,然后拼装成帧,最可靠的方法是使用状态机。我们定义几个状态:
- 等待帧头1:寻找
0x53。 - 等待帧头2:找到
0x53后,下一个必须是0x59,否则回到状态1。 - 等待长度:获取数据长度
len。 - 等待命令字:确认命令字是
0x80(数据帧)。 - 接收数据:根据
len接收后续的数据字节。 - 接收校验和:接收最后一个字节作为校验和。
- 校验和处理:计算前面所有字节的和,与接收的校验和比较。如果一致,则解析数据;不一致,则丢弃,回到状态1。
我们在main.c的USER CODE区域定义相关变量和函数:
/* USER CODE BEGIN PV */ #define RX_BUF_SIZE 20 uint8_t rx_buf[RX_BUF_SIZE]; // 接收缓冲区 uint8_t rx_index = 0; // 缓冲区索引 uint8_t frame_len = 0; // 当前帧数据长度 uint8_t data_len = 0; // 有效数据长度 uint8_t check_sum = 0; // 计算出的校验和 // 雷达数据结构体 typedef struct { uint8_t target_status; // 目标状态 uint16_t distance; // 距离,单位可能是厘米 uint8_t energy; // 能量值 } RadarData_t; RadarData_t radar_data = {0}; uint8_t radar_data_ready = 0; // 数据就绪标志 /* USER CODE END PV */ /* USER CODE BEGIN PFP */ void Radar_Data_Parse(uint8_t *buf, uint8_t len); /* USER CODE END PFP */然后,在main函数初始化部分,启动串口接收中断:
/* USER CODE BEGIN 2 */ // 启动串口空闲中断(更高效)或接收中断 // 这里使用接收中断示例 HAL_UART_Receive_IT(&huart1, &rx_buf[rx_index], 1); /* USER CODE END 2 */接着,实现串口中断回调函数和解析函数:
/* USER CODE BEGIN 4 */ // 串口接收完成中断回调函数 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { static uint8_t state = 0; // 状态机状态 uint8_t rx_byte = rx_buf[rx_index]; // 刚收到的字节 switch (state) { case 0: // 等待帧头0x53 if (rx_byte == 0x53) { check_sum = rx_byte; // 开始计算校验和 state = 1; } break; case 1: // 等待帧头0x59 if (rx_byte == 0x59) { check_sum += rx_byte; state = 2; } else { state = 0; // 失败,重置 } break; case 2: // 获取数据长度 data_len = rx_byte; check_sum += rx_byte; frame_len = data_len + 5; // 总帧长 = 数据长 + 头(2)+长度(1)+命令(1)+校验(1) rx_index = 0; // 重置缓冲区索引,准备存储数据部分 state = 3; break; case 3: // 等待命令字0x80 if (rx_byte == 0x80) { check_sum += rx_byte; rx_buf[rx_index++] = rx_byte; // 存入命令字 state = 4; } else { state = 0; // 不是数据帧,重置 } break; case 4: // 接收数据部分 rx_buf[rx_index++] = rx_byte; check_sum += rx_byte; // 判断是否接收完所有数据字节(data_len个,不包括命令字) if (rx_index >= (data_len + 1)) { // +1是因为命令字已经存了 state = 5; } break; case 5: // 接收校验和 if (rx_byte == (check_sum & 0xFF)) { // 校验通过 // 调用解析函数 Radar_Data_Parse(rx_buf, data_len + 1); // 传入数据部分(含命令字) radar_data_ready = 1; // 设置标志位 } // 无论校验是否通过,都重置状态机,准备接收下一帧 state = 0; rx_index = 0; check_sum = 0; break; default: state = 0; break; } // 重新启动接收中断,等待下一个字节 if (state != 5) { // 状态5时已重置,下一轮会从0开始 HAL_UART_Receive_IT(&huart1, &rx_buf[rx_index], 1); } else { // 状态5处理完后,需要手动启动一次,因为本轮中断已结束 // 更优做法是在状态5重置后直接调用,这里简化处理 state = 0; rx_index = 0; HAL_UART_Receive_IT(&huart1, &rx_buf[rx_index], 1); } } } // 雷达数据解析函数 void Radar_Data_Parse(uint8_t *buf, uint8_t len) { // buf[0] 是命令字 0x80 radar_data.target_status = buf[1]; // 假设数据1是目标状态 radar_data.distance = (buf[2] << 8) | buf[3]; // 假设数据2、3是距离高低位 radar_data.energy = buf[4]; // 假设数据4是能量值 } /* USER CODE END 4 */实操心得:状态机的编写要特别注意状态重置的时机。在任何一个状态匹配失败时,都应考虑是否要回到初始状态。校验失败后,也要完整重置。
rx_index和check_sum等变量的管理是容易出错的地方,建议画个状态转移图来辅助理解。
5. 主程序逻辑设计与实现
解析出雷达数据后,主程序的任务就变得直观了:根据数据做出决策,控制LED。
5.1 主循环逻辑设计
我们在main函数的while (1)循环中实现主逻辑。思路如下:
- 检查
radar_data_ready标志位,如果为1,表示有新数据。 - 读取
radar_data中的目标状态和距离。 - 应用判断逻辑:例如,如果检测到目标(
target_status == 0x01)且距离小于设定阈值(如200厘米),则点亮LED,并记录最后一次触发的时间。 - 如果未检测到目标,则检查自最后一次触发到现在是否超过了设定的延时时间(如10秒)。如果超时,则熄灭LED。
- 清除
radar_data_ready标志,等待下一帧数据。
这里引入一个“延时熄灭”的功能,可以避免人在探测范围内微小移动时灯光频繁闪烁。
5.2 代码实现与优化
首先,在USER CODE区域定义一些全局变量:
/* USER CODE BEGIN PV */ // ... (之前的雷达数据变量) uint32_t last_detect_time = 0; // 最后一次检测到目标的时间戳 uint32_t light_off_delay = 10000; // 灯熄灭延时,单位毫秒 (10秒) uint8_t light_status = 0; // 灯当前状态,0灭1亮 uint16_t detection_range_cm = 200; // 探测范围阈值,单位厘米 /* USER CODE END PV */然后,在main函数的while (1)循环中添加逻辑:
/* USER CODE BEGIN WHILE */ while (1) { // 1. 处理雷达数据 if (radar_data_ready) { radar_data_ready = 0; // 清除标志 if (radar_data.target_status == 0x01) { // 有目标 if (radar_data.distance < detection_range_cm) { // 在设定范围内检测到人 HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); // 点亮LED light_status = 1; last_detect_time = HAL_GetTick(); // 更新最后一次触发时间 } else { // 有目标但超出范围,可忽略或做其他处理 } } else { // 无目标,暂时不关灯,由下面的延时逻辑处理 } } // 2. 处理延时关灯逻辑 if (light_status == 1) { // 如果灯是亮的,检查是否超时 if ((HAL_GetTick() - last_detect_time) > light_off_delay) { HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); // 熄灭LED light_status = 0; } } // 3. 可以添加其他任务,如按键扫描修改参数等 // ... // 4. 短暂延时,避免CPU空转负载过高 HAL_Delay(10); } /* USER CODE END WHILE */这里使用了HAL_GetTick()函数来获取系统上电后的毫秒数,用于计算时间间隔。HAL_Delay(10)让主循环每10ms运行一次,这个周期对于人体感应来说足够快,同时也降低了CPU占用。
5.3 功能扩展思考
基本的感应灯逻辑已经实现。在此基础上,我们可以很容易地进行扩展:
- 距离分级控制:根据不同的距离范围,控制LED的亮度(PWM调光)或颜色(RGB LED)。
- 存在检测:Rd-03应该能区分微动和静止。可以结合能量值
energy和状态变化率,判断人是否只是移动了一下还是持续存在,实现“真存在感应”。 - 参数配置:通过STM32的另一个串口或USB连接电脑,发送指令来动态修改
detection_range_cm、light_off_delay等参数,甚至配置Rd-03本身的探测灵敏度、最大距离等(需查阅Rd-03的指令集)。 - 多雷达组网:使用多个Rd-03和STM32,或者一个STM32带多个串口,实现更大范围的覆盖或方向判断。
6. 调试、问题排查与优化实录
实际制作过程中,不可能一帆风顺。下面是我在调试这个项目时遇到的一些典型问题及解决方法。
6.1 常见问题速查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 上电后无任何反应,LED不亮 | 1. 电源接错或电压不对 2. STM32未正确下载程序 3. 晶振不起振 | 1. 用万用表测量VCC和GND之间电压是否为3.3V。 2. 检查BOOT0/BOOT1引脚状态,确保在用户闪存启动模式。尝试下载一个最简单的LED闪烁程序测试MCU。 3. 检查晶振电路,更换晶振或负载电容试试。 |
| LED常亮或常灭,不受控制 | 1. GPIO控制逻辑写反(高电平有效/低电平有效) 2. 雷达数据未正确解析,标志位从未置1 | 1. 检查电路,LED是共阳还是共阴接法?修改HAL_GPIO_WritePin的参数。2. 使用串口调试助手,直接连接Rd-03的TX,看是否有数据输出。确认波特率是否为256000。如果有数据,则问题在STM32解析端。 |
| 雷达有数据输出,但STM32解析不到 | 1. 串口波特率、数据位、停止位设置错误 2. 接线错误(RX/TX接反) 3. 中断未正确开启或优先级问题 4. 数据解析状态机有bug | 1.最可能的原因!用CubeMX和代码双重确认USART1的波特率设置为256000。 2. 检查接线:Rd-03 TX -> STM32 RX (PA10)。 3. 在CubeMX中确认NVIC里USART1全局中断已开启。 4.关键步骤:在 HAL_UART_RxCpltCallback函数开头,通过HAL_GPIO_TogglePin翻转一个测试用的GPIO(如另一个LED),看看中断是否真的进来了。然后逐步调试状态机。 |
| 感应不灵敏或误触发 | 1. 雷达安装位置不当(正对金属、玻璃或风扇) 2. 探测阈值设置不合理 3. 延时逻辑有误,导致状态抖动 | 1. 将雷达模块正面朝向开阔区域,远离大型金属物体和旋转扇叶。 2. 调整 detection_range_cm,或尝试解析energy值,设置一个能量阈值来过滤噪声。3. 增加“去抖动”逻辑:例如,连续3帧数据都显示有目标才认为有效触发。 |
| 灯熄灭延时不准 | 1.HAL_GetTick()溢出问题(约49天溢出一次,本项目可忽略)2. 主循环阻塞,导致时间判断滞后 | 1. 对于长时间延时,比较时间差时应使用(uint32_t)(HAL_GetTick() - last_detect_time)来避免溢出计算错误。2. 确保主循环中 HAL_Delay时间不长,且没有其他长时间阻塞的操作。复杂任务应使用状态机拆分或RTOS。 |
6.2 深度调试技巧:串口打印日志
当程序行为不符合预期时,最有效的调试手段是输出内部状态。对于STM32,可以通过串口打印调试信息到电脑。
重定向
printf:在main.c中添加以下代码,将printf输出重定向到USART1(需在CubeMX中开启串口)。/* USER CODE BEGIN 0 */ #include <stdio.h> #ifdef __GNUC__ #define PUTCHAR_PROTOTYPE int __io_putchar(int ch) #else #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f) #endif PUTCHAR_PROTOTYPE { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY); return ch; } /* USER CODE END 0 */在魔术棒 -> Target中,勾选
Use MicroLIB以使用精简版C库。打印关键信息:在状态机或主循环中插入
printf。// 在数据解析成功后打印 if(radar_data_ready) { printf("Status:%d, Dist:%dcm, Energy:%d\r\n", radar_data.target_status, radar_data.distance, radar_data.energy); radar_data_ready = 0; } // 在控制LED时打印 printf("Light turned %s\r\n", light_status? "ON":"OFF");打开串口调试助手,选择对应的COM口,波特率设置为115200(这是
printf常用的波特率,与雷达的256000不同,需要用STM32的另一个串口或之后改回来),就能看到实时的调试信息,对分析问题有极大帮助。
避坑指南:调试完成后,务必记得移除或禁用大量的
printf语句。因为格式化输出非常耗时,会严重拖慢主循环,影响雷达数据的实时处理,可能导致感应迟钝。正式发布时,可以定义一个调试宏来开关。
6.3 功耗优化考虑
如果希望制作电池供电的感应灯,功耗是关键。
- STM32进入低功耗模式:当灯熄灭且一段时间内无触发时,可以让STM32进入
STOP或SLEEP模式。此时,需要通过Rd-03的某个输出引脚(如果支持)或外部中断来唤醒MCU。Rd-03本身也有低功耗模式,需要查阅其手册进行配置。 - 降低系统时钟频率:在不需要高性能时,通过CubeMX将系统时钟从72MHz降低到更低频率(如8MHz),能显著降低动态功耗。
- 关闭外设时钟:在初始化后,关闭不用的外设(如ADC、SPI等)的时钟。
7. 项目总结与进阶玩法
通过这个项目,我们完成了一个完整的“传感器数据采集 -> MCU解析处理 -> 逻辑判断 -> 控制输出”的嵌入式系统闭环。Rd-03雷达模组让我们跳过了复杂的底层信号处理,而STM32的灵活性和强大生态让我们能轻松实现各种定制逻辑。
我个人在实操中的几点深刻体会:
- 数据手册至上:一切操作的基础是Rd-03的官方数据手册或通信协议文档。务必找到最新版本,确认帧格式、波特率、引脚定义。不同批次的模组协议可能有细微差别。
- 分阶段验证:不要试图一次性写完所有代码。先让LED闪烁,确保MCU工作;再连接串口打印“Hello World”,确保串口通;然后单独测试Rd-03输出原始数据;最后才写状态机解析逻辑。每一步都稳扎稳打。
- 状态机是串口解析的利器:对于不定长、带帧头的数据包,状态机比简单的数组匹配要健壮得多,能有效处理数据流中的错误和干扰。
- 延时逻辑防抖:在检测和控制之间加入适当的延时或计数判断,能极大提升产品的用户体验,避免因传感器噪声或人体微小晃动导致的灯光“鬼畜”。
这个简易雷达灯只是一个起点。你可以把它升级为:
- 智能床头灯:靠近变亮,远离变暗或熄灭。
- 安防报警器:结合蜂鸣器和无线模块(如ESP8266),在非法入侵时报警并推送消息。
- 人数计数器:通过分析距离和能量变化,尝试判断进出方向,实现简易人数统计。
硬件编程的魅力就在于,只要掌握了核心的通信和控制方法,剩下的就全凭你的想象力去创造了。希望这篇超详细的教程能帮你打通任督二脉,顺利点亮你的第一盏雷达感应灯。
