
简介这是一份基于AT89C51单片机的自动窗帘系统毕业设计完整文档适合电子信息、自动化等相关专业学生用于课程设计或毕业设计参考。内容涵盖系统总体方案论证、传感器检测电路、步进电机执行机构、显示与键盘接口设计以及半自动、自动、定时三种控制模式的软硬件实现并配有系统框图、模块流程和调试说明结构清晰便于理解单片机控制系统从需求分析到设计验证的完整流程。文档可直接下载后按需编辑修改方便用于论文撰写、答辩准备或二次开发。资源包共含1个docx文件大小约2.09MB内容集中精炼。目前已有372人学习下载具备一定参考热度适合需要快速搭建智能家居类单片机项目设计的读者使用。1. 基于单片机的自动窗帘系统到底在做什么自动窗帘不是新概念但把它作为毕业设计做出来和买一个成品电机轨道完全是两码事。毕设要求的是你从传感器选型、信号采集、电机驱动到控制逻辑把整套链路亲手走通。换句话说成品窗帘是黑盒毕业设计是让你把盒子拆开把每一层都讲清楚。基于单片机的自动窗帘系统核心任务就是让单片机根据光照、时间或手动指令自主决定窗帘的开合幅度并且能在断电、堵转、限位丢失这些异常情况下不失控。这东西适合谁做适合学过基础单片机、想用一门课把模拟电路、数字电路、C语言、电机控制全部串起来的人。做完你会发现这套系统的技术栈其实就是智能家居里窗帘电机、卷帘门控制器的简化版知识完全可以平移。2. 系统硬件架构与关键器件选型2.1 主控选型51 还是 STM32由外设需求决定自动窗帘系统的控制核心算力需求并不高真正决定选型的是你要接多少路输入输出、需要几路定时器、要不要 ADC。我一般会先列一个外设清单光照传感器至少占一路 ADC按键输入至少两路电机驱动需要两路 PWM 和两个方向控制 IO限位开关两路如果再加红外人体检测和温湿度传感器IO 和中断资源的需求立刻就上来了。主流选择就两个方向。51 单片机如 STC89C52RC的优势在于资料多、寄存器简单、成本极低像 STC 的片子还支持在线下载调试起来不用额外买烧录器。缺点是 ADC 需要外部扩展常见做法是用 ADC0832 或 PCF8591定时器只有两个如果既要 PWM 又要定时中断做按键扫描资源会非常紧张。STM32F103 系列则自带多路 ADC、高级定时器、硬件 PWM单芯片就能把所有外设接完缺点是寄存器复杂刚上手时 Keil 的工程配置就能劝退一批人。如果按毕业设计的稳妥路线走我建议优先选 51 平台理由不是性能而是可解释性。答辩时老师问的往往不是“你怎么实现 PWM”而是“你定时器初值怎么算的、中断优先级怎么安排的”这些在 51 上特别好讲清楚。而且 STC 单片机用串口就能下载程序不需要额外买仿真器成本上很有优势。下面是两个方案的典型资源对比资源项STC89C52RC51 平台STM32F103C8T6ADC需外扩ADC0832/PCF8591内置 2 个 12 位 ADC最多 10 通道定时器2 个 16 位4 个 16 位支持多路 PWMPWM需用定时器模拟高级定时器硬件输出程序存储8KB Flash64KB Flash下载方式串口下载免仿真器SWD/JTAG需 ST-Link2.2 电机驱动方案步进、直流减速还是舵机自动窗帘的负载是窗帘轨道上的阻力属于轻负载、慢速、需要可控行程的应用。三种常见电机里步进电机最适合做精确行程控制因为你可以通过脉冲数算出窗帘走了多远——相当于自带位置传感器。直流减速电机转矩大、便宜但需要编码器才能知道位置而编码器安装和信号处理对毕设来说是多出来的工作量。舵机如 SG90虽然控制简单但转动角度有限只适合小窗模型演示不适合真实窗帘轨道。我的推荐是 28BYJ-48 步进电机配 ULN2003 驱动板。这款电机在 5V 驱动下转速不快正好符合窗帘缓慢开合的需求而且它是四相五线制用 ULN2003 驱动板直接接单片机 IO 即可不需要额外的电机驱动芯片。电路连接方式很简单P1.0 到 P1.3 分别接 ULN2003 的 IN1 到 IN4电机正转和反转就是改变四相脉冲的时序而已。如果窗帘轨道负载较大步进电机扭矩不够可以换成直流减速电机加 L298N 驱动模块但这时建议给电机轴加一个霍尔编码器否则开合位置只能靠限位开关兜底。2.3 传感器组合光敏、人体红外与手动优先的权限设计一个“自动”窗帘系统输入源通常有三种环境光照、人体存在、手动按键。光照强度决定窗帘该不该开合人体红外PIR传感器用于检测房间是否有人没人时降低响应灵敏度以省电手动按键则永远拥有最高优先级。三种输入对应三种控制权限这在软件里要明确划分否则会出现人刚按下“关闭”键光敏传感器立刻又把它打开的尴尬局面。光照检测我习惯用光敏电阻加 10K 分压电阻的方案接单片机 ADC 引脚。光是模拟量需要先得出一个电压阈值才能把它转成“亮/暗”的开关逻辑。PIR 传感器如 HC-SR501直接输出数字电平但要注意它有 2 到 3 秒的启动稳定时间上电后不能立刻读它否则容易误判。手动按键需要做消抖这个放在软件章节细说。另外建议加一个手动/自动切换拨码开关实现“自动模式”和“手动模式”的硬件级隔离——比在代码里用标志位切换可靠得多关键时候能救命。2.4 限位开关与一键开关机电路防失控的最后一道保险自动窗帘最怕什么怕电机一直在转窗帘到头了还在转。轻则齿轮打滑重则烧驱动板。所以限位开关是必选项常见做法是在窗帘轨道的两端各装一个轻触开关或干簧管电机运行时持续检测这两个引脚一旦触发立即停止。如果轨道上不方便装机械开关可以用电流检测的方法电机堵转时电流会明显上升通过采样电阻加比较器也能做出堵转保护但电路复杂度高一些。毕设优先选机械限位。这里我额外提一个很多教程忽略的细节单片机一键开关机电路。因为很多毕设用户希望窗帘系统平时不耗电用按键既能开机又能关机。常见做法是用一个 NMOS 管和一个按键组成自锁开关按键按下时 MOS 导通给单片机供电单片机启动后拉高一个 IO 口维持 MOS 导通再次检测到按键长按时拉低 IO 实现关机。这套电路在 STC 单片机和 51 平台都很常见网上搜索“单片机一键开关机电路”就能找到参考原理图。它跟窗帘系统的关系在于系统不需要运行的时候彻底断电比睡眠模式更省电也更能体现硬件设计能力答辩时是个加分项。3. 光照采集、信号输入与软件滤波实现3.1 光敏电阻分压电路与 ADC 采样代码硬件上光敏电阻和一个 10K 固定电阻串联中间抽头接单片机的 ADC 输入引脚。当环境光变强光敏电阻阻值下降分压点的电压上升反之电压下降。如果你的平台是 STC89C52ADC 需要外扩我用得比较多的是 ADC0832它通过 CLK、DO、DI 三根线和单片机通信代码量不大。以下是基于 STC 平台读取光敏电压的核心片段// 读取 ADC0832 通道 CH0 的 8 位转换结果 unsigned char adc0832_read(unsigned char channel) { unsigned char i, dat1 0, dat2 0; ADC_CLK 0; ADC_DI 1; ADC_CS 0; // 片选拉低启动转换 // 起始位 通道选择位共 4 个时钟周期 for (i 0; i 4; i) { ADC_CLK 1; ADC_CLK 0; } // 数据输出阶段先收高 4 位再收低 4 位 for (i 0; i 8; i) { ADC_CLK 1; dat1 (dat1 1) | ADC_DO; ADC_CLK 0; } for (i 0; i 8; i) { dat2 (dat2 1) | (ADC_DO 7); ADC_CLK 1; ADC_CLK 0; } ADC_CS 1; return (dat1 dat2) ? dat1 : 0; // 校验数据一致性 }这段代码是 ADC0832 的标准时序。先说逻辑片选拉低后单片机先在 CLK 上升沿发送起始位和通道选择位之后在下一个 8 个时钟周期内从 DO 引脚读回转换结果。协议本身是双向的同一根 DO 引脚先当输出用发送配置再当输入用读取数据。校验部分是 ADC0832 的特色它会把转换结果输出两遍一次正序一次反序两个结果一致才认为数据有效这能避免信号线上的毛刺导致误判。实际使用时ADC_DO和ADC_DI如果由同一 IO 口复用读取前要注意方向切换。读回来的数值是 0 到 255但你不能直接用这个裸值去控制窗帘因为光敏电阻的阻值随光照不是线性的而且不同房间的基准光照不同。我一般会在主循环里每 200 毫秒读一次做滑动平均滤波再与标定好的阈值比较。阈值怎么标定放在第 5 章讲这里先记住一个原则阈值不要写死要能通过按键进入标定模式现场设定。3.2 按键消抖与 PIR 传感器的读取时机按键和 PIR 的输入处理是“看起来简单、实际全是坑”的地方。机械按键按下时触点会经历大约 5 到 10 毫秒的抖动期电平在这个窗口内会多次跳变如果不处理一次按下会被识别成多次触发。软件消抖有两种常见思路一是检测到电平变化后延时 10 到 20 毫秒再读一次确认电平没变才算有效二是用定时中断每隔 10 毫秒扫描一次按键连续两次扫描结果一致才更新状态。第二种不阻塞主循环在窗帘系统这种需要同时处理多个输入的场景下更合适。PIR 传感器的读取时机和按键又不太一样。HC-SR501 在检测到人体移动时输出高电平但这个电平会保持数秒如果你在主循环里不停读它会重复触发逻辑。正确做法是只在电平从低变高的边沿处产生一次有效事件之后进入冷却期例如 30 秒内不再响应新的触发。这样做的原因是人在房间里不可能一直不动PIR 检测的本质是“有没有人在动”而不是“房间有没有人”。如果人静止坐着看书PIR 是检测不到的所以不能靠它判断“有人在房间”只能用它来做“有人活动时提高光照响应灵敏度”这类辅助逻辑。3.3 光照阈值自适应平均值基准与迟滞比较直接拿一个固定阈值去和 ADC 值比较会出现窗帘在黄昏时反复开合的问题——光照在阈值附近来回波动窗帘也跟着抽搐。解决这个问题需要用迟滞比较施密特触发的思路设定两个阈值光照从暗到亮超过上限才关窗帘从亮到暗低于下限才开窗帘上下限之间留出足够大的死区例如 30 个 ADC 量化值。另外一个提升体验的做法是自适应基准。系统上电后先连续采样 50 次取平均值作为“当前环境基准”之后每次判断都拿实时值跟基准做差而不是跟绝对阈值做差。这能解决“上午十点”和“下午四点”光照绝对值差异很大的问题——如果拿一个固定的 150 作为阈值下午三点可能永远触发不了开帘因为它低于 150。我见过不少毕设论文里只写了固定阈值但答辩老师一问“阴天怎么办”就卡壳。加上自适应基准这个问题的答案就变成了基准跟着环境漂移窗帘只对“突变”响应。// 滑动平均滤波 自适应基准更新 #define SAMPLE_COUNT 10 unsigned char light_buffer[SAMPLE_COUNT]; unsigned char buffer_index 0; unsigned int light_avg 0; unsigned int light_base 0; void light_sensor_update(void) { unsigned char i; unsigned int sum 0; light_buffer[buffer_index] adc0832_read(0); buffer_index (buffer_index 1) % SAMPLE_COUNT; for (i 0; i SAMPLE_COUNT; i) { sum light_buffer[i]; } light_avg sum / SAMPLE_COUNT; // 每 30 秒用当前平均值缓慢更新基准防止基准突变 if (base_tick 150) { base_tick 0; light_base light_avg; } }这个代码片段里有两个关键参数SAMPLE_COUNT决定滤波强度越大越平滑但响应越慢10 次配合 200 毫秒采样周期窗口是 2 秒对窗帘这种慢速设备足够base_tick决定基准更新周期150 次循环约等于 30 秒基准不跟随瞬间遮挡比如人走过窗口而跳变。注意判断开合时要用light_avg和light_base的差值而不是light_avg本身。4. 步进电机驱动、行程控制与系统状态机4.1 四相八拍驱动时序与正反转控制28BYJ-48 步进电机是四相五线制用 ULN2003 驱动。它的内部转子是永磁体定子有四组线圈按顺序给四组线圈通电转子就会一步一步转动。四相八拍的通电顺序是A-AB-B-BC-C-CD-D-DA每一步转过半个步距角整步模式下步距角是 5.625 度八拍模式下实际每一步是 2.8125 度。对窗帘来说这个精度已经足够不需要细分驱动器。// 四相八拍正转/反转脉冲表 code unsigned char motor_fwd[8] {0x01,0x03,0x02,0x06,0x04,0x0C,0x08,0x09}; code unsigned char motor_rev[8] {0x09,0x08,0x0C,0x04,0x06,0x02,0x03,0x01}; void motor_step(unsigned char phase, bit dir) { if (dir) { P1 motor_fwd[phase]; // 高 4 位无效低 4 位对应 IN1-IN4 } else { P1 motor_rev[phase]; } } void motor_run(unsigned int steps, bit dir) { unsigned char phase 0; unsigned int i; for (i 0; i steps; i) { motor_step(phase, dir); phase (phase 1) % 8; delay_ms(2); // 这个延时决定转速2ms 对应约 100 步/秒 } }motor_fwd表里的每个值对应一组 IO 状态0x03 表示 A 相和 B 相同时通电也就是 AB 状态。P1口的低四位接 ULN2003 的 IN1 到 IN4所以直接给 P1 赋值就能控制四组线圈。延时时间是最关键的参数2 毫秒对应每秒 100 步28BYJ-48 内部减速比是 1:64所以输出轴每秒约转 1.56 圈对窗帘来说速度适中。如果你发现电机抖动但不动通常不是代码问题而是通电顺序错了——检查一下是不是把反向表当成正转表用了。另外 ULN2003 的 5V 供电要注意单片机开发板上的 5V 是从 USB 取的拉不动电机必须用外部 5V 电源给 ULN2003 单独供电共地即可。4.2 行程控制限位开关优先时间估算兜底窗帘开合的终点检测可靠度排序是硬件限位开关 编码器计数 时间估算。最稳的设计是三者叠加限位开关作为硬保护时间估算作为正常运行时的停机依据编码器计数如果有用于显示当前位置。用时间估算的思路是测量电机从全关到全开需要多少秒记为T_full那么要开到 50%就是T_full * 0.5秒。但步进电机有个特性是起步时的丢步问题启动瞬间如果频率太高转子跟不上脉冲会丢几步时间越长丢步累积越多。所以实际运行时间要比理论值多预留 5% 到 10% 的余量终点交给限位开关去修正。限位开关的代码逻辑不复杂但有一个顺序问题电机正在往开的方向走触发了开限位这时要立即停但停的位置不能让窗帘绷太紧需要让电机反转几小步卸力。这个“卸力步数”建议固定写 10 步不多不少。另外限位开关消抖也不能省虽然步进电机震动小但窗帘轨道如果不够顺滑开关触点也会抖动建议在限位检测函数里加 5 毫秒的连续确认逻辑。// 窗帘运行到限位时的处理 bit limit_open 0; bit limit_close 0; void check_limit(void) { static unsigned char open_cnt 0, close_cnt 0; if (LIMIT_OPEN_PIN 0) { // 低电平表示触碰到开限位 if (open_cnt 2) { // 连续两次检测为低才确认 limit_open 1; motor_stop(); motor_run(10, 0); // 反转 10 步卸力 } } else { open_cnt 0; limit_open 0; } // 关限位同理省略 }LIMIT_OPEN_PIN假设接了上拉电阻开关闭合时引脚被拉低所以在代码里检测低电平。连续两次确认是为了排除触点抖动——在主循环里这个函数跑得很频繁两次检测之间只有几百微秒对机械开关来说还不够完成一次完整的抖动周期但已经足以过滤大部分毛刺。卸力那 10 步执行完后要清一下方向标志防止下次启动时误以为还在运行中。4.3 系统状态机自动、手动、停止三态切换窗帘系统的控制逻辑如果全写在主循环的 if-else 里到最后一定是一坨乱麻。我建议画一个简单的状态机停止态IDLE、自动运行态AUTO_RUN、手动运行态MANUAL_RUN。停止态是默认状态收到指令后根据来源进入对应运行态运行态执行完或碰到限位再回到停止态。手动指令在任何状态下都优先打断自动运行这就是第 2 章说的权限设计。typedef enum { IDLE, AUTO_RUN, MANUAL_RUN } SysState; SysState cur_state IDLE; void system_tick(void) { switch (cur_state) { case IDLE: if (key_manual_pressed()) { cur_state MANUAL_RUN; motor_start(); } else if (light_level_changed() auto_mode_enabled()) { cur_state AUTO_RUN; motor_start(); } break; case AUTO_RUN: if (key_manual_pressed() || limit_reached() || motor_timeout()) { cur_state IDLE; motor_stop(); } break; case MANUAL_RUN: if (key_stop_pressed() || limit_reached()) { cur_state IDLE; motor_stop(); } break; } }状态机的好处是每个状态之间的迁移条件非常明确出问题时只需要看当前停在哪个状态、哪个条件没满足。motor_timeout()是一个软保护如果电机运行时间超过了T_full的 1.2 倍还没触发限位认为轨道卡死或限位开关损坏强制停机。这个超时判断很重要很多毕设作品在演示时出问题都是因为轨道上有个小坎卡住了窗帘电机一直转最后把齿轮打坏了。4.4 定时开合控制用定时器代替延时函数自动窗帘的另一个标配是“早上 7 点自动开晚上 18 点自动关”。51 单片机没有 RTC 模块常见做法有两种外接 DS1302 时钟芯片或者用定时器累加模拟时钟。DS1302 更精确还能掉电走时但多一个器件就多一个排错点。如果是演示用我倾向于用定时器累加定时器 0 配置为 50 毫秒中断每 20 次中断为 1 秒再累加得到分、小时和设定的时间比较即可。代码逻辑如下// 定时器 0 初始化12MHz 晶振50ms 中断 void timer0_init(void) { TMOD 0xF0; TMOD | 0x01; // 定时器 0 工作在方式 116 位定时 TH0 0x4C; // 50ms 初值65536 - 50000 * 12 / 12 TL0 0x00; ET0 1; EA 1; TR0 1; } unsigned int second_cnt 0; void timer0_isr(void) interrupt 1 { TH0 0x4C; // 重装初值 TL0 0x00; if (second_cnt 20) { // 50ms * 20 1 秒 second_cnt 0; virtual_clock_second; if (virtual_clock_second 60) { virtual_clock_second 0; virtual_clock_minute; // 分钟进位时检查是否到设定开合时间 check_schedule(); } } }初值0x4C00的计算过程12MHz 晶振机器周期是 12 个时钟周期即 1 微秒。50 毫秒需要计数 50000 次16 位定时器从 0 开始到 65535 溢出所以初值是 65536 - 50000 15536换算成十六进制就是 0x3CB0但上面代码写了 0x4C00这是因为 15536 的十六进制是 0x3CB0写 0x4C00 是为了凑 50ms 在 11.0592MHz 晶振下的值。这里要提醒一个典型坑如果用 11.0592MHz 晶振串口通信常用机器周期约 1.085 微秒同样的初值计时会偏长。所以做定时器计时前第一件事是确认晶振频率然后按初值 65536 - 定时时长 / 机器周期重新计算不要照抄网上的值。check_schedule()只在分钟进位时调用一天最多调用 1440 次不会挤占主循环时间。5. 调试手段、异常保护与现场标定技巧5.1 用串口打印和 VOFA 上位机观察内部状态自动窗帘系统的调试最痛苦的是“看不见内部状态”光照阈值设多少合适电机走多少步到位这些如果不打出来只能靠猜。我习惯的做法是UART 串口接一个 USB-TTL 模块定时把系统状态打成一行文本发到电脑。串口初始化代码在 51 上很成熟重点在于打印内容的设计。至少要包括当前状态机状态、光敏 ADC 原始值、滤波值、基准值、电机当前方向和累计步数、左右限位状态、最近一次停机原因。如果你用的是 STM32 平台强烈推荐 VOFA 上位机。它支持串口协议直接绘图你只需要按它的 JustFloat 协议发送三个 float 类型数据就能实时看到光照曲线和电机步数的波形。比如发送开度百分比、光照 ADC 值、电机转速三个量屏幕上能直观看到开度变化和光照变化是否同步比看串口助手的滚动文字高效得多。调试时用上位机答辩演示时却要让评委看到实物效果——所以串口打印是开发期工具正式演示时建议把打印关掉避免 UART 中断抢占电机控制的定时器时隙。我踩过这个坑开着串口打印时电机运行正常一关掉打印电机反而丢步排查了一下午才发现是波特率 9600 时串口中断频繁打断了步进电机的延时函数。5.2 看门狗与掉电恢复毕设演示翻车的高发区毕设演示时最尴尬的场面是系统跑着跑着程序跑飞了按复位键也没反应。看门狗定时器WDT就是干这个的。STC 单片机内置看门狗只需要在初始化时设置溢出时间然后在主循环里定期喂狗。但看门狗有个坑如果你在延时函数里等待电机走完几百步主循环长时间卡住看门狗会误判死机并复位系统。所以喂狗指令必须放在不会长时间阻塞的位置比如放在主循环开头而且电机延时要改用定时器方式不能用空循环延时。掉电恢复是另一个容易被忽视的问题。窗帘运行到一半突然断电重新上电后系统不知道窗帘当前是开还是关如果这时光敏传感器判断“很亮”要关窗帘电机会向关的方向走但窗帘可能已经到头了限位开关此时处于未触发状态因为皮带已经松了电机就会一直空转。常见解决方法是每次上电后先执行一次“寻找原点”程序以开限位为原点电机先向关的方向走几秒碰到关限位后记录当前位置为 0%再走到 100% 算出总步数。这个过程在答辩前一定要演示一遍给评委看它能体现你对系统异常状态的思考深度。5.3 光照阈值标定的三步法最后一个技巧是光照阈值的现场标定方法。不要用串口去改代码里的宏定义再重新编译下载那样太慢了。我建议在系统中实现一个标定模式长按某个按键 3 秒进入标定状态此时窗帘会自动运行到全开位置记录当前光照 ADC 值为light_cal_open再运行到全关位置记录light_cal_close然后自动计算迟滞区间unsigned int light_cal_open 0, light_cal_close 0; unsigned int threshold_high, threshold_low; void calibration_mode(void) { // 第一步全开位置的光照值 motor_run_to_open_limit(); delay_ms(500); light_cal_open light_avg; // 第二步全关位置的光照值 motor_run_to_close_limit(); delay_ms(500); light_cal_close light_avg; // 第三步计算迟滞阈值中间值 ± 死区 threshold_high (light_cal_open light_cal_close) / 2 DEAD_ZONE; threshold_low (light_cal_open light_cal_close) / 2 - DEAD_ZONE; }DEAD_ZONE建议取 20 到 30 之间。如果light_cal_open和light_cal_close的差值太小小于 50说明现场环境光照变化不大需要检查光敏电阻位置是否被窗帘遮住或者打光方向有问题。标定结束后把三个值存到单片机内部 EEPROMSTC 的片子内部有下次上电直接从 EEPROM 读取不用重新标定。这样整个系统的参数调整流程就是进入标定模式、走一遍全行程、自动存参数、重启生效整个过程两分钟搞定。调试时如果发现窗帘在阴雨天乱开关先重新标定一次八成是阈值基准漂了而不是代码逻辑出了问题。本文还有配套的精品资源点击获取