LED
Date: 2026-02-04
配置新项目
新建项目
-
选择
AT89C52芯片 -
中
Output页勾选Create HEX File -
修改
Project Targets为项目名、Group为"User"
服了这软件,不调DPI很糊,调了之后字体又小
代码文本配置
- 打开
Edit→Configuration - 调整
Encoding编码格式为UTF-8或GB2312 - 调整
Tab size为4,可根据需要选择Insert spaces for tabs,一个项目选一种格式!
镇楼
代码规范
/* 功能说明 */
/* 头文件声明 */
#include <REGX52.H>
/* 全局变量声明 */
unsigned char i = 0;
/* 函数声明或定义 */
void Delay(unsigned int ms); // 省略
/* 主函数 */
void main()
{
unsigned int j;
/* Loop */
while(1);
}
[!NOTE]
Keil C51目前默认兼容C89标准,规定==局部变量能且仅能在任一代码块(block)的起始位置声明==,也就是说,在一个代码块里,语句后面不能声明局部变量。包括C89在内的所有C标准规定,代码块之外不能出现语句,因此全局变量可在代码块外任意位置声明。
点亮 LED
CPU 通过MMIO(存储器映射 I/O)操作外设寄存器,写入数据后,经驱动器改变电路物理状态(如高低电平)。REGX52.H头文件定义了P1等外设寄存器的地址,代码中对P1赋值,本质是直接操作对应的8位寄存器,寄存器每一位电平会映射到P1口物理引脚上:给P1赋一字节值会同时控制P1_0~P1_7共8个引脚的电平;而直接操作P1_1这类位标识符(如P1_1=0),是对该寄存器指定某一位的单独写入,精准控制单个引脚电平,这是更直观的 MMIO按位操作体现。
简单点亮
单独给IO口赋值
P1_0 = 0;
直接对P1整体赋值
P1 = 0xAA;
利用掩码对P1整体赋值
例如要将P1下的1111 1111修改为1111 1101,即点亮第2个LED灯,可以利用按位与计算,结合掩码来控制。
P1 &= ~(1<<1);
闪烁
让LED灯闪烁本质就是利用延时实现其亮灭。
生成延时函数
- 打开
STC-ISP烧录工具 - 进入
软件延时计算器页,设置系统频率为12.000 MHz,定时长度为1ms,指令集为STC-Y1 - 根据需要,嵌套
while,修改为Delay()函数,建议使用unsigned int类型,范围合适
如果出现空操作函数_nop_()引发的报错,删除即可,或者导入intrins.h库。
[!NOTE]
intrins库是Keil C51编译器从C语言层面,对8051内核的硬件指令进行封装的库。
P1_1 = 0;
Delay(200);
P1_1 = 1;
Delay(200);
// 通过 异或操作符 XOR:不一样为真
P1_1 ^= 1;
// 1^1 == 0, 0^1 == 1
流水
简陋实现
一连串修改P1的各个引脚。稍微方便一点的就是利用掩码。
unsigned char i;
P1 = 0xFF;
for(i=0; i<8; i++)
{
P1 = ~(1<<i); // 直接赋值
Delay(200);
}
调用循环移位函数
// 函数原型
unsigned char _crol_(unsigned char val, unsigned char n);
unsigned char _cror_(unsigned char val, unsigned char n);
_crol_和_cror_是intrins库封装8位循环移位专用函数,仅对8位数据生效。==数据最高(低)位溢出后,直接补到低(高)位的空缺位置==,形成8位闭环循环。形象理解:8位数据,左右划出边界,手指拖来拖去(好幼稚)。
要想做流水灯,就需要有一个小烧0在1111 1111里游来游去,我们可以定义一个初始量为1111 1110。
#include <REGX52.H>
#include <intrins.h>
unsigned char ucLed = 0xFE;
/* ... */
P1 = ucLed;
Delay(200);
ucLed = _crol_(ucLed, 1);
按键
Date: 2026-02-04
独立按键
原理图
如原理图所示,4个按键的一端都连接GND接地,另一段分别连接在P3.4、P3.5、P3.6、P3.7四个IO口上。
当按键松开时,按键内部金属触点分离,IO引脚 → 按键 → GND 路径断开,无电流通过,上拉电阻将IO引脚电平强制拉为高电平,该高电平会实时映射到P3寄存器的对应位,读取结果为1。
按下按键,内部金属触点接触,IO引脚 → 按键 → GND 路径形成闭合回路,IO引脚被强制拉到GND的低电平,被实时映射到P3寄存器对应位,读取结果为0。
读取模板
/* 全局变量 */
unsigned char Key_Val, Key_Down, Key_Up, Key_Old;
// 按键读取函数
unsigned char Key_Read(); // 判断引脚的状态,给定编号
/* 主函数 */
void main()
{
while(1)
{
Key_Val = Key_Read(); // 读取键码值
Key_Down = Key_Val & (Key_Val ^ Key_Old); // 检测下降沿
Key_Up = ~Key_Val & (Key_Val ^ Key_Old); // 检测上升沿
Key_Old = Key_Val; // 扫描辅助变量
}
}
矩阵按键
原理图
如原理图所示,4×4矩阵按键由4条行线(连接P3.0、P3.1、P3.2、P3.3)、4条列线(连接P3.4、P3.5、P3.6、P3.7)和16 个按键组成。
为了节省IO口的使用,读取矩阵按键采用的是扫描的思路。拿逐行扫描,逐列检测举例,我们使P3.0接地,其余3行送高电平,再逐列读取寄存器,检测按键状态。当然,也可以采用逐列扫描,逐行检测的方式。
读取模板
unsigned char Key_Read()
{
unsigned char temp = 0;
P3_0 = 0; P3_1 = 1; P3_2 = 1; P3_3 = 1;
if (P3_4 == 0) temp = 1;
if (P3_5 == 0) temp = 2;
if (P3_6 == 0) temp = 3;
if (P3_7 == 0) temp = 4;
P3_0 = 1; P3_1 = 0; P3_2 = 1; P3_3 = 1;
if (P3_4 == 0) temp = 5;
if (P3_5 == 0) temp = 6;
if (P3_6 == 0) temp = 7;
if (P3_7 == 0) temp = 8;
P3_0 = 1; P3_1 = 1; P3_2 = 0; P3_3 = 1;
if (P3_4 == 0) temp = 9;
if (P3_5 == 0) temp = 10;
if (P3_6 == 0) temp = 11;
if (P3_7 == 0) temp = 12;
P3_0 = 1; P3_1 = 1; P3_2 = 1; P3_3 = 0;
if (P3_4 == 0) temp = 13;
if (P3_5 == 0) temp = 14;
if (P3_6 == 0) temp = 15;
if (P3_7 == 0) temp = 16;
return temp;
}
看起来笨拙,但其实效率很快,且通俗易懂。当然可以用循环的方式来简化代码,但这在减小代码量的同时,增加了理解难度。
[!TIP]
试图采用循环的方式简化矩阵按键的状态读取代码,是一个不太聪明的想法。
数码管
Date: 2026-02-05
原理图
数码管(Segment Display/LED)将8个LED封装在一起,内部由7个笔段(a~g)和1个小数点段(dp)组成,共8个段。每一组这样的8段一体结构,称为1位数码管。
根据公共端连接情况,数码管分为共阴极、共阳极,应根据具体情况给出高电平或低电平。蓝桥杯单片机组使用的CT107D开发板的数码管是共阳极的。与矩阵按键逐行、逐列扫描思想一致,多位数码管采用段选+位选动态扫描方式。开发板常见6位数码管,位选、段选的数据分别储存在1个字节里。
为了节省IO口,多位数码管的段选字节、位选字节均通过P0口分时传输,配合锁存器实现数据分离锁存,核心流程为P0送数据 → 控制对应锁存引脚 → 锁存数据。数码管驱动常用74HC573锁存器,==置高电平,准备接收数据;置低电平,锁存数据==。
如原理图,引脚P2.6连接SEG DLE,控制段选锁存,引脚P2.7连接SEG WLE控制位选锁存。
定时器
51单片机的定时器的电路和运转均在单片机内部完成,可用于计时系统,实现软件计时,也可替代长时间Delay等。STC89C52和IAP15F2K61S2芯片都有3个定时器(T0、T1、T2)。
定时器通过计数单元,根据时钟脉冲,每隔一个单位长度就增加计数值,当计时完成,就会向中断系统发出中断申请,使程序跳转到中断服务函数中执行。定时器溢出中断后,会跳到0再次定时。
由外部引脚提供脉冲时,定时器为计数器模式;当系统时钟(SYSclk)提供脉冲,定时器为定时器模式。晶振是系统时钟的计数单元,应根据具体情况配置定时器。
配置定时器
如图配置定时器,之后删除行AUXR &= 0x7F;,并在下面添加以下代码:
ET0 = 1; // 允许定时器0中断
EA = 1; // 中断全局开关
完整代码:
// 定时器初始化函数
void Timer0_Init(void) //1毫秒@12.000MHz
{
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //设置定时器模式
TL0 = 0x18; //设置定时初始值
TH0 = 0xFC; //设置定时初始值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0 = 1; // 允许定时器0中断
EA = 1; // 中断全局开关
}
中断
CPU 在正常执行程序期间,若出现中断请求信号,将在当前指令执行结束后暂停主程序,保存现场与断点地址,转而执行对应的中断服务程序以处理中断事件;中断处理完成后,CPU 恢复现场并返回原断点处继续执行主程序,该机制可使 CPU 快速响应外部或内部事件,无需通过主程序循环查询事件状态。中断系统一般允许多个中断源,CPU通常根据中断队列中优先级最高的中断请求。
中断服务函数
形如void Timer0Service() interrupt 1的函数是中断服务函数的声明,这是Keil C51编译器的一个语法糖。interrupt 1 不是随便写的数字,因为在8051体系里,中断靠中断号识别。8051有一个固定的中断向量表,这是硬件固有定义。在手册中我们发现,定时器0分配的中断号是1,因此当定时器0中断时,会自动调用interrupt 1的函数。
对于现在的数码管动态显示的具体情况,我们要求它需要一直保持1毫秒单位的中断,以达到持续扫描的目的。又因为定时器溢出中断后,初始值从0开始,使闹铃周期不为1毫秒,我们需要手动修改初始值。
// 定时器0中断服务函数
void Timer0Service() interrupt 1
{
TL0 = 0x18; // 设置定时初始值
TH0 = 0xFC; // 设置定时初始值
// ... ...
}
显示实现
数码管显示函数
// 翻手册
unsigned char code Seg_Dula[] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F, 0x00};
unsigned char code Seg_Wela[] = {0xFE, 0xFD, 0xFB, 0xF7, 0xEF, 0xDF};
void Seg_Disp(unsigned char wela, unsigned char dula)
{
// 消影
P0 = 0x00;
P2_6 = 1;
P2_6 = 0;
P0 = Seg_Wela[wela];
P2_7 = 1;
P2_7 = 0;
P0 = Seg_Dula[dula];
P2_6 = 1;
P2_6 = 0;
}
[!IMPORTANT]
千万注意定义函数时,段、位两个参数的位置!
动态显示
开发板的数码管采用动态结构,所有位共用同一个段数据,因此一次只能点亮一位;
如果想要显示多个字符或动态改变显示,写多个自上到下排列的Seg_Disp`函数会导致位切换时,原段码干扰,因此必须先消影;
消影会使数码管熄灭,点亮只能维持极短时间,因此显示本质上是瞬时的,而非持续的;
单次显示只维持极短时间,必须以固定频率不断刷新;
主循环无法提供稳定刷新,因此引入定时器中断;(while不是不能用,就是太垃圾了)
每1ms扫描一位,更新段码,即以6ms为一轮完成对全部数码管的扫描更新;
借助视觉暂留实现“看似同时显示”。
unsigned char Seg_Pos;
unsigned char Seg_Buf[6] = {1, 2, 3, 4, 5, 6};
以6位数码管为例,我们声明一个长度为6的数组Seg_Buf[]存储显示字符,然后在每一轮更新内,一位一位地更新数码管,即可实现动态显示。Seg_Pos仅仅是个工具变量。
void Timer0Service interrupt 1
{
if(++Seg_Pos == 6) Seg_Pos = 0;
Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos]);
}