简介

mcu101

当你在用单片机来实现自己的创意时,往往会发现显示输出和按钮输入是必不可少的,按钮可以用来设置一些状态或者输入参数,显示器则可以输出单片机内部的状态或别的一些有意义的内容(想象一下你的手机显示屏和电脑屏幕)。

在前几次工作坊里我们实际上已经接触到了显示输出的概念:那就是 LED。虽然 LED 是一种简便的显示装置,但总是有些不直观,比如显示的数字是二进制的。(记住:LED 是你的朋友!有时在调试程序的时候它们会给你带来方便)

本期工作坊,我们将研究如何为单片机接上按键,以及把我们的老朋友(LED指示灯) 换成更直观的数码管和 LCD,以构成更友好的人机接口。

我们来玩 7 段数码管

7 段数码管的内部结构

seven_seg

7 段数码管是最常见的显示装置之一,你应该在公交车上的时钟,电子万年历,微波炉,洗衣机,小区的对讲机上见过它们。故名思义,7 段数码管是由 7 段 LED 构成的,每一段都是一个 LED1

seven_seg_int

只要我们点亮不同的段,就能用这些段组合成阿拉伯数字。

comb18

共阴极和共阳极

七段数码管分两种,一种是共阴极(负极),一种是共阳极(正极)。

cacc

共阳极的数码管在内部所有段的正极都是连在一起的,与此对应,共阴极数码管在内部,所有段的负极都是连在一起的,如下图所示。

小游戏:which is which

如果你拿到一个数码管,上面没有标型号(无法在 google 里搜索),也没有说明书,如何找出哪个脚对应的是哪一段?

方法:用万用表或者电源+电阻。

点亮数码管

既然 7 段数码管内部是一些简单的 LED,那用单片机就可以直接控制它们,让我们来做个实验让数码管显示一些数字。

原理图:

sch

单片机与数码管之间的连接
单片机针脚 数码管段
PC0 a
PC1 b
PC2 c
PC3 d
PC4 e
PC5 f
PB0 g

这个连接关系不是固定的(不一定要这样连),但连接关系和我们的程序有关。

我们可以在程序里存一个表格,要显示某个数字的时候,查表把相应的数字赋给单片机的端口就可以了,下面是编码表:

七段数码管编码表
数字 dp g f e d c b a 十六进制编码
7 6 5 4 3 2 1 0
0 0 0 1 1 1 1 1 1 0x3F
1 0 0 0 0 0 1 1 0 0x06
2 0 1 0 1 1 0 1 1 0x5B
3 0 1 0 0 1 1 1 1 0x4F
4 0 1 1 0 0 1 1 0 0x66
5 0 1 1 0 1 1 0 1 0x6D
6 0 1 1 1 1 1 0 1 0x7D
7 0 0 0 0 0 1 1 1 0x07
8 0 1 1 1 1 1 1 1 0x7F
9 0 1 1 0 1 1 1 1 0x6F
A 0 1 1 1 0 1 1 1 0x77
b 0 1 1 1 1 1 0 0 0x7C
C 0 0 1 1 1 0 0 1 0x39
d 0 1 0 1 1 1 1 0 0x5E
E 0 1 1 1 1 0 0 1 0x79
F 0 1 1 1 0 0 0 1 0x71

下载源程序

#include <avr/io.h>
#define F_CPU 12000000UL  // 12 MHz
#include <util/delay.h>
#include <avr/pgmspace.h> // 访问程序空间,后面的 pgm_read_byte 函数就由这个库所提供

// 7-seg LED 段码表
// 位: 7  6 5 4 3 2 1 0
// 段: DP G F E D C B A
// 加额外的 PROGMEM (PROGram MEMory 的缩写)修饰是为了把表格
// 存储在 Flash 存储器(程序代码就是存储在这里的)里而非 RAM 里以节省内存
const uint8_t led_7[16] PROGMEM = {
0x3F, // 0
0x06, // 1
0x5B, // 2
0x4F, // 3
0x66, // 4
0x6D, // 5
0x7D, // 6
0x07, // 7
0x7F, // 8
0x6F, // 9
0x77, // A
0x7C, // b
0x39, // C
0x5E, // d
0x79, // E
0x71  // F
};

void init(void)
{
    DDRC = 0xFF;  // 将 PORTC 设置成输出模式
    PORTC = 0x00;

    DDRB = 0xFF;  // 将 PORTB 设置成输出模式
    PORTB = 0x00;
}

int main(void)
{
    uint8_t i, temp;

    init();

    while (1)
    {
        for (i = 0; i < 16; i++)
        {
            temp = ~pgm_read_byte(led_7 + i); // 查表并赋给一个临时变量,因为是共阳LED,所要要把值取反(~是取反操作符)
            PORTC = temp;        // 把值赋给 IO
            PORTB = (temp >> 6); // 把数据向右移 6 位才能对应到 PB0 上
            _delay_ms(1000);
        }
    }
}

注意,我们声明数组的时候用了 PROGMEM,它是 PROGram MEMory (程序存储空间)的缩写,表示将这个表格存储在代码(程序)空间而不是内存里,因为 program memory 很大(在我们用的芯片里有 32k 字节),而内存很小(在我们用的芯片里只有 2k 字节)资源比较有限,如果不加 PROGMEM 修饰,这个表格就会存储在内存里,对于简单的小程序没有问题,但对大一些的表格就有问题了。

[一张插图对比 program memory 和 SRAM 的大小]

七段数码管主要的用处是显示数字,但也能显示完整的字母表,见下图:

alphabet

我们可以观察到,像 K, M, Q, W 这些字线并不能正确地表示,但是如果联系上下文,人们还是可以识别它的意思。而且这样的显示有实际的用途(在一些控制仪表上可以做菜单)。这属于设计上的一种妥协。在做设计并发现资源有限时,可以采用类似的解决方案(比如莫尔斯码温度计用一个 LED 可以显示数字和字母)。ALTEC 的说明书是一个例子。

用 7 个笔划显示字母显然不够直观,于是人们开发了十四段十六段 LED 显示器(也称为“米”字管),它们从本质原理讲和七段是一样的,只不过用了更多的段,请参考游乐场中的例子。

更多数码管

我们成功点亮了 1 位数码管,1 位数码管只能显示一位数字,但时钟显示有多位数字,接下来我们看看多位数码管应该如何驱动。

clock01

clock02

这里使用共阴极 LED.

一位数码管需要 8 个 I/O 口(如果要显示小数点的话),那 4 位数码管需要 4*8 = 32 个 I/O 口,而我们所用的 MCU 一共只有十几个 I/O 可以用?怎么办?

更好的方法是建立显示缓冲并定时扫描。

LCD 显示

在现实生活中,LCD 是另一种常见的显示设备。它们小巧、省电(相比之下 LED 是比较耗电的)、显示内容丰富生动。

HD44780 兼容型字符液晶

hd44780

图形 LCD

lcd12864

Nokia 3310/5110 LCD

nokia3310

按下面的方法把 3310 LCD 与 AVR 连接起来:

3310sch

下载源程序

游乐场

16 段数码管显示

从前面的例子可以看到,七段数码可以很好的显示数字,但要显示英文字母就不方便了,这时可以使用 16 段数码管。

16seg

16 段显示器也有共阳极(CA)和共阴极(CC)的区别,下图的数码管是共阴极的,此图也显示了段和引脚之间的一一对应关系:

16seg_cc

下面的原理图和表格列出了 USnoobie 和 16 段显示器之间的连接关系:

16seg_sch

A B C D F E G H U P K M N R S T
I/O D6 D5 D4 D3 B5 B4 B3 B2 B1 B0 C5 C4 C3 C2 C1 C0

注意:在原理图里,表面上看 ATmega328P 和 16 段数码管之间没有任何连线,其际上它们是连接在一起的。在数码管的第一脚接线上标识有 SEG_A,在 ATmega328P 的 R16 一端也同样标有 SEG_A,这个标识就表明它们实际上是一条导线。这个名字(SEG_A, SEG_B…)在原理图里称为 netname,每个连接都称为一个 net. 在原理图里使用 netname 有一个显而易见的好处:让原理图更具“可读性”而不会因为连来连去的线而导致图纸变得难以阅读。

和七段数码管一样,点亮不同的段组合就会显示不同的字符,因此我们的首先任务是建立一个表格,在要显示一个字符时,只要查表并输出到 MCU 相应的端口就可以了。

字符 亮的段 高 8 位 低 8 位 十六进制
A B C D F E G H U P K M N R S T
A GHUKR 0 0 0 0 0 0 1 1 1 0 1 0 0 1 0 0 0x03A4
B ABCDFEPMS 1 1 1 1 1 1 0 0 0 1 0 1 0 0 1 0 0xFC52
.
.
.
.
.
.
.
.
.
.
.
.

上面的表格只列出了字母 A 和 B 的编码,手工制作字型表是一件费时又繁琐的工作,我们可以写一个程序来帮助完成这个工作,例如这个 quick & dirty 的 C 程序

有了表格,我们就可以显示东西了(前提条件是连好电路),试试下面的程序(下载源程序)。

#include <avr/io.h>
#define F_CPU 12000000UL  // 12 MHz
#include <util/delay.h>
#include <avr/pgmspace.h> // 访问程序空间
#include <font16seg.h>    // 16 seg LED font table

// 各个端口初始化
void init(void)
{
    DDRC = 0xFF; // 将 PORTC 设置成输出模式
    PORTC = 0x00;

    DDRB = 0xFF; // 将 PORTB 设置成输出模式
    PORTB = 0x00;

    DDRD = 0xFF; // 将 PORTD 设置成输出模式
    PORTD = 0x00;
}

// 显示一个字符
void led_put_char(char a)
{
    uint16_t temp;

    temp = pgm_read_word(font16seg + (a - 0x20)); // 查表,可打印字符是从0x20(空格)开始的,因此要减去这个偏移量
    PORTC = temp;      // 把最低的 6 位赋给 PORTC 0~5
    PORTB = temp >> 6; // 右移 6 位,赋给 PORTB 0~5
    PORTD = temp >> 9; // 右移 9 位,赋给 PORTD 3,4,5,6
}

// 显示字符串
void display_str(char *s)
{
    while (*s)
    {
        led_put_char(*s++);
        _delay_ms(500);
    }
}

// 显示完整的 ASCII 可打印字符表
void display_ascii_tab(void)
{
    char i;

    for (i = ' '; i <= '~'; i++)
    {
        led_put_char(i);
        _delay_ms(1000);
    }
}

// 显示一个简单的动画
void play_animation(void)
{
    uint8_t i;

    for(i = 0; i < 15; i++)
    {
        led_put_char('-');
        _delay_ms(50);
        led_put_char('\\');
        _delay_ms(50);
        led_put_char('|');
        _delay_ms(50);
        led_put_char('/');
        _delay_ms(50);
    }
}
 
int main(void)
{
    char i;

    init();

    while (1)
    {
        //led_put_char('A');            // 显示字母 A
        //display_str("Hello  World "); // 例行的 hello world
        //display_ascii_tab();          // 演示 ASCII 表
        play_animation();               // 演示动画
    }
}

letter_A

小数点呢?

如果还要显示小数点,则还需要一个 I/O。

能少用一些 I/O 吗?

完全可以。可以用类似于 74HC595 之类的移位寄存器或专用驱动芯片来驱动 16 段显示器。这样,你所需的 I/O 数量会从 16 个变成 3 个。利用 74HC595,我们可以把字节从单片机里“串行地”传输到 74HC595 里,然后在 74HC595 一端再把它们重新“组装”成并行数据。

当然,如果你的小作品只需要连接一个显示器,一个蜂鸣器和几个按键,那就不需要用扩展芯片。

发挥你的创意

用 16 段数码管可以实现一些小巧有创意的作品,比如:

请发挥你的创意。

点阵 LED

5x7 的点阵就可以显示英文字母。

5x7_sch


  1. 在某些很大的 7 段数码管里,每一段里不止一只 LED.