Skip to content
On this page

凹语言控制LCD1602液晶显示屏


这是凹语言在 第三届软件创新发展大会 上展出的LCD示例程序,效果如下:

1. LCD1602 显示屏

LCD1602(Liquid Crystal Display 1602)是一种常见的字符型液晶显示模块,其中1602表示它能够显示16列2行共32个字符字符。LCD1602内置了ASCII中字母、数字和常见的符号点阵字库。类似的还有LCD2004表示20列4行、LCD12864表示128列64行等不同尺寸的显示屏,但是他们引脚都是16个,控制的方式差不多。

下面是每个引脚的功能:

  • VSS(引脚1):这是接地引脚(0V)
  • VDD(引脚2):这是电源引脚(有3V和5V不同型号)
  • V0(引脚3):对比度调节引脚,连接到电位器的中间引脚,电位器的另外两个端子连接到VDD和GND,一般接的是可变电阻以控制电压
  • RS(寄存器选择,引脚4):用于选择指令寄存器或数据寄存器。当此引脚为低电平时,选择激活命令模式的指令寄存器。当此引脚为高电平时,选择激活数据模式的数据寄存器
  • RW(读/写,引脚5):用于选择读取或写入模式。当此引脚为低电平时,激活写入模式;当此引脚为高电平时,激活读取模式。一般都设置为写模式
  • E(启用,引脚6):用于通过从高到低切换来启动数据读/写操作。
  • D0至D7(引脚7-14):这些是数据引脚。D4-D7在4位和8位传输模式均用于数据传输。一般选择4位传输模式以减少引脚数量
  • A(阳极,引脚15):背光 LED 正极。一般需要通过电阻连接到5V
  • K(阴极,引脚16):背光 LED 负极。一般需要连接到GND

LCD液晶显示器的工作原理是通过使用液晶精确控制光线来在屏幕上创建图像或文本。每个液晶显示器内部都有一个背光源,提供稳定的光源。特殊的液晶被夹在两层偏光玻璃之间。

当电流流过这些液晶时,它们的排列方式会发生变化。这种排列方式会影响光线穿过液晶的方式。光线首先穿过第一层偏光玻璃,然后穿过取向液晶,液晶将光线扭转至特定角度。第二层偏光玻璃则根据扭转角度,让扭转的光线通过或阻挡。通过精确控制流向液晶不同区域(或像素)的电流,LCD 可以选择性地允许或阻挡特定区域的光线。这样就能在屏幕上显示图像、数字或文字。

2. arduino/lcd1602

根据LCD1602的引脚说明文档整理如下:

wa
// +---------------------------------------------------------------------+
// | LCD1602 Module                                                      |
// +-----+-------+---------------+---------------------+-----------------+
// | Pin | Label | Connected To  | Description         | In Code         |
// +-----+-------+---------------+---------------------+-----------------+
// |  1  | GND   | GND           | Ground              | -               |
// |  2  | VCC   | 5V            | Power Supply        | -               |
// |  3  | VO    | Potentiometer | Contrast Control    | -               |
// |  4  | RS    | D7            | Register Select     | digitalWrite(RS)|
// |  5  | RW    | GND           | Write Only (GND)    | always LOW      |
// |  6  | E     | D6            | Enable Signal       | pulseEnable()   |
// | 11  | D4    | D5            | Data Bit 4          | write4bits()    |
// | 12  | D5    | D4            | Data Bit 5          | write4bits()    |
// | 13  | D6    | D3            | Data Bit 6          | write4bits()    |
// | 14  | D7    | D2            | Data Bit 7          | write4bits()    |
// +-----+-------+---------------+---------------------+-----------------+

在4位只写模式下RW和D0-D3引脚不需要,而GND/VCC/VO/RW/A/K都有固定的接线。因此只需要定义6个控制引脚:

wa
// LCD1602引脚定义
global (
	RS :i32 = 7
	E  :i32 = 6
	D4 :i32 = 5
	D5 :i32 = 4
	D6 :i32 = 3
	D7 :i32 = 2
)

首先是初始化6个引脚为输出模式(对应LCD为输入):

wa
func LCDInit {
	arduino.PinMode(RS, arduino.OUTPUT)
	arduino.PinMode(E, arduino.OUTPUT)
	arduino.PinMode(D4, arduino.OUTPUT)
	arduino.PinMode(D5, arduino.OUTPUT)
	arduino.PinMode(D6, arduino.OUTPUT)
	arduino.PinMode(D7, arduino.OUTPUT)
	arduino.Delay(50) // 等待LCD启动
	...
}

通过D4-D7每次传入的是4bit的数据或命令,借助write4bits函数完成:

wa
func LCDInit {
	...
	// 初始化到4-bit模式
	write4bits(0x03)
	arduino.Delay(5)
	write4bits(0x03)
	arduino.DelayMicroseconds(150)
	write4bits(0x03)
	write4bits(0x02) // 设置4-bit模式
	...
}

通过D4-D7传入数据或命令

wa
func write4bits(value: byte) {
	arduino.DigitalWrite(D4, i32((value>>0)&0x01))
	arduino.DigitalWrite(D5, i32((value>>1)&0x01))
	arduino.DigitalWrite(D6, i32((value>>2)&0x01))
	arduino.DigitalWrite(D7, i32((value>>3)&0x01))
	pulseEnable()
}

pulseEnable函数通过控制E引脚的状态控制每个4bit传输:

wa
func pulseEnable {
	arduino.DigitalWrite(E, arduino.LOW)
	arduino.DelayMicroseconds(1)
	arduino.DigitalWrite(E, arduino.HIGH)
	arduino.DelayMicroseconds(1)
	arduino.DigitalWrite(E, arduino.LOW)
	arduino.DelayMicroseconds(100) // 等待命令执行
}

最后是补齐LCDInit函数中清空屏幕等初始化的收尾工作:

wa
func LCDInit {
	...
	// 几个基本设置
	command(0x28) // 4-bit, 2行, 5x8 点阵
	command(0x08) // 显示关闭
	command(0x01) // 清屏
	arduino.Delay(2)
	command(0x06) // 输入模式:写入后光标右移
	command(0x0C) // 显示开启,光标关闭
}

在通过LCDInit完成液晶屏初始化工作后,就可以通过命令和数据控制显示内容了。下面是发送命令或数据的send实现:

wa
// Send a full byte in two 4-bit chunks (mode: 0 = command, 1 = data)
func send(value, mode: byte) {
	arduino.DigitalWrite(i32(RS), i32(mode))
	write4bits(value >> 4)   // 高四位
	write4bits(value & 0x0F) // 低四位
}

send函数是向LCD发送数据或命令,mode参数表示命令还是数据,内部是通过RS引脚控制。基于send函数,可封装commandwriteChar写命令和写数据函数:

wa
// Send instruction command to LCD (RS=0)
func command(value: byte) {
	send(value, byte(arduino.LOW))
}

// Write a character to current cursor position (RS=1)
func writeChar(value: byte) {
	send(value, byte(arduino.HIGH))
}

比如可以通过命令控制光标的位置。第一行光标位置通过0x80到0x90命令控制,第二行光标位置通过0xC0到0xD0命令控制。封装的LCDSetCursor函数如下:

wa
func LCDSetCursor(row, col: i32) {
	if row == 0 {
		command(byte(0x80 + col))
	} else {
		command(byte(0xC0 + col))
	}
}

然后是在当前行列号位置显示一个字符:

wa
func LCDWriteChar(ch: rune) {
	writeChar(byte(ch))
}

还有清空屏幕的0x01命令:

wa
func LCDClear {
	command(0x01)
	arduino.Delay(2)
}

到此已经完成lcd1602包的封装,对外的API规格如下:

wa
// LCD1602引脚定义
global (
	RS :i32 = 7
	E  :i32 = 6
	D4 :i32 = 5
	D5 :i32 = 4
	D6 :i32 = 3
	D7 :i32 = 2
)

// 初始化液晶屏
func LCDInit()

// 设置光标位置
func LCDSetCursor(row, col: i32)

// 在当前位置开始写字符, 并移动光标
func LCDWriteChar(ch: rune)

// 清空屏幕
func LCDClear()

3. Arduino 示例程序

参考凹语言主仓库的waroot/examples/arduino-lcd1602目录,构造简化版程序如下:

wa
// 版权 @2025 arduino-lcd1602 作者。保留所有权利。

import (
	"arduino/lcd1602"
	"syscall/arduino"
)

func init {
	// 初始化屏幕
	lcd1602.LCDInit()
}

func Loop {
	const s = "hello wa-lang!"

	// 清空屏幕, 重新绘制字符串
	lcd1602.LCDClear()
	SayHello(0, 1, s)
	SayHello(1, 0, s)

	// 休眠500毫秒
	arduino.Delay(500)
}

SayHello在指定行列位置开始显示一个字符串:

wa
func SayHello(row, col: i32, s: string) {
	lcd1602.LCDSetCursor(row, col)

	for i := 0; i < len(s); i++ {
		lcd1602.LCDWriteChar(i32(s[i]))
	}
}

其中Loop函数会一直被循环调用执行,因此可以通过结合清屏和延时来实现动画,完整的例子请参考参考的示例代码。

4. 额外的收益

以上代码工作的原理可参考生成的C代码,用户可以通过修改模拟宿主导入函数的实现来进行更细粒度的定制。因为本质上是生成了完整的Arduino的C语言工程,所以理论上可以借助C语言编译器将凹语言编译到本地可执行程序。

以下是凹语言主仓库的waroot/examples/hello示例在Windows下的执行效果:

5. 小结

这个例子本身并不复杂,但其背后的技术演化从最初的Wasm3虚拟机、到重写wat后端衍生出的wat2c工具、到真正跑通全链路花了将近3年时间。

凹语言分别在2022年底和2024年底增加了和改善对Arduino平台的支持,当时的技术方案是通过Wasm3虚拟机来执行凹语言输出的wasm程序。但是 Wasm3 最小的硬件依赖是 ~64Kb Flash,对于小内存的单片机支持比较弱。开发组通过wat2c技术去掉了对Wasm3的性能损耗,终于可以让程序运行在只有2KB内存的Arduino Nano开发板上。

从第一个五年计划的“能用”,到第二个五年计划的“好用”,这个例子正是“好用”的一个侧面写照。我们对任何形式的讨论和合作保持开放态度,期待诸位一起推动新一轮演化!