做嵌入式这几年,我见过不少带“智能”二字的开源项目,但很多停在“检测”这一步:读个传感器、显示个数字,就结束了。真正能把检测、闭环控制、报警、上位机通讯串成一条完整链路的,其实不多。这套智能输液监护调控系统升级版,是我自己从零开始搭的一套完整方案,主控用STM32F103C8T6,源代码、原理图和仿真工程全部开源。它解决的是输液场景里最实际的问题:滴速怎么测准、怎么自动调稳、异常怎么报警、数据怎么发出去。如果你正在做课程设计、毕业设计,或者想从“会读传感器”进阶到“会做闭环控制”,这个项目应该能帮你省不少时间。
先声明一点:这套系统的定位是学习与实验验证项目,不是医疗产品,不能直接当临床设备用。但作为电子设计、嵌入式开发和自动控制的学习载体,它的完整度非常高——有光电检测、有步进电机执行机构、有PID闭环、有液晶显示、有串口协议,几乎把一个控制类项目该有的环节都覆盖了。
1. 项目整体设计与升级点拆解
1.1 系统到底在解决什么问题
传统输液依靠护士手动转动滚轮调整流速,滴速漂了不容易发现,患者活动、液体挂高挂低,都会让滴速悄悄变化。这套系统的目标就是把这件“靠人盯”的事自动化。
系统运行流程是这样的:红外对管安装在滴壶两侧,液滴落下时短暂遮挡红外光,传感器产生一个脉冲;STM32通过外部中断统计脉冲并计算实时滴速;用户用按键设定目标滴速后,PID控制器把“目标滴速”和“实测滴速”做比较,输出控制量调速步进电机驱动的蠕动泵;同时液位传感器监测输液瓶剩余量,温度传感器读取液体温度,OLED屏幕显示全部运行参数;一旦滴速偏差过大、液位过低或者出现堵塞,蜂鸣器、LED和上位机同时报警。
这里最关键的还不是“检测”,而是“调控”。很多人做类似项目只做到测量和显示,但升级版加入了步进电机和PID闭环,让滴速真正能自动拉回目标值。这是整个项目技术含量最高、也最适合用来讲清楚控制原理的部分。
1.2 升级版相对基础版改了什么
如果你手上有旧的基础版代码,对比一下就很明显了。
| 功能维度 | 基础版 | 升级版 |
|---|---|---|
| 滴速检测 | 红外对管计数 | 红外对管 + 去抖 + 滑动平均滤波 |
| 滴速控制 | 无,只报警 | 步进电机蠕动泵 + 增量式PID闭环 |
| 液位检测 | 无 | 非接触液位传感器,低位报警 |
| 温度检测 | 无 | 支持DHT11/DS18B20 |
| 人机交互 | LED指示 | OLED显示 + 按键设置目标滴速 |
| 通讯 | 无 | 串口自定义协议,可扩展WiFi模块 |
| 软件结构 | 顺序执行 | 状态机 + 模块化文件,预留RTOS接口 |
| 可靠性 | 基本没有 | 看门狗、按键消抖、数据滤波、异常恢复 |
升级版最大的变化是“从只测不控变成了测控一体”。基础版检测到滴速不对只能叫护士,升级版会自己先把滴速调回来,实在调不回来再报警。这个逻辑上的转变,恰好把自动控制里“反馈”的概念体现得很直观。
1.3 主控选型:为什么还是STM32F103C8T6
有人问,2025年了为什么不用H7、不用国产更高端的芯片?我的回答很简单:这套项目的需求,F103C8T6完全够用,而且资料多到你踩坑都有人垫着。
F103C8T6有64KB Flash、20KB RAM、3个USART、多个定时器、2个ADC,I2C和SPI也齐全。滴速检测用外部中断,电机控制用定时器PWM或者GPIO脉冲,显示用I2C,通信用USART,所有外设正好被利用起来。而且这颗芯片的购买成本很低,开发板、核心板、最小系统板遍地都是,教学视频和示例代码多,对刚接触STM32的人非常友好。
如果以后想升级彩色大屏、跑LVGL或者更复杂的GUI,再考虑STM32F407或者F429不迟。选型要跟着需求走,不是越贵越好。
2. 硬件设计与原理图要点
2.1 主控最小系统与电源电路
原理图是整个项目的地基。我见过太多照着网上模板抄原理图,最后板子跑不起来的情况,问题往往出在最基础的最小系统上。
STM32F103C8T6最小系统包含几部分:
- 8MHz无源晶振 + 两个负载电容。电容具体取多大,用公式算一下心里就有底了:外接电容C = 2 × 晶振负载电容CL - PCB杂散电容Cstray。比如某颗晶振的CL是12pF,PCB杂散电容按3~5pF估算,C = 2×12 - 4 = 20pF,取两个20pF就对了。如果CL是18pF,算下来要30~33pF。网上很多人直接抄20pF,只能说在多数情况下能用,但严格按公式来更稳妥。
- VCAP1和VCAP2引脚,必须各接一个2.2uF电容到地。这个电容漏掉的话,芯片上电复位和唤醒都会出问题。很多STM32F103板子“下载正常但程序不跑”,原因就在这。
- NRST复位电路:10kΩ电阻上拉到3.3V,再并联一个100nF电容到地,可选一个复位按键。
- BOOT0下拉到GND,BOOT1随意处理。这样默认从Flash启动。
- SWD下载接口,引出SWDIO、SWCLK、GND、3.3V、NRST五根线就够了。下载调试非常方便。
- 电源部分:USB 5V进AMS1117-3.3降到3.3V,输入输出各加10uF和100nF电容。每个VDD引脚旁边放一个0.1uF去耦电容,VDDA用1uF+0.1uF,VREF接3.3V。
这些元件不是可有可无的装饰。去耦电容靠近芯片引脚放置,是为了给高速开关的数字电路提供瞬态电流回路;晶振靠近MCU、封装走线对称,是为了减少寄生电容和干扰。原理图上省几颗电容,后面调试能多花几天。
2.2 滴速检测、电机驱动与传感器接口电路
滴速检测是整个系统的“眼睛”,电路上必须可靠。红外对管由发射管和接收管组成,发射管串一个限流电阻,接收管输出信号。液滴经过检测区域时遮挡红外光,接收管上的电压发生变化。
问题是液滴下落很快,信号脉宽短、边沿不够陡,直接接GPIO容易误触发。升级版的硬件方案是:接收管输出信号先经过LM393比较器整形,整成干净的方波,再送进STM32的外部中断引脚。LM393是开集输出,记得加上拉到3.3V的电阻(一般4.7kΩ到10kΩ)。如果不想用比较器,至少也要加一个RC滤波电路,否则滴速计数会跳得怀疑人生。
电机驱动我选的是28BYJ-48步进电机加ULN2003驱动板。这个组合成本低、驱动逻辑简单,四个IO口输出脉冲序列就能转。最重要的是步进电机在低速下有稳定的扭矩,蠕动泵夹住输液管后,转速和滴速近似线性,非常适合PID控制。如果你以后项目功率更大,可以换成42步进电机加DRV8825,原理一样。
电机和MCU要特别注意供电关系。电机启动瞬间电流很大,会拉低电源电压,导致MCU复位。升级版的推荐做法是:电机单独用5V或12V适配器供电,驱动板的电源和MCU电源分开,控制信号在两者之间加光耦隔离,或者至少保证可靠共地并在电机电源两端加大容量电容。别小看这个问题,“一开电机MCU就重启”是这类项目排名靠前的经典故障。
液位传感器用非接触电容式液位传感器,贴在输液瓶外壁检测低液位,输出高电平或低电平。温度部分可以选DHT11或者DS18B20,DHT11电路简单但时序比较敏感,数据线要接4.7kΩ上拉电阻,而且读取间隔必须大于1秒。DS18B20抗干扰更好,就是驱动代码稍长一点。OLED显示屏用0.96寸I2C接口的SSD1306,SCL、SDA各接一个4.7kΩ上拉电阻,地址一般是0x3C。
2.3 原理图绘制与PCB布线心得
原理图可以用嘉立创EDA、Altium Designer、KiCad都行,开源资料里最好同时放出原理图PDF和可编辑源文件,方便别人查看和修改。
绘制时强烈建议分模块画:电源模块、STM32最小系统、传感器接口、电机驱动接口、显示和按键、扩展排针。网络标号要见名知义,比如DROP_SENSOR、PUMP_STEP、MOTOR_DIR、LIQUID_LEVEL,不要用N1、N2这种谁都看不懂的标号。
PCB布线时,晶振尽量靠近MCU的OSC_IN/OSC_OUT引脚,走线短且对称,晶振下方不要走其他信号线。模拟信号线(比如滴速传感器输入)要短,最好有包地保护。电机驱动部分和传感器检测部分分区布局,大电流回路和小信号回路不要交叉。所有芯片电源引脚都要放0.1uF去耦电容,不是一个“总电容”替代得了的。
3. 软件设计与核心代码逻辑
3.1 模块化分层与状态机设计
这套代码我没有一上来就上RTOS,而是用裸机前后台加状态机的方式写,因为逻辑并不复杂,裸机更容易看懂。main函数里初始化外设后进入一个主循环,中断里做时间敏感的信号采集,主循环做PID计算、显示刷新和通讯处理。所有耗时操作都不要用delay死等,用标志位加非阻塞延时。
系统状态划分成几个基本状态:
typedef enum { ST_IDLE, // 待机 ST_SETTING, // 设置目标滴速 ST_RUNNING, // 正常运行 ST_ALARM, // 报警 ST_PAUSE // 暂停 } SysState;待机状态下系统只做初始化检查,设置状态下通过按键调整目标滴速,运行状态下执行检测、PID和控制,报警状态下蜂鸣器和LED动作并上报上位机。状态之间切换要确保资源释放干净,比如从运行切到暂停时,电机一定要停下来,不然蠕动泵继续转就是安全隐患。
模块化方面,我把代码分成drop_sensor.c、pid.c、motor.c、display.c、key.c、uart_protocol.c这几个独立文件。这样做的好处是,你想换一个电机驱动芯片,只需要改motor.c;想换一个显示屏,只需要改display.c,其他模块不用动。
3.2 滴速检测:从计数到滑动平均
滴速检测在中断里做。用STM32的外部中断,信号线接在PA0上,滴落一次触发一次中断。关键代码如下:
volatile uint32_t last_tick = 0; volatile float instant_rate = 0.0f; void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == DROP_SENSOR_PIN) { uint32_t now = HAL_GetTick(); uint32_t interval = now - last_tick; last_tick = now; if (interval > 30) // 滤掉抖动造成的毛刺 { // 按当前滴间间隔换算瞬时滴速:滴/分钟 instant_rate = 60000.0f / interval; } } }瞬时滴速能不能直接用?不能。液滴之间的时间间隔并不是恒定的,重力、管路弹性和药液表面张力都会导致间隔抖动。如果直接把瞬时值送进PID,控制量会跟着一起抖,系统很难稳定。
升级版的处理方法是加滑动平均滤波。维护一个长度为N的缓存数组,每测到一滴就更新一次平均值:
#define RATE_BUF_SIZE 8 float rate_buffer[RATE_BUF_SIZE]; uint8_t rate_index = 0; float rate_sum = 0.0f; float filtered_rate = 0.0f; void update_filtered_rate(float new_rate) { rate_sum -= rate_buffer[rate_index]; rate_buffer[rate_index] = new_rate; rate_sum += new_rate; rate_index = (rate_index + 1) % RATE_BUF_SIZE; filtered_rate = rate_sum / RATE_BUF_SIZE; }N取多少也有讲究。太小了滤波效果不明显,太大了测量滞后,PID控制器反应不过来。我实测下来8是比较合适的值,既能把抖动压住,又不至于让滴速响应慢半拍。
3.3 滴速闭环控制:增量式PID落地
PID部分是整个软件的核心。控制目标是让实测滴速贴近目标滴速,执行机构是步进电机驱动的蠕动泵。控制方式是:每隔1秒计算一次当前滴速误差,用增量式PID算出新的控制量,输出映射到步进电机的转速。
增量式PID公式:
Δu(k) = Kp×[e(k) - e(k-1)] + Ki×e(k) + Kd×[e(k) - 2×e(k-1) + e(k-2)]
这样算出来的Δu是上一次控制量基础上的增量,不会把累计误差一次性全压上去,对电机这种执行机构来说更平稳。
代码实现我习惯用结构体:
typedef struct { float Kp; float Ki; float Kd; float target; float actual; float err_prev; float err_prev2; float delta_out; float out_min; float out_max; } PidController; float pid_increment(PidController *pid, float actual) { float err = pid->target - actual; float delta = pid->Kp * (err - pid->err_prev) + pid->Ki * err + pid->Kd * (err - 2.0f * pid->err_prev + pid->err_prev2); pid->err_prev2 = pid->err_prev; pid->err_prev = err; if (delta > pid->out_max) delta = pid->out_max; if (delta < pid->out_min) delta = pid->out_min; return delta; }调参顺序是我踩过坑之后总结出来的:先把Ki和Kd设为0,只调Kp,观察滴速是否朝目标方向走;出现周期振荡就把Kp适当减小;然后加Ki消除目标值和实际值之间的静差,Ki不要太大,否则会低频振荡;最后根据情况决定要不要加Kd。Kd对噪声很敏感,而滴速测量已经经过了滑动平均,所以这项通常给得很小,甚至为0也可以。
还有一个容易被忽略的细节:如果长时间检测不到新液滴,要判定“停滴”或“堵塞”,进入报警状态。所以PID循环里也要做一个超时判断,比如连续3秒没有触发滴速更新,就置报警标志。
3.4 串口协议与上位机对接
升级版加入了自定义串口协议,目的是把系统状态实时上报给上位机,也为后续接WiFi模块、MQTT上云留好了接口。
协议帧格式是这样的:
| 帧头1 | 帧头2 | 命令字 | 数据长度 | 数据 | 校验和 |
|---|---|---|---|---|---|
| 0xAA | 0x55 | CMD | LEN | DATA | SUM |
命令字0x01表示上报状态,0x02表示设置参数。状态上报的数据包括目标滴速、实测滴速、液位状态、温度、报警标志,按固定顺序排列。校验和取前面所有字节累加的低8位,接收端必须校验通过才执行命令,否则直接丢弃,避免粘包和错帧。
接线就是把STM32的USART1 TX、RX接到USB转TTL模块,波特率115200,打开串口助手能看到周期性的状态帧。如果你手上正好有ESP8266模块,把USART2让它透传,连到家里的WiFi,再把MQTT客户端跑起来,手机端就能看到实时滴速了,这部分在仓库里给了引导文档。
4. 仿真环境搭建与整机验证
4.1 Proteus仿真流程
仿真工程不能用来看热闹,它最大的价值是让没有硬件的人也能把逻辑跑起来。我这套仿真用的Proteus 8,流程非常简单。
第一步,新建Proteus工程,从元件库找到STM32F103C8T6。第二步,搭最小电路,电源、复位、晶振这些在仿真里可以简化,但为了和原理图对应,建议都放上去。第三步,添加外设:LED、蜂鸣器、按键、虚拟串口终端。第四步,模拟滴速传感器——这一点比较关键,Proteus里没有现成的红外对管模型,我用一个信号发生器(DCLOCK)产生周期性脉冲,接到STM32的外部中断引脚上,用脉冲频率模拟滴速。这样你想测试“滴速突然变化时PID能不能拉回来”,只需要调节信号发生器的频率参数,非常直观。
第五步,双击芯片加载编译好的hex文件。在Keil MDK里编译工程,输出文件在MDK-ARM目录下的xxx.hex。第六步,点运行,OLED显示屏、按键、报警、串口输出就都能在仿真里看到了。
仿真时有一个问题要注意:Proteus里DHT11模型经常卡住读不到数据。这个不用纠结,仿真工程里我默认用一个可调电阻分压接到ADC引脚来模拟温度值,代码里读ADC换算成温度。等你拿到真实硬件,再把温度传感器驱动打开。
4.2 在线仿真:Wokwi等平台
除了Proteus,现在还有Wokwi这类在线仿真平台,浏览器打开就能用,非常适合及时验证一段逻辑。Wokwi的元件库里可以搜索STM32F103C8T6,能找到的话直接拖出来面试用。它支持在浏览器里连线、写代码、跑仿真,体验和Proteus互补。
我实际用下来的感受是:Wokwi更适合快速验证传感器逻辑、按键扫描、状态机切换这类纯软件的流程;Proteus更适合做整机电路级别的仿真演示,比如按键、电机、显示、串口一起跑起来的效果。两类工程我在仓库里都放了,方便不同习惯的人选择。
4.3 仿真和实物的差异
仿真毕竟是仿真,它验证的是逻辑,不是物理世界。仿真里的脉冲信号非常干净,真实红外对管的信号却带着毛刺和抖动;仿真里的步进电机只是一个转速模型,不会产生电流冲击,真实电机一启动就把电源电压拉低;仿真里也不存在线缆寄生电容、地线阻抗、静电干扰这些问题。
所以,仿真跑通只是第一步。烧录到真实板子之前,一定要分级测试:先确认电源输出正确,再测最小系统能否烧录和运行,然后分别验证传感器、电机、显示、通讯每个模块,最后再整合整个系统。这个顺序能帮你快速定位问题出在哪一层,而不是一上来就东改西改。
5. 常见问题与排查技巧实录
5.1 下载报错:no stm32 target found
这是一个出现频率很高的报错。遇到它不要慌,按顺序排查。
首先确认供电,3.3V和GND是否正常,芯片是否发烫;然后检查SWD接线,SWDIO、SWCLK、GND三条线有没有接反,目标板是否需要额外供电;再降低下载速度,有些线长了或者干扰大,高速下载不稳,把速度降到1MHz甚至更慢试试;还可以尝试“按住复位,点击下载,再松开复位”的方式,让芯片在上电复位瞬间进入调试模式。
如果是新出厂的芯片报“debug authentication”这类提示,可能是调试口被保护了,用STM32CubeProgrammer连接后,在Option Bytes里解除读保护。新芯片如果上电后程序卡在某种异常状态,也可以先用串口ISP方式全片擦除,再重新下载。
5.2 晶振不起振与时钟配置异常
晶振不起振的典型现象是:下载正常,但程序不跑,或者一运行就卡死在等待时钟就绪的循环里。检查思路是:先看晶振两个引脚有没有焊好,附近有没有虚焊、连锡;再看负载电容值是否合理,公式在前面已经算过了;用示波器探头测晶振引脚波形,正常应该能看到振荡,就算幅度不大也没关系,常见的是完全没波形。
最快速的定位方法是先把代码里的时钟源切换到内部HSI,把系统时钟改成8MHz。如果改成HSI之后程序能跑,那问题基本就锁定在外部晶振及其电路上了。这个方法我很常用,比换电容还快。
5.3 ADC读数漂移和电机干扰复位
ADC读数漂移,多半是电源纹波和地线噪声带来的。软件上可以做多重采样取平均值,或者中值滤波;硬件上在ADC引脚附近加100nF+10uF滤波电容,必要时串一个小电阻形成RC低通。如果ADC在电机运行时漂得厉害,优先检查电机和MCU是否真的共地,电机驱动器电源是否干净,控制信号线上有没有串电阻限制瞬态电流。
“一开电机MCU就复位”这个问题,我在2.2节说过,核心原因是电机启动电流把电源电压拉到了单片机最低工作电压以下。解决办法按优先级排:电机用独立电源,控制信号光耦隔离,电机电源两端加大容量电容(470uF到1000uF),主控电源用电感或磁珠和电机电源隔开。这四条能解决绝大多数这类问题。
5.4 串口和驱动识别问题
电脑上设备管理器里出现“STM32 Virtual COM Port”带黄色感叹号,通常是USB CDC驱动没装好,重新安装STM32官方驱动,或者更新到最新版本往往能解决。如果用的是CH340USB转串口模块,装上驱动还识别不了,优先怀疑USB线是不是只有充电没有数据功能——这种线特别迷惑人,换一根数据线试一下。串口乱码的话,检查三点:波特率和代码里是否一致、TX和RX有没有接反、两个设备之间GND有没有共地。
| 现象 | 可能原因 | 排查 / 解决办法 |
|---|---|---|
| 下载失败 no target found | 接线错误、供电不足、芯片锁死、速度过高 | 检查3.3V与GND、SWD连线;降低下载速度;按住复位再点下载;CubeProgrammer解锁 |
| 程序下载后不运行 | 晶振不起振、VCAP电容缺失、BOOT设置错 | 改用HSI验证;检查VCAP电容;确认BOOT0接地 |
| 滴速显示跳动很大 | 传感器信号抖动、未滤波 | 加比较器整形;软件做滑动平均;检查红外对管安装位置 |
| 一开电机MCU就复位 | 电机启动电流拉低电源电压 | 电机独立供电;加大电容;光耦隔离控制信号 |
| 虚拟串口感叹号 | CDC驱动缺失或冲突 | 重新安装官方驱动;禁用驱动签名;换USB线/口 |
| 串口乱码 | 波特率不一致、TX/RX接反、未共地 | 统一波特率;交换TX/RX;连接GND |
| DHT11一直读不到数据 | 上拉电阻缺失、读时序不对、读取间隔太短 | 加4.7k上拉;每次读取间隔大于1秒;检查引脚配置 |
6. 开源资料组织与二次开发建议
6.1 仓库目录与快速上手
开源不只是把文件丢到网上,资料组织得好,别人才能快速跑起来。仓库目录大概是这样:
Smart-Infusion-Monitor/ ├── hardware/ │ ├── schematics_pdf/ // 原理图PDF,方便直接看 │ ├── pcb/ // 可编辑的PCB源工程 │ └── datasheets/ // 用到的芯片和数据手册 ├── firmware/ │ ├── Core/ // HAL库生成的代码 │ ├── Drivers/ // ST官方驱动 │ ├── MDK-ARM/ // Keil工程,编译输出hex │ └── README.md // 编译和烧录说明 ├── simulation/ │ ├── proteus/ // Proteus仿真工程 │ └── wokwi/ // Wokwi在线仿真工程 └── docs/ ├── 硬件调试记录.md └── 参数整定记录.md拿到代码后,用Keil MDK打开MDK-ARM目录下的工程文件,先确认电脑上装了STM32F1系列的器件支持包,编译通过后用ST-Link下载,打开串口助手就能看到状态数据。烧录时务必确认芯片型号是STM32F103C8T6,F1系列选错型号一样能烧,但跑起来可能出奇怪问题。
6.2 怎么改成自己的课程设计或毕业设计
这套代码直接交上去肯定不行,但你可以基于它做合理的二次开发。最容易出成果的几个方向:把温度传感器从DHT11换成DS18B20并完善驱动;把步进电机驱动换成DRV8825,功率更大控制更精细;给系统加上WiFi模块,做一个手机App或者网页可视化界面;把单通道改成双通道,一个STM32管理两路输液,重点考验你的资源分配和任务调度能力。
代码本身的模块化结构就是为了方便替换而设计的。换传感器就改对应模块,换通讯协议就改uart_protocol.c,主循环基本不用动。不要再出现把全部功能堆在main.c里的写法,那会把自己也绕晕。
6.3 后续可以怎么扩展
如果只是想继续深入学习,这个系统可扩展的点非常多。想搞实时系统,就把它移植到FreeRTOS上,检测任务、控制任务、显示任务、通讯任务分开跑;想搞图形界面,换一块彩色TFT屏幕,跑LVGL,把滴速曲线画出来;想搞物联网,加一个ESP8266或者4G模块,把数据推到云平台;想搞低功耗,在待机状态进入STOP模式,用RTC唤醒定时巡检,用按键或传感器事件唤醒处理任务。
还有一个容易被忽略但有意义的改进:增加历史数据掉电存储。因为滴速、温度、报警记录这种数据,掉电后不想丢,可以用STM32内部的Flash模拟EEPROM来保存。这也是一个很好的学习点。
最后再分享一点个人的实际体会。这套系统我从基础版一路改到升级版,最大的教训是:先把检测做稳,再谈控制。一开始我急着调PID,结果滴速本身就没测准,PID算出来的控制量全是噪声,电机一抖一抖的,根本稳不住。后来老老实实把红外对管信号整形做好,把滑动平均滤波加上,再回来调PID,感觉完全不一样。很多同学卡在“PID调不通”上,其实问题不出在PID,而出在被控变量的测量上。希望这份开源的代码、原理图和仿真能帮你少走几步弯路,也期待你在滴速滤波、参数整定或者上位机可视化上有更好的改进。