第五周过渡模拟2与Led模块

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

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

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

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. 硬件原理图解

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

  • 译码器 (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); 
     }
 }