重温大学经典51单片机

95 阅读40分钟

一.核心板分析

1.命名规范

2.封装类型与引脚介绍

2.1封装类型

LQFP(Low-profile Quad Flat Package):

PDIP(Plastic Dual In-line Package)

2.引脚介绍

通用引脚(39个)时钟(2个)电源(2个)复位引脚(1个)复用
P0.0-P0.7P1.0-P1.7P2.0-P1.7P3.0-P1.7P4.0-P4.6XTAL1XTAL2VCCGNDRSTINT0(P3.2) 外部中断0INT1(P3.2) 外部中断1INT2(P4.3) 外部中断2INT3(P4.2) 外部中断3RXD(P3.0) 串口中断TXD(P3.1) 串口中断RD(P3.6) 外部数据存储器的读选能信号WR(P3.7) 外部数据存储器的写选能信号ALE(P4.5)T2(P1.0)T2EX(P1.1)

3.最小系统

单片机的最小系统包括电源、复位电路以及晶振。

4.引脚特性

引脚分析:

1.我们知道这款单片机的引脚默认为高电平,也就是锁存器的端口默认是1,经过反相器之后它会变成0,这样,我们的mos就不会导通,我们的引脚就不会接地,那默认就会是高电平,但是由于上拉电阻比较大,所以我们的电流就很小,为什么我们的电阻要很大?因为我们的引脚是可输入的(这个时候端口锁存器必须是1,因为如果是0的话,mos导通,外部引脚直接接地,就不可以设置高电平了,只会一直是低电平),若外部输入想输入低电平,但是我们由于是一个弱上拉,此处输入的引脚会被上方强制拉高。

2.当我们给其中一个引脚设置为0时,短接另外一个引脚,另外一个引脚就很容易被拉低,因为电阻很大,另外那头默认是1的情况下,接通的那头上拉电阻分压很多,所以就会被拉低。

综上所述,51单片机引脚具有弱上拉且容易被拉低的特性,这点在后面的数码管和矩阵按键时都会由应用到。

二、LED点灯大师

1.硬件原理图

原理分析:

可以看到,我们的LED的共阳极接入了一个三极管,当基极导通时,电源由集电极导通发射极,对应的led控制由P0控制,所以我们只需要控制P0这个寄存器即可。我们可以通过各种手段来控制LED的亮灭:

控制方式1:直接控制,通过操作gpio口控制led

控制方式2:通过按键控制,检测按键后并控制led(见后面章节)

控制方式3:通过外部中断控制,配置外部中断响应方式,在中断程序中控制led(见后面章节)

控制方式4:通过urat中断控制,收发特定的串口指令来控制led(见后面章节)

2.流水灯实现

这里我们只实现第一种控制方式,我们实现一个led的流水灯,每个灯之间延时1秒。

功能分析:

1.打开led使能

2.P0寄存器置为0xf7,这时点亮led1,延时1秒后点亮2,依次循环直到最后一个灯亮之后在将位置重置到第一颗灯。

代码分析:

1.由于流水灯之间需要延时,我们需要一个延时函数,通过翻阅资料我们可以得知。

2.需要设置一个算法,每次delay后,我们需要将高电平向右移位,其他位置为0即可。

代码实现:

1.延时函数的实现

/**
* 延时1ms的函数,我们会把代码抽取出来,这个计算跟时钟频率相关 
*  1个机械周期由12个时钟周期,1个指令周期由多个机械周期,时钟周期是晶振源的一个脉冲周期
* 比如我们这里的时11.0592Mhz,1个指令周期为0.09us,1个机械周期约为1.08us,下面的代码翻译成
* 汇编代码就差不多为1ms
**/
void Com_Util_Delay1ms(u16 count) //@11.0592MHz
{
    u8 i, j;
    while (count > 0) {
        count--;
        _nop_();
        i = 2;
        j = 199;
        do {
            while (--j);
        } while (--i);
    }
}

2.逻辑算法的实现

#include <STC89C5xRC.H>
#include "Com_Util.h"
#define LED_EN P34
#define LED_PIN P0

/**
 * led闪烁函数
 */
void blink_led()
{
    u8 i = 1;
    LED_EN  = 1; // 打开LED 设置为高电平,三极管导通,LED阳极接入高电平

    while (1) {
        LED_PIN = ~i;                 // 0000 0001 ->1111 1110 这个对应P00
        Com_Util_Delay1ms(1000); // 延时500ms
        i <<= 1;                 // 左移一位,i=2-> 0000 0010
        i = 1;
    }
}

三.数码管

1.硬件原理图

原理分析:

我们可以看到每个数码管都是由8个led组成,那总共由8个单个数码管,如果要单独控制某个led,则需要64个引脚,但是,我们的数码管的设计并不会这么呆板,我们数码管都是共阴极的,简化图如下:

从图中得知:我们每单个数码管的相同位置的led灯的阴极都使用同一个引脚,一共有8个单个数码管DIG1-DIG8,所以只需要8个引脚来控制阴极,那阳极怎么设计呢?我们是通过DIG1-DIG8中的每个led都会共1个阳极,这样我们也只需要由8个引脚来控制显示具体的某个位置。比如我们要在DIG8上显示1,阳极的8个引脚需要设置:0110 0000 ,阴极的设置为1001 1111,这样DIG8的bc就会导通并且显示,那如果要在DI7显示1,DIG8显示2,怎么办,我们需要在DIG7的位置显示1,然后在DIG8显示2即可,然后循环执行。对于共阴极的数码管,我们需要在1次显示某个位置,那么,我们可以使用38译码器来解决这个问题。

38译码器的原理

输入端输出端电源芯片选择输入
A0-A2Y0-Y7VCC/GNDCS1-CS3

我们通过芯片手册和原理图可以得知,我们需要给CS1-CS3分别为高 低 低,才可以通过设置A0-A2来控制输出引脚的高低电平来控制显示某个数码管。

245驱动芯片

我们知道51单片机的gpio口的上拉为弱上拉,所以它的电流不足以驱动这么多数码管的亮起,所以我们在其接入单片机的阳极增加一个XD74HC245驱动芯片,它是一个双向8路的总线收发器,用以提升电流。我们查看芯片手册得到下图

所以我们可以设置OE为0(P36为低时,其为低电平),DIR为H,来设置其使能。

2.数码管显示实现

1.单个数码管显示

我们需要在最后一个数码管上显示数字1

功能分析:

1.打开38译码器和245驱动的使能

2.38译码器的共阴极部分设置最后一个为0,245驱动芯片接入的部分BC对应的引脚设置为高。

代码分析:

1.数码管初始函数,用于初始化38和245

2.显示单个数码管,需要设置显示哪一个和具体的问题;

代码实现:


/**
 * @brief 初始化函数
 */
void digit_init(void)
{
    P36 = 0; // 置38译码器的CS1引脚为高电平和245驱动芯片的en为低电平
    P34 = 0; // 关闭LED开关
}
// 数字0-9的编码
static u16 codes[10] = {
    0x3F, // 0
    0x06, // 1
    0x5B, // 2
    0x4F, // 3
    0x66, // 4
    0x6D, // 5
    0x7D, // 6
    0x07, // 7
    0x7F, // 8
    0x6F  // 9
};
/**
 * @brief 显示单个数字
 * @param position 显示位置,0-7
 * @param value 显示的值,0-9
 */
void digit_show_sigle(u8 position, u8 value,u8 flag)
{

    DIGIT_SHOW_NUM = 0; // 清除显示的值
    // p13 p14 p15; 共阴极处理
    P1 &= 0xc7;             // 清除p13 p14 p15  11000111
    position <<= 3;         // 左移三位
    P1 |= position;         // 加上要显示的位置
    if(flag){
          DIGIT_SHOW_NUM = value; // 设置显示的值
    }else{
        DIGIT_SHOW_NUM = codes[value]; // 设置显示的值
    }
  
}

2.多个数码管显示

我们需要在最后一个数码管上显示数字12345678

功能分析:

1.在第一个位置上显示"1"

2.在第二个位置上显示"2"

3.依次循环显示"3","4","5","6","7","8"

代码分析:

1.实现一个函数,对于要显示的数字做取出单个后,并放置到要显示的数组当中;

2.对数组进行遍历,挨个调用单个显示。

代码实现:

#include "digit.h"
#define DIGIT_SHOW_NUM P0

/**
 * @brief 初始化函数
 */
void digit_init(void)
{
    P36 = 0; // 置38译码器的CS1引脚为高电平和245驱动芯片的en为低电平
    P34 = 0; // 关闭LED开关
}
// 数字0-9的编码
static u16 codes[10] = {
    0x3F, // 0
    0x06, // 1
    0x5B, // 2
    0x4F, // 3
    0x66, // 4
    0x6D, // 5
    0x7D, // 6
    0x07, // 7
    0x7F, // 8
    0x6F  // 9
};
/**
 * @brief 显示单个数字
 * @param position 显示位置,0-7
 * @param value 显示的值,0-9
 */
void digit_show_sigle(u8 position, u8 value,u8 flag)
{

    DIGIT_SHOW_NUM = 0; // 清除显示的值
    // p13 p14 p15; 共阴极处理
    P1 &= 0xc7;             // 清除p13 p14 p15  11000111
    position <<= 3;         // 左移三位
    P1 |= position;         // 加上要显示的位置
    if(flag){
          DIGIT_SHOW_NUM = value; // 设置显示的值
    }else{
        DIGIT_SHOW_NUM = codes[value]; // 设置显示的值
    }
  
}

static u8 value_array[10]; // 存放要显示的数字0-9 对应的编码
/**
 * @brief 遍历数字放到数组中去
 * @param value 要显示的数字
 */
static void entry_value(u32 value)
{
    u8 i = 7;
    u8 j ;
    // 先清空数组
    for (j = 0; j < 8; j++) {
        value_array[j] = 0x00;
    }

    // 如果是0,直接在最后一位显示0
    if (value == 0) {
        value_array[7] = codes[0];
        return;
    }

    while (value > 0) {
        value_array[i] = codes[value % 10]; // 12345678%10=8
        i--;
        value /= 10; // 12345678/10=1234567
    }
}
/**
 * @brief 开启动态扫描 在函数中循环中调用
 * @param value 要显示的数字
 */
static void entry_show()
{
    u8 i;
    for (i = 0; i < 8; i++) {
        digit_show_sigle(i, value_array[i],1); // 显示某个数字
    }
}
/**
 * @brief 显示多个数字
 * @param position 显示位置,0-7
 * @param value 显示的值,12345678
 */
void digit_show_multi(u32 value1)
{
    entry_value(value1); // 存放要显示的数字
    // while (1) {
    //     entry_show(); // 显示数字
    // }
}

void digit_show_refresh()
{
     entry_show();
}

四.按键部分

1.硬件原理图

原理分析:

可以看到,当我们按下按键时,与之对应的引脚就会被接地,从而被拉低,这是由于51单片高低引脚相接时,高的一段容易被拉低,因为弱上拉的缘故,这个前面关于单片机的引脚特性也介绍过。

2.按键实现控制led灯

我们通过控制sw1 sw2 sw3 sw4 的1次按压弹起来控制led1-led4的灯的亮灭

功能分析:

1.检测开关的1次按起

2.操作对应的led对应的引脚;

代码分析:

1.led使能开启;

  1. 检测开关是否按起;

3.操作对应的led灯

代码实现:

#include "key.h"
#include "Com_Util.h"

#define LED_EN P34 // led使能脚

#define KEY_1  P42 // 按键1
#define KEY_2  P43 // 按键2
#define KEY_3  P32 // 按键3
#define KEY_4  P33 // 按键4

#define LED_1  P00 // LED1
#define LED_2  P01 // LED2
#define LED_3  P02 // LED3
#define LED_4  P03 // LED4

static void led_on()
{

    if (LED_EN == 0) {
        LED_EN = 1; // 打开LED
    }
}

static bit check_key(u8 key)
{

    switch (key) {
        case 1:
            if (KEY_1 == 0) {           // 按键按下
                Com_Util_Delay1ms(10);  // 消除前面的抖动
                if (KEY_1 == 0) {       // 再次检测按键是否被按下
                    while (KEY_1 == 0); // 等待松开
                    // 这里已经松开了
                    return 1;
                }
            }
            break;
        case 2:
            if (KEY_2 == 0) {           // 按键按下
                Com_Util_Delay1ms(10);  // 消除前面的抖动
                if (KEY_2 == 0) {       // 再次检测按键是否被按下
                    while (KEY_2 == 0); // 等待松开
                    // 这里已经松开了
                    return 1;
                }
            }
            break;
        case 3:
            if (KEY_3 == 0) {           // 按键按下
                Com_Util_Delay1ms(10);  // 消除前面的抖动
                if (KEY_3 == 0) {       // 再次检测按键是否被按下
                    while (KEY_3 == 0); // 等待松开
                    // 这里已经松开了
                    return 1;
                }
            }
            break;
        case 4:
            if (KEY_4 == 0) {           // 按键按下
                Com_Util_Delay1ms(10);  // 消除前面的抖动
                if (KEY_4 == 0) {       // 再次检测按键是否被按下
                    while (KEY_4 == 0); // 等待松开
                    // 这里已经松开了
                    return 1;
                }
            }
            break;
        default:
            break;
    }
    return 0;
}

bit check_key1()
{
    return check_key(1);
}

void turn_led1()
{
    LED_1 = ~LED_1;
}
void turn_led2()
{
    LED_2 = ~LED_2;
}
void turn_led3()
{
    LED_3 = ~LED_3;
}
void turn_led4()
{
    LED_4 = ~LED_4;
}

bit check_key2()
{

    return check_key(2);
}

bit check_key3()
{

    return check_key(3);
}

bit check_key4()
{

    return check_key(4);
}

五.矩阵按键

1.硬件原理图

原理分析:

通过原理图我们可以看出,横向的开关都有一个GPIO口控制,每一列开关都由一个gpio口控制,我们检测某一个开关是否按下,由于51单片机很容易被接地拉低,所以只要设置横向为0,检测对应列的gpio口是否为低电平即可。

2.矩阵按键实现控制数码管数字

我们做一个简单的功能当我们按下矩阵按键5-20时,数码管显示对应的数字。

功能分析:

1.矩阵按键检测,可以参考前面的单个按键检测;

2.数码管的显示,参考前面的多个数字显示;

代码分析:

1.引入数码管头文件,并且初始化数码管;

2.判断按键是否按下,返回对应特定的数字;

3.调用数码管接口点灯。

代码实现:


#include <digit.h>
#include "key_max.h"
#include "Com_Util.h"
#define KEY_MAX_PIN P2 // 矩阵按键引脚

void key_max_init(void)
{
    digit_init(); // 调用数码管初始化函数
}
static u8 s_array[4] = {
    0xfe, // 1111 1110
    0xfd, // 1111 1101
    0xfb, // 1111 1011
    0xf7  // 1111 0111
};
/**
 * 检测按键
 */
static u8 check_key_max(void)
{

    u8 i;
    for (i = 0; i < 4; i++) {
        P2 = s_array[i]; // 1111 1110 P20=0
        if (P24 == 0) {
            Com_Util_Delay1ms(10);
            if (P24 == 0) {
                while (P24 == 1); //检测按键弹起
                return 5 + (i * 4);
            }
        }
        if (P25 == 0) {
            Com_Util_Delay1ms(10);
            if (P25 == 0) {
                while (P25 == 1);
                return 6 + (i * 4);
            }
        }
        if (P26 == 0) {
            Com_Util_Delay1ms(10);
            if (P26 == 0) {
                while (P26 == 1);
                return 7 + (i * 4);
            }
        }

        if (P27 == 0) {
            Com_Util_Delay1ms(10);
            if (P27 == 0) {
                while (P27 == 1);
                return 8 + (i * 4);
            }
        }
    }
    return 0; // 没有按键按下
}

void show_key_max()
{

    while (1) {
        u8 key = check_key_max(); //检测按键
        if (key) {
            digit_show_multi(key); // 矩阵按键测试,按下按键,数码管显示对应的数字
        }
        digit_show_refresh(); // 数码管刷新
    }
}

六.简单的蜂鸣器

简介

蜂鸣器是一种能够发出声音的电子元器件,常用于报警、提示和音频信号输出等场景。其内部结构如下图所示。

当电流通过线圈时会产生电磁场,电磁场与永磁体相互作用,从而使金属膜产生震动而发声。为使金属膜持续震动,蜂鸣器需要使用震荡电路进行驱动。有些蜂鸣器元件内部自带震荡驱动电路,这种蜂鸣器叫做有源蜂鸣器(Active Buzzer,自激式蜂鸣器);而有些则不带震荡驱动电路,这种蜂鸣器叫做无源蜂鸣器(Passive Buzzer,它激式蜂鸣器)。我们下面使用的是无源蜂鸣器。

1.硬件原理图

原理分析

通过原理图可以分析得出,当我们P4.6为高电平时,三极管就会导通,蜂鸣器就会接地,当其为低电平时,蜂鸣器就会通电。由于电生磁,就会来回不停的对金属膜进行震荡,那为什么我们需要在加一个二极管呢?这是因为当下方三极管不导通时,由于电感的“来拒去留"的特性,我们的蜂鸣器接到三极管的那端会产生大电压,如果没有二极管,可能就直接直接通过三极管释放了,有可能导致三极管击穿。

2.应用蜂鸣器

我们在按下矩阵按键的同时,不仅在数码管上显示对应的按键数字,同时也让蜂鸣器发出声音0.1s。

功能分析:

1.让蜂鸣器发出声音;

2.矩阵按键的同时应用蜂鸣器

代码分析:

1.给蜂鸣器发出500hz的方波,那么1次高低转化的就是0.002s,如果我们需要0.1s,那我们只需要震荡100次即可。我们我们增加一个这样的函数即可。

2.在上面矩阵按键的代码上加上蜂鸣器发生函数。

代码实现:

#include <STC89C52RC.h>
#include "Com_Util.h"
#define BUZZER P46
/**
 * @brief 蜂鸣器响0.1s
 *
 */
void buzzer_on(void)
{
    //这里会有50HZ 0.1s的响声
    u16 count = 100;
    while (count) {
        BUZZER = ~BUZZER;
        Com_Util_Delay1ms(1);
        count--;
    }
}

在矩阵按键代码中调用

void show_key_max()
{

    while (1) {
        u8 key = check_key_max(); //检测按键
        if (key) {
            digit_show_multi(key); // 矩阵按键测试,按下按键,数码管显示对应的数字
            buzzer_on(); // 蜂鸣器响声
        }
        digit_show_refresh(); // 数码管刷新
    }
}

七.点阵LED

1.硬件原理图

原理分析:

通过原理图我们可以得出,点阵LED其实也就是8*8的led灯,那我们就很容易想到前面学到使用38译码器的方式来节省引脚,这里我们换一个595的芯片,他也是可以通过3个引脚来控制其8个引脚的输出,我们将它接到晶振led每一行上的阳极上,阴极接到单片机的P0的8个引脚上。这样我们就可以按照前面数码管的形式一样通过逐行扫描来控制显示。

595芯片介绍

作用:节省51单片机芯片引脚

控制原理:

1.595引脚介绍

VCCSERG#RCKSCKSCLR#QH'QA-QH
电源串行输入输出使能端(1导通)存储寄存器时钟移位寄存器时钟移位寄存器清零端串行数据输出管脚输出

2.工作原理介绍

以为寄存器的输入受移位寄存器始终的上升沿控制,而存储寄存器的输入是由RCK的上升沿控制,当RCK是上升沿时,会把移位寄存器的内部数据全部加载到存储 寄存器并且并行输出。通过原理图可以看出,我们可以通过SER、SCK、RCK、G#四个引脚来控制595的输出。它来控制点阵LED的共阳极部分,刚好也是8行8个引脚。

2.点阵led的简单应用

我们需要通过操作595芯片和51单片机的引脚实现如下效果:

代码分析:

1.操作595串行输入、移位寄存器以及存储寄存器的操作来并行输出操作点阵led的阳极,来操作显示的行;

2.配合行显示来操作接入单片机一端的阴极引脚;

代码实现:

#include "led_max.h"
#include <STC89C5xRC.H>
#include "Com_Util.h"
#define SER            P10 // 595 SER输入 阳极
#define RCLK           P11 // 595 RCLK 输出时钟
#define SCLK           P12 // 595 SCLK 移位时钟
#define LED_MAX_595_EN P35 // 阴极引脚
#define LED_MAX_PIN    P0  // 阴极引脚 这个也接入了前面的led
#define LED_EN         P34 // led使能脚
void blink_led_max_init()
{
    LED_EN         = 0; // 关闭前面的led
    LED_MAX_595_EN = 0; // 打开595输出使能
}

void blink_led_max()
{
    u8 j;

    for (j = 0; j < 8; j++) {
        LED_MAX_PIN = 0xff; // 关闭显示

        if(j==0){
            SER=1;
        }else{
            SER=0;
        }
        SCLK = 0; // 输出低电平
        SCLK = 1; // 拉高时钟

        RCLK = 0; // 拉低时钟
        RCLK = 1; // 拉高时钟
          // 执行1次触发输出到595输出引脚为1000 0000
        LED_MAX_PIN = ~((1) << j); // 0000 0001   -> 1111 1110
    }
}

八.中断系统

1.中断系统概述

中断系统是单片机用于处理外部紧急事件的一种机制。中断系统工作的大致流程如下图所示:当CPU正在处理某项任务时,外部发生了某个紧急事件,此时CPU会暂停当前的工作,转而去处理这个紧急事件,处理完之后,再回到原来被中断的位置,继续处理原来的工作。

2.中断源及其分类

中断源是指能够引发中断的事件。 STC89C52RC共有8个中断源,8个中断源可分为3类,3个类别分别是外部中断、定时器中断、串口中断,下面简要介绍每种类型。

STC89C52RC共有8个中断源,8个中断源可分为3类,3个类别分别是外部中断、定时器中断、串口中断,下面简要介绍每种类型。

2.1外部中断

外部中断是指由单片机外部的紧急事件触发的中断,通过向单片机的特定引脚发送特定的信号触发。STC89C52RC共提供了4个外部中断引脚,分别是INT0,INT1,INT2,INT3,如下图所示。

51单片机的外部中断支持两种触发方式,分别是低电平触发下降沿触发

2.2 定时器中断

定时器中断是指由单片机内部的定时器触发的中断。

定时器是大多数单片机都具备的一个功能模块,用于实现定时任务。其用法是,设置一个定时值,然后开始计时,待计时结束后,触发相应的定时器中断,开发者可以在中断服务程序中编写定时任务的逻辑。

STC89C52RC共有三个定时器,分别是Timer0、Timer1、Timer2,每个定时器都有一个相对应的中断。

2.3 串口中断

串口中断是由单片机串口触发的中断。

串口是单片机用于收发数据的重要接口之一,当单片机通过串口接收到数据或者发送完数据后都会触发相应的中断。

STC89C52RC的串口引脚为TxD和RxD,其中TxD用于发送数据,RxD用于接收数据,如下图所示。

3.中断标志位

中断标志位用于标识某个中断是否发生,每个中断源都有一个与之对应的中断标志位。当某个中断发生时,相应的中断标位就会置为1,当CPU检测到标志位时,就会处理相应的中断。当中断处理完毕后,中断标志位需要复位(置0),以便接收下一次中断,有些中断源的标志位,会在CPU处理完中断后,自动复位,而有些则需要开发者手动复位,在使用中断时,需要注意查看手册说明。

4.中断服务程序

中断服务程序是指用于处理中断的一段代码, 当中断发生时,CPU就会暂停当前程序的执行,转而执行对应的中断服务程序,处理完中断后再恢复到原来的程序。

STC89C52RC共有8个中断源,分为4个外部中断、3个定时器中断和1个串口中断,开发者可以为每个中断源声明相应的中断服务程序,中断服务程序的声明语法如下。

5.中断优先级

STC89C52RC共有四个中断优先级,每个中断源都可以单独设置优先级。若多个中断同时发生,优先级高的会被优先处理;若两个中断的优先级相同,则根据其中断号决定处理顺序,中断号越小越优先。

除此之外,高优先级的中断还可以打断低优先级的中断,也就是说当CPU正在处理一个中断时,又发生了另外一个优先级比它还高的中断,此时CPU会暂停原来中断的服务程序,转而去处理这个高优先级的中断,处理完之后,再回到原来低优先级的中断服务程序。这个机制叫做中断嵌套,STC89C52RC支持两级中断嵌套。

6.使用外部中断0控制led闪烁

功能代码分析:

1.开启外部中断0 (下降沿触发,触发前需要对中断进行配置)

2.在中断服务程序中控制led的闪烁

我们通过翻阅资料得知,51单片机的中断的总开关寄存器位EA,设置高即可开启,同时中断0的开启寄存器是EX0,同时它的触发方式通过XICON(Auxiliary Interrupt Control,辅助中断控制)寄存器中、TCON( Timer 0 and 1 Control 寄存器控制,他们的对应寄存器位说明如下:

中断的控制的数字逻辑部分如下图

所以我们除了开启中断容许EA=1 ,EX1=1之外,需要通过TCON中的IT0设置为1(下降沿触发)

代码实现

#include "dri_int0.h"
#define INT0_PIN  P32 // 中断0的引脚

#define INT       EA  // 全局中断标志位
#define INT0      EX0 // 中断0标志位
#define INT0_MODE IT0 // 中断0模式

#define LED_EN P34 //led使能脚
#define LED_PIN P0 //8个led引脚
void init_int0(void)
{
    INT  = 1; // 打开总中断
    INT0 = 1; // 打开中断0
    IT0 = 1; // 设置中断0为边沿触发模式
    LED_EN = 1; // 打开led
}
void INT0_ISR(void) interrupt 0
{
    //按键SW4接的是P32引脚,也是中断0的复用引脚,按下时触发中断0
    P00 = ~P00; // 翻转led
}

7.定时器中断实现流水LED灯

功能代码分析:

1.定时器工作模式配置;

2.定时器的开启;

3.定时器计数方式的选择;

4.在定时器的中断服务程序中操作led灯。

定时器工作模式

是通过TMOD这个寄存器中的CT这位来控制,它可以设置我们是用计数/定时的脉冲获取方式:

我们通过定时器的结构图可以看出,当CT为低电平时,它的计数方式是由系统时钟提供,而设置为高电平时是由外部引脚控制。

定时器0的开启

通过我们上面的图可以看到,定时器0的开关control主要靠TR0和GATE两个位决定,如果开关是1,则TR0必须是1,并且GATE是0或者INT0为1,我们通过设置TR0为高 GATE 为低来开启定时器0,GATE位是由上面的TMOD控制,而TR0是由TCON中的位控制

定时器计数方式

我们前面可以看到,当我们开启定时器后,TL0和TH0就开始计数,他们是两个8位的寄存器,用来存储脉冲计数器的数值,他们的工作方式有四种,是由M0和M1控制的。

M0M1工作方式
00工作模式0(13位)
01工作模式1(16位)
10工作模式2(8位自动重载)
11工作模式3(双8位)

下面我们分别对这几种工作方式进行讲解。

工作模式0:

该模式下的脉冲计数器共有13位,最大计数为8192。如下图所示,TL0和TH0为两个8位寄存器,用于存储脉冲计数器数值,该模式下TL0只用到了低5位。

工作模式1:

该模式的脉冲计数器共有16位,最大计数为65536。如下图所示,TL0的8位和TH0的8位都用到了。

工作模式2:

前两种模式,一次定时完毕后,如需再次定时,需要开发者重新为脉冲计数器设定初始值。而该模式可以在脉冲计数器溢出时,自动重新设置初始值,很适合用于执行周期性任务。

该模式下,只用TL0寄存器用于存储脉冲计算器数值,TH0则用于存储脉冲计数器的初始值,每次TL0溢出之后, 都会自动将TH0的值重新装入TL0。

工作模式3:

该模式下,TL0和TH0分别用作一个8位脉冲计数器,如果需要使用两个8位定时器可使用该模式。

那么我们对于如何选用这里的工作模式呢?我们第二章点灯大师的延时函数里学到,默认的频率为11.0592Mhz,1个脉冲周期为1000 000/(11.0592 1000 000)=0.09us, 如果我们想定时1毫秒,则差不多需要1 000 /0.0912=925个计时脉冲,为啥要除以12?因为我们选择了12TMode。如果我们选择工作模式2,那我们初始值就要设置微65536-925=64611,那TL0和TH0的设置TL0=(64611&0xff),TH0=64611<<8移到高位即可。然后在定时器中断程序中每次都要给其赋1次初始值,1000次为1秒,我们就可以操作led了。


代码实现

上面已经对功能实现已经做了细致的拆分,包括如何设置定时器的工作模式、计数模式以及定时器的开启,接下来的代码实现就变得简单了。

8.UART串口中断通讯

1.串口通讯协议

1.通信相关概念

串行通信

数据通过一根线逐位传输

并行通信

数据通过多根线一起传输,需要考虑同步问题。

两者对比

****串行并行
传输速率
硬件成本
抗干扰能力
通讯距离

2.单工通讯和双工通讯

单工:数据只能在一个方向上传输,比如广播。

双工:允许数据在两个方向上传输,其又分为半双工和全双工。

半双工:允许数据可以双向传输,但是在一个时刻,只允许数据只能在一个方向传输,比如对讲机。

全双工:允许数据可以双向传输,允许数据同时双方向传输,比如打电话。

3.同步通信与异步通信

发送方和接收方如何对数据进行统一,比如A-B进行串行通讯,如果双方都是使用同一个时钟信号,比如接收方使在每一个时钟周期的下降沿进行采样,这种就是同步通信。异步通讯则是约定双方的数据通讯的频率,这样就能保证数据的正确解析,并且不需要额外的时钟信号。

2.UART通讯协议

概述

UART(univeral synchronous Receiver/Transmitter)是一种异步、全双工的串行通讯接口,常用于微控制器与计算机、微控制器与微控制器、微控制器和外设之前的数据交换。

协议格式

在UART通信中,数据是逐帧(Frame)发送的,每个数据帧通常包括起始位、数据位、校验位(可选)和停止位,具体结构如下图所示。

空闲状态为高电平,起始位为低电平,数据位可以设置为5-9位,常用的是8位,校验位为1位,常用的校验为奇偶校验,奇校验=高电压+校验位(1)=奇数,偶校验=高电平+校验位(1)=偶数,停止位表示一帧的结束,通常为1位和2位的高电平。

波特率

波特率(Baud Rate)用于表示数据的传输速率,发送方和接收方必须约定好传输速率,才能保证数据被正确的发送和接收。需要注意波特率(Baud Rate)和比特率(Bit Rate)的区别,比特率表示每秒传输的位(bit)数,而波特率表示每秒传输的符号(symbol)数。但是串口通信中,只有0和1这两个符号,因此1个符号用1位就能表示,所以此处的波特率和比特率是等价的。

2.51单片机串口工作模式与波特率的设置

工作模式的设置

51单片机集成了全双工的串行通讯的接口,可以设置四种工作模式。

其中模式0为同步通讯,该模式下,TxD引脚会作为时钟信号线,RxD作为数据信号线,该模式下只能实现半双工通讯。

模式1、2、3为经典的UART通讯,三者的区别在与是否有校验位,以及波特率是否可变。模式1的一个数据帧包含1个起始位,8个数据位和1个停止位。模式2和模式3的一个数据帧包含1个数据位,8个数据位,1个校验位和1个停止位。另外模式1和模式3的波特率可由定时器1进行配置,因而可以自由设置,而方式2的波特率直接由系统时钟决定,因而不可自由配置。

工作模式需要通过SCON(Serial Control,串行口控制)寄存器中的SM0SM1两个控制位进行设置,如下图所示。

波特率的设置

方式1的波特率会受到两个因素的影响,分别是SMOD控制位和定时器1的溢出频率,具体作用如下图所示。

常见的波特率有:4800、9600、19200、38400、57600、115200等,下面以9600为例。

我们从原理图上可以看到,波特率的设置需要依赖两个设置:1.SMOD设置;2.定时器1的溢出率。

对于SMOD的设置,我们可以通过寄存器来操作,它位于位于PCON(Power Control,电源控制)寄存器,如下图所示:

我们将其设置为0,也就是波特率=(1/32)x (定时器1的溢出率),那溢出率怎么算?我们先看下定时器的原理图:

我们可以看到SYSckl/12/(256-TH1)或者SYSckl/6/(256-TH1)为其的溢出频率,所以如果我们需要9600的比特率,那么TH1则得到的初始值为253。根据前面的设置,我们需要设置定时器1的工作模式以及计时模式。具体代码如下:

  // 1.串口工作模式
    SM0 = 0;
    SM1 = 1

    // 2. 波特率
    // 2.1 SMOD
    PCON &= 0x7F;
    // 2.2定时器1
    // 2.2.1 工作模式
    TMOD &= 0x0F;
    TMOD |= 0x20;
    // 2.2.2 初始值
    TL1 = 253;
    TH1 = 253;
    // 2.2.3 启动定时器
    TR1 = 1;

   // 4.串口中断相关配置
    EA  = 1;//总开关
    ES  = 1; //串口中断开关
    ET1 = 1; //定时器1开关

3.串口读写与简单应用

串口读写流程

默认情况下,串口并不会接收数据。如需接收数据,需要先将REN(Receive Enable) 控制位置为1,REN控制位位于SCON寄存器,如下图所示。

REN置为1后,上图中的1 0 跳变检测器( 1-To-0 Transition Detector 就会开始工作,具体来讲就是不断检测RxD引脚的起始位。当检测到1到0的跳变后,就会启动接收控制器( Rx Control ,接收控制器会将接收到数据逐位移入到输入移位寄存器( Input Shift REG ,直到接收到停止位,就算完成了一帧数据的接收。

正常情况下,接下来,接收控制器会将输入移位寄存器(Input Shift REG) 中的数据加载到读取缓冲器(SBUF) 中,并将读取中断控制位RI置1,向CPU请求中断,CPU检测到中断请求后就执行相应的中断服务程序,开发者就能在中断服务程序中读取SBUF获取当前帧的数据了。

但是上述操作(加载数据到SBUF和RI置位)的执行是有条件的,满足条件才会执行,不满足,那么当前数据帧就会被丢弃,具体条件如下:

1.结束位正常

也就是说发送完一帧数据和接收完一帧数据之后,执行的都是串口中断的中断服务程序,因此,再编写该中段服务程序时,需要注意判断当前中断到底是由发送操作触发的,还是由接收操作触发的。

2.读取中断标志位为复位状态

读取中断标志位RI必须等于0,也就说要保证上一帧数据已经被读取或处理完毕,才能处理当前帧。

总结: 接收数据需要先使能接收,也就是将REN控制位置1,然后开启串口中断,并在中断服务程序中 读取SBUF

代码示例如下。

/*
 * 串口中断的中断号为4
 /
void Dri_UART_Handler() interrupt 4
{
    / 检查接收中断标志位RI,如果为1,表示有一帧数据接收完成 */
    if (RI == 1) {
    }
    /* 检查发送中断标志位TI,如果为1,表示有一帧数据发送完成 */
    if (TI == 1) {
    }
}

另外RI和TI标志位,只能由软件复位,也就是需要在中断服务程序中将其设置为0,如下。

/*
* 串口中断的中断号为4
*/
void Dri_UART_Hander() interrupt 4
{
    /* 检查接收中断标志位RI,如果为1,表示有一帧数据接收完成 */
    if (RI == 1) {
        RI = 0;
    }

    /* 检查发送中断标志位TI,如果为1,表示有一帧数据发送完成 */
    if (TI == 1) {
        TI = 0;
    }
}

使用uart控制led灯

#include "dri_uart.h"

static bit s_is_sending = 0; // 0:未在发送 1:正在发送
static char s_buffer    = 0;

static void init_budrate()
{

    // 1.串口工作模式 0 1 为8位数据位 波特率设置需要根据定时器1的溢出率及串口的工作模式SMOD来设置
    SM0 = 0;
    SM1 = 1;

    // 2. 波特率
    // 2.1 SMOD
    PCON &= 0x7F; // 将SMOD设置为0
    // 2.2定时器1的设置
    // 2.2.1 工作模式
    TMOD &= 0x0F;
    TMOD |= 0x20;
    // 2.2.2 初始值 这是设置波特率的关键溢出率=(SYSCLK/12/(256-T))而波特率=(1/32)x (定时器1的溢出率)
    TL1 = 253;
    TH1 = 253;
    // 2.2.3 启动定时器
    TR1 = 1;

    // 开关配置
    EA  = 1; // 中断总开关
    ES  = 1; // 串口中断开关
    ET1 = 1; // 定时器1开关
}

static void init_rc()
{
    //  1.接收数据相关配置
    REN = 1; // 使能接收
    SM2 = 0; // 接收停止位模式
    RI  = 0; // 接收数据标志位 有数据的话并且有停止位的时候会单片机会将其置为1,这个时候我们就可以从寄存器中取数据了SBUF
    TI  = 0; // 发送数据标志位 当我们发送标志位允许时,这个标志位会置1,然后我们就可以发送数据了,
}
void uart_init(void)
{
    init_budrate();
    init_rc();
}

static void Dri_UART_SendChar(char c)
{
    while (s_is_sending == 1);//等待之前的发送完成
    s_is_sending = 1; // 标记正在发送
    SBUF         = c;
}


static void Dri_UART_RecData()
{
    P34 = 1; // 打开LED使能
    if (s_buffer == 'A') {
        P00 = 0; // LED1亮
    } else if (s_buffer == 'B') {
        P01 = 1; // LED1灭
    }
    
    Dri_UART_SendChar('1'); // 发送数据
}
// 串口中断函数
void Dri_UART_Handler() interrupt 4
{
    /* 检查接收中断标志位RI,如果为1,表示有一帧数据接收完成 */
    if (RI == 1) {

        s_buffer = SBUF; // 读取接收到的数据

        Dri_UART_RecData(); // 处理接收到的数据
        RI   = 0;    // 需要手动清除结束读取
  
    }
    /* 检查发送中断标志位TI,如果为1,表示有一帧数据发送完成 */
    if (TI == 1) {
        s_is_sending = 0; // flag置0,表示发送完成
        TI           = 0; // 需要手动清除结束发送
    }
}

九.I2C协议与EEPROM


1.概述

I2C(Inter-Integrated Circuit),通常简称为IIC,是一种用在集成电路(IC)之间的串行通信总线。它是由Philips(现在的NXP半导体)在上世纪80年代开发的,并在之后广泛应用于各种电子设备和嵌入式系统中。

2. 信号线

I2C为同步串行通信,使用两根线路进行通信,分别是数据线(SDA)和时钟线(SCL),SDA线用于数据传输,SCL线用于数据传输的同步。SCL的每个时钟周期,SDA传输一位数据。

I2C规定,数据的接收方会在每个时钟周期的高电平期间读取数据,具体来讲就是在SCL处于高电平时,读取SDA上的数据,如下图所示。

因此,SDA必须在SCL低电平期准备好要发送的下一位数据,然后在SCL高电平期间保持稳定。

3. 主从架构

I2C采用主从架构,一个主设备可连接多个从设备。主设备负责发起通信和控制总线,而从设备负责响应主设备的请求。如下图所示。

I 2 C总线中的每个设备都有一个唯一的地址(用7位二进制数字表示) ,用于在总线上标识自己。主设备可以根据地址选择性的与特定的从设备进行通信。

需要注意的是,SCL信号线上的时钟信号始终由主设备产生,而SDA信号线上的数据信号既可由主设备产生,也可由从设备产生。当主设备向从设备发送数据时,SDA信号由主设备产生,从设备接收信号;当主设备从从设备读取数据时,SDA信号由从设备产生,主设备接收信号。

4.通讯协议

I2C协议规定,当SDA和SCL均为高电平时,总线为空闲状态;

主设备和从设备间的每次通信,都需要以一个起始信号开始,以一个结束信号终止。起始信号和结束信号的定义如下:

起始信号:当SCL处于高电平时,SDA由高变低。

停止信号:当SCL处于高电平时,SDA由低变高。

起始信号和结束信号,都只能由主设备产生。

确认信号

I2C协议规定,发送方每发送一个字节(8位)的数据,接收方都要向发送方回复一个1位的确认信号,如下图所示。

如果该确认信号为0表示接收方已成功接收到该字节,发送方可继续发送下一字节,这个信号在I2C协议中称为ACK(Acknowledge);如果该信号为1,则表示接收方未能成功接收到该字节,或者不希望接收更多数据,该信号在I2C协议中称为NACK(Not Acknowledge)。


从机地址和读写标识

由于一个I2C总线上可能有多个从设备,所以开始通信前,主设备需要先与目标设备取得联系,然后再进行数据传输,除此之外,主机还需要向目标设备明确本次通信的操作是写数据还是读数据。

上述操作的实现思路如下:

当主设备发送起始信号之后,会向所有设备发送一个字节的数据,这一个字节中,前7位为目标设备地址,第8位为读/写标识(1表示读,0表示写),如下图所示。

当各从设备收到这个字节的数据后,会将7位地址与自身进行对比,相同则会向主设备回复确认信号,不相同则不做任何回应。

当主设备收到目标设备的确认信号后,便会开始与该设备进行通信。

通讯流程总结

综上所述,当主设备想要与某个从设备进行通信时,需要经历如下流程。

(1)发送起始信号

(2)发送目标从设备地址+读写标识位

(3)接收从设备回复的确认信号

(4)与从设备进行数据传输(发送/接收)

(5)发送终止信号

5.驱动编写

6.搭配EEPROM使用

EEPROM(Electrically Erasable Programmable Read-Only Memory,电可擦写可编程只读存储器)是一种非易失性存储器(断电后仍能保留数据),可以多次写入和擦除数据。EEPROM广泛应用于需要永久存储数据的电子设备中, 常用于存储设备工作模式、用户偏好设置等信息。

虽然叫做只读存储器(ROM),但EEPROM是即可读又可写的,这种看似矛盾的命名源于其历史发展。最早期的ROM是在制造过程中被一次性编程,出厂之后便只能读取,不能再修改,后期的各种可编程(可写)的ROM,都是基于最早期的ROM发展而来的,所以ROM这个名称就被一直沿用下来了。现在的ROM基本指非易失性存储器。

下面使用AT24C02CN,其存储容量为2K(2048位,256字节),采用I2C协议进行读写。

引脚介绍

内存结构

读操作

1.Byte Write

2.Page Write

写操作

1.Current Address Read

EEPROM内部有一个Address Register(地址寄存器),用于记录当前操作(读/写)的字节地址,每当完成一个字节的操作后(读/写),该地址会自动指向下一个字节。

Current Address Read读取的就是Address Register中的地址所指向的这一个字节,

2. Random Read

Random Read用于读取任意指定地址的一个字节,时序图如下。

3. Sequential Read

Sequential Read用于读取连续的多个字节。其起始位置可以是Address Register记录的当前地址,也可以是用户指定的任意位置,指定起始位置的方式仍然是在Sequential Read前增加一个Dummy Write操作

EEPROM+I2C搭配使用

向EEPROM写入"123456789"后读取后,并显示到数码管上。