说实话我刚看到这个项目标题的时候心里第一反应是终于有人把“输液监护”这种偏医疗的赛道做成了适合嵌入式学习者直接上手复现的开源方案了。智能输液监护调控系统乍一听好像是医院里才会出现的设备但拆开看本质它就是一个非常典型的单片机闭环控制系统传感器采集特征信号MCU做运算判断执行机构根据控制量动作再通过反馈不断修正输出。把这一套链路玩明白基本就把STM32的核心外设串起来了中断、定时器、PWM、I2C、状态机一个都没落下。这套升级版系统能做什么一句话说清楚通过红外对管实时检测输液滴速在屏幕上显示当前滴速和目标滴速当实际滴速偏离设定值时由单片机自动控制蠕动泵电机的转速来闭环调节同时监测输液瓶液位低液位时蜂鸣器分级报警全程按键可设置参数不需要人工一直盯着输液管。适合正在做课程设计、毕业设计或者想把STM32从点灯进阶到完整项目的小伙伴认真把代码和原理图啃下来收获会非常大。我把代码、原理图、仿真工程全部整理成开源包了这篇就把设计思路、硬件电路、软件逻辑、仿真调试过程以及我亲手踩过的坑一次性讲透。1. 项目整体设计与升级点拆解1.1 这套系统到底要解决什么问题输液监护在真实场景里最大的痛点是什么主要就是三个第一输液速度靠人工看滚轮调节医生的处方要求比如“每分钟40滴”实际执行的时候很难精确病人手臂稍微一动滴速就漂了第二液体快输完的时候如果没人发现存在回血风险尤其夜里陪护容易困第三护士站要巡回检查人力成本高。从工程角度拆解这些问题可以转化为几个明确的技术指标实时检测每分钟滴落多少滴精度至少在正负2滴以内能根据设定值自动调整输液速度液位低到阈值时能可靠报警整个系统响应不能太慢滴速一旦偏差就要在几秒内修正回来。这套升级版系统就是冲着这几个目标设计的定位是教学验证和产品预研级原型不是医疗注册设备但信号链路和控制逻辑是完整可迁移的。1.2 从初版到升级版到底升级了什么我最早公开过的初版其实是个“检测显示报警”的半成品红外对管检测滴速OLED显示数字滴速越限了就响蜂鸣器。很多同学跑通之后都会问同一个问题检测到了滴速不对然后呢总不能只报警让护士跑来处理吧。所以这次升级版的核心变化就是从“监护”升级到了“监护调控”。具体来说升级点可以列成这张表每个模块都是针对初版缺陷补上的模块初版能力升级版能力解决的问题滴速检测红外对管直接读IO容易误计数红外对管比较器整形边沿清晰抖动导致滴速乱跳执行机构无蠕动泵直流减速电机PWM调速人工调滚轮不精确控制算法无增量式PID闭环调速滴速自动回归设定值液位监测无电容式液位传感器软件去抖低液位及时报警报警策略单一蜂鸣器多级状态机预警/滴空分级误报漏报数据接口无预留串口/蓝牙透传数据上报可选1.3 系统架构与核心模块选型整体架构我按“感知层—控制层—执行层—交互层”来组织这对后续代码分层很有帮助。感知层包括红外对管滴速传感器和液位传感器控制层是STM32F103C8T6主控负责信号采集、算法运算和状态管理执行层是蠕动泵电机及其驱动电路交互层是OLED显示、按键、蜂鸣器和LED指示。主控选STM32F103C8T6没有悬念蓝色药丸板价格便宜、资料多、标准库和HAL库教程遍地都是中密度Flash 64KB对这套系统完全够用。滴速传感器选槽型红外对管放在茂菲氏滴管位置液滴落下瞬间遮断红外光产生脉冲。执行机构选蠕动泵因为蠕动泵通过滚轮挤压硅胶管输送液体液体不接触泵体干净且流量跟转速近似线性。电机驱动我最后用直流减速电机加PWM方案比步进电机方案便宜用N-MOS管就能驱动。2. 硬件原理图设计详解与关键电路2.1 STM32F103C8T6最小系统与硬件布局原理图里最小系统这部分看起来简单但很多自制板子跑不起来问题往往出在这里。STM32F103C8T6是LQFP48封装最小系统包括8MHz无源晶振作为HSE时钟源两个20pF负载电容NRST复位引脚接10K上拉电阻到3.3V再并联100nF电容到地BOOT0和BOOT1引脚都通过10K电阻下拉到地确保从主Flash启动VCAP引脚接2.2uF陶瓷电容这是内部降压稳压器的输出漏了它芯片根本启动不了。电源去耦是关键中的关键。VDD引脚附近要放100nF瓷片电容VBAT接3.3VVDDA引脚接3.3V并且也要放去耦电容。晶振底下我习惯铺一块完整的GND铜皮并且不在下面走信号线这样可以最大限度减少高频噪声对时钟的干扰。原理图画完后这些细节直接决定PCB打样回来能不能一次跑起来。2.2 液滴检测电路比较器整形与去抖这是整套系统硬件上最重要的一处改动也是初版滴速乱跳的根源。初版把红外接收管的输出直接接到STM32的IO口结果红外对管在液滴遮挡边缘的时候输出信号并不是一个干净的下降沿而是带着一长串抖动单片机一次液滴可能触发四五次中断滴速显示直接翻倍。升级版的做法是红外发射管串一个220Ω限流电阻接收管配合一个10K上拉电阻形成分压结构输出信号送进LM393比较器。比较器的同相输入端接信号反相输入端接一个电位器分压出来的参考阈值同时在比较器输出与同相输入端之间接一个100K到1M的正反馈电阻构成滞回比较器也就是施密特触发器结构。这样信号一旦超过阈值输出立刻翻转低于另一个阈值才会翻回来形成稳定的边沿不再反复抖动。滞回比较器为什么这么有效这里多说一句。红外对管的输出在液滴半遮挡状态时会缓慢变化如果只有一个固定阈值信号会在阈值附近来回穿越输出就出现高频抖动。引入正反馈之后阈值本身被“撑开”成一个窗口信号只有完全越过窗口上沿才翻转回来后必须跌到窗口下沿才复位中间这层缓冲直接把抖动吞掉了。接线时LM393输出加上拉电阻到3.3V再接STM32的PA0PA0配置为EXTI0下降沿中断。2.3 蠕动泵电机驱动方案与电路参数执行机构我选的是12V直流减速电机蠕动泵控制方式为PWM调速。驱动方案没有用L298N那种大模块直接用一颗AO3400 N沟道MOS管搞定。AO3400是逻辑电平MOS栅源电压2.5V就能导通3.3V单片机IO口可以直接驱动不用额外的电平转换这一点是很多人容易踩的坑普通MOS需要10V以上Vgs才能完全导通IO口直驱会导致管子工作在放大区发热严重。具体电路电机正极接12V负极接MOS管的D极S极接GNDG极通过一个10Ω电阻连接STM32的PA6PA6复用为TIM3_CH1输出PWM。电机两端并联一个SS34肖特基二极管做续流方向是阴极接12V、阳极接电机负极防止断电瞬间的反电动势打坏MOS管。电机供电两端再放一个100uF电解电容稳压。PWM频率选择10kHz这个频率下电机运转平稳不会有可闻噪声又远高于机械响应带宽不会出现一卡一卡的现象。占空比范围我设定在20%到80%之间低于20%电机可能转不起来高于80%已经接近最大流量而且可能存在堵转风险软件层做限幅。2.4 电源树与PCB布线注意事项这套系统的电源树是12V输入起步的外部12V适配器进来之后先经过SS34防反接二极管防止电源接反烧板子再经过一颗SMBJ15A TVS管吸收浪涌然后进LM2596降压到5V。5V这路主要给OV7670这种需要5V的器件供电不过我这边5V主要是给逻辑电路和红外对管5V再通过AMS1117-3.3降到3.3V给STM32、OLED、传感器供电。PCB布线上有几个我实测过的经验。第一电机属于强干扰源电机驱动线和滴速传感器信号线要拉开距离最好在两个区域之间铺一条GND分隔带电机线双绞或者套磁环能显著减少辐射干扰。第二红外检测信号线是模拟信号信号走线尽量短直接靠近MCU放置如果板子面积允许红外对管和LM393比较器的GND单独拉一条线回到主GND的星点不要串进数字地环路。第三晶振底下不穿线旁边不走大电流。3. 软件核心代码实现与逻辑拆解3.1 工程目录结构与开发环境搭建软件工程我基于标准外设库StdPeriph构建实测Keil MDK 5环境下编译零警告。工程结构按模块划分不是一股脑塞进main.cProject/ ├── User/ │ ├── main.c │ └── stm32f10x_it.c ├── System/ │ ├── clock_init.c │ ├── gpio_init.c │ └── timer_init.c ├── Driver/ │ ├── oled.c │ ├── key.c │ ├── buzzer.c │ ├── motor.c │ ├── level_sensor.c │ └── drip_sensor.c ├── Middleware/ │ ├── pid.c │ └── state_machine.c └── output/ └── 智能输液监护.bat // 一键编译脚本开发环境用Keil MDK芯片选STM32F103C8Flash下载算法选择STM32F10x Med-density 128K Flash。下载器用ST-Link V2SWD四线接法SWDIO、SWCLK、GND、3.3V必须共地。3.2 滴速采集外部中断加定时器滑动窗口滴速采集是整个控制环路的数据源头这部分做不准后面PID再牛也是白搭。我的方案是外部中断计数加定时器秒中断滑窗。红外比较器输出接PA0配置为EXTI0下降沿中断每次中断滴数计数器加1。要注意的是虽然硬件已经有滞回比较器某些滴速极低或气泡经过时信号边缘还是可能带毛刺我在中断里加了一个简单的时间戳去抖记录上一次中断的SysTick值如果两次中断间隔小于2ms直接丢弃本次计数。也就是最低支持500滴每秒远高于实际需要不影响正常滴速。volatile uint16_t drip_count 0; void EXTI0_IRQHandler(void) { static uint32_t last_tick 0; uint32_t now get_sys_tick_ms(); if (EXTI_GetITStatus(EXTI_Line0) ! RESET) { if (now - last_tick 2) { drip_count; last_tick now; } EXTI_ClearITPendingBit(EXTI_Line0); } }接下来是滴速换算这是这套系统里我反复调过的一个点。如果每秒钟读一次计数然后乘以60短时间窗口下滴速波动会非常大因为液滴本身就不是绝对均匀的。我最开始就是这么做的结果OLED上显示的滴速在38到44之间跳来跳去PID也跟着乱动。后来改成滑动窗口开一个长度为5的环形缓冲区每秒更新一次数据。每次定时器中断就把当前drip_count存入缓冲区然后清零计数器。5秒窗口内的总滴数恰好就是一分钟滴速的近似值因为每分钟滴速等于每5秒滴数乘以12但直接取5秒总值作为“每分钟滴速”的标称值更直观我后续所有逻辑都用这个量。void TIM2_IRQHandler(void) { static uint8_t sec_index 0; static uint16_t sec_buffer[5] {0}; uint32_t sum 0; if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); sec_buffer[sec_index] drip_count; drip_count 0; for (int i 0; i 5; i) { sum sec_buffer[i]; } current_drip_rate sum; // 5秒内总滴数对应“每分钟滴速”的标定量 sec_index (sec_index 1) % 5; } }这样算出来的滴速非常平滑PID收到的是一个稳定的反馈值不会因为单次滴落间隔波动而误动作。3.3 增量式PID调速与PWM闭环控制控制算法我用增量式PID这是嵌入式直流电机调速最成熟的方案。增量式PID的优点是输出的是控制量的增量不会超调积分饱和的问题算法上没有误差累积单片机实现也简单。先初始化PWMTIM3_CH1输出1kHz PWM驱动电机void Motor_PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period 999; TIM_TimeBaseStructure.TIM_Prescaler 71; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 500; TIM_OC1Init(TIM3, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_Cmd(TIM3, ENABLE); }PID部分每1秒执行一次用10秒窗口的平均滴速作为反馈。这里写成简化逻辑每秒把current_drip_rate更新一次后调用PID计算。float target_drip_rate 40.0f; // 目标滴速 float current_drip_rate 0.0f; // 当前滴速 float pid_Kp 0.8f, pid_Ki 0.05f, pid_Kd 0.2f; float prev_err 0, prev2_err 0; uint8_t pwm_duty 50; void PID_Update(void) { float err target_drip_rate - current_drip_rate; float delta pid_Kp * (err - prev_err) pid_Ki * err pid_Kd * (err - 2 * prev_err prev2_err); int16_t new_duty pwm_duty (int16_t)delta; if (new_duty 80) new_duty 80; if (new_duty 20) new_duty 20; pwm_duty (uint8_t)new_duty; TIM_SetCompare1(TIM3, pwm_duty * 10); prev2_err prev_err; prev_err err; }注意TIM_Period设为999所以TIM_SetCompare1(TIM3, pwm_duty * 10)将100%占空比映射到CCR1000。我上面初始化里Pulse500是50%占空比。PID限幅要配合电机特性20%是最低可靠启动占空比80%是限制最高速防止滴速过冲。这个限幅在电机启动阶段尤其重要上电瞬间直接把占空比打到80%电机猛转会把输液管挤坏。还要处理一个特殊情况如果目标滴速大于0但持续10秒检测不到任何滴数说明可能存在堵塞或传感器故障此时不能继续调PID加大占空比必须进入故障状态停电机并报警。这个超时保护逻辑我放在了状态机里。3.4 液位状态检测与报警状态机液位传感器用的是电容式非接触液位传感器贴在输液瓶外壁接近底部的位置。传感器输出数字量高/低电平液体低于传感器位置时输出电平翻转。这个信号虽然简单但直接读GPIO会有抖动问题尤其瓶子晃动时液面波动会造成电平来回跳。我在软件里做了一次数值滤波连续读到10次相同电平才认为是有效状态变化大约对应100ms的稳定时间完美消除了误判。系统整体用状态机管理这是这套软件里最值得学习的设计。状态分别是IDLE待机、RUN运行、LOW_LEVEL低液位预警、EMPTY滴空报警、FAULT故障。状态切换条件我画成一张表当前状态触发条件跳转状态动作IDLE按下开始键RUN启动电机按目标滴速调速RUN液位传感器低电平连续有效LOW_LEVEL蜂鸣器间歇短鸣OLED提示RUN10秒无滴数且目标滴速大于0FAULT停电机蜂鸣器连续鸣叫LOW_LEVEL液位传感器恢复高电平RUN取消报警继续调速LOW_LEVEL滴空信号持续超过30秒EMPTY停电机长鸣FAULT按下复位键IDLE清状态回待机状态机实现用一个枚举变量加一个switch-case即可不涉及复杂操作系统。这部分的工程意义在于把报警逻辑从主循环抽离状态切换清晰后期加新状态也很容易。3.5 显示与按键交互逻辑显示用0.96寸OLEDSSD1306控制芯片I2C接口接在PB8和PB9上。按键一共3个设定键、加键、减键接在PB0、PB1、PB10都配置为上拉输入按键按下时连接GND检测低电平。按键扫描放在主循环里每10ms扫描一次按下后再做20ms延时消抖。UI逻辑分两级界面主界面和设置界面。主界面显示目标滴速、当前滴速、运行时间、系统状态设置界面通过加键和减键调整目标滴速。设定键短按进入设置界面长按2秒保存参数并返回主界面。OLED刷新周期设为200ms一次太快闪烁明显太慢数据滞后。实测下来这个交互逻辑已经够用了但如果想增加更多功能比如修改PID参数也可以在这个框架上扩展设置界面无非就是再加个参数索引。4. 仿真搭建与调试实录4.1 Proteus仿真搭建与信号模拟仿真部分我用Proteus 8.13以上的版本支持STM32F103C8T6模型加载编译生成的Hex文件就能跑。建仿真工程的步骤说几个关键点元件列表里搜STM32F103C8拖进来后双击设好程序文件路径和时钟频率8MHz。原理图里晶振可以不画直接在MCU属性里配置HSE频率但电源网络必须连对VDD、VDDA全部接3.3VVSS接地VCAP接2.2uF电容否则仿真可能直接不运行。我在仿真文件里模拟液滴信号的方法是用一个Pulse Generator脉冲发生器输出接到MCU的PA0引脚脉冲频率就对应滴速。比如要模拟“每分钟40滴”就是40/60约等于0.667Hz脉冲周期1.5秒高电平时间设成100ms。液滴传感器用按钮开关手动模拟实际调试中可以一边跑程序一边按按钮。仿真工程里我提供了两个版本一个用Pulse Generator模拟液滴脉冲方便你测试PID调速另一个加了一个电位器模拟液位传感器信号旋转电位器模拟液位下降。OLED在Proteus里也能仿真不过资源占用较多如果电脑配置一般建议直接用虚拟终端Virtual Terminal打印日志看滴速和占空比足够。4.2 仿真调试技巧与和实物的差异仿真时最大的技巧是善用示波器。可以在PA6引脚挂一个示波器观察PWM波形变化看看PID调节时占空比是否在合理范围变化。调PID参数时直接在代码里修改宏重新编译、重新加载Hex比在实物上改方便得多。仿真和实物之间有几个明显差异需要注意。第一仿真中GPIO翻转速度远慢于真实芯片PWM波形看起来可能不够精细这是仿真器模型限制不用纠结。第二仿真的ADC和中断行为跟实物略有差异传感器的模拟信号在仿真里是理想的而真实红外对管信号需要整形所以仿真跑通只能证明逻辑正确不代表硬件电路没坑这是所有仿真工作的共识。第三OLED仿真比较慢我建议按键交互调试时用LCD1602模型替代等整体逻辑通了再切回OLED减少等待。我自己的排查经验是先在仿真里把状态机跑熟确认每个状态切换条件都正确再烧录到实物。仿真可以帮你把“逻辑bug”全部排掉实物调试时只需要专注于“硬件bug”问题定位范围大大缩小。5. 常见问题与避坑指南5.1 下载、烧录与调试器连接故障这个坑我估计百分之九十的ST-Link用户都遇过Keil里报错“No STM32 Target Found! If your product embeds debug authentication...”说白了就是调试器找不到芯片。我总结三步排查法照着做基本都能解决第一步检查物理连接。SWDIO、SWCLK、GND这条链路必须畅通用万用表量一下STM32的3.3V引脚对地电压是否正常BOOT0必须拉低否则芯片停在系统存储器模式SWD不响应。第二步检查调试器驱动ST-Link在设备管理器里必须识别正常如果出现黄色感叹号重新安装驱动。第三步把连接速率调低在Keil的Settings窗口把SWD Frequency从默认的几兆降到4MHz很多接杜邦线情况下速率过高导致同步失败。如果提示debug authentication通常是芯片的调试接口被某种方式锁住了用STM32CubeProgrammer连接选择“Connect under reset”模式尝试整片擦除后重新下载。这个方法实测能救回来大部分被锁的芯片。5.2 传感器检测异常与电机驱动故障滴速乱跳是老问题了如果你用的还是直接读IO的方案赶紧改比较器电路。硬件已经改了还乱跳检查一下滴速传感器的阈值电压参考阈值设得太接近信号电平就会出现不规律抖动。另外红外对管容易受环境光干扰我给发射管加了遮光罩实测白天强光下计数明显稳定。电机不转或发热严重优先查MOS管驱动。前面提过普通MOS管阈值电压高3.3V的GPIO根本驱动不充分管子处于线性区电机没劲而且管子烫得要命。换成AO3400这类逻辑电平MOS后问题迎刃而解。另外续流二极管不能省没有它电机断电瞬间产生的反电动势尖峰会直接击穿MOS管栅极。蠕动泵转速波动则要查电源。电机启动瞬间电流很大如果5V或12V被拉垮STM32会复位现象就是系统频繁重启。解决方法是给电机单独供电不建议直接用开发板的3.3V给电机供电电源树设计时就要把电机供电和逻辑供电分开。5.3 PID参数整定经验PID参数整定这块我调了很久总结一个简单有效的步骤。首先只保留比例项Ki和Kd都设为0Kp从0.3开始慢慢加观察系统响应。目标滴速从40改成50看当前滴速能不能平稳上升超调是否严重震荡是否明显。然后加积分项Ki从0.01开始加作用是消除稳态误差但如果加太大系统会来回过冲。最后微分项在滴速这种带噪场合作用有限Kd取值要很小甚至可以不启用。我的最终参数是Kp0.8Ki0.05Kd0.2这个参数在我这套蠕动泵设备上响应平稳大约3到5秒内能把滴速调节到位。注意这个参数不能直接照搬到你自己的设备上泵的特性不同延迟不同最好按上面步骤重新整定。红外滴速信号的采样噪声远大于普通温度传感器微分项对噪声敏感这也是Kd不能调大的原因。最后分享一个调PID时的调试技巧通过串口把目标滴速、当前滴速、PWM占空比三个数据实时输出用串口助手记录成CSV文件导入Excel画曲线一眼就能看出系统是过冲还是震荡比盯着OLED屏幕猜快得多。这套系统的后续扩展方向也很多比如加上WiFi模块做远程监控、用FreeRTOS把状态机改成带优先级的多任务、把PID前馈加进去提高响应速度。项目开源包里包含了完整代码、原理图PDF和Proteus仿真工程资料拿到手先对照这篇把原理捋一遍再动手改效率会高很多。