简介:面向学习STM32嵌入式开发与公路超重检测的读者,这份工程资料围绕四路称重采集、数据处理、OLED实时显示、阈值声光报警及联网上报展开,适合课程设计、毕业设计或物联网监测项目参考。压缩包共306个文件,约10.31MB,以57个c源文件与63个h头文件为核心,配有uvprojx工程配置、o与crf编译中间文件及map、lst链接映射文件,可直接打开工程编译并梳理外设驱动与通信协议实现。已有333人学习下载。其价值在于提供较完整的软硬件协同代码框架,称重传感器数据采集、四路重量累加判断、显示与报警控制、联网传输逻辑均可在源码中定位;工程还包含启动文件与目标配置,便于在Keil环境下复现实验,按模块阅读、移植和二次开发,也能帮助理解STM32F10x外设库在称重监测场景中的组织方式。
1. 四路称重不是把四个秤拼起来:公路超重检测的真实需求切面
公路超重检测这件事,一旦落到现场就不只是"装个秤":车轮压上称重板的位置偏一点、车速快一点、昼夜温差十几度,读数就能差出两三吨。基于 STM32 的四路超重检测系统要解决的正是这种不确定性——四路称重通道对应四条并排车道,也可以对应同一秤台的四个受力区,主控逐路采集、独立判定,再通过物联网模组联网把超重记录送上平台。它适合两类人:一类是要交毕设或课设、需要一套从硬件选型到联网上报都讲得通的学生;另一类是要在园区、厂区、低速治超点做一套低成本落地装置的从业者。下面按需求、架构、硬件、固件、避坑、验收的顺序把这套方案摊开,重点放在能直接抄走的部分。
2. 系统总体架构:四路称重单元、STM32 主控与联网层的职责边界
2.1 从秤台到平台的四段链路
整套装置我一般拆成四段:现场秤台、采集前端、主控单元、联网层。第一段是四块称重板,每块板下面装 1 只(小量程)或 4 只(大量程拼板)电阻应变式传感器,四块板并排铺在车道上,板与板之间留 20~30mm 伸缩缝,前后各做 1.5m 左右的过渡斜坡,让车轮是"滚上去"而不是"砸上去"。第二段是采集前端,每块板配一片 24 位 ADC,四路完全独立,信号线走屏蔽双绞,进接线盒再统一进机柜。第三段是 STM32 主控,负责轮询四路数据、滤波、标定换算、超重判定、本地显示与声光报警、断网缓存。第四段是联网层,用 4G Cat.1 模组或 Wi-Fi 串口模组,把记录以 MQTT 报文发到平台。
这样分段的好处是责任清晰:秤台出问题看机械与接线,读数跳动看前端与供电,判定不准看固件参数,数据丢失看联网与缓存。排障时不用一上来就怀疑"整个系统坏了"。四段之间只有两类接口——模拟毫伏信号和串口/SPI 数字信号,边界明确,换任意一段都不影响其它段。
四路为什么要独立而不是四路合一?因为四条车道的车是随机到达的,A 车道上板的同时 C 车道可能也在上板,如果四路共用一个 ADC 靠模拟开关切换,切换瞬间的建立时间和通道间串扰会直接吃掉小信号精度。独立四片 ADC 各走各的时钟,代价只是多几根 GPIO,这笔账很划算。
2.2 STM32 的资源分配与外设映射
主控选型上,这套方案用 STM32F103C8T6 就够:72MHz 主频、64KB Flash、20KB RAM,跑四路 80Hz 采样加一套 MQTT 上报余量充足。如果后续要加 SD 卡、以太网或动态称重算法,换 F407 更稳妥。外设分配我一般按下面这张表来定,定完就写进原理图,后面改代码不容易乱。
| 外设/引脚 | 用途 | 说明 |
|---|---|---|
| PA0~PA3 | 四路 HX711 的 SCK 输出 | 每路独占一根时钟,避免共用时钟导致的通道错位 |
| PA4~PA7 | 四路 HX711 的 DOUT 输入 | 上拉输入,等待转换完成信号拉低 |
| USART1 | 调试串口 | 115200,用于标定命令与日志输出 |
| USART2 | 联网模组 | AT 指令或透传,波特率按模组手册 |
| I2C1 | OLED 显示屏 | 现场显示四路实时重量与状态 |
| SPI1 | W25Q64 外部 Flash | 断网记录环形缓存,8MB 可存数万条 |
| PB0~PB2 | 三色状态灯 | 正常/告警/故障 |
| PB5 | 蜂鸣器 | 超重时本地声光提示 |
| PB8 | 继电器输出 | 可选,联动道闸或拦停设备 |
| TIM3 | 10ms 系统节拍 | 调度采集、判定、上报状态机 |
这里有个容易忽略的细节:四路 HX711 的 DOUT 引脚在数据未就绪时是高电平,数据就绪后拉低。如果四路都接外部中断,四路同时就绪时会挤在一起抢占,反而不好调度。我的做法是 10ms 节拍里轮询四路,每路带独立超时计时器,谁超时谁记一次错误,连续超时 5 次就判定该路断线并在屏幕上标红。这比中断方案更可控,也更容易在答辩时讲清楚。
2.3 数据契约:一条超重记录该带哪些字段
联网层最容易返工的地方不是通信本身,而是字段没定好。前几版只发了"车道号 + 重量",结果平台侧想复核的时候发现不知道是哪台设备、什么时候、按哪个限重标准判的。后来我把字段固定成下面这一组,一条记录大约 180 字节,MQTT 单包发得下。
{ "dev_id": "RD-SCALE-0007", "seq": 10241, "ts": 1730000000, "lane": 3, "peak_kg": 3860, "stable_kg": 3812, "limit_kg": 3000, "overload": true, "ratio": 1.27, "temp_c": 27.5, "calib_ver": "v1.3", "status": 0, "crc": 51734 }各字段的用途要讲清楚:seq是设备侧自增序号,平台靠它做幂等去重,避免断网补传时重复入库;peak_kg是上板过程中的峰值保持值,stable_kg是稳定后的读数,两个都发,平台复核时能看出这次称重是不是有冲击;ratio直接算好,省得平台再除;temp_c用于事后分析温漂;calib_ver是关键,标定参数一改,历史数据要能追溯是按哪版参数算的;status用位标志表示传感器断线、电池欠压、Flash 写满等异常。字段定完再写固件,后面加功能只是往后面追加,不用改协议版本。
3. 硬件选型与前端设计:传感器、24 位 ADC 与供电的三处关键取舍
3.1 称重传感器与秤台结构怎么定
传感器选型先定量程。这套方案面向低速通过场景,单块称重板按最大轴重 30t 设计,选 40t 量程的悬臂梁式或柱式传感器,留 1.3 倍余量。公路场景冲击系数不能省,车轮上板的瞬时冲击可能是静态重量的 1.5~2 倍,量程卡太紧的话传感器会长期在满量程附近工作,零点漂移会很明显,血泪经验是宁可多花点钱选大量程,也别在标定阶段天天追着零点跑。
灵敏度按 2mV/V、激励 5V 算,满量程输出 10mV 左右。这个信号很小,走线超过 3m 就必须用屏蔽双绞线,屏蔽层只在机柜侧单端接地,秤台侧悬空,否则地环流会把工频干扰直接灌进放大器。秤台结构上,一块板用四角支撑时,四个角的受力差异会导致偏载误差,通常的做法是四角各装一只传感器后并接到同一个接线盒做求和,或者干脆一块板只用一只中心支撑的柱式传感器配导向柱,偏载影响小、标定简单,更适合学生项目。
安装面必须平整,用水平仪校过再灌浆固定。板与路面高差控制在 ±3mm 以内,高差大了车轮上板就是一次撞击,不光是读数问题,长期还会把传感器震松。这些机械上的细节在图纸上看不出来,但决定了整套装置三个月后还能不能测准。
3.2 采集前端:HX711 与 ADS1256 的取舍
前端芯片的取舍直接决定系统定位,我在方案里做过一次对比,结论写进下表:
| 项目 | HX711 类模块 | ADS1256 |
|---|---|---|
| 分辨率 | 24 位 | 24 位 |
| 有效采样率 | 10Hz / 80Hz 可选 | 最高 30kSPS |
| 通道数 | 单芯片 2 通道差分 | 8 通道 |
| 接口 | 两线位操作时序 | SPI |
| 单路成本 | 低 | 高 |
| 适用场景 | 静态称重、车速 ≤10km/h 的低速通过 | 动态称重、高速预检 |
这套方案定位是低速与近静态称重,四片 HX711 各管一路、80Hz 输出,成本低、接线简单、每路互不影响,是最合适的组合。80Hz 意味着每 12.5ms 一个点,车辆以 5km/h 通过 1m 长的称重板大约需要 0.7s,能采到 50 多个点做峰值提取,够用。如果后面要升级成不停车动态称重,就得换 ADS1256 并把采样率提到 1kSPS 以上,还要加加速度传感器做冲击补偿,那是另一个量级的工程,不建议在课设阶段硬上。
选 HX711 也要接受它的脾气:内部 PGA 增益 128 时输入范围只有 ±20mV,传感器偏载或零点偏移过大就会饱和,表现为读数卡在满量程不动。所以标定时要先确认空载输出在 ±5mV 以内,超出就得在接线盒里调平衡电阻或者软件上加偏置。
3.3 电源、接地与防护:决定长期稳定性的隐形部分
现场供电一般是 24V 或 12V,进机柜先过防雷和 TVS,再进隔离 DC-DC 降到 12V,然后分两路:一路 LDO 出 5V 专门给传感器激励和模拟前端,另一路开关电源出 3.3V 给 MCU 和联网模组。模拟 5V 和数字 3.3V 必须分开走,最后在机柜的一个点上单点连接,这个点选在电源入口附近。很多人图省事用一路 5V 全带,结果联网模组一发射,四路读数集体跳几十公斤,这就是共地阻抗在作怪。
传感器激励电压的稳定性直接进结果:5V 激励波动 1%,10mV 满量程输出就波动 0.1mV,换算到 30t 量程上是 300kg。所以模拟 5V 用基准好一点的 LDO,不要用 MCU 板上的 5V 排针直接引。线束上每路加共模电感和 100nF 去耦,机柜内强弱电分槽走线,交流线和信号线至少隔 200mm。
还有一处防护容易被省掉:联网模组的电源入口要加储能电容,4G 模组发射瞬间电流能冲到 2A,如果不给够,电压跌落会让 MCU 复位,现场表现就是"设备隔一会儿自己重启一次",翻车率极高。加上 470uF 电解并联 100nF 瓷片,问题基本消失。
4. 固件落地:四通道采集、滤波标定、超重判定与断网补传
4.1 采集调度:四路 HX711 的时序与超时重试
HX711 的时序不复杂,但时序不满足就会读出乱码或者卡死,这是四路方案里最高频的故障源。下面这段是单路读取的核心,四路只是循环调用。
/* 读一帧 HX711:前 24 个时钟出数据,第 25 个时钟选下一帧的通道与增益 */ static int32_t hx711_read_raw(hx711_t *h, uint8_t gain_ch) { uint32_t t0 = millis(); /* 等待 DOUT 拉低 = 本次转换完成,超过 120ms 认为该路断线 */ while (gpio_read(h->dout) == 1) { if (millis() - t0 > 120) { h->err_cnt++; return HX711_ERR_TIMEOUT; /* 上层据此标记该路故障 */ } } int32_t v = 0; for (int i = 0; i < 24; i++) { gpio_write(h->sck, 1); delay_us(1); /* 高电平最小 0.2us,留 5 倍余量 */ v = (v << 1) | gpio_read(h->dout); gpio_write(h->sck, 0); delay_us(1); /* 低电平同样留余量 */ } /* 第 25~27 个时钟决定下一帧通道:1 个时钟 = A 通道增益 128 */ for (int i = 0; i < gain_ch; i++) { gpio_write(h->sck, 1); delay_us(1); gpio_write(h->sck, 0); delay_us(1); } if (v & 0x800000) v |= 0xFF000000; /* 24 位补码符号扩展到 32 位 */ h->err_cnt = 0; return v; }参数与逻辑说明:gain_ch取值 1/2/3 分别对应 A 通道增益 128、B 通道 32、A 通道增益 64,本方案固定传 1。delay_us(1)是保守值,实际芯片允许更短,但四路 GPIO 位操作在 72MHz 下本来就快,留余量比省那几微秒重要。符号扩展这一行如果漏掉,负值会变成 1600 多万,滤波后直接算出天文数字的重量,这是很典型的踩坑点。超时返回负的错误码,而不是返回 0,因为 0 是一个合法读数,混在一起会让上层没法区分"真的空载"和"这路挂了"。
调度上,TIM3 每 10ms 进一次中断置标志,主循环里依次读四路,读不完下一轮继续。四路 80Hz 加上位操作开销,CPU 占用不到 5%,余量留给联网和显示。
4.2 滤波与标定:从原始码值到公斤
原始码值不能直接乘系数就用,现场噪声、车辆发动机振动、风都会让读数上下跳。我一般用"中位值 + 一阶低通"两级:先取最近 5 个点排序取中位,去掉冲击性的野值,再过一阶低通平滑。低通系数按采样率定,80Hz 下取 0.2 左右,响应快又不至于太抖。
#define MED_N 5 /* 中位值滤波:抑制单点野值,代价是 2 个采样周期延迟 */ static int32_t median5(int32_t *buf) { int32_t t[MED_N]; memcpy(t, buf, sizeof(t)); for (int i = 0; i < MED_N - 1; i++) for (int j = i + 1; j < MED_N; j++) if (t[j] < t[i]) { int32_t tmp = t[i]; t[i] = t[j]; t[j] = tmp; } return t[MED_N / 2]; } /* 两点标定:raw0 = 空载码值,raw1 = 已知砝码码值,kg1 = 砝码重量 */ static void ch_calib(ch_t *c, int32_t raw0, int32_t raw1, float kg1) { float span = (float)(raw1 - raw0); if (fabsf(span) < 1000.0f) { c->calib_ok = 0; return; } /* 跨度太小,标定无效 */ c->k = kg1 / span; /* 灵敏度,kg/码 */ c->b = -(float)raw0 * c->k; /* 零点偏置 */ c->calib_ok = 1; } /* 码值转公斤,再做一阶低通 */ static float ch_update(ch_t *c, int32_t raw) { c->buf[c->idx++ % MED_N] = raw; if (c->idx < MED_N) return c->kg; /* 预热期不输出 */ int32_t m = median5(c->buf); float kg = c->k * (float)m + c->b; c->kg = c->kg + 0.2f * (kg - c->kg); /* 一阶低通,系数越小越平滑 */ return c->kg; }span的阈值保护很重要:如果标定时砝码没放稳或者线接反,跨度可能是负数或者接近 0,不做判断就会算出一个荒谬的系数,之后所有数据都是错的。标定流程我固定成三步:空载稳定 10 秒取平均写raw0,放标准砝码稳定 10 秒取平均写raw1,计算并写入 Flash,同时把calib_ver加一。四路要分别标,不能拿一路的系数套另外三路,即使同型号传感器灵敏度也有几个百分点的差异。
零点漂移用自动跟踪来处理:连续 30 秒读数都在满量程 2% 以内且波动小于 0.5%,就认为当前是空载,把零点缓慢往当前值靠,每秒最多修正 0.05%。这样温度变化引起的缓慢漂移能自己吃掉,又不会把一辆慢速通过的小车误当成零点。
4.3 超重判定状态机:什么时候算一辆车、什么时候算超重
判定逻辑是这套系统最需要设计的地方。四路各自独立跑一个状态机,避免互相干扰。
typedef enum { S_IDLE, S_LOADING, S_HOLD, S_REPORT } lane_state_t; /* 每 10ms 调一次,lane 内保存本路状态与峰值 */ void lane_task(ch_t *c) { float kg = c->kg; switch (c->st) { case S_IDLE: if (kg > TRIG_KG) { /* 超过 500kg 认为有车上板 */ c->peak = kg; c->t_enter = now_ms(); c->st = S_LOADING; } break; case S_LOADING: if (kg > c->peak) c->peak = kg; /* 峰值保持,抓冲击瞬间 */ if (now_ms() - c->t_enter > 800) { /* 上板超过 0.8s 进入稳定判定 */ c->stable = kg; c->st = S_HOLD; } break; case S_HOLD: if (kg > c->peak) c->peak = kg; if (kg < TRIG_KG * 0.6f) { /* 车已驶离 */ c->overload = (c->peak > LIMIT_KG); c->st = S_REPORT; } break; case S_REPORT: rec_push(c); /* 写缓存并置上报标志 */ c->st = S_IDLE; break; } }TRIG_KG是上板触发阈值,取限重的 15%~20%,太低会被行人或杂物触发,太高会漏掉小车。LIMIT_KG按车道限重配置,四路可以不同。峰值和稳定值都记,是因为低速通过时峰值更能反映真实轴重,而静止停车场景下稳定值更准,两个数都留着,平台侧按场景选。S_HOLD里用 0.6 倍触发阈值作为驶离判据,是防抖:如果只用一个阈值,车在板边晃一下就反复触发,一条记录变成三条。
这里没有做轴数识别。完整的轴重识别需要多块板串联、按轴距分割波形,属于动态称重的范畴,课设阶段把单车总重测准、超重判对,已经能讲清楚完整闭环。
4.4 联网上传:MQTT 报文、断网环形缓存与幂等补传
联网层的设计原则只有一条:先落盘,再上报,确认后才删。顺序反过来,一次断网就能丢掉一整天的数据。
/* 记录结构,固定长度,便于 Flash 环形缓冲按槽位读写 */ typedef struct __attribute__((packed)) { uint32_t seq; uint32_t ts; uint16_t lane; uint16_t peak_kg; uint16_t stable_kg; uint16_t limit_kg; uint8_t overload; int8_t temp_c; uint8_t status; uint8_t calib_ver; uint16_t crc; } rec_t; /* 共 24 字节,W25Q64 可分 32 万槽 */ /* 上报:发布到 MQTT,QoS1,收到 PUBACK 才标记该槽位可回收 */ int net_publish(const rec_t *r) { char topic[64], payload[192]; snprintf(topic, sizeof(topic), "road/scale/%s/rec", DEV_ID); rec_to_json(r, payload, sizeof(payload)); return mqtt_publish(topic, payload, QOS1, 3000); /* 3s 超时 */ }上报顺序是:主循环从 Flash 环形区取最老一条未确认记录 → 组 JSON → 发布 → 等待 PUBACK → 标记回收。断网时发布直接失败,记录留在原地,联网后从最老的开始补,天然保证顺序。seq与ts一起作为幂等键,平台侧见到重复seq直接丢弃。时间戳来源是模组拿到的网络时间,模组没联网时用 RTC 的粗略时间并在status里置"时间不可信"位,补传时平台能识别。
Flash 环形缓冲要处理写满的情况:写满后覆盖最老的未确认记录,并在status里置溢出标志,同时本地屏幕提示。宁可丢最老的,也不能停写,停写就意味着后续数据全丢。
5. 避坑与排查:四路称重系统最容易翻车的五个点
5.1 四路读数集体跳变,单独一路却正常
现象:静态放置时读数稳定,一旦联网模组开始工作或者蜂鸣器响,四路读数同时上下跳几十到几百公斤。
原因:共地阻抗和电源耦合。联网模组发射瞬间电流突变,在数字地和模拟地的公共走线上产生压降,前端参考电压跟着动。蜂鸣器同理,感性负载关断时的反电动势会顺着 3.3V 串回模拟侧。
解决:模拟 5V 与数字 3.3V 分开供电、单点接地;模组电源入口加 470uF 储能电容;蜂鸣器加续流二极管并由独立 GPIO 驱动三极管,不要直接从 MCU 引脚灌电流;信号线全部走屏蔽双绞,屏蔽层单端接地。改完之后同样的测试条件下跳动能压到 2kg 以内。
5.2 某一路读数卡在满量程不动
现象:一路数据恒为正满量程,其它三路正常,换成另一片 HX711 后故障跟着模块走或者跟着传感器走。
原因:两种可能。一是传感器零点偏移太大,超出 PGA 输入范围导致饱和,常见于安装时传感器被预紧或者线接反;二是这一路的 SCK 与 DOUT 接错,读出来的是移位寄存器里的残留值。
解决:先测传感器空载输出,用万用表测 mV 级差分电压,正常应在 ±5mV 以内,超了就在接线盒里调平衡或者换安装方式;再核对引脚定义,注意 HX711 的 DOUT 是输出、SCK 是输入,接反了在某些板子上不会立刻坏但一定读不对。软件上加饱和检测:连续 10 帧等于满量程就置故障位并在屏幕标红,不要让它静默地参与判定。
5.3 空载漂移,半夜自动报超重
现象:白天正常,凌晨温度降下来之后零点慢慢往下走,几个月后开始出现无车时的超重记录。
原因:没有做零点跟踪,也没有温度补偿。应变式传感器的零点温漂是物理特性,靠选好传感器只能减小不能消除,30t 量程下漂个 0.5% 就是 150kg,再加上标定后一直没复标,误差会累积。
解决:固件里加空载自动跟踪(连续 30 秒低重量低波动就缓慢修零点),同时在记录里带温度字段,方便事后分析漂移趋势;现场每季度用砝码复标一次,把新的calib_ver写进去。这套组合做完,半年内的零点漂移能控制在 0.2% 以内。
5.4 车辆快速通过时峰值抓不到,读数偏小
现象:慢速通过测出来 3.8t,开快一点只测出 2.9t,速度越快偏得越多。
原因:采样率不够加上滤波过度。HX711 在 80Hz 下每 12.5ms 一个点,车速 20km/h 时车轮在 1m 板上只停留 0.18s,只能采到 14 个点,再被中位值滤波砍掉几个、低通平滑一遍,峰值就被抹平了。
解决:这条路本来就不该跑快。方案里把适用车速明确限定在 10km/h 以内并配减速带和提示牌;固件上把峰值保持放在滤波之前,用原始码值直接取极值,滤波只用于稳定值判定;如果确实要测高速,就得换 ADS1256 提高采样率并加入冲击补偿,这不是调参数能解决的。
5.5 断网恢复后数据重复入库或者整段丢失
现象:网络恢复后平台收到大量重复记录,或者某段时间的记录完全不见。
原因:重复是因为没有幂等键,补传时把已发过的又发了一遍;丢失是因为先删缓存后等确认,或者缓存区写满后直接停写。
解决:每条记录带设备侧自增seq,平台按dev_id + seq去重;上报流程改成"发布 → 等 PUBACK → 标记可回收",超时就不标记;缓存写满时覆盖最老记录并置溢出标志,绝不停写。这三条是联网设备的基本功,缺一条都会在现场出问题。
6. 验收与答辩:用一组可复现的测试数据证明它测得住
验收不要只演示"放个重物屏幕上数字变了",那证明不了任何精度。我一般按下面这张表逐项跑,每项都留数据。
| 测试项 | 方法 | 合格判据 |
|---|---|---|
| 零点稳定性 | 空载连续采集 30 分钟 | 四路漂移均小于满量程 0.2% |
| 线性度 | 用 20%、40%、60%、80% 量程砝码逐级加载 | 每点误差小于 0.5% FS |
| 重复性 | 同一载荷加载卸载 10 次 | 极差小于 0.3% FS |
| 偏载 | 同一载荷放在板面四个角 | 四角读数差小于 1% FS |
| 超重触发 | 加载到限重的 95%、105% | 95% 不报警,105% 必报警并生成记录 |
| 断网补传 | 断网状态下产生 20 条记录后恢复 | 平台收齐 20 条,无重复 |
| 通道隔离 | 单路加载,观察其余三路 | 其余三路变化小于 5kg |
演示顺序也有讲究:先讲架构图说清四段链路,再演示正常称重,然后做一次故障注入——拔掉一路信号线,让屏幕显示该路故障而其余三路正常,这比讲十句"系统很稳"都有说服力。最后打开平台看记录列表,指着seq、peak_kg、calib_ver说明每条记录都能追溯。
答辩最常被问的三个问题,提前准备好答案:为什么四路要独立 ADC(通道串扰与并发上板);为什么采样率只有 80Hz 还够用(限定车速下的采样点数计算);超重判定依据峰值还是稳定值(按场景说明,两个都留)。把这三问答顺,基本就稳了。
从那以后我每次做带传感器的项目,都会在画板之前先把"空载、满载、偏载、断线、断网"这五种状态的预期行为写成一张表,再开始写代码。因为真正让项目翻车的从来不是算法有多难,而是这些没人愿意提前想的边界情况。希望帮到你。
本文还有配套的精品资源,点击获取