简介:一套面向STM32嵌入式开发与医疗设备控制场景的智能注射控速系统资料,围绕STM32监控、电机控制、脉搏检测与人机交互展开,适合有C语言和单片机基础的学习者用于项目参考或毕业设计。资源共225个文件,以C源码、H头文件、Keil工程相关格式(uvprojx、uvoptx、map、hex等)为主,同时包含39个o目标文件和38个crf编译辅助文件,压缩包约5.6MB,目录结构清晰,便于直接打开工程查看代码和编译配置。已有116人学习下载。资料详细展示了基于PWM的电机调速实现、通过ADC采集脉搏信号的流程、LCD显示模块的驱动方式,以及STM32CubeMX/Keil环境下的固件开发思路;结合工程中的分层代码,可帮助读者快速理解智能注射系统的整体框架、掌握嵌入式外设驱动的综合调用方法,是一份兼顾原理讲解与实际工程代码的实用参考。
1. 注射速度的关键不在算法复杂度,而在闭环是否真的闭合
智能注射控速系统挂在 STM32 上,核心问题不是“控制器有多智能”,而是“电机到底转没转到你让它转的速度”。手推注射 10 mL 用时还能估算,但 0.2 mL/h 的速率靠开环 PWM 根本维持不住。电机负载变化、电池电压跌落、活塞阻力波动,任何一项都会让实际流量偏离设定值。所以这套系统首先要做的是监控闭环:编码器测角位移,按固定周期计算平均速度,PID 把速度误差拉回来。“监控”和 motor control 在这里不是两个模块,而是同一件事的两面。做这类设备的工程师,功夫主要花在选型、参数整定和故障判定上,最容易被绊住的就是三件事:定时器怎么配、PID 放在哪个中断、堵转和超速怎么判得准。
2. 电机与控制链路选型:编码器、驱动芯片与 STM32 定时器
2.1 电机选型中的关键矛盾:步进电机的丢步不可检测
步进电机在注射泵里很常见,便宜、开环控制简单,用细分驱动一圈能到几千步。但开环步进有一个对监控系统致命的缺点:丢步不可检测。负载大时电机会往前走几步又弹回,计数器上 PWM 步步数已经到位,实际行程却偏了。对注射这类对累计量敏感的场景,监控不到丢步就等于输错了总量。所以我一般建议优先选带编码器的直流减速电机,或者带霍尔传感器的无刷电机。这类系统成本高一些,但编码器装在电机轴或出轴侧,位置真值能持续读回来。
如果坚持用步进电机,必须把编码器加在传动末端,并且编码器线数不能太低。每圈 12 个脉冲是底线,四倍频后每圈 48 个计数,低于这个分辨率,低速堵转很难被及时察觉。选型时还要把丝杠导程和减速比纳入计算:电机每转的编码器计数乘以减速比,再除以丝杠导程,得到的分辨率应优于系统最小流量档位对应的推进距离。拿 0.1 mL/h 的低速档来说,如果注射器内径 20 mm,每 10 分钟推进 0.53 mm,对应到编码器上至少要有几十个计数,否则速度闭环在低速段就是靠噪声在工作。
2.2 用 STM32 定时器输出 20kHz PWM 的参数确认
电机控制常用的 PWM 频率是 20 kHz,高于人耳听觉范围,还能让电感的电流纹波可控。以 STM32F103 为例,内核和APB1总线都是 72 MHz,TIM2 挂在 APB1 上。设置预分频 PSC=0,自动重载 ARR=3599,输出频率就是 72 MHz / 3600 = 20 kHz,占空比分辨率约为 0.028%。如果从 F103 移植到 G431 这类新系列,定时器时钟变为更高频率,PSC/ARR 必须重新算。用 CubeMX 更换芯片型号后,时钟树会自动重配,但工程里手动写的定时器初始化代码仍会生效,出问题先核对这两行。
配置 PWM 输出的常见代码如下:
/* 以 STM32F103 + HAL 为例:TIM1_CH1 输出 20kHz PWM */ TIM_HandleTypeDef htim1; htim1.Instance = TIM1; htim1.Init.Prescaler = 0; /* 72MHz 不分频 */ htim1.Init.Period = 3600 - 1; /* 20kHz, 周期 50us */ htim1.Init.CounterMode = TIM_COUNTERMODE_UP; HAL_TIM_PWM_Init(&htim1); TIM_OC_InitTypeDef oc = {0}; oc.OCMode = TIM_OCMODE_PWM1; oc.Pulse = 1800; /* 初始 50% 占空比 */ oc.OCPolarity = TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(&htim1, &oc, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);代码里 PSC 决定计数时钟分频,ARR 决定计数周期,两者共同决定 PWM 频率; Pulse 是比较值,运行时用__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, pulse)更新占空比即可。注意 TIM1 和 TIM8 是高级定时器,PWM 输出前还要把主输出使能HAL_TIM_PWM_Start正确调用,曾有人在 F103 上只配置了 BRK 相关寄存器没开主输出,波形怎么都出不来。频率改到 16 kHz 以下时,电机啸叫会明显变大,不建议把周期拉得太长。
2.3 编码器接入 TIM 编码器模式并换算成速度
编码器不用 GPIO 外部中断计数,直接用定时器的编码器接口模式,硬件自动处理正交相位,不用软件判方向。以 TIM2 的 CH1/CH2 接入两路正交信号为例,配置代码:
/* 编码器输入:PA0/PA1 -> TIM2_CH1/CH2 */ GPIO_InitTypeDef io = {0}; io.Mode = GPIO_MODE_INPUT; io.Pull = GPIO_PULLUP; HAL_GPIO_Init(GPIOA, &io); /* PA0/PA1 复用为 TIM2 输入 */ TIM_Encoder_InitTypeDef enc = {0}; enc.EncoderMode = TIM_ENCODERMODE_TI12; /* TI1+TI2 四倍频 */ enc.IC1Polarity = TIM_ICPOLARITY_RISING; enc.IC2Polarity = TIM_ICPOLARITY_RISING; htim2.Instance = TIM2; htim2.Init.Period = 0xFFFF; HAL_TIM_Encoder_Init(&htim2, &enc); HAL_TIM_Encoder_Start(&htim2, TIM_CHANNEL_ALL);四倍频模式下,TIM2->CNT 的每个计数代表 1/4 个编码器线数。读取差值后按下列公式换算成速度:
int16_t delta_cnt = (int16_t)(TIM2->CNT - last_cnt); last_cnt = TIM2->CNT; /* 每 1ms 调用一次:delta_cnt/(4*ENC_LINES) 是电机轴圈数 */ float speed_rpm = (float)delta_cnt / 4.0f / ENC_LINES / 0.001f * 60.0f;ENC_LINES是编码器每圈线数,0.001f是速度环采样周期,单位是秒。换算成注射速度时,再把转速乘以减速比倒数、丝杠导程或轮周长。关键点:编码器信号线必须接在支持编码器模式的定时器通道上,而且同一组占用不要和 PWM 通道冲突。参考 IO 规划如下:
| 信号 | STM32 GPIO | 说明 |
|---|---|---|
| 电机 PWM | PA8(TIM1_CH1) | 20kHz 输出,驱动芯片 PWM 输入 |
| 方向控制 | PC2 | 高电平正转,低电平反转 |
| 驱动使能 | PC3 | 高电平使能驱动输出 |
| 电流 ADC | PA6 | 12 位 ADC 采样电机工作电流 |
| 编码器 A | PA0(TIM2_CH1) | 正交编码输入 |
| 编码器 B | PA1(TIM2_CH2) | 正交编码输入 |
2.4 驱动与隔离:方向、使能和电流上限的 IO 规划
驱动芯片选择上,常见做法是使用片内集成 H 桥的半桥驱动,如 DRV 系列、TB6612、IFX 系列等,根据电机额定电流选用。驱动输出能力要留有 1.5 倍以上余量,堵转瞬间冲击电流很容易超过额定值。驱动芯片的电流检测引脚引出后接一个采样电阻放大,再进 STM32 的 ADC,采样窗口避开 PWM 开关瞬间,通常取中心对齐采集中间点,这样读到的才是平均电流而不是毛刺。
隔离问题在这类设备上比较重要:电机电源是 24 V 或 12 V,STM32 是 3.3 V 逻辑,PWM 信号直接过去容易把逻辑电平拉坏。我会在 PWM、方向、使能三根线和驱动之间放数字隔离器,编码器信号返回时也做电平转换或隔离。共地处理要统一,模拟地、功率地单点相连,避免电机启停时逻辑电压出现尖峰。这步没做好,后面串口看到的 PID 曲线会莫名出现毛刺,而且往往只在电机加电才出现。
3. 用 PID 做注射速度闭环:motorcontrol 的核心实现
3.1 位置环和速度环为什么必须串级
只做速度闭环,电机会稳定地转,但注射泵最终关心的是“一段时间内走了多少距离”,也就是位置。速度环的稳态误差不会完全消失,位置就会慢慢漂。反过来,只做位置环,响应太慢,遇到阻力突变容易超调。所以 motor control 的常见结构是串级:外环位置环,输出作为内环速度环的目标值;内环速度环,输出 PWM 占空比。速度环跑 1 kHz,位置环跑 10~50 Hz,内外环频差足够大才稳定。
位置环的输入是目标位置与当前编码器累计位置的差,经 P 控制或 PI 控制后限幅,输出作为速度目标。例如设定 10 秒推完 5 mm,位置环输出目标速度约 30 mm/min。如果外环直接输出大速度值,内环会尽力跟随,但当活塞到达终点时位置误差减小,速度目标也会平滑降到零,不会冲过头。这就是先串级再调参的价值。
3.2 PID 代码放在定时器中断的正确位置
速度环的关键是代码执行时间必须稳定,否则采样间隔抖动会直接变成速度噪声。常见做法是把速度环放在基础定时器的更新中断里,中断频率就是采样频率。1 kHz 中断里只做三件事:读编码器、跑 PID、更新占空比。避免在中断里做浮点打印、Flash 写入、串口等待。
增量式 PID 实现对比位置式更适合做限幅和抗积分饱和:
typedef struct { float kp, ki, kd; float target; /* 目标速度 */ float integral; /* 积分累加值 */ float last_error; /* 上一次误差 */ } PidCtrl; float pid_run(PidCtrl *pid, float actual) { float error = pid->target - actual; float i_term = pid->ki * pid->integral; float d_term = pid->kd * (error - pid->last_error); float out = pid->kp * error + i_term + d_term; pid->integral += error; if (pid->integral > 200.0f) pid->integral = 200.0f; if (pid->integral < -200.0f) pid->integral = -200.0f; pid->last_error = error; if (out > 1000.0f) out = 1000.0f; if (out < -1000.0f) out = -1000.0f; return out; }代码中pid->integral限制在 ±200,是为了防止低速长时间堵转时积分饱和。out限幅到 ±1000,对应 PWM 占空比的偏移量,例如 50% 基线上加 100 就是增加约 10% 占空比。中断里调用pid_run后,把返回值换算成占空比阈值写入比较寄存器。注意pid->target由位置环更新,中断外用volatile修饰,避免编译器缓存导致读不到更新值。
3.3 用串口把 PID 曲线“拉平”再调参
只靠示波器看 PWM 不够直观,我会把目标速度、实际速度、PID 输出、误差四个量周期性地从串口发出来。发送频率不能太高,否则中断或主循环被阻塞,一般每 50 ms 打一条。重定向printf到 USART 后,代码像这样:
/* 主循环里每 50ms 执行一次 */ void debug_send_pid(PidCtrl *pid, float speed_actual, float pwm_out) { char buf[128]; int n = snprintf(buf, sizeof(buf), "target=%.2f actual=%.2f out=%.1f err=%.2f\r\n", pid->target, speed_actual, pwm_out, pid->target - speed_actual); HAL_UART_Transmit(&huart1, (uint8_t*)buf, n, 10); }上位机用串口绘图工具打开,选择按回车分隔的数据,就能看到一条条曲线。STM32 串口调试 PID 的重点是观察三段:启动瞬间有没有超调、稳态时实际速度发散还是收敛、阻力变化后多久追回目标值。超调大就降 kp 或加 kd;稳态有静差就加 ki;曲线高频抖动则检查编码器读数是不是带了噪声,不要急着调参。
3.4 PID 初值表和调节顺序
不同电机的电气常数和机械惯量差别很大,但初值可以从一个基准开始试。速度环目标的量纲是 mm/min,实际速度也是 mm/min,k 值就按这个量纲取。常用初值推荐如下:
| 参数 | 初值范围 | 现象与调整方向 |
|---|---|---|
| kp | 0.5~2.0 | 低于 0.3 响应慢,高于 3 容易啸叫 |
| ki | 0.01~0.05 | 偏大导致低频振荡,静差消不掉再加 |
| kd | 0.001~0.005 | 抑制超调,但对编码器噪声敏感 |
| 积分限幅 | 100~300 | 低速档用小限幅,高速档加大 |
调节顺序按“先 P、再 I、最后 D”的经典路数。先只保留 kp,给定一个中等速度目标,看实际速度跟不跟得上;出现等幅振荡,kp 减一半再试。跟随稳定后加 ki,每次增加 0.005,观察到静差被吃干净就停,加多了曲线会出现低频波浪。最后加 kd 压启动超调,起始设 0.001,对噪声敏感就退回零。整个调参过程要保证编码器信号敢上电测速,否则调出来的是假参数。
4. 监控与保护:堵转、超速、位置与掉电保持
4.1 堵转检测:两个维度同时判断
堵转是注射系统里最危险的异常,电机卡住但 PWM 还在推,轻则烧驱动,重则管路压力异常。只用电流判据不可靠,因为不同阻力下正常工作电流差别很大。我会同时看编码器和电流:PWM 持续输出但编码器计数长时间不变化,判定为停转;采样电流同时超过堵转阈值,立即切断输出并置错误标志。
typedef struct { int16_t last_cnt; uint32_t last_time_ms; uint32_t stall_ms; /* 持续静置时间 */ uint8_t triggered; } StallDetect; void stall_check(StallDetect *sd, int16_t now_cnt, uint32_t now_ms) { if (abs((int16_t)(now_cnt - sd->last_cnt)) < 2) { if (now_ms - sd->last_time_ms > STALL_TIMEOUT_MS) { sd->triggered = 1; /* 200ms 内计数变化不足 2 */ } } else { sd->last_cnt = now_cnt; sd->last_time_ms = now_ms; } }STALL_TIMEOUT_MS一般取 200~500 ms,按最低速度档算一下:最低速时每 1 ms 可能只有 1 个计数,阈值设 2 个计数可避免误判。电流判据作为第二道闸,采样值超过预设上限立即刹车。注意堵转后不要马上松 PWM,先置位错误码,等上位机确认清除错误再重新使能。
4.2 超速与位置越限监控
超速往往是 PID 参数振荡或外力拖动导致。当实际速度超过目标速度的 1.2 倍并且持续时间超过采样窗口,就应触发超速保护。监控逻辑放在速度环中断里,用一个简单的计数器:
if (speed_actual > pid.target * 1.2f) { if (++over_speed_cnt > 10) { error_flags |= ERROR_OVER_SPEED; disable_motor(); } } else { over_speed_cnt = 0; }位置越限需要区分边界和软限位。编码器累计位置超过软件软限位就减速接近,碰到硬限位或超出最大安全行程立即停机。位置记录最好带符号,正转加计数、反转减计数,否则反转后位置监控会错乱。上电时先执行回零动作,寻找机械原点或限位开关,清除累计位置基准,这是位置监控可靠运行的前提。
4.3 掉电瞬间位置保持与再启动衔接
注射中途掉电,最怕重新上电后不记得之前推了多少,直接从头开始会造成剂量超量。位置信息要能在掉电瞬间保存下来。STM32 内部 Flash 是常见存储介质,但擦除整扇区需要时间,掉电瞬间可能来不及。我会用双扇区方案:正常运行期间,每 30 秒把当前位置交替写入两个 Flash 扇区,写完一个扇区后擦除另一个;掉电检测中断到来时,只需写当前值到已经擦好的扇区。
掉电检测使用 PVD 可编程电压检测,触发外部中断:
void PVD_IRQHandler(void) { if (__HAL_PWR_GET_FLAG(PWR_FLAG_PVDO)) { /* 掉电瞬间保存位置到备用扇区 */ HAL_FLASH_Unlock(); HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, BACKUP_SECTOR_ADDR, motor_position); HAL_FLASH_Lock(); } __HAL_PWR_CLEAR_FLAG(PWR_FLAG_PVDO); }这种写法的前提是BACKUP_SECTOR_ADDR所在扇区已经擦除过,否则HAL_FLASH_Program会返回错误。PVD 阈值要选得比系统工作电压低、比复位电压高,给中断留出充足时间。如果系统有 RTC 备份寄存器且 VBAT 持续供电,也可以把位置存进备份寄存器,速度更快,但要注意备份寄存器断电即失,存储周期和应急电源策略要一致。
4.4 错误码设计与状态上报结构体
监控状态要能被上位机或本地屏准确识别,错误码设计要稳定。一个字节错位就能定位问题,后续加历史记录也更方便。以下是这套系统的错误码表,可以作为工程里的枚举定义:
| 错误码 | 含义 | 处理建议 |
|---|---|---|
| 0x00 | 正常 | 无操作 |
| 0x01 | 堵转 | 切断输出,等待人工清除 |
| 0x02 | 超速 | 记录速度值,降 PWM |
| 0x03 | 位置越限 | 停机并提示回零 |
| 0x04 | 掉电恢复 | 上电后核对存储位置 |
| 0x05 | 电流超限 | 检查机械阻力与驱动电流 |
上报结构体建议用固定长度,串口协议里带上帧头、数据长度和 CRC:
typedef struct { uint8_t head; uint8_t len; uint8_t status; uint8_t error; uint32_t position; float speed_actual; uint16_t crc; } MotorMonitorFrame;MotorMonitorFrame定义后要检查对齐,串口发送时用结构体指针转字节流需注意小端字节序。这里的关键是监控状态和错误码必须记录在 RAM 里,掉电恢复时先读 Flash 中的错误码再决定是否允许自动重启。注射系统的逻辑是:发生堵转或超速后,不允许电机自动恢复,必须由操作人员确认并手动复位,避免设备在无人看管时重新推进。
5. 系统联调与参数标定技巧
5.1 用梯形斜坡避免机械冲击
直接让速度环从零跳到目标值,活塞和管路会因为冲击出现一个速度尖峰,低速注射还会造成起始段过冲。我会在目标速度入口加斜坡限幅,每 1 ms 速度环周期让目标值增加固定步长,模拟加速过程。
float ramp_update(float target, float step) { static float current = 0.0f; if (target > current) { current += step; if (current > target) current = target; } else if (target < current) { current -= step; if (current < target) current = target; } return current; }step取最大加速度除以速度环频率。例如期望 500 ms 内完成从零到 30 mm/min 的加速,step = 30 / 500 = 0.06。停机时同样限幅减速,避免突然刹停产生负压。带负载后若出现爬行,把加速时间适当拉长到 1 秒,机械响应会平滑很多。
5.2 全行程标定与验证手段
试运行阶段不能只看曲线平滑,要看累计位置是否与设定一致。我会在注射过程中每推进固定行程打一个标记,比如每推进 10 mm 在方格纸上记录一次时间,反推各区间平均速度,实测速度与目标速度误差应在标定精度范围内,若某段误差明显偏大,基本指向机械回差或编码器松动。空载测试和带负载测试要分开做,带载后 PID 的 ki 往往需要略增以克服静摩擦。调好的参数按高速档、低速档分别保存,不要用一组参数硬跑全量程。
5.3 现场联调的排查顺序
联调时容易遇到的现象和排查顺序整理如下,出现异常先看电机有没有转,再看编码器读数有没有跟随,最后才动 PID。
| 现象 | 可能原因 | 排查顺序 |
|---|---|---|
| 电机嗡嗡响不出力 | PWM 频率太低、使能信号没拉高 | 测波形、量使能引脚 |
| 堵转误报频繁 | 编码器接线虚、CNT 不变化 | 用手转电机看串口速度 |
| 速度波动大 | 电流采样噪声进入 PID | 看 PWM 与电流波形 |
| 掉电恢复位置不对 | Flash 扇区未预擦除 | 检查备用扇区状态 |
最后的调试技巧是给已经调好的参数在 Flash 里留一份出厂备份和一份运行参数区,参数修改可以随时回滚。固件升级时备份区不动,避免一次参数错调就让整机返厂。调好后把错误码表和标定记录冻结在工程里,后续改机械结构或更换电机时,按 PWM 频率、编码器线数、速度环初值、堵转阈值的顺序重新走一遍即可。
本文还有配套的精品资源,点击获取