简介:基于51单片机的脉搏呼吸监测报警设计资料,融合仿真电路图、单片机程序与模拟信号处理原理图,面向电子工程、自动化等专业的课程设计与毕业设计场景,也可作为单片机爱好者学习传感器信号处理的参考案例。系统实现了脉搏和呼吸频率的实时检测,通过LCD1602液晶屏显示测量值,利用独立按键设置上下限报警阈值,超过范围即触发声光报警,覆盖了从数据采集、数据处理到显示交互的完整流程。压缩包内共46个文件,体积约2.43MB,主要包含仿真工程文件、C语言源程序、AD原理图文档、可烧录的hex文件、元件清单、多张流程框图以及PDF版原理图预览,结构划分清晰,便于按模块查阅和复用。目前已有78人学习下载,除了可直接运行的仿真与代码外,还提供了呼吸流程图、脉搏流程图、按键流程图等设计文档,能帮助使用者快速理解各功能模块的逻辑关系。对于需要完成类似人体生理参数监测项目或希望掌握51单片机外设应用的开发者,这套资料具有较高的参考价值,可明显缩短系统原型搭建与调试的时间。
1. 拆一个 51 核的脉搏呼吸报警项目,先搞清楚它到底在测什么
医疗电子入门里最常见的选题,不是心电,就是脉搏加呼吸。这个项目把两者合到了一起,做了个带上下限报警的监测仪,跑在 51 单片机上,用 Proteus 仿真验证,附带 AD 原理图。它的典型应用场景是病房或家庭护理:传感器给出脉冲信号,单片机数脉冲算频率,LCD1602 显示实时值,超限就驱动蜂鸣器和 LED 报警。为什么这个东西到现在还有参考价值?因为它的技术骨架——外部中断计数、定时器闸门、按键设置阈值、LCD 驱动——正好覆盖了从 51 单片机过渡到 ARM 之前必须掌握的裸机外设用法。你要是在做 51 单片机课程设计、protues 仿真入门,或者想验证自己写的中断嵌套和按键消抖逻辑,这个包是能直接跑起来看效果的。它的完整度在于:仿真图、源程序、.hex 文件、AD 原理图和流程图全给齐了,不是只给一段残缺代码。
2. 系统硬件架构与传感器信号调理:别急着写代码,先把计数通道定下来
2.1 从传感器信号到单片机引脚:波形整形的必要性
光电式脉搏传感器的原始输出是一个随血流脉动变化的微弱模拟信号,幅度从几毫伏到几十毫伏不等,还叠加了环境光和工频噪声。如果直接把这个信号送进单片机引脚,结果只有一个:无法稳定触发中断,频率计数全是乱的。
处理路径是固定的:前置放大 → 滤波 → 比较器整形 → 限幅保护 → 单片机输入引脚。项目里 AD 部分的核心就是这条链路,前级用运放搭同相放大,把 mV 级信号放大到 V 级,再用比较器和一个基准电压比较,把正弦样式的波形变成标准方波。方波的上升沿就代表一次脉搏搏动,单片机只需要数上升沿的次数。
呼吸信号的来源类似。胸带式呼吸传感器或压电薄膜传感器在胸腔扩张收缩时产生形变,输出一个缓慢变化的电压。它的频率比脉搏低得多,成年静息状态下每分钟 12 到 20 次,也就是 0.2 到 0.33 Hz。对单片机的计数器而言,这个频率是极其宽松的。真正麻烦的不是频率低,而是传感器信号太弱、太缓,比较器容易在阈值附近抖动,导致一个呼吸周期里出现多个沿。
这里有一个容易被初学者忽略的选型点:比较器整形之后,必须在输出端加一个施密特触发器或 RC 正反馈网络,制造滞回区间。没有滞回,仿真里看着正常的逻辑,实物上一接传感器就可能狂跳中断。
2.1.1 单片机引脚资源分配
51 单片机(这里以 STC89C52 或 AT89S52 为参考)有 32 个 IO 引脚,但能作为外部中断输入的只有 INT0(P3.2)和 INT1(P3.3)。脉搏和呼吸两个信号正好各占一个外部中断引脚,用下降沿或上升沿触发。
这个选择比用定时器计数引脚更合理。定时器 T0 和 T1 虽然也可以配置成计数器模式,但只有 T0 的 P3.4 和 T1 的 P3.5 两个外部计数输入,且计数功能一旦启用,该定时器就无法再做时间基准。而本项目里需要一个定时器产生 1 秒闸门,用来计算频率,所以必须把两个定时器都留给时间基准,让外部中断承担计数角色。
另一个原因在于动态调节。外部中断在每次脉冲沿到来时给软件变量自增,主循环在闸门到期后读取变量计算频率,这种方式在低频率信号下误差可控。如果反过来用定时器直接计数,脉冲频率极低时(呼吸 0.3 Hz),1 秒只能计数 0 到 1 次,分辨率为 1 bpm,显示会一卡一卡的。
| 信号 | 接入引脚 | 触发方式 | 中断服务 |
|---|---|---|---|
| 脉搏方波 | P3.2 / INT0 | 下降沿 | pulse_count++ |
| 呼吸方波 | P3.3 / INT1 | 下降沿 | breath_count++ |
| 蜂鸣器 | P1.0 | 低电平驱动 | 报警输出 |
| 报警 LED | P1.1 | 低电平驱动 | 报警指示 |
| LCD1602 RS | P2.0 | 复用数据口 | 指令/数据选择 |
| LCD1602 RW | P2.1 | 复用数据口 | 读/写控制 |
| LCD1602 EN | P2.2 | 复用数据口 | 使能脉冲 |
| LCD1602 D4-D7 | P2.4-P2.7 | 四线模式 | 数据位 |
| 独立按键 | P1.2-P1.5 | 高电平有效 | 阈值设置 |
当然,IO 分配在 Proteus 仿真和 AD 原理图里可以灵活调整,仿真环境里没有传感器接入,直接用 DCLOCK 数字时钟源产生脉冲即可。
2.2 Proteus 仿真中的传感器模拟方式
真实传感器在 Proteus 里没有现成的模型,常规做法是用信号发生器或数字脉冲源替代。脉搏信号的频率取 72 次/分钟(1.2 Hz),呼吸取 18 次/分钟(0.3 Hz)。在 ISIS 界面左侧工具栏的"Generator Mode"里选"DCLOCK"或"DIGITAL PULSE",接到单片机的 P3.2 和 P3.3 引脚上,把频率参数分别设为 1.2 Hz 和 0.3 Hz,幅度设为 0 到 5 V。
一个重要细节:仿真里脉冲源是理想的,不存在抖动和毛刺,所以你在仿真时看到的频率值会非常稳定。但实物中传感器信号即使整形了也可能有边缘抖动。所以拿到这个项目包,验证逻辑功能时用仿真,验证信号处理时必须回到 AD 原理图上检查比较器阈值设计和滞回网络,两者解决的问题完全不同。
3. 频率测量、按键阈值设置与 LCD1602 驱动的核心代码实现
3.1 1 秒闸门与外部中断计数:主循环、中断和显示三者怎么协作
频率测量的本质是固定时间窗口内统计脉冲个数。窗口太长,显示刷新慢;窗口太短,低频信号损失精度。1 秒窗口对脉搏(1-3 Hz)和呼吸(0.2-0.5 Hz)都是可接受的折中方案。使用定时器 0 产生 50 ms 中断,软件累加 20 次得到 1 秒。
外部中断 0 和外部中断 1 分别处理脉搏和呼吸的下降沿,中断服务程序里只做变量自增,不处理任何显示和判断逻辑,保证中断服务程序足够短。这个写法和很多入门教程不一样,教程里喜欢在中断里直接判断是否报警,看起来省代码,但定时器中断里如果有耗时操作,会产生计数丢中断的隐患。
下面给出一个可以直接运行在 12 MHz 晶振下的代码框架,对应项目包里的 main.c 和 lcd1602.c:
// main.c 核心代码:1s 闸门 + 外部中断计数 #include <reg52.h> #include "lcd1602.h" sbit BEEP = P1^0; sbit LED_ALARM = P1^1; sbit KEY_MODE = P1^2; // 模式/设置键 sbit KEY_UP = P1^3; // 上限加/调整向上 sbit KEY_DOWN = P1^4; // 下限减/调整向下 sbit KEY_CONFIRM = P1^5; // 确认键 volatile unsigned int pulse_count = 0; volatile unsigned int breath_count = 0; volatile unsigned int timer_50ms = 0; unsigned char flag_1s = 0; unsigned int pulse_rate = 0; // 脉搏频率 次/分钟 unsigned int breath_rate = 0; // 呼吸频率 次/分钟 // 报警上下限默认值 unsigned int pulse_high = 100; unsigned int pulse_low = 60; unsigned int breath_high = 24; unsigned int breath_low = 12; // 定时器0 50ms初始化 void timer0_init(void) { TMOD &= 0xF0; TMOD |= 0x01; // 定时器0,模式1,16位定时 TH0 = 0x4C; // 50ms @ 12MHz TL0 = 0x00; ET0 = 1; TR0 = 1; EA = 1; } // 外部中断0:脉搏下降沿 void int0_isr(void) interrupt 0 { pulse_count++; } // 外部中断1:呼吸下降沿 void int1_isr(void) interrupt 2 { breath_count++; } // 定时器0中断:50ms 累加 20 次得 1s void timer0_isr(void) interrupt 1 { TH0 = 0x4C; TL0 = 0x00; timer_50ms++; if (timer_50ms >= 20) { timer_50ms = 0; flag_1s = 1; } }这段代码的关键点在于:外部中断服务函数中只做pulse_count++和breath_count++,这两个变量用volatile修饰,防止 Keil C51 优化器把它们误优化到寄存器里。中断函数里不能调用 LCD 显示函数,因为在 51 单片机里,显示函数涉及较长的循环操作,会拉长中断响应时间。定时器 0 重装初值的写法直接写在中断函数里,没有用自动重装模式,因为定时器 0 模式 2 是 8 位计数器,最大定时时间不够 50 ms(12 MHz 下最长 256 个机器周期即 256 us),所以要手动重装 16 位初值。
3.2 主循环:读计数、算频率、判断报警阈值
在flag_1s置位后,主循环需要立即读取pulse_count和breath_count并清零。这里有一个数据一致性问题:如果在主循环读取计数值时恰好外部中断触发了pulse_count++,读到的值就是"读到一半"的旧值,问题不大,但更好的做法是先关闭外部中断,再复制变量,再打开。
// main.c 主循环部分 void main(void) { unsigned int pulse_temp = 0; unsigned int breath_temp = 0; lcd1602_init(); timer0_init(); IT0 = 1; // 外部中断0下降沿触发 IT1 = 1; // 外部中断1下降沿触发 EX0 = 1; EX1 = 1; lcd1602_show_string(0, 0, "PR:000 BPM BR:00"); lcd1602_show_string(0, 1, "H:100 L:060 H:24 L:12"); while (1) { if (flag_1s) { flag_1s = 0; // 关闭外部中断,保证读取一致性 EX0 = 0; EX1 = 0; pulse_temp = pulse_count; breath_temp = breath_count; pulse_count = 0; breath_count = 0; EX0 = 1; EX1 = 1; // 频率换算:1s 内的脉冲数即 Hz,转每分钟 pulse_rate = pulse_temp * 60; breath_rate = breath_temp * 60; // 更新 LCD 显示 lcd1602_show_uint8(3, 0, pulse_rate); lcd1602_show_uint8(6, 0, breath_rate); // 报警逻辑 if ((pulse_rate > pulse_high) || (pulse_rate < pulse_low) || (breath_rate > breath_high) || (breath_rate < breath_low)) { BEEP = 0; LED_ALARM = 0; } else { BEEP = 1; LED_ALARM = 1; } } scan_key(); } }这里有一个很关键的换算逻辑:1 秒窗口内计数到的脉冲个数,数值上等于多少 Hz。比如 1 秒内数到 30 个脉搏脉冲,那就是 30 Hz,但这显然不符合人体生理,因为静息脉搏 60-100 次/分钟对应 1-1.67 Hz。所以代码里用pulse_temp * 60把每秒脉冲数换算成每分钟,这正是"次/分钟"这个概念的本质。对应到仿真里,DCLOCK 设为 1.2 Hz,1 秒内计数 1 到 2 个上升沿,pulse_temp * 60得到 60 到 120。你会发现仿真读数在 60 和 120 之间跳变,原因在于 1 秒闸门和 1.2 Hz 脉冲的相位不完全对齐,这是固定窗口计数的固有误差。
3.3 独立按键设置阈值的状态机实现
按键要支持"上限加、上限减、下限加、下限减",最简单的做法是四个独立按键分别对应四个方向的调整。虽然这个项目标注了独立按键,但没有说明按键数量。从 PCB 引脚复用考虑,2 个按键加一个模式键的方案更常见:模式键循环选择当前要修改的参数(脉搏上限、脉搏下限、呼吸上限、呼吸下限),加减键调整数值。
按键处理的核心是消抖。仿真里按键是理想的,按下即闭合,不加消抖也能跑通。但实物如果不在软件里做消抖,机械抖动会造成一次按下被识别成多次。
// 按键扫描:模式键切换调整项,加减键修改阈值 void scan_key(void) { static unsigned char set_index = 0; // 0=P高 1=P低 2=B高 3=B低 static unsigned int key_count = 0; static bit last_mode = 0; if (KEY_MODE == 1) { // 按键按下(高电平有效) if (key_count < 10) { key_count++; } if (key_count == 5) { // 持续稳定后生效,约 50ms 消抖 set_index = (set_index + 1) % 4; lcd1602_show_uint8(11, 1, set_index); // 显示当前调整项编号 } } else { key_count = 0; } if (KEY_UP == 1) { switch (set_index) { case 0: if (pulse_high < 200) pulse_high++; break; case 1: if (pulse_low > 30) pulse_low--; break; case 2: if (breath_high < 40) breath_high++; break; case 3: if (breath_low > 6) breath_low--; break; } lcd1602_show_uint8(3, 1, pulse_high); // 刷新 LCD 对应位置 } if (KEY_DOWN == 1) { switch (set_index) { case 0: if (pulse_high > pulse_low) pulse_high--; break; case 1: if (pulse_low > 30) pulse_low--; break; case 2: if (breath_high > breath_low) breath_high--; break; case 3: if (breath_low > 6) breath_low--; break; } lcd1602_show_uint8(7, 1, breath_high); // 刷新 LCD 对应位置 } // 延时等待按键释放 while (KEY_UP == 1 || KEY_DOWN == 1); }注意last_mode变量的存在意义。它用来记录上一次模式键的状态,只有检测到从 0 变 1 的上升沿时才认为是一次新的按下,连续按住不会连续切换。这是按键处理中容易被忽略的一点:很多人在仿真中测试按键正常,到实物上发现按一次跳好几个数值,原因往往是缺少边沿检测,把"按住期间持续为 1"当成了"多次按下"。
4. 从 Proteus 仿真联调到 AD 原理图转实物:参数设置和隐藏的坑
4.1 仿真环境里的操作顺序
拿到项目包后,先用 Proteus 打开仿真.DSN,然后双击 51 单片机芯片,在 Program File 栏加载 main.hex。晶振频率和代码里的定时器初值必须匹配。如果代码按 12 MHz 写的 50 ms 初值 0x4C00,仿真里晶振却设置成 11.0592 MHz,定时周期会变成 54 ms 左右,显示刷新偏差不大,但串口波特率相关的模块会出问题。本项目中只有定时和显示,影响是显示刷新略慢,逻辑仍然正确。
建议的联调步骤:
- 先把两个 DCLOCK 的频率都设为 0 Hz,确认 LCD 正常显示"PR:000 BPM BR:00",初始化和显示函数没问题。
- 设脉搏信号 1.2 Hz,观察一秒后 PR 值是否在 60-120 之间跳变,同时把报警阈值改到 80-90 之间,触发一次报警验证蜂鸣器逻辑。
- 设呼吸信号 0.3 Hz,观察 BR 值是否在 12-24 区间振荡。
- 连接虚拟示波器(Virtual Oscilloscope)到 P3.2 引脚,确认方波下降沿的时序和 LCD 刷新时序没有明显冲突。
一个常见的仿真排错现象是:LCD 有背光但无字符。先量单片机 XTAL 引脚是否有脉冲,再看 EA 引脚是否接高电平,最后检查程序里 LCD 时序的延时是否过短。Proteus 里如果延时时间给得太短,虽然逻辑波形正确,但液晶模块没有响应。
4.2 AD 原理图的模块划分:传感器电路要单独留测试点
项目包含原理图 Sheet1.SchDoc 和 Free Documents 里的输出文件。原理图模块按功能应该划分为:最小系统(单片机+晶振+复位电容)、电源(5V 稳压)、LCD1602 接口、独立按键、声光报警。传感器信号调理部分作为独立电路块,输入端接接插件,模拟地和数字地之间用 0 欧电阻或磁珠隔离。
| 模块 | 关键器件 | 参数/模型 | 注意事项 |
|---|---|---|---|
| 信号放大 | LM358 或 LM324 | 同相放大,增益 20-50 倍 | 电源去耦电容靠近运放引脚 |
| 比较整形 | LM393 或 TLV3501 | 滞回比较,Vref 取电源中值 | 反馈电阻计算滞回宽度 |
| 光电脉搏探头 | 红外 LED + 光敏管 | 发射管限流 10-20 mA | 遮光罩不可省 |
| 蜂鸣器驱动 | 三极管 S8550 + 蜂鸣器 | IO 低电平导通 | 蜂鸣器线圈并联续流二极管 |
| 晶振电路 | 12 MHz + 2x22pF 负载电容 | 晶振频率与代码匹配 | PCB 布局尽量靠近单片机 |
从 AD 原理图转到 PCB 时,信号调理电路的地线处理很重要。脉搏传感器信号的幅度只有毫伏级,模拟地和数字地在单片机底部单点汇合,避免数字信号回流穿过模拟区。这是仿真中完全体现不出来、但实物必踩的坑。AD 原理图检查时重点看网络标号是否和单片机引脚一一对应,特别是 P3.2 和 P3.3,在仿真和实物上对应着单片机物理引脚 12 和 13,原理图和 PCB 的封装引脚映射不能出错。
4.3 Proteus 仿真和实物测试的差异清单
仿真里信号源是理想的,按键是理想的,LCD 时序也是理想化的。实物会引入电源纹波、按键抖动、传感器信号漂移、LCD 对比度调节等问题。项目包里有一个"新建文本文档.txt",里面通常记录了作者实测时发现的问题和改动记录,拿到包先打开看,那里比任何教程都更接近实际踩坑点。
具体差异参考:
- Proteus 里不需要外接上拉电阻,但 51 单片机 P0 口在实物上必须外接 10k 排阻
- 仿真里蜂鸣器可以直连 IO 模拟,实物必须加三极管驱动,芯片 IO 输出电流不足以直接驱动蜂鸣器
- 传感器在仿真里不存在,DCLOCK 输出摆幅 0-5 V,实物传感器经过整形后摆幅可能只有 3.3 V,如果单片机是 5V 供电,需确认阈值电平兼容
5. 周期法替代固定窗口计数:低频率信号下的精度提升实测技巧
固定 1 秒窗口计数在脉搏 1.2 Hz、呼吸 0.3 Hz 时误差是显著的,根本原因是窗口和信号周期之间没有对齐。改进方向是周期法:不数"固定时间内有多少个脉冲",而是数"一个脉冲周期内有多少个定时器计数值"。
以脉搏为例,在两次脉搏下降沿之间,用定时器 0 记录经过的机器周期数。假设晶振 12 MHz,一个机器周期 1 us,连续两个下降沿之间记录了 830000 个机器周期,则脉搏频率是 1/0.83 s = 1.2 Hz,也就是 72 次/分钟。这个方法在低频下比固定窗口计数精确得多,因为测量的直接是信号周期本身,不受窗口对齐影响。
具体实现思路是:INT0 下降沿触发时,读取当前定时器值并和上次记录值做差,差值的倒数乘以 60 得到频率。如果信号频率升高到几十 Hz,周期法对定时器溢出处理和重载时机要求更高,需要用 16 位定时器并处理溢出中断记录。测量精度限度和优化手段如下:
| 信号频率范围 | 适用方法 | 精度特征 | 优化手段 |
|---|---|---|---|
| 0.2 - 1 Hz | 周期法 | 周期时长 1-5 s,计数分辨率高 | 处理定时器溢出,滑动平均 |
| 1 - 5 Hz | 周期法或计数法 | 计数法窗口偏差不足一个脉冲 | 多窗口平均,丢弃突变值 |
| 5 Hz 以上 | 计数法优先 | 周期太短,定时器读取开销占比高 | 扩展 1s 窗口为 2s |
呼吸频率 0.3 Hz 意味着一个完整周期约 3.3 秒,周期法测量时,每 3 秒多才能刷新一次显示,响应较慢。所以改进版本常见做法是动态切换:呼吸用 2 秒窗口计数,脉搏用周期法,两者独立运算。这个技巧可以直接在项目包的 main.c 基础上改动,不改硬件。
最后两段值得特别关注:报警阈值设置存在边界条件,pulse_high不能设置成低于pulse_low,pulse_low不能低于生理极限值,否则会出现永远报警或永不报警的假死状态。在按键处理里加上大小关系约束,比在仿真后加一个值域判断要可靠得多。set_index切换时,LCD 第二行对应位置的数字要做闪烁提示,没有这种视觉反馈,用户会以为按键失效。把模式切换的闪烁逻辑写进主循环的刷新分支里,改动量不大,但体验差异明显。
本文还有配套的精品资源,点击获取