点亮数码管

本节课利用已经学习的LED知识去控制一个8位数码管。

本节的原理比较简单。不需要多少时间讲。

更多时间是跟大家一起编码调试,从中学习一些编码思路和学习方法。

什么是数码管

数码管是什么?下图就是一个数码管

从硬件上个看,其实就是8个LED组合在一起。8个LED应该有16个引脚,但是数码管上只有10个引脚。为什么呢?

请看下图:

1个LED有两个引脚,要控制LED,1个引脚接控制信号,另外一个引脚接电源或者地(高驱动或低驱动,下同)。

那么,当有8个LED,只需要8根IO口控制状态,其他IO全部接到地或者电源即可。

当用高驱动时,LED负极全部接到地,这种数码管就叫做共阴极数码管。

当用低电平驱动时,LED正极全部接到电源,这种数码管就叫做共阳极数码管。

数码管实物中,小数点LED通常单独引出两个引脚,由我们在电路图上连接在一起。

原理图

从原理可知,控制数码管需要8根IO口。下图就是原理图。

IO口选择PE7---PE14,这8个IO口是连续的,方便代码控制。

共阴极数码管,所有负极接到地。正极通过1个限流电阻接到控制IO口。

接口设计

什么是接口?

  1. 两件事物之间的交互通道叫做接口。
  2. 软件中,应用程序控制硬件用的函数,就是接口。
  3. 从上往下看,即用户角度看硬件,用户想要什么功能?
  4. 从下往上看,硬件有什么功能?能提供什么功能?(注意二者区别)

刚刚开始学编程,这些设计理念可以了解,慢慢实践

一位数码管有什么功能?

  1. 首先,有8个LED可以点亮。
  2. 然后,8个数码管可以组成数字。

从用户角度看,我们用数码管做什么呢?通常我们需要的功能是显示数字,而不是点亮某个段。

所以,我们就定义数码管的功能是:显示数字

函数接口如下:

/*
    定义一个seg_display
    输入参数有2个,分别是char型的num,char型的dot
    没有返回值。
*/
void seg_display(char num, char dot)

num就是要显示的数字:0~9

dot表示要不要点亮小数点

到此,我们编码前的学习和设计就完成了,下面开始实现功能。

编码调试

  1. 第一步,点亮LED

这一步在上一节调试LED时已经学过,代码如下:

``` / 调用库函数RCC_APB2PeriphClockCmd 传入两个参数RCC_APB2Periph_GPIOD,ENABLE RCC_APB2Periph_GPIOD是一个宏定义, ENABLE是一个新定义的枚举类型FunctionalState / RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE);

GPIO_ResetBits(GPIOE, GPIO_Pin_7|GPIO_Pin_8|GPIO_Pin_9|GPIO_Pin_10|GPIO_Pin_11|GPIO_Pin_12|GPIO_Pin_13|GPIO_Pin_14);
/* Configure PD0 and PD2 in output pushpull mode */
/* Configure PD0 and PD2 in output pushpull mode */
/*赋值给结构体变量GPIO_InitStructure的成员,
    注意,GPIO_InitStructure是实体,所以用点,
    如果是一个结构体指针,就用->
    GPIO_InitStructure->GPIO_Pin
*/  
 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7|GPIO_Pin_8|GPIO_Pin_9|GPIO_Pin_10|GPIO_Pin_11|GPIO_Pin_12|GPIO_Pin_13|GPIO_Pin_14;
 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
 GPIO_Init(GPIOE, &GPIO_InitStructure);

GPIO_SetBits(GPIOE, GPIO_Pin_7);
GPIO_SetBits(GPIOE, GPIO_Pin_8);
GPIO_SetBits(GPIOE, GPIO_Pin_9);
GPIO_SetBits(GPIOE, GPIO_Pin_10);
GPIO_SetBits(GPIOE, GPIO_Pin_11);
GPIO_SetBits(GPIOE, GPIO_Pin_12);
GPIO_SetBits(GPIOE, GPIO_Pin_13);
GPIO_SetBits(GPIOE, GPIO_Pin_14);

```

几个关键点:

  1. 记得打开IO口时钟。

  2. 先设置状态,再配置IO口为输出。防止IO口配置完后LED闪一下

(特别是在控制电机时,一定要配置为确定状态后再将GPIO外设连接到IO口)

  1. 然后调用GPIO_SetBits控制IO口。

  2. 用调试器一个一个LED数码管轮流测试,看是不是能点亮、熄灭。

为什么要一个一个调试?因为这样测试可以要测试出硬件上IO口短路的情况。

如果8个IO口一起控制亮灭,就无法知道IO口有没有短路。

  1. 用直接控制IO的方法实现接口

接口函数原型我们已经定义好:

void seg_display(char num, char dot)

既然我们都会控制LED了,那么,就用控制LED的方法实现这个函数。

```

GPIO_ResetBits(GPIOE, GPIO_Pin_7|GPIO_Pin_8|GPIO_Pin_9|GPIO_Pin_10|GPIO_Pin_11|GPIO_Pin_12|GPIO_Pin_13|GPIO_Pin_14);

    if(dot  == 1)
    {
                GPIO_SetBits(GPIOE, GPIO_Pin_7);
    }

    switch(num)
    {
        case 0:
            GPIO_SetBits(GPIOE, GPIO_Pin_8|GPIO_Pin_9|GPIO_Pin_10|GPIO_Pin_11|GPIO_Pin_12|GPIO_Pin_13);
            break;

        case 1:
            GPIO_SetBits(GPIOE, GPIO_Pin_9|GPIO_Pin_10);
            break;

        case 2:
            GPIO_SetBits(GPIOE, GPIO_Pin_8|GPIO_Pin_9|GPIO_Pin_11|GPIO_Pin_12|GPIO_Pin_14);
            break;

        case 3:
            GPIO_SetBits(GPIOE,GPIO_Pin_8|GPIO_Pin_9|GPIO_Pin_10|GPIO_Pin_11|GPIO_Pin_14);
            break;

        case 4:
            GPIO_SetBits(GPIOE, GPIO_Pin_9|GPIO_Pin_10|GPIO_Pin_13|GPIO_Pin_14);
            break;

        case 5:
            GPIO_SetBits(GPIOE, GPIO_Pin_8|GPIO_Pin_10|GPIO_Pin_11|GPIO_Pin_13|GPIO_Pin_14);
            break;

        case 6:
            GPIO_SetBits(GPIOE, GPIO_Pin_8|GPIO_Pin_10|GPIO_Pin_11|GPIO_Pin_12|GPIO_Pin_13|GPIO_Pin_14);
            break;

        case 7:
            GPIO_SetBits(GPIOE, GPIO_Pin_8|GPIO_Pin_9|GPIO_Pin_10);
            break;

        case 8:
            GPIO_SetBits(GPIOE, GPIO_Pin_8|GPIO_Pin_9|GPIO_Pin_10|GPIO_Pin_11|GPIO_Pin_12|GPIO_Pin_13|GPIO_Pin_14);
            break;

        case 9:
            GPIO_SetBits(GPIOE, GPIO_Pin_8|GPIO_Pin_9|GPIO_Pin_10|GPIO_Pin_13|GPIO_Pin_14);
            break;

        default:
            break;
    }   
```

进入函数后,先把所有的LED都熄灭。

然后用if语句判断dot参数,如果等于1,就点亮小数点。

再用switch语句,根据num的值,点亮不同的LED,组成num指定的数字。

如此,就是实现了功能,在main函数中调用这个函数,就能显示指定的数字了。(先用调试器加断点运行查看执行结果)

  1. 用同时操作一组IO口的方法

上面的方法尽管实现了功能,但是你有没有觉得,这么简单的功能用了这么复杂的代码,是不是很不美?代码也很啰嗦。

其实我们有更简洁的方法。很多同学可能想到直接把GPIO_SetBits函数的第二个参数定义为一个数字,看起来就更简洁了。

我们说的不是这种优化,我们能把代码优化得很简单。(用GPIO_SetBits也可以优化,大家自己实验

先看下GPIO_SetBits和GPIO_ResetBits函数。这两个函数操作不同的寄存器,对指定的IO进行置位或清零。

如果我们去查规格书,可以发现我们可以直接设置一个寄存器输出0或1,而不是置位或清零操作。不用看寄存器,直接看库函数也能看到。

注意额,不是GPIO_WriteBit,这个函数只是将GPIO_SetBits和GPIO_ResetBits组合使用。

我们说的是GPIO_Write,这个函数直接写ODR寄存器,你写入什么值,IO口就输出什么值。

用这个函数还有一个好处:将8个LED当做一个整体。

代码如下:

``` if(dot == 1) { GPIO_SetBits(GPIOE, GPIO_Pin_7); }

    switch(num)
    {
        case 0:
            GPIO_Write(GPIOE, 0x3f00);
            break;

        case 1:
            GPIO_Write(GPIOE, 0x0600);
            break;

        case 2:
            GPIO_Write(GPIOE, 0x5b00);
            break;

        case 3:
            GPIO_Write(GPIOE,0x4f00);
            break;

        case 4:
            GPIO_Write(GPIOE, 0x6600);
            break;

        case 5:
            GPIO_Write(GPIOE, 0x6d00);
            break;

        case 6:
            GPIO_Write(GPIOE, 0x7d00);
            break;

        case 7:
            GPIO_Write(GPIOE, 0x0700);
            break;

        case 8:
            GPIO_Write(GPIOE,0x7f00);
            break;

        case 9:
            GPIO_Write(GPIOE, 0x6700);
            break;

        default:
            break;
    }

```

这种方法跟置位方法的区别是:

置位需要两步,先清所有IO。

再置位要点亮的IO。

GPIO_Write一步就可以将所有IO设置为指定值。

  1. 用查表法

表是什么?表,就是数组

从上面的switch我们可以看出,不同数字对应不同的值。而且:

switch的参数num是连续的值0~9

因此我们可以用表获取要写到IO口的值,然后,抛弃switch。

``` / 定义一个全局数组SegTab,数组成员类型是uint16_t 并初始化数组。 这个数组是数码管显示0-9的段定义。 请看seg_display函数, 例如,第一个值是0x3f00 在seg_display,取这个数,输出到IO口,LED就能显示0。 / uint16_t SegTab[10]={0x3f00, 0x0600, 0x5b00, 0x4f00, 0x6600, 0x6d00, 0x7d00, 0x0700, 0x7f00, 0x6700};

..........

if(dot == 1) { GPIO_SetBits(GPIOE, GPIO_Pin_7); }

    if(num >= 10)
        return;


    GPIO_Write(GPIOE, SegTab[num]);

```

用了表,SegTab表中的值就是对应的数码管点亮值。

很长的switch变为一行代码。

  1. 修复同时控制一组IOBUG

用上面的函数做实验,我们会发现,其他IO口也被我们控制了,不应该这样。

原因是GPIO_Write一次性写一组IO,但是我们只是用了其中的8个IO。另外8根IO也被我们输出为0了。

解决这个问题的方法就是:

读回-->修改-->写进

记住,这是一个重要方法。

代码如下:

``` uint16_t tmp;

    if(dot  == 1)
    {
        GPIO_SetBits(GPIOE, GPIO_Pin_7);
    }

    if(num >= 10)
        return;

    tmp = GPIO_ReadOutputData(GPIOE);
    tmp = tmp&0x80ff;
    tmp = tmp | SegTab[num];
    GPIO_Write(GPIOE, tmp);

```

GPIO_ReadOutputData读回当前GPIOE的输出值,注意,是读输出值,而不是输入值

位与上0x80ff,意思是将为0的位清零,这些位就是我们准备要设置的IO口。

位或上要写的值SegTab[num],或的功能是有1为1。

再输出。

如此,GPIO_Write操作就只会改变数码管的IO口。

请先理解位与和位或。

和逻辑与逻辑或是不一样的。

  1. 小数点也合进来。

小数点的IO正好也在GPIOE,同一组IO口,可以合并进来。

最终代码

``` / 写一个IO口,回读--写模式 为什么呢?因为我们只是使用了一个IO口中的几个管脚 比如GPIOE,一共有16个脚, 我们只是用了8个脚。 GPIO_Write函数是一次性设置16个脚。 如果不回读直接设置,那么,除了我们使用的8个脚之外的脚就会被意外改变。 / tmp = GPIO_ReadOutputData(GPIOE); /清空我们使用的几个管脚对应的位/ tmp = tmp&0x807f;//位与,注意和&&的区别,&&是逻辑比较 / 将我们要使用的几个管脚设置为我们需要的值, 比如,显示0,那么值就是 SegTab[0], 也就是0x3f00, 或操作是有1为1. 那么,经过下面的或操作, 我们的管脚,需要设置为1的位,就会是1, 我们不使用的管脚,原来是1的,现在也不会被改变,还是1. / tmp = tmp | SegTab[num];//位或,注意和||的区别

    if(dot  == 1)
    {
        /*
            如果需要显示数码管的小数点,就将对应位设置为1
            0x0080, 为1的位是bit7,因为数码管的小数点接在GPIOE.7上。
        */
        tmp = tmp | 0x0080;
    }
    GPIO_Write(GPIOE, tmp);

```

本文档没列出所有代码,请查看例程代码获取完整版本。

结束

SegTab这个数组,就是在数码管这个现实设备上显示数字的点阵字库。


END