☰
51单片机光照强度显示:光敏电阻与BH1750方案对比与实现
2026/10/15 19:50:02 网站建设 项目流程

简介:51单片机光照强度显示程序面向单片机初学者与嵌入式开发人员,提供从传感器数据采集到LCD1602实时显示的完整方案。资源共23个文件、39KB,核心代码包括main.c、lcd.c、i2c.c源程序,以及lcd.h、i2c.h头文件,还包含pro.hex烧录文件、STARTUP.A51启动代码和Keil工程文件,可直接打开工程学习或烧录到开发板验证。程序实现了LCD1602液晶驱动、I2C接口光照传感器读取、ADC转换与亮度阈值提示,覆盖寄存器选择、使能控制、数据线配置等底层细节,并加入滤波与校准思路以提升测量稳定性。目前已有1249人学习,代码结构清晰、注释明确,适合课程设计、毕业设计或自学练手,也可迁移至智能照明、农业环境监测等场景。

1. 先把光照强度显示讲清楚:读到的数字到底是不是 Lux

“51单片机光照强度显示程序”常出现在课程设计和入门电子制作里。它要解决的问题其实很简单:把环境光的强弱变成传感器信号,再让单片机换算成照度(单位 Lux),送到 LCD 或数码管上显示出来。很多新手把模拟输入引脚直接读到的 0—255 当成光照强度,结果开灯时数值变小、关灯时反而变大,或者换块板子数据就不对了。这篇文章会沿着“传感器选型→电路→代码→校准→踩坑”这条线,给出能直接照做的方案。适合手上有常见 51 内核单片机、能编写 C51 程序的读者;哪怕刚接触 ADC 和 I2C,也可以照着把程序跑通,再逐步理解背后的换算关系。

2. 光照强度显示的两条主流路线:光敏电阻加 ADC,还是数字光传感器

2.1 光敏电阻加 ADC:最便宜的模拟量方案

光敏电阻的特点是暗处阻值很高,强光下阻值很低。常见的硫化镉光敏电阻,暗阻可以到几百千欧,强光下可能只有一两千欧。这个阻值变化不能直接进单片机 GPIO,因为 GPIO 只能识别高低电平,读不出中间量。常见做法是把光敏电阻和一个固定电阻组成分压电路,中间点接到 ADC 的输入引脚。比如光敏电阻接 VCC,固定电阻接地,那么光照越强、光敏电阻阻值越小,分压点电压就越高。

ADC 承担“把电压变成数字”的工作。经典 51 内核芯片大多不带片上 ADC,所以需要外接。常见的 ADC 芯片有 ADC0832、PCF8591、XPT2046 等。ADC0832 是 8 位,速度够用,时序简单,适合第一次做;PCF8591 走 I2C 总线,还能顺便学 I2C;XPT2046 是 12 位,精度更高,但时序稍复杂。如果你只是想在数码管上显示个大概光照值,8 位 ADC 已经够用,毕竟光敏电阻本身的离散性和温漂比 ADC 分辨率带来的误差大得多。

以 ADC0832 为例,典型接线是 CS、CLK、DI/DO 三根线接单片机普通 IO,CH0 接分压中点。换算流程是:先读回 0—255 的 ADC 值,除以满量程得到电压;再根据分压公式算出当前光敏电阻阻值;最后通过阻值—照度曲线或校准表得到 Lux。这条路线总成本最低,也能帮你理解采样、参考电压、线性度这些概念,缺点是每个光敏电阻都要单独校准,否则显示值只能看趋势,不能当准确照度用。

2.2 数字光传感器 BH1750:I2C 直接读 Lux

如果你不想碰标定这件事,BH1750 这类数字环境光传感器是更省心的选择。它把光敏二极管、积分放大、模数转换和 Lux 计算公式都封装在芯片内部,单片机只需要通过 I2C 总线发几条命令,就能读回 16 位照度数据,分辨率可以到 1 Lux。对课程设计来说,这几乎是“开箱即用”的方案。

BH1750 只有 VCC、GND、SCL、SDA 四根核心引脚,ADDR 引脚接地时,7 位 I2C 地址是 0x23,对应的写地址是 0x46、读地址是 0x47。如果 ADDR 接 VCC,地址会变成 0x5C,写地址 0xB8、读地址 0xB9。测量流程一般是这样:先发 0x01 让芯片上电,再发连续高分辨率模式命令 0x10,等待 180ms 左右,然后读两个字节,合成后的数值除以 1.2 就是 Lux。这个 1.2 是芯片手册里的固定转换系数,不是我们自己估计出来的。

// 读取 BH1750 光照强度,返回单位:Lux unsigned short BH1750_ReadLux(void) { unsigned char b0, b1; unsigned short val; I2C_Start(); I2C_SendByte(0x46); // ADDR 接地时的写地址 I2C_SendByte(0x01); // 上电命令 I2C_Stop(); delay_ms(10); I2C_Start(); I2C_SendByte(0x46); // 写地址 I2C_SendByte(0x10); // 连续高分辨率模式,分辨率 1 Lux I2C_Stop(); delay_ms(180); // 等待芯片完成一次测量 I2C_Start(); I2C_SendByte(0x47); // 读地址 b0 = I2C_RecvByte(1); // 收到第一个字节后回 ACK b1 = I2C_RecvByte(0); // 收到最后一个字节后回 NACK I2C_Stop(); val = ((unsigned short)b0 << 8) | b1; return (unsigned short)((float)val / 1.2f); }

这段代码里的 I2C_Start、I2C_SendByte、I2C_RecvByte 需要用 GPIO 模拟实现,51 内核芯片即使没有硬件 I2C 也能跑。发送地址后要判断 ACK,否则容易卡在等待上。180ms 延时是 BH1750 的测量周期,改太短会读到上一次的数据。如果你用的是 0x20 单次测量命令,读完一次后芯片会自动掉电,适合低功耗场景,但每次测量前都要重新发送测量命令。

2.3 方案怎么选:不是越贵越好

对比项光敏电阻 + ADCBH1750 数字传感器
硬件成本很低,几元到十几元稍高,但仍有不少低成本模块
精度低,受元件离散性影响大高,芯片内部已完成换算
是否校准必须校准不需要
占用引脚CS/CLK/DI 三根,加一路模拟输入SCL/SDA 两根
学习价值能学到分压、ADC、非线性换算能学到 I2C 协议
适合场景课程设计、传感器原理实验快速做成品、智能家居项目

我一般的建议是:如果你是奔着“把流程跑通、能交差”去做的,优先选 BH1750,少走弯路;如果你是想把 ADC、分压、标定这些概念都亲手实践一遍,就做光敏电阻加 ADC。两条路线都逃不开两个共同问题:一是采样值要稳定,二是最终显示单位要是 Lux。下面我以光敏电阻加 ADC0832 加 LCD1602 为例,把完整程序拆开讲。

3. 手把手把程序跑通:光敏电阻加 ADC0832 加 LCD1602

3.1 最小电路:光敏电阻分压和 LCD1602 接线

这个例子里的硬件组合很典型:51 内核单片机负责控制,ADC0832 负责采集光敏电阻分压点的电压,LCD1602 负责显示。接线分成两部分,先看 ADC 部分。ADC0832 的 CS 接 P1.0,CLK 接 P1.1,DI/DO 复用同一个引脚接 P1.2。分压点接 CH0,VCC 和 GND 接电源。LCD1602 的数据口 D0—D7 接 P0.0—P0.7,RS、RW、EN 分别接 P2.0、P2.1、P2.2。P0 口内部没有上拉电阻,外部要接一个 10kΩ 排阻,否则 LCD 容易花屏。

分压电路建议这样接:光敏电阻一端接 VCC,另一端接 ADC 的 CH0 和固定电阻;固定电阻另一端接 GND。固定电阻先选 10kΩ,实际调试时可以换 20kΩ 或用电位器调到动态范围最合适。判断标准是:在室内灯光下,分压点电压大概在 1—2V;用手电筒照时,电压接近 3V 以上;用手捂住光敏电阻时,电压降到接近 0V。这个电压范围能保证 ADC 的 8 位分辨率不被浪费。

器件引脚51 引脚说明
ADC0832 CSP1.0片选,低电平有效
ADC0832 CLKP1.1时钟输入
ADC0832 DI/DOP1.2配置时发数据,读取时收数据
ADC0832 CH0分压点光敏电阻和固定电阻的中点
LCD1602 RSP2.0寄存器选择
LCD1602 RWP2.1读写选择
LCD1602 ENP2.2使能信号
LCD1602 D0—D7P0.0—P0.7数据总线,需要外部上拉

上电后先用万用表量分压点电压,确认电压随光照变化方向正确,再接单片机。很多翻车事故都发生在电路阶段,等代码写完再回头量电压,反而要拆半天。

3.2 读取 ADC 0832 并换算成光敏电阻阻值

ADC0832 的时序不算复杂,但网上代码版本很多。常见做法是先片选拉低,然后在时钟配合下发送配置位:起始位、单端模式选择、通道选择。这里用 CH0,配置位依次是 1、1、0。之后 DI/DO 引脚释放,进入输出状态,在时钟下降沿或上升沿逐位读出 8 位数据。下面这段是实际可用的读取函数。

sbit ADC_CS = P1^0; sbit ADC_CLK = P1^1; sbit ADC_DIO = P1^2; // 读取 ADC0832 指定通道(ch=0 表示 CH0,ch=1 表示 CH1) unsigned char ADC0832_Read(unsigned char ch) { unsigned char i, dat; ADC_CS = 0; ADC_CLK = 0; ADC_DIO = 1; // 起始位 1 ADC_CLK = 1; ADC_CLK = 0; ADC_DIO = 1; // 单端模式 1 ADC_CLK = 1; ADC_CLK = 0; ADC_DIO = ch ? 1 : 0; // 通道选择:CH0 为 0,CH1 为 1 ADC_CLK = 1; ADC_CLK = 0; ADC_DIO = 1; // 释放数据线,等待 ADC 输出 for (i = 0; i < 8; i++) { ADC_CLK = 1; ADC_CLK = 0; dat = (dat << 1) | ADC_DIO; } ADC_CS = 1; return dat; }

这个函数返回 0—255 的采样值。使用时要注意,ADC0832 的 DI 和 DO 是同一根线,所以在配置阶段它是输入,读到数据阶段要把它释放为输出。很多代码在配置完通道后忘了把 DI 拉高释放,导致读取阶段一直读到 1 或 0,显示值就固定在某一个数。读回来的 8 位数据是原码,不需要额外反转。

拿到 ADC 值后,要先换成电压,再算光敏电阻阻值。分压公式是:光敏电阻在上、固定电阻在下时,分压点电压等于 VCC 乘以固定电阻除以(光敏电阻加固定电阻)。反推光敏电阻阻值就是固定电阻乘以(VCC 减分压电压)再除以分压电压。代码里要处理分压电压等于 0 或接近 VCC 的极端情况,否则除零会让程序跑飞。

#define VREF 5.0f // 实测 VCC,别盲信稳压输出 #define R_FIX 10000.0f // 分压固定电阻,单位欧姆 // 根据 ADC 采样值计算当前光敏电阻阻值,单位 kΩ float GetLightResistanceKohm(void) { unsigned char adc_value; float v_adc; adc_value = ADC0832_Read(0); v_adc = (float)adc_value * VREF / 255.0f; if (v_adc < 0.01f) // 接近 0V,可视为无光照或断路 return 999.0f; if (v_adc > VREF - 0.01f) // 接近 VCC,可视为短路或强光 return 0.1f; return (R_FIX * (VREF - v_adc) / v_adc) / 1000.0f; }

这段代码把阻值单位换成干欧,方便后面做 Lux 映射。VREF 一定要用万用表实测,而不是直接写 5.0V。很多 USB 供电的板子实际电压只有 4.6V 左右,写错会导致两块板子显示差异明显。R_FIX 也要和你实际焊的固定电阻一致,最好用万用表量一下,因为普通电阻误差就能到 ±5%。

3.3 从电阻换算成 Lux:先跑通波形,再谈准确

光敏电阻的阻值和照度不是线性关系,而是近似指数关系。同一颗光敏电阻,在不同照度下阻值可以差两个数量级。因此,直接从电阻计算 Lux 的通用公式并不存在,必须针对手头传感器校准。程序第一次跑通时,可以先用分段查表的方式占位,让显示值能随光照变化,之后再用第 5 章的两点校准法替换。

// 粗略映射:正常室内日光灯下约 100-300 Lux,手电筒直射约 5000 Lux // 注意:这是临时查表,正式使用前请做两点校准 unsigned int LuxFromResistance(float r_kohm) { if (r_kohm > 100.0f) return 10; // 很暗 if (r_kohm > 50.0f) return 30; // 黄昏 if (r_kohm > 20.0f) return 80; // 室内暗角 if (r_kohm > 10.0f) return 200; // 普通室内 if (r_kohm > 5.0f) return 500; // 靠窗 if (r_kohm > 2.0f) return 2000; // 强光 return 8000; // 直射光 }

这个分段查表的边界值不用背,重点是用它把“电阻变化”转成“光照值变化”的流程走通。调试时先看 ADC 原始值,再看电阻值,最后看输出 Lux。如果 ADC 值正常但 Lux 不变化,说明查表没被调用;如果电阻值算出来对,但 Lux 一直落在某个分段,说明固定电阻选得不合适,导致采样值只在一个小范围内变。

3.4 在 LCD1602 上显示光照值

LCD1602 显示光照值,最直接的方法是调用标准库函数把浮点数转成字符串。但 C51 编译器对浮点格式化的支持比较耗资源,所以我会把 Lux 转成无符号整数,再用自写的整数转字符串函数显示。这样代码更小,也更容易控制显示位置。

#include <stdio.h> // 把无符号整数转成字符串,最多 5 位数字 void UintToStr(unsigned int val, char *buf) { unsigned char i; char tmp[5]; for (i = 0; i < 5; i++) { tmp[i] = (char)('0' + val % 10); val /= 10; } for (i = 0; i < 5; i++) { buf[i] = tmp[4 - i]; } buf[5] = '\0'; } void DisplayLux(unsigned int lux) { char buf[6]; UintToStr(lux, buf); // 第一行显示 "Lux: 00123" 一样的效果 LCD_WriteCmd(0x80); // 设置 DDRAM 地址到第一行 LCD_WriteString("Lux:"); LCD_WriteString(buf); LCD_WriteCmd(0xC0); // 第二行显示状态 LCD_WriteString("Light Sensor OK"); }

LCD_WriteCmd 和 LCD_WriteString 是 LCD1602 的底层驱动函数,网上有很多成熟写法,这里不再展开。要注意的是刷新频率不要太高。很多新手在主循环里不加延时,把 LCD 刷个不停,结果显示闪烁、字符错位。我一般会把显示刷新放到 500ms 一次,或者只在数值变化超过 5 Lux 时才刷新,这样眼睛看起来非常稳定。

4. 光照强度显示调不通?这 4 个高频坑按“现象-原因-解决”排查

4.1 现象:ADC 数值来回跳,显示的光照强度像抽风

用光敏电阻做采集时,ADC 读数不稳定是很常见的。现象是 LCD 上最后一位数字不停乱跳,手靠近传感器时更明显。原因有两层:一是光敏电阻本身有响应噪声,光线稍微抖动就会造成阻值波动;二是 ADC 的参考电压来自主板稳压输出,USB 供电时纹波大,采样值自然跟着漂。

解决方法是先软件滤波,再考虑硬件滤波。软件上可以做“连续采 8 次取平均”,或者用一阶递推滤波:filter = filter * 0.7 + new_sample * 0.3。一阶滤波平滑效果好,但响应会变慢,适合光照变化不快的场景。硬件上,在 ADC 输入引脚对地并联一个 0.1μF 陶瓷电容,滤掉高频噪声。不要靠拉长延迟来假装稳定,那样只会掩盖问题。

4.2 现象:光照越强显示反而越小,甚至一直是 255

这个坑我第一次做时也踩过。现象是拿手电筒照光敏电阻,LCD 上 Lux 值反而下降,或者一直满量程。原因通常是把分压电阻位置接反了。如果光敏电阻接地、固定电阻接 VCC,那么光照越强,光敏电阻阻值越小,分压点电压就越低,ADC 值变小,显示值自然反着走。

解决方法是先确认电路拓扑:光敏电阻上 VCC,固定电阻下地,分压点接 ADC。用万用表量分压点电压,暗处低、亮处高,再继续调软件。另一个常见情况是固定电阻太大,导致强光下光敏电阻阻值小于固定电阻,分压电压已经接近 VCC,ADC 提前饱和。把固定电阻换成 10kΩ 电位器,边调边看电压,找到在目标光照范围内变化最明显的阻值。

4.3 现象:同一个光照下,两块板子显示值差很多

如果你把程序从一块板子烧到另一块板子,发现显示值差 50% 以上,不要先怀疑光敏电阻坏了。原因一般是参考电压没校准。很多稳压模块实际输出电压不是标准的 5.00V,4.7V 或 5.2V 都很正常。代码里如果把 VREF 固定写成 5.0f,ADC 值相同对应的电压就不一样,算出来的阻值当然也不一样。

解决方法是拿到板子先量 VCC,实测多少就写多少进宏定义。光敏电阻的离散性也很大,同一型号不同个体在相同光照下阻值可能差 30%。所以光敏电阻方案在批量使用时必然要做逐个校准。如果不想校,唯一的办法是换 BH1750 这类数字传感器,芯片出厂时已经把校准值固化在内部。

4.4 现象:LCD 显示乱码、缺笔画,或者主循环跑起来后亮度值不更新

LCD 乱码大多出现在第一次上电时,屏幕上出现一片方块或随机字符。原因多数是 P0 口没有上拉,导致数据总线电平不稳定;也有可能是 LCD 初始化时序太短,芯片还没准备好就开始写数据。解决方法是:P0 口接 10kΩ 上拉排阻;初始化里在延时和命令之间加足够的时间,通常每个命令之后延时 5ms 以上;尽量做忙检查,在写数据前确认 BF 位不为 1。

亮度值不更新则往往是刷新逻辑的问题。程序主循环里每次都调用显示函数,但又没有清除残留字符,或者用了 delay 阻塞,导致看起来像“卡死”。我习惯把“采样、计算、显示”拆成三个独立步骤,显示函数内部先清屏,再写新内容,并且刷新间隔控制在 500ms 左右。这样即使采样偶尔抖动,显示也不会跟着闪。

5. 两点校准与弱光强光提示:让显示程序真正能日常用

5.1 两点校准法:没有照度计也能标出 Lux

分段查表能跑通流程,但显示值不是真实照度。要变成可用的 Lux 显示,推荐做两点校准。找个手机光线传感器 App,能显示大致 Lux 值,虽然精度有限,但作为参照足够。先在室内灯光下记录一个点:读当前光敏电阻值 R1 和手机照度 L1;再用手电筒近距离照传感器记录第二个点 R2 和 L2。假设阻值 R 和照度 L 满足指数关系 L = a 乘以 R 的 b 次方,那么可以算出:

b = ln(L1 / L2) / ln(R1 / R2) a = L1 / (R1 的 b 次方)

然后把 a、b 写死在程序里,替换掉之前的查表函数。

// 两点校准参数,单位:R 为 kΩ,Lux 为 lx // 例子:暗处 20k 对应 50Lux,强光 3k 对应 3000Lux #define CAL_A 135.0f #define CAL_B -0.85f unsigned int LuxFromResistanceCal(float r_kohm) { float lux; if (r_kohm < 0.5f) r_kohm = 0.5f; // 防止数值过小导致结果溢出 lux = CAL_A * pow(r_kohm, CAL_B); if (lux < 0) lux = 0; if (lux > 65535) lux = 65535; return (unsigned int)lux; }

校准后如果显示值与参照偏差大,别急着改参数,先确认两个采样点是否覆盖了实际使用环境。两点校准只在这两个点附近比较准,离得越远误差越大。想要更宽范围,可以增加采样点,用分段指数插值,但课程设计里两点基本够用。

5.2 显示里加上光照档位,读起来更直观

Lux 数字是给工程师看的,非专业用户更习惯“暗、正常、强光”这样的概念。可以在 LCD 第二行把光照强度映射成档位:低于 30 Lux 显示“Dark”,30—300 Lux 显示“Normal”,高于 300 Lux 显示“Strong”。这样整个显示程序就不再只是“测了个数字”,而是一个能直接看懂实际环境状态的装置。

5.3 一个让我少走弯路的小习惯

我自己调试这类程序时,习惯先让串口或 LCD 单独显示 ADC 原始值,确认采样链路正常后,再切换到换算后的 Lux。很多翻车问题其实都出在“显示模块正常、传感器接线正常,但中间换算写错了”这种情况。比如计算时把分压公式方向搞反,或者忘记把千欧单位换算进去。先看原始值,再算电阻,最后换 Lux,这个顺序能帮你快速定位是哪一步出了问题。校准参数也别一次到位,先用临时查表跑通,再用两点校准替换。希望帮到你。

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

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

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

立即咨询