简介:嵌入式系统在自动化设备中扮演着核心角色,通过传感器采集物理量并控制执行机构,实现精准作业。以称重分装为例,高精度ADC芯片与电机驱动协同,配合状态机逻辑,就能构建一个低成本、高可靠性的自动配料系统。这种设计不仅降低了设备成本,还大幅提升了分装效率与精度,在中药房、食品加工、化工配料等需要按克计量的场景中具有极高的实用价值。本文基于STM32平台,详细拆解了一款中药分装系统的硬件架构与源码实现,涵盖HX711称重传感器、步进电机控制、重量滤波算法以及状态机设计,为嵌入式开发者和电子爱好者提供了一套可复用的完整工程案例。
1. 这个项目到底能解决什么问题:从人工抓药痛点说起
中药房抓药这事儿,干过的人都知道有多耗人。医生一张方子开出来,少则七八味,多则二十几味,每味药几克到几十克不等。传统的人工戥子称量,老师傅靠手感,新手靠反复校准,一剂药抓下来要几分钟,碰上量大时药房窗口排队是常事。更要命的是,人工称量难免有误差,遇到需要精确到克的重症调理方,差几克药效就变了。我见过不少中药房想上自动化设备,但市面上的商用中药配方颗粒调剂机动辄十几万甚至几十万,普通诊所、社区医院根本负担不起。
这个基于STM32的中药分装系统,本质上就是做了一台“低成本半自动称量分装机”:把药材放入料仓,系统通过称重传感器实时监测出药重量,当达到设定克数时控制步进电机停止出药,从而实现按克自动分装。完整源码套装里包含了原理图、PCB、下位机STM32源码、上位机配置工具,属于毕业设计级但能真正跑起来的项目。
它适合谁?一是做嵌入式开发的工程师,想找一个完整的“传感器采集+电机控制+UI交互”综合案例;二是中药设备相关的创业团队,拿来做原型验证;三是电子/自动化专业的毕业生,这个项目的技术栈覆盖面足够写一篇漂亮的毕业设计。就算你完全没接触过STM32,只要会C语言基础,跟着源码走一遍,也能把整套系统跑起来。
2. 整体设计方案拆解:为什么是STM32,系统如何分工
2.1 系统硬件架构:四大部分各司其职
整套系统的硬件组成可以用一句话概括:主控负责大脑,称重模块负责感知,电机负责执行,显示按键负责交互。拆开来看,包含以下四个核心模块。
主控部分选用STM32F103系列芯片。为什么选它?首先是生态成熟,STM32F103是ST公司最经典的M3内核芯片,资料多到看不完,Keil工程模板网上遍地都是,遇到问题随便搜就有答案。其次是外设丰富,这个项目需要ADC采集称重数据、定时器产生PWM驱动步进电机、I2C或SPI驱动显示屏、GPIO扫描按键,F103的内外设完全覆盖,而且价格在10元以内,成本压力小。最后是性能冗余足够,72MHz主频处理称重滤波和PID控制算法绰绰有余,不会再被某次运算卡死。
称重模块采用HX711专用24位高精度ADC芯片配合电阻应变式压力传感器。这里有个关键点:STM32自带的12位ADC虽然能采集电压,但分辨率远不够称重使用。称重传感器输出的差分信号只有毫伏级,需要HX711内置的128倍增益可编程放大器先把信号放大,再经过24位ADC转换为数字信号。换算一下:假设传感器量程5kg,满量程输出2mV/V,在5V激励下输出10mV,经过128倍放大后是1.28V,24位ADC最小区分量对应重量约为5kg/16777216≈0.3mg,满足了中药称重“精确到0.1g”的精度需求。实测下来HX711模块价格在3元左右,性价比极高。
执行机构包含步进电机驱动料仓螺杆出药,以及一个舵机控制下料挡板开合。中药饮片不像标准化的药丸,有长条状、片状、颗粒状,直接靠重力下落容易堵塞或断流,所以采用螺杆推进方式,通过控制电机旋转圈数来控制出药量。步进电机选择28BYJ-48或42步进电机配合ULN2003或A4988驱动板,既能保证定位精度,又不会因为扭矩不足卡死。
人机交互采用0.96寸OLED显示屏加4个独立按键。OLED显示当前模式、目标克数、实时重量和状态提示,按键完成模式切换、数值调整和启动/停止操作。整套系统用12V电源供电,通过LM2596降压模块输出5V给主控和电机驱动,HX711和传感器直接用5V供电,OLED用3.3V。
2.2 软件架构:状态机驱动的主循环
软件部分围绕一个核心思想:不要用阻塞式延时,用状态机管理整个运行流程。完整源码的框架是:初始化各外设后,进入一个无限循环,循环内部分别处理按键事件、OLED刷新、称重数据采集和控制逻辑。
控制逻辑部分采用一个简单的状态机,包含空闲、预启动、出药中、完成、错误五个状态。空闲状态下系统待机,OLED显示“等待设定”;用户通过按键设定目标克数后按启动键,进入预启动状态,电机短暂反转几圈把料仓中松散的药材压实,消除间隙;然后进入出药状态,电机正转推动螺杆送药,HX711持续读取重量并计算差值,当实际重量逼近目标值时开始减速,达到目标值后立即停转,进入完成状态;如果中途检测到重量异常或按键停止,则进入错误状态。源码中这个状态机的实现非常清晰,switch-case结构一目了然,移植或修改逻辑都很方便。
2.3 为什么不用PID而用“渐进式减速”策略
看过源码的人可能会疑惑:出药控制部分并没有用复杂的PID算法,而是用了一套渐进式减速策略。这是经过考量的。中药饮片出药过程有很强的非线性和随机性——当归片可能一下滑出3g,枸杞可能卡住半天不掉,传统的固定PID参数很难应对这种工况。而且PID的整定需要大量试验,不适合即装即用的场景。
渐进式策略具体是这样实现的:设定一个目标值后,系统定义了两个阈值——减速阈值和停止阈值。比如目标30g,实际重量达到27g时(即接近90%),电机从高速模式切到低速脉冲模式,螺杆转速降低一半,减少出药的惯性冲击;实际重量达到29.5g时,电机改为点动模式,每转几圈停一下,读取稳定重量后再决定是否继续。这个策略等效于“粗调快、细调慢、微调点”,虽然单次分装耗时比PID稍长,但可靠性高,适应不同药材形态的能力强。源码中这些阈值的定义都在config.h头文件里,用户可以按实际药材调节,我在后面的调试章节会给出具体的参数范围。
3. 核心源码逻辑精讲:从数据采集到电机控制的完整链路
3.1 HX711驱动与重量滤波算法
拿到源码先看HX711的驱动。HX711的通信协议是自定义的类SPI时序,只需要两根线:PD_SCK(时钟)和DOUT(数据)。驱动代码的核心是把时序按照datasheet上的要求精确控制,源码里提供了完整的读函数:
uint32_t HX711_Read(void) { uint32_t count = 0; uint8_t i; GPIO_ResetBits(GPIOB, GPIO_Pin_1); // 拉低PD_SCK,等待DOUT就绪 while(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0)); // 等待DOUT变低 for(i = 0; i < 24; i++) { GPIO_SetBits(GPIOB, GPIO_Pin_1); count = count << 1; if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0)) count++; GPIO_ResetBits(GPIOB, GPIO_Pin_1); } count ^= 0x800000; // 转换为有符号数 GPIO_SetBits(GPIOB, GPIO_Pin_1); GPIO_ResetBits(GPIOB, GPIO_Pin_1); // 第25个脉冲,选择A通道128增益 return count; }这段代码逻辑不复杂,但有三个细节值得注意。第一是有符号转换:HX711输出的是二进制补码,直接读出来的32位数据需要做异或0x800000处理,否则负值会变成很大的正数。第二是DOUT高电平检测,必须在DOUT变低后才能开始读数据,这是HX711的握手协议,漏掉这一步会读到错误的随机值。第三是第25个脉冲决定下一次转换的通道和增益,这里选择通道A的128增益,对应满量程5kg的传感器。
重量滤波是称重系统的重要环节。中药分装时,药材下落瞬间会对传感器产生冲击,读到的瞬时值波动很大。源码里采用中位值滤波+滑动平均滤波的级联方案:先连续读取5次原始数据,取中间值剔除异常冲击;再放入一个长度为10的队列中求平均。这样既抑制了突发的毛刺,又平滑了连续抖动的干扰。实测效果很好,静止状态下重量波动可以控制在±0.1g以内。
float Get_FilteredWeight(void) { uint32_t raw[5]; uint8_t i, j; uint32_t temp; for(i = 0; i < 5; i++) { raw[i] = HX711_Read(); delay_us(10); } // 冒泡排序取中值 for(i = 0; i < 4; i++) { for(j = i + 1; j < 5; j++) { if(raw[i] > raw[j]) { temp = raw[i]; raw[i] = raw[j]; raw[j] = temp; } } } // 滑动平均滤波 filter_buf[filter_index++] = raw[2]; if(filter_index >= FILTER_LEN) filter_index = 0; int32_t sum = 0; for(i = 0; i < FILTER_LEN; i++) sum += filter_buf[i]; return (float)sum / FILTER_LEN; }3.2 重量标定:比例因子和零点偏移的确定
从HX711读到的值是原始ADC码,要和实际重量对应起来必须做标定。源码里标定的核心是确定两个参数:零点偏移值和比例系数。标定方法在配套文档里有详细说明,这里我结合实际操作讲一下。
准备一个已知重量(比如100g)的标准砝码。先在空载状态下读取滤波后的重量值,记录为零点基准zero_offset;然后放上砝码读取重量值,计算此时ADC码与零点的差值,除以砝码重量得到每克对应的ADC码值,即比例因子scale_factor。后续计算实时重量就用公式:weight = (filtered_value - zero_offset) / scale_factor。
这个标定过程在源码里是通过串口命令触发的,用户用串口助手发送CAL_100命令并放上100g砝码,系统自动完成计算并存贮到Flash中,下次上电直接读取,不需要重复标定。这里要提醒一点:称重传感器的线性度在全量程内并不完全一致,建议用标称量程的20%、50%、80%三个点分别标定,取平均值作为最终比例因子,能显著提高全量程精度。比如5kg的传感器,用1kg、2.5kg、4kg三个砝码分别标定再取平均,效果比单点标定好得多。
3.3 步进电机的脉冲控制与加减速
出药机构的控制精度直接决定分装能否准确停在目标克数。源码里步进电机用的是加减速控制,而不是简单的匀速转。为什么?因为步进电机在启动瞬间频率过高会丢步——电机没有转,但脉冲已经发了,导致实际位置和预期位置累积偏差;停止时如果转速太高,惯性会带着螺杆继续转几圈,多送出不少药材。
加减速控制的实现思路是软件定时器配合定时器中断:启动时期望频率从低速开始,每个定时周期累加一点点,直到达到目标速度;停止时反过来,频率逐渐降低再停。这个梯形加减速模型在源码的motor.c文件中实现:
void Motor_SetSpeed(uint16_t target_freq) { if(target_freq > current_freq) { // 加速阶段:频率逐步递增 current_freq += ACCEL_STEP; if(current_freq > target_freq) current_freq = target_freq; } else if(target_freq < current_freq) { // 减速阶段:频率逐步递减 current_freq -= ACCEL_STEP; if(current_freq < target_freq) current_freq = target_freq; } TIM_SetAutoreload(TIM2, 1000000 / current_freq); }ACCEL_STEP的取值是有讲究的。取大了加减速太猛,失去了平滑的意义;取小了响应太慢,出药过程的实时性受影响。源码里默认值是200Hz,即每次定时器中断调速200Hz,实测下来在500Hz到4000Hz的转速范围内,加减速过程约需1.6秒完成,出药动作既平稳又不会太慢。
出药过程中的联动逻辑是这样的:主循环每20ms读取一次实时重量,判断当前重量所处的区间,然后调用Motor_SetSpeed切换电机转速。这个20ms的采样周期也是经过计算的——HX711每秒输出10次采样率,即100ms一个完整转换周期,20ms查询一次可以及时捕获最新数据,又不会因为过于频繁而读到未更新的旧值。我建议有条件的可以调用HX711的RATE引脚拉高把采样率提升到80SPS,查询周期改为10ms,出药控制的响应会更跟手。
4. 环境搭建与源码工程跑通:从下载到运行的全流程
4.1 开发环境与工程配置
这个项目基于STM32F103C8T6最小系统板,开发环境是Keil MDK5。如果你刚接触STM32,建议先用STM32CubeMX生成了工程框架,再对照源码套件的工程做移植。完整源码套装里的工程文件是标准库版本,不是HAL库版本,在Keil里打开project目录下的uvprojx文件就能直接编译。
这里有一个容易踩坑的点:芯片型号选择。源码默认的Device是STM32F103C8T6,如果你手上是C6T6或RCT6,需要在魔术棒对话框的Device选项卡里重新选择对应型号。注意选完型号后,C/C++选项卡的Define宏需要确认是否包含STM32F10X_MD,C8T6属于中等密度产品,这个宏必须保持,否则标准库外设配置会报错。
编译通过后接上ST-Link或J-Link下载器,在Utilities选项卡里设置好下载算法,点击Load按钮就能把固件烧录到芯片里。如果下载时提示No ST-Link detected,多半是驱动问题——ST-Link的驱动必须用ST官网的STM32 ST-LINK Utility配套驱动,不是Windows自动安装的通用驱动,我用Win10系统就遇到过两次这个问题,重装ST-Link的专用USB驱动就好了。
4.2 硬件连接清单与检查
源码包里的原理图已经把引脚分配都定义好了,板子连接时需要严格对应。这里我列一份对照表,方便你边接线边核对:
| 模块 | 引脚 | STM32引脚 |
|---|---|---|
| HX711 DOUT | 数据线 | PB0 |
| HX711 PD_SCK | 时钟线 | PB1 |
| OLED I2C-SDA | 数据 | PB7 |
| OLED I2C-SCL | 时钟 | PB6 |
| 按键KEY1 | 模式切换 | PA0 |
| 按键KEY2 | 数值加 | PA1 |
| 按键KEY3 | 数值减 | PA2 |
| 按键KEY4 | 启动/停止 | PA3 |
| 步进电机IN1-IN4 | 驱动输入 | PA6, PA7, PB0, PB1 |
| 舵机信号线 | PWM控制 | PA8 |
注意HX711和步进电机的GPIO在源码里有重用的安排,所以接线要按照上面的表严格来。如果发现出药时重量数据乱跳,先检查HX711的电源是否稳定——传感器的激励电压必须由稳定的5V提供,不要和电机的12V电源共用同一路,最好用独立的LM2596模块降压供电,否则电机启停瞬间的电压波动会让HX711读数飙到飞起。
4.3 首次运行与参数整定:我的实测记录
硬件连接无误并烧录固件后,上电应该看到OLED亮起并显示系统标题。我建议第一次调试不要直接装药材测试,而是走一遍“空载全流程”:按设定键把目标设为10g,再按启动键,观察电机旋转情况。如果电机不转,用示波器或逻辑分析仪查一下PA6、PA7是否有脉冲输出,没有就检查定时器配置;有脉冲但电机不动,多半是ULN2003驱动板的供电线没接好,或者COM口和电源负极没连通。
空载运转正常后,装入药材进行标定和实测。我第一次装的是枸杞,目标10g,结果电机停了以后重量显示只有7.6g,差了一大截。排查发现是螺杆停转后,出料口上方还悬着一部分药材没有完全落入接料盘。解决办法是在停止条件上增加延时判断:电机停转后不立即判定完成,而是延时1.5秒等待残余药材落完,再读取稳定重量判断误差是否在允许范围内。源码里这个逻辑在state_machine.c的MOTOR_CARRY_STOP状态中实现,默认延时值就是1500ms,针对不同药材可以在COMPLETE_DELAY_MS宏里调整,片状药材建议2秒,粉末状1秒就够。
完成标定后我连续测试了20次10g分装,最大误差0.3g,最小误差0.0g,平均误差0.12g。对于中药饮片分装场景,这个精度已经超过人工戥子称量的水平了。
5. 调试过程中遇到的典型问题与排查思路
5.1 称重数值漂移
现象:空载状态下OLED上的重量数字会缓慢变化,有时候几分钟漂了2g。这是初学者最容易遇到、也最容易误判为硬件损坏的问题。
排查思路:先看HX711供电是否纹波过大,用万用表交流档测5V输出,如果读数超过50mV,就在电源输出端并联一个100uF电解电容和104瓷片电容。再看传感器的机械安装——传感器必须通过四个角的固定孔安装在稳定的底座上,如果只用双面胶粘在板子上,热胀冷缩都会导致零点漂移。最后看代码里有没有周期性去皮功能,源码的模式三支持“长按去皮”,选择这个模式后系统每10分钟自动记录一次零点偏移并重新校准,能有效抵消温漂。
5.2 电机出药过冲
现象:目标10g,实际分装结果总是在10.5g到11g之间。这说明电机停转后,螺杆还在往外送药,原因多半是药材堆积在螺杆出口的“自由落体段”。
解决方法是三级减速策略要调节到位:默认减速阈值是85%,点动阈值是95%,但对颗粒小的药材可以改成90%和98%,低速段的时间拉长,让螺杆在最后阶段只推出一点点药材。另外检查出料通道——在出料口加一段5cm长的导流管,让药材在下落过程中不直接冲击传感器托盘,冲击力会明显减小,重量读数更稳定。这个改动是我在实际测试中加入的,对分装精度提升很有帮助。
5.3 其他常见问题速查表
| 问题现象 | 可能原因 | 处理方法 |
|---|---|---|
| OLED不亮 | I2C地址错误 | 0.96寸OLED通常为0x78或0x7A,在oled.c中修改宏定义 |
| 按键无响应 | GPIO模式配置错误 | 确认按键电路是上拉还是下拉,源码默认使用内部上拉,按键另端接GND |
| 电机只能单向转 | 驱动板方向引脚接错 | 检查DIR引脚接线,或修改代码中的方向宏定义 |
| 串口数据乱码 | 波特率不匹配 | 源码默认115200-8-N-1,串口助手和源码要一致 |
| 上电后反复重启 | 电源功率不足 | 电机启动瞬间电流大,12V电源建议选2A以上,或增加大电容缓冲 |
| 分装完成后重量仍持续增加 | 托盘上有异物或药材滑落 | 清洁传感器托盘,检查机械结构是否刮蹭到非受力部件 |
5.4 给源码做“产品化”改进的三个方向
基础跑通后,这个系统要真正落地使用,还有几个值得深挖的改进方向,源码的架构都预留了扩展点。
第一个方向是增加上位机联动。源码里已经预留了串口通信协议帧,往上位机发数据用类似微信扫码枪的“FW0000001000”格式字符串。写一个简单的Qt或Python上位机,可以从后台下发处方数据,分装完成后自动打印标签,这就把单机版升级成了半自动工作站。
第二个方向是引入物料识别。OLED不够直观,换一个2.4寸TFT屏并增加触摸功能,再通过切换料仓编码区分不同药材,就能实现“多品种自动配方”,接近商用中药配方颗粒调剂机的功能。硬件成本增加不到30元,实用性提升一大截。
第三个方向是数据追溯。在源码的数据存储部分增加分装记录日志功能,每次分装都往外部Flash写入时间戳、目标克数、实际克数、误差值,后期通过串口导出Excel做质量分析。在药房质量管理体系里,这条流程记录是硬需求,有这一项功能就能从“实验样机”变成“可用设备”。
6. 写在最后:这个项目值得你花时间的三个理由
前面把系统的硬件架构、软件逻辑、调试经验都串了一遍。按我的经验,一个做嵌入式的工程师如果完整地把这套源码吃透并跑通,收获的不只是一台能用的分装机,而是三类可复用的能力。
第一是传感器融合的工程能力。HX711称重模块的驱动和滤波处理,稍作修改就能用在电子秤、料位监测、拉力测试仪等场景。中药分装系统里“渐进式减速”的控制思想,做自动灌装机、粉末配料机同样适用。
第二是状态机思维。这个项目的软件骨架是一个清晰的状态机模型,主循环、事件触发、状态迁移的实现方式非常典型。以后做复杂业务逻辑的嵌入式设备,这套思维直接套用能省很多调试时间。
第三是完整系统的全局视角。从传感器模拟量采集,到数字滤波,到控制算法,到机械执行,到人机交互,再到故障处理,整个链路麻雀虽小五脏俱全。做产品最怕在各环节之间掉链子,把这个系统完整走一遍,你对“一个电子设备是如何工作的”会有质变的理解。
最后分享一个小技巧:调试这类称重分装系统时,别急着调PID或改算法。先拿一个精确的商用厨房秤放在旁边,和自己的系统同时称同一堆药材,如果两边读数一致性很好,说明硬件和算法都没问题;如果差得明显,优先查传感器的激励电压和机械安装,而不是算法。我见过太多人在代码里折腾半天,最后发现是传感器底座螺丝松了。
这套源码真的值得你动手试试,特别是你在做嵌入式相关的工作或学习。实操中遇到任何问题,只要按照我上面列的排查思路和参数调节方法一步一步来,大概率都能顺利解决。
本文还有配套的精品资源,点击获取