简介:这份资源是基于51单片机开发的光照强度显示程序,面向电子嵌入式方向的学生、开发者及物联网入门人群。项目通过光敏传感器采集环境光照信号,经单片机内部ADC转换为数字量后,在LCD1602液晶屏上实时显示强度数值,适合用于智能照明、小型环境监测等场景。压缩包共包含23个文件,总大小约39KB,以C语言源码、头文件、Hex烧录文件和编译过程文件为主,代码结构清晰,便于直接阅读和移植学习。已有1250人下载学习,可作为51单片机入门综合练习:一方面展示了LCD1602驱动编写、I2C接口配置和ADC采样流程,另一方面也提供了传感器数据处理、阈值判断与显示切换的实现思路。通过该程序,读者能掌握从硬件引脚连接到软件驱动的完整开发流程,并可根据需要扩展出光线提示或自动化控制功能。
1. 光照强度显示程序:用 51 单片机和 LCD1602 把环境亮暗读出来
把一块光敏电阻、一个 ADC 芯片和一块 LCD1602 组合在一起,就能做出一个实时显示光照强度的 51 单片机小工程。这个项目看起来是入门练习,实际上把“模拟信号采集、ADC 串行时序、LCD 并行总线显示、主循环刷新”这几条硬骨头全串起来了。项目本体是一份完整的 C 语言工程:光敏电阻把光照变成电压,ADC 把电压变成 8 位数字量,51 单片机再把数字量换算成 0~255 的强度值,最后通过 LCD1602 显示出来。适合刚学完 GPIO 和定时器、想碰真实模拟量的同学,也适合需要快速做一个光照采集模块的开发者。
2. 硬件电路怎么搭:光敏电阻、ADC0832 与 LCD1602 的接线分工
2.1 为什么必须加 ADC:普通 51 看不见模拟电压
51 单片机的 IO 口只有高低电平两种输入,直接拿 IO 去接光敏电阻分压点,只能读到“很亮”和“很暗”两个状态,中间连续的光照强度完全读不出来。常见做法是外扩一颗 8 位 ADC,这里我用的是 ADC0832。它和 51 之间通过串行时钟通信,只占 4 个 IO 口,比并行的 ADC0804/0809 省引脚,非常适合这种小工程。
信号链路是这样的:5V 经过光敏电阻和固定电阻分压,光照越强光敏电阻阻值越小,分压点电压越高;ADC0832 采样这个电压,输出 0~255 的数字量;51 收到数字量后,直接当成光照强度值显示在 LCD1602 上。为了让读数方向符合直觉,我让采样点的电压和光照强度同向变化,这样 LCD 上的数字越大代表越亮。
2.2 接线表与分压电路参数
下面这张表是基于“8 位 LCD 数据口 + 串行 ADC”的典型接法,端口号不是死的,你改到别的引脚时记得同步改代码头部的 sbit 定义。
| 信号点 | 接到 51 的位置 | 说明 |
|---|---|---|
| LCD D0~D7 | P0 口 8 个引脚 | LCD1602 使用 8 位数据模式 |
| LCD RS | P2.0 | 0 写命令,1 写数据 |
| LCD RW | P2.1 | 读写选择,本项目只写,固定 0 |
| LCD EN | P2.2 | 使能信号,下降沿锁存数据 |
| ADC CS | P1.0 | ADC0832 片选,低有效 |
| ADC CLK | P1.1 | 串行时钟 |
| ADC DI | P1.2 | 主机输出通道/模式配置 |
| ADC DO | P1.3 | ADC 返回数据 |
| ADC CH0 | 分压采样点 | 光敏电阻和 10k 电阻的中点 |
| ADC VREF | 5V | 参考电压,和电源共用 |
分压电路用的是一个光敏电阻加一个 10k 固定电阻,接法为“5V → 光敏电阻 → 采样点 → 10k → GND”。如果你手头只有电位器,也可以直接把光敏电阻换成电位器,旋转电位器就能模拟光照变化,这个替身验证法在调试时特别香。
提示:LCD1602 的 V0 是液晶对比度脚,建议用一个 10k 电位器接到 GND,不要直接接地。直接接地经常出现屏幕有背光但一个字也不显示的情况。
3. 代码骨架拆开看:ADC 采集、LCD 刷新与主循环
3.1 LCD1602 驱动:命令字才是液晶的“语言”
LCD1602 最基础的操作就是写命令和写数据。下面这一段是 8 位模式驱动代码,适用于大多数 51 工程,你需要做的是把延时时间按实际主频微调。
#define LCD_PORT P0 sbit LCD_RS = P2^0; sbit LCD_RW = P2^1; sbit LCD_EN = P2^2; void LCD_Delay(void) { unsigned int i; for (i = 0; i < 100; i++); } void LCD_WriteCommand(unsigned char cmd) { LCD_RS = 0; // RS=0 表示写入命令 LCD_RW = 0; // 只写不读 LCD_PORT = cmd; LCD_EN = 1; // EN 拉高再拉低产生下降沿,锁存数据 LCD_Delay(); LCD_EN = 0; } void LCD_WriteData(unsigned char dat) { LCD_RS = 1; // RS=1 表示写入数据 LCD_RW = 0; LCD_PORT = dat; LCD_EN = 1; LCD_Delay(); LCD_EN = 0; } void LCD_Init(void) { LCD_Delay(); LCD_WriteCommand(0x38); // 8 位模式,两行显示,5x7 点阵 LCD_WriteCommand(0x0C); // 显示开,光标关,不闪烁 LCD_WriteCommand(0x06); // 写入后地址自动加 1,整体右移 LCD_WriteCommand(0x01); // 清屏 } void LCD_SetCursor(unsigned char col, unsigned char row) { unsigned char addr; if (row == 0) addr = 0x00 + col; else addr = 0x40 + col; LCD_WriteCommand(0x80 | addr); // 设置 DDRAM 地址 } void LCD_ShowString(unsigned char col, unsigned char row, char *s) { LCD_SetCursor(col, row); while (*s) { LCD_WriteData(*s++); } }0x38 是最常用的初始化命令,它同时决定了总线宽度、行数和点阵字体;0x0C 关闭光标能避免显示一堆下划线小方块;0x06 让字符写入后自动右移,这样连续打印字符串时不用手动移动地址。
代码里的 LCD_Delay 是纯软件延时,只是为了让 EN 的高电平宽度足够。运行在 11.0592MHz 或 12MHz 的 51 上基本都能工作,但如果换到倍频或降低主频,需要把这个延时加长。项目中的 RW 引脚全程固定为 0,因为没人从 LCD 读忙标志,等时间靠延时来凑。这是一种简单但有效的做法。
3.2 ADC0832 采集:串行时序里的下降沿读取
ADC0832 是 8 位串行 ADC,主机要先通过 DI 给它发送“起始位 + 工作模式 + 通道号”配置,然后才能从 DO 读到采样值。下面这个函数只处理单端输入,光照传感器接到 CH0,config 参数传 0 表示选通道 0。
sbit ADC_CS = P1^0; sbit ADC_CLK = P1^1; sbit ADC_DI = P1^2; sbit ADC_DO = P1^3; unsigned char ADC0832_Read(unsigned char channel) { unsigned char i; unsigned char val = 0; ADC_CS = 1; ADC_CLK = 0; ADC_DI = 0; ADC_CS = 0; ADC_DI = 1; // 起始位,必须为 1 ADC_CLK = 1; ADC_CLK = 0; ADC_DI = 1; // SGL=1,单端输入模式 ADC_CLK = 1; ADC_CLK = 0; ADC_DI = channel; // channel=0 选 CH0,channel=1 选 CH1 ADC_CLK = 1; ADC_CLK = 0; ADC_DI = 0; // 配置结束,释放数据线 for (i = 0; i < 8; i++) { ADC_CLK = 1; ADC_CLK = 0; // 下降沿时 DO 输出一位数据 val <<= 1; if (ADC_DO) val++; } ADC_CS = 1; return val; }这段代码的关键是时序:配置位在时钟上升沿被 ADC 锁存,输出数据在下降沿被单片机读取。所以每次循环都是先拉高 CLK 再拉低,然后立刻读 DO,循环 8 次正好收完一个字节。
如果你把通道传成 1,那么 CH1 会作为输入通道。ADC0832 还支持差分模式,但这里光敏电阻分压输出是单端电压,不需要差分,所以代码里固定 SGL=1。实际工程中,除了通道切换外,整个函数可以直接复用。
3.3 主循环:采集一次、显示一次、再延时
主函数没有用操作系统,就是一个简单的轮询循环。每次循环采集一次光照强度,转成三位数字显示在 LCD 第一行,然后延时 200ms,让肉眼能看清数字变化。
void Delay_ms(unsigned int ms) { unsigned int i, j; for (i = 0; i < ms; i++) for (j = 0; j < 120; j++); } void LCD_ShowNumber(unsigned char col, unsigned char row, unsigned char val) { LCD_SetCursor(col, row); LCD_WriteData('0' + val / 100); // 百位 LCD_WriteData('0' + (val % 100) / 10); // 十位 LCD_WriteData('0' + val % 10); // 个位 } void main(void) { unsigned char light; LCD_Init(); LCD_ShowString(0, 0, "Light: "); while (1) { light = ADC0832_Read(0); // 采集 CH0 电压 LCD_ShowNumber(7, 0, light); // 第 7 列起显示三位数 LCD_ShowString(10, 0, "/255"); Delay_ms(200); } }这里没有把 ADC 值换算成实际照度单位 lux,是因为光敏电阻和固定电阻的分压曲线是非线性的,想测绝对照度需要标定曲线。作为学习 Demo,0~255 直接显示已经能看出相对强弱。LCD_ShowNumber 每次显示固定三位,比如 023 或 250,避免上一次残留数字造成误读。
如果你想让显示更平滑,可以加一个简单滤波:连续取 5 次 ADC 值,去掉最大和最小,再求平均。我一般会在 ADC0832_Read 外层套一个average(),成本低,防抖动效果明显。
4. 从编译到点亮:工程配置、烧录与上电验证
4.1 工程文件组织:把代码拆成三个源文件
完整工程不建议全塞在一个 main.c 里。我按功能拆成 LCD 和 ADC 两个模块,main.c 只负责业务逻辑,这样后续换屏幕或换 ADC 时不需要动主流程。目录结构长这样:
project/ ├── main.c # 主循环、延时 ├── lcd1602.h # LCD 接口声明 ├── lcd1602.c # LCD 命令/数据函数 ├── adc0832.h # ADC 接口声明 └── adc0832.c # ADC0832 读写函数新建工程时把这三个 C 文件都加入项目,头文件放在同一目录。芯片型号选择你手里的 51,如果代码里有依赖时钟的延时,就把晶振频率填成板上的实际值。编译前必须确认输出 HEX 文件,否则烧录工具没有二进制可以下载,这一步通常藏在工程的输出选项里,新手经常编译成功后却找不到文件。
4.2 烧录与上电验证的完整步骤
先断电,按 2.2 节的接线表把所有线接好,重点检查 LCD 数据线和 P0 口方向。P0 在很多 51 最小系统板上是没有内部上拉的,如果你的 P0 口接了 LED 排阻,LCD 数据会互相干扰,最好把 LED 相关的跳线帽拔掉。
然后编译生成 HEX 文件,打开字符串烧录软件,选择正确的串口和芯片型号。多数 51 板用串口下载,下载时有个“冷启动”操作:先点下载按钮,再给板子上电。如果顺序反了,烧录软件会一直提示等待上电。
下载完成后直接上电,LCD1602 第一行应显示Light: 123 /255类似内容,数值会随着光照强度变化。用手遮住光敏电阻,读数明显下降;用手机闪光灯照,读数明显上升。如果数字反了,说明分压电阻和光敏电阻在电路中的位置对调了,把两个电阻的位置交换一下即可。
如果没有光敏电阻,用 10k 电位器替代:电位器两端接 5V 和 GND,中间抽头接 ADC CH0,旋转电位器时 LCD 数值会连续变化。这个等效验证法可以帮你把 LCD 和 ADC 链路分开调试:先确认液晶能出字,再去调 ADC。我每次调这种项目都会先在固定电压下看 ADC 输出是否稳定,再接入光敏电阻,踩坑率低很多。
5. 常见问题排查:黑屏、乱码、数值不动的 5 个坑
5.1 LCD1602 背光亮但一个字都不显示
现象:屏幕背光亮,亮度正常,但第一行全黑或者全空,写字符无效。
原因:最常见是对比度脚 V0 直接接地,导致字符对比度太高或太低,看不出字;其次是初始化代码里的 0x38 没有被执行,LCD 停留在未知模式。
解决:在 V0 和 GND 之间接一个 10k 电位器,慢慢旋转找字符出现的点。如果旋转后只出现一排黑块,说明 0x38 没有正确写入,检查 RS 和 EN 接线是否接反。我把 RS 和 RW 接过反,花了一个小时才发现。
5.2 显示乱码、字符位置错乱或重复
现象:明明写的是 Light,屏幕上却出现不连续字符或半段汉字。
原因:写命令和写数据时,数据口电平没有稳定就被 EN 下降沿锁存。P0 口接上拉不均匀或者延时太短,都会导致这种问题。
解决:把 LCD_Delay 的延时值加大到 200~300,再就是把写入顺序改成“先放数据,再拉高 EN,延时,再拉低 EN”。另外,确认数据线是不是 8 根都接对了,错位一根就会出现整屏幕乱码。
5.3 ADC 采样值永远是 255 或 0
现象:LCD 上数字一直卡在 255 不动,或者一直是 0,遮光也没有反应。
原因:ADC 输入电压超过参考电压时通常会输出满量程 255,低于 0 时输出 0。如果采样点直接悬空或者 DO/DI 接反,读回来也是固定值。
解决:先用万用表测采样点和 GND 之间的电压,确保在 0~5V 之间。如果分压点电压正常但 ADC 还是 255,检查 VREF 是否接了 5V,以及 ADC0832 的 GND 有没有和单片机的 GND 共地。模拟地和数字地不共地,是最隐蔽的坑。
5.4 数值变化范围很小,只在 100~120 之间跳
现象:光照从暗到亮变化很大,但 LCD 读数只变化十几个数。
原因:分压电阻和光敏电阻阻值匹配差。光敏电阻暗阻可达 1M 欧姆,亮阻低到几 k,如果固定电阻选得太大,亮暗两端的电压差就很小。
解决:把固定电阻换成 10k 左右。也可以在代码里做映射,只取 ADC 值的低 6 位,把 200 以上的变化放大到 0~255,但这是治标。从硬件上调整分压区间,才能让整个量程都有反应。
5.5 烧录成功但程序不跑,按一下复位才跑
现象:上电后 LCD 不亮或全黑,按下 reset 键程序恢复正常。
原因:下载完成后单片机没有从起始地址正确复位,串口下载器和上电时序冲突。
解决:先断开 ADC 采样线,只留 LCD 和最小系统测试。若问题依旧,检查复位电路的上电复位电容是否正常。很多板子按复位能跑,说明程序本身没问题,只是上电瞬间时钟还没稳,给程序加一个更长的主程序初始化延时即可。
6. 二次开发:加阈值报警、改 I2C OLED 和串口上报
6.1 把结果显示改成串口上报
LCD1602 显示是给人看的,但如果这个光照模块要接上位机,就需要把采样值通过串口发出去。做法是在主循环里用标准库函数格式化成字符串,再通过串口发送。注意 51 的波特率依赖定时器 1 和晶振频率,用 11.0592MHz 晶振,设 9600 波特率时参数正好是整数倍,所以这类工程我强烈建议不要换晶振频率。
void UART_Init(void) { SCON = 0x50; // 串口模式 1,允许接收 TMOD &= 0x0F; TMOD |= 0x20; // 定时器 1 工作模式 2 TH1 = 0xFD; // 9600 波特率,11.0592MHz TL1 = 0xFD; TR1 = 1; }如果只想加阈值报警,就在主循环里加一句比较。比如光照低于 30 时点亮 LED,高于 200 时关闭。这个阈值可以定义成全局变量,方便后续用按键或上位机修改。
6.2 把 LCD1602 换成 I2C 接口 OLED
LCD1602 并行接线占了 11 个 IO,如果你想把这款光照计做成便携设备,可以考虑把显示模块换成 I2C 接口的 OLED。硬件上只需要 SCL 和 SDA 两根线,代码里 LCD_WriteData 全部替换成 OLED 的字符串写入函数,主循环逻辑不用动。很多开发板上的 51 模拟 I2C 工程可以直接移植过来,这个项目的数据采集部分完全可以复用。
从那以后我每次拿到这种“传感器 + 显示”的小项目,都强制走一遍“先把输出模块点亮,再读传感器,最后联调”这个顺序。屏幕没亮就去追传感器,十有八九两头都炸。这个项目的改造空间很大:加 EEPROM 存阈值、加按键校准、加蓝牙上报,都是在现有主循环的骨架上往前走。希望这些踩坑和拆解能帮你把 LCD1602 和 ADC 的组合真正跑起来。
本文还有配套的精品资源,点击获取