蓝桥杯单片机笔记01

LED

Date: 2026-02-04

配置新项目

新建项目

  • 选择AT89C52芯片

  • image-20260204131854520Output页勾选Create HEX File

  • image-20260204132015993修改Project Targets为项目名、Group"User"

服了这软件,不调DPI很糊,调了之后字体又小

代码文本配置

image-20260204142740337

  • 打开EditConfiguration image-20260204142921671
  • 调整Encoding编码格式为UTF-8GB2312
  • 调整Tab size4,可根据需要选择Insert spaces for tabs,一个项目选一种格式!

镇楼

image-20260204133010576image-20260204133019561

代码规范

/* 功能说明 */

/* 头文件声明 */
#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类型,范围合适

image-20260204175054601

如果出现空操作函数_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位数据,左右划出边界,手指拖来拖去(好幼稚)。

要想做流水灯,就需要有一个小烧01111 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

独立按键

原理图

image-20260204190132980image-20260204190003414

如原理图所示,4个按键的一端都连接GND接地,另一段分别连接在P3.4P3.5P3.6P3.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;  // 扫描辅助变量
    }
}

矩阵按键

原理图

image-20260204195821478

如原理图所示,4×4矩阵按键由4条行线(连接P3.0P3.1P3.2P3.3)、4条列线(连接P3.4P3.5P3.6P3.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

原理图

image-20260205140315022

数码管(Segment Display/LED)将8个LED封装在一起,内部由7个笔段(a~g)1个小数点段(dp)组成,共8个段。每一组这样的8段一体结构,称为1位数码管。

根据公共端连接情况,数码管分为共阴极、共阳极,应根据具体情况给出高电平或低电平。蓝桥杯单片机组使用的CT107D开发板的数码管是共阳极的。与矩阵按键逐行、逐列扫描思想一致,多位数码管采用段选+位选动态扫描方式。开发板常见6位数码管,位选、段选的数据分别储存在1个字节里。

image-20260205142910644

为了节省IO口,多位数码管的段选字节、位选字节均通过P0口分时传输,配合锁存器实现数据分离锁存,核心流程为P0送数据 → 控制对应锁存引脚 → 锁存数据。数码管驱动常用74HC573锁存器,==置高电平,准备接收数据;置低电平,锁存数据==。

如原理图,引脚P2.6连接SEG DLE,控制段选锁存,引脚P2.7连接SEG WLE控制位选锁存。

定时器

51单片机的定时器的电路和运转均在单片机内部完成,可用于计时系统,实现软件计时,也可替代长时间Delay等。STC89C52IAP15F2K61S2芯片都有3个定时器(T0T1T2)。

定时器通过计数单元,根据时钟脉冲,每隔一个单位长度就增加计数值,当计时完成,就会向中断系统发出中断申请,使程序跳转到中断服务函数中执行。定时器溢出中断后,会跳到0再次定时。

由外部引脚提供脉冲时,定时器为计数器模式;当系统时钟(SYSclk)提供脉冲,定时器为定时器模式。晶振是系统时钟的计数单元,应根据具体情况配置定时器。

配置定时器

image-20260205220827743

如图配置定时器,之后删除行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]);
}