基于STM32的智能输液监护调控系统:从滴速检测到PID闭环控制 📅 发布时间:2026/9/5 5:58:00 👁 浏览次数: 1. 为什么要自己搭一套智能输液监护系统从医院场景倒推出来的需求做嵌入式这几年我陆陆续续接触过不少医疗电子相关的项目但真正让我下定决心把整套智能输液监护调控系统开源出来的其实是一次去医院的陪护经历。当时隔壁床一位老人输液药液快滴完时家属没注意护工又忙不过来最后是靠邻床提醒才叫来护士换药。那一刻我就在想输液监护这件事表面上是个盯着吊瓶的小事实际上牵扯到患者安全、护士工作量、家属焦虑感一堆问题而市面上多数输液泵的价格又让很多基层医疗机构和家庭场景望而却步。正好那段时间我在做STM32相关的项目手头有F103C8T6的板子又刚研究了步进电机驱动和多种传感器融合的方案于是就有了这套智能输液监护调控系统升级版。所谓的升级版是相对于我上一版只做监测报警来说的——上一版只能检测滴速异常和液位过低然后报警这一版加上了自动调控的能力也就是说系统不仅能发现问题还能在安全范围内做初步处理比如暂停输液、调整滴速甚至可以和上位机联动由医护人员远程确认后执行操作。整个项目全部开源包含三样东西完整可编译的STM32工程代码、Altium Designer绘制的原理图、Proteus仿真工程。也就是说你有三种方式来使用这套项目手上只有开发板想快速验证逻辑可以直接看代码和仿真工程想做一块正经的PCB原型机可以直接参考原理图甚至改版想把它做成毕业设计、竞赛作品或者产品原型这套项目提供了完整的基线版本。项目的主要功能和性能指标我先放在这儿方便你判断这套东西是否值得深入看功能模块具体能力实现方式滴速检测红外对管遮挡计数实时计算滴速滴/分钟外部中断 定时器窗口统计液位检测输液管末段液位传感器低于阈值报警数字IO轮询 消抖处理自动调控步进电机驱动蠕动泵PID闭环调节滴速增量式PID PWM细分驱动人机交互OLED显示实时参数按键设置目标滴速I2C驱动SSD1306 GPIO按键扫描报警系统声光报警异常状态分级提示蜂鸣器 LED 串口上报上位机通信串口协议上报数据和接收指令USART1中断收发 自定义帧协议这套系统最能打的一点是它把监测和调控这两件事同时给做了。很多开源项目只做监测数据报了就不管了也有很多项目只做调控但不检测实际效果。这套系统是先把滴速的实际值测准然后用PID去逼近目标值形成了一个完整的闭环控制链路。2. 系统硬件设计原理图里的每一路信号都是有讲究的2.1 主控选型STM32F103C8T6为什么是刚刚好的选择主控我用了STM32F103C8T6这颗芯片在嵌入式圈子里几乎人手一块某宝上几块钱就能买到Flash虽然只有64KB但跑这套逻辑绰绰有余。选它有几个具体考虑第一外设够用且不浪费。系统需要的核心外设是外部中断、定时器、I2C、USART、GPIO、ADCF103C8T6全都具备而且数量正好匹配滴速检测用PA0的外部中断液位传感器用PB0~PB3的数字输入步进电机用TIM2输出PWM驱动OLED走PB6/PB7的I2C1串口用PA9/PA10。资源表排得满满当当但又不紧张这种刚刚好的感觉实际开发中非常舒服。第二3.3V供电体系统一。整块板子使用3.3V作为主逻辑电压传感器模块、OLED模块都能直接由板载LDO供电不需要额外电平转换电路。这大大简化了原理图的设计也降低了新手焊接时的出错概率。第三生态成熟调试工具链完备。无论是用标准外设库还是HAL库网上都有海量参考资料。我自己用标准外设库写的这套代码底层寄存器操作比较直观也方便你移植到寄存器版或者HAL版。2.2 电源设计与隔离模拟和数字部分为什么必须分开先说一个很多人会忽略的问题这套系统里既有传感器的小信号红外对管的微弱电流变化又有步进电机的驱动电流瞬时电流可能到几百毫安甚至1A如果电源不处理好会出现一个非常典型的现象——电机一转滴速检测值就开始乱跳。原因很简单电机启动时拉低电源电压导致红外对管的发射管光强闪烁接收端的信号就出现了毛刺。解决方案是在原理图里做了模拟电源和数字电源的分区处理输入电源先经过一个SS34肖特基二极管做反接保护然后进入AMS1117-3.3稳压输出3.3V主电源模拟部分红外对管接收端、液位传感器的比较器供电通过一个磁珠L1从3.3V分出VCC_ANA数字部分MCU、OLED、蜂鸣器、电机驱动逻辑侧直接接VCC_3V3模拟地AGND和数字地GND单点连接在电路板的电源入口处汇合。这么做的好处是电机和蜂鸣器工作时的电流波动主要影响数字电源而模拟电源被磁珠的高频阻抗挡在外面红外对管接收端的信号稳定性显著提升。实测下来电机工作时滴速检测值的波动从原来的±5滴/分钟降到了±1滴/分钟以内这个改进对PID控制至关重要——你给PID的反馈数据如果本身就不准后面调得再辛苦也是白搭。2.3 红外对管滴速检测电路一个下拉电阻的坑我踩了整整一下午滴速检测的原理不复杂输液滴壶两侧分别放红外发射管和接收管液滴落下时会短暂遮挡红外光接收管的状态就会从导通变到截止用一个上/下拉电阻把这个变化转成电平跳变MCU就能通过外部中断捕捉到每一次滴落。但这套方案里有个很隐蔽的坑就是接收管的偏置电阻怎么选。我给的是10K下拉电阻信号接到比较器的同相输入端。一开始为了省事我没用比较器直接把接收管的分压信号送进MCU的GPIO结果发现信号边缘特别软滴速快了之后经常丢中断。后来查了逻辑分析仪才知道红外管的光电响应不是瞬间完成的分压信号上升沿有几十毫秒的斜坡MCU的施密特触发虽然能处理一定的边沿抖动但高速滴落时连续两个脉冲间隔太短信号还没恢复到阈值就又掉下去了就会漏检。最终的电路改成了LM393比较器整形接收管信号经过10K/10K分压后进比较器的同相端反相端接一个可调电位器提供参考电压。调整电位器可以设置触发阈值保证滴落瞬间比较器快速翻转输出方波直接进MCU的外部中断引脚。这个方案的抗干扰能力比直接接GPIO强很多测试中在每分钟20滴到200滴的范围内都能稳定计数。2.4 步进电机与蠕动泵驱动为什么我不建议你用直流电机自动调控的核心执行机构是蠕动泵——一根软管被滚轮反复挤压推动液体往前流动。蠕动泵的流量和转速近似线性所以用步进电机驱动比直流电机有天然优势精确控制步进电机按固定角度走可以精确控制挤压频率从而实现精确的滴速控制保持力矩步进电机在停止状态下也有保持力矩可以防止蠕动泵在停止时由于液体压力产生倒流无需编码器开环就能达到流量控制精度降低系统复杂度和成本。电机驱动我选了ULN2003这是最经典的达林顿管阵列芯片5线四相步进电机28BYJ-48的标准搭配。原理图里每一相都串了限流电阻并且在电机电源端加了100uF电解电容和104陶瓷电容做去耦。关于电机驱动我的建议是如果你要做一个真正的产品级原型建议把ULN2003换成A4988或者DRV8825驱动板。前者电流能力弱噪音大力矩也一般但用在Demo和毕设场景完全够用且极便宜。后者支持细分驱动噪音小很多但需要额外的逻辑电源和电机电源分离设计。这也是为什么我在原理图里保留了电机供电接口的独立性方便你后续替换驱动方案。2.5 完整BOM和成本估算整套硬件做下来大约多少钱我把原理图里所有器件整理一下给你做个成本参考按某宝零售价估算不含PCB打样费器件型号/规格数量参考单价小计主控STM32F103C8T616元6元红外对管红外发射/接收一体或分体2组1元2元比较器LM39310.5元0.5元稳压AMS1117-3.310.3元0.3元OLED屏SSD1306 0.96寸 I2C115元15元步进电机28BYJ-48 ULN200318元8元蜂鸣器有源蜂鸣器11元1元按键轻触开关40.2元0.8元电阻电容各种规格若干-约3元PCB打样嘉立创5片包邮1次约20元20元传感器管子输液管液位检测附件1套约10元10元大约66.6元。这个成本做出来的东西功能上对标几千块的输液泵的监测和基础调控能力性价比是碾压级的。当然电机力矩、控制精度、安全认证这些和医疗级设备肯定是没法比的所以这个项目的定位很明确教学演示、原理验证、原型开发、个人学习而不是临床使用。3. 核心代码与软件逻辑从滴速计算到PID闭环控制3.1 工程代码的整体架构和文件划分代码我基于标准外设库写的工程结构严格按照模块化思想划分。你下载下来用Keil5打开就能直接编译不需要额外配置路径。主目录如下SmartInfusion/ ├── User/ │ ├── main.c // 主逻辑循环 │ ├── stm32f10x_it.c // 中断服务函数 │ └── system_stm32f10x.c // 系统时钟配置 ├── App/ │ ├── droplet_detect.c/h // 滴速检测模块 │ ├── liquid_level.c/h // 液位检测模块 │ ├── motor_control.c/h // 步进电机控制模块 │ ├── pid.c/h // PID算法模块 │ ├── oled_display.c/h // OLED屏显示模块 │ ├── key_scan.c/h // 按键扫描模块 │ └── alarm.c/h // 报警模块 ├── BSP/ │ ├── usart.c/h // 串口初始化与收发 │ ├── i2c.c/h // I2C软件模拟 │ └── timer.c/h // 定时器初始化 └── Hardware/ ├── stm32f10x_conf.h └── stm32f10x.h这种分层的好处是App层的每个模块只做自己的事通过接口函数互相调用不直接操作寄存器。比如滴速检测模块对外暴露一个DropletDect_GetSpeed()函数主循环只用调这个函数拿结果至于内部是外部中断计数还是定时器计算完全不用关心。你要改成HAL库只需要把BSP层重写一遍App层代码几乎不用动。3.2 滴速检测的算法细节怎么把两次滴落间隔换算成滴/分钟滴速检测的核心是外部中断定时器窗口统计。我在EXTI0_IRQHandler中断服务函数里做两件事第一读取当前系统滴落计数器的值g_dropletCount加1 第二判断如果当前处于统计窗口内定时器每5秒触发一次窗口更新就记录两次滴落的时间戳计算出当前滴速。// App/droplet_detect.c 核心片段 volatile uint32_t g_dropletCount 0; volatile uint32_t g_lastTick 0; volatile uint16_t g_speedDropletsPerMin 0; static uint32_t s_windowStartTick 0; static uint32_t s_windowDropletCount 0; void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) ! RESET) { uint32_t now TIM2-CNT; // 如果两次滴落间隔太短判定为抖动忽略 if (now - g_lastTick 10) { g_dropletCount; g_lastTick now; } EXTI_ClearITPendingBit(EXTI_Line0); } } void DropletDect_UpdateSpeed(void) { uint32_t now TIM2-CNT; uint32_t elapsed now - s_windowStartTick; if (elapsed 5000) // 5秒统计窗口 { uint32_t dropletsInWindow g_dropletCount - s_windowDropletCount; // 换算为每分钟滴速 g_speedDropletsPerMin (uint16_t)((float)dropletsInWindow * 12.0f); s_windowStartTick now; s_windowDropletCount g_dropletCount; } }这里有个容易被新手忽略的细节为什么统计窗口是5秒而不是1秒或者10秒如果窗口太短比如1秒那么滴速慢时比如每分钟20滴一次滴落间隔是3秒一个1秒窗口内可能一次滴落都没有测出来的速度就是0滴/分钟这显然不对窗口太长比如10秒系统对滴速变化的实时性就差。5秒是一个折中——对于每分钟12滴以上的输液速度临床上常见范围是20~60滴/分钟5秒窗口至少能捕捉到1~5个脉冲换算成滴/分钟误差可控。实际测试中这个窗口下稳态误差在±2滴以内对PID控制来说是够用的。另外注意代码里有个抖动过滤两次中断间隔小于10ms就忽略。因为红外对管在液滴落下的瞬间可能出现回弹液滴表面张力导致光线多次遮挡不处理就会一次滴落记成多次。3.3 PID调参实战记录从发散到稳定的完整过程PID是这套系统里最硬核的部分也是很多人在毕设或竞赛里最容易翻车的地方。我把实际调参过程记录下来因为这比单纯贴代码有用得多。首先明确被控对象和控制量被控对象蠕动泵的滴速单位滴/分钟执行器步进电机的转速单位拍/秒对应相序脉冲频率反馈量红外对管测得的实际滴速我最初写的PID参数是理论值P1.0I0.1D0.0想着反正系统惯量不大先跑起来看看。结果一跑电机疯狂振荡滴速在目标值上下大幅摆动有时还冲出安全范围触发报警。用串口把数据导出来画图就是那种经典的等幅振荡曲线。后来我系统地做了调整第一步把I和D先设0只留P。从P0.1开始往上涨每次跑30秒看响应曲线。发现P0.3时系统已经能接近目标值但有稳态误差P0.5时就开始出现轻微过冲P0.8时明显振荡。于是把P定在0.35。第二步加I消除稳态误差。I0.05观察系统能不能缓慢逼近目标值。发现响应偏慢调到0.08后稳态误差基本消除但出现了一点低频波动。又调回0.06波动消失。第三步加D抑制过冲。目标值从30滴突变为60滴时纯PI会产生约15%的过冲。D0.2时过冲减小到5%以内但响应速度略有下降。最后D取0.15过冲约3%调节时间约10秒各项指标都满足需求。最终参数// App/pid.c 最终调参结果 PID_TypeDef g_pid { .targetValue 40.0f, // 目标滴速 .kp 0.35f, // 比例系数 .ki 0.06f, // 积分系数 .kd 0.15f, // 微分系数 .outputLimit 1000.0f, // 输出限幅步进电机最高脉冲频率 };关于PID有个非常重要但经常被忽略的点输出限幅。步进电机的脉冲频率不是可以无限大的超出电机响应能力后脉冲再多也是白搭反而会让电机丢步。我把输出限幅设为1000PPS对应蠕动泵最高流速约每分钟120滴这个值在医疗输液场景下已经非常高了。还有一个工程技巧积分分离。当实际滴速和目标值偏差超过设定范围时我设为20滴把积分项清零防止积分饱和导致系统过冲。这个逻辑用代码写很简单但对控制效果的提升非常明显。float PID_Calculate(PID_TypeDef *pid, float currentValue) { float error pid-targetValue - currentValue; float proportional pid-kp * error; float integral pid-integral; if (error 20.0f) // 积分分离偏差过大时关闭积分 { integral pid-ki * error; pid-integral integral; } else { pid-integral 0.0f; } float derivative pid-kd * (error - pid-lastError); pid-lastError error; float output proportional integral derivative; // 输出限幅 if (output pid-outputLimit) output pid-outputLimit; if (output -pid-outputLimit) output -pid-outputLimit; return output; }3.4 串口协议设计自定义帧格式 握手应答机制上位机通信的串口协议我设计成一个简单的自定义帧帧头 设备地址 命令字 数据长度 数据域 校验字节。校验采用异或和够简单也够用。// 协议帧格式 // | 0xAA | 0x5A | ADDR | CMD | LEN | DATA... | XOR_CHECK |命令分为三类0x01 查询命令上位机请求设备状态设备回复当前滴速、液位状态、报警状态0x02 控制命令上位机设置目标滴速、启动/暂停电机0x03 主动上报设备在滴速异常、液位过低时主动上报报警事件。握手流程是设备上电后每2秒发送一次心跳帧上位机收到后如果设备地址匹配就回复ACK之后进入正常工作模式。这个握手机制解决了一个很实际的痛点如果上位机软件没打开或者串口被占用设备会一直尝试连接但不会误动作。有人可能会问为什么不用Modbus这样现成的协议因为项目定位是轻量级教学/原型Modbus虽然规范但帧结构较复杂在小资源MCU上跑起来有点大材小用。自己定义协议帧的好处是代码量小、逻辑直观而且上位机这边我用Python的pyserial写了个简单的调试工具几行代码就能解析帧并实时显示数据。4. 仿真工程与实物联调Proteus仿真能验证什么不能验证什么4.1 仿真工程的搭建细节Proteus仿真工程里我把硬件设计等比例映射进去STM32F103C8T6模型、LM393比较器模型、红外对管可以用开关信号发生器模拟、步进电机用仿真模型、OLED用I2C显示模型。你在Proteus里打开工程就能直接跑程序烧到虚拟MCU里OLED会显示仿真滴速和状态。仿真工程的价值主要在逻辑验证阶段。比如我想测试目标滴速从40变成80时PID如何响应在实物上需要手动调整吊瓶高度或者更换泵管在仿真里只需要改一个参数就行。这种改参秒级验证的效率优势在开发初期非常明显。4.2 仿真和实物最大的差异信号完整性但必须坦白讲仿真不能替代实物验证最大的原因在于信号完整性。Proteus里的红外对管不会受到电机电源波动的影响LM393的输出也是理想的数字电平但在实物上电机的EMI干扰、电源纹波、导线电感这些东西全都存在。我在实物调试时就遇到过一个非常诡异的现象电机转动后滴速检测值从每分钟60掉到了每分钟8一开始还以为是传感器坏了后来用示波器一看原来是电机在地线上产生的纹波干扰了红外接收管的参考电压导致比较器误触发。这类问题在仿真里根本复现不出来所以我的建议是用仿真验证算法逻辑用实物验证信号链路。两者配合才能保证系统的可靠性。4.3 上位机联调Python开个可视化的数据监控小工具串口协议既然定好了上位机端我也顺手写了个Python小工具用来实时显示滴速曲线和系统状态。代码很简单核心逻辑就是串口接收和解析——读取到帧头0xAA 0x5A后按帧格式解析命令把滴速数据画成折线图。import serial import matplotlib.pyplot as plt import matplotlib.animation as animation ser serial.Serial(COM3, 115200, timeout1) speed_history [] def parse_frame(data): # 完整帧解析逻辑 # 如果数据帧的校验通过返回滴速值 return speed def update_plot(frame): raw_data ser.read(64) if raw_data: speed parse_frame(raw_data) if speed is not None: speed_history.append(speed) if len(speed_history) 100: speed_history.pop(0) plt.cla() plt.plot(speed_history) plt.xlabel(Time) plt.ylabel(Droplets/min) plt.title(Infusion Speed Monitor) ani animation.FuncAnimation(plt.gcf(), update_plot, interval500) plt.show()这个工具在调PID时作用巨大。你能直观看到系统的响应曲线比如是否存在过冲、稳态误差有多大、调节时间有多长。如果没有可视化工具只靠OLED屏上的数字这类调试基本没法做。4.4 三套使用路径的配合关系至此你应该能看出整个项目代码、原理图、仿真三者之间的关系了路径需要的资源适合场景能验证什么纯仿真仿真工程 代码逻辑入门、PID调参、演示讲解算法正确性、逻辑分支、控制效果实物原型原理图 代码毕设实物、竞赛作品、产品验证电路设计、信号质量、实际控制精度二次开发全部代码 原理图扩展功能、改成无线方案、升级屏幕等架构扩展性、代码可维护性我之前遇到过不少问能不能只做仿真不做实物的同学——能但你要清楚仿真做出来的东西只能证明逻辑对了不能证明电路对了。反过来如果你直接开打PCB却不先跑仿真一旦PID逻辑有bug改板子的成本比改代码高得多。所以最稳的路径永远是仿真先行实物跟上两者互相印证。5. 从初版到升级版的演进换代过程中踩过的最记忆深刻的几个坑5.1 初版报警误报率为什么居高不下来自滴壶挂壁水珠的干扰上一版的系统主要做监测报警但初版做完后我拿到真实输液场景测试发现误报率高到没法用。最典型的一个场景输液滴壶上方的管壁挂了一颗水珠它不落下只是挂在管壁上红外对管长时间处于遮挡状态系统就判定为滴速为0并报警。但实际上输液是正常的只是那滴水珠刚好挡住了红外线。解决思路是区分长时间遮挡和持续滴落的波形特征。真正的滴落是一个短暂的脉冲——遮挡时间通常在50~200ms左右然后迅速恢复挂壁水珠则是持续的遮挡。于是代码里加了一个逻辑如果遮光持续时间超过500ms就不算一次滴落而是标记为疑似挂壁进入异常检测分支。这个改进没有增加任何硬件成本纯靠软件算法就解决了。机电系统里的很多问题都是这样先别急着换硬件想想能不能通过软件逻辑来看穿物理世界的噪声。5.2 升级版新加入的电机控制给电源带来的连锁反应升级版加了电机之后新的问题又来了电机一启动OLED屏幕亮度就闪烁串口偶尔还会出现乱码。排查之后发现是电源的问题——电机启动瞬间电流尖峰导致3.3V电压跌落MCU和OLED虽然不是同一个电源网络但地线是共用的地线上的压差直接影响了信号质量。这个问题的处理我在前面原理图部分已经提到模拟/数字分离、电机电源独立、加去耦电容。但更重要的是要有排查思路先用示波器看电源纹波再用逻辑分析仪看串口信号逐步缩小范围最后锁定到地线公共阻抗噪声。分享这个例子的目的是想说设计时预留好测试点非常重要。我在原理图的电源、地、关键信号上都加了测试焊盘调试时直接夹示波器探头就行不用拆线省了很多时间。5.3 两个防呆设计堵转保护与报警延迟消抖升级版另外加了两个防呆设计我觉得对做同类项目的你有参考价值第一个是堵转保护。蠕动泵在输液过程中如果输液管被折叠或者患者移动扯到了管路泵的负载会突然增大步进电机可能丢步堵转。如果不处理电机会持续发热严重时可能损坏电机或管路。我在电机控制模块里加了一个逻辑如果连续10次PID运算中输出值都超过上限的90%但实际滴速几乎为0就判定为堵转立即停止电机并报警。// motor_control.c void Motor_CheckStall(void) { static uint8_t highOutputCount 0; if (g_pid.output g_pid.outputLimit * 0.9f g_speedDropletsPerMin 5) { highOutputCount; if (highOutputCount 10) { Motor_StopEmergency(); Alarm_Raise(ALARM_STALL); highOutputCount 0; } } else { highOutputCount 0; } }第二个是报警延迟消抖。液位传感器在输液过程中可能因为气泡经过产生短暂的误触发信号如果一触发就报警会很烦人。我加了一个软件消抖逻辑液位低信号必须持续超过3秒才触发报警。这个延迟时间既不影响安全性输液管末段的液位低到报警点后到完全滴空还有至少几十秒的缓冲又有效避免了误报。5.4 从这一版还能往哪些方向继续升级这套系统现在的开源版本其实还有很大的扩展空间。如果你想在这个基础上继续做我建议按下面的优先级考虑第一加无线通信模块。把串口协议改成通过ESP8266/ESP32的WiFi模块或者HC-05蓝牙模块传输就能实现护士站大屏监控。硬件只需要在USART1上挂一个透传模块协议几乎不用改。第二加多通道支持。当前版本是单通道输液监护如果要做多通道主控建议换成STM32F103ZET6144pin的型号它有更多的外部中断引脚和定时器通道靠软件框架的模块化复制一份App层的逻辑就能扩展一路。第三加断电续调。用EEPROMI2C接口的AT24C02保存当前的PID参数和运行状态掉电后重新上电可以自动恢复中断前的输液任务不用护士重新设置。第四加GSM/4G远程报警。对于家庭场景用SIM800L模块发送短信报警这是目前很多商用产品不具备但实际需求很强的功能。这套项目做到这版已经是从传感器采集到闭环控制从本地显示到上位机联动的全链路闭环了。结构上足够覆盖一个典型的嵌入式系统包含的几乎所有技术要素中断、定时器、PWM、PID、串口协议、模块化编程。如果你正在做类似的项目可以直接拿这套代码和原理图当基线把精力花在你特有的创新点上比从零开始要高效得多。最后分享一个我做这类项目多年下来的体会做嵌入式系统功能实现只是第一步把稳定可靠四个字刻进代码里才是真正拉开差距的地方。比如滴速检测的抖动过滤、报警的延时消抖、电机的堵转保护这些初看都是小事但正是这些细节决定了别人愿意不愿意把你的作品当成产品来用。希望这套开源项目能帮你少走一些我走过的弯路。