news 2026/9/7 14:36:38

51单片机光照强度显示程序:BH1750与LCD1602实战解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
51单片机光照强度显示程序:BH1750与LCD1602实战解析

简介:51单片机光照强度显示程序是一份适合嵌入式入门开发者与电子爱好者的完整工程,解决如何通过51单片机读取光照传感器信号,并借助LCD1602液晶屏实时显示环境光照强度的问题。程序涉及ADC模数转换、I2C总线通信、液晶驱动时序控制等多个知识点,代码注释简洁、模块划分清晰。压缩包共23个文件,大小仅39KB,以main.c、lcd.c、i2c.c等C语言源码为主体,配套lcd.h、i2c.h头文件及STARTUP.A51启动文件,同时提供pro.uvproj工程配置、pro.hex可直接烧录的机器码文件,以及编译生成的lst、obj、m51等辅助文件,方便对照学习或二次开发。目前已有1246人学习下载,用户可基于源码中的滤波处理、校准算法与光照阈值提示逻辑,进一步拓展到智能照明、农业环境监控等场景。 很多人在流水灯和数码管之后,第二个想上手的传感器项目往往就是它:51单片机光照强度显示程序。这个项目听起来简单,真正做起来却绕不开一串问题:光敏电阻的阻值怎么变成单片机认识的数字?读到的原始量怎么换算成勒克斯?如果换数字传感器BH1750,I2C时序又该怎么调?再加上LCD1602本身还有对比度、4线模式初始化这些坑,光是让屏幕稳定显示一个可信的亮度值,就能卡住不少人。

这篇文章从我实际做过的方案出发,以数字光照传感器BH1750为主线,把硬件接线、I2C读写、数据换算、显示刷新、常见故障排查全部拆开讲;同时给出“光敏电阻+ADC0832”模拟方案的接线和要点,方便想学ADC原理的同学对照。打算做课程设计、电子竞赛,或者单纯想把传感器采集链路搞明白的,都可以参考。

1. 项目需求拆解:光照检测不是“量个亮度”那么简单

1.1 光照强度这个连续量,怎么变成单片机认识的数字

51单片机判断外界信号只有两种状态:引脚上是高电平还是低电平。开关、按键、红外对射接收头这类数字量信号,直接读IO就行;但“光照强度”是模拟量,它在现实世界中连续变化,电压和阻值都不是规整的0或1,单片机不能直接处理。

所以光照检测的第一件事,是把光信号变成电信号,再把电信号量化成数字。常见做法有三种:

  • 光敏电阻:光线越强,阻值越小。它必须和一个固定电阻串联分压,把阻值变化转换成0~5V的电压,再送进ADC芯片量化。
  • 光敏二极管/三极管:输出电流随光照变化,通常要加运放转换电压,电路复杂一些。
  • BH1750数字光照传感器:内部集成了光电二极管、放大电路、ADC和I2C接口,上电后直接输出16位二进制数据,按公式换算成以“勒克斯”为单位的照度值。

用生活类比来说,光敏电阻像水龙头,水流大小取决于光线,你需要在旁边装一块固定挡板,量挡板两侧的压力差来判断水流;BH1750则像一块自带刻度和屏幕的水表,直接把流量读数告诉你。对于做“显示程序”来说,BH1750省去了复杂的标定流程,这是它成为主方案的根本原因。

1.2 显示端选型:为什么又是LCD1602

显示端也有几个选择:数码管只能显示数字,显示“lx”单位很尴尬,还要维护段码表;点阵屏适合做动态图形和滚动效果,做数字显示有点大材小用;OLED效果最好,但51驱动OLED要自己实现画点、字库,难度陡增。LCD1602是字符型液晶,2行16列,能把“Light: 123 lx”这种完整句子直接打在屏幕上。

更重要的是,LCD1602在51生态里太成熟了。万年历、电子时钟、电子秤、温度报警器这些经典项目全都在用,无论你遇到什么奇怪故障,几乎都能搜到对应案例。所以这个项目我选LCD1602做显示,并且用4线模式来接,目的是省出4个IO口给其他外设。

1.3 两种方案的选型对比表

方案主要器件输出形式是否需要校准代码复杂度适合场景
数字方案BH1750模块I2C输出16位,直接算lx不需要中等,要理解I2C时序课程设计、需要明确照度值
模拟方案光敏电阻+ADC08328位ADC,范围0~255必须自行标定较低,时序简单学习ADC原理、定性判断亮度

为了兼顾“能出结果”和“能学原理”,后面主线展开BH1750方案,同时补充模拟方案的接线和坑。核心的“采集—处理—输出”思想是一致的,换传感器只是换驱动层。

2. 硬件电路搭建:两种方案的引脚连接与关键细节

2.1 元器件清单

硬件部分很简单,不需要额外买太多东西:

  • 51单片机开发板或最小系统板,常见STC89C52RC,晶振11.0592MHz或12MHz都行。
  • BH1750光照传感器模块。
  • LCD1602液晶屏加10K电位器。
  • 杜邦线若干,最好再备一块面包板做扩展。
  • 如果走模拟方案:光敏电阻(5516/5506比较常见)、10K定值电阻、ADC0832模块。

不管你用的是江科大那类开发板,还是金沙滩的板子,引脚定义都要以自己板子上的原理图为准。下面给的接线是针对标准开发板缺省引脚的示例,实际使用时要灵活调整。

2.2 数字方案:BH1750与51单片机的接线表

BH1750引脚51单片机引脚说明
VCC5V模块带稳压电路时可直接接5V
GNDGND共地
SCLP3.6I2C时钟线
SDAP3.7I2C数据线
ADDRGND设备地址选择为0x23

这里有两个细节值得说。第一,为什么选P3.6/P3.7而不是P1口?因为很多开发板的P1口已经被LED、矩阵键盘占用了,P3.0/P3.1要留给串口,P3.2/P3.3是外部中断入口,P3.6/P3.7正好空出来做I2C。当然这只是硬件资源规划的习惯,你用P1口模拟I2C一样能跑通。

第二,BH1750裸芯片的工作电压是2.4V~3.6V,但市面上绝大多数模块都带稳压和上拉电路,5V供电没问题。如果在洞洞板上自己焊裸芯片,先确认模块背面没有稳压芯片再上电,否则芯片发热、读数异常甚至直接烧毁。

2.3 显示模块:LCD1602的4线接法

LCD1602引脚51单片机引脚说明
VSSGND电源地
VDD5V电源正
VO电位器中间脚对比度调节
RSP2.0寄存器选择
RWP2.1读写选择
ENP2.2使能脉冲
D4~D7P2.4~P2.74位数据线
A5V背光正极
KGND背光负极

RW引脚可以直接接地,因为本项目只写不读。但建议还是用IO口控制,方便以后扩展读忙标志。4线模式只需要接高4位数据线,命令先送高4位再送低4位,能省出P2.3和P0口全部引脚。

VO接电位器这个细节非常关键。新买的1602上电后屏幕全黑,十有八九不是接错线,而是对比度电位器拧在了不合适的位置。把电位器从一端缓缓拧到另一端,会看到屏幕从全黑逐渐过渡到出现清晰字符。有条件的可以用万用表量一下,VO电压在0.4V~1V左右时字迹最清楚。

2.4 模拟方案:光敏电阻+ADC0832的接法

ADC0832引脚51单片机引脚功能
CSP1.0片选
CLKP1.1时钟输入
DI/DOP1.2数据输入/输出共用引脚
CH0光敏分压中间点通道0模拟输入

光敏电阻分压电路这样搭:光敏电阻一端接VCC,另一端与10K定值电阻串联到GND,两个电阻的中间节点接ADC0832的CH0。光照越强,光敏电阻阻值越小,节点电压越低;光照越弱,阻值越大,节点电压越高。建议在CH0对地并联一个0.1uF陶瓷电容,过滤高频干扰,否则读数容易跳。

如果做这个模拟方案,10K定值电阻不是固定的,要根据实际环境光照范围调整。可以用电位器代替定值电阻,调到一个采样值变化最明显的阻值位置,再换成同阻值的固定电阻。

3. 核心代码实现:从I2C时序到LCD显示的完整通路

3.1 工程拆分:不要把全部代码堆进main.c

51程序不大,但照样建议拆文件。我在工程里建了lcd1602.c、bh1750.c、main.c三个模块。模块化的好处是出问题容易定位:屏幕没显示,基本不用看bh1750.c;读数全是255,问题多半在传感器驱动或接线,不会去翻LCD代码。以后想把BH1750换成其他传感器,或者把LCD1602换成OLED,只改对应模块,主程序基本不动。

3.2 LCD1602驱动:4线模式的初始化顺序

LCD1602初始化顺序是最大的坑,先看代码:

void Lcd1602_Init(void) { Lcd_Delay(1000); // 等待液晶上电自检 Lcd1602_WriteCmd(0x30); Lcd_Delay(500); Lcd1602_WriteCmd(0x30); Lcd_Delay(500); Lcd1602_WriteCmd(0x30); Lcd_Delay(500); Lcd1602_WriteCmd(0x20); // 切换为4线模式 Lcd1602_WriteCmd(0x28); // 2行显示,5x7点阵 Lcd1602_WriteCmd(0x0C); // 开显示,关光标 Lcd1602_WriteCmd(0x01); // 清屏 Lcd_Delay(1000); Lcd1602_WriteCmd(0x06); // 地址指针自动加1 }

为什么前面要连续发三个0x30?因为LCD1602上电后默认认为自己工作在8线模式,如果硬件只接了D4~D7,直接发0x28这种4线命令它是听不懂的。必须先用8线模式命令把它唤醒,再发0x20切换成4线模式,最后才是功能设置。这个顺序错一步,屏幕上就会出现乱码,或者只有第一行有反应。

3.3 BH1750驱动:I2C时序代码逐段解读

STC89C52没有硬件I2C外设,所以只能用软件模拟。BH1750的7位地址由ADDR引脚决定,ADDR接GND时地址是0x23,左移一位后写地址是0x46,读地址是0x47。核心代码:

void I2C_Start(void) { SDA = 1; SCL = 1; I2C_Delay(); SDA = 0; I2C_Delay(); SCL = 0; } void I2C_Stop(void) { SDA = 0; SCL = 1; I2C_Delay(); SDA = 1; I2C_Delay(); } void I2C_WriteByte(unsigned char dat) { unsigned char i; for(i = 0; i < 8; i++) { SDA = (dat & 0x80) ? 1 : 0; I2C_Delay(); SCL = 1; I2C_Delay(); SCL = 0; dat <<= 1; } SDA = 1; // 释放SDA,等待从机应答 SCL = 1; I2C_Delay(); SCL = 0; }

I2C的起始条件是SCL高电平期间SDA产生下降沿,停止条件是SCL高电平期间SDA产生上升沿。写字节时数据在SCL高电平期间必须保持稳定,所以在SCL拉高之前先把SDA设置好,这个先后顺序不能反。

BH1750的命令不多,这个项目只用到两条:0x01上电,0x10连续高分辨率模式。连续高分辨率模式分辨率是1lx,测量时间约120ms。初始化函数:

void BH1750_Init(void) { BH1750_WriteCmd(0x01); // 上电 BH1750_WriteCmd(0x10); // 连续高分辨率模式 }

读取光强的函数:

unsigned int BH1750_ReadLux(void) { unsigned char high, low; unsigned int lux; I2C_Start(); I2C_WriteByte(0x47); // 读地址 high = I2C_ReadByte(); // 读高字节 I2C_SendAck(0); // 发送应答 low = I2C_ReadByte(); // 读低字节 I2C_SendAck(1); // 发送非应答 I2C_Stop(); lux = (high << 8) | low; lux = lux * 10 / 12; // 除以1.2,整数运算 return lux; }

换算公式是厂商手册规定的:高分辨率模式下,寄存器值除以1.2就是勒克斯。这里我特意不用浮点除法,因为51内核没有硬件浮点单元,调用float库会占用几千字节Flash,运行速度也慢。用整数运算lux * 10 / 12结果一样,代码却小得多。

3.4 主循环与显示格式:好读比炫技重要

主程序逻辑很直接:

void main(void) { unsigned int lux; Lcd1602_Init(); BH1750_Init(); Lcd1602_ShowString(0, 0, "Light: "); while(1) { lux = GetAverageLux(10); DisplayLux(lux); DelayMs(500); } }

显示函数有个小讲究。LCD1602显示过的字符不会自动清除,如果上一次显示“1234 lx”,这次变成“5 lx”,屏幕上会残留“34”之类的残影。所以显示时要固定输出4位数字,单位后面再补两个空格清残影:

void DisplayLux(unsigned int lux) { unsigned char digit[4]; digit[0] = '0' + lux / 1000 % 10; digit[1] = '0' + lux / 100 % 10; digit[2] = '0' + lux / 10 % 10; digit[3] = '0' + lux % 10; if(lux < 1000) digit[0] = ' '; if(lux < 100) digit[1] = ' '; if(lux < 10) digit[2] = ' '; Lcd1602_SetCursor(1, 0); Lcd1602_WriteData(digit[0]); Lcd1602_WriteData(digit[1]); Lcd1602_WriteData(digit[2]); Lcd1602_WriteData(digit[3]); Lcd1602_ShowString(1, 4, " lx "); }

刷新间隔控制在500ms。光照变化本来就是缓慢过程,人眼不需要50ms一刷。刷新太快反而会让字符抖动闪烁,看起来非常廉价。

3.5 软件滤波:为什么读数一直在跳

光照值跳动有三个来源:光照本身波动(日光灯有100Hz频闪)、电源纹波、LCD和传感器共用电源带来的干扰。数字传感器BH1750还算稳定,模拟方案会更明显。解决办法是软件滤波:

unsigned int GetAverageLux(unsigned char times) { unsigned long sum = 0; unsigned char i; for(i = 0; i < times; i++) { sum += BH1750_ReadLux(); DelayMs(15); } return (unsigned int)(sum / times); }

连续读10次取平均,显示立即稳很多。注意BH1750的测量时间是120ms,如果循环读太快,两次读数之间测量还没完成,读到的就是旧数据,看上去忽大忽小。加延时不仅是为了滤波,也是给传感器足够的测量时间。

4. 调试验证与踩坑实录:从乱码到0xFF的完整排查链路

4.1 LCD1602全黑或全是方块时,先查这三处

第一处是VO对比度。新屏的电位器可能在一个极端位置,导致屏幕全黑。转动10K电位器,直到看见清晰字符,这个问题五秒钟就能排除。

第二处是初始化顺序。4线模式必须按“0x30三次到0x20再到0x28”的顺序来,缺少前面三个0x30就会出现乱码或只有第一行有反应。这个在上面代码里已经强调过。

第三处是EN引脚脉冲宽度。LCD1602像一台老式打字机,EN引脚是空格键,只有按的时间足够长,它才来得及把字符打上去。很多示例代码里EN拉高时间只有几个空指令,在12MHz主频下勉强能跑;如果你换了大屏、长线或者仿真器,就可能采样不到数据。把写命令函数里EN拉高时间加长到50us左右,问题基本能解决。

4.2 BH1750读回0xFF的完整排查过程

有次我调板子,BH1750读回来的一直是0xFF。第一反应是I2C时序错了,对着手册改了好几次起始条件和延时,都没用。后来用万用表量SDA和SCL的静态电压,发现SDA一直低、SCL正常高,顺着杜邦线一查才发现,SDA插到了P3.6,SCL插到了P3.7,跟代码里定义正好反了。

这个错误之所以隐蔽,是因为I2C是双向线,SDA上一直有从机拉低的信号,从波形上看又像是有应答,特别容易让人误以为是时序问题。后来我长记性,上电之前一定先对着接线表逐根确认,而不是急着改代码。

排查步骤总结一下:

  1. 先量模块VCC对地电压,确认是5V(或3.3V方案),排除供电不足。
  2. 写一段测试代码:让SCL置高、SDA置高,用万用表量对应引脚电压,确认引脚定义和实际接线一致。
  3. 再测起始条件:执行I2C_Start后,SDA先从高变低,如果没有跳变,说明电路连接或IO定义有问题。
  4. 最后才扫I2C地址:向0x46发写命令后(对应ADDR接GND的写地址),用示波器观察第9个时钟是否有低电平应答位。

如果手头有逻辑分析仪,直接抓波形是最快的,没有的话,万用表电压法也能解决大部分问题。

提示:如果你是在Proteus里做仿真,BH1750的仿真模型不一定有。想先验证显示逻辑,可以用ADC0832方案代替,或者在开发板上直接实测,不要过分依赖仿真。

4.3 数字跳变:先别怀疑传感器,检查电源和刷新策略

读数跳变时很多人的第一反应是传感器坏了,其实问题往往出在三个方面。光照本身就在波动,窗户附近的自然光、日光灯的频闪都会让传感器采到变化;开发板从USB取电,又带LCD又带传感器,5V纹波可能偏大;杜邦线过长或接触不良也会引入噪声。

解决的顺序不要乱:先加软件滤波,再看刷新间隔,最后换独立5V电源或理顺杜邦线。实测下来,滤波和刷新间隔两个措施就能让显示稳定很多。如果数据还是跳,把USB供电换成DC电源适配器,或者给模块单独供电并共地,问题基本能解决。

4.4 模拟方案ADC0832的特别提醒

如果走光敏电阻加ADC0832的路线,最坑的是DI和DO共用一根引脚。51的准双向IO在读取外部信号前,必须先向引脚写1,把内部上拉打开。很多照抄代码的人容易漏掉这一步,导致读到的永远是0或者乱跳。

通道选择的时序也容易错:START之后,第一个时钟下降沿选择CH0还是CH1,第二个下降沿选择单端还是差分模式,顺序反了数据完全不对。这个方案的时序相比BH1750的I2C更繁琐,新手如果调不出来,建议先换BH1750把整个流程跑通,等把ADC原理学明白再回来调。

5. 进阶方向:从“显示程序”到“闭环控制系统”

5.1 阈值控制:让光照值自己去开灯

显示只是第一步,加上阈值判断就能变成自动控制。在while循环里加几行代码:

if(lux < 30) LED = 1; // 太暗,打开补光灯 else LED = 0;

把LED换成继电器,就能控制真正的照明设备。这个方向本质上是“传感器采集→逻辑判断→执行输出”的闭环,和智能窗帘、智能灯光控制是同一套思路,也是从“显示程序”升级为“控制系统”的最短路径。

5.2 串口上报与数据可视化

把光照值通过串口发到上位机,是排查问题的最好手段。初始化串口波特率9600,直接printf("lux=%d lx\r\n", lux),用串口助手就能看到数据曲线。这个功能的额外价值是:当LCD显示异常时,用串口看数据可以迅速判断问题在传感器还是显示端,不用拿着万用表到处戳。

5.3 怎么让这个项目在答辩或汇报时更好讲

如果这是课程设计,建议至少包含三个功能点:实时显示、越限报警、串口上报。再手工做一张照度等级说明表,比如办公室环境通常是300~500lx,户外阳光在10000lx以上,答辩时能把显示数值的实际意义讲清楚,评委印象分会高不少。

再进阶一点,可以加两个按键调节阈值,第二行显示“TH: xxx lx”,项目就有了参数设置功能。把BH1750的数据同时发给OLED和LCD,或者加上蓝牙模块让手机看数据,都是比较出彩的扩展方向。

最后分享一个小技巧:LCD1602的刷新不要放在while循环里裸奔。我第一次做的时候,每次读完光强立刻写屏,结果字符一直在抖,看着非常廉价。后来把刷新间隔控制在500ms,才稳定下来。光照变化本来就是缓慢过程,人眼根本不需要50ms一刷。同样是显示程序,刷新策略、格式化输出和滤波处理才是拉开差距的地方。希望这篇能把你的51单片机光照强度显示程序一次调通,少走我当年走过的弯路。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 14:35:17

MaaFgo v1.2升级指南:稳定、可控、可复用的自动化挂机工具

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 14:34:49

AI女友产品拆解:从大模型到情感陪伴系统的技术架构与落地实践

我一直觉得&#xff0c;希腊神话里皮格马利翁的故事&#xff0c;是整个赛博时代最好的隐喻。那个国王雕刻了一尊少女像&#xff0c;日复一日地凝视她、和她说话&#xff0c;最终爱上了自己的作品。到了今天&#xff0c;我们把雕像换成了大模型生成的一段段对话&#xff0c;把雕…

作者头像 李华
网站建设 2026/9/7 14:33:18

OneDrive本地同步配置与无法登录卸载安装排障指南

你电脑里的OneDrive&#xff0c;到底是用来存东西的&#xff0c;还是只会占着任务栏图标给你添堵的&#xff1f;这段时间我帮不少同事和朋友处理过OneDrive的同步问题&#xff0c;发现大多数人根本分不清自己的文件是在云端还是本地。这个标题问的其实就是一件很基础又很实用的…

作者头像 李华