
简介这是一份面向电子信息、自动化及单片机方向毕业设计与课程设计的光控路灯完整方案文档围绕STC89C52单片机、光敏电阻与LED路灯的自动控制展开重点解决传统定时路灯在季节更替、极端天气下误开误关以及能源浪费等问题。资源共1个doc文件压缩包约1.32MB内容涵盖硬件电路设计、PCF8591的A/D转换与IIC总线模块、AT24C02存储、七段数码管显示、系统软件流程、程序源代码以及附录中的仿真图与源程序并附有结论调试、常见问题解决办法和心得体会。设计采用光线强度、时间与道路车流量三重控制模式以光敏电阻阻值200欧为阈值实现路灯自动启闭能够帮助读者理清传感器采样、模数转换到执行控制的完整链路。目前已有656人学习适合需要参考完整选题报告结构、电路与代码实现及仿真验证过程的读者。1. 光控路灯要解决的是把“几点开灯”换成“多暗才开灯”光控路灯要解决的问题很具体夏天晚上七点半天还亮着时控的灯已经点亮遇上日全食那种正午发黑它照样不亮。这套以 51 单片机为主控的光控路灯设计用光敏电阻采集光照、PCF8591 做 8 位模数转换、AT24C02 保存判断阈值、74HC573 锁存后驱动四位七段数码管和 LED 路灯配完整的 Keil C51 程序与 Proteus 仿真图是单片机课程设计里最经典的一类题目。真正卡住人的从来不是把原理图连起来而是三件事IIC 时序写不对ADC 一个数都读不回来阈值写死在一条if里傍晚 LED 会反复闪仿真里晶振频率和程序里的延时假设对不上数码管像坏了一样乱跳。这套方案适合刚学完 51 的人照着复现也适合已经会点灯的人回头补一遍总线时序和参数整定的基本功——把这三处理顺光控、时控、车流量控制的扩展都能挂上去。2. 光敏电阻到 LED 的硬件链路分压、ADC 与锁存的分工2.1 光强采集的三种做法与取舍光敏电阻是阻值型器件光照越强阻值越小暗态能到几百 kΩ强光下降到一个 kΩ 量级变化范围大但线性度很差。直接拿它接一只 LM393 电压比较器电路最省可阈值是电位器拧出来的改一次要动一次手也没有任何数据可以记录、显示。PCF8591 这类带 IIC 接口的 8 位 ADC把光强变成一个 0~255 的字节阈值就成了程序里的一个变量能写进 EEPROM能加迟滞窗口和计时判断。BH1750 一类的数字光照传感器直接输出勒克斯线性和一致性更好代价是多一颗器件、多一套协议反而看不清模数转换这条链路上要练的东西。方案需要的外设阈值能否软件修改典型使用场景光敏电阻 LM393 比较器比较器、电位器否只求亮灭、不做记录光敏电阻 PCF8591IIC 接口 8 位 ADC是可存 EEPROM课程设计要显示采样值BH1750 等数字光照芯片IIC 光照传感器是单位是 lx需要照度可标定、可对标课程设计选中间这一列最划算成本不高IIC 时序、模数转换、阈值判断三个知识点全在里面数码管还能把采样值实时显示出来方便现场调门限。2.2 元件分工与几个必须记住的参数器件关键端口/参数在系统里干什么STC89C52仿真可用 AT89C5211.0592MHz 晶振 22pF×29 脚复位31 脚/EA接高电平采样、判断、扫描显示光敏电阻暗阻数百 kΩ亮阻约 1kΩ把光强转成阻值变化PCF8591写地址0x90、读地址0x91AIN0~AIN3VREF 接 5V模数转换输出 0~255AT24C02写地址0xA0、读地址0xA1内部写周期 5ms掉电保存阈值74HC573 ×2LE 接 P1.0/P1.1OE 接地锁存段码和位选四位七段数码管共阴限流电阻 220Ω显示当前采样值晶振选 11.0592MHz 是为了串口波特率能整除这套设计里虽然没用到串口但延时函数都是按这个频率标定的换 12MHz 后时序会整体偏一点IIC 一般撑得住数码管扫描会略有变化。2.3 分压电阻取值与 ADC 码值换算光敏电阻不能直接接到 ADC 输入端必须和一只固定电阻串联分压分压点接到 AIN0。取固定电阻 R_fix 在下、光敏电阻 R_LDR 在上则分压点电压 V 5V × R_fix / (R_LDR R_fix)环境变暗、R_LDR 变大时电压反而下降把两者位置对调方向就反过来。程序里的判断方向由这个接法决定不是约定俗成的。以 R_fix 10kΩ 为例几个典型点的换算关系如下光照状态近似 R_LDR分压点电压ADC 码值强光1kΩ4.55V约 232室内正常10kΩ2.50V约 127傍晚临界40kΩ1.00V约 51夜间200kΩ0.24V约 12原设计把判断条件写成“光敏电阻阻值大于等于 200 欧时点亮”这句话直接照抄进代码会踩坑常见光敏电阻的亮阻都在 1kΩ 上下200Ω 这个门限几乎永远触发不到灯会一直灭。两种改法一是把 200 当成 ADC 码值也就是 0~255 里的 200属于偏暗的判断点二是按欧姆认真算先用下面这段把码值反推成阻值看清楚实测数据落在哪个区间再定门限。/* 由 ADC 码值反推光敏电阻阻值全部用整数运算避免 51 上跑浮点 */ unsigned int adc_to_ohm(unsigned char code, unsigned int r_fix) { unsigned long v_adc; /* 分压点电压单位 mV */ if (code 0) return 60000U; /* 全暗阻值视为无穷大用一个上界代替 */ if (code 255) return 1U; /* 全亮阻值趋近于零 */ v_adc (unsigned long)code * 5000UL / 255UL; /* 5V 参考、8 位分辨率 */ return (unsigned int)((unsigned long)r_fix * (5000UL - v_adc) / v_adc); }逻辑上只做了一件事把code * 5000 / 255得到毫伏值再用串联分压公式反解。参数里r_fix传固定电阻的实际阻值10kΩ 就传 10000code是 PCF8591 读回来的原始字节两个端点单独处理是为了避开除零和整数溢出。51 上做这种换算建议用unsigned long中间结果最大到 60000×5000int装不下。2.4 IIC 上拉电阻与地址脚SDA、SCL 两根线各接一只 4.7kΩ 电阻上拉到 5V这是 IIC 总线能工作的前提Proteus 里漏画这两只电阻仿真可能靠默认电平侥幸跑通实物板子上一定读不到数据。AT24C02 和 PCF8591 的 A0~A2 全接地时写地址分别是0xA0和0x90读地址是0xA1和0x91程序里的常量要和硬件一致。PCF8591 的 VREF 接 5V不能超过 VCC74HC573 的 OE 接地让输出始终有效靠 LE 控制锁存。3. IIC 时序在 C51 里落地PCF8591 采样与 AT24C02 存取3.1 起始、停止与位收发IIC 的坑几乎全在时序的第一个字节上。用普通 IO 口模拟时SDA 和 SCL 都定义成sbit每一位之间插几个空操作保证从机来得及响应。起始条件是 SCL 保持高电平期间 SDA 出现下降沿停止条件反过来很多例程把这两句顺序写反现象就是第一次读 ADC 返回 0xFF。#include reg52.h #include intrins.h #define uchar unsigned char sbit SDA P3^2; /* IIC 数据线双向 */ sbit SCL P3^3; /* IIC 时钟线主机输出 */ /* 位延时11.0592MHz 下约 5us总线速度远低于 100kHz稳妥优先 */ void iic_delay(void) { _nop_(); _nop_(); _nop_(); _nop_(); } void iic_start(void) { SDA 1; SCL 1; iic_delay(); SDA 0; iic_delay(); /* SCL 高电平期间 SDA 下降沿 */ SCL 0; } void iic_stop(void) { SDA 0; SCL 1; iic_delay(); SDA 1; iic_delay(); /* SCL 高电平期间 SDA 上升沿 */ } /* 发送一字节返回从机应答位0应答1非应答 */ bit iic_send(uchar dat) { uchar i; bit ack; for (i 0; i 8; i) { SDA (dat 0x80) ? 1 : 0; /* 高位先行 */ dat 1; iic_delay(); SCL 1; iic_delay(); SCL 0; } SDA 1; /* 释放数据线等从机拉低 */ iic_delay(); SCL 1; iic_delay(); ack SDA; SCL 0; return ack; } /* 接收一字节参数 ack1 表示主机回非应答结束读取 */ uchar iic_recv(bit ack) { uchar i, dat 0; SDA 1; for (i 0; i 8; i) { SCL 1; iic_delay(); dat (dat 1) | SDA; SCL 0; iic_delay(); } SDA ack ? 1 : 0; iic_delay(); SCL 1; iic_delay(); SCL 0; return dat; }参数说明iic_send的返回值可以直接当故障标志用读 ADC 前先判断它是不是 0能省掉大量“数码管显示 255 不知道为什么”的时间。iic_recv的形参对应第 9 个时钟周期主机给出的应答读单字节时传 1连续读多字节时前几字节传 0、最后一字节传 1。3.2 PCF8591 单通道采样PCF8591 的控制字节决定通道和输出使能写地址是0x90读地址是0x91。第一次读回的值是上一次转换的结果这个特性在设计里必须处理否则显示的总是慢一拍的数据。控制字位含义D7 D6固定 00D5模拟输出使能这里不用写 0 或 1 均可D4自动增量标志D3固定 0D2通道选择高位D1 D0通道选择00AIN001AIN1 依此类推/* 读取 PCF8591 指定通道ch0 对应 AIN0 */ uchar pcf8591_read(uchar ch) { uchar val; iic_start(); iic_send(0x90); /* PCF8591 写地址 */ iic_send(0x40 | (ch 0x03)); /* D5 置 1低两位选通道 */ iic_start(); /* 重复起始切到读方向 */ iic_send(0x91); /* PCF8591 读地址 */ iic_recv(1); /* 丢弃上一次的转换结果 */ val iic_recv(1); iic_stop(); return val; }这里的“重复起始”是必须的先写控制字再读数据如果写成停止后再起始部分批次的 PCF8591 会丢掉通道设置读回来的一直是 AIN0。写控制字和读数据之间的间隔不要塞长延时越长越容易被主循环的其他任务打乱节奏。3.3 AT24C02 保存阈值阈值要断电不丢写进 AT24C02 的 0x00、0x01 两个字节。AT24C02 的字节写之后有约 5ms 的内部擦写周期这期间它不响应总线程序里必须等否则下次上电读出来还是旧值甚至 0xFF。/* 写一字节到指定地址写完必须等内部写周期 */ void at24c02_write(uchar addr, uchar dat) { iic_start(); iic_send(0xA0); iic_send(addr); iic_send(dat); iic_stop(); Delay(10); /* 10ms 覆盖 5ms 写周期 */ } uchar at24c02_read(uchar addr) { uchar dat; iic_start(); iic_send(0xA0); iic_send(addr); iic_start(); iic_send(0xA1); dat iic_recv(1); iic_stop(); return dat; }Delay(10)是按毫秒级的软件延时来写的和晶振频率绑定。阈值只在按键修改时才写入不要放进主循环不然每轮都写一次EEPROM 的寿命会被毫无意义地消耗掉。3.4 采样滤波去极值再平均单次采样很容易被手电、车灯晃一下主循环里跑 8 次采样去掉一个最大值和一个最小值再平均抖动会小一个量级。/* 8 次采样去极值后平均返回值仍是 0~255 */ uchar adc_filter(uchar ch) { uchar i, min, max, v[8]; unsigned int sum 0; for (i 0; i 8; i) v[i] pcf8591_read(ch); min max v[0]; for (i 0; i 8; i) { if (v[i] min) min v[i]; if (v[i] max) max v[i]; sum v[i]; } return (uchar)((sum - min - max) / 6); }数组开 8 个字节加上unsigned int的求和变量对 STC89C52 的 256 字节内部 RAM 来说完全可以承受。采样次数从 8 改成 4 也行除数要同步从 6 改成 2漏改这一处会出现数值整体偏大的怪现象。4. 数码管动态扫描与 LED 阈值判断74HC573 锁存与迟滞比较4.1 引脚分配与 74HC573 的作用P0 口接两块 74HC573一块送段码、一块送位选LE 分别接 P1.0 和 P1.1OE 都接地。74HC573 是八路透明锁存器LE 为高时输出跟随输入LE 为低时锁存保持这一条正是防止“换位瞬间串码”和“LED 一闪一闪”的关键。LEOE输出行为高低跟随 Dn 输入变化低低保持最后一次输入与 Dn 无关任意高高阻态负载等于断开LED 路灯的控制脚接 P1.2高电平点亮。源程序里还有一行sbit rst P3^4;注释写着 DS1302 复位端但这套设计里并没有 DS1302是别的例程里抄过来的残留引脚删掉不影响功能留着也只在 P3.4 上多占一个脚。4.2 段码位码表与动态扫描共阴数码管的段码表是固定的位选码按扫描顺序取低电平有效。扫描节奏控制在 2ms 一位四位一轮 8ms刷新率超过 100Hz肉眼看不到闪烁。sbit LE_SEG P1^0; /* 段码锁存使能 */ sbit LE_BIT P1^1; /* 位选锁存使能 */ sbit LED P1^2; /* LED 路灯控制端 */ uchar code Duanma[10] {0x3f,0x06,0x5b,0x4f,0x66, 0x6d,0x7d,0x07,0x7f,0x6f}; uchar code Weima[4] {0xfe,0xfd,0xfb,0xf7}; /* 共阴位选低电平有效 */ void disp_val(unsigned int val) { uchar buf[4], i; buf[0] val / 1000 % 10; buf[1] val / 100 % 10; buf[2] val / 10 % 10; buf[3] val % 10; for (i 0; i 4; i) { P0 0x00; LE_SEG 1; LE_SEG 0; /* 先消隐防止换位串码 */ P0 Weima[i]; LE_BIT 1; LE_BIT 0; P0 Duanma[buf[i]]; LE_SEG 1; LE_SEG 0; Delay(2); } }Duanma里每一位对应 a~g、dp 七段的亮灭共阴下 1 为亮0x06 就是数字 1。Weima四组对应四位若换成共阳数码管段码要整体取反、位选改成高电平有效改一种不改另一种会出现“位选对了但数字全反”的现象。Delay(2)就是 2ms 左右的位保持时间调大到 5ms 会出现可见闪烁调小到 0.5ms 会亮度不足。4.3 阈值判断迟滞窗口加计时确认最朴素的写法是if (adc 200) LED 1; else LED 0;傍晚光照在门限附近来回摆动LED 就会一秒亮灭好几次继电器方案里这种抖动还会烧触点。合格的做法是给判断加上两样东西一个迟滞窗口开灯门限和关灯门限差 20~30 个码值一个计时确认条件连续满足若干毫秒才真正动作。后者同时也解决了“虚强光”——手电扫一下、远处车灯照一下不足以让路灯熄灭。uchar th_on 200; /* 开灯门限可由 EEPROM 覆盖 */ uchar th_off 170; /* 关灯门限两者之差构成迟滞窗口 */ #define KEEP_TICKS 300U /* 主循环约 10ms 一轮300 轮约 3 秒 */ /* 返回新的路灯状态0灭1亮 */ uchar lamp_decide(uchar adc, uchar state) { static unsigned int cnt 0; if (state 0) { if (adc th_on) { if (cnt KEEP_TICKS) { cnt 0; return 1; } } else cnt 0; } else { if (adc th_off) { if (cnt KEEP_TICKS) { cnt 0; return 0; } } else cnt 0; } return state; } /* 上电从 EEPROM 恢复门限空片或异常值回落到默认值 */ void params_load(void) { uchar v at24c02_read(0x00); th_on (v 0xFF || v 100) ? 200 : v; v at24c02_read(0x01); th_off (v 0xFF || v th_on) ? (th_on - 30) : v; }判断方向要和 2.3 里的分压接法对齐分压点电压随环境变暗而升高时用adc th_on点灯接法相反就把两个比较符对调。KEEP_TICKS依赖主循环的实际周期如果后续把 IIC 延时加大或采样次数改多这个数要跟着重算最稳的做法是把它改成定时器中断里的计数。4.4 主循环主循环只做四件事读门限、采样、显示、判断各干各的互不阻塞。void main(void) { uchar adc, state 0; /* state 初值 0表示上电默认灯灭 */ params_load(); /* 从 AT24C02 取回阈值 */ while (1) { adc adc_filter(0); /* AIN0 采样并滤波 */ disp_val(adc); /* 数码管显示当前采样值 */ state lamp_decide(adc, state); /* 迟滞 计时判断 */ LED state ? 1 : 0; } }把采样值直接显示在数码管上是现场标定门限的前提遮住光敏电阻读一次暗态值用手电照一下读一次亮态值两个数记下来门限就有依据了不用猜。5. Proteus 仿真联调从 .hex 加载到门限整定5.1 仿真工程的几个必改项配置项正确取值不这样设的后果MCU 型号AT89C52替代 STC89C52找不到器件或引脚不匹配Program FileKeil 生成的 .hex只加载 .c 文件仿真无法启动Clock Frequency11.0592MHz与代码里的延时假设不符数码管偏暗或乱闪PCF8591 VREF接 5V采样值与实际光强对不上IIC 上拉SDA/SCL 各 4.7kΩ 到 5V数据线拉不起来读数恒为 0xFF光敏电阻模拟LDR 元件或电位器手动调阻值无法构造暗态和亮态两个测试点仿真里的光敏电阻常见做法是用 LDR 元件配合光源强度调节或者直接用电位器代替手动拧到暗态和亮态两组值验证程序能否正常翻转。用光源时记得把仿真速度调到与实际接近否则光照变化在仿真里快到程序来不及采样。5.2 常见失败现象与定位路径现象大概率原因处置数码管全亮、显示乱码段位接反或共阴共阳选错换 7SEG-MPX4-CC 型号核对段码表读数恒定 0xFF上拉电阻缺失或时序不对补 4.7kΩ检查起始条件顺序读数慢一拍没丢弃 PCF8591 首次结果读两次用第二次LED 频繁闪没有迟滞窗口和消隐加 th_on/th_off 差值换位前写 P00x00上电读回 0xFF写 EEPROM 后没等写周期写入后加 10ms 延时强光一晃灯就灭缺少计时确认KEEP_TICKS 抬到 3~5 秒对应值调试顺序建议从数据通路查起先用仿真里的逻辑分析仪或虚拟终端确认 SDA/SCL 上有完整的起始、地址、应答波形再看 PCF8591 返回的字节是不是随电位器变化最后才看 LED 的翻转逻辑。跳过前两步直接调门限等于在一片未知里猜数。5.3 门限整定与向现实场景的扩展整定门限最省事的办法是利用数码管的实时显示把环境光遮到傍晚那一档读出码值记作 A完全遮住读一次夜态值记作 B用手电照住读一次亮态值记作 C。取th_on落在 A 与 B 之间、略偏向 Ath_off取th_on - 30左右两个值分别写进 AT24C02 的 0x00 和 0x01断电重上电读回核对一遍。想把这套设计往“三重模式控制”的方向补接口都已经留好了PCF8591 还剩 AIN1~AIN3 三路接上电位器或者霍尔测速模块就能把车流量纳入判断主循环里再叠一层“深夜降功率”的时段逻辑即可光敏电阻换成 BH1750 后门限单位从 ADC 码值变成勒克斯整定会更容易复现。整个系统里最脆的一环始终是那颗光敏电阻的长期稳定性实物化时把它换成带防护罩的封装比在代码里加多少滤波都管用。本文还有配套的精品资源点击获取