太阳能锂电池充电监测系统:51单片机ADC采样与LCD显示设计
2026/9/13 9:16:57 网站建设 项目流程

简介:基于51单片机太阳能锂电池充电电压电流检测液晶显示设计,是一套面向电子设计与嵌入式初学者的完整课设/毕设资源,覆盖太阳能充电管理、充电电压与电流采集、LCD1602液晶显示及过压过流保护等核心环节,可帮助解决从电路搭建到程序调试的系列问题。包内共121个文件,压缩包约57MB,以jpg电路图、doc/docx设计文档、pdf芯片资料、mp4演示视频以及c/h程序源码为主,兼顾原理图、源码、论文和答辩材料,目录结构清晰便于按阶段查阅。目前已有1107人学习下载,作为含程序、电路、文档的一体化方案,适合毕业设计、电子竞赛或单片机实训时快速参考与二次开发。

1. 太阳能锂电池充电监测系统在讲什么

把一块太阳能板、一颗锂电池、几片充放电管理芯片、一颗51单片机和一块液晶屏拼在一起,做成一个能边充电边把电池电压、充电电流实时显示出来的装置,这就是这类设计的主体。对做过工程的人来说,太阳能板并不是一个稳定电源,它的输出随光照飘,直接接到锂电池上既充不满也容易过充;锂电池又需要恒流/恒压两阶段充电,所以中途必须有一颗充电管理IC把关。51单片机在这里承担“看门狗加前台”的角色:把电压电流采进来,换算成真实值,送到液晶上。这类题目常见于单片机课程设计和电子竞赛,难点反而不在51,而在采样和标定,显示的数字能不能跟万用表对上,才是验收的关键。

2. 硬件选型与采样通路搭建:太阳能充电回路与电压电流检测电路

2.1 太阳能板的输出特性与充电管理芯片怎么配

太阳能板在光照足够时开路电压往往远高于标称值,比如标称5.5V的板子空载可能到6V以上,而且内阻随光照变化很大。锂电池的充电曲线是先恒流(CC)充到4.2V,再转恒压(CV)把电流压下去,不能靠简单串电阻和二极管解决。所以充电管理芯片的输入范围、耐压和是否带光伏适配逻辑,是选型时第一件要想清楚的事。

常见做法是三选一:

芯片类型输入电压典型值特点
CN3058线性充电管理4.0~6.0V适合小功率太阳能板直连,电流由外部电阻设定
TP4056线性充电管理4.0~8.0V最常见,使用时要保证VCC稳定在5~6V
CN3083线性充电+光伏适配4.0~6.0V内部有MPPT逻辑,适合光照波动场景

做课程设计,我一般会优先推荐 CN3058 或 TP4056 配一块小功率太阳能板。如果希望“太阳能板直接怼上去也能正常充电”,CN3083 这类带光伏适配的芯片更省心。不要为了图省事直接用一个5V稳压电源做验证,因为最后接太阳能板测试时,芯片会频繁进入欠压保护,到那时再换方案代价很高。

2.1.1 输入端保护和电压裕量

太阳能板正负极接错会把充电管理芯片打穿,输入端至少串一颗肖特基二极管,或者依赖芯片自带的防反接电路。选型时还要看数据手册里的绝对最大输入电压,留出20%以上的裕量,不要卡着6V用标称6V的光板,否则光照强时很容易超限。

2.2 电流检测:采样电阻放高端还是低端

电流检测有两种惯用位置:低端采样把采样电阻串在地回路里,高端采样把采样电阻串在电源正端。低端采样电路简单,同相放大即可,但采样电阻会把地电位抬起来,单片机的地和电池负极之间出现一个随电流变化的电位差,系统里又有液晶背光、充电电流,这个“地弹”会直接影响ADC精度。所以我一般倾向把采样电阻放在电池正端回路里,也就是高端采样,用INA180这一类的电流检测放大器把压差放大后再进ADC。INA180的共模输入范围可以到26V,太阳能充电系统里不会超。

采样电阻取值要同时照顾压降、功耗和信号幅度。锂电池充电电流按0.5A设计时,用0.05Ω电阻,满电流压降只有25mV,经INA180增益20倍后输出0.5V,这个幅度对51单片机的10位ADC很合适。如果电阻取0.1Ω,压降50mV,输出1V,分辨率更高,但电阻功耗为25mW,仍在0.25W电阻的安全范围内。

2.2.1 不接运放直接采差分电压行不行

有人在PCF8591的两个模拟输入端直接接采样电阻两端测压差,这种接法在8位ADC下基本不可用:5V参考电压下1LSB约19.5mV,对应0.05Ω电阻约0.39A,电流显示会一跳一个值。要用51单片机做电压电流检测,一级差分放大至少要有10到20倍增益,这是硬需求,不能省。

2.3 电压检测:电池电压和太阳能板电压分两路采集

锂电池端电压要采,太阳能板输入端电压也要采,不要为了省一个ADC通道就把两个检测点并在一起。判断太阳能板是否在发电、估算充电效率都需要光伏端电压数据。51单片机的ADC参考电压是5V,电池最高4.2V按理可以直接接,但为了和充电引起的地电位抖动隔离,标准做法还是分压后再采样。

电池侧建议R1=20kΩ接电池正极,R2=20kΩ接地,分压比0.5,4.2V电池送到ADC引脚才2.1V;太阳能板侧按板子开路电压选分压比,标称6V板子同样按0.5分压。分压电阻的取值原则是:阻值不能太大,否则采样保持电容充不满;也不能太小,否则白白耗电。20kΩ+20kΩ在电池4.2V时流过约0.1mA电流,对课程设计完全够用,同时可在ADC引脚并联一个0.1μF电容,降低采样瞬间的电压跌落。

2.4 采样回路的参数对照表

下面这张表是搭这套采集通路时的默认参数。上电后先用万用表量一遍分压点实际电压,不要急着把5V和分压比写进代码,等真实值测出来再更新。

采样点器件/回路设计值备注
电池电压R1/R2分压后接P1.0分压比0.5万用表实测后修正代码常量
充电电流BAT+串0.05Ω采样电阻,INA180输出接P1.10.5A对应0.5VINA180增益20倍
太阳能电压R3/R4分压后接P1.2按板子空载电压选建议实测量程
LCD1602P0.0~P0.7加P2.5~P2.78位模式P0口需要外接上拉

把预设计参数集中在代码顶部,后续换量程只改宏:

#define SUPPLY_VOLTAGE_MV 5000UL /* ADC参考电压,实测后用真实值替换 */ #define BAT_DIV_RATIO 2.0f /* 1/分压比,即(R1+R2)/R2 */ #define PV_DIV_RATIO 2.0f /* 太阳能板分压比,按实测改 */ #define SENSE_RES_OHM 0.05f /* 采样电阻阻值 */ #define SENSE_GAIN 20.0f /* INA180增益 */

这些宏决定了后面所有工程量换算,也是整篇代码里最值得先校准的地方。SUPPLY_VOLTAGE_MV如果直接写5000,而实际稳压输出只有4.95V,显示值会整体偏高1%,这就是很多板子“显示电压和万用表对不上”的根源。

3. 51单片机ADC采样与工程量换算:从原始码到真实电压电流

3.1 先看手头芯片有没有片内ADC:STC89C52还是AT89C52

51单片机并非原生都有ADC。常见的AT89C52没有片内模数转换器,要外挂PCF8591或ADC0809;而STC89C52RC、STC12C5A60S2这些增强型51内置了ADC,直接省掉一个外设。判断方法非常简单:看数据手册里有没有ADC_CONTR这个特殊功能寄存器,或者直接查型号后缀。这也是51单片机数字电压表类设计里常见的两条实现路线:有片内ADC就用片内,没有片内ADC就外挂。

在Proteus仿真里,STC系列的单片机ADC模型支持度一般,很多人会用AT89C52配合电位器加PCF8591来完成仿真,这就是为什么网上大量资料默认走外挂ADC。如果你手上的实物板是STC89C52RC,优先用片内ADC,电路少一根I2C线,采样速度也够用。只有用到AT89C52或Proteus仿真需要时,才走PCF8591路线。

3.2 STC片内ADC的寄存器操作和通道切换

STC89C52RC的ADC是10位、8通道,挂在P1口上。需要操作三个特殊功能寄存器:

寄存器地址作用
ADC_CONTR0xBCADC开关、启动控制、通道选择与转换完成标志
ADC_DATA0xAD转换结果高8位
ADC_LOW20xBE转换结果低2位

关键位是ADC_CONTR的bit7打开ADC电源,bit4是转换完成标志ADC_FLAG,bit3写1启动转换,bit2~bit0选择模拟通道。下面这段查询式读取函数是常用的基础代码:

sfr ADC_CONTR = 0xBC; sfr ADC_DATA = 0xAD; sfr ADC_LOW2 = 0xBE; unsigned int read_adc(unsigned char channel) { unsigned int result = 0; ADC_CONTR = 0x80 | 0x08 | (channel & 0x07); /* 开ADC电源,启动转换,选通道 */ _nop_(); _nop_(); /* 等待电源稳定 */ while (!(ADC_CONTR & 0x10)); /* 轮询ADC_FLAG,等待转换完成 */ ADC_CONTR &= ~0x10; /* 软件清零标志位 */ result = (ADC_DATA << 2) | (ADC_LOW2 & 0x03); /* 拼接10位结果 */ return result; }

0x80是ADC_POWER,0x08是ADC_START,channel & 0x07把通道号限制在0~7。while循环如果ADC引脚被外部强拉力拉死会一直卡住,工程上通常加一个超时计数,到最大计数值就按上一次结果处理。这个细节在实际板子偶发卡程序时很有用。Keil5建工程时,芯片型号可以选STC89C52RC,没有对应器件就先选AT89C52编译,烧录时再用STC-ISP按实际型号下载。

3.3 外挂PCF8591的I2C读取写法与首读丢弃

做Proteus仿真或者用老式AT89C52时,PCF8591是低成本替代方案。它有一个典型坑:芯片上电后的第一次读,返回的是上一次通道的旧转换值,因此连续读取时要先读一次丢掉,第二次读到的才是当前通道的有效结果。手写I2C时,读写地址、控制字和通道选择要对齐:

unsigned char pcf8591_read(unsigned char channel) { unsigned char value; I2C_Start(); I2C_SendByte(0x90); /* 器件地址,写方向 */ I2C_SendByte(0x40 | (channel & 0x03)); /* 0x40打开ADC,低2位选通道 */ I2C_Stop(); I2C_Start(); I2C_SendByte(0x91); /* 同一地址,读方向 */ value = I2C_RecvByte(); /* 第一次读,旧数据,丢弃 */ value = I2C_RecvByte(); /* 第二次读,当前通道 */ I2C_SendNack(); /* 主机回NACK,结束本次读 */ I2C_Stop(); return value; }

控制字0x40表示使能ADC并且使用单端输入,低2位选择通道,只接两路模拟量时够用。8位ADC的工程量换算比较粗暴:raw/256乘以参考电压,分辨率约20mV,电压显示到0.01V时末位会一直跳,这是8位精度极限,不是程序写错了。

注意:PCF8591连续读取时,第一次读到的旧数据必须主动丢弃,否则显示值会延迟一个采样周期。

3.4 采样均值滤波:先清飞点再平均

ADC第一次上电或通道切换瞬间容易采到异常值,直接平均会把一个1023的毛刺带进结果。我习惯先做一次滤波:连续采5个值,丢弃最大值和最小值,再对剩下3个取平均。对充电电流这种变化很慢的直流信号,这套处理已经足够,不需要上复杂的滑动平均滤波器。

unsigned int filter_adc(unsigned char channel) { unsigned char i, j; unsigned int buf[5], sum = 0, tmp; for (i = 0; i < 5; i++) { buf[i] = read_adc(channel); for (j = i; j > 0; j--) { /* 插入排序,读完一个排一个 */ if (buf[j] < buf[j - 1]) { tmp = buf[j]; buf[j] = buf[j - 1]; buf[j - 1] = tmp; } } } for (i = 1; i < 4; i++) sum += buf[i]; /* 去掉最小和最大 */ return sum / 3; }

注意buf[5]sum的类型。五个10位数的和最大不超过5115,用unsigned int不会溢出;但如果你把采样次数提高到32次,就必须换unsigned long,否则Keil C51下会得到回绕后的错误值。这里取5采3,是速度和稳定性的折中。

3.5 工程量换算:参考电压、分压比和增益集中在一处

有了滤波后的原始码,换算是固定的几个公式:电压等于raw/1024乘以参考电压再乘以分压比倒数;电流等于raw/1024乘以参考电压,再除以增益和采样电阻的乘积。注意分压比是“真实电池电压/ADC引脚电压”,别把R2/(R1+R2)搞混。

float get_battery_voltage(void) { unsigned int raw = filter_adc(CH_BAT); float adc_volt = (float)raw / 1024.0f * ((float)SUPPLY_VOLTAGE_MV / 1000.0f); return adc_volt * BAT_DIV_RATIO; } float get_charge_current(void) { unsigned int raw = filter_adc(CH_CURR); float adc_volt = (float)raw / 1024.0f * ((float)SUPPLY_VOLTAGE_MV / 1000.0f); return adc_volt / (SENSE_GAIN * SENSE_RES_OHM); }

SUPPLY_VOLTAGE_MV只在换算入口出现一次,标定时把它改成万用表实测电压,就能整体修正ADC参考电压带来的误差。51单片机上用float做乘除会多占几百字节ROM,但这类采样显示系统代码空间通常够用,清晰比极端优化更重要。

4. LCD1602液晶显示设计:界面布局、刷新与充电状态机

4.1 LCD1602走8位还是4位数据线

51最小系统板的IO一般够用,LCD1602用8位并行模式最稳妥,初始化时序简单,不容易犯4位模式下半字节错位的毛病。4位模式能省4个IO,但写一个字节要拆成两次发送,调试时对着逻辑分析仪看不出哪次是高四位,课程设计阶段不建议为省IO徒增复杂度。8位接线固定为P0接数据口,P2接控制口。

#define LCD_DATA P0 sbit LCD_RS = P2^6; sbit LCD_RW = P2^5; sbit LCD_EN = P2^7; void lcd_write_cmd(unsigned char cmd) { LCD_RS = 0; /* 命令模式 */ LCD_RW = 0; /* 写方向 */ LCD_DATA = cmd; LCD_EN = 1; _nop_(); _nop_(); LCD_EN = 0; /* 下降沿锁存 */ delay_short(40); /* 命令执行时间 */ } void lcd_set_cursor(unsigned char row, unsigned char col) { lcd_write_cmd((row == 0) ? (0x80 + col) : (0xC0 + col)); }

LCD_RW始终写0,不使用读忙标志,靠延时兜底。在11.0592MHz晶振下,delay_short(40)大约40微秒,能覆盖LCD控制器大多数命令执行时间。如果改用查询忙标志,需要先把P0切到输入模式再读BF位,代码复杂度更高,对显示速度没有实质提升。

提示:P0口内部没有上拉电阻,8位数据线必须外接10kΩ排阻,否则LCD1602会偶发乱码或花屏。

4.2 显示内容的排版:固定字符和刷新变量分开

界面可以两行解决:第一行显示电池电压和充电电流,第二行显示太阳能板电压和充电状态。题目关心的电压电流检测结果要一眼可见,单位固定写在字符串里,数值部分单独刷新。用sprintf拼字符串在51上是很大的坑,stdio.h的整型转换和浮点支持占用大量ROM,而且Keil下还要重定向putchar,很可能一调sprintf整个LCD就不动了。常见做法是自己把数值拆成字符。

屏幕布局建议如下:

屏幕位置固定内容变量内容刷新周期
第1行第0~5列V=4.10200ms
第1行第8~13列I=0.45A200ms
第2行第0~6列Solar=5.82V200ms
第2行第8~15列State:CC/CV/FULL状态变化时

电压值先换算成毫伏整数,再做除法取位。例如4.15V存成4150,整数位是4150/1000=4,小数位分别是(4150/100)%10=1和(4150/10)%10=5。这样完全避开浮点格式化:

void lcd_display_voltage(unsigned char row, unsigned char col, float volt) { unsigned int mv = (unsigned int)(volt * 1000.0f + 0.5f); /* 四舍五入到毫伏 */ lcd_set_cursor(row, col); lcd_write_char(mv / 1000 + '0'); lcd_write_char('.'); lcd_write_char((mv / 100) % 10 + '0'); lcd_write_char((mv / 10) % 10 + '0'); lcd_write_char('V'); }

+0.5f是为了抵消浮点截断误差,比如4.199V算成4199还是4200,加半毫伏再取整更稳。lcd_write_char内部就是置LCD_RS=1后,按写命令同样的时序输出一个字节。

4.3 刷新策略:按节拍只改变化的位置

LCD1602本身是静态显示,不需要像TFT-LCD液晶显示模组那样不停刷整屏,但如果在while循环里每几毫秒重写整行,字符会肉眼可见地闪烁。正确做法是用定时器做一个200ms的刷新节拍,每次刷新把电压、电流字符串写到固定位置;只有按键切页或状态变化时才清屏。

unsigned char refresh_flag = 0; void timer0_isr(void) interrupt 1 { static unsigned char tick = 0; TH0 = 0x4C; TL0 = 0x00; /* 11.0592MHz模式1,50ms重装初值 */ if (++tick >= 4) { /* 4x50ms=200ms */ tick = 0; refresh_flag = 1; } } void main(void) { init_timer0(); lcd_init(); while (1) { if (refresh_flag) { refresh_flag = 0; v_bat = get_battery_voltage(); i_chg = get_charge_current(); v_pv = get_pv_voltage(); lcd_display_ui(v_bat, i_chg, v_pv); } if (key_pressed()) handle_key(); /* 按键切页或进入标定状态 */ } }

200ms的节拍既能看清电流变化,人眼又不会觉得闪烁。51的浮点运算本身较慢,采样加换算在200ms周期内完全执行得完,不存在实时性压力。如果以后把刷新周期改到50ms,就要考虑把ADC采样和显示拆成两个异步任务。

4.4 充电状态判断:CC、CV、FULL三态比干显示电压直观

锂电池充电过程阶段明显,只显示4.18V和0.12A,不如直接显示“恒流充电”“恒压充电”“已充满”。三态判断只需要电压和电流两个输入量:

typedef enum { CHG_CC, CHG_CV, CHG_FULL } charge_state_t; charge_state_t get_charge_state(float v_bat, float i_chg) { if (v_bat < 4.10f) return CHG_CC; /* 电压低,恒流阶段 */ if (v_bat < 4.20f && i_chg > 0.08f) return CHG_CV; /* 接近满电但还在充电 */ return CHG_FULL; /* 充电完成 */ }

状态阈值要和充电管理芯片的终止电流对应起来。TP4056默认的截止电流是设定充电电流的1/10,0.5A设计时约50mA转灯,代码里的0.08fA留了一点回差,避免在边界反复横跳。状态变化时清屏一次并刷新状态字符,比每200ms一直重写状态更稳当。

5. 系统标定与误差校验:让LCD显示值和万用表实测值对上

5.1 两点标定比靠电阻标称值靠谱得多

电阻标称值有1%误差,ADC参考电压随稳压芯片批次漂移,只靠理论分压比和标称Vref算出来的显示值,误差普遍在0.1V以上。正确的做法是拿一个可调电源在ADC输入点加两个已知电压,读出对应原始码,再做增益和零点补偿。

两点标定的公式是:假设在ADC输入点量到V1和V2,对应的滤波原始码是raw1和raw2,则增益系数k=(V2-V1)/(raw2-raw1)。程序里不要再用1024/5V的理论值,直接按volt = k*(raw-raw1)+V1计算。这样把分压电阻误差、Vref误差和ADC非线性带来的系统误差一次吸收掉。

标定参数可以整理成一个结构体:

typedef struct { int raw_zero; /* 0V输入处的原始码 */ float volts_per_bit; /* 增益系数,单位V/bit */ } adc_scale_t; float adc_to_volt(adc_scale_t s, int raw) { return (float)(raw - s.raw_zero) * s.volts_per_bit; }

标定顺序是:先在上电无信号状态记录raw_zero,再输入一个已知电压记录第二点,算出volts_per_bit。把这两个值存到STC芯片的DataFlash里,上电先读,读不到再用默认值。STC的IAP读写例程网上很多,直接复制到工程即可,这是课程设计里避免每次重新烧录标定值的一个加分层操作。

5.2 电流标定:用电子负载或压降法,别直接串万用表

电流通路标定不能只靠电压公式反向推导。我一般用可调电子负载设置恒流0.2A、0.4A、0.6A三个点,同时记录LCD显示值和电子负载上的电流读数,做三点校验。如果手头没有电子负载,可以用大功率电阻并联散热风扇作为负载,但电流会随电池电压变化,读数不稳定。串万用表测电流时,要把表笔插到10A插孔,普通200mA挡内阻加上接触电阻会直接改变充电电流,测出来的值本身就不准。

现场最可靠的自检方法是压降法:用万用表毫伏挡测采样电阻两端电压,再按I=U/R算出真实电流。万用表毫伏挡内阻足够大,不会干扰原电路,这比任何估算都直接。

5.3 三个容易翻车的点

现象原因检查方法
电压显示稳定但整体偏高0.2VVref不是5.00V,或分压电阻标称值与实际偏差大用万用表量ADC引脚电压,反推实际分压比
电流始终是0.00A采样电阻被跳线短路,或INA180输出没接对ADC通道断电后用万用表电阻挡测采样电阻两端
上电后LCD只有方块LCD初始化时序太短,或P0口没有上拉排阻用逻辑分析仪抓EN下降沿,检查初始时序

最后一个技巧:把标定参数和充电状态阈值放到同一个头文件里管理,标定完一组数据立即固化到DataFlash,并把采样时间戳一起存进去。这样上课验收时可以直接打开数据显示上次标定结果,而不是重新插拔烧录器反复试。

校准做完,用满电和亏电的锂电池各跑一次完整充电,对比万用表实测曲线,如果CC/CV切换点的电压误差在±0.03V以内,说明采样链路和显示链路都可信,下一步再去加数据记录或者蓝牙上报,不要让显示端继续消耗调试时间。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询