基于STM32与Rd-03雷达模组实现人体感应灯:嵌入式开发实战

📅 2026/7/31 15:26:11 👁️ 阅读次数 📝 编程学习
基于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相比,它的优势很明显:

  1. 穿透性强:可以穿透亚克力、塑料外壳等非金属材料,方便隐藏式安装,外观更整洁。
  2. 不受环境影响:对温度、光照、气流不敏感,稳定性好。
  3. 提供距离信息:不仅能检测有无,还能知道目标离雷达有多远,这为实现“靠近亮灯”、“区域触发”等复杂逻辑提供了可能。
  4. 输出信息丰富:通过串口输出的是已经处理好的结构化数据包,包含目标状态、距离、能量值等,MCU解析起来非常方便。

STM32微控制器:这里我选用的是最常见的STM32F103C8T6(俗称“蓝莓派”或最小系统板)。选择它是因为:

  • 资源足够:拥有USART串口用于与Rd-03通信,足够的GPIO控制LED,定时器用于实现延时。
  • 生态完善:资料多,社区活跃,用STM32CubeMX工具配置起来非常快。
  • 成本低廉:核心板价格亲民,适合学习和DIY。

当然,你也可以用STM32F0、G0系列,或者ESP32、Arduino等,只要带串口就行。原理是相通的。

2.2 硬件连接清单与原理

你需要准备以下材料:

  1. 安信可Rd-03雷达模组 x1
  2. STM32F103C8T6最小系统板 x1
  3. USB转TTL串口模块(用于调试和供电) x1
  4. LED灯及220Ω限流电阻 x1
  5. 杜邦线若干
  6. 电脑一台(安装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.3VGND都共地。可以用STM32板上的3.3V输出给Rd-03供电,但要注意STM32板载LDO的带载能力。更稳妥的做法是,使用USB转TTL模块的3.3V输出,或者一个独立的3.3V稳压模块,同时给两者供电。

具体的接线表示例:

元件引脚连接至引脚说明
Rd-03VCC电源3.3V务必是3.3V,5V会烧毁模组!
Rd-03GND电源/STM32GND共地
Rd-03TXSTM32PA10(USART1_RX)雷达数据输出
Rd-03RXSTM32PA9(USART1_TX)雷达指令输入
STM32PC13LED正极通过220Ω电阻控制输出
LED负极电源/STM32GND回路
STM323.3V电源3.3V如果独立供电,此线可不接
STM32GND电源GND共地

注意:Rd-03的工作电压是3.3V,且其串口电平也是3.3V TTL,与STM32完全兼容。切勿接入5V!首次上电前,务必再三检查VCC电压。

3. 开发环境搭建与STM32基础工程配置

3.1 工具链准备:STM32CubeMX + Keil MDK

我习惯使用STM32CubeMX进行图形化引脚和时钟配置,然后用Keil MDK-ARM进行代码编写和调试。这是目前STM32开发非常主流和高效的一套组合。

  1. 安装STM32CubeMX:从ST官网下载安装。它集成了HAL库,能自动生成初始化代码。
  2. 安装Keil MDK:并安装对应的STM32F1xx Device Family Pack。
  3. 安装串口调试助手:如SSCOMXCOMPutty,用于观察Rd-03的原始数据输出和调试。

3.2 使用CubeMX配置工程骨架

  1. 新建项目:打开CubeMX,选择STM32F103C8Tx
  2. 系统核心(SYS):在SYS选项卡下,将Debug改为Serial Wire。这样才能用ST-LINK进行调试和下载。
  3. 时钟(RCC):在RCC选项卡下,将High Speed Clock (HSE)选择为Crystal/Ceramic Resonator,为外部8MHz晶振。
  4. 时钟树配置:进入Clock Configuration标签页。通常将HSE输入后,经过PLL倍频,将系统时钟(SYSCLK)设置为72MHz。这是F103的典型最高主频,能提升处理性能。
  5. 串口配置(USART1)
    • Pinout & Configuration标签页,找到USART1
    • 将模式设置为Asynchronous(异步通信)。
    • 参数设置:波特率256000(这是Rd-03出厂默认波特率),字长8 Bits,停止位1 Stop Bit,无校验位。这些参数必须与雷达模组一致,否则收不到正确数据。
    • 使能串口全局中断(NVIC Settings中勾选USART1 global interrupt)。这样当串口收到数据时,CPU会暂停当前任务去处理数据,保证实时性。
  6. GPIO配置(PC13)
    • 找到PC13引脚,将其设置为GPIO_Output
    • System Core->GPIO中,可以设置上电后的默认输出电平为低(LED灭),输出模式为推挽输出,速度低速即可。
  7. 生成工程
    • 进入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会通过串口持续发送数据帧。根据安信可的官方资料,其数据帧格式通常如下(具体需以最新手册为准):

字节序号内容说明
00x53帧头,固定
10x59帧头,固定
20x020x01数据长度(后续有效数据字节数)
30x80命令字(表示这是雷达数据)
4DATA1数据1(例如:目标状态,0x00=无目标,0x01=有目标)
5DATA2数据2(例如:距离信息高字节)
6DATA3数据3(例如:距离信息低字节)
7DATA4数据4(例如:信号能量值)
8SUM校验和(通常为前面所有字节的和,取低8位)

一帧数据共9个字节。我们的任务就是编写程序,从串口数据流中准确地识别并提取出每一帧,然后解析出我们关心的信息——主要是目标状态距离

4.2 编写数据解析状态机

在串口中断中一个字节一个字节地接收,然后拼装成帧,最可靠的方法是使用状态机。我们定义几个状态:

  1. 等待帧头1:寻找0x53
  2. 等待帧头2:找到0x53后,下一个必须是0x59,否则回到状态1。
  3. 等待长度:获取数据长度len
  4. 等待命令字:确认命令字是0x80(数据帧)。
  5. 接收数据:根据len接收后续的数据字节。
  6. 接收校验和:接收最后一个字节作为校验和。
  7. 校验和处理:计算前面所有字节的和,与接收的校验和比较。如果一致,则解析数据;不一致,则丢弃,回到状态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_indexcheck_sum等变量的管理是容易出错的地方,建议画个状态转移图来辅助理解。

5. 主程序逻辑设计与实现

解析出雷达数据后,主程序的任务就变得直观了:根据数据做出决策,控制LED。

5.1 主循环逻辑设计

我们在main函数的while (1)循环中实现主逻辑。思路如下:

  1. 检查radar_data_ready标志位,如果为1,表示有新数据。
  2. 读取radar_data中的目标状态和距离。
  3. 应用判断逻辑:例如,如果检测到目标(target_status == 0x01)且距离小于设定阈值(如200厘米),则点亮LED,并记录最后一次触发的时间。
  4. 如果未检测到目标,则检查自最后一次触发到现在是否超过了设定的延时时间(如10秒)。如果超时,则熄灭LED。
  5. 清除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_cmlight_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,可以通过串口打印调试信息到电脑。

  1. 重定向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库。

  2. 打印关键信息:在状态机或主循环中插入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 功耗优化考虑

如果希望制作电池供电的感应灯,功耗是关键。

  1. STM32进入低功耗模式:当灯熄灭且一段时间内无触发时,可以让STM32进入STOPSLEEP模式。此时,需要通过Rd-03的某个输出引脚(如果支持)或外部中断来唤醒MCU。Rd-03本身也有低功耗模式,需要查阅其手册进行配置。
  2. 降低系统时钟频率:在不需要高性能时,通过CubeMX将系统时钟从72MHz降低到更低频率(如8MHz),能显著降低动态功耗。
  3. 关闭外设时钟:在初始化后,关闭不用的外设(如ADC、SPI等)的时钟。

7. 项目总结与进阶玩法

通过这个项目,我们完成了一个完整的“传感器数据采集 -> MCU解析处理 -> 逻辑判断 -> 控制输出”的嵌入式系统闭环。Rd-03雷达模组让我们跳过了复杂的底层信号处理,而STM32的灵活性和强大生态让我们能轻松实现各种定制逻辑。

我个人在实操中的几点深刻体会:

  1. 数据手册至上:一切操作的基础是Rd-03的官方数据手册或通信协议文档。务必找到最新版本,确认帧格式、波特率、引脚定义。不同批次的模组协议可能有细微差别。
  2. 分阶段验证:不要试图一次性写完所有代码。先让LED闪烁,确保MCU工作;再连接串口打印“Hello World”,确保串口通;然后单独测试Rd-03输出原始数据;最后才写状态机解析逻辑。每一步都稳扎稳打。
  3. 状态机是串口解析的利器:对于不定长、带帧头的数据包,状态机比简单的数组匹配要健壮得多,能有效处理数据流中的错误和干扰。
  4. 延时逻辑防抖:在检测和控制之间加入适当的延时或计数判断,能极大提升产品的用户体验,避免因传感器噪声或人体微小晃动导致的灯光“鬼畜”。

这个简易雷达灯只是一个起点。你可以把它升级为:

  • 智能床头灯:靠近变亮,远离变暗或熄灭。
  • 安防报警器:结合蜂鸣器和无线模块(如ESP8266),在非法入侵时报警并推送消息。
  • 人数计数器:通过分析距离和能量变化,尝试判断进出方向,实现简易人数统计。

硬件编程的魅力就在于,只要掌握了核心的通信和控制方法,剩下的就全凭你的想象力去创造了。希望这篇超详细的教程能帮你打通任督二脉,顺利点亮你的第一盏雷达感应灯。