☰
51单片机+PROTEUS仿真流水灯:C51工程化实现与联调
2026/10/30 5:41:52 网站建设 项目流程

简介:面向51单片机初学者与课程实验的流水灯仿真练习包,围绕C51编程、Keil工程构建与Proteus仿真验证展开,帮助读者完成从代码编写到电路现象观察的完整练习。压缩包共18个文件,约985KB,包含C源程序、HEX可执行文件、Keil工程及启动代码、编译链接中间文件,以及Proteus工程、位图与MP4演示素材,便于直接打开工程复现和对照学习。已有5389人学习下载,说明该案例在入门教学与自学中具有一定参考价值。资源实现单灯轮流点亮、从左到右依次累加点亮、间隔1s延时、八灯全亮后蜂鸣器提示再熄灭循环等多花样效果,可用于理解GPIO输出、循环与延时、蜂鸣器控制及软硬件联调思路,适合作为课堂演示、实验报告和二次改编的基础素材。

1. 从一块焊废的板子说起

我见过太多人做流水灯,第一步是把八个 LED 焊到洞洞板上,通电之后发现亮的顺序和脑子里想的不一样,拆了重焊,一晚上就过去了。51单片机 + PROTEUS仿真这条路的顺序恰好相反:先在电脑里把电路和程序跑通,确认时序、电平、接线都对,再决定要不要动手焊。它要解决的不是"灯会不会亮",而是"在没有任何硬件损耗的前提下,把从寄存器配置到外设驱动的完整链路走一遍"。适合的人群很明确:正在做单片机课程设计、需要交一份能演示能答辩的作品的同学,以及刚接手 8051 老平台、想快速把手感找回来的工程师。八个灯只是表象,底下练的是 I/O 口操作、延时精度、定时器中断和工程分层这四件事。把这四件事在仿真里做扎实,后面接数码管、矩阵键盘、串口都不会太慌。

2. 需求与选型:八个灯背后到底要交付什么

2.1 需求边界:不止是"从左流到右"

如果只写一个P1 = 0xFE; delay(); P1 = 0xFD; delay();的循环,那这个项目大概二十分钟就结束了,也交不出一份像样的方案。我在拆需求的时候习惯先把"能看到的效果"列清楚,再倒推要写哪些代码。

这个项目合格的需求边界通常是这样的:

需求项具体要求对应技术点
基础流水8 路 LED 单灯自左向右循环位移运算、延时函数
双向流水左移到头再右移回来,不出现跳变循环移位、边界判断
多花样至少 3 组花样可切换code 区查表、状态机
速度可调按键切换快/慢两档或连续调速外部中断或按键扫描
时序稳定每步间隔误差不超过 ±5%定时器中断

把这张表列出来的好处是,后面写的每一段代码都能对应到具体某一行需求,答辩的时候被问到"你这个功能为什么这么设计",你有话可说。很多同学栽在"做完了但说不清楚做了什么",根源就在需求阶段没有把边界写下来。

还有一条容易被忽略的需求:可观测性。仿真环境里没有示波器探头夹在引脚上,你得靠 PROTEUS 自带的探针和虚拟示波器去确认 P1 口每一位的实际电平变化。这意味着代码里要预留可以单步观察的中间状态,而不是把八个灯的操作全部压缩在一行里。

2.2 为什么是 C51,而不是汇编或 32 位平台

选型这件事,我一般的判断标准是看"这个项目要证明什么能力"。

用汇编写流水灯,能证明你懂指令周期和寄存器,但代码量会膨胀得很难维护,八个花样写完,光是查表寻址那几行就能让评审看晕。用 STM32 或者别的 32 位平台,硬件资源是够了,可流水灯这个题目本身撑不起那么重的技术栈,反而会显得"杀鸡用牛刀",答辩时被追问"为什么不用 51"会很尴尬。

C51 的位置在中间:它有_crol_、_cror_这类内联函数直接映射到循环移位指令,也有code关键字把常量塞进 Flash 而不是那可怜巴巴的 128 字节 RAM,还有interrupt n这种语法糖把中断向量号写清楚。这些特性刚好覆盖流水灯项目需要的全部能力,同时又不会把注意力从"外设驱动"这件事上带走。

再补一句实在话:51 的资料密度是其他平台比不了的。任何一个寄存器的作用、任何一条时序的细节,你几乎都能在半小时内找到三份以上互相印证的说明。对第一次做完整项目的人来说,降低不确定性比追求先进性重要得多。

2.3 电气参数先定死:共阳接法、限流电阻、12MHz

动手画原理图之前,有几个参数必须先拍板,否则后面代码里的电平逻辑全是猜的。

接法选共阳。八个 LED 的阳极统一接 VCC,阴极各串一个限流电阻接到 P1.0~P1.7,程序输出低电平点亮。这么选的理由很实际:51 的 I/O 口灌电流能力(约 10~20mA)明显强于拉电流能力(约 60μA 量级),共阳接法让电流从 VCC 流进引脚,是这颗芯片最舒服的工作方式。如果接成共阴,灯会暗得几乎看不见,然后你就开始怀疑是不是代码写错了,白白浪费两小时。

限流电阻取 330Ω 左右。按 5V 供电、LED 正向压降 2V、目标电流 8mA 估算,R = (5-2)/0.008 ≈ 375Ω,取标称值 330Ω 或 470Ω 都在合理区间。仿真里电阻取值不影响"灯亮不亮"的判定,但影响你养成的习惯——真焊板子的时候,省掉这颗电阻,LED 和单片机 IO 口会一起报废。

晶振定 12MHz。传统 12T 模式的 8051 在这个频率下,一个机器周期正好 1μs,延时函数的循环次数可以直接按"多少次循环约等于多少微秒"来估。如果换成 11.0592MHz,串口波特率会更好算,但软件延时的换算会变得很别扭,对流水灯这种纯 I/O 项目没有必要。

提示:这三个参数一旦写进方案文档,代码、原理图、验收标准就得全部对齐。中途改晶振频率而不改延时参数,是后面最常见的翻车原因之一。

3. 总体设计:三层结构与四个模块的职责划分

3.1 分层思路:把"变"和"不变"分开

流水灯项目小,但小项目更容易写成"一坨"。我见过不少课程设计的main.c从头到尾两百行,延时、移位、按键判断、花样切换全糊在一起,改一个灯的顺序要翻半天。所以在动手写第一行代码之前,先把结构立起来。

我一般按三层来切:

硬件抽象层负责和寄存器打交道,包括端口方向、初值设定、定时器配置。这一层的特点是"换个引脚就要改",所以单独放。

驱动层负责"让灯呈现出某种状态",比如点亮第 n 位、输出一个字节、执行一次移位。它不关心花样是什么,只关心"给一个字节,端口就输出这个字节"。

应用层负责决定"下一步该显示什么",也就是花样序列和切换节奏。这一层改得最频繁,所以要和下面两层彻底隔开。

这么分的好处很直接:调试的时候,如果灯完全不亮,你去查硬件抽象层;如果灯亮但顺序错,你去查应用层的花样表;如果灯闪得忽快忽慢,你去查驱动层的延时或定时器。定位范围一下子缩小到一二十行。

3.2 文件划分与头文件约定

落到具体文件上,通常是这么几个:

/* led.h —— 对外只暴露这三个接口,应用层不碰寄存器 */ #ifndef __LED_H__ #define __LED_H__ void led_init(void); /* 端口初始化,全部熄灭 */ void led_write(unsigned char dat); /* 输出一个字节,低电平点亮 */ void led_show(unsigned char index); /* 按花样表下标显示第 index 步 */ #endif

led.c里放具体实现,main.c只负责调度。这样拆的意义在于:假设验收时老师要求"把 LED 从 P1 挪到 P2",你只需要改led.c里的一行宏定义,其他文件一个字都不用动。同理,如果要求把流水灯扩展成"流水灯 + 数码管",新加一个seg.c就行,不会牵一发动全身。

头文件里我习惯加#ifndef / #define / #endif这套卫哨,虽然这个规模的项目几乎不会出现重复包含,但这是工程习惯的一部分,评审看到会加分。

3.3 花样状态表:把"长什么样"从代码里剥出来

这是整个设计里最值得说的一点。很多人写多花样是这么干的:

if (mode == 0) { /* 一段移位代码 */ } else if (mode == 1) { /* 另一段移位代码 */ } else if (mode == 2) { /* 再一段 */ }

每加一个花样就要加一个分支,代码越写越长,最后自己都数不清有几个else。

更稳的做法是把花样序列抽成一张表,代码只负责"按顺序取表里的下一个值":

/* pattern.h —— 花样表定义,放在 code 区,不占内部 RAM */ #ifndef __PATTERN_H__ #define __PATTERN_H__ #define PATTERN_LEN 24 extern code const unsigned char pattern[PATTERN_LEN]; #endif
/* pattern.c */ #include "pattern.h" /* 低电平点亮,所以取反后的值才是实际输出 */ code const unsigned char pattern[PATTERN_LEN] = { 0xFE, 0xFD, 0xFB, 0xF7, 0xEF, 0xDF, 0xBF, 0x7F, /* 单灯左移 8 步 */ 0x7F, 0xBF, 0xDF, 0xEF, 0xF7, 0xFB, 0xFD, 0xFE, /* 单灯右移 8 步 */ 0xFC, 0xF3, 0xCF, 0x3F, /* 双灯间隔 4 步 */ 0x00, 0xFF, 0x00, 0xFF /* 全亮全灭 4 步 */ };

这张表有 24 个元素,前 8 个是单灯从 P1.0 走到 P1.7,接着 8 个是走回来,然后 4 个双灯交替,最后 4 个全亮全灭。整个过程只用一个下标idx自增、到 24 归零,就能跑完所有花样,没有一行分支判断。要加新花样,改表加长度宏就行,主循环逻辑不动。

code这个关键字必须写。不加的话,pattern会被分配进内部 RAM,51 的内部 RAM 只有 128 字节,24 个元素加上栈和变量,很容易就溢出。溢出的表现非常玄学——花样跑着跑着开始乱跳,你盯着代码看半小时也看不出问题,因为它根本不是逻辑错,是内存踩了。

提示:判断有没有踩内存,最简单的办法是看 Keil 编译输出的data段大小,超过 128 字节就要警惕。

4. 核心实现:三种流水灯写法的取舍与代码落地

4.1 位移法:<<和_crol_的边界差异

最直观的写法是移位。先看一个能跑但有隐患的版本:

#include <reg52.h> /* 12MHz、12T 模式下约 1ms,实际值需按编译结果校准 */ void delay_ms(unsigned int ms) { unsigned int i, j; for (i = ms; i > 0; i--) for (j = 110; j > 0; j--) ; } void main(void) { unsigned char i; P1 = 0xFF; /* 共阳接法,先全部熄灭 */ while (1) { for (i = 0; i < 8; i++) { P1 = ~(1 << i); /* 1 左移 i 位取反,第 i 位输出低电平 */ delay_ms(200); /* 每步停 200ms */ } } }

这段代码的关键在~(1 << i)。i从 0 到 7,1 << i依次得到0x01、0x02、0x04……0x80,取反后就是0xFE、0xFD……0x7F,正好让低电平从 P1.0 挪到 P1.7。i到 8 之后for循环自然退出,重新从 0 开始,不需要额外处理边界。

为什么不用P1 = P1 << 1?因为普通左移会把移出去的那一位丢掉,补 0 进来。第一次0xFE << 1变成0xFC,第二次0xF8,跑完八个灯就全灭了,然后卡在那里一动不动——这就是很多人"灯只跑一遍"的真正原因。要循环移位就得用<intrins.h>里的_crol_:

#include <reg52.h> #include <intrins.h> /* _crol_ 循环左移内联函数 */ void delay_ms(unsigned int ms) { unsigned int i, j; for (i = ms; i > 0; i--) for (j = 110; j > 0; j--) ; } void main(void) { unsigned char dat = 0xFE; /* 初始只有最低位为 0 */ P1 = 0xFF; while (1) { P1 = dat; /* 输出当前状态 */ delay_ms(200); dat = _crol_(dat, 1); /* 循环左移一位,最高位补到最低位 */ } }

_crol_和<<的区别就在这个"补位"上:前者把移出的最高位补到最低位,后者补 0。流水灯要的是转圈,不是走到底就死。这个差异看着小,但它是区分"背过代码"和"想过原理"的分水岭,答辩时经常被问。

再说延时函数的参数。外层i是毫秒数,内层j = 110是经验值,表示内层空转 110 次大约耗时 1ms。这个数不是固定的:它跟晶振频率、编译器优化等级、变量类型都有关系。它只是个近似值,必须实测校准。

校准方法很朴素:把delay_ms(1000)接在一个空闲引脚上,用 PROTEUS 的虚拟示波器测高电平持续时间,看是偏长还是偏短,然后按比例调j。我做过几次,12MHz、默认优化等级下,实测值一般落在 100~130 之间。

还有一个血泪经验:内层循环变量j千万别写成unsigned char。char最大 255,如果你写成for (j = 300; j > 0; j--),300会被截断成44,延时变成原来的十几分之一,灯跑得飞快,而编译器一句警告都没有。用unsigned int,或者干脆把循环次数控制在 255 以内。

4.2 查表法:把 RAM 省下来,把花样交出去

上一节的位移法适合做单向流水,但花样一多就力不从心。查表法的思路是:把"第几步显示什么"预先算好存进表里,运行时只做查表和输出。

#include <reg52.h> #define PATTERN_LEN 24 code const unsigned char pattern[PATTERN_LEN] = { 0xFE, 0xFD, 0xFB, 0xF7, 0xEF, 0xDF, 0xBF, 0x7F, 0x7F, 0xBF, 0xDF, 0xEF, 0xF7, 0xFB, 0xFD, 0xFE, 0xFC, 0xF3, 0xCF, 0x3F, 0x00, 0xFF, 0x00, 0xFF }; void delay_ms(unsigned int ms) { unsigned int i, j; for (i = ms; i > 0; i--) for (j = 110; j > 0; j--) ; } void main(void) { unsigned char idx = 0; P1 = 0xFF; while (1) { P1 = pattern[idx]; /* 查表输出 */ delay_ms(150); idx++; if (idx >= PATTERN_LEN) /* 到末尾归零,形成循环 */ idx = 0; } }

三个要点。

第一,表用code const双保险。code让编译器把它放进 Flash,const让编译器知道这块内容不会被改,可以做进一步优化。两个都写,占用的是程序空间而不是那 128 字节 RAM,扩展花样到一百多个也不慌。

第二,下标归零的判断写成if (idx >= PATTERN_LEN)而不是if (idx == PATTERN_LEN)。因为idx是unsigned char,如果哪天表长被改成 256 的倍数或者下标被别的逻辑改坏,==可能永远不成立,程序就会越界读到 Flash 末尾的随机数据,表现是花样突然乱跳。用>=是留一层保险。

第三,查表法最大的价值是可配置性。验收时老师如果说"我要看一个跑马灯加速再减速的效果",你不用改程序结构,只需要重算表里的字节或者调整延时参数。这种"改数据不改逻辑"的设计,在真实项目里是基本要求。

如果希望花样切换更灵活,可以把表按段切开,加一个段起始和段长度的索引表,运行时按mode选择段,这就是个最小状态机了:

/* 段索引表:{起始下标, 段长度} */ code const unsigned char seg[3][2] = { {0, 8}, /* 单灯左移 */ {8, 8}, /* 单灯右移 */ {16, 8} /* 双灯与全亮全灭 */ };

mode由按键改,主循环里P1 = pattern[ seg[mode][0] + pos ],pos在seg[mode][1]范围内递增。加了这一层,按键切换花样就变成"改一个变量"的事。

4.3 定时器法:把延时从主循环里赶出去

前面两种写法的共同短板是:延时期间 CPU 什么也干不了。按键按下去没反应,因为程序正卡在for循环里数数。这在纯演示时看不出来,但一旦加上按键调速,问题立刻暴露——按键要按住小半秒才有反应。

解决办法是把节拍交给定时器。T0 每 50ms 中断一次,中断服务程序里累加计数,凑够四次数就切换一次灯的状态,主循环彻底解放:

#include <reg52.h> volatile unsigned char tick = 0; /* 50ms 计数 */ volatile unsigned char step = 0; /* 当前点亮第几位 */ volatile unsigned char speed = 4; /* 几步一换,即速度档位 */ /* T0 方式1,12MHz 下定时 50ms */ void timer0_init(void) { TMOD &= 0xF0; TMOD |= 0x01; /* 方式1:16 位定时器 */ TH0 = 0x3C; /* 65536 - 50000 = 15536 = 0x3CB0 */ TL0 = 0xB0; ET0 = 1; /* 开 T0 中断 */ EA = 1; /* 开总中断 */ TR0 = 1; /* 启动定时器 */ } void timer0_isr(void) interrupt 1 { TH0 = 0x3C; /* 重装初值,方式1 不会自动重装 */ TL0 = 0xB0; if (++tick >= speed) { tick = 0; P1 = ~(1 << step); /* 点亮第 step 位 */ step = (step + 1) & 0x07; /* 0~7 循环,与 0x07 保证不越界 */ } } void main(void) { P1 = 0xFF; timer0_init(); while (1) { /* 主循环留给按键扫描、串口、显示刷新 */ } }

几个参数值得逐个说清楚。

TH0 = 0x3C; TL0 = 0xB0;这两个值来自65536 - 50000 = 15536,十六进制就是0x3CB0。50000 是 50ms 对应的机器周期数,因为 12MHz、12T 下 1 个机器周期刚好 1μs。换成 11.0592MHz 晶振,这个数就得重新算,不然整个节拍会偏 8% 左右。

interrupt 1是定时器 0 的中断向量号,这个号是固定的,写错就进不了中断服务程序,表现是灯一动不动但程序没死机。51 的向量号:外部中断 0 是 0,定时器 0 是 1,外部中断 1 是 2,定时器 1 是 3,串口是 4。记不住就查手册,别猜。

step = (step + 1) & 0x07;这一行是越界保护。0x07是00000111,按位与之后结果永远落在 0~7 之间。相比if (step >= 8) step = 0;,它少一个分支,在中断里执行更短。这类小技巧在中断服务程序里是有意义的,因为中断里多待一微秒,主循环就少一微秒。

volatile修饰的三个变量不能省。中断里会改它们的值,主循环里可能会读,如果编译器不知道这一点,会把这些变量优化进寄存器,导致主循环读到的是旧值。这类问题在仿真里表现为"按键改了速度但灯不变",查起来极其折磨人。

speed这个变量的作用是调速:值越大,切换一步需要的 50ms 次数越多,灯走得越慢。把这个变量交给主循环里的按键去改,就实现了运行中调速,而且因为切换动作发生在中断里,按键响应不会被阻塞。这是定时器方案相对软件延时最实在的收益。

5. 仿真联调:PROTEUS 原理图与 HEX 加载的避坑清单

5.1 元件清单与最小连线

PROTEUS 里搭这个电路,用到的元件不多,但每一个都有讲究:

元件名(PROTEUS 库中关键字)参数数量作用
AT89C51默认1主控
CRYSTAL12MHz1晶振
CAP30pF2晶振负载电容
CAP-ELEC10μF1复位电容
RES10kΩ1复位电阻
RES330Ω8LED 限流
LED-GREEN默认8指示灯
RESPACK-810kΩ1P0 口上拉(若用到 P0)

连线要点:晶振和两个 30pF 电容接到 XTAL1、XTAL2,电容另一端接地;复位电路是 10μF 电容正极接 VCC、负极接 RST,同时 RST 经 10kΩ 电阻接地;八个 LED 的阳极统一接 VCC,阴极各串 330Ω 电阻后接到 P1.0~P1.7。

如果为了扩展把灯改接到 P0 口,必须加 RESPACK-8 排阻做上拉。P0 口内部是开漏结构,不接上拉电阻时输出高电平的实际上是高阻态,LED 会一直微亮或者完全不亮,这个现象和"代码没跑起来"几乎无法区分,是新手最容易卡住的地方。

5.2 HEX 加载与时钟频率:两个必须对齐的开关

程序编译完,Keil 要勾选Options for Target → Output → Create HEX File,否则输出目录里只有.obj没有.hex。这一步漏掉,PROTEUS 里加载时根本找不到文件。

加载的动作是双击原理图里的 AT89C51,在弹出的属性框里找到Program File一栏,选中刚才生成的 hex 文件。同一栏下面还有一个Clock Frequency,默认值通常是 12MHz,但要逐字确认它和你代码里的假设一致。这个值不影响代码执行逻辑,但影响仿真时间轴——如果你代码按 12MHz 算延时,而这里填的是 1MHz,灯会慢十二倍,你会误以为延时函数写错了。

还有一个细节:每次改完代码重新编译,PROTEUS 不一定会自动重新读取 hex。稳妥的做法是双击单片机,重新选一次文件路径,或者点一下属性框的确定让它重新加载。文件时间戳这个东西,仿真器有时候认不准。

5.3 五条最容易翻车的排查清单

现象一:八个灯全部常亮或者全部不亮,改代码也没用。原因通常出在接法上。共阳接法要求 LED 阳极接 VCC、阴极接 IO,程序输出低电平才亮;如果原理图接成了共阴,程序输出的低电平反而让灯灭,逻辑整个反过来。解决方式是把 LED 元件在 PROTEUS 里旋转 180 度重新连,或者在代码里把输出取反,但推荐改硬件,因为代码取反会让你后面所有的花样表都要重算一遍,容易出错。

现象二:接在 P0 口的灯怎么都不亮,接 P1 就正常。原因是 P0 口开漏无内部上拉,输出高电平时处于高阻态,带不动负载。解决方式是在 P0 和 VCC 之间加一个 RESPACK-8 排阻(10kΩ),或者干脆把灯挪到 P1/P2。这个坑的迷惑性在于,用万用表量 P0 口电压可能是正常的,但灯就是不亮,因为驱动力不够。

现象三:代码改了,仿真的效果一点没变。两条排查路径。第一,确认 Keil 里Create HEX File勾上了、编译输出窗口显示creating hex file...、hex 文件的时间戳是刚才。第二,确认 PROTEUS 里的单片机真的重新加载了文件。这两步任何一步没做到,你看到的都是上一版程序的效果。我一般的习惯是每次改完代码,先看编译输出有没有报错,再回仿真里复位一次(点停止再点运行),确保从复位向量重新开始执行。

现象四:灯跑得飞快或者慢得离谱,延时参数怎么调都不对。先看 PROTEUS 里单片机的Clock Frequency和代码假设是否一致,再看延时函数的内层循环变量类型。如果内层用的是unsigned char而循环次数写成 300,300 会被截断成 44,延时缩短到七分之一。还有一种情况是编译器优化等级调高了,空的for循环被整个优化掉,延时直接变成 0,这时候要么把循环变量声明成volatile,要么改用定时器。

现象五:花样跑着跑着开始乱跳,或者灯的顺序完全对不上表。大概率是内存或下标问题。先看花样表有没有加code,没加的话 24 个字节塞进 128 字节 RAM,加上栈空间很容易溢出,溢出的表现就是随机乱跳。再看下标有没有做越界保护,idx++之后如果只写了if (idx == 24),某些异常路径下可能跳过这个判断。把比较改成>=,把step的自增改成& 0x07,这两个小改动能挡掉大部分玄学问题。

注意:排查顺序建议从"硬件接法 → 时钟频率 → hex 是否更新 → 代码逻辑"逐层往下。反过来查,很容易在代码里找一个根本不存在的 bug。

6. 验收与答辩:怎么让演示不翻车

验收这件事,我一般会提前一天把整条链路自己走一遍,而不是等到现场才第一次完整运行。流水灯看着简单,但现场翻车的概率并不低,原因往往不是代码错,而是环境没对齐。

先给一份可以照着检查的验收表:

验收项判定方法合格标准
基础流水复位后观察 30 秒8 灯依次点亮,循环无中断
双向流水观察左移到头后的动作从第 8 位原路返回,无跳变
花样切换按一次切换键花样立即改变,无卡顿
速度调节长按调速键快慢两档切换明显,误差可接受
复位可靠性反复按复位键 5 次每次都从第 1 位开始
时序精度虚拟示波器测单步周期与设定值偏差在 ±5% 以内

演示时有个技巧:先讲结构,再跑效果。上来就点运行,八个灯一闪,评审脑子里没有框架,看完也记不住你做了什么。更好的顺序是先在原理图上指一下晶振和复位电路,说明为什么选 12MHz、为什么用共阳;再打开工程目录,说清楚led.c、pattern.c、main.c各管什么;最后才运行。这样即使演示过程中出了小问题,评审也知道你的设计是完整的,只是环境问题。

被问到"为什么不用定时器"或者"为什么延时不准"时,别急着解释,直接把timer0_isr那段代码翻出来,说清楚 50ms 的初值是怎么算的、speed变量怎么实现调速。能当场把参数推导讲明白,比跑十遍效果都有说服力。

从那以后我每次带人做这类项目,都强制要求先写验收表再写代码——表里列不出来的功能,说明需求还没想清楚,写出来的代码大概率要返工。这套流程放到数码管、矩阵键盘上一样适用,只是表里的行数变多而已。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询