STM32智能输液监护调控系统:从硬件到PID闭环实践
2026/9/7 11:44:10 网站建设 项目流程

说实话,我刚看到这个项目标题的时候,心里第一反应是:终于有人把“输液监护”这种偏医疗的赛道,做成了适合嵌入式学习者直接上手复现的开源方案了。智能输液监护调控系统,乍一听好像是医院里才会出现的设备,但拆开看本质,它就是一个非常典型的单片机闭环控制系统:传感器采集特征信号,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.3V,VDDA引脚接3.3V并且也要放去耦电容。晶振底下我习惯铺一块完整的GND铜皮,并且不在下面走信号线,这样可以最大限度减少高频噪声对时钟的干扰。原理图画完后,这些细节直接决定PCB打样回来能不能一次跑起来。

2.2 液滴检测电路:比较器整形与去抖

这是整套系统硬件上最重要的一处改动,也是初版滴速乱跳的根源。初版把红外接收管的输出直接接到STM32的IO口,结果红外对管在液滴遮挡边缘的时候,输出信号并不是一个干净的下降沿,而是带着一长串抖动,单片机一次液滴可能触发四五次中断,滴速显示直接翻倍。

升级版的做法是:红外发射管串一个220Ω限流电阻,接收管配合一个10K上拉电阻形成分压结构,输出信号送进LM393比较器。比较器的同相输入端接信号,反相输入端接一个电位器分压出来的参考阈值,同时在比较器输出与同相输入端之间接一个100K到1M的正反馈电阻,构成滞回比较器,也就是施密特触发器结构。这样信号一旦超过阈值,输出立刻翻转,低于另一个阈值才会翻回来,形成稳定的边沿,不再反复抖动。

滞回比较器为什么这么有效,这里多说一句。红外对管的输出在液滴半遮挡状态时会缓慢变化,如果只有一个固定阈值,信号会在阈值附近来回穿越,输出就出现高频抖动。引入正反馈之后,阈值本身被“撑开”成一个窗口,信号只有完全越过窗口上沿才翻转,回来后必须跌到窗口下沿才复位,中间这层缓冲直接把抖动吞掉了。接线时,LM393输出加上拉电阻到3.3V,再接STM32的PA0,PA0配置为EXTI0下降沿中断。

2.3 蠕动泵电机驱动方案与电路参数

执行机构我选的是12V直流减速电机蠕动泵,控制方式为PWM调速。驱动方案没有用L298N那种大模块,直接用一颗AO3400 N沟道MOS管搞定。AO3400是逻辑电平MOS,栅源电压2.5V就能导通,3.3V单片机IO口可以直接驱动,不用额外的电平转换,这一点是很多人容易踩的坑,普通MOS需要10V以上Vgs才能完全导通,IO口直驱会导致管子工作在放大区,发热严重。

具体电路:电机正极接12V,负极接MOS管的D极,S极接GND,G极通过一个10Ω电阻连接STM32的PA6,PA6复用为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.c:

Project/ ├── 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,芯片选STM32F103C8,Flash下载算法选择STM32F10x Med-density 128K Flash。下载器用ST-Link V2,SWD四线接法: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的优点是输出的是控制量的增量,不会超调积分饱和的问题,算法上没有误差累积,单片机实现也简单。

先初始化PWM,TIM3_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%占空比映射到CCR=1000。我上面初始化里Pulse=500是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寸OLED,SSD1306控制芯片,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.3V,VSS接地,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都设为0,Kp从0.3开始慢慢加,观察系统响应。目标滴速从40改成50,看当前滴速能不能平稳上升,超调是否严重,震荡是否明显。然后加积分项,Ki从0.01开始加,作用是消除稳态误差,但如果加太大,系统会来回过冲。最后微分项在滴速这种带噪场合作用有限,Kd取值要很小,甚至可以不启用。

我的最终参数是Kp=0.8,Ki=0.05,Kd=0.2,这个参数在我这套蠕动泵设备上响应平稳,大约3到5秒内能把滴速调节到位。注意这个参数不能直接照搬到你自己的设备上,泵的特性不同,延迟不同,最好按上面步骤重新整定。红外滴速信号的采样噪声远大于普通温度传感器,微分项对噪声敏感,这也是Kd不能调大的原因。

最后分享一个调PID时的调试技巧:通过串口把目标滴速、当前滴速、PWM占空比三个数据实时输出,用串口助手记录成CSV文件,导入Excel画曲线,一眼就能看出系统是过冲还是震荡,比盯着OLED屏幕猜快得多。这套系统的后续扩展方向也很多,比如加上WiFi模块做远程监控、用FreeRTOS把状态机改成带优先级的多任务、把PID前馈加进去提高响应速度。项目开源包里包含了完整代码、原理图PDF和Proteus仿真工程,资料拿到手先对照这篇把原理捋一遍,再动手改,效率会高很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询