ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32智能输液监护系统:PID闭环控制与Proteus仿真全开源

STM32智能输液监护系统:PID闭环控制与Proteus仿真全开源 刚做完一版带仿真和上位机联调的智能输液监护调控系统把代码、原理图和Proteus仿真整个打包开源了。这项目其实是我之前那个基础版的完全重构不是小修小补而是把整个控制逻辑、硬件架构、异常处理机制都重写了一遍。现在把整个设计思路、踩坑经历、还有关键代码逻辑全部捋一遍给后面要做类似项目的朋友一个完整的参考。1. 为什么重做这个输液监护系统旧版的痛点和升级目标先说背景。我之前那版输液系统其实已经能跑基本功能都有——滴速检测、阈值报警、OLED显示。但真正拿到医院场景下去想问题就暴露出来了第一滴速控制是开环的。电机转多快滴速就多少但液体粘稠度变化、输液架高度差、病人手臂活动导致针头位置变动都会让实际滴速偏离设定值。旧版完全没有反馈调节机制报警了只能人工处理。第二异常检测太粗糙。就一个堵管报警气泡、液面低、脱针这些情况全都靠阈值猜误报率很高护士被折腾得不行。第三没有数据记录和远程监控接口。输液过程中的滴速曲线、报警记录都没存想复盘某个时间点发生了什么很难。所以这次升级版定下的核心目标就是四条闭环控制用红外对管实时检测滴速PID调节蠕动泵转速让实际滴速始终咬住设定值。多级异常检测堵管、气泡、液面低、输液完成、脱针五种异常情况独立检测分级报警。数据可追溯通过串口上报所有运行参数到上位机上位机保存数据曲线方便后期分析。仿真可复现完整Proteus仿真工程不需要实物硬件也能跑通整个控制逻辑适合没条件打板的人学习和验证。整篇文章我会按硬件设计、软件架构、PID控制、仿真搭建、实测调参这条线走最后把开源包里每个文件是干嘛的、拿到手怎么跑起来都说清楚。2. 硬件架构与原理图设计关键器件选型和电路细节2.1 主控选型与引脚规划主控用的是STM32F103C8T6蓝色药丸板最常见那颗芯片。选它的原因很简单性价比高、资料多、72MHz主频完全够用而且F1系列的老老实实开定时器、ADC、串口这些外设特别稳不会像某些新系列那样寄存器配置各种坑。引脚规划我直接列个表方便你对照原理图看功能模块引脚配置说明红外对管滴速传感器PA0外部中断输入上升沿触发蠕动泵PWM控制PA1TIM2_CH220kHz PWM舵机夹管机构PA2TIM2_CH350Hz PWM液面低检测传感器PA3ADC1_IN3模拟量输入气泡检测传感器PB5外部中断输入带滤波压力传感器(堵管检测)PA4ADC1_IN4模拟量输入OLED I2CPB6/PB7硬件I2C1按键PB0-PB3上拉输入带软件消抖蜂鸣器PB4推挽输出三极管驱动串口1PA9/PA10115200-8-N-1对接上位机这个引脚规划最关键的考虑是中断优先级布局。红外对管滴速检测是核心信号优先级必须最高我在NVIC里给了抢占优先级0气泡检测给抢占优先级1串口接收给抢占优先级2。这样即使串口数据再频繁也不会干扰滴速计数。2.2 滴速检测电路红外对管的关键坑滴速检测是整个系统的眼睛这部分的电路设计直接决定检测可靠性。我用的是一对红外发射管和接收管卡在滴壶两侧。液体滴落时红外光被折射接收管电平翻转。实际画原理图时有三个坑要提醒坑一红外发射管必须限流。我实测过直接接3.3V供电红外管电流能冲到50mA以上虽然不会马上烧但寿命急剧缩短。串一个220Ω限流电阻把电流控制在15mA左右最稳这样发射功率足够让接收管可靠翻转。坑二接收管输出要加施密特触发器整形。如果不加滴落瞬间波形会有严重的毛刺和抖动一个水滴可能触发三四次中断滴速直接翻倍。我在接收管后面加了一级74HC14施密特反相器效果立竿见影波形干净得多了。如果你不想加芯片也可以在软件里做10ms的窗口滤波但效果不如硬件整形。坑三红外对管很容易受环境光干扰。尤其是医院的强光灯和太阳光里面红外分量很重会让接收管一直导通或者一直截止信号彻底失效。解决思路是两个一是发射管用38kHz载波调制接收端用一体化红外接收头像遥控器那样这个方案最抗干扰但电路复杂二是在发射管和接收管外面套黑色热缩管只留一个狭缝让光束穿过物理隔绝环境光。我选的是第二个方案成本低效果好实际测试在强光下也能稳定检测。2.3 蠕动泵驱动PWM调速与电机保护蠕动泵的驱动我用的是一颗AO3400 N-MOS管加续流二极管低速PWM控制。为什么不用现成的电机驱动模块因为蠕动泵的电流不大正常工作在200mA以内堵转也就400mA左右AO3400的持续电流能力是5.7A余量非常充足。而且P-MOS/N-MOS方案只需要一个PWM引脚就能驱动省IO。马达两端要反向并联一个SS34肖特基二极管做续流不然PWM关断瞬间的反向电动势会打坏MOS管。这属于基础操作了但我见过真的有人省这颗二极管然后MOS管一个月烧三颗的。另外蠕动泵电机是空心杯直流电机堵转的时候电流飙升但没有立刻烧毁可长时间堵转会发热时间久了电刷磨损很严重。所以在软件里我做了堵转保护——连续10秒检测滴速为零且泵在运行就判定为堵管自动断电并报警。这个逻辑我在后面软件部分详细说。2.4 传感器选型和布局气泡、液面、压力检测升级版一共加了三个传感器这里逐个说清楚选型和电路思路。气泡检测用的是一对红外对管卡在输液管上。当管路里有气泡通过时红外透射率变化接收管输出一个短暂的脉冲。这个原理和滴速检测一样但难点在于气泡走的很快脉冲宽度可能只有几毫秒所以检测引脚必须支持外部中断并且中断服务程序要足够短不能在里面做复杂处理。我只在中断里置一个标志位具体的计数逻辑放在主循环里处理。不过这里要提醒一下输液管里极小气泡直径小于管径1/3是检测不到的这是物理极限不要指望电路能解决。气泡检测只能拦截大气泡和连续小气泡这也是我在项目文档里特意注明的。液面低检测没有用浮子开关那玩意儿机械结构容易卡死。我用的是贴在储液瓶侧壁的电容式液位传感器输出模拟量通过ADC采样判断液位高度。当液位低于设定位置时传感器输出的电压会有一个明显的跳变软件里做一个两段式判断电压跳变且持续3秒以上才确认为液面低避免输液过程中碰触瓶子导致误报。堵管检测用的是薄膜压力传感器贴在输液管靠近针头的一端。这里有个设计细节要注意压力传感器必须靠近病人端因为堵管发生时压力变化最明显的是针头附近而不是滴壶附近。我把压力传感器通过一个LM358运放做了放大放大倍数10倍输出接ADC。堵管时压力会缓慢上升软件里检测到压力持续上升且滴速降为零就判定为堵管。3. 系统软件架构状态机设计、数据流与任务调度3.1 总任务框架前后台系统 时间片轮询整个软件我用的是前后台架构——中断负责紧急事件主循环负责业务逻辑。没上RTOS因为这个项目业务逻辑虽然多但都是顺序性的用状态机完全能管理好上RTOS反而增加学习成本和调试难度。主循环里的任务调度用了一个简单的定时器时间片轮询SysTick定时器设成1ms中断一次每个中断加1ms计数然后主循环里判断各个任务的周期标志。具体任务划分如下任务周期说明滴速计算任务500ms根据滴速计数值计算实时滴速并做滑动平均PID调节任务500ms滴速偏差计算、PID计算、输出PWM占空比异常检测任务200ms气泡/堵管/液面/输液完成检测与状态切换OLED刷新任务200ms刷新显示内容带滚动翻页串口上报任务500ms上报滴速、设定值、运行状态、报警信息按键扫描任务20ms按键消抖、短按/长按识别这个时间片轮询的架构是裸机开发的经典做法好处是任务之间不会互相阻塞坏处是任务多了以后扩展性受限。但对于这个项目规模完全够用。3.2 状态机的设计与状态转移条件系统的核心逻辑是一个五状态状态机这是整个软件的灵魂RUNNING 正常运行滴速闭环控制 STANDBY 待机等待启动指令 ALARM 报警状态停止输液蜂鸣器响 PAUSED 暂停状态保持当前滴速设定停止泵 FINISHED 输液完成自动停止状态转移的条件我整理成了一张表当前状态触发条件下一状态任何状态长按启动/停止按键STANDBYSTANDBY短按启动键且参数正常RUNNINGRUNNING滴速偏差超限持续30秒ALARMRUNNING检测到气泡ALARMRUNNING检测到堵管ALARMRUNNING液面低且液量小于5mlALARMRUNNING累计液滴数达到设定总量FINISHEDALARM短按确认键故障已排除STANDBYRUNNING短按暂停键PAUSEDPAUSED短按启动键RUNNING这里有一个重要的设计理念——所有异常都回到STANDBY而不是直接回到RUNNING。目的是强制操作人员确认报警原因并排除故障之后才能重新启动输液避免因为自动恢复导致误判风险。医疗设备不比普通消费电子产品安全冗余宁可多不能少。3.3 滴速计算与软件滤波算法滴速计算看起来简单其实这里有个精度问题。如果直接数1分钟内的脉冲数那响应太慢了病人滴速从60掉到30你得等30秒才能发现。所以我的做法是滑动窗口计数法// 每500ms读取一次滴速计数值 // 实际滴速 2秒内的滴数 × 30换算成滴/分钟 uint16_t get_RPM(void) { static uint16_t last_count 0; uint16_t current_count drop_count; uint16_t diff current_count - last_count; last_count current_count; // 每500ms采样一次diff 半秒内的滴数 // 滴速(d/min) diff * 2 * 60 / 60 diff * 2 // 但是为确保稳定做3次滑动平均 return (diff * 2 avg_buf[0] avg_buf[1]) / 3; }这段代码里最关键的是avg_buf滑动平均缓冲区的设计。我存了最近三次的计算结果取平均来平滑掉因为水滴偶尔粘连导致的波动。但注意这个滑动平均不能做得太长如果平均窗口超过10秒PID控制就会出现明显滞后系统会变得迟钝甚至震荡。我实测下来的最优窗口是3-5次采样的平均。另一个容易忽略的细节是滴速计数的防抖。红外对管检测到一滴水时输出的脉冲宽度大概在1-2ms左右但有时候水滴在滴壶里弹跳会瞬间产生二次信号。我处理的方法是在中断服务程序里做一个静态的屏蔽时间戳void EXTI0_IRQHandler(void) { uint32_t now HAL_GetTick(); if ((now - last_drop_tick) 30) { // 30ms防抖窗口 drop_count; last_drop_tick now; } __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0); }30ms的屏蔽窗口意味着最大检测滴速是2000滴/分钟远超实际需求的200滴/分钟上限不会造成漏检同时又能有效滤掉弹跳信号。3.4 串口通信协议怎么和上位机优雅地对接数据上报这块我设计了一个很轻量的帧格式没有用什么复杂的协议栈就是一个帧头加数据区加校验帧头(0xAA 0x55) | 数据长度(1字节) | 数据区 | CRC8校验(1字节)数据区里打包了当前时间戳、设定滴速、实际滴速、PID输出值、运行状态、异常类型这六个关键数据。为什么用CRC8而不是累加和因为串口通信的干扰一般不会太大但医疗场景下偶尔的误码可能导致报警信息被错误传输CRC8的计算量也不大STM32F103跑起来毫无压力。上位机我用的Python PyQt5 pyqtgraph串口数据解析之后实时画三条曲线——设定滴速曲线、实际滴速曲线、PID输出占空比曲线。这样PID的调节过程就能可视化看到调参直接看曲线形状比看数字直观太多了。这套上位机的代码我也放进开源包里了。4. 闭环控制逻辑核心增量式PID手把手推导和参数整定4.1 为什么输液泵必须用增量式PID这次升级版最核心的变化就是从开环控制变成了闭环PID控制。我先说结论增量式PID比位置式PID更适合这个场景。对比一下两种算法的输出差异位置式PID的输出是控制量的绝对值也就是说PWM占空比直接就是个0到100的数。增量式PID的输出是控制量的增量也就是说这次和上次相比占空比变化了多少是一个delta值。增量式的核心优势在于——它不需要累加历史误差不会因为积分项累积而导致大幅的超调或震荡。对蠕动泵来说还有另外一层原因蠕动泵的转速和PWM占空比在一定范围内基本线性但过了线性区以后死区和饱和区非常明显。增量式PID天然带有抗饱和能力输出超过范围就直接削顶回来时因为没有历史积分包袱恢复也快。4.2 增量式PID公式推导和代码实现增量式PID的公式推导可以从位置式出发。位置式PID输出u(k) Kp * e(k) Ki * Σe(i) Kd * (e(k) - e(k-1))然后计算增量Δu(k) u(k) - u(k-1) Kp * (e(k) - e(k-1)) Ki * e(k) Kd * (e(k) - 2*e(k-1) e(k-2))最后实际输出的占空比PWM(k) PWM(k-1) Δu(k)代码实现如下typedef struct { float Kp; float Ki; float Kd; float error_current; float error_last; float error_previous; float output; } PID_HandleTypeDef; float PID_Calc(PID_HandleTypeDef *pid, float target, float actual) { float delta; pid-error_current target - actual; // 增量式计算 delta pid-Kp * (pid-error_current - pid-error_last) pid-Ki * pid-error_current pid-Kd * (pid-error_current - 2 * pid-error_last pid-error_previous); // 输出限幅防止PWM突变 if (delta 20) delta 20; if (delta -20) delta -20; pid-output delta; // 总输出限幅 if (pid-output 100) pid-output 100; if (pid-output 0) pid-output 0; // 更新历史误差 pid-error_previous pid-error_last; pid-error_last pid-error_current; return pid-output; }注意两个限幅的地方delta限幅和总输出限幅。delta限幅是用来防止PWM剧烈变化的试想如果突然掉一滴大水滴速瞬间飙到200PID输出delta可能直接跳50那蠕动泵就会猛地加速一下反而加剧了系统的振荡。所以delta限幅一定要做这在实际调参数过程中非常关键。4.3 调参实战临界比例度法的完整流程PID调参网上有很多理论但真正上手调又是另一回事。我这套系统的调参过程是这样的分享出来给你参考第一步先把Ki和Kd设成0只保留Kp。从很小的值比如0.2开始逐步增大。增大到系统开始出现等幅振荡时记下这个临界增益Kc和振荡周期Tc。我实测下来这个系统的临界增益大概是Kp3.5左右这时滴速会在设定值附近持续振荡转速波动周期大约4秒。第二步根据Ziegler-Nichols经验公式计算初始参数Kp 0.6 * Kc 2.1Ki 1.2 * Kc / Tc 1.05Kd 0.075 * Kc * Tc 1.05第三步在这个初始参数基础上手工微调。我的实际经验是Ziegler-Nichols给出的参数偏激进会有约15%的超调量。对输液系统来说超调意味着滴速冲过头——宁可响应慢一点也不能超调大否则病人受不了。所以我把Kp降到1.8Ki降到0.8Kd保持1.0左右。这样调整下来实际滴速从60滴/分钟到40滴/分钟的阶跃响应稳定时间大约是18秒左右最大超调量只有3滴这在医疗场景下完全可接受。我实测了几组参数整理成表格给你参考参数组KpKiKd稳定时间超调量特点Z-N激进2.11.051.0512秒9滴响应快但超调明显实测优化1.80.81.018秒3滴平衡推荐保守慢速1.20.50.828秒1滴适合新生儿科4.4 抗积分饱和和其他工程细节增量式PID虽然天然抗积分饱和能力比位置式强但我在实际测试中还是遇到了一个问题当系统持续在输出上限运行时比如输液管路堵塞前的阻力增大PWM占空比会一直顶在100%一旦障碍消除系统会以很猛烈的速度冲过设定点。这个问题的根源是积分项已经累积到了很大值。我的解法是给积分分离——当偏差绝对值大于10滴/分钟时Ki直接乘以0来停止积分累加当偏差回到5滴/分钟以内时再恢复积分功能。代码实现起来就一行判断if (fabs(pid-error_current) 10.0f) { // 启用积分项 delta pid-Ki * pid-error_current; }效果非常显著加入积分分离后从堵管恢复到正常输液的过渡过程超调从原来的12滴直接降到了4滴。另外一个细节是PID计算任务的周期不能太短也不能太长。太短了执行频率过高但每次调节量很小控制滞后大太长了调节间隔大中间的过程偏差检测不到。实测下来500ms的调节周期最合适既有足够的时间让蠕动泵响应PWM变化又能及时跟踪滴速变化。5. 异常管理怎么处理五种输液异常分级报警策略5.1 输液完成和液面低不容忽视的边界条件输液完成判断是输液系统的基础功能但这里有个问题——你没法直接知道输液瓶空了只能靠滴速和液面传感器间接判断。我的判断逻辑是双重确认先检测液面低传感器的模拟量电压跳到阈值以上然后在接下来10秒内滴速计数值为0。两个条件同时满足才会判定为输液完成。为什么要双重条件因为输液过程中如果病人活动导致针头脱落或者输液管打折滴速也会降为0但这时候液面低传感器不会触发。如果只检测滴速为0就会把脱针误判成输液完成仍然继续泵送这是很危险的。液面低和输液完成的区别在于——液面低但滴速还在缓慢下降时是即将完成前兆系统会先进入预警状态OLED上显示液面低蜂鸣器鸣叫3秒后停止只有当滴速完全归零以后才进入输液完成状态这时候才允许护士来换药或撤针。5.2 气泡检测毫秒级响应必须前置处理气泡检测的响应速度要求最高因为气泡一旦进入血管时间久了会有气体栓塞的风险。从气泡穿过检测点到进入人体的时间取决于输液管长度和流速以一般输液管的长度和成人输液速度这个时间窗口大概有10-30秒。所以软件层面必须在气泡信号触发后的1秒内完成判定并且停机。实际中断服务程序里我只做了标志位的置位然后200ms周期任务里去读取这个标志位uint8_t bubble_flag 0; // 中断服务函数 void EXTI9_5_IRQHandler(void) { if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_5) ! RESET) { bubble_flag 1; __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_5); } } // 200ms周期任务 void BubbleCheck_Task(void) { if (bubble_flag) { bubble_flag 0; // 连续检测到2次气泡且间隔小于10秒确认为气泡报警 static uint32_t last_bubble_time 0; uint32_t now HAL_GetTick(); if (now - last_bubble_time 10000) { system_state ALARM; alarm_type BUBBLE_ALARM; } last_bubble_time now; } }连续两次气泡才报警的逻辑是为了防止单次误触发。确实存在这种情况——输液管被外力弹了一下或者气泡在检测点附近来回移动会产生一个假的气泡脉冲。连续两次检测到气泡且间隔在10秒内基本可以确认真有气泡通过了。5.3 堵管检测如何用压力变化趋势而不是绝对阈值判断堵管检测我一开始用的是绝对压力阈值压力超过某个值就报警。结果发现不行因为输液管路的堵塞程度不一样压力上升的幅度也不同有时候堵得不够厉害压力只是缓慢上升永远达不到阈值。后来换成了趋势判断每500ms采样一次压力值缓存最近10次的值到环形缓冲里如果压力持续上升且滴速同时降为0就判定为堵管。趋势判断比阈值判断更可靠的原因在于——堵管是一个逐渐变差的过程在压力还没有上升到危险值之前趋势已经暴露了问题。具体实现void BlockCheck_Task(void) { static uint16_t pressure_buffer[10] {0}; static uint8_t index 0; pressure_buffer[index] read_ADC(PRESSURE_ADC_CH); index (index 1) % 10; if (index 0) { // 缓冲区满一轮 uint16_t min_val 4096, max_val 0; for (int i 0; i 10; i) { if (pressure_buffer[i] min_val) min_val pressure_buffer[i]; if (pressure_buffer[i] max_val) max_val pressure_buffer[i]; } // 如果压力持续上升且滴速为0 if ((max_val - min_val) 100 get_RPM() 0) { system_state ALARM; alarm_type BLOCK_ALARM; } } }这里max_val - min_val 100表示10次采样5秒内压力ADC值变化超过100大概对应0.3V的模拟电压变化这个灵敏度实测下来能检测到针头被折叠导致的堵管同时不会被输液过程中的正常压力波动干扰。5.4 分级报警策略蜂鸣器、OLED、串口联动报警不止是蜂鸣器响那么简单我在系统里定义了三级报警策略报警级别事件类型蜂鸣器动作OLED显示串口上报一级紧急气泡、堵管连续长鸣红色报警图标类型立即上报带时间戳二级严重输液完成、脱针间歇鸣叫30秒黄色警告图标类型立即上报三级提醒液面低、滴速偏差超限短鸣3秒蓝色提示数值周期上报累计状态为什么要分级因为不同异常的紧急程度和安全风险完全不一样。气泡和堵管是立刻有生命危险的必须马上响铃输液完成和脱针虽然不能等太久但有几十秒的缓冲窗口液面低和滴速偏差则属于预警性质的给护士一个提醒就好不用惊动整个病区。OLED的显示我做了两个页面一个是实时运行页显示设定滴速/实际滴速/运行状态另一个是报警历史页记录最近5次报警的时间和类型。翻页用短按按键切换长按按键清零记录。6. 仿真环境搭建与实测验证Proteus怎么搭起来、结果怎样6.1 Proteus仿真工程搭建步骤这个项目我给了完整的Proteus仿真文件但很多人拿到手不知道怎么跑起来这里详细说清楚流程和几个最容易出问题的点。第一步Proteus版本要8.9以上。旧版本对STM32F103C8T6的支持不完善经常出现仿真时外设不响应的情况。我在8.9和8.12上分别测试过都能正常运行。第二步把工程里编译好的hex文件加载到STM32芯片上。双击STM32芯片在Program File一栏选择hex文件然后设置Crystal Frequency为8.0MHz。这里有个坑——如果晶振频率没设对TIM定时器的时基就不对滴速计算时间基准全乱PID参数全部失效。第三步仿真里的元器件连接要和原理图一一对应。红外对管我用的Proteus库里的IR发射管和IR接收管液面低传感器用了一个滑动变阻器模拟压力传感器用了一个可变电压源模拟。这些模拟元件的参数都需要和实际硬件行为一致。比如滑动变阻器的位置就是液面位置你拖动它改变电压值就能模拟液面传感器的状态变化。6.2 仿真的局限性和应对措施Proteus仿真有个局限必须说清楚——它不能真实模拟蠕动泵的物理特性。也就是说你在仿真里看到的滴速其实是靠PWM占空比映射出来的虚拟滴速而不是真正有水滴在滴落。这意味着PID参数在仿真里调出来的值放到真实硬件上还需要重新微调。不过仿真也不是没有价值。它在验证软件逻辑正确性方面非常有优势。我把仿真里能测的流程全跑了一遍正常输液流程启动-泵转-滴速达到设定值-稳定运行-达到总量自动停止。整个状态机流转正确OLED显示切换正常。气泡报警流程模拟气泡信号输入系统500ms内进入报警状态蜂鸣器响OLED显示报警类型串口上报数据。输液完成流程滑动变阻器模拟液面低滴速计数值降到0系统进入FINISHED状态。堵管报警流程压力电压源电压缓慢上升滴速信号停止10秒内系统判断为堵管。这些逻辑验证用真机测试需要大量时间和护理资源仿真里一遍就能跑完省事很多。6.3 实测数据分析和性能指标真机实测我搭建了一套完整的测试平台蠕动泵用的是实验室常见的BT100-1L蠕动泵头输液管用的是标准医用输液器测试液体就是生理盐水。以下是几个关键指标的实测数据测试项目设定值实测值偏差滴速稳定精度60滴/分58-62滴/分±3%允许±5%滴速调节时间从一个设定到另一个18秒稳定达标气泡响应时间气泡通过检测点500ms内达标堵管检测时间针头折叠8秒报警达标输液完成误差设定总量500ml实际输液498ml99.6%准确滴速稳定精度的关键指标是±3%以内这个精度在临床护理中完全够用。传统人工调节靠护士用眼睛看秒表数滴速误差能做到±10%就算熟练了。从±10%到±3%这套系统带来的提升是实实在在的。7. 开源资料包结构说明代码怎么看、怎么用、怎么二次开发7.1 文件目录总览整个开源包里包含这些内容我按功能分了几个目录SmartInfusion_Pump/ ├── Firmware/ │ ├── Core/ │ │ ├── Inc/ // 头文件 │ │ └── Src/ // 源文件main.c, gpio.c, tim.c, adc.c, usart.c │ ├── Drivers/ │ │ ├── STM32F1xx_HAL_Driver/ // STM32官方HAL库 │ │ ├── BSP/ // 自定义板级驱动oled.c, motor.c, key.c, beep.c │ │ └── Middleware/ // PID算法、状态机、协议解析 │ ├── MDK-ARM/ // Keil工程文件 │ └── EVAL/ // 编译好的hex文件 ├── Hardware/ │ ├── Schematics/ │ │ ├── Smart_Infusion_Schematic.pdf // PDF原理图 │ │ └── Smart_Infusion_Schematic.SchDoc // Altium Designer源文件 │ └── BOM.xlsx // 元件清单 ├── Simulation/ │ ├── Smart_Infusion_Proteus.pdsprj // Proteus仿真工程 │ └── README.md // 仿真使用说明 ├── HostComputer/ │ ├── PC端监控程序.py // Python上位机 │ └── requirements.txt // Python依赖清单 └── Docs/ ├── 用户手册.pdf └── 项目说明文档.md7.2 代码架构阅读指南整套固件我用的STM32CubeMX生成的HAL库工程然后在这个基础上做了二次封装。建议阅读代码时按这个顺序先从BSP/motor.c开始看这是最底层的硬件驱动就一个函数——根据PWM占空比参数设置TIM2的CCR值逻辑最简单适合先建立对代码风格和命名习惯的感知。然后看Middleware/pid.c这部分我已经在上面详细讲过了代码就是增量式PID标准实现配合pid.h里可以调节的宏定义参数很容易自己改参数做实验。接着看Middleware/state_machine.c这是整个系统逻辑最复杂的一部分。它定义了一个状态机结构体的转移函数所有状态转移条件都在里面集中管理。看这个文件时一定要配合state_machine.h里定义的事件枚举一个事件对应一个转移分支逻辑清爽不容易遗漏。最后看main.c的while(1)循环你会发现主循环非常干净就一句Task_Scheduler();调用。真正干活的时间片轮询函数是Task_Scheduler它按周期调用各个任务的执行函数。这种架构的好处是——你想加一个新任务只需要在任务调度器里加一行调用就行不用大改结构。7.3 如何驱动OLED和按键BSP层封装的思路OLED我用了I2C接口SSD1306驱动芯片128x64分辨率。BSP里封装了三层接口OLED_Clear()——清屏OLED_ShowString()——显示字符串OLED_ShowInt()——显示整数这三层封装已经够用了再往上就是业务层的组合了比如在运行页里拼一个完整显示界面就是调用这几个函数组合。我不建议把UI逻辑和显示驱动混在一起那样代码会很乱。按键部分我做了短按、长按、双击三种识别。短按和长按的区别是靠按下时间来判断的——按下时间小于800ms是短按大于800ms是长按。双击是短按后500ms内再按一次。这三种键值分别对应不同的菜单操作比如短按切换菜单页长按进入设置模式双击执行启动/停止。7.4 二次开发路线建议最后聊一点我对这个项目二次开发的建议给你几个方向做参考想做远程监护的可以在这个系统基础上加一个ESP8266或ESP32模块把串口数据透传到WiFi再接一个MQTT服务器护士站的电脑或者手机小程序就能实时看到所有输液状态。改动量很小串口协议已经留好了只需要在发送端加一层网络转发逻辑。想提升控制精度的可以尝试把PID改成模糊自适应PID。蠕动泵管路的老化会造成流量特性漂移固定参数的PID会慢慢变差。模糊控制不需要精确建模只要根据偏差和偏差变化率的语言规则调整参数就能适应管路老化。这个我在另外一个项目里试过效果确实不错。想走产品化路线的需要注意这个系统的传感器部分目前是分立的模块要做成产品得考虑用集成的传感器模组比如迈瑞和科曼的输液泵上用的都是高度集成的滴速检测模组。另外要把电源管理换成隔离电源设计毕竟医疗设备的电气安全有强制标准。8. 开发过程中踩过的坑和最后的经验总结最后这篇文章收尾我把自己在开发升级版过程中踩过的几个大坑拿出来复盘一下。第一个大坑是红外滴速检测的抖动问题。最初版本我用的是接收管电平变化直接进定时器结果在实验室里测试时滴速显示经常从60瞬间跳到120再跳回60。当时我一度以为是电磁干扰围了几圈屏蔽线都没解决最后用示波器看了波形才发现是信号抖动触发多次中断。这个问题的本质原因是红外发射管和接收管之间存在固定的直流耦合水滴通过时光路连续折射导致接收信号呈波浪形变化过零点多次。解决办法就是我前面说的施密特触发器整形加30ms软件防抖窗口双管齐下才根治。第二个大坑是PID调节方向搞反了。第一次接上闭环控制时PWM占空比增加滴速反而下降系统控制方向整体反了PID输出越大滴速偏差越大直接发散。这个问题的根源是蠕动泵的弹片压紧力比较紧电机在低速段基本不转动或者蠕动管压缩不到位液体根本不流动等PWM占空比超过40%以后才开始有液体流动。这就在低速段形成了一个虚假的转动方向——PWM增加时蠕动泵转子在快速空转但管内的液体因为还没被完全挤压推出而处于停滞状态滴速反而从60降到0。这个坑的教训是任何控制系统开环上去之前一定要先做一个全面的静特性测试——测出PWM在0到100时对应的滴速变化确认单调性之后才能上闭环。第三个大坑是OLED和ADC共享I2C总线导致的干扰。这个比较隐蔽现象是接了OLED之后ADC采样值偶尔会出现连续相同的值导致液面低误判。排查了很久才发现OLED的I2C信号线在刷新显示的瞬间会产生比较大的地线扰动而ADC的参考电源没有做去耦就容易被干扰。解决方法是ADC的VDDA引脚单独接一个10uF钽电容加100nF陶瓷电容去耦并且把I2C通讯的速率从400kHz降到200kHz。用示波器看改动之后ADC采样值的波动幅度直接从±20降到±3。这个项目从设计到开源差不多用了三个月时间中间改了四版原理图重写了两版软件架构。整个做下来我最大的体会是——像输液监护这种和医疗安全相关的系统最难的不是某一个算法或者某一个电路而是怎么把所有功能模块安全地整合在一起。状态机设计、分级报警、双重确认机制这些看似不性感的逻辑反而才是系统的灵魂。希望这份开源资料能给做嵌入式、做医工交叉项目的朋友一些参考。如果后面大家跑仿真或者改代码中遇到问题欢迎在开源社区交流讨论。
返回列表