前言
为什么突然想使用 Arduino 来控制 LCD 1602?原因很简单,因为现在正在学习数字电路,下个学期又要接触单片机。所以一半是为了预习,另一半也只是纯粹为了好玩,而且手头上正好也有这么一个元件,就开始了折腾。
我最早是受到了 The 8-Bit Guy 和 Ben Eater 的视频的启发。观看这两位的视频后,觉得 LCD 1602 的引脚功能很好理解,于是便买了一个回来。得益于批量生产,这种元件的价格已经相当廉价且皮实耐造, 20 块左右便到手了。
元件功能
首要的任务便是弄清楚每个引脚的具体功能,然后再连接到 Arduino 上面。引脚的功能如下表:
| 引脚Pins | 符号Symbol | 电平Level | 描述Description |
|---|---|---|---|
| 1 | GND |
0V | 接地 |
| 2 | VCC |
5.0V | 正极 |
| 3 | VO |
(可变) | 对比度调节 |
| 4 | RS |
0/1 | 寄存器选择,低电平为指令寄存器,高电平为数据寄存器 |
| 5 | R/W |
0/1 | 读写控制,低电平为写入,高电平为读取 |
| 6 | E |
1, 1 → 0 | 使能,脉冲信号触发 |
| 7 | DB0 |
0/1 | 数据位 0 |
| 8 | DB1 |
0/1 | 数据位 1 |
| 9 | DB2 |
0/1 | 数据位 2 |
| 10 | DB3 |
0/1 | 数据位 3 |
| 11 | DB4 |
0/1 | 数据位 4 |
| 12 | DB5 |
0/1 | 数据位 5 |
| 13 | DB6 |
0/1 | 数据位 6 |
| 14 | DB7 |
0/1 | 数据位 7 |
| 15 | A |
- | 背光正极 |
| 16 | K |
- | 背光负极 |
1 号和 2 号引脚就是元件的接地和正极,后者就接 Arduino 的 +5V 电源引脚;3 号V0为对比度,需要通过一个电位器连接正极和负极来调整。而 15 和 16 号引脚A和K为背光的正极(Anode)和负极(Cathode),为了避免电流可能过大,因此使用了一个 220Ω 的电阻连接到电源上。
由于目前仅测试 LCD 1602 的显示功能,所以 5 号读写控制引脚就直接接地。4 号寄存器选择和 6 号使能分别接 Arduino 的 12,11 号引脚。剩下的数据位就很好办了,直接按从小到大的顺序接 9~2 号引脚。
由于我并没有排线,在实物连接的时候,只能临时通过以购买的杜邦线和跳线来接线,所以看起来就比较凌乱。而且当时不慎将数据位的连接顺序弄反了,还花了一些时间来排错。不过幸运的是,一番折腾之后屏幕终于点亮了。接下来就是写代码使其运行了。
程序编写
Arduino 提供了库LiquidCrystal.h用来驱动这类屏幕。然而我并不打算直接使用这个库中预先封装好的各种功能。因为希望能够从更底层开始控制,最终目的是通过 MOS 6502 搭建的 8 位计算机,运行汇编代码并将结果输出到这种屏幕上😎。
为了使其能够正常显示,第一步就是初始化屏幕(Initialize)。根据手册,在 8 位总线模式下的初始化过程如图所示:
根据手册,为了确保 LCD 能够正确识别并进入 8 位总线模式,建议重复三次0x30指令,并每次发送前后都要稍作等待。不过为了简单起见,我就只采用发送一次的方案,信号持续时间 5ms。接下来的几条指令分别是设置显示行数为单/双、字体尺寸、开启显示、清屏、以及输入模式设定。
我采用的设置如下:
| 指令Instruction | 指令码Instruction Code | 描述Description | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
RS |
R/W |
D7 |
D6 |
D5 |
D4 |
D3 |
D2 |
D1 |
D0 |
||
| 功能设置Function Set | 0 |
0 |
0 |
0 |
1 |
1 |
1 |
1 |
0 |
0 |
|
| 显示控制Display ON/OFF Control | 0 |
0 |
0 |
0 |
0 |
0 |
1 |
1 |
1 |
1 |
|
| 清屏Clear Screen | 0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
1 |
|
| 输入模式Entry Mode Set | 0 |
0 |
0 |
0 |
0 |
0 |
0 |
1 |
1 |
0 |
|
OK,现在该发送什么指令已经确定了;接下来就是写代码。
// LCD1602Tesing.ino
#define RS 12 // RS 使用引脚 12
#define E 11 // E 使用引脚 11
// 数据位
const int dataPins[8] = { 9, 8, 7, 6, 5, 4, 3, 2 };
首先定义好 Arduino 上要使用的引脚。
// 指令
const int funcSet = 0b00111100; // 8 位总线模式,双行显示,5 * 8 字体大小
const int dispCtrl = 0b00001111; // 开启显示,隐藏光标,光标不闪烁
const int entryMode = 0b00000110; // 光标向右移动(增加),整个显示内容不移动
const int cls = 0b00000001; // 清屏
接下来定义要发送的指令。
随后便是重中之重。该如何把这些指令发送到各个引脚上呢?首先要明确写入指令的过程。由于这四条都是指令而非数据,所以RS应设为低电平。
其次就是按位发送指令。而既然提到了按位,逻辑运算肯定是少不了了。这点可以通过for循环从低位引脚开始,通过右移运算依次移动数据,并将该数据的最右侧的一位与数字1进行按位与&运算,0 & 1 =0, 1 & 1 = 1。这样就可以确保结果只会输出一位二进制数据。
最后,设置好各个引脚的电平以后就要在使能引脚上发送一个脉冲以送入数据。根据时序图,一个有效的脉冲时间组成为上升、下降沿时间 加上脉冲宽度(即高电平的时间) ,可得 。由于上升、下降沿的时间短到几乎可以忽略不计,所以只要保证 就足够了。 代码如下:
// 将数据写入引脚
void writeByte(bool rs, byte data, int delayMs) {
// 设置 RS 的电平
// 高电平数据模式,低电平指令模式
if (rs)
digitalWrite(RS, HIGH);
else
digitalWrite(RS, LOW);
// 将数据写入数据位
for (int i = 0; i < 8; i++) {
digitalWrite(dataPins[i], (data >> i) & 1);
}
// 在使能引脚上生成脉冲
digitalWrite(E, HIGH);
delay(1);
digitalWrite(E, LOW);
// 延迟,确保各个功能执行完毕
delay(delayMs);
}
这个函数将传入三个参数:rs为布尔值,选择寄存器的模式;byte是Arduino.h中定义的数据类型,其本质就是无符号 8 位整数类型,将传入一个字节大小的命令;整数delayMs为延迟的毫秒数。digitalWrite()也是在Arduino.h中定义的函数,传入的参数为引脚编号和电平值,可为HIGH或者LOW。这样,就可以直接调用这个函数来发送数据了。主程序如下:
void setup() {
Serial.begin(9600);
// 设置引脚为输出模式
pinMode(RS, OUTPUT);
pinMode(E, OUTPUT);
// 设置数据引脚为输出模式
for (int i = 2; i <= 9; i++)
pinMode(i, OUTPUT);
// 写入数据
writeByte(false, funcSet, 5); // 功能设置
writeByte(false, dispCtrl, 1); // 显示控制
writeByte(false, cls, 1); // 清屏
writeByte(false, entryMode, 1); // 输入模式
}
void loop() {
}
至此,关于 LCD 1602 的初始化工作就完成了。LCD 1602 在启动时第一排会有一排白色的方块,而如果电路连接以及指令发送正确的话,成功初始化以后是看不见这行方块的。由于现在只是完成初始化了,还没有发送任何要输出的内容,所以屏幕还是一块空白,只有光标在闪烁。关于如何使其显示字符、自定义字符,以及更多复杂操作,将在后续文章中继续进行介绍。
后记
由于笔者这个学期学校里有数字电路的课程,以及下个学期即将到来的单片机,加上平时会观看一些 8-bit 计算机的视频,而网上也有不少通过面包板搭建这样的计算机的视频,自然而然萌生了兴趣;因此便决定趁早开始研究这些东西。笔者并不是那种坐在教室里光听理论的类型,更喜欢的方式是先动手,再根据实际情况中遇到的问题去查资料。整个过程受益颇多,也相信这是未来更深一步学习譬如汇编,以及实现通过 MOS 6502 构建一台能变成的计算机的终极目标的重要基础。