简介:本资源为面向电子信息、自动化等专业毕业设计的单片机光控路灯完整方案,适合需要完成课程设计或毕业设计的学生、指导教师参考。内容以51单片机为核心,结合光敏电阻、PCF8591模数转换、IIC总线与AT24C02存储、LED照明等模块,构建可随外界光线强度自动开闭的路灯控制系统,并融合时间与车流量三重控制思路,兼顾节能与自动管理。资源共1个doc文档,约1.32MB,内含设计说明书、系统硬件与软件设计流程、仿真图及源程序等,从摘要、绪论、目标与任务到硬件设计、软件设计、结论调试、心得体会与参考文献,结构完整,可直接作为论文框架与实现依据。文档还给出光敏电阻阻值200欧的判别阈值等具体参数,便于读者理解模数转换与IIC通信的实现细节。目前已有656人学习下载,适合作为单片机入门实践与毕业设计写作的参考。
1. 光控路灯的毕业设计,真正要交付的是三样东西
答辩现场最常见的一幕:仿真图跑起来了,灯也亮了,老师问一句「天黑到什么程度才开灯,这个值你量过吗」,人就卡住了。基于单片机的光控路灯设计含程序和仿真图,本质上不是三样独立的东西,而是一条链:光敏电阻把照度转成电压,单片机按某个阈值判断天黑天亮,再通过驱动级点亮路灯。程序决定了阈值的判断方式,仿真图决定了这条链能不能在电脑上先跑通,而阈值标定决定了它在真实环境里会不会整夜闪烁。
这份设计适合单片机原理及应用刚学完的本科生,也适合做单片机课程设计想拿高分的同学。51 单片机、STC 单片机都能做,差异主要在有没有片上 ADC。下面从选型讲到能跑的 C51 程序,再到 Proteus 仿真图的搭法和标定手法,按能复现的顺序排。
2. 光敏电阻取信号:比较器方案与 ADC 方案的选型对比
光控路灯的输入端只有一个目标:把「多亮」变成一个单片机能处理的数字量。这一步的元件选择和电路接法,直接决定了后面程序写多复杂、答辩能讲多深。
2.1 光敏电阻分压为什么比光电池更适合路灯
常见的光敏元件有三类:光敏电阻、光电池、光电二极管。路灯场景下几乎清一色用光敏电阻,原因是它的阻值跨度大且便宜。硫化镉光敏电阻在室内灯光下亮阻大约几百欧到几 kΩ,完全遮光时暗阻能到几百 kΩ 甚至几 MΩ,跨越三到四个数量级。把它和一个固定电阻串起来接在 VCC 和 GND 之间,中间节点就是一个随光照变化的电压,程序读一次 ADC 就得到一个 0 到 255 的数值。
光电池输出的是电流信号,本身内阻低、开路电压受照度影响不线性,要送到单片机还得加运放做跨阻放大,一颗运放加偏置网络的成本早就超过光敏电阻加分压电阻了。光电二极管响应快,适合做通信或者反射式检测,放在路灯这种分钟级变化的应用里属于杀鸡用牛刀。
提示:光敏电阻的响应时间在几十毫秒到几百毫秒量级,别拿它做过零检测或者快速遮光计数,路灯这种场景反而要利用它的慢响应做软件滤波。
接法上要留意方向。把光敏电阻放在分压电路的上臂,也就是 VCC 到节点之间,光照越强阻值越小,节点电压越高,ADC 读数越大;放在下臂则完全反过来。这个方向直接决定程序里判断「天黑」是用小于号还是大于号,写程序前先在纸上把这条推一遍,比在仿真里试半天快得多。
2.2 LM393 比较器方案与 ADC0832 模拟量方案的取舍
拿到分压节点电压之后有两条路。一条是接到 LM393 这类比较器的同相端,另一路接电位器分压做基准,比较器输出直接给单片机一个高低电平。另一条是接 ADC0832 这样的串行 ADC,或者用自带 ADC 的 STC12C5A60S2、STC15W 系列,读出具体数值由程序判断。
| 对比项 | LM393 开关量方案 | ADC0832 模拟量方案 | 片内 ADC 方案(STC 系列) |
|---|---|---|---|
| 元件成本 | 最低,一颗比较器加电位器 | 中,需外部 ADC 芯片 | 低,省掉外部 ADC |
| IO 占用 | 1 根 | 4 根(CS/CLK/DI/DO) | 1 根模拟输入 |
| 阈值调整方式 | 拧电位器,不可软件改 | 改代码常量即可 | 改代码常量即可 |
| 能否做迟滞 | 需正反馈电阻搭施密特电路 | 软件迟滞,自由设定 | 软件迟滞,自由设定 |
| 答辩可延伸点 | 少,容易讲完 | 时序、量化误差、标定 | 采样时间、参考电压精度 |
做毕业设计通常推荐 ADC 方案,原因不在精度,而在可调。路灯的开关阈值和当地纬度、季节、安装位置都有关系,硬件电位器一旦装上就不好改,而阈值写成一个#define就能随环境调,标定的时候也方便把实测数据写进报告。如果手上有 STC 带 ADC 的芯片,优先用它,少焊一颗芯片就少一个调试环节。
2.3 51 单片机最小系统与驱动级的接线要点
最小系统部分:12MHz 或 11.0592MHz 晶振加两个 30pF 起振电容,复位脚接 10uF 电容和 10kΩ 电阻构成上电复位,EA 接 VCC 使用片内程序存储器。IO 分配按下面的表来,留出冗余方便后面加串口调试。
| 单片机引脚 | 连接对象 | 说明 |
|---|---|---|
| P1.0 ~ P1.3 | ADC0832 的 CS、CLK、DI、DO | 软件模拟串行时序 |
| P1.4 | 光敏电阻分压节点(备用) | 换用片内 ADC 时启用 |
| P2.0 | 驱动三极管基极 | 经 1kΩ 限流,控制 LED 或继电器 |
| P3.0 / P3.1 | 串口 RXD / TXD | 标定时打印 ADC 值 |
| XTAL1 / XTAL2 | 晶振 | 配 30pF 对地 |
| RST | 10uF + 10kΩ | 上电复位 |
驱动级的选择看负载。仿真和小功率实物用 LED 加限流电阻最省事,P2.0 输出高电平时经 1kΩ 到 NPN 三极管基极,集电极串 LED 和 330Ω 到 VCC。要走 220V 路灯就得用继电器或可控硅,这两种都要做隔离,继电器线圈两端必须并一颗续流二极管,否则三极管关断瞬间的反峰电压会把管子打穿。引脚定义先在头文件里集中写好,后面所有代码都引用这几个宏,换板子的时候只改这一处。
#include <reg52.h> /* ADC0832 接口 */ sbit ADC_CS = P1^0; /* 片选,低电平有效 */ sbit ADC_CLK = P1^1; /* 串行时钟,单片机输出 */ sbit ADC_DI = P1^2; /* 单片机 -> ADC,送通道选择位 */ sbit ADC_DO = P1^3; /* ADC -> 单片机,读转换结果 */ /* 路灯驱动,高电平点亮 */ sbit LAMP = P2^0;这段定义放在工程主文件顶部,sbit是 Keil C51 的关键字,只能用在可位寻址的寄存器空间上,P0 到 P3 都满足。把硬件映射集中成一处的好处是后面换端口不用满工程搜索,也方便答辩时指着代码讲 IO 分配。
3. 光控路灯程序:ADC0832 采样、迟滞比较与 PWM 调光的 51 单片机实现
程序部分要解决三个问题:怎么把 ADC0832 的串行数据读进来,怎么判断天黑天亮而不在临界点反复开关,以及路灯点亮后怎么调光。三块分开写,最后在主循环里串起来。
3.1 ADC0832 读数的 C51 时序代码
ADC0832 是 8 位逐次逼近型转换器,双通道单端输入,接口是类 Microwire 的三线串行。DI 和 DO 在芯片上是复用引脚的,但分成两根线接更直观,单片机只写 DI、只读 DO,不会冲突。
/* 读取 ADC0832 指定通道,ch 取 0 或 1,返回 0~255 */ unsigned char ADC0832_Read(unsigned char ch) { unsigned char i, val = 0; ADC_CS = 0; /* 片选拉低,开始一次转换 */ ADC_CLK = 0; ADC_DI = 1; /* 起始位 S = 1 */ ADC_CLK = 1; ADC_CLK = 0; ADC_DI = 1; /* SGL/DIF = 1,单端输入模式 */ ADC_CLK = 1; ADC_CLK = 0; ADC_DI = ch & 0x01; /* ODD/SIGN:选择通道 0 或 1 */ ADC_CLK = 1; ADC_CLK = 0; ADC_DI = 1; /* 释放数据线,进入转换期 */ for (i = 0; i < 8; i++) { /* 高位在前读出 8 位结果 */ ADC_CLK = 1; ADC_CLK = 0; val = (val << 1) | ADC_DO; } ADC_CS = 1; /* 片选拉高,结束本次转换 */ return val; }逻辑上分两段。前三个时钟周期是配置期,单片机依次把起始位、单端模式和通道号推给 ADC,每个位都在时钟的下降沿被芯片锁存。之后芯片进入转换,第八个时钟下降沿之后结果就绪,接下来八个周期的下降沿依次输出数据的最高位到最低位,val = (val << 1) | ADC_DO这句就是把新读到的位拼到低位上。返回时拉高片选,芯片复位等待下一次启动。
参数和坑要盯三处。第一,通道号只有低两位有效,写ch & 0x01是防止调用时传了越界值。第二,时钟高低电平之间的时间不能太短,代码里靠指令周期自然形成的电平宽度在 12MHz 下大约几微秒,对 ADC0832 的几百千赫兹上限来说是够的,但如果以后把单片机主频拉到 35MHz,就得在ADC_CLK = 1后面插几个空操作。第三,没有滤波的 ADC 读数在最后一位上会跳,实际使用建议连续读三次取中间值,或者直接在软件层面对结果做一阶滤波。
3.2 迟滞比较:把临界抖动压下去的三个阈值参数
如果程序写成「读数小于 60 就开灯,否则关灯」,那么当环境照度恰好停在对应 60 的位置时,ADC 读数的末位噪声会让灯在几秒内反复开关,继电器咔咔响,LED 一闪一闪。解决办法是引入迟滞:开灯和关灯用两个不同的阈值,中间留一条回差带。
| 宏定义 | 示例值 | 含义 | 调大后的影响 |
|---|---|---|---|
DARK_TH | 60 | 读数低于此值判定天黑,开灯 | 更晚开灯,回差带宽 |
LIGHT_TH | 90 | 读数高于此值判定天亮,关灯 | 更早关灯,回差带宽 |
SAMPLE_MS | 200 | 两次采样的间隔 | 响应变慢,抗瞬时干扰变强 |
三个值里真正决定抗抖效果的是DARK_TH和LIGHT_TH的间距。间距太小等于没有迟滞,间距太大则会出现天已经亮了不少、灯还亮着的情况,夏天早晨尤其明显。经验做法是先量出全天 ADC 读数的变化曲线,把开灯点和关灯点取在曲线斜率最大的位置附近,两个值之间留出大约 20% 的量程。上面的 60 和 90 对应 255 量程的 12% 和 35%,实际值要在现场标定,别直接抄。
第三个参数是采样间隔。黄昏时光照变化很慢,每 200ms 采一次绰绰有余,采样稀疏一点还能过滤掉汽车大灯、闪电这类瞬时强光。如果做实物时发现灯会被路过车辆的远光灯误关,把SAMPLE_MS调到 500 甚至 1000,再加一个「连续三次满足条件才动作」的计数器,基本就能压住。
3.3 定时器 0 做 PWM 调光
路灯整夜全功率运行既费电又折寿,可以用 PWM 把占空比降下来。51 单片机没有硬件 PWM,用定时器 0 定时中断配合一个软件计数器就能做出来。
#define PWM_LEVEL 100 /* PWM 分辨率,100 级 */ volatile unsigned char duty = 0; /* 占空比 0 ~ 100,由主循环修改 */ static unsigned char pwm_cnt = 0; void Timer0_Init(void) /* 12MHz 晶振,50us 中断,PWM 频率 200Hz */ { TMOD &= 0xF0; TMOD |= 0x01; /* 定时器0,工作方式1,16位 */ TH0 = (65536 - 50) / 256; TL0 = (65536 - 50) % 256; ET0 = 1; /* 开定时器0中断 */ EA = 1; /* 开总中断 */ TR0 = 1; /* 启动定时器 */ } void Timer0_ISR(void) interrupt 1 { TH0 = (65536 - 50) / 256; /* 重新装初值,别忘 */ TL0 = (65536 - 50) % 256; pwm_cnt++; if (pwm_cnt >= PWM_LEVEL) { pwm_cnt = 0; } LAMP = (pwm_cnt < duty) ? 1 : 0; }中断里只做三件事:重装初值、计数器加一、比较输出电平。重装初值一定要写在中断函数开头,漏掉这一句定时器会从 0 重新计数,中断周期变成 65ms 左右,PWM 频率直接掉到听得到的范围,LED 会明显闪。分辨率取 100 级、中断周期 50us,一个完整 PWM 周期就是 5ms,对应 200Hz,人眼看不到闪烁,用手机摄像头拍也不容易出横纹。
占空比变量加volatile是关键,主循环在改它、中断在读它,编译器如果把它优化进寄存器,中断里读到的就永远是旧值。主循环里修改duty时最好一次赋值,避免出现中间态。
3.4 主循环与状态机
主循环只做采样、判断、改占空比三件事,用一个state变量记住当前是开灯还是关灯状态,迟滞的两次判断都依赖这个状态。
#define DARK_TH 60 #define LIGHT_TH 90 void Delay_ms(unsigned int ms) { unsigned int i, j; for (i = 0; i < ms; i++) for (j = 0; j < 110; j++); /* 12MHz 下约 1ms */ } void main(void) { unsigned char light; unsigned char state = 0; /* 0 = 灯灭,1 = 灯亮 */ Timer0_Init(); while (1) { light = ADC0832_Read(0); if (!state && light < DARK_TH) { /* 由亮转暗,开灯 */ state = 1; duty = 85; } else if (state && light > LIGHT_TH) { /* 由暗转亮,关灯 */ state = 0; duty = 0; } Delay_ms(200); } }状态机的好处是判断条件写成了「当前灭并且够暗」和「当前亮并且够亮」,两个条件不会同时成立,也就不会出现开灯和关灯逻辑打架。duty = 85表示开灯后先按 85% 占空比运行,如果要做后半夜降功率,可以在开灯后启动一个长定时器,运行四小时后把duty改成 40,这个扩展点写进答辩 PPT 比单纯说「能自动开关」有说服力。延时函数用双重循环凑出来,误差不重要,因为它只控制采样节奏。
4. Proteus 仿真图怎么搭:从元件库到能跑起来的光控路灯电路
仿真图是这份设计里最容易被卡住的环节,卡点通常不在电路本身,而在元件找不到、hex 加载不上、时钟频率设错。按下面的顺序搭,基本不会返工。
4.1 元件关键字与放置清单
Proteus 的 Pick Devices 对话框里直接输关键字搜索,下面这些名字在各版本里都通用。
| 元件关键字 | 数量 | 作用 |
|---|---|---|
AT89C51 | 1 | 主控,程序与 STC89C52 兼容 |
CRYSTAL | 1 | 晶振,属性改 12MHz |
CAP/CAP-ELEC | 2 / 1 | 30pF 起振电容、10uF 复位电容 |
RES | 若干 | 10k 复位与上拉、1k 基极限流、330 限流 |
LDR | 1 | 光敏电阻,放分压上臂 |
ADC0832 | 1 | 8 位串行 ADC,Data Converters 库里 |
2N5551 | 1 | NPN 驱动三极管 |
LED-YELLOW | 1 | 路灯指示 |
POT-HG | 1 | 可调电位器,代替 LDR 扫阈值 |
电源和地不用单独放,每个元件属性里都有隐藏的电源脚,Proteus 会自动连到全局网络。晶振和复位电路的画法和实物一致,两个 30pF 电容分别从 XTAL1、XTAL2 接地,复位脚上拉 10k 到 VCC、下接 10uF 到 VCC 做上电复位。
4.2 单片机加载 hex 与晶振、复位电路设置
Keil 那边先建工程、选AT89C51器件、把源文件加进去,Output 选项卡里勾上 Create HEX File,编译一次拿到.hex。回到 Proteus 双击单片机,弹出属性框里填两项:Program File 指向那个 hex,Clock Frequency 填 12MHz。
这两项必须对得上。晶振属性里画的 12MHz 只是给仿真器看的,真正决定仿真速度的是 Clock Frequency 这个数值。如果这里填了 11.0592 而代码里的定时初值是按 12MHz 算的,PWM 频率会整体偏掉大约 8%,仿真里看不出大问题,搬到实物上时序就对不上。反过来也一样,代码和这个数字必须来自同一个晶振频率。
另一个常见现象是仿真跑起来灯一直不亮或者一直亮着,先看 ADC0832 的 CS 有没有接对引脚,再确认 DI 和 DO 没有接反。DI 是单片机输出、ADC 输入,DO 方向相反,接反了程序读到的一直是全 0 或者全 1,看上去就像阈值判断失效。
4.3 用可调电位器替代 LDR 做阈值扫掠
Proteus 里的 LDR 元件阻值不会随光照实时变化,指望拖动它来模拟黄昏是不现实的。做阈值验证时的做法是:先把它换成POT-HG电位器,一个固定端接 VCC、一个接 GND、滑动端接 ADC 输入,仿真运行时用鼠标拖动滑块,人为把节点电压从高扫到低。
拖动过程中观察两个点。第一个是 LED 从灭变亮的瞬间,记下当时的滑块位置对应的电压,这就是实际的开灯阈值。第二个是反过来从亮变灭的瞬间,记下对应的电压。两者的差值就是迟滞窗口,如果这个差值太小,说明代码里的DARK_TH和LIGHT_TH挨得太近,回到程序里把间距拉开。用电压值比用 ADC 读数更直观,因为分压比和参考电压都摆在电路上。
阈值扫完之后再把电位器换回 LDR,接法和分压电阻的位置保持一致。这样仿真图交付时是一只标准光敏电阻,而验证过程用电压源搞定,两边都不耽误。答辩时如果老师问「你怎么知道阈值设的对不对」,可以直接说用可调分压扫过整个量程、记录了两个跳变点,这个回答比「试出来的」扎实得多。
5. 答辩前把阈值标定准:仿真与实物差异的排查手法
仿真能跑不等于实物能用,两者最大的差异来自光敏电阻的实际阻值范围。仿真里 LDR 的属性是一个固定值,实物上同一型号的暗阻可能从 200kΩ 到 1MΩ 不等,分压出来的电压自然不同,所以阈值必须在实物上重新量。最省事的办法是借串口把 ADC 读数打出来。
void Uart_Init(void) /* 11.0592MHz 下 9600bps,T1 方式2 */ { SCON = 0x50; /* 方式1,允许接收 */ TMOD |= 0x20; /* 定时器1,方式2,自动重装 */ TH1 = 0xFD; /* 9600bps 重装值 */ TL1 = 0xFD; TR1 = 1; } void Uart_SendByte(unsigned char c) { SBUF = c; while (!TI); /* 等发送完成 */ TI = 0; } /* 主循环里插一行:Uart_SendByte(light); 用串口助手看波形 */TH1和TL1都填0xFD是 9600bps 在 11.0592MHz 下的标准重装值,换成 12MHz 就要重算,误差会到 8% 以上,串口助手收到的就是乱码。连续采一整天,把读数按时间记下来,就能看到黄昏时数值下降的曲线,把开灯点定在曲线下降最陡的位置,关灯点往回让 20% 到 30%,写进报告里就是一组有依据的参数,而不是随手填的数字。
实物调试还有两个高频现象值得提前准备。一是白天灯不灭,通常是光敏电阻被安装盒挡住一部分,实际照度没到关灯阈值,把探头挪到不被遮挡的位置,或者适当调低LIGHT_TH。二是夜里灯一直闪,多半是供电纹波导致 ADC 参考不稳,在 ADC 的 VCC 脚旁边并一颗 0.1uF 瓷片电容,再配合前面说的连续采样计数,基本能解决。
最后留一个容易被忽略的细节:换晶振要同步改定时初值。用 11.0592MHz 做串口标定、却把 PWM 初值按 12MHz 写,得到的 PWM 频率会从 200Hz 变成 217Hz 左右,单看灯看不出问题,但用示波器量一下就露馅。把晶振频率定义成一个宏,定时初值用宏推导出来,比在两个地方各写一遍数字稳妥得多。
本文还有配套的精品资源,点击获取