蓝桥杯第二周-单片机基础

第二周:单片机基础应用笔记

:package: 第一讲:流水灯与IO控制

:one: 点亮LED——初识GPIO

整体控制(群控模式)

可以把P1口看作一个有8个开关的总控板,一次性设置所有开关状态。

#include <REGX52.H>

void main()
{
    while(1)
    {
        P1 = 0xAA;  // 二进制 1010 1010,间隔点亮LED
    }
}

比喻:像电闸总开关,一推全亮/全灭。

单独控制(精细操作)

每个IO口就像独立的电灯开关,可单独操作。

#include <REGX52.H>

void main()
{
    while(1)
    {
        P1_0 = 0;  // 第一个灯亮(低电平驱动)
        P1_1 = 1;  // 第二个灯灭
        P1_2 = 1;  // 第三个灯灭
        P1_3 = 0;  // 第四个灯亮
    }
}

:two: LED闪烁——引入时间概念

延时函数的生成

使用STC-ISP工具生成精确延时,就像设置一个“定时沙漏”。

操作步骤

  1. 打开STC-ISP → 软件延时计算器
  2. 选择:12MHz, STC-Y1
  3. 生成代码
void Delay(unsigned int xms)  // @12.000MHz
{
    while(xms--)
    {
        unsigned char i, j;
        i = 2;
        j = 239;
        do {
            while (--j);
        } while (--i);
    }
}

实现流水闪烁

让灯光像接力跑一样逐个传递。

void main()
{
    while(1)
    {
        P1_0 = 0; P1_3 = 1;
        Delay(1000);  // 亮1秒
        
        P1_1 = 0; P1_0 = 1;
        Delay(1000);
        
        P1_2 = 0; P1_1 = 1;
        Delay(1000);
        
        P1_3 = 0; P1_2 = 1;
        Delay(1000);
    }
}

:three: 使用库函数实现流水灯

调用现成的“灯光特效库”,让代码更简洁。

#include <REGX52.H>
#include <intrins.h>  // 内含循环移位函数

// 延时函数(同上)
void Delay(unsigned int xms){...}

unsigned char ucLed = 0xFE;  // 初始值:1111 1110(仅第一个灯亮)

void main()
{
    while(1)
    {
        ucLed = _crol_(ucLed, 1);  // 循环左移1位
        P1 = ucLed;                // 输出到LED
        Delay(500);                // 延时0.5秒
    }
}

函数说明

  • _crol_(P1, n):P1向左循环移n位
  • _cror_(P1, n):P1向右循环移n位

:video_game: 第二讲:按键交互

:one: 按键原理

按键就像一个“电流阀门”,按下时接通电路,改变引脚电平。

连接方式决定电平

  • 左边接地(GND) → 按下时右边引脚=0(低电平)
  • 左边接VCC → 按下时右边引脚=1(高电平)

独立按键引脚对应

  • S1 → P3_4
  • S2 → P3_5
  • S3 → P3_6
  • S4 → P3_7

:two: 按键读取函数

给每个按键分配一个“身份证号”。

unsigned char Key_Read()
{
    unsigned char temp = 0;
    if(P3_4 == 0) temp = 1;  // S1按下→返回1
    if(P3_5 == 0) temp = 2;  // S2按下→返回2
    if(P3_6 == 0) temp = 3;  // S3按下→返回3
    if(P3_7 == 0) temp = 4;  // S4按下→返回4
    return temp;
}

:three: 按键检测“四行金句”

这是检测按键动作的核心逻辑,必须熟记!

unsigned char Key_Val, Key_Down, Key_Up, Key_Old;

Key_Val = Key_Read();                    // 1. 读取当前按键状态
Key_Down = Key_Val & (Key_Val ^ Key_Old);// 2. 检测按下瞬间(下降沿)
Key_Up = ~Key_Val & (Key_Val ^ Key_Old); // 3. 检测释放瞬间(上升沿)
Key_Old = Key_Val;                       // 4. 保存当前状态供下次比较

比喻

  • Key_Down:发现有人刚坐下(按下瞬间)
  • Key_Up:发现有人刚站起来(释放瞬间)
  • Key_Old:记住上一刻谁在坐着

:four: 按键控制LED

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;
        
        // 按键功能映射
        if(Key_Down == 1) P1_0 = 0;  // S1按下→LED1亮
        if(Key_Up == 2)   P1_0 = 1;  // S2释放→LED1灭
        if(Key_Old == 3)  P1_1 = 0;  // S3按住期间→LED2亮
        else              P1_1 = 1;  // 否则→LED2灭
    }
}

:five: 按键控制流水灯启停

引入“系统标志位”作为总开关。

unsigned char ucLed = 0xFE;
unsigned char Key_Val, Key_Down, Key_Up, Key_Old;
bit System_Flag = 0;  // 0:停止,1:运行

void main()
{
    while(1)
    {
        // 按键检测(略)
        
        if(System_Flag == 1)  // 系统运行中
        {
            ucLed = _crol_(ucLed, 1);
            P1 = ucLed;
            Delay(500);
        }
        
        // 按键控制标志位
        if(Key_Down == 1) System_Flag = 1;  // S1启动
        if(Key_Down == 2) System_Flag = 0;  // S2停止
    }
}

:six: 矩阵键盘

把16个按键排列成4×4矩阵,节省引脚资源。

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;
    // ... 以此类推
    
    return temp;
}

注意:使用矩阵键盘时,独立按键引脚被占用,二者不能同时使用。

:seven: 综合项目:彩灯控制系统

实现多种灯光模式的可控切换。

功能需求

  • 模式1:顺序流水(左→右)
  • 模式2:逆序流水(右→左)
  • 模式3:对称展开(从中间向两边)
  • 模式4:对称收缩(从两边向中间)

按键定义

  • S1:启动
  • S2:暂停
  • S3:模式+1
  • S4:模式-1
// 关键代码段
unsigned char LED_Mode = 0;  // 当前模式
unsigned char LED_Date[4] = {0x7E, 0xBD, 0xDB, 0xE7};  // 对称模式数据
bit System_Flag = 1;  // 默认启动

void main()
{
    while(1)
    {
        // 按键检测(略)
        
        // 按键处理
        switch(Key_Down)
        {
            case 1: System_Flag = 1; break;  // 启动
            case 2: System_Flag = 0; break;  // 暂停
            case 3: LED_Mode++; if(LED_Mode == 4) LED_Mode = 0; break;
            case 4: LED_Mode--; if(LED_Mode == 255) LED_Mode = 3; break;
        }
        
        // 灯光控制
        if(System_Flag == 1)
        {
            switch(LED_Mode)
            {
                case 0: /* 模式1代码 */ break;
                case 1: /* 模式2代码 */ break;
                case 2: /* 模式3代码 */ break;
                case 3: /* 模式4代码 */ break;
            }
        }
    }
}

:1234: 第三讲:数码管显示

:one: 数码管结构

数码管就像“数字积木”,由8个LED段(a-g+dp)组成,可拼出0-9的数字。

硬件特性

  • 6位数码管:位置编号0-5(从左到右)
  • 每位可独立显示不同数字

:two: 静态显示(单数字)

只能在一个固定位置显示一个数字。

// 位选数组:选择哪一位数码管
unsigned char Seg_Wela[] = {0xFE, 0xFD, 0xFB, 0xF7, 0xEF, 0xDF};

// 段选数组:显示什么数字(0-9),最后0x00表示全灭
unsigned char Seg_Dula[] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 
                            0x6D, 0x7D, 0x07, 0x7F, 0x6F, 0x00};

// 显示函数:三步法(消影→位选→段选)
void Seg_Disp(unsigned char wela, dula)
{
    P0 = 0x00;        // 1. 消影(清除残影)
    P2_6 = 1; P2_6 = 0;  // 锁存消影数据
    
    P0 = Seg_Wela[wela];  // 2. 选择显示位置
    P2_7 = 1; P2_7 = 0;   // 锁存位选数据
    
    P0 = Seg_Dula[dula];  // 3. 显示具体数字
    P2_6 = 1; P2_6 = 0;   // 锁存段选数据
}

:three: 动态显示(多数字)

利用人眼视觉暂留,快速切换显示不同位置的数字。

核心思想

比喻:就像快速转动的手电筒,虽然每次只照一个地方,但转得够快时,看起来所有地方都是亮的。

#include <REGX52.H>

// 数码管缓冲区:存储6位数码管要显示的数字
unsigned char Seg_Buf[] = {1, 2, 3, 4, 5, 6};

// 定时器初始化(1ms中断一次)
void Timer0Init(void)
{
    TMOD &= 0xF0; TMOD |= 0x01;  // 定时器0,模式1
    TL0 = 0x18; TH0 = 0xFC;      // 1ms定时初值
    TF0 = 0;                     // 清除标志
    TR0 = 1;                     // 启动定时器
    ET0 = 1;                     // 允许定时器中断
    EA = 1;                      // 开启总中断
}

// 定时器中断服务函数
void Timer0Service() interrupt 1
{
    static unsigned char Seg_Pos = 0;  // 当前显示位置
    
    TL0 = 0x18; TH0 = 0xFC;  // 重装初值
    
    // 切换到下一位
    if(++Seg_Pos == 6) Seg_Pos = 0;
    
    // 显示该位置的数字
    Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos]);
}

void main()
{
    Timer0Init();  // 初始化定时器
    while(1)
    {
        // 主循环可更新Seg_Buf内容
        // 例如:显示变量Time的百位、十位、个位
        // Seg_Buf[0] = Time / 100 % 10;
        // Seg_Buf[1] = Time / 10 % 10;
        // Seg_Buf[2] = Time % 10;
    }
}

:four: 数字分解技巧

如何把一个整数分解为各个数位?

unsigned int Time = 567;  // 要显示的数字

// 分解方法(通用公式):
Seg_Buf[0] = Time / 100 % 10;  // 百位:567÷100=5
Seg_Buf[1] = Time / 10 % 10;   // 十位:567÷10=56 → 56%10=6
Seg_Buf[2] = Time % 10;        // 个位:567%10=7

:memo: 关键知识点总结

概念 比喻 核心要点
GPIO控制 电灯开关 P1整体赋值 vs P1_x单独控制
延时函数 定时沙漏 用STC-ISP生成,控制时间节奏
循环移位 旋转木马 _crol_()左移,_cror_()右移
按键检测 门铃感应 “四行金句”检测按下/释放瞬间
矩阵键盘 交叉路口 行列扫描,节省引脚
数码管 数字积木 位选(哪里)+段选(什么)
动态显示 手电筒快转 利用视觉暂留,定时器刷新
中断系统 闹钟提醒 定时到点自动执行,不占用主循环

:light_bulb: 编程思想提升

  1. 状态机思维:用System_Flag控制系统状态
  2. 模块化设计:功能分离(按键、显示、灯光独立)
  3. 缓冲区概念Seg_Buf作为显示数据中间站
  4. 中断服务:时间敏感操作交给中断处理
  5. 位操作技巧:灵活运用&|^~进行位运算

学习如同点亮LED,需要耐心连接每个引脚,当所有知识正确连通时,智慧的灯光自然就会亮起。 :sparkles: