十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于51单片机的光强测量与调光系统设计

基于51单片机的光强测量与调光系统设计 简介基于51单片机光强测量、LCD1602显示的设计资源适合电子工程初学者与嵌入式开发爱好者。内容围绕光敏传感器如BH1750采集环境光强经单片机处理后实时显示于LCD1602屏幕涵盖原理图、源码与工程文件可帮助读者搭建一套实用的光强监测系统。压缩包共18个文件包含3张JPG原理图、2个lst列表、2个obj目标、1个c源程序、1个a51汇编、1个hex烧录文件、1个uv2工程文件及stc51_bh1750等芯片相关文件整体仅671KB覆盖设计、编译、烧录全流程结构清晰便于对照学习与二次开发。目前已有1456人学习。通过这份资源读者可获得可直接运行的C/汇编程序、电路连接原理图、Keil工程配置以及STC51与BH1750的接口实现为理解光强检测、LCD1602显示、定时器与IO操作提供有力支撑。1. 光强测量用51单片机而非ARM开发板成本和时序都更直白光强测量不需要高分辨率光谱分析大多数补光控制、环境监测和课程设计只要分辨到几个勒克斯就够。我用51单片机组这套系统时选STC89C52、光敏电阻、ADC0804和LCD1602理由是每颗芯片的数据手册和接法在社区里积累了很多可复现的参考。51没有内置ADC所以必须先有一个分压电路把光敏电阻的电阻变化转换成电压再用外部ADC量化最后把数字量换算成光强显示在LCD1602上。这套链路中真正影响成败的不是MCU编程而是分压参数、ADC0804时钟、LCD初始化时序和P0口上拉。下面直接按原理图、驱动代码、Proteus验证、扩展调光的顺序展开每一步都给出可以改的参数和可以观测的节点。2. 硬件链路光敏电阻分压、ADC0804接口和LCD1602接线2.1 光敏电阻分压网络固定电阻选10K的电压推演光敏电阻在原理图中不能直接接ADC因为ADC只能读电压。常见做法是用一支光敏电阻和一支固定电阻串联把电阻变化变成电压变化。我习惯把光敏电阻接VCC、固定电阻接GNDADC0804的VIN接固定电阻两端。这样当光照增强时光敏电阻阻值下降固定电阻分到的电压升高ADC读数变大逻辑上与“光强越高数字越大”保持一致。以GL5528为例暗态阻值约1MΩ亮态阻值在10-20kΩ之间。固定电阻选10kΩ可以得到下面这个电压表光敏电阻阻值光照状态分压点电压(V)1MΩ暗环境0.05100kΩ弱光0.4550kΩ室内采光0.8320kΩ明亮房间1.6710kΩ强光2.505kΩ阳光直射3.33这里能看到10kΩ固定电阻把电压范围压缩到0.05-2.5V附近ADC0804默认满量程约5V电压只用了半个量程。这是有意的取舍光敏电阻指数响应明显亮到一定程度阻值下降放缓给后半段留出余量能避免阳光直射时电压顶到5V导致读数饱和。如果固定电阻改成5.1kΩ强光时分压点只有约1.67V分辨率反而更低。可以用一段简单脚本复算其他阻值组合调整时只需要改固定阻值和光敏电阻列表vcc 5.0 r_fixed 10e3 # 固定电阻单位欧姆 r_light_list [1e6, 100e3, 50e3, 20e3, 10e3, 5e3] for r_light in r_light_list: vout vcc * r_fixed / (r_light r_fixed) print(f{r_light:8.0f} Ohm - {vout:.3f} V)脚本逻辑是串联分压公式改变r_fixed或r_light_list就能模拟不同光敏电阻规格。99%的51单片机光强测量项目不需要在单片机里做大数推导这两行Python足够决定原理图电阻取多大。2.2 ADC0804引脚映射数据口放P1才能避开LCD的P0总线设计原理图时最容易踩的坑是让ADC0804和LCD1602共用P0口一旦LCD写数据P0上的信号同时打到ADC数据口上轻则显示乱码重则损坏芯片。我一般把ADC0804的八位数据口固定在P1LCD1602数据口留在P0。P1是准双向口直接读电压没有问题P0是开漏结构必须外接10kΩ上拉电阻。ADC0804在原理图中的网络标号如下ADC0804引脚接到51单片机或网络说明DB0-DB7P1.0-P1.7转换结果输出CSP2.5片选低电平有效RDP3.7读数据低电平有效WRP3.6启动转换低脉冲INTRP3.2转换完成信号低电平有效VIN分压点光敏电阻和固定电阻的中点VIN-GND差分输入负端VREF/2悬空内部默认2.5V满量程5VCLKR经10kΩ接CLKINRC振荡器电阻CLKIN经150pF接地RC振荡器电容VCC5V电源AGND/DGNDGND模拟地和数字地建议单点接地ADC0804自身不带时钟必须在CLKR和CLKIN之间接一个RC振荡器。按10kΩ和150pF计算频率约为1/(1.1×R×C)606kHz这个频率足够满足典型转换时间要求。不要省掉这颗电容和电阻否则Proteus仿真里会一直出现INTR不翻转的现象。2.3 LCD1602接线的对比度偏置和P0上拉LCD1602的VSS、VDD、V0三个脚最容易被忽略。VSS接地VDD接5VV0必须接电位器中间抽头来调整对比度不能直接接地或直接飞线到VCC。悬浮的V0会让LCD背光亮起但屏幕没有任何字符。数据线D0-D7接P0.0-P0.7RS接P2.6E接P2.7RW直接接GND这样只使用两个控制口程序上也省去读写忙标志的等待。由于P0口是开漏输出必须在原理图上给P0的八个引脚都加10kΩ上拉电阻。实际制板时用排阻更方便Proteus仿真中用一个RESPACK-8即可。若不加上拉LCD的数据线在写操作时会拉出错误的电平Keil里编译出的字符显示全部错位。2.4 51单片机最小系统晶振、复位和电源去耦51单片机光强测量涉及模拟电压采集电源纹波直接影响ADC读数。最小系统保留12MHz晶振和两个30pF负载电容复位电路使用10μF电容和10kΩ电阻。靠近VCC引脚放一个100nF去耦电容ADC0804的VCC也要单独加去耦不要和单片机共用一个过孔。原理图中还有两个细节需要检查ADC0804的VREF/2不要接到3.3V否则模拟输入范围会被压到2×3.3V6.6V左右偏离5V参考51的P3.6和P3.7虽然标称是WR/RD但程序里用普通IO模拟时序不需要把ADC挂到外部RAM地址空间。3. 程序实现LCD1602驱动、ADC0804采集与数值换算3.1 LCD1602驱动中的关键时序参数程序使用Keil C51编写包含reg52.h头文件。LCD1602的写命令和写数据本质上都是通过RS区分RS0写命令RS1写数据。E引脚先拉高数据稳定后拉低下降沿锁存数据。#include reg52.h #define uchar unsigned char #define uint unsigned int sbit lcd_RS P2 ^ 6; sbit lcd_EN P2 ^ 7; #define LCD_PORT P0 void delay_ms(uint n) { uint i, j; for (i 0; i n; i) for (j 0; j 120; j); } void lcd_cmd(uchar cmd) { lcd_RS 0; LCD_PORT cmd; lcd_EN 1; delay_ms(2); lcd_EN 0; delay_ms(2); } void lcd_data(uchar dat) { lcd_RS 1; LCD_PORT dat; lcd_EN 1; delay_ms(2); lcd_EN 0; delay_ms(2); } void lcd_init(void) { delay_ms(15); lcd_cmd(0x38); // 8位数据模式两行显示 lcd_cmd(0x38); lcd_cmd(0x08); // 关闭显示 lcd_cmd(0x01); // 清屏 lcd_cmd(0x06); // 光标右移 lcd_cmd(0x0C); // 开显示不显示光标 }LCD初始化里多写了一次0x38是为了兼容不同上电时序的1602模块。如果单片机速度很快而模块上电还没稳定第一次初始化命令可能丢失。这里每次命令之间都加了2ms延时虽然比读忙标志慢但在十二兆赫兹主频下约1ms的操作周期足够稳定。3.2 ADC0804读取函数WR脉冲启动转换并轮询INTRADC0804的读取过程分成三步CS拉低选中芯片WR输出一个低脉冲启动转换然后等待INTR变为低电平表示转换完成。转换完成后RD拉低使数据出现在DB0-DB7上单片机读P1即可。sbit adc_CS P2 ^ 5; sbit adc_WR P3 ^ 6; sbit adc_RD P3 ^ 7; sbit adc_INTR P3 ^ 2; #define ADC_PORT P1 uchar adc_read(void) { uchar dat; uint timeout; adc_CS 0; adc_WR 0; adc_WR 1; // 产生一个低脉冲启动转换 timeout 0; while (adc_INTR 1) // 等待转换完成 { if (timeout 10000) return 0xFF; // 超时保护避免死循环 } adc_RD 0; ADC_PORT 0xFF; // P1写1为输入做准备 dat ADC_PORT; // 读取转换结果 adc_RD 1; adc_CS 1; return dat; }这里网络标号中的INTR接线一定要在原理图中连到P3.2。若省略INTR接线while会一直等待程序就卡死超时保护把返回值设为0xFF方便从LCD上看出是ADC通道故障还是光路故障。P1写0xFF这行容易被删掉但对准双向口来说先写1再读是最稳妥的做法。3.3 主循环把ADC原始值映射成0-100光强值ADC0804是8位分辨率采集值范围0-255。直接显示原始值不直观通常映射到0-100的百分比对应“当前光强占满量程的比例”。uchar adcVal; uint light; void main(void) { lcd_init(); lcd_cmd(0x80); lcd_data(L); lcd_data(i); lcd_data(g); lcd_data(h); lcd_data(t); lcd_data(:); lcd_data( ); while (1) { adcVal adc_read(); // adcVal / 255 * 100使用整数乘法避免浮点 light (uint)((unsigned long)adcVal * 100UL / 255UL); lcd_cmd(0x80 7); lcd_data(0 light / 100); lcd_data(0 light / 10 % 10); lcd_data(0 light % 10); lcd_data(%); lcd_data( ); delay_ms(300); // 300ms刷新一次 } }换算公式里先把adcVal转成unsigned long再乘100防止8位溢出。LCD上显示的“light”变量范围是0-100如果显示“100%”说明分压电压接近5V满量程。这个百分比不是真实勒克斯只是针对当前硬件参数的光强比例。想要真实照度值需要做两点校准这部分在第六章展开。4. 在Proteus中仿真51单片机光强测量原理图的检查方法4.1 元件放置顺序先最小系统再LCD最后ADCProteus仿真比实物节省器件成本但模型连接错误同样会让人白忙。搭建时先放STC89C52和晶振复位电路跑一个LED闪烁程序确认最小系统能运行再放LCD1602和10k排阻确认能显示字符最后加入ADC0804和分压电路。这种顺序能精确锁定问题发生在哪一层。需要确认的元件有STC89C52、ADC0804、LCD1602、RESPACK-8、电阻若干、电位器用于模拟光强、12MHz晶振、两个30pF电容、10μF电解电容和10kΩ复位电阻。光敏电阻在Proteus中可以直接用可变电阻或电位器替换通过按键改变阻值模拟环境光照变化。4.2 用电压表和发光二极管观察ADC0804是否在工作Proteus里最容易出现的问题是仿真能跑但ADC数据不变化。这时先在分压点放一个DC电压表转动电位器看电压是否在0-3.3V之间变化。若电压有变化而LCD数值不动再检查ADC0804的INTR线上是否出现低脉冲。可以把虚拟示波器的探针接到CLKOUT引脚正常时能看到约600kHz的时钟方波。若CLKOUT无输出说明RC振荡器没起振常见原因是150pF电容放错位置或接地引脚没接。若时钟正常但仍无数据把ADC0804的DB0引脚接一个LED手动调节电压时LED亮度变化说明转换结果在输出问题出在51的读取时序上。4.3 原理图网络标号检查防止“接线没连上”的隐形错误在AD或立创EDA中画原理图时网络标号同名才算连接这是最常被忽视的地方。例如分压点标“LIGHT”ADC0804的VIN脚标“LIGHT”两个网络才算接通。如果一边写“LIGHT”另一边写“LIGHT1”拖线的视觉引导会让检查者误以为已经连接。检查清单包含以下项目P0.0-P0.7是否都加了10kΩ上拉LCD的V0是否接了电位器而不是直接悬空ADC0804的CLKIN和CLKR是否都连到RC网络INTR是否连到P3.2VIN-是否接地而不是悬空51的EA引脚是否接到VCC。STC系列运行内部程序时EA必须接高电平漏掉这个引脚在任何开发板上都会导致仿真不干活。5. 数据抖动、量程调整与多通道采集的扩展做法5.1 滑动平均滤波抑制光强读数跳动光敏电阻在室内荧光灯下读数会周期性跳动原因不是转换功能出错而是市电灯光50Hz频闪和电源纹波叠加到了模拟输入。最见效的软件手段是滑动平均保留最近八次采样值的和每来一个新值就替换掉最早那个。#define FILTER_SIZE 8 uchar filter_buf[FILTER_SIZE]; uchar filter_index 0; uint filter_sum 0; void filter_reset(void) { uchar i; for (i 0; i FILTER_SIZE; i) filter_buf[i] 0; filter_sum 0; filter_index 0; } uchar filter_add(uchar new_val) { filter_sum - filter_buf[filter_index]; filter_sum new_val; filter_buf[filter_index] new_val; filter_index (filter_index 1) % FILTER_SIZE; return (uchar)(filter_sum / FILTER_SIZE); }滑动平均的窗口大小直接决定响应速度和滤波强度。窗口取8时对50Hz闪光电平能压掉大部分毛刺同时又不至于让手电筒快速扫过时读数跟不上。若放在阳光传感器上窗口可以加大到16或32因为日光变化本身很慢。5.2 用分压电阻和VREF/2调整量程如果当前电路在强烈光照下读数已经顶到255说明分压电压超过ADC输入范围。需要增大固定电阻或减小供电分压。量程调整本质是解一个分压方程假设期望最大光照时光敏电阻为R_min希望此时ADC输入为V_max固定电阻取值为R_fixed V_max × R_min / (5V - V_max)例如最大光照时光敏电阻降到10kΩ希望此时电压为4.0V那么R_fixed设置为4.0×10k/(5.0-4.0)40kΩ。固定电阻从10k改到40k后弱光电压更低但强光范围更宽。另一个方法是给ADC0804的VREF/2引脚接外部电压把满量程从5V压到2.5V对应的VREF/2电压为1.25V。这样每比特分辨率从约19.5mV提升到约9.8mV适合低光照测量但输入电压不能超过2.5V。5.3 扩展多通道用PCF8591替代ADC0804如果后续还要接温度、湿度等模拟传感器ADC0804的八位并行接口会占用太多IO。更合理的器件是PCF8591它内部集成了四路ADC和一个DACI2C接口只需要两根线。51单片机用两根普通IO口模拟I2C读取PCF8591某个通道的代码框架如下sbit scl P2 ^ 0; sbit sda P2 ^ 1; uchar pcf_read_channel(uchar ch) { uchar dat; i2c_start(); i2c_send(0x90); // PCF8591写地址 i2c_send(0x40 | ((ch 0x03) 4)); // 模拟输入使能并选择通道 i2c_start(); i2c_send(0x91); // PCF8591读地址 dat i2c_recv(); // 第一次读取是前一通道值丢弃一次 dat i2c_recv(); i2c_stop(); return dat; }通道选择字节的低三位是通道号第四位是模拟输入使能位。这里最关键的是要连续读两次首次读出来的值属于上一个通道直接使用会造成数据串位。这种接法比ADC0804多出一片芯片但释放了P1口接矩阵键盘或4×4按键时更从容。5.4 常见故障排查表在实际调试51单片机光强测量系统时可以按照下面这张表快速定位故障现象可能原因排查方法LCD无显示对比度电位器没调调V0抽头电压到0.4-1.0VLCD显示一条方框P0数据线没有上拉给P0加10kΩ排阻LCD显示乱码数据线顺序接反核对P0.x到D0-D7顺序ADC读数固定255分压电压超过量程测VIN对地电压ADC读数固定0输入电压接近0V检查光敏电阻是否虚焊程序卡在whileINTR没接到P3.2补接线或改用延时等待读数每隔几分钟跳变光敏电阻上积灰或受热清洁表面避免贴紧发热元件6. 两点校准变成真实勒克斯值和PWM背光控制6.1 两点校准把ADC原始值映射到LUXProteus里能跑通不代表实物显示的就是真实照度。光敏电阻和固定电阻的误差会导致同一ADC值对应的勒克斯不同实物必须校准。用手头条件就行关灯读一次记为ADC暗值a1开一盏已知照度的台灯或手机光线传感器读一次记为a2和参考照度L2。以0勒克斯作为第二个隐含点计算斜率kL2/(a2-a1)。在代码中放开注释#define LUX_K 180 // 校准系数单位 LUX / ADC值建议扩大10倍 #define LUX_B 0 // 0点补偿 uint light_lux ((uint)LUX_K * adcVal) / 10 LUX_B;把计算出的一盏灯下的数值和手机照度App对比如果偏差大把暗态值补到LUX_B里。这套两点校准只对中间区间有效强光下光敏电阻非线性明显想覆盖0-10000LUX需要做分段查表。6.2 用光强值控制PWM占空比直接把测量转成智能控制采集出的light变量可以进一步用来驱动LED补光。光照越弱LED越亮这是最常见的使用场景。用定时器0产生PWM占空比跟随light变化sbit pwm_lead P2 ^ 4; uint pwm_duty 0; void timer0_init(void) { TMOD 0x01; // 定时器0模式1 TH0 0xFC; // 1ms定时初值12MHz TL0 0x18; TR0 1; ET0 1; EA 1; } void timer0_isr(void) interrupt 1 { static uchar cnt 0; TH0 0xFC; TL0 0x18; cnt; if (cnt pwm_duty) pwm_lead 1; else pwm_lead 0; if (cnt 100) cnt 0; }在无限循环中把pwm_duty更新为(100 - light) * 80 / 100这样light越小pwm_duty越大LED越亮。pwm_duty范围被限制在80以内是为了避免低光照时占空比100%造成LED电流过大。定时器中断里不要做LCD刷新因为LCD写时序需要较多延时放到主循环里更安全。验证PWM调光是否正常只需要在Proteus中把光敏电阻替换成电位器缓慢减小电阻值观察LED亮度是否随着LCD数字增大而降低。若LED亮度无变化先查P2^4引脚是否正确和LED留有100-300Ω限流电阻再查定时器初值是否按12MHz计算。注意定时器中断里对cnt的操作必须用static变量否则每次中断后cnt归零PWM周期就失效了。本文还有配套的精品资源点击获取
返回列表