电子秤项目在单片机课程设计和毕业设计里出现的频率相当高,我见过太多人卡在同一道坎上:实物称重电路调不通,就想先在Proteus里仿真验证逻辑,结果仿真也不是装上元件就能跑的,光是HX711的时序、传感器模型的选择、标定公式的处理就能劝退一批人。这篇教程就把我用Proteus 8 Professional配合Keil5做51单片机+HX711电子秤仿真的完整过程拆开讲清楚,从硬件电路搭建、代码时序分析到常见的仿真坑,全部给到能直接照做的方案,源码部分也会逐段解释关键逻辑,不让你对着代码发懵。
1. 为什么称重首选HX711:从精度、成本到Proteus仿真可行性的权衡
1.1 51单片机内置ADC的局限
先解决第一个问题:为什么称重电路几乎不用51单片机自带的ADC模块?STC89C52这类经典51芯片本身没有集成ADC,即便你用STC12C5A60S2这类带8路10位ADC的增强型51,直接采集称重传感器的差分毫伏信号也远远不够。
称重传感器(常见的悬臂梁式、平行梁式)在满量程时的输出灵敏度通常是1mV/V到2mV/V,配合5V激励电压,满量程输出也就是5mV到10mV。10位ADC的理论分辨率是5V/1024≈4.88mV/LSB,你连满量程输出都分不出几个台阶,更别说称出1g以内的变化。即便用外部独立的16位ADC芯片比如ADS1115,价格和驱动复杂度又上去了。HX711就是冲着这个场景来的:专为高精度电子秤设计的24位Σ-Δ型ADC,内部集成稳压、放大、时钟,两线数字接口,成本几块钱还能在Proteus里直接仿真,性价比直接拉满。
1.2 HX711的核心参数与工作模式
HX711有两个差分输入通道,通道A的可编程增益为128或64,通道B固定增益32。电子秤常规接法是传感器输出接通道A,增益128倍,此时满量程输入范围约为±20mV,刚好匹配常见的1mV/V传感器在5V供电下的输出范围。片上自带稳压电路,可以直接用稳压后的AVDD给传感器供电,简化了外部电路。
内部时钟约10MHz,数据输出速率有两个档位:SDA引脚(PD_SCK)拉高的脉冲个数控制着读数和通道/增益切换。SDA拉低时为掉电模式,这也是Proteus仿真中一个关键细节:如果SCK引脚电平状态不对,HX711模型可能一直处于掉电状态,读回来的数据就是恒定的0x800000或乱跳。
1.3 Proteus仿真HX711的独特优势
很多初学者以为Proteus里的HX711只是个"摆设模型",仿真意义不大,实际上不是这样。Proteus的HX711库模型是支持真实时序交互的——你通过SCK引脚给它发送25个脉冲(24个数据位+1个通道增益选择位),它会按照24位二进制补码的格式把数据逐位移出。这意味着你的代码时序如果写错了,仿真里同样会读出错误数据,跟实物的表现几乎一致。
而且仿真环境里你可以用虚拟示波器、虚拟终端、逻辑分析仪去观察DOUT引脚的电平变化,这在实物上需要用万用表或示波器探头慢慢点的调试过程,在仿真里可以看得一清二楚。对课程设计来说,先把整套系统在Proteus里跑通,再画PCB做实物,成功率会高很多。
2. 硬件电路搭建:Proteus元件选型、传感器模型与连线细节
2.1 元件清单与Proteus检索方法
先给完整元件清单,Proteus 8 Professional及以上版本亲测可用:
| 元件名称 | Proteus检索关键字 | 说明 |
|---|---|---|
| 单片机 | AT89C51 / AT89C52 | 用AT89C52更合适,RAM和ROM更大 |
| 称重传感器 | LOADSENSOR | 库里的应变片式压力传感器模型,输出差分信号 |
| HX711 | HX711 | 在Analog IC或专门传感器分类里搜索 |
| 液晶显示 | LM016L / LM044L | 16x2字符液晶,课程设计常用 |
| 晶振 | CRYSTAL | 11.0592MHz或12MHz均可 |
| 电容/电阻 | CAP / RES | 按电路需求放置 |
| 电位器 | POT-HG | 仅作标定模拟,仿真里常用 |
注意Proteus元件库的版本差异很大。有些老版本(比如8.6以下)搜不到HX711,我遇到过读者用Proteus 8.0找不到HX711模型的情况。如果你用的版本没有这个元件,两个解决办法:一是换用Proteus 8.9以上版本,库模型比较全;二是在库搜索框输入HX711后如果没结果,可以去Proteus官网的元件库下载页面找第三方库文件(需要下载.pd4库文件放到Library目录)。不要为了省事用AD620仪表放大器替代HX711,那样代码和参考设计全对不上,反而更麻烦。
2.2 LOADSENSOR传感器模型的关键设置
Proteus里的LOADSENSOR模型不是简单的电阻应变片桥,它自带一个输入参数:力的大小(用Force属性设置,单位牛顿)。双击元件后你会看到一个Force参数,默认可能是10N或100N,仿真时它就是你的"砝码",你改这个参数就相当于往电子秤上放重物。
这是仿真中最容易忽略的点。很多人的电路接好了、程序烧进去了,LCD上数据却纹丝不动,排查半天发现是Force参数设成了0,传感器输出为零,HX711当然读不出东西。仿真时你可以把Force设为50N,然后一边运行一边改这个值,观察读数变化,特别直观。
传感器供电接E+和E-,信号输出接S+和S-。HX711的AVDD输出(通常是4.3V到4.8V)可以直接给传感器的E+供电,E-接地。如果你用外部5V给传感器供电,注意HX711通道A的差分输入范围是±20mV(增益128倍),如果传感器满量程输出超过这个范围,读出来的数据就会饱和,这在实物上会损坏芯片,仿真倒是不怕,但结果也不会对。
2.3 电路接线对照
我给你一套可以直接照着画的接线关系:
- HX711的E+接传感器E+(红色线),E-接传感器E-(黑色线)
- HX711的A+接传感器S+(绿色线),A-接传感器S-(白色线)
- HX711的VCC接5V,GND接地
- HX711的DOUT接单片机P1.0,PD_SCK接P1.1
- 单片机P2.0~P2.7接LCD1602的数据口D0~D7
- LCD1602的RS接P3.5,RW接P3.6,EN接P3.4
- 晶振电路:11.0592MHz晶振两端接30pF电容到地
- 复位电路:10uF电容+10K电阻经典复位电路
如果你想把VCC和模拟电源分开接,HX711的VCC可以单独接一个经过LC滤波的5V,VFB引脚接一个0.1uF电容到地,这在实物上能明显降低纹波,仿真里不接也能跑。
2.4 下拉电阻与上拉电阻的取舍
HX711的DOUT和PD_SCK是开漏输出,Proteus仿真模型里不接上拉电阻也能出结果,实物上一般也不需要外部上拉,因为HX711内部有弱上拉。但51单片机的P1口内部没有上拉,如果你用P0口接HX711,那必须外部接10K上拉电阻,否则高电平读不进去,数据全是0xFF。这也是一个典型的"仿真能过、实物翻车"陷阱,原理上必须讲清楚。
3. 核心代码实现:24位数据读取时序、均值滤波与标定逻辑
3.1 HX711数据读取时序的本质
HX711的通信时序说简单也简单:数据线DOUT平时为高电平,当单片机准备好接收数据时,DOUT会被拉低,表示数据已经准备好。此时单片机给PD_SCK发送25个高电平脉冲,前24个脉冲对应24位数据,从最高位(D23)到最低位(D0)逐位移出,第25个脉冲控制下一次转换的通道和增益。
关键点:DOUT必须在第25个脉冲的下降沿之后才会恢复高电平,标志一次完整读数结束。如果你读完24位就停了,DOUT不会再输出下一次数据。这就是为什么初始化函数里通常需要"清空一次数据"——上电后HX711可能处于内部状态不确定的状态,连续读两次,丢掉第一次的结果,第二次才是稳定的。
// HX711 数据读取核心函数 unsigned long HX711_Read(void) { unsigned long value = 0; unsigned char i; EA = 0; // 关总中断,防止时序被定时器打断 DOUT = 1; SCK = 0; while(DOUT); // 等待DOUT变低,变低表示数据准备好 for(i = 0; i < 24; i++) { SCK = 1; // 产生上升沿,数据移出 value = value << 1; // 左移一位 SCK = 0; if(DOUT) { // 读取当前位 value++; } } // 第25个脉冲,选择通道A,增益128 SCK = 1; value = value ^ 0x800000; // 24位二进制补码转无符号数 SCK = 0; EA = 1; // 恢复中断 return value; }3.2 二进制补码转换的细节
HX711输出的是24位二进制补码,最高位是符号位。直接把读到的24位数据当成无符号整数用会出问题:空载时读到的可能是0x800000左右(约8388608),加载后数据增大,但如果你把负的传感器偏移量(比如传感器零点输出为负)读回来,最高位是1,数值看起来会特别大。
处理方式有两种。第一种是按二进制补码转有符号数:读到的24位数据如果最高位是1,先减1再取反,然后加负号,得到有符号的int型值。第二种更常用也更简洁:直接读无符号24位值,然后在标定的时候把空载读数作为"零点偏移"基准,每次读数减去这个基准,就得到了相对变化量。
我在代码里用了value = value ^ 0x800000这一行,这是把24位补码等效转成以0x800000为中心的无符号值。这样空载值大约在0x800000附近,加载后值单调增加,后续标定减去零点值就得到正比于重量的差值。这种方法好理解,也方便串口调试观察。
3.3 为什么必须加均值滤波
HX711是24位高精度ADC,分辨率极高,也意味着它对噪声极其敏感。即使仿真环境里没有实物噪声,Proteus的模型也会模拟一定的随机扰动,你会发现连续读10次数据,最后几位一直在跳。实物上更严重,电源纹波、机械振动、温度漂移都会叠加进去。
解决办法就是多次采样取平均,工程上叫滑动平均滤波。
// 多次读取取平均,提高稳定性 unsigned long HX711_GetValue(unsigned char times) { unsigned long sum = 0; unsigned char i; for(i = 0; i < times; i++) { sum += HX711_Read(); } return sum / times; }times取值多少合适?我实测下来,5次平均在仿真里就已经很稳了,实物上如果电源质量一般,建议8~10次。但注意不要贪多:HX711默认输出速率10Hz,读一次约100ms,你取10次平均,刷新一次就1秒了,用户体验很差。如果需要更快响应,可以把HX711的RATE引脚接高电平,输出速率翻倍到20Hz,或者降低平均次数。这是一个精度与实时性的取舍,要根据实际场景调整,没有绝对最优解。
3.4 标定逻辑:从原始读数到实际重量
标定是电子秤的灵魂。称重传感器的输出电压与受力是线性关系,所以重量 = K × (当前读数 - 零点读数),K就是标定系数,单位是克/每码值。
标定过程分两步:
第一步,记录零点偏移。空载时调用HX711_GetValue读到的平均值记为offset。
第二步,放已知重物,计算标定系数。比如放一个100g的标准砝码,读到的平均值为value,那么:
- 100g对应的码值差值 = value - offset
- 标定系数K = 100.0 / (value - offset)
之后任意重量 = K × (当前读数 - offset)。
仿真里模拟标定很简单:把LOADSENSOR的Force设成0,记录offset;设成1N(约102g),记录value;然后你就能算出K。注意Proteus里Force单位是牛,1N对应约102g,你可以在标定时直接按1N=100g处理,这样后面显示的重量数值比较规整。但为了严谨,建议在代码注释里写清楚单位换算关系,免得换个人看代码时一头雾水。
#define FILTER_TIMES 5 // 滤波次数 long offset = 0; // 零点偏移 float K = 0.0; // 标定系数,单位:g/码值 void HX711_Calibration(void) { unsigned long value; // 空载读数作为零点 offset = HX711_GetValue(FILTER_TIMES); // 放100g标准重物(仿真里调整Force参数) value = HX711_GetValue(FILTER_TIMES); K = 100.0 / (value - offset); } float HX711_GetWeight(void) { unsigned long value = HX711_GetValue(FILTER_TIMES); long delta = value - offset; return delta * K; }这里有个非常隐蔽的坑:offset和delta都可能出现负数。一是传感器安装预压导致空载输出不为零,二是你放上去的重量太小,读数变化量在噪声范围内,delta偶发为负。所以代码里必须判断:delta小于0时按0处理,否则显示负数会很奇怪。
另一个坑是标定系数K丢了。很多人在开发板上调通了,断电重启后发现重量不对了,就是因为K和offset存在RAM里,掉电丢失。课程设计要求不高可以直接写死在程序里,每次上电重新标定也不是不行;如果要做得正规一点,建议把K和offset存储在单片机内部EEPROM里(比如STC系列自带EEPROM,AT89C52没有EEPROM就要外挂AT24C02)。这个根据自己的开发板型号灵活处理。
4. Proteus仿真调试全流程:从加载程序到LCD稳定显示
4.1 Keil5工程配置与HEX文件生成
写代码之前先把Keil5的工程配置好,这里有一个高频错误:新装Keil5默认没有安装51系列芯片包,如果芯片型号列表里找不到AT89C52,需要先下载安装C51相关的Device Pack。Keil5和Keil4的安装逻辑不一样,Keil4装完自带C51编译器,Keil5需要独立安装C51的包。
工程配置步骤:
- 打开Keil5,点击Project → New uVision Project,取好工程名
- 在弹出的Select Device窗口里输入AT89C52,选中对应芯片
- 左侧Project栏右键Target,选择Options for Target,或者直接点魔法棒图标
- 切换到Output选项卡,勾选Create HEX File
- 如果代码里用到了定时器中断,需要把Interrupt的勾选保持默认,用不到就不用管
- 点击编译按钮(F7),生成HEX文件
生成HEX文件之后,打开Proteus。默认情况下Proteus不会自动加载HEX,需要双击AT89C52元件,在Program File一栏点击文件夹图标,选择Keil输出目录下的.hex文件。这一步做完,点击Proteus左下角的运行按钮,仿真就开始跑了。
4.2 程序烧录进Proteus仿真芯片
这里说一个很多人不知道的细节:Proteus仿真51单片机不需要烧录器,只要加载HEX文件即可。但你加载完HEX之后,如果改过代码重新编译,Proteus里可能会继续运行旧程序。很多人改完代码发现仿真结果没变化,就是因为Proteus没有重新加载HEX,或者Proteus还开着旧的仿真进程。稳妥做法是:先停止仿真,重新加载HEX文件,再运行仿真。
Proteus默认的仿真速度是实时仿真,你可以在右下角看到仿真进度条的运行时间。如果代码里有死循环等待某个引脚电平变化,而外部没有触发条件,仿真可能看起来像"卡死"了,这时候按左下角的暂停按钮,检查代码里有没有while(1)死循环在等待一个永远不会变的条件。
4.3 观察重量变化:调整传感器Force参数
仿真运行后,怎么模拟"放重物"?双击LOADSENSOR元件,会弹出属性对话框,里面有一个Force属性。你把它改成10N(大约1公斤),点击确定,程序下一次采样就能读到新数据,LCD上的数字会跟着变化。
我测试时的操作路径是:先把Force设为0,观察LCD显示0g;然后把Force改为0.5N,LCD显示约50g;再改为1N,LCD显示约100g。这个变化过程非常直观,就像真的在秤盘上放砝码。如果你的LCD读数不变化,从下面三个方向排查:一是程序有没有编译成功、HEX有没有正确加载;二是HX711的DOUT和PD_SCK接的是不是程序里写的引脚;三是LOADSENSOR的Force是不是0。
4.4 虚拟终端和变量观测技巧
Proteus里最实用的调试工具是Virtual Terminal(虚拟终端)。在元件库搜索VIRTUAL TERMINAL,放到原理图上,把它的RXD接到单片机的TXD引脚(P3.1),TXD接到单片机的RXD引脚(P3.0),然后把程序里的串口初始化补上,就能在虚拟终端里实时看到调试数据。
我在调试HX711时序的时候,习惯在关键位置加串口输出:
void UART_Init(void) { TMOD = 0x20; // 定时器1,方式2,8位自动重载 TH1 = 0xFD; // 波特率9600,晶振11.0592MHz TL1 = 0xFD; TR1 = 1; SM0 = 0; SM1 = 1; // 串口方式1 REN = 1; // 允许接收 }然后在HX711_Read函数里临时插入一条串口打印语句,看DOUT在时序中的作用是否正常。虚拟终端的好处是数据流清晰,能直观看到每次读到的原始值,判断滤波器有没有生效。
另外Proteus 8里的调试方法很实用:在HX711的DOUT引脚放一个逻辑探针(Logic Probe),或者直接右键引脚选择"View State",就能实时看到引脚电平,排查DOUT是否真的拉低了。时序代码写对没有,看DOUT的电平变化频率是最直观的判断依据。
5. 避坑指南:我在HX711+Proteus仿真中踩过的6个坑
5.1 DOUT一直为高电平,程序卡死在while(DOUT)
这是最高频的坑。程序一运行就卡在等待DOUT变低的死循环里,LCD没显示,仿真像死机。绝大多数原因是SCK引脚初始状态不对。HX711的PD_SCK如果一直处于高电平,芯片就一直处于掉电或复位状态,DOUT永远不会拉低。解决办法:确保程序初始化时把SCK赋0,且SCK引脚必须接在51单片机准双向IO口上,P0口需要外部上拉,P1/P2/P3口直连没问题。
另一个原因是Proteus模型里HX711上电后需要一定时间来稳定,而你的程序在main函数开头就调用HX711_Read,此时芯片还没准备好。解决:在初始化代码里加一个延时,比如上电后delay_ms(100)再开始读数据。
5.2 读到的24位数据全是0xFFFFFF
这个现象对应的数据转换有问题。0xFFFFFF是24位补码的-1,通常说明你的时序没有真正读到数据,或者说读到的每一位都是高电平。排查思路:先用虚拟终端打印HX711_Read的返回值,如果恒定0xFFFFFF,基本可以断定SCK和DOUT中有一个引脚接错了,或者芯片根本没工作。检查DOUT和SCK是否接反、引脚号是否和代码一致。
5.3 LCD显示但数字乱跳,稳定不下来
仿真中数字跳动的第一原因不是噪声,而是你没有做滤波,读了几次原始值直接用来显示。24位ADC的末几位跳动是正常现象,不做均值滤波,显示结果自然会跳。第二个原因是标定系数K不合理,把微小码值差异放大成很大的重量变化。检查一下标定时代码里读到的空载值和加负载值之间的差值,如果差值只有几百,那么K会很大(比如0.2g/码值),噪声引起的波动就会很明显。
第三个不太容易想到的原因:Proteus仿真中如果其他元件在消耗大量CPU资源(比如LCD频繁刷新、复杂图形),仿真速度跟不上,HX711的时序就会被程序的内部延时拉开。这个可以通过把LCD刷新频率降低、减少无意义的屏幕清空操作来解决。
5.4 实物和仿真数据差异过大
Proteus仿真用的是理想模型,传感器输出线性度非常好,电源噪声也小。实物上传感器供电纹波、布线质量、稳压芯片的噪声都会叠加进测量结果。我在实物上调试时,最有效的优化是给HX711的AVDD和VCC分别加一个100uF电解电容和0.1uF瓷片电容组合,给传感器桥路的S+和S-之间并一个0.1uF电容滤除高频干扰。另外一个容易被忽略的点是传感器外壳必须固定牢靠,机械谐振会直接变成读数上的周期性波动。
5.5 标定系数在断电后丢失
前面提过,RAM里存的标定系数断电就没了。这个问题在课程设计答辩时特别容易暴露:评委让你演示,你上电发现显示0g之后放上砝码数字完全不对,瞬间尴尬。我建议代码里做两个标定模式:长按按键进入标定模式,此时显示当前读数;短按确认零点,再放砝码确认满量程。标定完成后把K和offset烧写到EEPROM,下次上电直接读取。你如果没有硬件EEPROM,至少把标定结果用十六进制写死在程序里,也行。
5.6 Proteus仿真速度慢,刷屏明显
Proteus的LCD1602模型刷新比实物慢得多,如果你的主循环里频繁调用LCD写函数,仿真速度会肉眼可见地掉帧。优化方案:只在重量变化超过阈值(比如1g)时才刷新LCD显示,或者把刷新频率控制在每秒3到5次。人类肉眼对重量数字的响应不需要那么高频,这个频次完全够用,而且CPU占用率会大幅降低。
6. 从仿真到实物:移植时的接线改动与代码适配
6.1 实物硬件的选型建议
仿真跑通之后,做实物前先确认几件事。单片机建议直接用STC89C52RC或STC12C5A60S2,前者是完全兼容AT89C52的,程序几乎不用改;后者速度快一个数量级,但引脚复用要注意,如果你用到的引脚恰好是ADC或PWM功能引脚,初始化时要关掉复用功能。
HX711模块买独立的称重模块板(红板或蓝板,淘宝搜"HX711模块"就能找到),模块上自带稳压和滤波,比自己搭电路稳定得多。传感器推荐5kg或10kg量程的平行梁式传感器,配一个称重托盘。电源建议用USB口的5V供电,不要用充电宝的5V(纹波太大),实验室直流稳压电源最好。
6.2 与Proteus仿真的硬件差异分析
实物和仿真最核心的区别是:仿真里HX711是理想模型,引脚时序只要逻辑对就能跑通;实物上HX711的DOUT和SCK信号线要尽量短,避免和电机、继电器这类大功率器件共地线。如果在实物上发现读数异常,优先检查电源:HX711的VCC和传感器供电是不是同一路5V,如果是,务必加上LC滤波(串联10uH电感再并100uF电容)。
另外实物上因为晶振频率偏差,串口波特率、定时器延时都可能和仿真不一样。这个不影响称重功能,但如果你的程序里有基于延时的时间特性(比如按键消抖、超时判断),要注意用示波器实测校准。
6.3 功能扩展:键盘校准、超重报警、数据存储
项目做完基础功能后,建议加几个实际功能让整体更完整:
- 键盘校准:两个按键,一个进校准模式,一个确认当前重量。校准模式实现思路:第一次按键记录零点,放上1kg砝码后再次按键记录满量程点,自动计算K和offset。
- 超重报警:加一个蜂鸣器,当重量超过设定阈值,比如2kg,蜂鸣器响,这个用有源蜂鸣器最简单,给高电平就响。
- 数据存储:外挂AT24C02存储标定参数,上电时先读取,如果校验失败(首次上电),再进入标定流程。这就解决了5.5节说的问题。
- 串口上位机:用串口把重量数据传给PC,做一个简单的上位机显示曲线。HX711数据率10Hz,画实时曲线完全够用。
7. 完整源码
我整理了一份可以直接编译运行的完整代码,基于AT89C52最小系统。源码包含初始化、HX711驱动、LCD1602驱动、按键标定和主循环逻辑,代码里注释写清楚了每个函数干什么。
// ==================================================== // 51单片机 + HX711 电子秤 Proteus仿真程序 // MCU: AT89C52 / 晶振: 11.0592MHz // HX711: DOUT -> P1.0, PD_SCK -> P1.1 // LCD1602: 数据口 P2, RS->P3.5 RW->P3.6 EN->P3.4 // ==================================================== #include <reg52.h> #include <string.h> #include <stdio.h> #define FILTER_TIMES 5 sbit HX711_DOUT = P1^0; sbit HX711_SCK = P1^1; sbit LCD_RS = P3^5; sbit LCD_RW = P3^6; sbit LCD_EN = P3^4; #define LCD_DATA P2 long offset = 0; float K = 0.0f; unsigned char calibration_step = 0; void delay_ms(unsigned int ms); // ================== LCD1602驱动 ================== void LCD_WriteCmd(unsigned char cmd) { LCD_RS = 0; LCD_RW = 0; LCD_DATA = cmd; LCD_EN = 1; delay_ms(1); LCD_EN = 0; } void LCD_WriteData(unsigned char dat) { LCD_RS = 1; LCD_RW = 0; LCD_DATA = dat; LCD_EN = 1; delay_ms(1); LCD_EN = 0; } void LCD_Init(void) { LCD_WriteCmd(0x38); // 8位数据,2行,5x7点阵 LCD_WriteCmd(0x0C); // 显示开,光标关 LCD_WriteCmd(0x06); // 文字不动,光标右移 LCD_WriteCmd(0x01); // 清屏 } void LCD_SetCursor(unsigned char row, unsigned char col) { unsigned char addr; if(row == 0) { addr = 0x80 + col; } else { addr = 0xC0 + col; } LCD_WriteCmd(addr); } void LCD_ShowString(unsigned char row, unsigned char col, unsigned char *str) { LCD_SetCursor(row, col); while(*str) { LCD_WriteData(*str++); } } // 显示浮点数到LCD void LCD_ShowFloat(unsigned char row, unsigned char col, float num, unsigned char decimal) { char buf[16]; sprintf(buf, "%0.*f", decimal, num); LCD_ShowString(row, col, (unsigned char*)buf); } // ================== HX711驱动 ================== unsigned long HX711_Read(void) { unsigned long value = 0; unsigned char i; EA = 0; HX711_DOUT = 1; HX711_SCK = 0; while(HX711_DOUT); // 等待数据准备好 for(i = 0; i < 24; i++) { HX711_SCK = 1; value = value << 1; HX711_SCK = 0; if(HX711_DOUT) { value++; } } HX711_SCK = 1; // 第25个脉冲,通道A,增益128 value = value ^ 0x800000; // 补码转无符号偏移值 HX711_SCK = 0; EA = 1; return value; } unsigned long HX711_GetValue(unsigned char times) { unsigned long sum = 0; unsigned char i; for(i = 0; i < times; i++) { sum += HX711_Read(); } return sum / times; } // ================== 标定 ================== void HX711_Calibration(void) { unsigned long v; LCD_ShowString(0, 0, "Calibration"); switch(calibration_step) { case 0: // 第一步:空载采集零点 LCD_ShowString(1, 0, "Zero: Remove"); v = HX711_GetValue(FILTER_TIMES); offset = (long)v; calibration_step = 1; break; case 1: // 第二步:放100g标准重物 LCD_ShowString(1, 0, "Load: 100g"); v = HX711_GetValue(FILTER_TIMES); if(v > offset) { K = 100.0f / (float)(v - offset); calibration_step = 2; } break; case 2: // 标定完成 calibration_step = 0; LCD_ShowString(1, 0, "Done!"); delay_ms(1000); break; } } // ================== 主函数 ================== void main(void) { unsigned long value; float weight; LCD_Init(); delay_ms(100); // 等待HX711上电稳定 // 上电进入标定模式(如需正常称重,可把下面这行注释掉) // 实际使用时可以把标定结果固化在代码里,跳过每次标定 // offset = 8388608; K = 0.001; // 示例默认值 while(1) { value = HX711_GetValue(FILTER_TIMES); weight = ((long)value - offset) * K; if(weight < 0) { weight = 0; } LCD_ShowString(0, 0, "Weight:"); LCD_ShowFloat(0, 8, weight, 1); LCD_ShowString(0, 12, "g"); // 标定按键:P3.2 if(P3_2 == 0) { delay_ms(20); // 按键消抖 if(P3_2 == 0) { HX711_Calibration(); while(P3_2 == 0); // 等待按键松开 } } } } void delay_ms(unsigned int ms) { unsigned int i, j; for(i = 0; i < ms; i++) { for(j = 0; j < 120; j++); } }这段代码在Proteus仿真里编译烧录后,默认会走到标定逻辑。你第一次上电先按键进入标定,空载确认零点,然后放上Proteus里传感器Force=1N的负载,确认100g标定点,之后就正常显示重量。代码里把标定步骤通过LCD提示文字展示出来,操作起来很清楚。
调试代码时有一个建议:先注释掉标定相关代码,直接写死offset和K的默认值(空载读数大约0x800000,K可以先给一个近似值),等确认HX711能读出稳定的值时,再恢复标定逻辑细调系数。这样把问题隔离,先解决"读不到数",再解决"读数不准",排查效率高很多。
我自己做类似项目时还有一个习惯:在代码里预留一个串口调试开关,用宏定义#define DEBUG_UART 1控制。调试时打开串口打印原始值,功能稳定后关掉,LCD显示和串口输出互不干扰。别小看这个习惯,它能帮你省掉至少一半的调试时间。