ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

51单片机8个流水灯程序:从裸IO到定时器、PWM与74HC595

51单片机8个流水灯程序:从裸IO到定时器、PWM与74HC595 简介面向单片机课程设计与51系列入门学习者这份文档围绕经典题目“8个流水灯”给出可参考的C51完整代码与注释解析。程序以REG52.H、INTRINS.H为头文件把P1口定义为LED输出端用table1数组存放8种亮灭模式在主循环中查表输出并配合软件延时形成流动灯光效果。延时部分由_nop_()逐级嵌套实现5微秒、50微秒、100微秒到毫秒级控制便于理解端口电平与时间的配合。资源为单个doc文件压缩包约40KB篇幅精简适合课堂实验与课程设计报告参考。读者可掌握I/O端口控制、查表法、循环结构与软件延时等基础并能在原框架上增加按键检测改成多种花样流水灯。目前已有331人学习下载。1. 8个流水灯程序到底是哪8个课程设计验收现场最怕老师指着屏幕问一句这8个流水灯程序除了快慢不一样到底差在哪。很多人交上去的文档里8 段代码只是把延时参数改了改核心还是同一个 P1 口移位相当于一份代码抄了八遍。真正能撑起一份单片机课程设计的 8 个流水灯程序应该是一条从裸 IO 到定时器中断、从阻塞延时到状态机、从单端口驱动到串行扩展的难度阶梯循环左移、数组查表、双向往返、累积拖尾、定时器中断驱动、软件 PWM 呼吸、按键切换状态机、74HC595 串行扩展。这 8 个程序覆盖了 51 单片机入门阶段几乎所有必考知识点做单片机毕业设计或者刷蓝桥杯单片机基础题的拿这套序列当训练清单比零散抄代码有效得多。下面按“先跑通、再加结构、最后上中断和状态机”的顺序往下走。2. 51单片机流水灯的硬件底座与 Keil 工程最小跑通路径写代码之前先把三件事定死LED 是共阳还是共阴、限流电阻取多大、程序怎么烧进去。这三件事任何一件错了后面 8 个程序全是白写所以先把硬件和工具链对齐。2.1 共阳还是共阴LED 接法直接决定你的码表共阳极接法是把 LED 阳极统一接 VCC阴极接到单片机 IO输出低电平点亮所以第一颗灯的初始码是0xFE二进制 1111 1110最低位为 0。共阴极正好相反阳极接 IO、阴极接地高电平点亮初始码是0x01。开发板上绝大多数是共阳但自己焊的洞洞板经常顺手接成共阴移植别人代码时就会出现“全亮变全灭”的经典事故。稳妥做法是在文件头部把极性抽成宏后面所有程序都以宏为准切换硬件只改一行#include reg52.h #include intrins.h #define LED_PORT P1 /* LED 挂在哪组 IO */ #define LED_ON 0 /* 共阳极写 0共阴极改成 1 */ #define LED_OFF 1 /* 与 LED_ON 相反 */ /* 起始状态只点亮最低位那颗 LED */ #if LED_ON 0 #define LED_FIRST 0xFE #else #define LED_FIRST 0x01 #endif参数说明LED_ON的取值不是随便写的它对应端口的实际输出电平共阳极必须给 0 才能形成灌电流回路LED_FIRST用预处理分支生成避免运行时判断编译后与手写常量等价不占额外空间。提示如果板子上是 8 个独立 LED 接到了不同端口这套码表思路要整体改成位操作别硬套字节移位。2.2 最小系统里的限流电阻、晶振与 P0 口上拉限流电阻按欧姆定律估R (VCC - VF) / IVF 是 LED 正向压降红黄光约 2.0V蓝绿白光约 3.0V 到 3.2V。下面是几种常见组合供电LED 类型目标电流限流电阻说明5V红色 2.0V5mA560Ω亮度够端口压力小5V红色 2.0V10mA300Ω开发板常见取值5V蓝白 3.2V5mA360Ω压降大注意亮度一致性3.3V红色 2.0V5mA260Ω低压系统别用 5V 电阻晶振和复位电路是“程序烧进去了但不跑”的头号嫌疑常见 12MHz 或 11.0592MHz 晶振两端各挂 20pF 到 30pF 电容复位脚经 10uF 电容接 VCC、10kΩ 电阻接地。用 12MHz 是为了机器周期好算1 个机器周期 1us用 11.0592MHz 是为了串口波特率能整除做第 8 个程序时必须用后者。P0 口是开漏结构做普通 IO 驱动 LED 时必须外接 4.7kΩ 到 10kΩ 上拉电阻否则推不出高电平。很多人把 LED 接到 P0 直接写P0 0xFE结果灯要么全不亮要么微亮就是在这一步翻车。2.3 从 Keil 工程到烧录上手的最小跑通路径工具链固定的操作顺序如下每一步都不能省步骤操作关键点1Keil 新建 uVision Project器件选 AT89C52 或 STC89C52 一类 8051 内核2新建 main.c 并加入 Source Group文件名不要带中文和空格3Options for Target → Output勾选 Create HEX File否则生成不了烧录文件4BuildF7输出窗口 0 Error 才算过5串口下载工具选择型号和 COM 口点“下载”后再给单片机上电冷启动握手第 5 步的冷启动是关键STC 系列单片机靠上电时的串口握手进入下载模式先点下载再上电顺序反了会一直提示“正在检测目标单片机”。下面这段代码是整套 8 个程序共用的最小可运行版本先把它烧进去确认硬件没问题#include reg52.h #include intrins.h void delay_ms(unsigned int ms) { unsigned int i, j; for (i 0; i ms; i) for (j 0; j 120; j); /* 约 1ms 12MHz需按实测校准 */ } void main(void) { unsigned char led LED_FIRST; LED_PORT 0xFF; /* 上电全灭避免一片全亮 */ while (1) { LED_PORT led; delay_ms(200); /* 200ms 走一位 */ led _crol_(led, 1); /* 8 位循环左移位 7 补回位 0 */ } }逻辑说明_crol_是intrins.h提供的 8 位循环移位内联函数_crol_(0xFE, 1)得到0xFD移 8 次正好回到0xFE不会出现“移出去就回不来”的断流。delay_ms是纯软件空循环Keil 在优化等级较高时可能把内层循环裁掉如果发现灯闪得飞快把i、j声明成volatile或者降低优化等级再编译。3. 前4个程序用软件延时和移位把流水灯跑起来前 4 个程序都不依赖任何外设只靠 P1 口和延时函数是验证硬件、熟悉 C51 语法的基础层。它们之间的区别不在效果花哨程度而在“状态怎么组织和推进”。3.1 程序1位运算循环左移最快的上手版本void mode1_shift(void) { unsigned char led LED_FIRST; while (1) { LED_PORT led; delay_ms(200); led _crol_(led, 1); /* 循环左移方向固定 */ } }这个版本代码最短但它把“显示”和“时间”焊死在一起主循环里除了等延时什么都干不了按键按下去也没反应。它的价值是验证码表和极性不适合当最终提交版本。3.2 程序2数组查表法把任意花样变成一张表查表法的思路是把每个时刻要输出的字节按顺序写进常量数组用下标推进。好处是花样随便改不用动移位逻辑/* 共阳极低电平点亮code 关键字把表放到 Flash 里省 RAM */ unsigned char code pattern[8] { 0xFE, 0xFD, 0xFB, 0xF7, 0xEF, 0xDF, 0xBF, 0x7F }; void mode2_lookup(void) { unsigned char i 0; while (1) { LED_PORT pattern[i]; delay_ms(200); i (i 1) 0x07; /* 0~7 循环比 if 判断省周期 */ } }参数说明code是 C51 特有的存储类型修饰符把数组放进程序存储器8 字节的小表影响不大但换成几十上百帧的动画表时能省下宝贵的 128 字节内部 RAM。i (i 1) 0x07等价于对 8 取模但编译器会生成ANL指令在定时器中断里调用时更省时间。需要“亮两个灭一个”之类非均匀花样直接把表改成对应字节序列即可逻辑代码一行不动这是查表法比移位法更好维护的地方。3.3 程序3双向往返用一个方向标志控制推进方向往返流水的关键是判断什么时候掉头最省资源的方式是记录当前灯的位置而不是记录方向void mode3_pingpong(void) { unsigned char i 0; unsigned char dir 0; /* 0 递增1 递减 */ while (1) { LED_PORT pattern[i]; delay_ms(150); if (dir 0) { if (i 7) dir 1; /* 到最右端掉头 */ } else { if (i 0) dir 0; /* 到最左端掉头 */ else i--; } } }容易写错的地方是i递减前先判i 0如果写成if (--i 0)会因为无符号数减到 0 后再减变成 255 而直接越界。用无符号 char 做下标时这类边界判断必须前置。注意如果对首尾各亮一次的视觉效果不满意改成 6和 1就能让灯在最边上多停一拍靠参数微调比改逻辑更省事。3.4 程序4累积拖尾用移位实现“点亮叠加、熄灭削减”拖尾效果是让亮灯的个数逐渐增加到全亮再逐渐减少到全灭实现只需要一条移位加补位void mode4_tail(void) { unsigned char i; unsigned char led LED_FIRST; while (1) { /* 阶段一低位不断补 0亮的灯越来越多 */ for (i 0; i 7; i) { LED_PORT led; delay_ms(120); led 1; /* 左移低位自动补 0 */ } /* 此时 led 已接近 0x00八灯全亮 */ /* 阶段二高位不断补 1亮的灯越来越少 */ for (i 0; i 8; i) { LED_PORT led; delay_ms(120); led (led 1) | 0x80; /* 右移高位补 1 熄灭该位 */ } } }参数说明led 1在共阳极下低位补 0 意味着“又点亮一颗灯”所以第一段循环每走一次亮灯数加一第二段| 0x80是强制把最高位熄灭保证右移不会引入 1。两段循环次数分别是 7 和 8差一次是因为0xFE起步时已经点亮一颗这个偏移量是最常被数错的地方数错了就会看到“全亮之后卡一下才继续”。4. 后4个程序定时器中断、PWM、状态机与串行扩展前 4 个程序的通病是把 CPU 时间全耗在空循环上后 4 个程序要解决的问题是怎么在流水灯跑的同时还能响应按键、接收串口命令、驱动更多 LED。4.1 程序5定时器0中断驱动的流水灯用定时器产生时间基准主循环彻底解放出来unsigned char tick 0; unsigned char led LED_FIRST; void Timer0_Init(void) { TMOD 0xF0; TMOD | 0x01; /* 定时器0方式116位定时 */ TH0 0x3C; /* 50ms 12MHz 的高8位 */ TL0 0xB0; /* 低8位65536-5000015536 */ ET0 1; /* 允许定时器0中断 */ EA 1; /* 开总中断 */ TR0 1; /* 启动计时 */ } void Timer0_ISR(void) interrupt 1 { TH0 0x3C; /* 方式1不自动重装必须手动补 */ TL0 0xB0; if (tick 4) { /* 4×50ms 200ms 移动一位 */ tick 0; LED_PORT led; led _crol_(led, 1); } }逻辑说明方式 1 是 16 位定时溢出后计数器归零所以中断服务函数里必须重新写入初值漏写这一句现象是“第一次延时正常之后快到看不清”。interrupt 1是 C51 约定的中断向量号定时器 0 固定为 1。参数说明TMOD 0xF0先清掉定时器 0 的低 4 位再用| 0x01设置方式 1这种“先与后或”的写法比直接TMOD 0x01更安全因为后者会把定时器 1 的配置清掉——等做第 8 个程序的串口时定时器 1 正被波特率发生器占用直接赋值会让串口莫名其妙失联。4.2 程序6软件 PWM 做呼吸式流水灯呼吸效果本质是占空比缓慢变化用同一个定时器把中断周期压到 100us在主循环里改占空比unsigned char pwm_duty 0; /* 0~100单位 % */ unsigned char pwm_cnt 0; unsigned char breath_dir 1; /* 1 变亮0 变暗 */ unsigned char slow_tick 0; void Timer0_PWM_ISR(void) interrupt 1 { TH0 0xFF; /* 100us 12MHz65536-100 */ TL0 0x9C; if (pwm_cnt 100) pwm_cnt 0; LED_PORT (pwm_cnt pwm_duty) ? 0xFE : 0xFF; /* 只调 P1.0 亮度 */ if (slow_tick 20) { /* 每 2ms 调一次占空比 */ slow_tick 0; if (breath_dir) { if (pwm_duty 100) breath_dir 0; } else { if (pwm_duty 0) pwm_duty--; else breath_dir 1; } } }参数说明PWM 频率等于 1/100us 即 10kHz远高于人眼闪烁阈值看到的是连续亮度而不是闪烁slow_tick的分频值决定呼吸速度从 20 改到 10 就是快一倍改到 50 就慢下来。把LED_PORT换成 8 位掩码就能做出整排灯一起呼吸但要注意 8 个灯同时以 100% 占空比点亮时总电流是单个灯的 8 倍做整排呼吸时最好把占空比上限压到 60% 左右。4.3 程序7按键切换模式的状态机到这一步8 个模式可以塞进一个状态机按键只负责改状态号sbit KEY P3^2; unsigned char mode 0; unsigned char key_lock 0; void key_scan(void) /* 主循环里每 10ms 调一次 */ { if (KEY 0) { if (key_lock 0) { key_lock 1; /* 锁住防止长按连跳 */ mode (mode 1) % 8; } } else { key_lock 0; /* 松开才解锁 */ } } void main(void) { Timer0_Init(); /* 时间基准交给中断 */ while (1) { key_scan(); switch (mode) { case 0: /* 调用程序1的显示逻辑 */ break; case 1: /* 调用程序2的显示逻辑 */ break; default: break; } } }逻辑说明key_lock实现的是“只认按下沿”机械按键抖动通常在 10ms 内结束配合 10ms 的扫描周期天然起到消抖作用不需要额外delay_ms(10)。如果用外部中断接按键就必须在中断里加硬件消抖电路或者在 ISR 里做定时复核否则一次按下去会跳好几个模式。注意switch分支里的显示逻辑不能再用阻塞式延时否则按键响应会被拖慢这是从程序 1 迁移到程序 7 时最容易偷懒出错的地方。4.4 程序874HC595 串行扩展与串口指令控制IO 不够或者要把灯板挪远的时候74HC595 是最常见的方案3 根线换 8 路输出sbit DS P3^4; /* 串行数据 */ sbit SHCP P3^5; /* 移位时钟 */ sbit STCP P3^6; /* 锁存时钟 */ void hc595_send(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { DS (dat 0x80) ? 1 : 0; /* 高位先出 */ dat 1; SHCP 0; SHCP 1; /* 上升沿移位 */ } STCP 0; STCP 1; /* 上升沿锁存到输出 */ }参数说明移位必须在SHCP上升沿完成所以代码里先拉低再拉高STCP一定要在 8 位全部移完之后再触发提前拉高会出现中间态一闪而过的“鬼影”。595 的输出电流能力有限驱动 8 个 LED 时每路仍要串限流电阻超过 8 路就级联把前一级的Q7接到下一级的DS。加上串口指令后上位机发1到8就能切模式波特率 9600 的初始化如下void UART_Init(void) /* 9600bps 11.0592MHz */ { TMOD 0x0F; TMOD | 0x20; /* 定时器1方式28位自动重装 */ TH1 0xFD; /* 256 - 11059200/(384×9600) 253 */ TL1 0xFD; SCON 0x50; /* 方式18位UART允许接收 */ PCON 0x7F; /* SMOD0波特率不加倍 */ TR1 1; ES 1; EA 1; } void UART_ISR(void) interrupt 4 { if (RI) { RI 0; /* 标志位必须手动清零 */ mode (SBUF - 1) % 8; /* 收到 1~8 切换模式 */ } if (TI) TI 0; }逻辑说明方式 2 的定时器 1 是 8 位自动重装溢出后自动从TH1重载到TL1所以中断里不用补初值这也是串口和定时器 0 写法不一致的原因。RI和TI都必须软件清零漏清会一直重复进入中断。5. 8个流水灯程序联调排错与定时器参数整定代码写完了跑不出效果问题九成集中在三处极性接反、中断初值算错、编译选项不对。按顺序排查比反复改代码快得多。5.1 灯不亮、全亮、乱亮的排查顺序现象大概率原因快速验证方法8 灯全亮不灭共阳共阴判断反了改LED_ON宏重新编译全灭、程序像没跑晶振未起振或复位脚悬空万用表测晶振两端直流电压约 1/2 VCC接 P0 口时全灭或微亮P0 开漏未加上拉加 4.7kΩ 上拉到 VCC只亮一两颗后卡死移位越界或表下标越界在循环里打印/观察led变量闪得飞快、延时无效编译器把空循环优化掉了循环变量加volatile亮灭顺序对但节奏乱定时器中断未重装初值检查 ISR 头部是否重写 TH0/TL0一按按键就复位按键未加下拉、电源跌落按键并 0.1uF 电容检查供电排查顺序建议从供电和晶振开始再查端口结构最后才怀疑代码。前两项用万用表五分钟能确认代码问题往往要花半小时。5.2 定时器初值与串口波特率对照表12MHz 晶振、定时器 0 方式 116 位常用初值定时时长计数差值TH0TL0典型用途1ms10000xFC0x18软 PWM 时间片2ms20000xF80x30数码管动态扫描5ms50000xEC0x78按键扫描节拍10ms100000xD80xF0通用节拍20ms200000xB10xE0舵机基准50ms500000x3C0xB0流水灯分频基准计算方法是初值 65536 - 定时时长(us)12MHz 下一个机器周期正好 1us所以数值可以直接相减。换成 11.0592MHz 时机器周期约为 1.085us同样的初值会偏大约 8%做精确计时需要按比例重算。串口波特率必须用 11.0592MHz 晶振才有整数解定时器 1 方式 2波特率SMODTH1/TL1误差120000xE80240000xF40480000xFA0960000xFD01920010xFD05760010xFF0SMOD是PCON的最高位置 1 表示波特率加倍。用 12MHz 晶振跑 9600 会出现约 8.5% 的误差短帧可能勉强能通长帧必然丢字节这是“串口调试助手偶尔收得到偶尔收不到”的根因。5.3 Keil 编译警告与烧录失败的对应处理几个高频报错warning C206: missing function-prototype是因为函数在调用之后才定义把函数声明提到文件顶部即可C51 不做隐式声明推导。warning C318: cant open file xxx.h说明头文件路径没加在 Options for Target → C51 → Include Paths 里补上目录。_crol_未定义标识符是漏了#include intrins.h这个头文件不包含在任何默认包含链里。编译通过了但烧录工具提示“正在检测目标单片机”按这个顺序查COM 口是否被 Keil 或串口助手占用、型号是否选对STC89C52 和 STC89C52RC 的下载协议不同、是否执行了断电再上电的冷启动。另外 Options for Target → Output 里的 Create HEX File 没勾选时编译成功也不会生成 hex烧录工具自然找不到文件。6. 答辩向的验证方法与可复用技巧代码能跑不等于能答辩老师通常会追问“你怎么证明这个延时是准的”“这个 PWM 占空比到底是多少”。下面几个方法能把口头描述变成可测量的事实。6.1 用示波器和万用表量化你的流水灯最直接的是把示波器探头搭在 P1.0 上测出的方波周期应该等于“单步延时 × 8”。程序 1 里延时 200ms测得周期 1.6s 才算对如果测出来是 0.8s说明延时函数比标称值快了一倍回头改内层循环次数。没有示波器就用万用表直流电压档测 LED 两端普通流水的平均电压约为“点亮时间占比 × 压降”而 PWM 呼吸灯在占空比 50% 时读数约为满亮时的一半这个读数可以直接写进答辩 PPT 当证据。6.2 延时函数的实测校准流程软件延时不准是常态校准方法很简单在主循环里翻转一个空闲 IO用示波器或逻辑分析仪测高电平持续时间与目标值比对后按比例调整循环次数。void delay_calib(void) /* 校准用翻转 P2.0 产生标准方波 */ { while (1) { P2 ^ 0x01; delay_ms(100); /* 理论高电平 100ms实测后修正系数 */ } }参数说明P2 ^ 0x01每次都翻转最低位得到的方波周期是2 × delay_ms。测出来 180ms 就说明实际延时是 90ms把delay_ms内层循环次数乘以 100/90 即可。做完校准再切回流水灯程序延时精度能从 ±30% 收敛到 ±5% 以内。6.3 把 8 个模式写成可扩展的表驱动结构最后给一个让代码显得“有设计感”的小技巧不要把 8 段逻辑散在各处而是统一成模式函数加一张调度表typedef void (*mode_fn)(void); void mode_shift(void); void mode_lookup(void); void mode_pingpong(void); void mode_tail(void); /* 其余模式同理声明 */ /* 表放在代码段下标即模式号新增模式只加一行 */ code mode_fn mode_table[8] { mode_shift, mode_lookup, mode_pingpong, mode_tail, /* ... */ }; void dispatch(void) { static unsigned char last 0xFF; if (mode ! last) { /* 只在模式变化时切换避免重复初始化 */ last mode; } mode_table[mode](); /* 调用当前模式的单步执行体 */ }这样做的实际收益是把“模式切换”和“模式实现”解耦答辩时被要求现场加一个新模式只需要写一个新函数并占一个表项不用动switch结构。需要注意每个模式函数不能写成死循环必须改成“执行一步就返回”的形式把节奏交给定时器中断控制这也是从程序 1 演进到程序 8 最本质的一次结构升级。本文还有配套的精品资源点击获取
返回列表