当你在用单片机来实现自己的创意时,往往会发现显示输出和按钮输入是必不可少的,按钮可以用来设置一些状态或者输入参数,显示器则可以输出单片机内部的状态或别的一些有意义的内容(想象一下你的手机显示屏和电脑屏幕)。
在前几次工作坊里我们实际上已经接触到了显示输出的概念:那就是 LED。虽然 LED 是一种简便的显示装置,但总是有些不直观,比如显示的数字是二进制的。(记住:LED 是你的朋友!有时在调试程序的时候它们会给你带来方便)
本期工作坊,我们将研究如何为单片机接上按键,以及把我们的老朋友(LED指示灯) 换成更直观的数码管和 LCD,以构成更友好的人机接口。
7 段数码管是最常见的显示装置之一,你应该在公交车上的时钟,电子万年历,微波炉,洗衣机,小区的对讲机上见过它们。故名思义,7 段数码管是由 7 段 LED 构成的,每一段都是一个 LED1:
只要我们点亮不同的段,就能用这些段组合成阿拉伯数字。
七段数码管分两种,一种是共阴极(负极),一种是共阳极(正极)。
共阳极的数码管在内部所有段的正极都是连在一起的,与此对应,共阴极数码管在内部,所有段的负极都是连在一起的,如下图所示。
如果你拿到一个数码管,上面没有标型号(无法在 google 里搜索),也没有说明书,如何找出哪个脚对应的是哪一段?
方法:用万用表或者电源+电阻。
既然 7 段数码管内部是一些简单的 LED,那用单片机就可以直接控制它们,让我们来做个实验让数码管显示一些数字。
原理图:
单片机针脚 | 数码管段 |
---|---|
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 的大小]
七段数码管主要的用处是显示数字,但也能显示完整的字母表,见下图:
我们可以观察到,像 K, M, Q, W 这些字线并不能正确地表示,但是如果联系上下文,人们还是可以识别它的意思。而且这样的显示有实际的用途(在一些控制仪表上可以做菜单)。这属于设计上的一种妥协。在做设计并发现资源有限时,可以采用类似的解决方案(比如莫尔斯码温度计用一个 LED 可以显示数字和字母)。ALTEC 的说明书是一个例子。
用 7 个笔划显示字母显然不够直观,于是人们开发了十四段和十六段 LED 显示器(也称为“米”字管),它们从本质原理讲和七段是一样的,只不过用了更多的段,请参考游乐场中的例子。
我们成功点亮了 1 位数码管,1 位数码管只能显示一位数字,但时钟显示有多位数字,接下来我们看看多位数码管应该如何驱动。
这里使用共阴极 LED.
一位数码管需要 8 个 I/O 口(如果要显示小数点的话),那 4 位数码管需要 4*8 = 32 个 I/O 口,而我们所用的 MCU 一共只有十几个 I/O 可以用?怎么办?
更好的方法是建立显示缓冲并定时扫描。
在现实生活中,LCD 是另一种常见的显示设备。它们小巧、省电(相比之下 LED 是比较耗电的)、显示内容丰富生动。
按下面的方法把 3310 LCD 与 AVR 连接起来:
从前面的例子可以看到,七段数码可以很好的显示数字,但要显示英文字母就不方便了,这时可以使用 16 段数码管。
16 段显示器也有共阳极(CA)和共阴极(CC)的区别,下图的数码管是共阴极的,此图也显示了段和引脚之间的一一对应关系:
下面的原理图和表格列出了 USnoobie 和 16 段显示器之间的连接关系:
段 | 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(); // 演示动画
}
}
如果还要显示小数点,则还需要一个 I/O。
完全可以。可以用类似于 74HC595 之类的移位寄存器或专用驱动芯片来驱动 16 段显示器。这样,你所需的 I/O 数量会从 16 个变成 3 个。利用 74HC595,我们可以把字节从单片机里“串行地”传输到 74HC595 里,然后在 74HC595 一端再把它们重新“组装”成并行数据。
当然,如果你的小作品只需要连接一个显示器,一个蜂鸣器和几个按键,那就不需要用扩展芯片。
用 16 段数码管可以实现一些小巧有创意的作品,比如:
请发挥你的创意。
5x7 的点阵就可以显示英文字母。
在某些很大的 7 段数码管里,每一段里不止一只 LED.↩