STM32智能输液监控系统:增量式PID闭环调速设计与Proteus仿真实践
手头这个项目是STM32F103C8T6平台的智能输液监护调控系统升级版代码、原理图、Proteus仿真三件套齐整。这两年陆续有不少人做输液监控类的课设和毕设但多数停在“检测报警”这一步泵能转、滴速能测却没有形成真正的闭环调控。这次我重新画了板子、重写了底层驱动和控制算法把整个系统从“能演示”提升到“能连续跑一两个小时不慌”的状态这里把设计思路和踩过的坑一并记录下来给准备做类似项目的同学当个参考。这套系统的核心价值在于两点一是不光能实时测出当前输液滴速还能根据设定目标速度自动调整蠕动泵转速做到无人工干预下的闭环调节二是对异常情况滴速偏差过大、输液完成、管路堵塞能及时报警并夹紧管路防止空气进入。升级版相对基础版的区别主要在控制层——加入了增量式PID算法同时优化了滴速检测的滤波逻辑人机交互从按键加数码管改成了旋转编码器加OLED屏幕操作手感好了不少。这套资料更适合有STM32基础、想进阶做完整小系统的同学或者做课程设计、毕业设计需要“软硬结合”完整方案的人。1. 项目整体设计与方案选型1.1 升级版要解决的核心问题基础版的输液监护仪通常只做一件事用红外对管卡在莫非氏滴管两侧液滴落下时遮挡红外光产生一个脉冲单片机数脉冲个数再换算成“滴/分钟”。这个方案本身没毛病但实际用起来有几个让人头疼的地方——第一个是环境光干扰白天靠窗的位置红外的误触发率能高到你怀疑人生第二个是患者体位变动或者输液架晃动滴速会突然波动第三个最要命就是它只能“看”不能“治”滴速偏快了或者变慢了只能靠护士手动去滚轮调节。升级版的目标很明确在这个基础上加执行机构用蠕动泵代替人手调节让系统自动把滴速拉回设定值。同时把抗干扰和异常处理做得更扎实不能因为一次误检就乱报警也不能因为病人动了一下就让泵疯狂加速减速。整套系统的功能定位如下实时测量输液滴速精度到1滴/分钟以内支持旋转编码器设定目标滴速范围1~60滴/分钟增量式PID闭环控制蠕动泵自动修正速度偏差OLED实时显示设定速度、实际速度、泵状态和报警信息滴速异常、输液完成、管路堵塞时声光报警并自动夹管掉电保存参数重新上电后恢复上次设置1.2 硬件方案选型思路主控这块没什么悬念选了STM32F103C8T6。这个芯片被用得太多资料全、外设够用定时器、ADC、I2C、USART、PWM都有、价格也稳做这种小系统性能绰绰有余。有人问为什么不用ESP32或者国产替代芯片我觉得在这个项目里没必要——不需要WiFi不需要复杂计算F103的低功耗特性和外设配置已经非常成熟并且Proteus里的仿真模型也是以这个芯片为主方便做软硬件联调。滴速检测方案我对比过几次最终用的还是红外对管加电压比较器。摄像头方案精度高但成本和功耗都上去了超声波方案对液滴这种小目标完全不合适。红外对管的问题是输出信号毛刺多液滴经过检测区域时需要一定的时间阈值响应快了容易把飞溅的水珠也算进去响应慢了又可能漏检。解决办法是前端加一级LM393比较器整形用可调电阻设定阈值电压把红外接收管的模拟信号变成干净的高低电平脉冲再送进单片机的定时器输入捕获引脚。执行机构选了蠕动泵加28BYJ-48步进电机。蠕动泵的好处是液体只接触泵管不接触泵体换管路就能继续用符合医疗场景的基本卫生要求28BYJ-48虽然转速慢、扭矩一般但胜在便宜、控制简单、低速下扭矩还可以配合4相8拍驱动完全能带动输液泵管。如果想升级平滑性可以考虑42步进电机加A4988驱动板但功耗和体积都会明显增加。人机交互这块显示用0.96寸I2C接口OLED四根线就能接显示信息密度比数码管高太多输入用旋转编码器而不是矩阵键盘主要是调节数字时手感好一个旋钮完成加减和确认三个动作代码里只需要一个外部中断加两个GPIO就能解析方向和步数。2. 硬件设计与原理图核心模块拆解2.1 主控最小系统与电源分配原理图画起来主控最小系统是最基础的但也是最容易被忽视的。STM32F103C8T6的BOOT0要接10K下拉电阻到地保证从Flash启动这个引脚悬空状态下偶尔能跑、偶尔起不来排查时极其浪费时间。复位电路用经典的10K上拉加0.1uF电容到地NRST引脚低电平复位。晶振用8MHz无源晶振两个20pF负载电容匹配如果有条件可以加一个1M电阻并联在晶振两端帮助起振。电源部分这次特意做了两个供电回路系统主电源MCU、传感器、OLED、编码器用AMS1117-3.3从5V降压步进电机驱动单独从5V取电。主要原因是我实测时发现28BYJ-48启动瞬间电流能冲到250mA以上如果和MCU共用一条电源轨OLED亮度会闪、红外检测阈值会漂严重时候还会让单片机复位。原理图上这两个回路的滤波电容也分开布局数字地和功率地在电源入口处单点汇合模拟地和数字地用0欧电阻隔离。供电入口加了一个SS34肖特基二极管做防反接容量够大、压降小。电源指示灯选蓝色LED串联1K电阻限流亮度适中不刺眼。这个部分虽然简单但做出来整板的稳定性会有明显提升。2.2 滴速检测电路与信号调理滴速检测是整个系统的感知端这部分做得不好后面控制算法再厉害也白搭。红外发射端我用一个8050三极管驱动红外发射管串联100欧限流电阻发射电流控制在20mA左右。接收端用光敏三极管基极悬空集电极接一个10K电阻到VCC发射极接地液滴经过时遮挡红外光集电极电压会从高电平跳变到低电平。这里注意一点光敏管的响应速度跟负载电阻有关阻值越大输出幅度越大但响应越慢实测10K是个比较合适的折中值。信号调理的核心是LM393比较器。红外接收端输出的模拟信号接到LM393的同相输入端反相输入端接一个10K电位器调节比较阈值。LM393是开漏输出必须接一个10K上拉电阻到3.3V才能输出正常的高电平。我建议把比较器输出的脉冲信号同时引到两个地方一路进STM32的PA0TIM2_CH1做输入捕获另一路留给示波器观察调试时能直观看到波形有没有问题。这里有一个关键的硬件细节光电对管安装位置一定要加遮光罩。滴管两侧各留一个狭缝只让液滴经过的路径上的光线通过否则环境光直接打在接收管上LM393的输出就会一直翻转。我用黑色热缩管套在管子上做了个简易遮光结构效果立竿见影。2.3 步进电机驱动与蠕动泵控制28BYJ-48步进电机是5线4相制驱动板用ULN2003原理图上输入侧接STM32的四个GPIOPA4~PA7输出侧接电机线圈。ULN2003内置续流二极管电机换相瞬间产生的反向电动势会被吸收不需要额外加保护电路。每个输入引脚串一个1K电阻防止上电瞬间GPIO为高电平时电流过大。驱动时序用经典的4相8拍A-AB-B-BC-C-CD-D-DA。8拍模式比4拍模式步距角小一半分辨率更高蠕动泵需要的转速本来就慢8拍模式下转矩波动也小一些。控制周期上我建议PID输出映射到步进脉冲周期间隔而不是直接控制速度这样更平滑。蠕动泵的安装也是个容易忽略的细节。泵管必须卡在泵轮和泵壳之间的轨道上松紧程度直接影响输液精度——太紧电机转不动太松泵管挤压不充分液体流量和电机转速不成比例。第一次装的时候调了快半小时后来总结经验装上泵管后手动转电机感觉有阻尼但能顺畅转动这个松紧度就差不多。2.4 显示、输入与报警电路OLED用的是SSD1306驱动芯片的0.96寸屏I2C地址默认0x3C。I2C的SDA和SCL都要接上拉电阻到3.3VSTM32的PB6和PB7内部上拉也可以用但我还是习惯外部加4.7K上拉实测波形更稳。OLED的供电用3.3V部分型号在5V下也能跑但时间长了容易发烫别图省事直接怼5V。旋转编码器用的是EC11带按键功能。A、B相分别接PB0、PB1配置成外部中断触发模式选双边沿。按键那一相接PB2按下时接地配置成输入下拉加外部中断。编码器的A、B相到MCU之间各加一个100nF电容到地做硬件消抖比纯软件消抖可靠得多而且不占用CPU时间。蜂鸣器用有源蜂鸣器一个NPN三极管驱动GPIO控制三极管通断。有源蜂鸣器内部自带振荡源给高电平就响不用PWM驱动。并联一个SS34二极管防反向电动势——蜂鸣器是感性负载断电瞬间会产生反向高压不接二极管三极管会有击穿风险。3. 软件架构与核心代码实现3.1 程序整体流程与模块划分代码结构上我按功能拆成了几个模块main.c负责初始化调度timer.c管理滴速检测和系统计时motor.c封装步进电机驱动pid.c实现增量式PIDoled.c处理显示encoder.c解析旋转编码器输入alarm.c管理报警状态机。每个模块接口清晰改一个功能不用动其他地方调试起来很省心。系统主流程是典型的“主循环中断”结构。主循环里跑状态机和OLED刷新定时器中断里做高速实时任务——TIM2的输入捕获处理滴速脉冲TIM3产生1ms时基驱动PID计算和电机脉宽更新。关键点在于PID计算放在中断里OLED刷新放在主循环里两者用共享变量通信访问共享变量时暂时关闭中断保护避免数据错乱。状态机是系统的大脑我定义了以下几种运行状态IDLE待机显示系统就绪SETTING设置目标滴速RUNNING运行中PID闭环控制ALERT报警夹管并停机PAUSE暂停运行状态切换由事件驱动比如在IDLE状态短按编码器按键进入SETTING设定完成后长按确认进入RUNNING运行中检测到滴速异常进入ALERT。这种设计比裸奔式的if-else嵌套清晰得多后面加新功能也方便。3.2 滴速检测核心实现滴速检测的核心是靠TIM2的输入捕获功能测量相邻两个液滴脉冲的时间间隔。输入捕获的原理是硬件自动记录定时器计数值不需要CPU逐条指令参与精度很高而且不阻塞主循环。用定时器输入捕获测滴速计算逻辑是这样的定时器时钟72MHz预分频设为72计数频率就是1MHz即每微秒计一个数。液滴脉冲到来时捕获寄存器记下当前计数器的值和上一次捕获值做差得到的就是两个液滴之间的微秒数。滴速换算成“滴/分钟”公式很简单滴速滴/分钟 60,000,000 / 脉冲间隔微秒比如间隔500ms就是60/0.5120滴/分钟这个速度太快了现实中输液一般不超过60滴/分钟所以正常脉冲间隔应该在1秒以上。实际代码里不能只看单次间隔液滴在管口悬挂的过程中有时候会因为表面张力反复抖动产生连续两三个伪脉冲。我的处理办法是连续采集5个脉冲间隔去掉最大值和最小值取中间3个的平均值作为有效数据。这样可以滤掉大部分干扰又不会像纯均值滤波那样把真实的速度变化“抹平”掉。以下是定时器输入捕获初始化代码void TIM2_Capture_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_ICInitTypeDef TIM_ICInitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; // 使能时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); // PA0 配置为浮空输入作为TIM2_CH1输入捕获引脚 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); // 72MHz/72 1MHz计数频率 TIM_TimeBaseStructure.TIM_Period 0xFFFF; TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); // 输入捕获配置上升沿触发直接映射到TI1 TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x0F; // 滤波抗毛刺 TIM_ICInit(TIM2, TIM_ICInitStructure); NVIC_InitStructure.NVIC_IRQChannel TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); TIM_ITConfig(TIM2, TIM_IT_CC1, ENABLE); TIM_Cmd(TIM2, ENABLE); }中断回调里处理捕获数据并做滤波计算uint32_t last_capture 0; uint32_t interval_buffer[5]; uint8_t buffer_index 0; uint32_t filtered_interval 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_CC1) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); uint32_t current_capture TIM_GetCapture1(TIM2); uint32_t interval current_capture - last_capture; last_capture current_capture; // 忽略明显异常的间隔200ms或10s if (interval 200000 || interval 10000000) { return; } interval_buffer[buffer_index] interval; if (buffer_index 5) { buffer_index 0; // 冒泡排序取中间3个的平均值 uint32_t temp[5]; for (int i 0; i 5; i) temp[i] interval_buffer[i]; for (int i 0; i 4; i) { for (int j 0; j 4 - i; j) { if (temp[j] temp[j1]) { uint32_t t temp[j]; temp[j] temp[j1]; temp[j1] t; } } } filtered_interval (temp[1] temp[2] temp[3]) / 3; } } }3.3 增量式PID控制步进电机闭环控制是升级版的核心改动。手动调节输液速度的问题在于人的反应速度跟不上滴速的变化而且调节量完全靠经验。PID控制器的思路就是不断对比实际滴速和目标滴速根据偏差的大小和变化趋势自动调节电机的转速。增量式PID和位置式PID的区别在于它不是直接输出一个绝对的控制量而是输出控制量的增量执行机构只需要在上一次的基础上增加或减少这个增量就行。这样有个好处即便PID参数设置有问题导致输出振荡也不会一下子跳到极端值系统相对安全。增量式PID公式如下ΔU(k) Kp * [e(k) - e(k-1)] Ki * e(k) Kd * [e(k) - 2*e(k-1) e(k-2)]其中e(k)是本次偏差e(k-1)是上次偏差e(k-2)是上上次偏差。每次计算完新的控制量就是上一次的控制量加上这个增量。代码实现如下typedef struct { float Kp; float Ki; float Kd; float target; float current; float error_last; float error_prev; float output; } PID_TypeDef; void PID_Init(PID_TypeDef *pid, float kp, float ki, float kd) { pid-Kp kp; pid-Ki ki; pid-Kd kd; pid-target 40.0f; pid-current 0.0f; pid-error_last 0.0f; pid-error_prev 0.0f; pid-output 0.0f; } float PID_Calculate(PID_TypeDef *pid, float target, float current) { pid-target target; pid-current current; float error target - current; float delta_u pid-Kp * (error - pid-error_last) pid-Ki * error pid-Kd * (error - 2.0f * pid-error_last pid-error_prev); pid-error_prev pid-error_last; pid-error_last error; pid-output delta_u; // 输出限幅防止电机转速超出合理范围 if (pid-output 100.0f) pid-output 100.0f; if (pid-output 0.0f) pid-output 0.0f; return pid-output; }PID参数整定我推荐用“先P后I再D”的顺序。先把Ki和Kd设成0只保留Kp从小到大慢慢加观察滴速的响应曲线——系统开始稳定震荡时记下此时的Kp作为临界增益然后再逐步加Ki消除稳态误差。这个项目里我最后用的参数是Kp0.8Ki0.15Kd1.2实际响应大概需要5到10秒能把滴速稳定在目标值附近。注意这个参数跟你的机械结构、泵管松紧度都有关系换了一根新的泵管或者换了一个电机参数可能需要微调。PID计算周期设成1秒一次。因为滴速的反馈本身要累积1.5秒左右才能算出一次有效值PID算得太快反而会在两次有效反馈之间瞎折腾。控制输出映射到电机上时我做了线性转换PID输出的0~100对应步进电机的脉冲周期从200ms到5ms变化。这样电机随着控制量增大而加速随着控制量减小而减速。3.4 状态机与报警逻辑状态机的实现不复杂关键是报警触发的条件要设计得合理不能误报也不能漏报。我设置了以下几种报警场景滴速偏差超过设定值的20%且持续10秒以上判定为异常3秒内没有检测到任何液滴脉冲判定为输液完成或堵管实测滴速超过60滴/分钟判定为失控报警触发后系统会做两件事一是蜂鸣器间歇鸣叫、OLED图标闪烁二是夹管机构动作——我用一个舵机旋转90度压紧输液管防止空气继续进入静脉。这个安全机制在真实医疗场景里是必备的在实验环境里也体现了一个完整的监护系统该有的逻辑。状态切换的代码结构用了函数指针数组的方式每个状态对应一个处理函数事件发生时查表切换。代码可读性和可扩展性都不错后面想加“预充”或者“排空”模式只需要新增状态和处理函数就行不用改主体逻辑。4. 仿真搭建与调试要点4.1 Proteus仿真电路搭建方法Proteus里搭建这套系统核心思路是用信号发生器来模拟红外对管输出的液滴脉冲。真实硬件中LM393比较器输出的是一串方波脉冲脉冲频率对应滴速在Proteus里我直接用DSEG信号发生器即数字脉冲信号源接到PA0引脚设置脉冲频率来模拟不同滴速。搭仿真电路的时候注意几点首先是STM32F103C8T6的模型在Proteus里选对封装VDD和VSS引脚一定要接电源和地否则程序下载后仿真跑不动其次是OLED在Proteus里有现成模型SSD1306的I2C接口接到PB6和PB7跑起来能直接显示内容第三步进电机和ULN2003在仿真里也有对应的模型可以用虚拟示波器观察电机驱动的时序波形。调滴速检测逻辑的时候我建议把信号发生器的频率设成2Hz左右对应30滴/分钟观察OLED上的显示值是否接近30。如果显示偏大或偏小优先检查TIM2的预分频系数和捕获中断的处理逻辑先确认微秒计数值对不对再往下查滤波算法。4.2 仿真与实际运行的差异提醒仿真跑通了不代表实物就能直接跑这里面的差异我踩了不少坑。Proteus里红外传感器、比较器、电机这些模拟器件的电气特性都是理想化的不存在环境光干扰、触点抖动、电源噪声这些实际难题。所以仿真里的滴速波形干净漂亮代码放到实物上就可能会出现误触发、信号抖动。我在实际调试中遇到的第一个典型问题是比较器的输出在滴速较快时出现毛刺单片机把同一个液滴的上升沿和下降沿都当作有效脉冲导致滴速读数翻了近一倍。排查方法是先用示波器观察比较器输出波形确认波形是不是一个完整干净的低电平脉冲如果波形边沿有毛刺就需要调整比较器阈值电压或者在软件里加大输入捕获的滤波系数。另一个差异是电机启动的机械惯性。仿真里步进电机给脉冲就转、停脉冲就停但实际蠕动泵有管路的弹性和流体的惯性电机停转后泵管会缓慢恢复形状液体还会继续流一会儿。这意味着PID控制的实际响应会有滞后参数需要比仿真里更保守一些否则容易震荡。5. 实测经验与常见问题排查5.1 实测中遇到的高频问题第一类问题是滴速检测不稳定。红外对管装好后我第一天测试时发现滴速读数跳得厉害从设定值40跳到十几又跳回五十几。排查下来有两个原因一是遮光罩没有完全遮住环境光窗户边下午太阳斜射进来会直接打在接收管上二是滴管的固定位置偏了液滴不是垂直经过检测区而是从边缘滑过去导致红外遮挡不彻底。解决方案是把遮光罩加长加严实然后用3D打印做了一个滴管固定座保证滴管垂直插在检测槽里。第二类问题是蠕动泵丢步。28BYJ-48在快速启动或者负载突然变大时会出现丢步现象现象是泵的实际转速比程序设定的慢但程序并不知道还是按照自己的节奏发脉冲PID闭环发现滴速偏慢就继续加速直到控制量输出饱和。丢步的根本原因就是启动频率太高超出了电机的自启动频率范围。解决办法是把步进电机的加速过程做成梯形加速曲线起步时脉冲频率低然后逐步提高避免瞬间高速。第三类问题是OLED刷新导致滴速显示卡顿。一开始我在主循环里每50ms刷新一次OLED结果屏幕闪得不行而且滴速数字刷新时视觉上有明显的跳动。后来把刷新频率降到200ms一次同时只在数值变化超过1时才更新屏幕感官上反而流畅了。OLED显示本身有I2C通信的耗时刷新太频繁会挤占主循环里其他任务的执行时间。5.2 问题排查速查表现象可能原因排查方法解决方案滴速读数跳变/偏大环境光干扰、比较器阈值不当、脉冲滤波不足示波器观察比较器输出波形看是否有毛刺加遮光罩调整比较器阈值电位器增大TIM捕获滤波器系数滴速读数偏小/漏检液滴路径偏移、红外对管灵敏度不足检查滴管是否垂直经过检测区调整滴管固定位置重新对准红外对管电机不转电机驱动引脚接错、电源不足、GPIO配置错误万用表量电机线圈电压检查ULN2003输入电平核对原理图连接检查电源电流是否满足电机启动要求电机丢步/失步启动频率过高、负载过大监视电机是否发出“咔哒”声但不同步增加梯形加速曲线降低启动频率PID输出振荡Kp过大、控制周期太短观察滴速曲线是否等幅振荡降低Kp增加Ki的积分作用适当延长控制周期系统运行中自动重启电源压降过大、看门狗误触发用示波器抓取电源电压实测波形电机单独供电加大电源滤波电容OLED显示乱码/不亮I2C地址不对、上拉电阻缺失用I2C扫描程序枚举设备地址确认OLED地址是0x3C或0x3D补上拉电阻编码器操作反向A/B相定义反了转动旋钮观察位数变化方向软件里交换A/B相极性或将A/B接线对调5.3 升级迭代方向这套系统做到现在这个版本已经能满足基本的监护闭环控制需求。我在测试时也在想后续还能往哪些方向扩展。第一个方向是无线化加一个ESP8266或者蓝牙模块把滴速和报警信息发到手机或护士站这在实际场景里意义很大。第二个方向是输液余量检测用称重传感器或者液位传感器监测输液瓶的重量变化提前预警“快输完了”而不是等到滴管里没有液滴了才报警。第三个方向是数据记录把每次输液的滴速曲线存到SD卡或者Flash里方便事后分析。代码上也有优化空间。目前PID的参数是静态的如果输液过程中管路被压迫或者液体粘稠度变化固定参数可能不够用。可以考虑引入模糊PID或者自适应控制根据偏差大小动态调整PID参数。不过这些属于锦上添花先把基础版本的稳定性做好更重要。最后分享一个我个人的调试习惯任何嵌入式系统先保证“开环能跑”再做“闭环控制”。也就是先手动设置电机转速确认蠕动泵的实际输出和预期一致确认滴速检测读数是准确的然后再把PID加上去。很多人一上来就写完整闭环代码结果出了问题不知道是传感器不准、电机执行不对还是PID参数不对排查起来非常痛苦。从这个项目的经验来看按“检测—执行—控制”的顺序逐层验证调试效率是最高的。