做了一版基于STM32与R60ABD1毫米波雷达的非接触式睡眠监护系统,这几天实测下来整体效果比较满意。这个项目解决的痛点是传统的睡眠监测要么靠手环贴在皮肤上,要么靠摄像头,老人戴着不舒服,摄像头又有隐私顾虑。用毫米波雷达做非接触式监护,人只要躺在床上,模块就能感知到人体存在、呼吸频率和心率变化,不需要任何穿戴设备。对于做嵌入式、物联网或者毕业设计的朋友来说,这个方案可玩性很高,成本不高,代码也不复杂,而且数据链路非常清晰,从雷达串口到STM32解析再到OLED显示和告警,是一个典型的完整实战项目。这篇博文就把我从选型、硬件搭建、协议解析到联调排错的整个开发过程写清楚,特别是那些常规文档里不会写的坑。
1. 项目整体设计与思路拆解
1.1 系统能做什么
非接触式睡眠监护说白了就是人躺在床上,设备不说话,通过电磁波来感知“床上有没有人、这个人睡没睡着、呼吸节律正不正常”。我最终做出来的功能主要有四个:
- 人体存在检测:判断床上是否有人,能分辨“没人”“有人不动”“有人翻身”三种情况;
- 呼吸与心率估算:通过60GHz雷达信号处理后的串口输出,直接读模块算好的数值;
- 睡眠过程记录:把整晚的数据存到SD卡,或者通过ESP8266上传,方便第二天回看曲线;
- 异常告警:设置心率/呼吸上下限,持续超限一段时间就触发蜂鸣器提醒,方便家属或护工处理。
这里要先把预期管理做好:R60ABD1这个模块本身已经做了大量信号处理,输出的是结构化的生命体征数据,不是让你自己从零去做FFT和微波算法。对初学者来说,这个项目的难度就从“微波信号处理”降到了“串口数据解析+业务逻辑设计”,大部分挑战集中在协议解析、可靠性判断和场景适配。换句话说,就算你不懂雷达原理,只要会看数据手册、会写串口收发,一样能把这套系统做出来。
1.2 器件选型背后的考量
先说为什么用STM32。这个芯片在嵌入式领域的生态太成熟了,资料多、教程多、报错一搜就有答案。从项目本身来看,雷达模块通过串口输出数据,主控只需要做解析、显示、告警、存储这几个活,用STM32F103C8T6这颗十几块钱的芯片就完全够用,没必要上Linux级别的处理器。而且STM32的功耗不高,后续想改成电池供电的床头设备也比较现实。我用的是大家常说的“蓝丸”最小系统板,开发环境用Keil5,配合HAL库和CubeMX,工程搭建很快。
再说为什么选R60ABD1毫米波雷达,而不是市面上常见的5.8GHz人体感应模块或热释电探头。5.8GHz只能告诉你“这里有人”,热释电只对运动敏感,人睡着之后身体基本不动,它就判断成“没人”了,这俩都做不了睡眠监护。R60ABD1工作在60GHz频段,波长只有5毫米左右,对胸廓起伏、心跳引起的皮肤微动非常敏感,所以它能做到非接触式的生命体征感知。这也是整个项目最核心的选型原因:不是随便一个雷达都能测呼吸和心率的。
1.3 整体架构与数据流
整个系统的数据流划分非常清晰,我在设计阶段就按链路拆好了模块,后面开发基本没走弯路。
| 环节 | 器件/位置 | 作用 |
|---|---|---|
| 数据采集 | R60ABD1雷达模块 | 采集回波信号,计算人体状态、距离、呼吸率、心率 |
| 主控处理 | STM32F103C8T6 | 串口读取数据,解析协议帧,做状态判断和逻辑控制 |
| 人机交互 | OLED、按键、蜂鸣器 | 显示实时数据,响应设置,触发告警 |
| 记录/扩展 | SD卡模块或ESP8266 | 存储整晚数据,上报到服务器 |
数据链路是:毫米波雷达 -> TTL串口 -> STM32串口DMA接收 -> 帧解析 -> 呼吸心率与状态提取 -> OLED显示/SD卡存储/蜂鸣器告警。这个链路看起来简单,但每一步都有细节。比如测试时把雷达直接接到电脑USB串口上,数据一切正常,一旦换到STM32上就出现“一开雷达板子就重启”的电源问题,这类问题放到后面专门讲。
2. 硬件准备与电路搭建
2.1 核心器件清单
我先列一份自己实际用到的清单,照着买就行。
- STM32F103C8T6最小系统板(带USB转串口更方便)
- R60ABD1毫米波雷达模块(60GHz,TTL串口输出)
- 0.96寸OLED屏幕(I2C接口,SSD1306驱动)
- 有源蜂鸣器模块
- AMS1117-3.3稳压模块或更好的LDO
- 杜邦线、面包板(调试用)
- CH340或CP2102的TTL转USB模块(必须备一个,排查问题要用)
有条件的建议直接画一块PCB,把雷达和主板分开布局,后期做产品会顺手很多。我第一版是面包板搭的,雷达用手拿着靠近床测试,数据虽然能跑,但线一多就容易接触不良,后面换了PCB才算真正进入可靠测试阶段。
2.2 电源与电平匹配
电源问题是这个项目里最容易翻车的地方。大多数60GHz雷达模块的供电电压是5V,逻辑电平一般是3.3V,和STM32的串口直接连接问题不大。但模块运行时的电流能到一两百毫安,启动瞬间还会更高,如果直接用STM32核心板上的3.3V引脚给它供电,很容易造成电压跌落,表现就是雷达初始化失败、串口乱码或者板子反复重启。
我最终的做法是给雷达单独供电:5V输入先进一颗AMS1117-3.3,按模块峰值电流留两倍余量,再给雷达供电。主控和雷达共地,但电源走线分开。同时,串口连接上,雷达的TX接STM32的PA10(USART1_RX),STM32的PA9(USART1_TX)接雷达的RX,GND必须接在一起。不共地的话,大概率第一帧就是乱码。
另外,很多模块支持通过串口指令修改参数,比如检测距离门限、灵敏度、上报周期。我习惯把上报周期调到1秒,距离门限调到2米,这样床外有人走动时不会被误判进睡眠区域。这些参数在你手里的数据手册里一般都会有说明,拿到模块先花半小时把手册里的寄存器表和指令表过一遍,比盲目写代码高效得多。
2.3 雷达安装位置与朝向
雷达安装位置直接影响数据质量,这个也是被很多人忽略的点。我在床头柜、床侧、床正上方三个位置都试过,效果差异非常明显。
最好的方案是装在床正上方的天花板或支架上,雷达正面朝下,垂直照射人体胸腹部。如果只能放床头,就让雷达朝斜下方对着人的上半身,尽量避免正对窗户、空调、风扇、窗帘这些容易产生多普勒干扰的物体。60GHz雷达虽然波束不算特别宽,但床边空调的出风、窗帘飘动都会造成存在状态的误判,这个问题等到真实睡眠测试时会暴露得淋漓尽致。
3. R60ABD1毫米波雷达:原理与数据解读
3.1 为什么毫米波能测呼吸和心率
要理解这个项目,得先搞明白雷达怎么“看到”呼吸。FMCW雷达会连续发射频率变化的信号,并和回波信号做混频,得到包含目标距离信息的差频信号。对于静止的人来说,胸部和皮肤会随呼吸、心跳产生毫米量级的起伏,这个起伏会让雷达回波相位发生变化。60GHz雷达的优势在于波长只有5毫米左右,对亚毫米级的体表位移非常敏感,所以能提取出呼吸和心跳引起的微动信号。
R60ABD1模块内部已经完成了这些信号处理,直接输出的是目标状态、距离、呼吸频率和心率等结构化数据。对我们做应用层的人来说,核心任务就是把这些串口数据可靠地读出来、解析出来,再结合睡眠场景做业务判断。理解这一点非常关键,它决定了你把精力花在“调协议”上,而不是傻乎乎去学雷达信号处理。
3.2 串口协议与数据帧解析
拿到模块后第一件事,先看数据手册里的协议说明。R60ABD1这类模块一般用TTL串口输出固定帧,常见格式会有帧头、帧长、功能码、数据段和校验位。我调试时处理的帧结构大致是这样的:
// 帧结构示意:0xAA 0x55 [长度] [功能码] [数据区...] [校验和] // 数据区中通常包含存在状态、距离、呼吸率、心率等字段 typedef struct { uint16_t header; // 帧头 uint8_t length; // 长度 uint8_t cmd; // 功能码 uint8_t data[16]; // 数据段 uint8_t checksum; // 校验 } radar_frame_t;注意,不同批次、不同厂家的模块协议可能不一样。最常见的坑是直接抄网上的代码去解析别人的帧格式,结果字段对不上,呼吸心率永远是0。正确做法是先把模块接到USB串口上,用串口助手看原始HEX数据,再对照手册一个字节一个字节校对,确认帧头和校验方式后再动手写解析代码。我第一次就是因为偷懒套模板,解析出来的呼吸率一直是0,对了一遍原始数据才发现是字段偏移量算错了。
下面这段代码是状态机同步的解析框架,核心思路是帧头对了才继续,校验不对就重新找帧头:
void radar_parse(uint8_t byte) { static uint8_t rx_buf[32]; static uint8_t state = 0; static uint8_t idx = 0; static uint8_t len = 0; switch (state) { case 0: if (byte == 0xAA) state = 1; break; case 1: if (byte == 0x55) { state = 2; idx = 0; } else state = 0; break; case 2: len = byte; state = 3; rx_buf[idx++] = byte; break; case 3: rx_buf[idx++] = byte; if (idx == len + 3) { // 校验和判断 if (check_sum(rx_buf, len + 2) == rx_buf[len + 2]) { handle_frame(&rx_buf[3], len); } idx = 0; state = 0; } break; default: state = 0; break; } }代码本身不复杂,关键是字段提取那一部分必须和你手里的模块对齐。调通之后可以把每个字段的偏移量加上调试日志,用上位机对比验证,确认显示的数据和模块输出一致。
再提一句:模块的上报周期如果是默认的5秒甚至更长,屏幕上的呼吸率会更新得很慢,调试时很容易误以为板子卡死了。我习惯一上来就把上报周期改成1秒,后面所有调优都基于这个频率来做。
3.3 关键字段的使用逻辑
解析出来的字段通常包括存在状态、运动状态、距离、呼吸率、心率。存在状态和运动状态我基本直接拿来用,距离字段用来做区域判断。比如设置一个“床铺区域”是距离小于1.5米,只有目标在这个距离内才做睡眠分析,这样能过滤掉走廊有人走动引起的误判。
呼吸率和心率不是每个时刻都可信。模块在“没有人”或者“目标正在大幅运动”时,输出的数值可能就是0,或者保持上一次的值。我通常在业务层做两件事:
- 只在“有人且非剧烈运动”的状态下记录呼吸率和心率;
- 对数值做平滑,取最近15次上报的中位数,而不是拿单帧值去报警。
这样处理后,人翻个身,数据也不会瞬间跳到离谱的数值。等进入睡眠状态后,呼吸率和心率才会被持续更新到OLED和SD卡上。
4. STM32固件开发实战
4.1 开发环境与工程搭建
Keil5环境第一次用的人要确认两件事:芯片包和编译器版本。装了Keil5之后去Pack Installer里勾选STM32F1系列芯片包,不然工程根本建不起来。另外记得在魔术棒选项里把编译器版本选对,AC6和AC5对一些老代码的兼容性不一样,我见过不少同学因为默认AC6导致各种编译报错,换成AC5问题就消失了。
工程模板我用的HAL库加CubeMX,因为初始化代码生成太方便了,串口DMA、定时器、I2C这些外设在CubeMX里点几下就能配好。如果还不会用CubeMX,建议专门花半天时间熟悉一下,这个时间投入非常值得。
4.2 串口DMA接收与空闲中断
雷达数据是一条持续不断的字节流,如果每收到一个字节都进中断做解析,CPU占用高,而且容易丢数据。我改用DMA接收加串口空闲中断,把完整一帧数据搬到缓冲区后再统一解析。CubeMX里配置USART1为115200-8-N-1,开启DMA接收和空闲中断。
// 串口DMA接收初始化示例 __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); HAL_UART_Receive_DMA(&huart1, uart_dma_buf, DMA_BUF_SIZE);在空闲中断回调里判断当前DMA接收到了多少数据,然后把有效数据拷贝到解析缓冲区。这样每一帧数据被完整打包,解析函数不用处理单字节中断,逻辑清爽很多。要注意的是,DMA缓冲区长度必须大于雷达单帧最大长度,否则一帧数据会被拆成两次拷贝,解析时容易判成两条断裂的帧。我用256字节的缓冲区就足够了。
串口回调里千万不要做重活。我见过有人直接在回调里写SD卡、刷OLED,结果整个系统的实时性被拖垮。正确做法是回调里只做标志位和缓冲区切换,真正的解析、显示、存储都放在主循环里处理。
4.3 睡眠状态判定与告警逻辑
我把睡眠状态机简化成四个状态:无人、躺床、睡眠、离床。初始状态是无人,检测到有人存在且距离小于门限后进入“躺床”;如果一段时间内持续检测到呼吸信号但运动状态为静止或微动,就认为进入“睡眠”;当存在状态变成无人持续超过设定时间,判定为“离床”,可以触发提示。
typedef enum { STATE_NOBODY = 0, STATE_INBED, STATE_SLEEPING, STATE_LEFT } slp_state_t;异常告警我采用“连续N次生效”的防抖策略。比如心率小于45或者大于130,连续5次上报(每次1秒)都超限,蜂鸣器才响。直接单次触发的话,翻身和信号波动都会造成误报。另外当状态变成“无人”时,不做心率告警,只做离床提醒,这个逻辑一定要分开处理,不然半夜人起来上厕所,蜂鸣器突然响了,反而把家人吓一跳。
OLED显示我用的SSD1306驱动,屏幕分三行:第一行大字显示呼吸率,第二行显示心率,第三行显示当前状态。刷新频率1秒一次已经足够,频繁刷新会占用I2C总线,拉低系统响应速度。
4.4 数据记录与上报扩展
要记录整晚数据,可以用SPI接口的SD卡模块,每10秒写一行CSV,字段是时间、状态、呼吸率、心率。文件系统用FATFS,注意开机挂载一次,写入用f_open + f_write + f_close,不要频繁挂载,否则可能损坏文件系统。日志文件按照日期命名,方便回看。
如果后续要接手机或平台,可以加一块ESP8266或者ESP32,STM32把JSON字符串通过串口发给WiFi模块,再走MQTT上报到服务器。这里不展开讲,但结构上建议预留一个数据上报函数接口,后面做扩展会省很多事。
5. 系统联调与实测经验
5.1 实测流程
联调的第一天不要急着上真床测试,先用手贴近雷达模拟人体。我当时的做法:让人坐在雷达前静止不动,看输出距离、存在状态是否正常;再用缓慢起伏的手掌模拟呼吸,观察呼吸率数据是否跟着变化。这一步能快速确认解析逻辑和显示链路没问题,再进入真实睡眠测试。
真实测试时把雷达固定在床正上方,高度在0.5米到1米之间,记录一整晚数据。雷达只要上电就会持续输出,我用一个移动电源给整个系统供电,SD卡每10秒写一条数据,第二天早上查看数据曲线。这里有个细节:移动电源要选输出电流足够的,否则雷达启动瞬间会造成系统复位。
5.2 参数调优
调优过程中有几个参数起到决定性作用:
- 上报周期:默认如果是5秒,一定要改成1秒,否则实时性太差;
- 检测距离门限:根据床和雷达的实际距离来,我调成1.5米;
- 状态切换延时:从睡眠切换到离床的时间不能太短,我设置成连续5分钟无人再切换;
- 呼吸心率平滑窗口:取15个点的中位数,可以有效滤掉毛刺,但窗口太大会掩盖真实异常。
这些参数不要硬套,跟你雷达的安装位置和房间环境有关。稳妥的做法是把参数定义成结构体,通过串口指令在运行时调整,省得每次重新烧录固件。我调试时就用XCOM给板子发指令改门限,观察OLED上的数值变化,效率比反复刷固件高多了。
5.3 踩过的坑
这一节写几个印象深刻的坑,帮大家少走弯路。
首先是供电不足导致数据全0。最早用STM32板载3.3V给雷达供电,雷达一启动,OLED就开始乱码,数据全变零。后来用万用表测了电压才发现,雷达启动瞬间把3.3V拉到2.7V以下,LDO根本扛不住。解决方法是给雷达单独一路LDO供电,主控和雷达电源分开。
然后是串口没有共地。有一次我把STM32板子和雷达模块分别接了两个USB口调试,数据一直乱码,查了半天才发现两个设备的地电位不一样。把GND连在一起后马上正常,这个低级错误值得专门提醒。
还有空调风引起的误触发。一开始把雷达放在床侧,正好对着空调出风口,半夜空调一启动,“存在状态”就忽有忽无。后来调整了安装位置,并把检测距离门限调小,这个问题才彻底消失。
最后是距离字段跳动。模块输出的距离偶尔会跳变,比如人在2米处却显示0.3米。我用平滑滤波加距离可信范围判断来解决,超过0到3米范围的字段直接丢弃,状态保持原值,避免因为一个异常帧触发误报警。
6. 常见问题排查速查表
6.1 雷达不出数据
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 串口无任何输出 | 接线错误/未共地 | 用万用表测RX、TX、GND是否连通 |
| 长时间没有帧 | 供电不足 | 万用表测模块供电电压,看启动时是否有明显跌落 |
| 收到乱码 | 波特率不匹配/电平不匹配 | 轮流试9600、115200,并确认逻辑电平兼容 |
如果还是没有数据,把模块直接接到USB转TTL模块上,排除STM32端的问题。如果USB助手有数据而STM32没有,重点检查DMA初始化顺序、空闲中断是否被其他中断抢占,以及CubeMX生成的时钟配置是否正确。
6.2 呼吸率心率跳动太大
这种情况多半不是协议问题,而是场景问题。靠近雷达的窗帘、风扇、被子大幅动作都会干扰信号。先看模块输出的“运动状态”字段,如果它上报的是运动模式,本身就说明目标在大幅活动,此时的呼吸率和心率本来就不准,业务层直接不更新显示即可。
还可以把检测距离门限调小,把床外目标排除。数据平滑别做太重,否则真实异常也被滤掉了,我实测15点中位数已经够用,再大的窗口会影响响应速度,半夜心率突然下降时反而不容易触发报警。
6.3 系统偶尔死机
系统长时间运行后卡死,优先怀疑DMA缓冲区覆盖和SD卡写入。DMA缓冲区和解析缓冲区要分开,解析完立刻释放;SD卡写入时如果文件系统操作耗时过长,中断回调被阻塞也会造成死机。把SD卡写入放到主循环定时器任务里,不要在串口中断里做任何文件系统操作。
另外一个容易被忽略的点是蜂鸣器驱动。如果用阻塞延时去控制蜂鸣器响1秒,这个期间整个主循环被卡住,雷达数据没人处理,缓冲区就会溢出。正确做法是用定时器或者非阻塞延时来控制蜂鸣器时长。
最后再分享一个实用小技巧:调试时给雷达模块加一个1Hz的心跳LED,只要LED还在闪,就说明模块内核还在正常运行,很多“板子死了”的判断其实是误报。睡眠监护这个方向还有很多可扩展的东西,比如结合温湿度传感器做环境舒适度分析,或者把整晚数据通过蓝牙同步到手机App。我在做这套系统时最大的体会是,毫米波雷达把以前只有医疗设备才能做的生命体征采集拉到了消费级和玩家级,剩下的工作更多在于怎么把数据用得足够聪明、足够可靠。希望这篇实战记录能给你提供一条清晰的路。