|
| 1 | +# 凹语言控制LCD显示屏 |
| 2 | + |
| 3 | +- 时间:2025-07-16 |
| 4 | +- 撰稿:凹语言开发组 |
| 5 | +- 转载请注明原文链接:[https://wa-lang.org/smalltalk/st0082.html](https://wa-lang.org/smalltalk/st0082.html) |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +凹语言分别在2022年底和2024年底增加了和改善对Arduino-wasm平台的支持,当时的技术方案是通过Wasm3虚拟机来执行凹语言输出的wasm程序。Wasm3 是一个高性能的 WebAssembly 解释器并提供了对 Arduino 的支持。但是 Wasm3 最小的硬件依赖是 ~64Kb Flash,只能在 Arduino Nano 33 等高端的单片机测试。对于只有 2KB 内存的 Arduino Nano 底端单片机缺乏支持。 |
| 10 | + |
| 11 | +得益于2024年8月凹语言开发组重写了后端的Wat到wasm解析和编码代码,在此工作基础上同年10月实现了一个将wat翻译到C语言的工具。为了支持小内存的单片机环境,开发组决定通过Wat2c的方案绕过对Wasm3虚拟机的依赖,直接输出Arduino需要的C程序。经过大半年的开发完善,终于可以让LCD1602液晶屏显式程序运行在只有2KB内存的Arduino Nano开发板上。 |
| 12 | + |
| 13 | + |
| 14 | + |
| 15 | +## 1. LCD1602 显示屏 |
| 16 | + |
| 17 | +LCD1602(Liquid Crystal Display 1602)是一种常见的字符型液晶显示模块,其中1602表示它能够显示16列2行共32个字符字符。LCD1602内置了ASCII中字母、数字和常见的符号点阵字库。类似的还有LCD2004表示20列4行、LCD12864表示128列64行等不同尺寸的显示屏,但是他们引脚都都16个,控制的方式差不多。 |
| 18 | + |
| 19 | + |
| 20 | + |
| 21 | +下面是每个引脚的功能: |
| 22 | + |
| 23 | +- VSS(引脚1):这是接地引脚(0V) |
| 24 | +- VDD(引脚2):这是电源引脚(有3V和5V不同型号) |
| 25 | +- V0(引脚3):对比度调节引脚,连接到电位器的中间引脚,电位器的另外两个端子连接到VDD和GND,一般接的是可变电阻以控制电压 |
| 26 | +- RS(寄存器选择,引脚4):用于选择指令寄存器或数据寄存器。当此引脚为低电平时,选择激活命令模式的指令寄存器。当此引脚为高电平时,选择激活数据模式的数据寄存器 |
| 27 | +- RW(读/写,引脚5):用于选择读取或写入模式。当此引脚为低电平时,激活写入模式;当此引脚为高电平时,激活读取模式。一般都设置为写模式 |
| 28 | +- E(启用,引脚6):用于通过从高到低切换来启动数据读/写操作。 |
| 29 | +- D0至D7(引脚7-14):这些是数据引脚。D4-D7在4位和8位传输模式均用于数据传输。一般选择4位传输模式以减少引脚数量 |
| 30 | +- A(阳极,引脚15):背光 LED 正极。一般需要通过电阻连接到5V |
| 31 | +- K(阴极,引脚16):背光 LED 负极。一般需要连接到GND |
| 32 | + |
| 33 | +LCD液晶显示器的工作原理是通过使用液晶精确控制光线来在屏幕上创建图像或文本。每个液晶显示器内部都有一个背光源,提供稳定的光源。特殊的液晶被夹在两层偏光玻璃之间。 |
| 34 | + |
| 35 | + |
| 36 | + |
| 37 | +当电流流过这些液晶时,它们的排列方式会发生变化。这种排列方式会影响光线穿过液晶的方式。光线首先穿过第一层偏光玻璃,然后穿过取向液晶,液晶将光线扭转至特定角度。第二层偏光玻璃则根据扭转角度,让扭转的光线通过或阻挡。通过精确控制流向液晶不同区域(或像素)的电流,LCD 可以选择性地允许或阻挡特定区域的光线。这样就能在屏幕上显示图像、数字或文字。 |
| 38 | + |
| 39 | + |
| 40 | +## 2. `arduino/lcd1602` 包 |
| 41 | + |
| 42 | +根据LCD1602的引脚说明文档整理如下: |
| 43 | + |
| 44 | +```wa |
| 45 | +// +---------------------------------------------------------------------+ |
| 46 | +// | LCD1602 Module | |
| 47 | +// |-----+-----------------------+---------------------+-----------------+ |
| 48 | +// | Pin | Label | Connected To | Description | In Code | |
| 49 | +// |-----+-------+---------------+---------------------+-----------------+ |
| 50 | +// | 1 | GND | GND | Ground | - | |
| 51 | +// | 2 | VCC | 5V | Power Supply | - | |
| 52 | +// | 3 | VO | Potentiometer | Contrast Control | - | |
| 53 | +// | 4 | RS | D7 | Register Select | digitalWrite(RS)| |
| 54 | +// | 5 | RW | GND | Write Only (GND) | always LOW | |
| 55 | +// | 6 | E | D6 | Enable Signal | pulseEnable() | |
| 56 | +// | 11 | D4 | D5 | Data Bit 4 | write4bits() | |
| 57 | +// | 12 | D5 | D4 | Data Bit 5 | write4bits() | |
| 58 | +// | 13 | D6 | D3 | Data Bit 6 | write4bits() | |
| 59 | +// | 14 | D7 | D2 | Data Bit 7 | write4bits() | |
| 60 | +// +-----+-------+---------------+---------------------+-----------------+ |
| 61 | +``` |
| 62 | + |
| 63 | +在4位只写模式下RW和D0-D3引脚不需要,而GND/VCC/VO/RW/A/K都有固定的接线。因此只需要定义6个控制引脚: |
| 64 | + |
| 65 | +```wa |
| 66 | +// LCD1602引脚定义 |
| 67 | +global ( |
| 68 | + RS :i32 = 7 |
| 69 | + E :i32 = 6 |
| 70 | + D4 :i32 = 5 |
| 71 | + D5 :i32 = 4 |
| 72 | + D6 :i32 = 3 |
| 73 | + D7 :i32 = 2 |
| 74 | +) |
| 75 | +``` |
| 76 | + |
| 77 | +首先是初始化6个引脚为输入模式: |
| 78 | + |
| 79 | +```wa |
| 80 | +func LCDInit { |
| 81 | + arduino.PinMode(RS, arduino.OUTPUT) |
| 82 | + arduino.PinMode(E, arduino.OUTPUT) |
| 83 | + arduino.PinMode(D4, arduino.OUTPUT) |
| 84 | + arduino.PinMode(D5, arduino.OUTPUT) |
| 85 | + arduino.PinMode(D6, arduino.OUTPUT) |
| 86 | + arduino.PinMode(D7, arduino.OUTPUT) |
| 87 | + arduino.Delay(50) // 等待LCD启动 |
| 88 | + ... |
| 89 | +} |
| 90 | +``` |
| 91 | + |
| 92 | +然后是设置为4bit传输模式: |
| 93 | + |
| 94 | +```wa |
| 95 | +func LCDInit { |
| 96 | + ... |
| 97 | + // 初始化到4-bit模式 |
| 98 | + write4bits(0x03) |
| 99 | + arduino.Delay(5) |
| 100 | + write4bits(0x03) |
| 101 | + arduino.DelayMicroseconds(150) |
| 102 | + write4bits(0x03) |
| 103 | + write4bits(0x02) // 设置4-bit模式 |
| 104 | + ... |
| 105 | +} |
| 106 | +``` |
| 107 | + |
| 108 | +通过D4-D7传入数据或命令 |
| 109 | + |
| 110 | +```wa |
| 111 | +func write4bits(value: byte) { |
| 112 | + arduino.DigitalWrite(D4, i32((value>>0)&0x01)) |
| 113 | + arduino.DigitalWrite(D5, i32((value>>1)&0x01)) |
| 114 | + arduino.DigitalWrite(D6, i32((value>>2)&0x01)) |
| 115 | + arduino.DigitalWrite(D7, i32((value>>3)&0x01)) |
| 116 | + pulseEnable() |
| 117 | +} |
| 118 | +``` |
| 119 | + |
| 120 | +`pulseEnable`函数通过控制`E`引脚的状态控制每个4bit传输: |
| 121 | + |
| 122 | +```wa |
| 123 | +func pulseEnable { |
| 124 | + arduino.DigitalWrite(E, arduino.LOW) |
| 125 | + arduino.DelayMicroseconds(1) |
| 126 | + arduino.DigitalWrite(E, arduino.HIGH) |
| 127 | + arduino.DelayMicroseconds(1) |
| 128 | + arduino.DigitalWrite(E, arduino.LOW) |
| 129 | + arduino.DelayMicroseconds(100) // 等待命令执行 |
| 130 | +} |
| 131 | +``` |
| 132 | + |
| 133 | +最后是`LCDInit`函数中清空屏幕等初始化的收尾工作: |
| 134 | + |
| 135 | +```wa |
| 136 | +func LCDInit { |
| 137 | + ... |
| 138 | + // 几个基本设置 |
| 139 | + command(0x28) // 4-bit, 2行, 5x8 点阵 |
| 140 | + command(0x08) // 显示关闭 |
| 141 | + command(0x01) // 清屏 |
| 142 | + arduino.Delay(2) |
| 143 | + command(0x06) // 输入模式:写入后光标右移 |
| 144 | + command(0x0C) // 显示开启,光标关闭 |
| 145 | +} |
| 146 | +``` |
| 147 | + |
| 148 | +在通过`LCDInit`完成液晶屏初始化工作后,就可以通过命令和数据控制显式内容了。下面是发送命令或数据的`send`实现: |
| 149 | + |
| 150 | +```wa |
| 151 | +// Send a full byte in two 4-bit chunks (mode: 0 = command, 1 = data) |
| 152 | +func send(value, mode: byte) { |
| 153 | + arduino.DigitalWrite(i32(RS), i32(mode)) |
| 154 | + write4bits(value >> 4) // 高四位 |
| 155 | + write4bits(value & 0x0F) // 低四位 |
| 156 | +} |
| 157 | +
|
| 158 | +``` |
| 159 | + |
| 160 | +`send`函数是向LCD发送数据或命令,`mode`参数表示命令还是数据,内部是通过`RS`引脚控制。基于`send`函数,可封装`command`和`writeChar`写命令和写数据函数: |
| 161 | + |
| 162 | +```wa |
| 163 | +// Send instruction command to LCD (RS=0) |
| 164 | +func command(value: byte) { |
| 165 | + send(value, byte(arduino.LOW)) |
| 166 | +} |
| 167 | +
|
| 168 | +// Write a character to current cursor position (RS=1) |
| 169 | +func writeChar(value: byte) { |
| 170 | + send(value, byte(arduino.HIGH)) |
| 171 | +} |
| 172 | +``` |
| 173 | + |
| 174 | +比如可以通过命令控制光标的位置。第一行光标位置通过0x80到0x90命令控制,第二行光标位置通过0xC0到0xD0命令控制。封装的`LCDSetCursor`函数如下: |
| 175 | + |
| 176 | +```wa |
| 177 | +func LCDSetCursor(row, col: i32) { |
| 178 | + if row == 0 { |
| 179 | + command(byte(0x80 + col)) |
| 180 | + } else { |
| 181 | + command(byte(0xC0 + col)) |
| 182 | + } |
| 183 | +} |
| 184 | +``` |
| 185 | + |
| 186 | +然后是在当前行列号位置显式一个字符: |
| 187 | + |
| 188 | +```wa |
| 189 | +func LCDWriteChar(ch: rune) { |
| 190 | + writeChar(byte(ch)) |
| 191 | +} |
| 192 | +``` |
| 193 | + |
| 194 | +还有清空屏幕的0x01命令: |
| 195 | + |
| 196 | +```wa |
| 197 | +func LCDClear { |
| 198 | + command(0x01) |
| 199 | + arduino.Delay(2) |
| 200 | +} |
| 201 | +``` |
| 202 | + |
| 203 | +到此已经完成lcd1602包的封装,对外的API规格如下: |
| 204 | + |
| 205 | +```wa |
| 206 | +// LCD1602引脚定义 |
| 207 | +global ( |
| 208 | + RS :i32 = 7 |
| 209 | + E :i32 = 6 |
| 210 | + D4 :i32 = 5 |
| 211 | + D5 :i32 = 4 |
| 212 | + D6 :i32 = 3 |
| 213 | + D7 :i32 = 2 |
| 214 | +) |
| 215 | +
|
| 216 | +// 初始化液晶屏 |
| 217 | +func LCDInit() |
| 218 | +
|
| 219 | +// 在当前位置显示字符 |
| 220 | +func LCDWriteChar(ch: rune) |
| 221 | +
|
| 222 | +// 清空屏幕 |
| 223 | +func LCDClear() |
| 224 | +``` |
| 225 | + |
| 226 | +## 3. Arduino 程序 |
| 227 | + |
| 228 | +参考凹语言主仓库的`waroot/examples/arduino-lcd1602`目录,构造简化版程序如下: |
| 229 | + |
| 230 | +```wa |
| 231 | +// 版权 @2025 arduino-lcd1602 作者。保留所有权利。 |
| 232 | +
|
| 233 | +import ( |
| 234 | + "arduino/lcd1602" |
| 235 | + "syscall/arduino" |
| 236 | +) |
| 237 | +
|
| 238 | +func init { |
| 239 | + // 初始化屏幕 |
| 240 | + lcd1602.LCDInit() |
| 241 | +} |
| 242 | +
|
| 243 | +func Loop { |
| 244 | + const s = "hello wa-lang!" |
| 245 | +
|
| 246 | + // 清空屏幕, 重新绘制字符串 |
| 247 | + lcd1602.LCDClear() |
| 248 | + SayHello(0, 1, s) |
| 249 | + SayHello(1, 0, s) |
| 250 | +
|
| 251 | + // 休眠500毫秒 |
| 252 | + arduino.Delay(500) |
| 253 | +} |
| 254 | +``` |
| 255 | + |
| 256 | +`SayHello`在指定行列位置开始显示一个字符串: |
| 257 | + |
| 258 | +```wa |
| 259 | +func SayHello(row, col: i32, s: string) { |
| 260 | + lcd1602.LCDSetCursor(row, col) |
| 261 | +
|
| 262 | + for i := 0; i < len(s); i++ { |
| 263 | + lcd1602.LCDWriteChar(i32(s[i])) |
| 264 | + } |
| 265 | +} |
| 266 | +``` |
| 267 | + |
| 268 | +其中Loop函数会一直被循环调用执行,因此可以通过结合清屏和延时来实现动画,完整的例子请参考参考的示例代码。 |
| 269 | + |
| 270 | +## 4. 额外的收益 |
| 271 | + |
| 272 | +以上代码工作的原理可参考生成的C代码,用户可以通过修改模拟宿主导入函数的实现来进行更细粒度的定制。因为本质上是生成了完整的Arduino的C语言工程,所以理论上可以借助C语言编译器将凹语言编译到本地可执行程序。 |
| 273 | + |
| 274 | +以下是凹语言主仓库的`waroot/examples/hello`示例在Windows下的执行效果: |
| 275 | + |
| 276 | + |
| 277 | + |
| 278 | +## 5. 小结 |
| 279 | + |
| 280 | +这个文章分享的是如何通过凹语言在Arduino平台来控制LCD显示的例子。例子本身并不复杂,但其背后的技术演化从最初的Wasm3虚拟机、到重新wat后端衍生的wat2c根据、到真正跑通全链路花了将近3年时间。从第一个五年计划的“能用”,到第二个五年计划的“好用”,这个例子正是“好用”的一个侧面写照。 |
| 281 | + |
| 282 | +我们对任何形式的讨论和合作保持开放态度,期待诸位一起推动新一轮演化! |
0 commit comments