初始单片机.md

1.如何将HEX文件烧录到单片机

STC-ISP

STC-ISP是一款单片机下载编程烧录软件,是针对STC系列单片机而设计的,可下载STC89系列、12C2052系列和12C5410等系列的STC单片机,使用简便。

  1. 思路:将电脑磁盘上已存在的文件通过串口的方式下发到单片机中。

  2. USB转串口驱动安装:CH340。

  3. 将单片机通过type-C数据线和电脑连接(注意是数据线,不是充电线。数据线一般都有圆形、方形、箭头三线标识)

  4. 连接完毕后,打开此电脑,右键 / 属性 / 设备管理器 / 端口,检查是否识别成功,如果成功则会显示端口号

    1. image-20230413204914698
  5. 下载STC-ISP软件

  6. 根据自己的单片机,在软件页面选择单片机型号:我的是STC89C52RC / STC89C52RC/LE52RC

  7. 确认串口号是否和第4步显示的串口号一致

  8. 打开程序文件,在上一节的示例项目中,我们已经建立了一个hex文件:moban.hex

    1. 初始单片机.md_第1张图片
  9. 点击下载编程,但是会显示正在检测目标单片机…(这说明还没有开始下载)

    1. 初始单片机.md_第2张图片
  10. 给单片机重新上电,对于我的单片机来说,是有一个开关,捎下去即可。重新上电后,发现从原先的两盏灯在亮变成了三盏灯亮起来(我们的代码中给了两盏灯低电平),说明操作成功。

    1. image-20230413211433349

2.单片机概述

概念(理解为PC电脑主板)

单片机(Single-Chip Microcomputer)是一种集成电路芯片

  • 把具有数据处理能力的中央处理器CPU、随机存储器RAM(也叫做数据存储器)、只读存储器ROM(也叫做程序存储器)

  • 多种I/O口中断系统定时器/计数器等功能

  • 可能还包括显示驱动电路脉宽调制电路(PWM)模拟多路转换器A/D转换器等电路

集成到一块硅片上构成的一个小而完善的微型计算机系统,在工业控制领域广泛应用。

单片机开发板与51芯片手册)

什么是发开发板?:基于单片机设计一些外接电路,模块以满足学习,日常调研等工作需求

  • 资源最少的单片机称为最小系统,如果以后做项目缺少什么外设,用杜邦线插到单片机引出的那几排插针(暴露出来的芯片引脚)上即可与单片机通信了。
  • 不管是8位的单片机开发,还是做arm单片机开发,都有对应的单片机手册。我们把它当做字典来看,不要全看。
  • 在手册中,嵌入式面试前你需要了解以下内容都是什么(先了解有个大致概念,反正记不住):
    1. 单片机的命名规则:在【手册1.5节】,STC 89 C 52 RC/RD+ - 40 I LQFP 44
      1. 涉及到:工作电压、ROM大小、RAM大小、工作频率、工作温度、封装类型、管脚数
    2. 一些特性:在【手册1.1节】,比如:
      1. 工作电压:5V、3V。用USB供电,或者几节1.5V干电池供电。
      2. 工作频率:就像我们电脑上的CPU,i7/i5/i3 有它们的主频一样。
      3. 应用程序空间:程序跑起来后允许的最大占用内存字节
      4. 集成的RAM:比如说有512B、1280B
      5. I/O口:①推荐使用P1/P2/P3/P4,因为它们是内部带着准上拉电路的I/O口,这意味着我们未来用这些口去接一些红外传感器、烟雾传感器等等外设时有了较高电压的保证。②P0口是开漏输出,作为总线扩展不用加上拉电阻,作为I/O口使用时,比如说控制继电器的时候,需要上拉电阻。
      6. ISP/IAP:在系统可编程/在应用可编程,意思是不用像古老的单片机一样在修改程序时需要把芯片拿下来修改,但是现在只需要通过串口就能把程序录入进去。
      7. 看门狗(缺省):STC89C52默认是没有用看门狗的,学32、ARM的时候会进行学习。看门狗一般用在程序比较复杂,逻辑不清楚了,需要重新给单片机上电运行的时候。比方说程序每隔10min就要去“喂”一次看门狗,那么如果程序跑飞了,就没有在固定的时间内运行“喂”看门狗的那几行代码,这条狗就会叫,狗一叫就重新启动系统,让程序恢复到正常的状态。
      8. 3个16位定时器/计数器
      9. 4路外部中断,下降沿触发中断,低电平触发中断
      10. UART:通用异步串行口(通用异步收发器),可以用定时器软件实现多个UART
      11. 工业级温度:-40℃~85℃。商业级温度:0℃ ~ 70℃。我的单片机是工业级的,用I表示。
      12. 封装方式:LQFP(领取富婆,芯片的引脚封装是呈现正方形的) ,STC公司实际上推荐的封装方式是LQFP 44,因为体积小,功耗低。

51单片机开发板原理图

在开发板生产前,硬件工程师会将电路的原理图画好,我们在进行单片机学习时,重要的两个参考对象就是:

  • 开发板原理图
  • 芯片手册
  • 阅读方式是先看子功能(比如按键key、LED等等),再看看这些电路图上电线的标识符。

  • 初始单片机.md_第3张图片

3.微处理器外部结构(概念)

注:摘自周佳社的微机原理,课程讲解的芯片是8086CPU,我们学习8051单片机时可以借鉴一下微机原理的知识,两者差不多,没必要单独去学8086。如果想知道两者区别,可以点击这个网页链接。

微处理器集总线

微处理器的外部结构表现为“数量有限的输入输出引脚”。这些输入输出引脚构成了“微处理器集总线”。输入输出引脚按其功能分就是三种,即数据引脚(数据总线DB)地址引脚(地址总线AB)控制引脚(控制总线CB)

  • 复用引脚:有些数据引脚和地址引脚是两用的,复用引脚需要经过系统总线形成电路进行分解。
  • 地址引脚的数目决定了CPU能够寻址的外部存储器空间的大小以及外部I/O地址空间。51单片机,有16根地址引脚,它能寻址的外部存储器空间是216地址单元(一个地址单元占用一个字节)= 64 KB,其中也包括外部I/O地址。

I/O端口寄存器

I/O端口寄存器是I/O接口中,用来实现I/O设备与CPU之间的数据交换的端口(通道),CPU给这个寄存器分配了一个地址,就是I/O端口地址。

  • CPU与智能设备的数据交换原理:通过三个端口寄存器始终保持握手状态

    1. 初始单片机.md_第4张图片
    2. 状态输入端口:CPU需要知道打印机的状态,判断它连接好了吗,墨水是否足够,装上纸了吗,是否还在打印等,这些状态信息,CPU是不知道的,但是I/O接口可以获取到,因此需要在接口电路中设计了一个能传送状态信息的通道,这个通道就是一个端口,称为状态输入端口。

    3. I/O端口地址:CPU需要读这个状态输入端口,因此需要给它分配一个地址,最后,这个状态信息沿数据总线传送给CPU。地址总线上的地址信号没有选中端口的地址时,即地址信号无效时,端口的输出端呈现 (高阻),三态表示通道未导通;当地址总线上的地址信号选中端口地址时,端口的输出端呈现二态,二态表示通道导通(若状态输入端口的输出端没有三态功能,需要自己设计,若没有这样的设计,那么会出现同一位数据总线在同一个时刻会出现两个以上的状态,即0和1,这种情况称为总线的竞争,轻则计算机瘫痪,重则冒烟(倒灌电流))。I/O端口地址就是状态输入端口的三态控制端。

    4. 读取状态与输出信息:若是0(设备没有准备好),则接着取状态,直到所有状态都准备好了;然后这时,CPU才可以通过数据总线给打印机输出数据。此时需要再设计一个通道,也就是一个I/O端口,称为数据输出端口,CPU的数据先输出给这一端口,由这个端口再给设备,同样,这个端口也必须有端口地址。

    5. 必要的命令输出端口:由于CPU很快而打印机很慢,所以在数据输出端口的输出端必须具有锁存功能(锁存器,就是D触发器),数据保存在那,让CPU干别的活去,但是在CPU没有输出数据给打印机之前,与设备相连的数据总线上是有数据残留的,设备不知道目前数据线上的数据是否有效,需要CPU告诉它,因此在I/O接口中还要设计一个端口,称为命令输出端口,CPU将数据总线上的某一根给命令输出端口,当命令输出端口的输出端将0传递给打印机时,打印机才知道它与数据输出端口之间的数据总线上的数据有效。


4.微处理器内部结构(概念)

四大部件

  1. ALU:算数、逻辑运算。比如最早出现的加法机就是通过复杂的逻辑电路实现的(你可以通过《穿越计算机的迷雾》这本书了解)。
  2. 工作寄存器:暂存少量数据,提高计算速度。分为数据寄存器和地址寄存器。我们学习51的目的之一就有寄存器
  3. 控制器:用户把编写的程序编译成机器语言(注意区别于符号语言和高级语言)之后,这些程序存放在外部存储器中的代码区之中。这些代码是各种指令的集合,每条指令显然都有各自的存放地址。CPU的控制器干的就是如何取指令的工作。它包括:
    1. 指令寄存器:指令暂存
    2. 指令译码器:指令翻译
    3. 程序计数器:指针自动偏移
    4. 控制逻辑部件:产生控制信号(用高低电平发送读写命令)
    5. 堆栈指针:提供堆栈数据的操作地址
    6. 微处理器状态字:PSW,运算后的数据不论是存在寄存器还是存储器当中,在CPU内部一定有存放运算状态的寄存器,称为处理器状态字寄存器,也称为标志字寄存器。比如你要判断运算结果是否溢出、是否是0、是否有符号、是否进位或者借位、是否是奇偶数、是否有辅助进位,等等。
  4. I/O控制逻辑:处理I/O 操作

5.现代微机系统(概念)

  • 初始单片机.md_第5张图片

四大组成部分

  1. 中央处理器(CPU)或微处理器:将控制器和ALU集成在一块半导体集成电路芯片上;由于控制器是整个计算机的中央指挥机关,因此半导体公司将其称为中央处理器,Central Processing Unit。
  2. 系统总线形成电路:当芯片的引脚变多,那么它的体积必然也要增大,因此半导体厂家在研制芯片时,使得芯片外围的部分引脚必然采用了复用引脚。例如数据-地址复用引脚不仅能当做数据引脚,也能当做地址引脚。但是,在实际应用中,要保证在总线上传送信息时,不同的信息要走专用的通道,此时复用了,怎么办?解决方法是:设计一部分电路,称为“系统总线形成电路”(如上图),把引脚都接到电路上来,然后把复用引脚上的复用信息分开,分开后使得各种信息走专用的总线(三总线)。(复用信息通过系统总线形成电路分开的原理是每条信息都有作用的时间,并且不同的信息有时序关系)。在上图中我们可以看出三种总线的信息传输有双向有单向。由于芯片直接带动负载的能力很弱,芯片中也不可能有放大器件,因此需要系统总线形成电路将微处理器集总线转化为系统集总线,简称为系统总线。同时,系统总线形成电路实际上就是微处理器的唯一需要驱动负载。挂接在系统总线上的器件,包括存储器、I/O,对于系统总线来说,都是一个负载。因此,系统中大量负载的驱动实际上交给了系统总线。
  3. 存储器子系统对于我们的51单片机来说,CPU内部自带一定容量的ROM和RAM
    1. ROM(Read Only Memory,只读存储器,程序存储器):掉电后不丢失,它所存放的是系统加载后的管理程序,ROM里的内容是用专用设备写入的。比如:电脑主板上有一块EPROM(Erasable Programmable Read Only Memory,紫外线可擦除可编程只读存储器),它里面存放的是BIOS(Basic Input/Output System,基本输入输出系统),计算机一加电,CPU先从BIOS内取程序代码执行,主要任务是①对RAM进行自检,检查每个地址单元的每个Bit是否都能写能读 & ②对主板上的I/O接口电路初始化 & ③从磁盘上读取操作系统到RAM。
    2. RAM(Randon Access Memory,随机存取存储器,数据存储器,内存):能读能写的存储器,用于存放程序在运行期间产生的数据等,掉电后信息丢失。
  4. I/O设备:地址总线上的地址有很多,CPU是通过执行指令来区分哪个地址的设备,到底是存储器还是I/O设备;一个I/O设备上只有一个控制引脚,所以需要将一把地址翻译成给I/O接口的一个命令,此时就需要I/O地址译码电路。如上图,一个简单的译码电路是用与非门完成的。

6.51的基本I/O口控制

关于单片机编程的那点事

  • 单片机编程是什么:人类要求单片机干具体的活,有点像提前写个小纸条贴冰箱上,告诉你女朋友煮鸡蛋不加水

  • 单片机执行程序怎么理解: 女朋友看到纸条后,照做!

  • 单片机能做什么事情:就是硬件的那些事,比如 I/O口供电,串口数据传输等。

  • 肉眼可见的引脚是什么:就是单片机芯片引脚通过电路线引出去的插针。方便人类对单片机I/O口操作(通过杜邦线接传感器),串口插东西。

  • 单片机CPU怎么找到I/O口:通过寻址,说白了有一些地址数据代表了某些I/O口,我们只需要在包含头文件之后拿来用就行了(具体怎么实现的不要着急了解,汇编的内容)。

    • 关于头文件:我们早就知道编译后会把程序中的include的头文件照搬过来和我们写的程序一起编译成二进制文件。那头文件里有啥啊?无非就是一些宏定义、函数声明、外部变量声明、构造类型声明等等。可以说头文件提前帮你做了很多事情。

    • 实际上,因为我们的示例程序中一开始就inlcude了"reg52.h"这个头文件,所以我们在用Keil完成编译后在main.c的下方会多出来一个reg52.h这个头文件,我们可以双击自行去查阅这个头文件。我们可以很容易找到这个头文件中有表示I/O口的地址常量的宏定义。从这里,我们又很容易看出51的指针变量占用1个字节,或者说用8位二进制数表示地址。

    • 初始单片机.md_第6张图片

I/O口结构(手册第4章)

  • 尽量用P1/P2/P3/P4,不要用P0
初始单片机.md_第7张图片
  • RC系列(CPU内部添加了RAM)的单片机新增了P4口:如果你想用P4口,那么就把手册中的代码拷贝到"reg52.h"中
初始单片机.md_第8张图片

sfr指令找到I/O口

sfr“指令”: 用来直接描述硬件地址,可以先理解成“一组IO口”中起始地址中的数据

代码形式例如: sfr P0 = 0x80;

如果想让P0口的7个针脚都输出低电平,那么代码就写成:P0= 0; 用的比较少。

sbit指令具体找到I/O口某个针脚

sbit“指令”:对应可位寻址空间的一个位(bit)。可以先理解成“一个IO口/针脚”的地址中数据

代码形式例如:sbit led2 = P3^6; 也就是我们找到了P3口的第6位(第6个位,即引脚P3.6)。

  • I/O口及其针脚的示意图:我们将来用一个字节来表示一个I/O口。这个I/O口中还有8个引脚,每个引脚占1bit。

  • 原理:将来我们只需要设置I/O口的每个针脚为0或1就行了,所以“一个IO口/针脚”的地址中数据就是一个二进制数。

  • 初始单片机.md_第9张图片

SFRF和SBIT不是ANSI C

sfr指令和sbit指令不是标准C,所以不认识sfr和sbit,C51单片机在底层上去解析了这两个指令。在头文件"reg52.h"中这两个指令都被使用了。

单片机的输入输出

  1. 输入:IO口的 输入 能 把外面东西拿回来。比如将传感器的out引脚用杜邦线链接到单片机的某个插针上。

    sbit IFfire = P1^0;        //让单片机的P1.0针脚接着火灾传感器
    int main()
    {
        int io_data;
        io_data = IFfire; // 给变量赋值,就是输入,或者理解为,这个针脚有数据啦,拿个变量来保存
        if(io_data == 0){
        }
        return 0;
    }
    
  2. 输出:IO口的 输出 能 把东西给外面。比如单片机的某个插针用杜邦线连接到继电器的in引脚上,能够控制继电器的通断。

    sbit IOPORT = P1^0; //让单片机的P1.0针脚接着某些执行器
    int main()
    {
        IOPORT = 1;// 被给予一个数值,引起IO口电压变化(一般1输出高电平,0输出低电平),来影响外接的电路
        return 0;
    }
    
  • 89C52比较简单粗暴,关于引脚输入还是输出的功能不需要配置, 但是比如说像Arduino Uno R3开发板就是要用pinmode()函数设置引脚的状态,比如输入、输出、输入上拉。STM32也需要配置引脚的状态。

7.关于LED的那点事(practice)

灯为什么会亮(给个低电平)

  1. 查看开发板原理图,找到LED:可以发现我的开发板一上电后,D4因为一头接地所以就会亮,而D5和D6因为一头悬空所以暂时不会亮

    • 初始单片机.md_第10张图片
  2. 用ctrl + F 的方式在原理图其他位置找到LED1以及LED2标号:记住:原理图中标号一样代表是同一根“电线”。

    1. 芯片手册很大的话,肉眼很难去找,因此要用ctrl + F的方式

灯的闪烁(给个延时)

  1. 思路:

    全局变量: sbit指令找到D5这个LED,引脚为P3^7  
    1. while死循环,重复执行D5的亮灭
        1.1 让D5亮
        1.2 延时,延时期间保持灯亮
        1.3 让D5灭
        1.4 延时,延时期间保持灯灭
    
  2. 延时多少呢?在没有学到定时器之前,我们用STC-ISP中的一个工具——软件延时计算器

    初始单片机.md_第11张图片
    • 我们单片机的晶振是11.0592MHz,不用改。
    • 输入定时的长度,单位比如说设置成ms,长度比如说设置成500。那么我们知道亮灭这个过程经过了1秒钟。
    • 8051指令集我们修改成Y1,因为Y1适用于STC89C系列(右侧方框有显示)
    • 点击生成C代码,复制代码。

    复制来的代码里面的nop()指令其实是包含在头文件intrins.h当中的。nop()是一个空操作,对应于汇编语言中的NOP语句。执行该函数,将占用1个机器周期的时间,常用于局部短暂延时。

  3. LED闪烁代码:

    #include "reg52.h"
    #include "INTRINS.H"
    sbit ledD5 = P3^7;   //对D5这个LED操作
    /* API1: 从STC-ISP的软件延时计算器中拷贝下来的函数,用于延时 */
    void Delay500ms();
    int main(void)
    {
        while(1){
            ledD5 = 0;
            Delay500ms();
            ledD5 = 1;
            Delay500ms();
        }
        return 0;
    }
    void Delay500ms()        //@11.0592MHz
    {
        unsigned char i, j, k;
    
        _nop_();
        i = 4;
        j = 129;
        k = 119;
        do
        {
            do
            {
                while (--k);
            } while (--j);
        } while (--i);
    }
    

8.按键控制灯(practice)

单片机怎么知道按键被按下了呢?

  1. 传统的方式(开关直接控制灯):按键不是传统理解的开关,第一点,传统的开关在按下后不能自动弹起;第二点,传统开关和灯是在电路的同一条干路或者支路上的。
    • 初始单片机.md_第12张图片
  2. 单片机的方式(按键间接控制灯):单片机中的按键能够自动弹起,并且我们单片机编程按键和灯是分开的两个电路。通过单片机作为媒介来处理数据,通过逻辑来实现按键控制灯。

​ 通过查阅开发板原理图:

初始单片机.md_第13张图片
  • 按键没被按下之前:支路“5V——R1——C3——GND”导通,节点KEY1相当于接了一个上拉电阻,因此KEY1处是高电平(1)
  • 按键被按下之后:支路“5V——R1——SW1——GND”导通,节点KEY1相当于接地,因此KEY1处是低电平(0)
  1. 经过搜索,KEY1被接在了芯片的引脚P2.1上。
    • 初始单片机.md_第14张图片
  2. 因此,只要不断用单片机识别引脚P2^1的电平状态,就能读取到按键SW1是否被按下

查询法按键控制灯

对于我的51单片机开发板来说:

  • 用引脚P2^1识别按键SW1是否被按下,0说明按键被按下,1说明按键没被按下。
  • 用引脚P2^0识别按键SW2是否被按下,0说明按键被按下,1说明按键没被按下。

按键的软件消抖

  • 示例代码(无软件消抖):用SW1控制D5,用SW2控制D6,功能是按下按键LED灯才会亮,松开按键LED灭。

    全局变量:
        sbit指令找到与SW1连接的引脚,为P2^1
        sbit指令找到与SW2连接的引脚,为P2^0
        sbit指令找到D5这个LED,引脚为P3^7
        sbit指令找到D6这个LED,引脚为P3^6  
    1. while死循环,按下按键LED灯才会亮,松开按键LED灭
        1.1 判断SW1是否为0
            1.1.1 如果是,则说明按下了
                1.1.1.1 令D5亮: ledD5 = 0;
            1.1.2 否则,则说明没按下
                1.1.2.1 令D5灭: ledD5 = 1;
        1.2 判断SW2是否为0
            1.2.1 如果是,则说明按下了
                1.2.1.1 令D6亮: ledD6 = 0;
            1.2.2 否则,则说明没按下
                1.2.2.1 令D6灭: ledD6 = 1;
    
    #include "reg52.h"
    sbit SW1 = P2^1;
    sbit SW2 = P2^0;
    sbit ledD5 = P3^7;
    sbit ledD6 = P3^6;
    int main(void)
    {
        /* 查询法按键控制灯,不妨用SW1控制D5,用SW2控制D6 */
        while(1){
            if(SW1 == 0){
                ledD5 = 0;
            }else{
                ledD5 = 1;
            }
            if(SW2 == 0){
                ledD6 = 0;
            }else{
                ledD6 = 1;
            }
        }
        return 0;
    }
    

存在的问题:有时候在工业环境中,因为抖动而非人为的按键,造成了灯亮和灯灭。比如说时间久了以后,LED的针脚出现了松动。

解决方法:软件消抖。原理:人按下按键是有个操作时间的,比方说0.1s,0.2s等等,但是因为抖动按下按键的操作时间会比人为操作要短,比方说0.01s ~ 0.05s。所以当我们不能判断按键的状态改变是因为抖动还是人为的时候,我们就等50ms再判断。因此,给查询法按键控制灯程序加一个延迟就可以消除抖动。

  • 修改后的代码(加入软件消抖):用SW1控制D5,用SW2控制D6,功能是按下按键LED灯才会亮,松开按键LED灭。

    #include "reg52.h"
    sbit SW1 = P2^1;
    sbit SW2 = P2^0;
    sbit ledD5 = P3^7;
    sbit ledD6 = P3^6;
    /* API1: 从STC-ISP的软件延时计算器中拷贝下来的函数,用于软件消抖 */
    void Delay50ms();
    int main(void)
    {
        /* 查询法按键控制灯,不妨用SW1控制D5,用SW2控制D6 */
        while(1){
            Delay50ms();
            if(SW1 == 0){
                ledD5 = 0;
            }else{
                ledD5 = 1;
            }
            if(SW2 == 0){
                ledD6 = 0;
            }else{
                ledD6 = 1;
            }
        }
        return 0;
    }
    void Delay50ms()        //@11.0592MHz
    {
        unsigned char i, j;
    
        i = 90;
        j = 163;
        do
        {
            while (--j);
        } while (--i);
    }
    

灯状态切换(I/O状态翻转)的两种思路

两种切换LED灯亮灭状态的思路:

  • 用两个按键控制一个LED:一个负责查询有没有按下“开机”按键,另一个负责查询有没有按下“关机”按键
  • 用一个按键控制一个LED:人用眼睛去看,灯亮着的时候你想关闭就按一下按键,灯灭的时候你想打开就按一下按键。

代码心得:

  1. 灯状态的切换实质上就是I/O状态的翻转,不仅用在本次测试代码中,在其他I/O状态切换中也十分常见。
  2. 写状态切换的代码时,我们有两个思路,一个比较蠢,一个比较明智。先说比较蠢的方法:对当前灯的状态进行判断,如果灯亮着,就把让灭掉;如果灯灭着,就把它亮起来。再说比较明智的做法:用逻辑非运算符 ! 对操作数取其反向状态
  • 示例代码(一个按键控制一个LED,50ms延时):用SW1控制D5,用SW2控制D6,功能是按下按键并松开后,切换LED状态,保持LED长亮或长灭。

    全局变量:
        sbit指令找到与SW1连接的引脚,为P2^1
        sbit指令找到与SW2连接的引脚,为P2^0
        sbit指令找到D5这个LED,引脚为P3^7
        sbit指令找到D6这个LED,引脚为P3^6  
    f1. 50ms延时函数
    1. 初始化D5状态: ledD5 = 0;
    2. 初始化D6状态: ledD6 = 0;
    3. while死循环,用按键切换LED状态
        3.1 50ms延时
        3.2 判断SW1是否为0
            3.2.1 如果是,
                3.2.1.1 切换D5的状态: ledD5 = !ledD5;
            3.2.2 否则,啥也不干
        3.3 判断SW2是否为0
            3.3.1 如果是,
                3.3.1.1 切换D6的状态: ledD6 = !ledD6;
            3.3.2 否则,啥也不干
    
    #include "reg52.h"
    sbit SW1 = P2^1;
    sbit SW2 = P2^0;
    sbit ledD5 = P3^7;
    sbit ledD6 = P3^6;
    void Delay50ms();
    int main(void)
    {
        /* 查询法按键控制灯,不妨用SW1控制D5,用SW2控制D6 */
        ledD5 = 0;
        ledD6 = 0;
        while(1){
            Delay125ms();
            if(SW1 == 0){
                ledD5 = !ledD5;
            }
            if(SW2 == 0){
                ledD6 = !ledD6;
            }
        }
        return 0;
    }
    void Delay50ms()        //@11.0592MHz
    {
        unsigned char i, j;
    
        i = 90;
        j = 163;
        do
        {
            while (--j);
        } while (--i);
    }
    

存在的问题:按下按键后灯可能亮一下就灭掉了,无法正确切换状态。

解决方法:

  1. 把延时的时间稍微加长,至少大于等于正常人按下按键的操作时间——0.1s,保证灯不至于在按下按键的操作时间内有多次状态切换。经过我个人的测试,这个时间在0.1s~0.2s都是可以接受的。设置成0.2s以上的延时的话,你可能要按的时间稍微长一点才会出现反应。当然如果你一直按着按键的话,那么就和我们之前灯的闪烁程序无异了(因为灯的闪烁实质上也是I/O状态翻转问题)。
  2. 选择用2个按键控制1个LED的方法,这样比较可靠,只需要给上50ms的软件消抖就行了。
  • 修改后的代码(一个按键控制一个LED,125ms延时):用SW1控制D5,用SW2控制D6,功能是按下按键并松开后,切换LED状态,保持LED长亮或长灭。

    #include "reg52.h"
    #include "INTRINS.H"
    sbit SW1 = P2^1;
    sbit SW2 = P2^0;
    sbit ledD5 = P3^7;
    sbit ledD6 = P3^6;
    /* API1: 从STC-ISP的软件延时计算器中拷贝下来的函数,用于LED状态切换时的延时 */
    void Delay125ms();
    
    int main(void)
    {
        /* 查询法按键控制灯,不妨用SW1控制D5,用SW2控制D6 */
        ledD5 = 0;
        ledD6 = 0;
        while(1){
            Delay125ms();
            if(SW1 == 0){
                ledD5 = !ledD5;
            }
            if(SW2 == 0){
                ledD6 = !ledD6;
            }
        }
        return 0;
    }
    
    void Delay125ms()        //@11.0592MHz
    {
        unsigned char i, j;
    
        i = 225;
        j = 28;
        do
        {
            while (--j);
        } while (--i);
    }
    

标志位优化按键控制灯

应用场景:当业务逻辑比较复杂时,我们不希望在流程控制语句中直接修改I/O状态,而是先记住某个I/O口的状态,然后在需要修改的地方对状态进行修改。

这种方法在我们学习C语言的时候也经常使用。我通常用flag变量来表示标志位。

单片机中的方法:我们使用标志位时不希望看到0和1,因为代码的可阅读性不好,所以我们一开始给0和1分别用符号常量ON和OFF表示(宏定义)。

  1. 用标志位实现代码8.2.1(按键软件消抖):

    #include "reg52.h"
    
    #define LED_ON  0
    #define LED_OFF 1
    
    sbit SW1   = P2^1;
    sbit SW2   = P2^0;
    sbit ledD5 = P3^7;
    sbit ledD6 = P3^6;
    
    /* API1: 从STC-ISP的软件延时计算器中拷贝下来的函数,用于软件消抖 */
    void Delay50ms();
    
    int main(void)
    {
        char D5flag = LED_OFF;
        char D6flag = LED_OFF;
        /* 查询法按键控制灯,不妨用SW1控制D5,用SW2控制D6 */
        while(1){
            /* 软件消抖 */
            Delay50ms();
            /* 按下按键LED灯才会亮,松开按键LED灭。 */
            if(SW1 == 0){
                D5flag = LED_ON;
            }else{
                D5flag = LED_OFF;
            }
            if(SW2 == 0){
                D6flag = LED_ON;
            }else{
                D6flag = LED_OFF;
            }
            /* 标志位判断 */
            if(D5flag == LED_ON){
                ledD5 = 0;
            }else{
                ledD5 = 1;
            }
            if(D6flag == LED_ON){
                ledD6 = 0;
            }else{
                ledD6 = 1;
            }
        }
        return 0;
    }
    
    void Delay50ms()        //@11.0592MHz
    {
        unsigned char i, j;
    
        i = 90;
        j = 163;
        do
        {
            while (--j);
        } while (--i);
    }
    
  2. 用标志位实现代码8.2.2(I/O状态翻转):

    #include "reg52.h"
    
    #define LED_ON  0
    #define LED_OFF 1
    
    sbit SW1   = P2^1;
    sbit SW2   = P2^0;
    sbit ledD5 = P3^7;
    sbit ledD6 = P3^6;
    
    /* API1: 从STC-ISP的软件延时计算器中拷贝下来的函数,用于LED状态切换时的延时 */
    void Delay125ms();
    
    int main(void)
    {
        char D5flag = LED_OFF;
        char D6flag = LED_OFF;
        ledD5 = LED_OFF;
        ledD6 = LED_ON;
        /* 查询法按键控制灯切换LED状态,用SW1控制D5,用SW2控制D6 */
        while(1){
            /* 延时,包含软件消抖以及保证灯不至于在按下按键的操作时间内有多次状态切换 */
            Delay125ms();
            /* 按下按键并松开后,切换LED状态,保持LED长亮或长灭。 */
            if(SW1 == 0){
                D5flag = ! D5flag;
            }
            if(SW2 == 0){
                D6flag = ! D6flag;
            }
            /* 标志位判断 */
            if(D5flag == LED_ON){
                ledD5 = 0;
            }else{
                ledD5 = 1;
            }
            if(D6flag == LED_ON){
                ledD6 = 0;
            }else{
                ledD6 = 1;
            }
        }
        return 0;
    }
    
    void Delay125ms()        //@11.0592MHz
    {
        unsigned char i, j;
    
        i = 225;
        j = 28;
        do
        {
            while (--j);
        } while (--i);
    }
    

你可能感兴趣的:(单片机)