蓝桥杯单片机复习笔记:过渡模拟二

第一部分:软件逻辑与算法 (大脑篇)

这部分是程序的灵魂,决定了单片机怎么“思考”。

1. 数码管光标闪烁 (500ms)

核心思想:利用定时器制造一个“心跳”,然后根据心跳决定显示数字还是黑屏。

  • 原理:Timer_500Ms 负责计时,Seg_Star_Flag 负责翻转状态(0变1,1变0)。
  • 代码解析:
// --- 定时器中断内 ---
if(++Timer_500Ms == 500) // 每500毫秒触发一次
{
    Timer_500Ms = 0;     // 必须清零,不然下次没法计时了
    Seg_Star_Flag ^= 1;  // 【考点】异或操作:0变1,1变0,实现“亮/灭”切换
}

// --- 数码管显示函数内 ---
// 保护机制:如果当前位没有数据(11),就不处理,防止乱码
if(Seg_Input[2] == 11) 
{
    // 【三目运算符】
    // 翻译:Flag是1吗?是就正常显示数字;不是就显示10(10代表熄灭)
    // 效果:数字 -> 熄灭 -> 数字 -> 熄灭 ...
    Seg_Buf[3 + Seg_Input_Index] = Seg_Star_Flag ? Seg_Input[Seg_Input_Index] : 10;
}

2. 小数点输入逻辑 (状态机)

核心思想:为了防止用户乱按小数点,必须加“锁”。

  • Point_Flag 就是那把锁。一次输入只能按一次小数点。
// --- 按键处理函数 ---

case 11: // S11 小数点键按下
    // 【三重安检】
    // 1. Seg_DispMode == 0: 必须在采集界面
    // 2. Point_Flag == 0: 之前没按过小数点
    // 3. Seg_Input[0] != 11: 第1位必须有数字(不能上来就按小数点)
    if(Seg_DispMode == 0 && Point_Flag == 0 && Seg_Input[0] != 11)
    {
        Seg_Point[2 + Seg_Input_Index] = 1; // 当前位置点亮小数点
        Point_Wela = Seg_Input_Index;       // 记住小数点在第几位(核心!)
        Point_Flag = 1;                     // 上锁!之后再按S11也进不来了
    }
    break;

case 16: // S16 采集/确认键按下
    if(Seg_Disp_Mode != 0) // 如果不在采集界面 -> 切换到采集界面
    {
        Seg_Input_Index = 0; // 光标归零
        Point_Flag = 0;      // 解锁小数点
        for(i = 0; i < 3; i++) Seg_Input[i] = 11; // 清空数组
        Seg_Disp_Mode = 0;   // 状态切换
    }
    else // 已经在采集界面了 -> 说明用户输完了,想确认
    {
        // 【有效性检查】
        // 如果没按过小数点(Point_Flag==0) 或者 没输满3位
        // 判定为:无效输入!
        if(Point_Flag == 0 || Seg_Input_Index < 3)
        {
             Seg_Input_Index = 0;
             Seg_Point[3] = Seg_Point[4] = 0; // 灭掉小数点
             Point_Flag = 0;
             for(i = 0; i < 3; i++) Seg_Input[i] = 11; // 清空重来
        }
        else // 输入合法
        {
             // 提取数据:百位*100 + 十位*10 + 个位
             // 注意:这里原代码Seq_Input可能是笔误,应为 Seg_Input
             Temperature = Seg_Input[0] * 100 + Seg_Input[1] * 10 + Seg_Input[2] + 5; 
        }
    }
    break;

3. 长按 vs 短按 (按键进阶)

核心思想:用时间来区分意图。

  • 按下开始计时。
  • 抬起时,看计时器:时间短 = 短按;时间长 = 长按。
// --- 步骤1:按下开始计时 ---
if(Seg_Disp_Mode == 2) // 只有在设置界面才有效
{
    if(Key_Down == 14) // S14 被按下
        Time_Flag = 1; // 开启计时开关
}

// --- 步骤2:定时器里数数 ---
if(Time_Flag == 1)
{
    // 如果一直按着,Count_500Ms就会一直加
    if(++Count_500Ms == 600) 
        Count_500Ms = 600; // 封顶限制,防止溢出
}

// --- 步骤3:判断是长是短 ---

// 【情况A:短按逻辑】
// 如果松手时(Key_Up),计数值还没到500(0.5秒)
if(Count_500Ms < 500) 
{
    if(Key_Up == 14) // 手抬起来了
    {
        Time_Flag = Count_500Ms = 0; // 状态清零,为下次做准备
        if(++Parameter[Parameter_Index] > 70) // 正常加1
            Parameter[Parameter_Index] = 10;
    }
}
// 【情况B:长按逻辑】
else 
{
    // 注意:这里是用 Key_Old 判断,说明手还按着没松开!
    if(Key_Old == 14) 
    {
        // 只要按着,代码跑到这就加1,跑得很快,实现“连发”
        if(++Parameter[Parameter_Index] > 70) 
            Parameter[Parameter_Index] = 10;
    }
    // 松手后清零
    if(Key_Up == 14)
        Time_Flag = Count_500Ms = 0;
}

4. LED 亮度等级 (PWM技术)

核心思想:“欺骗眼睛”。灯其实一直在闪,只是闪得太快,人眼看着就像“变暗”了。

  • Led_Num:一个从0跑到9的计数器。
  • Led_Pwm:亮度阈值(比如3,6,9)。
    • 如果 Led_Pwm 是 3:10次里亮3次(暗)。
    • 如果 Led_Pwm 是 9:10次里亮9次(亮)。
// --- 定时器中断内快速执行 ---
if(++Led_Num == 10) Led_Num = 0; // 0-9 循环计数

// 【PWM 核心生成】
if(Led_Num < Led_Pwm)
    Led_Disp(Led_Pos, ucLed[Led_Pos]); // 亮灯
else
    Led_Disp(Led_Pos, 0); // 灭灯

// --- 根据温度设置亮度阈值 ---
if(Temperature > Parameter_Ctrol[0])
    Led_Pwm = 3; // 高温 -> 占空比 30% -> 最暗?(看题目要求,通常越小越暗)
else if(Temperature < Parameter_Ctrol[0] && Temperature > Parameter_Ctrol[1])
    Led_Pwm = 6; // 舒适 -> 占空比 60% -> 中等
else
    Led_Pwm = 9; // 低温 -> 占空比 90% -> 最亮

第二部分:硬件底层驱动 (手脚篇)

这部分是单片机的“生理本能”,控制锁存器和译码器。

1. 硬件原理图解

image-20260204105045635

image-20260204105228695

单片机控制外设主要靠两个门神:

  • 译码器 (74HC138):管家,决定选谁(LED还是数码管?)
  • 锁存器 (74HC573):大门,决定数据是穿过去还是被锁住。

2. 初始化 (Init.c)

目的:上电第一件事,把所有可能乱叫乱亮的东西全关掉。

#include <Init.h>

void System_Init()
{
    // 1. 关闭 LED
    P0 = 0xff; // 准备数据:全1 (LED是低电平亮,全1就是全灭)
    // 这里的位运算是为了只改变P2的高3位(Y4C),不影响低5位
    P2 = P2 & 0x1f | 0x80; // 选中 Y4 (LED)
    P2 &= 0x1f;            // 锁存!(关门,保持全灭状态)
    
    // 2. 关闭 蜂鸣器/继电器
    P0 = 0x00; // 准备数据:全0 (蜂鸣器高电平响,全0是关)
    P2 = P2 & 0x1f | 0xa0; // 选中 Y5 (蜂鸣器)
    P2 &= 0x1f;            // 锁存!
}

3. LED 驱动 (Led.c) —— 高分写法

亮点:使用了 static 缓存状态。

为什么这么写? 如果不记录状态,当你为了点亮 L2 去修改 P0 时,可能会不小心把原本亮着的 L1 给灭了。

#include <Led.h>

// addr: 灯的编号(0-7)
// enable: 1=亮, 0=灭
void Led_Disp(unsigned char addr, enable)
{
    // static:静态变量,函数运行完也不会丢失数据,像记事本一样
    static unsigned char temp = 0x00;     // 逻辑状态记事本
    static unsigned char temp_old = 0xff; // 硬件状态备份

    // --- 步骤1:只改记事本 ---
    if(enable)
        temp |= 0x01 << addr;    // 将第addr位 置1
    else
        temp &= ~(0x01 << addr); // 将第addr位 置0

    // --- 步骤2:只有状态变了才动硬件 (高效!) ---
    if(temp != temp_old)
    {
        P0 = ~temp; // 取反,因为LED是低电平亮 (逻辑1 -> 硬件0)
        
        P2 = P2 & 0x1f | 0x80; // 打开 Y4 锁存器
        P2 &= 0x1f;            // 锁住数据
        
        temp_old = temp;       // 更新备份
    }
}

4. 主函数 (main.c)

#include <Init.h>
#include <Led.h>

void main()
{
    System_Init(); // 上电先灭掉所有无关外设
    while(1)
    {
        // 这里的 1 代表 enable,点亮第0个灯
        // 得益于 Led_Disp 里的 temp!=temp_old 判断,
        // 虽然在死循环里,但不会一直疯狂操作IO口,很稳定。
        Led_Disp(0, 1); 
    }
}

更多推荐