STM32的开源项目里,医疗电子方向一直关注度很高,智能输液监护调控系统算是这类项目里的常青树。最近我把手上这套升级版彻底整理完了,代码、原理图、仿真文件全部开放,主控用的是STM32F103C8T6,功能上比基础版多了闭环滴速控制、气泡检测、液位检测、分级报警和OLED人机交互,可复现性很强。不管你是正在找毕设方向,还是想给嵌入式简历加一个完整的医疗电子案例,这套工程都值得花几个晚上跑一遍。这篇博文就用来记录我从需求拆解、硬件设计、软件调试到仿真跑通的完整过程,顺带把踩过的坑也交代清楚。
1. 项目整体设计与思路拆解
1.1 升级版到底比基础版强在哪
很多新手做的第一版输液监护系统,通常只有红外对管检测滴速,加一个LCD显示,然后人工手动调滚轮来控制快慢。这种方案做课程设计没问题,但离“监护调控”这四个字其实差得很远:滚轮稍微碰一下,滴速就飘了;液体快输完了没人盯着就容易回血;管路里进了气泡也不能第一时间发现。
这次升级版主要补了三块短板。第一是闭环控制,用步进电机驱动蠕动泵代替人手调滚轮,STM32实时读取滴速,跑PID算法自动修正电机速度,目标设置40滴/分就能稳定维持在40滴/分左右。第二是安全监测,增加了气泡检测、低液位检测、堵管检测,任何一个异常都会触发分级报警。第三是人机交互,OLED屏实时显示目标滴速、当前滴速、累计量和报警状态,按键可以修改参数,整机逻辑用状态机管理,行为可预测、可追溯。
从学习角度来说,这套项目覆盖了GPIO、外部中断、定时器、PWM、ADC、I2C、UART这些STM32的常用外设,还涉及传感器信号调理、执行机构驱动、PID控制算法和嵌入式状态机设计。如果读者只是单纯想把外设API调通,那网上有一大堆demo;但这个项目赚就赚在逼你把这些外设串成一个能闭环工作的系统。
1.2 系统架构拆解:感知、决策、执行三层
整个系统的信号链路可以分成三层,和护士人工监护输液过程做个类比就很好理解。
感知层相当于护士的眼睛。滴速传感器用红外对管卡在莫非氏滴管两侧,液滴下落瞬间遮挡红外光,接收管输出一个脉冲,STM32数脉冲就知道滴速。气泡检测是另一组红外对管,贴在滴壶下方输液管两侧,利用空气和液体对红外光折射率的差异来判断管路里有没有连续气泡。再配合一个称重模块HX711,实时采集输液瓶重量变化,既能估算剩余液量,也能辅助判断是否输液完成。
决策层相当于护士的大脑,也就是STM32F103C8T6。它读取感知层的信号,经过滤波、换算、比较,得出当前状态。状态正常就维持当前控制策略;状态异常就切换报警状态,同时通过蜂鸣器和OLED提示护士介入。
执行层相当于护士的手。STM32输出PWM或者脉冲序列,通过ULN2003驱动28BYJ-48步进电机,电机带动蠕动泵滚轮挤压输液管,从而控制液体流速。注意这里的控制对象是步进电机的转速,转速越快,单位时间内挤压管子的次数越多,输液速度越快。
1.3 技术选型背后的原因
主控选STM32F103C8T6没有太多悬念。这颗芯片已经是开源嵌入式项目的“万金油”,72MHz主频、64KB Flash、20KB RAM,外设资源对这个项目来说完全够用,而且网上资料和学习路线极其成熟。128KB Flash的CBT6也不贵,但C8T6在Proteus里仿真支持更好一些。
滴速检测没用摄像头识别或者超声波方案,因为红外对管方案最稳定也最便宜。难点在于信号调理,后面第2章会详细讲。执行机构选用步进电机而不是直流电机,是因为步进电机可以精确控制转速和转角,开环情况下就能获得不错的重复精度,PID调起来也更容易收敛。蠕动泵是现成的泵头结构,把输液管卡进去就能用,不需要自己设计复杂的流体机构。
为什么不用FreeRTOS?因为单任务循环在这个场景下完全够用,而且裸机逻辑更容易让初学者读懂。把实时性要求高的滴速计数放在中断里处理,其他计算放在主循环里跑,这个分工模式本身就是嵌入式开发的经典套路。
2. 硬件电路与原理图实现
2.1 主控最小系统与供电设计
STM32F103C8T6的最小系统不算复杂,但每一部分都有讲究。供电采用三级结构:外部12V适配器先进LM2596降压到5V,给步进电机和比较器供电,再经AMS1117-3.3线性稳压到3.3V,给主控和传感器供电。电机和逻辑电路分开供电,是为了避免步进电机启动瞬间拉低电压导致单片机复位,这个坑我调试时踩过好几次。
晶振电路很多人是照抄参考设计,但电容取值还是有讲究的。8MHz晶振的负载电容CL通常在12pF到20pF之间,匹配电容计算公式是:
CL = C1 * C2 / (C1 + C2) + Cstray
其中Cstray是PCB走线和引脚寄生电容,经验值3pF到5pF。如果晶振规格书中CL=12pF,取Cstray为4pF,那么C1和C2各取16pF比较合适,实际使用贴片电容没有16pF这个标称值,那就用15pF或者20pF,偏差不大都能正常工作。我的板子上焊的是两个20pF电容,实测串口波特率误差在允许范围内。
复位电路用10k上拉电阻加100nF电容到地,BOOT0引脚通过10k电阻下拉到地,让芯片从Flash启动。下载调试接口引出SWDIO、SWCLK、GND、3V3四根线,一个四针排针就能搞定。这里不建议用JTAG,20针接口浪费引脚空间,SWD两根线足够用。
2.2 滴速检测传感器与信号整形电路
滴速检测的红外对管,发射管和接收管分别安装在滴壶两侧,位置要正对,而且尽量贴近管壁,避免环境光干扰。这个机械定位很关键,我在洞洞板上手动弯脚固定,结果前后两版数据差别很大,后来用3D打印做了一个小卡座才算稳定。
传感器输出的原始信号是接收管两端电压的变化,变化幅度不大、边沿也不陡,直接接单片机引脚会导致外部中断乱触发。我的做法是在传感器后面加一级LM393比较器整形。接收管串联一个电阻把光信号转成电压,接到LM393的同相输入端,反相输入端接一个10k电位器调节阈值。没有液滴遮挡时,接收管受光强、电压高,输出低电平;液滴落下来挡住光路,电压降低,输出翻转为高电平。阈值调在正常光照和完全遮挡的中间位置,抗干扰效果最好。
比较器输出是开漏结构,需要加一个10k上拉电阻到3.3V。如果环境光干扰严重,可以在接收管前面加一片滤光片,或者用脉冲调制方式驱动发射管,让单片机只识别同一个频率的信号,这是工业上常用的抗环境光手段,项目进阶时可以试试。
2.3 步进电机驱动电路设计
执行机构我选择了28BYJ-48步进电机,这是一个四相五线减速电机,减速比1/64,非常适合低速蠕动泵场景,便宜、扭矩够用、控制简单。驱动芯片选ULN2003,本质上就是达林顿晶体管阵列,内部自带续流二极管,可以省掉外部二极管。
接线顺序不能乱。ULN2003的IN1到IN4分别接STM32的PA0到PA3,OUT1到OUT4接电机五线排线的四个相线,电机公共端接5V。驱动方式用四相八拍,也就是半步模式,每个脉冲电机转过半个步距角,配合1/64减速比,最终轴的步进精度足够细腻,蠕动泵出液也更均匀。
PWM并不是电机调速的唯一方式。对于步进电机,更常见的做法是定时器输出固定频率的脉冲串,频率越高电机转得越快。我在代码里就用定时器PWM输出,通过修改PWM频率来控制速度。有人会问为什么不用PWM占空比,因为步进电机需要的是脉冲序列,不是模拟电压,占空比只影响平均电压,不直接改变转速,这个区别新手容易混淆。
电源方面,电机启动瞬间电流可以达到300mA以上,5V的LM2596输出端要并一个470uF电解电容稳压。如果在仿真里跑这个电路,要注意给电机驱动留足够电源余量,不然虚拟环境下也可能出现复位的现象。
2.4 气泡与液位检测电路
气泡检测我用的也是红外对管,但安装位置和信号处理方式和滴速检测不同。滴速检测要的是脉冲边沿,气泡检测要的是长时间的光路变化。当输液管里是液体时,红外光线透过率相对低,接收管输出一个稳定电平;管路里连续通过气泡时,空气对红外光的折射和散射会让接收管电平产生明显跳变,用ADC采样或者比较器输出都能捕捉到。
我在设计时留了两路接口:一路是红外对管接收信号直接进STM32的ADC引脚,软件里做滑动平均值滤波,超过阈值就判定气泡;另一路是进比较器输出数字电平,适合后续扩展。实测下来,ADC成分辨率更高,可以区分小气泡和大气泡,但干扰也更多;数字电平成分稳定,但无法分级。这个项目最终用了ADC方案,整数倍采样后判断是否连续N次超过阈值,再触发报警,防止单个杂散信号误报。
低液位检测最直接的方式是安装一个液位开关,浮球式或者光电式都行。但考虑到输液瓶是耗材,换瓶子就意味着传感器也要跟着动,我加了一路HX711称重方案作为辅助。HX711是24位高精度ADC,直接读取桥式应变片压力传感器的电压差,软件里换算成克数。当重量低于设定的空瓶阈值时,判定输液完成。称重方案的优点是通用性强,不需要改动输液管和瓶体结构,适合做成品原型。
2.5 原理图绘制和布局心得
原理图工具我用的是嘉立创EDA,因为开源项目分享给其他人看更方便,AD或者KiCad也没问题。实际的工程文件里包含了完整原理图,这里讲几个必须注意的细节。
去耦电容是所有硬件工程师反复强调的东西,但新手依然容易忘。STM32每个电源引脚旁边都要放一个100nF陶瓷电容,而且要尽量靠近引脚,否则高速翻转时电源噪声会很大,极端情况下会引起外设误动作。晶振附近不要走高频信号线,尤其是不要和PWM输出线平行走线,否则时钟容易受干扰,串口乱码、定时器计数不准都可能和这个有关。
排针和其他接口的标注要清晰。SWD、滴速传感器、电机接口、电源输入的丝印必须写清楚,不然隔几天自己看了都懵。调试时我喜欢在每个电源域加一个LED电源指示灯,3V3、5V、12V各一颗,上电一眼就能看出哪路没输出。测试点也尽量多留几个,示波器探头可以直接夹。
3. 软件逻辑与关键代码实现
3.1 工程结构与开发环境
开发环境我用的是STM32CubeIDE,配合HAL库。选HAL库而不是标准外设库,主要是新项目上手快,而且CubeMX生成的初始化代码不容易出错。工程按功能模块划分文件,这个习惯对后期维护和二次开发非常重要。
项目根目录下主要模块有:main.c负责主循环和状态机调度,bsp_dripper.c负责滴速传感器和气泡传感器读取,pid.c实现增量式PID计算,motor.c封装步进电机驱动,ui.c管理OLED显示和按键扫描,alarm.c管蜂鸣器和错误状态。
有人可能会觉得裸机工程没必要分这么细,但我自己写了两版之后明确告诉你,模块化能救命。第一版把所有代码堆在一个main.c里,调试气泡检测的时候改了一个变量,滴速显示就开始乱跳,排查了半天发现是全局变量重名了。拆成独立模块之后,每个文件只暴露必要接口,问题边界清楚很多。
3.2 滴速采集:外部中断与滤波算法
滴速采集是整个系统的数据源头。如果滴速值不准,后面的PID再调也是白搭。我用外部中断加定时器实现:滴速传感器整形后的脉冲信号接入STM32的PA4引脚,配置为上升沿触发外部中断;TIM2配置成1秒中断,每个周期统计一次外部中断次数。
核心代码逻辑如下:
volatile uint32_t dripper_tick_count = 0; volatile uint32_t dripper_drips_per_min = 0; void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == DRIPPER_PIN) { dripper_tick_count++; } } void TIM2_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE)) { // 每一秒读取一次计数,换算成“滴/分钟” dripper_drips_per_min = dripper_tick_count * 60; dripper_tick_count = 0; __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE); } }这个原始数据直接拿来控制电机是不够稳的。实测中液滴下落偶尔会出现连续两滴靠得很近,或者一滴被管道边缘弹开,导致脉冲间隔不均匀,瞬时滴速能跳10滴/分以上。我做了两级处理:先对原始滴速做中值滤波,连续取5次数据排序后取中间值,滤掉毛刺;再把中值结果做滑动平均,窗口长度取4,让显示和控制曲线都平滑下来。
堵管检测也是在这一层完成。如果连续超过10秒没有检测到滴速脉冲,同时电机还在运行,就认为管路堵塞,系统进入堵管报警状态。这个检测逻辑放在主循环里就行,不需要额外定时器。
3.3 滴速闭环控制:从手调到PID
开环控制把电机设定在一个固定转速,然后祈祷滴速稳定,这在实验室里能凑合,稍微有点外部扰动就会拉胯。比如液面高度变化、输液管被压了一下、温度变化导致液体黏度改变,都会影响实际流速。反馈控制才是这个项目的正路。
PID控制的目标非常明确:让当前滴速稳定在目标滴速附近。控制量是步进电机的PWM频率。我把增量式PID写成了独立模块,方便参数整定和移植:
typedef struct { float kp; float ki; float kd; float target; float last_error; float integral; float output; } PID_t; void PID_Calculate(PID_t *pid, float current) { float error = pid->target - current; float proportional = pid->kp * error; pid->integral += error; float integral = pid->ki * pid->integral; float derivative = pid->kd * (error - pid->last_error); float output = proportional + integral + derivative; // 输出限幅,防止电机频率过高或过低 if (output > 2000) output = 2000; if (output < 100) output = 100; // 积分限幅,防止长时间饱和 if (pid->integral > 5000) pid->integral = 5000; if (pid->integral < -5000) pid->integral = -5000; pid->last_error = error; pid->output = output; }参数整定建议按先比例、后积分、再微分的顺序来。先把Ki和Kd设为0,只调Kp,观察滴速围绕目标值振荡的情况;Kp合适后加入Ki消除稳态误差,此时要注意系统可能开始周期振荡,需要适当减小Kp;Kd在这个项目里一般不加大,因为滴速信号本身已经做了滤波,微分项放大了噪声反而容易引起电机抖动,我的实际参数是Kp=0.8,Ki=0.05,Kd=0。
控制上还有两个细节值得一提。一是目标滴速变化时不要直接跳到新值,可以做一个斜坡函数,让目标值每秒靠近设定值一定量,这样系统不容易超调;二是电机启动初期,滴速还没建立起来,PID会大比例输出,容易导致流速冲过头,所以启动后前两秒我做了开环缓启动,等第一个滴速脉冲稳定出现后再切入闭环。
3.4 人机交互与报警状态机
系统有五个按键:启动、暂停、目标滴速+、目标滴速-、报警复位。OLED左侧显示目标滴速和当前滴速,右侧显示累计量、剩余量和系统状态。一屏装不下,所以做了一级菜单循环,按键长按进入参数调节界面,短按切换显示项。
整机逻辑用状态机管理,这是我认为这版项目最值得学习的部分。状态一共六个:IDLE待机、RUNNING正常运行、PAUSED暂停、ALARM_BUBBLE气泡报警、ALARM_EMPTY输液完成报警、ALARM_OCLUSION堵管报警。
主循环里对滴速、气泡检测、称重数据每个周期做一次判断,比如当前处于RUNNING状态时检测到气泡信号,就立即停止电机、置位蜂鸣器、切换状态到ALARM_BUBBLE,同时OLED显示报警信息。等护士处理完,按键触发复位,系统先回到PAUSED状态,再按启动才继续运行。使用状态机的好处是行为可预测,不会出现“这个报警为什么自己消失了”这种诡异问题。代码里状态迁移都集中在state_machine.c中,逻辑一目了然。
4. 仿真搭建与系统联调
4.1 Proteus仿真环境搭建
很多人以为这个项目一定要有实物才能做,其实Proteus仿真把大部分功能都验证得很充分。仿真工程里用了STM32F103C8T6的Proteus模型,配合LM393比较器、ULN2003、OLED模型和步进电机模型。
滴速传感器的仿真可以简化处理。因为Proteus里没有现成的红外对管滴速传感器模型,我用一个脉冲发生器VPULSE接到比较器输入端来模拟液滴信号。VPULSE的频率对应滴速,比如要模拟60滴/分,就设置频率为1Hz的方波,占空比30%。这样CPU看到的输入信号和真实红外传感器整形后的输出几乎没有区别,可以用来验证滴速计算和PID控制逻辑。
OLED在Proteus里有仿真模型,用I2C接口连接就能显示内容。如果Proteus版本比较旧没有这个模型,也可以用虚拟终端+串口输出代替显示功能,效果一样能确认逻辑正确。我习惯两种方式同时跑,OLED看界面,串口看调试数据。
4.2 仿真信号调试与虚拟仪表
仿真的价值在于可以精确控制输入,这在实物测试里反而不容易做到。我可以把VPULSE频率设成0.8Hz、1Hz、1.5Hz,分别对应48滴/分、60滴/分、90滴/分,观察系统在PID调节下的收敛过程。
这里要提醒一点:仿真里的AD采样、比较器、电机模型都是理想化的,没有真实环境中的噪声和延迟,所以仿真中表现良好的PID参数,实物上未必能直接用。比较稳妥的做法是,先用仿真把代码逻辑、状态迁移、显示内容全部验证好,再在实物上进行第二轮整定。我遇到过不少人和我抱怨“仿真跑得好好的,实物完全不对”,多半就是因为把仿真结果当成了实物预期。
虚拟示波器是个好帮手。把TIM2输出的PWM引脚用Probe拉进虚拟示波器,可以直观看到电机速度变化,也就是PID控制量的变化,比看数字调试信息直观很多。步进电机模型的转速也可以用电压表估算,但精度有限,建议以串口打印的滴速数据为准。
4.3 从仿真到实物:联调顺序与测试数据
从仿真转到实物时,我强烈建议按“先小后大、先开环后闭环”的顺序来。第一步只下载一个LED闪烁程序,确认最小系统、SWD和供电正常;第二步点亮OLED并扫描按键,确认外设I2C正常;第三步接上滴速传感器,用示波器看整形后波形,确认中断能触发;第四步用开环PWM驱动电机,确认转向和转速范围;全部通过后才把PID闭合起来。
下面是实测的一组数据,环境温度25度左右,输液管是普通一次性输液器,液体是纯净水,目标滴速分别设了40、60、80滴/分,等待系统稳定2分钟后记录:
| 目标滴速(滴/分) | 当前滴速均值(滴/分) | 最大偏差(滴/分) | 稳定时间(秒) |
|---|---|---|---|
| 40 | 39.8 | 1.2 | 12 |
| 60 | 60.3 | 1.8 | 15 |
| 80 | 79.6 | 2.5 | 18 |
可以看到目标滴速越高,误差波动越明显,这和蠕动泵在高速转动时出液脉冲叠加效应有关系,属于正常现象。如果偏差超过3滴/分,我会优先检查滴速传感器的安装是否移位,其次再考虑PID参数。
5. 常见问题与排查技巧实录
5.1 滴速值跳变、显示不稳定
新用户反馈最多的是OLED上滴速数字一直跳,哪怕电机没开也在乱跳。这个问题的根源通常不是算法,而是传感器信号质量。先拿示波器探头夹在比较器输出引脚上,看波形是不是干净的标准方波。如果上升沿有一长串抖动,说明阈值电压设置得太靠近信号中间区域的缓慢变化部分,调整电位器让阈值尽量靠近“完全遮挡”时的电压值。如果信号本身没问题,再检查传感器安装是否松动,液滴是否每次都落在红外光路的正中间。软件侧的滤波只是补救手段,不是根治手段。
5.2 步进电机抖动、失步、噪声大
电机抖动通常有三个原因。一是PWM频率太低,28BYJ-48在频率低于100Hz时会明显一顿一顿,至少设到500Hz以上;二是输出限幅设置得太窄,PID输出顶到上限后电机转速跟不上;三是电源带载能力不够,电机一启动就把5V拉垮,驱动芯片逻辑混乱,这种情况用示波器看5V电源波形就能确认。失步问题则和加减速有关,目标转速突变时电机丢步,所以我在代码里做了斜坡启动,从80Hz起每100ms增加20Hz,实测对减少失步很有帮助。
5.3 仿真与实物表现不一致
仿真和实物不一致是嵌入式开发的常态,不要慌。最常见的差异来源有三个:传感器模型缺失、电源模型理想化、时间基准不精确。仿真中我用VPULSE模拟滴速,现实中红外传感器有边沿抖动;仿真中LED等效代替步进电机,现实中电机有惯性、启动电流和振动。面对差异时不要急着改PID参数,先逐一确认采集进来的数据是否和实际物理量一致,数据链路没问题了再谈控制效果。
5.4 ST-LINK下载失败、提示no stm32 target found
这个报错在STM32开发里太经典了。先用万用表量一下目标板3.3V是否正常,然后确认SWDIO、SWCLK、GND三根线没有接反。如果线序没问题,检查BOOT0引脚是不是被意外拉高了,BOOT0为高电平时芯片进入系统存储器模式,SWD是连不上的。还有一个容易忽略的原因是目标板供电不足,ST-LINK的3.3V输出能力有限,如果板子外围器件太多,就要外接独立电源。最后,如果以上都不行,按住复位键点击下载,在下载瞬间松开复位键,有时候能让芯片复位时序对上。
5.5 常见问题速查表
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| 滴速数字乱跳 | 传感器阈值不合适、安装偏移、环境光干扰 | 示波器观察比较器输出波形,调整阈值电位器 |
| OLED不显示 | I2C地址错误、接线松脱、初始化时序不对 | 用I2C扫描代码检查地址,确认SCL/SDA上拉电阻 |
| 电机不转 | 驱动芯片供电异常、控制引脚接错、PWM未输出 | 检查ULN2003输入输出电平,用示波器看PWM |
| 电机抖动失步 | PWM频率太低、电源掉电、限幅过窄 | 提高PWM频率,增大电源电容,调整PID输出范围 |
| 气泡传感器误报 | ADC采样毛刺、阈值太灵敏、气泡排完但标志未清除 | 增加连续计数判断,调整阈值,检查复位逻辑 |
| 系统反复复位 | 电源跌落、看门狗超时、晶振起振不良 | 示波器看电源波形,检查NVIC配置,更换晶振电容 |
| 报警一直响无法复位 | 状态机卡死、按键消抖失败、条件未解除 | 串口打印状态值,逐个状态排查迁移条件 |
6. 开源资料使用与二次开发建议
拿到这套开源工程后,建议不要直接烧录就跑,那样学到的内容会少很多。第一步先把原理图从头到尾对照看一遍,不懂的元件去查数据手册,把每个芯片为什么在那里、为什么接这些电阻电容搞清楚。第二步自己画一遍最小系统板,哪怕不制板,只是在软件里排一下版也能帮你加深理解。第三步才是下载验证核心功能,并用调试器观察滴速采集和PID控制的中间变量。
如果你手里没有实物,仿真工程也能把这个项目跑通大半,但要注意在Proteus里调好的PID参数到了实物大概率要重新整定。如果你打算在此基础上做更完整的产品原型,建议优先考虑三个方向:一是换用无刷电机或者直流电机加编码器方案,提高长时运行可靠性,蠕动泵在24小时连续工作时对电机寿命要求比较高;二是用低功耗单片机加OLED休眠机制,把整机功耗降下来,方便电池供电;三是增加无线通信模块,把报警信息推送到护士站,但这会引入安全风险和协议设计问题,需要更多思考。
最后再分享一点个人体会。这个项目我前后改了三个版本,最深的感受是:不要一上来就追求“高级”。第一版我也想过直接用摄像头的机器视觉识别液滴,用WiFi模块上云管理,结果被硬件稳定性和外设调试耗掉了大量时间。后来回归到红外对管加步进电机、裸机状态机这套最朴素的方案,功能和稳定性反而快速达标。嵌入式项目的难点从来不是哪个外设不会用,而是当所有外设组合在一起时,你还能不能清晰地分析和解决问题。这套系统即使不做任何扩展,把每一个模块的“为什么”都搞清楚,收获也比泛泛刷十个demo大得多。