4、显示驱动基础:LCD/OLED屏幕驱动、SPI/I2C协议、显示文字与图形
各位同学,欢迎来到第四章。这一章我们聊聊「显示」这件事。
自动售货机没有屏幕,就像人没有眼睛。用户选了什么商品、付了多少钱、找零多少,全得靠屏幕告诉他。所以,驱动屏幕是嵌入式UI的基本功。
我个人习惯把显示驱动分成三层:物理层(怎么连线的)、协议层(怎么传数据的)、应用层(怎么画东西的)。咱们一层一层往下拆。
4.1 屏幕选型:LCD还是OLED?
先说说屏幕本身。自动售货机里最常见的两种:LCD和OLED。
| 特性 | LCD(如12864) | OLED(如SSD1306) |
|---|---|---|
| 功耗 | 较高(需要背光) | 低(自发光) |
| 对比度 | 一般 | 高(纯黑) |
| 视角 | 有限 | 广视角 |
| 成本 | 低 | 稍高 |
| 寿命 | 长 | 有机材料会衰减 |
我在项目中遇到过一个问题:某款自动售货机放在户外,夏天暴晒。LCD屏幕的背光老化得特别快,半年就发黄了。后来换成OLED,虽然贵了几块钱,但显示效果一直很稳定。嗯,这里要注意:户外场景优先考虑OLED,室内场景LCD性价比更高。
4.2 通信协议:SPI vs I2C
屏幕和单片机怎么说话?主流就两种:SPI和I2C。说白了,就是两种「握手」的方式。
4.2.1 SPI协议
SPI,全称Serial Peripheral Interface。它用四根线:
- SCLK:时钟线,由主机控制节奏
- MOSI:主机输出,从机输入
- MISO:主机输入,从机输出(很多屏幕只用一根数据线,所以这根可以省)
- CS:片选线,选哪个设备说话
SPI的特点就是「快」。你想想看,它全双工,时钟可以跑到几十MHz。刷新一帧128x64的图片,SPI只需要几毫秒。I2C可能要几十毫秒。
核心要点:SPI适合需要快速刷新的场景,比如动画、视频。I2C适合数据量小、引脚少的场景。
4.2.2 I2C协议
I2C只有两根线:SDA(数据线)和SCL(时钟线)。它靠地址来区分设备。一个总线上可以挂多个设备,每个设备有唯一的地址。
我曾经踩过一个坑:I2C总线上挂了屏幕和温度传感器,结果屏幕显示乱码。查了半天,发现是温度传感器的地址和屏幕冲突了。后来换了地址才解决。所以I2C设计时一定要检查地址冲突。
4.3 驱动一个OLED屏幕(SSD1306)
咱们拿最常见的SSD1306 OLED屏幕举例。它支持I2C和SPI两种接口。我个人习惯用I2C,因为省引脚,接线简单。
4.3.1 硬件连接
以I2C为例,接线如下:
单片机 -> OLED
VCC -> 3.3V
GND -> GND
SCL -> PB6 (I2C1_SCL)
SDA -> PB7 (I2C1_SDA)
小技巧:SSD1306的I2C地址通常是0x3C或0x3D。如果屏幕没反应,试试换一个地址。我遇到过一批屏幕地址是0x3D,手册上写的是0x3C,差点被坑。
4.3.2 初始化代码
驱动屏幕的第一步是初始化。说白了,就是告诉屏幕:「嘿,我要开始用了,你准备好。」
# MicroPython示例
from machine import Pin, I2C
import ssd1306
# 初始化I2C
i2c = I2C(1, scl=Pin(6), sda=Pin(7), freq=400000)
# 扫描I2C设备,确认地址
devices = i2c.scan()
print("I2C devices:", [hex(addr) for addr in devices])
# 初始化OLED,128x64分辨率
oled = ssd1306.SSD1306_I2C(128, 64, i2c)
# 清屏
oled.fill(0)
oled.show()
这段代码跑通后,屏幕应该会亮起来,但什么也不显示。别急,接下来我们画点东西上去。
4.4 显示文字
自动售货机最常用的就是显示文字。比如「请投币」、「余额:5.00元」。
SSD1306内置了ASCII字库,可以直接用。但中文不行,需要自己准备字库。
4.4.1 显示英文字符
# 显示文字
oled.text("Hello, Vending!", 0, 0)
oled.text("Price: 3.50", 0, 16)
oled.text("Insert Coin", 0, 32)
oled.show()
这里要注意:text()函数的参数是(x, y)坐标。SSD1306的坐标原点在左上角,x向右增加,y向下增加。每个字符占8x16像素(默认字体)。
4.4.2 显示中文(取模方式)
中文显示需要自己准备字模。我常用的方法是:用PCtoLCD2002软件取模,生成16x16的点阵数据。
# 显示一个16x16的中文字符
# 假设"投"字的字模数据
font_data = [
0x00, 0x00, 0x7F, 0xFC, 0x08, 0x20, 0x08, 0x20,
0x08, 0x20, 0x08, 0x20, 0x08, 0x20, 0x08, 0x20,
0x08, 0x20, 0x08, 0x20, 0x08, 0x20, 0x08, 0x20,
0x08, 0x20, 0x7F, 0xFC, 0x00, 0x00, 0x00, 0x00
]
# 将字模绘制到屏幕上
for y in range(16):
for x in range(16):
if font_data[y*2 + x//8] & (0x80 >> (x%8)):
oled.pixel(x, y, 1)
oled.show()
注意:取模方式有很多种(横向、纵向、字节顺序等)。一定要和你的代码匹配。我曾经因为取模方向搞反了,显示出来的汉字全是镜像的,调试了一下午才发现。
4.5 显示图形
除了文字,自动售货机还需要显示图形。比如商品图标、二维码、进度条。
4.5.1 画基本图形
# 画一个矩形边框
oled.rect(10, 10, 50, 30, 1) # x, y, width, height, color
# 画一个填充矩形
oled.fill_rect(70, 10, 50, 30, 1)
# 画一条线
oled.line(10, 50, 120, 50, 1)
# 画一个圆(SSD1306库没有直接支持,需要自己实现)
# 可以用Bresenham算法
def draw_circle(oled, x0, y0, r, color):
x = 0
y = r
d = 3 - 2 * r
while x <= y:
oled.pixel(x0 + x, y0 + y, color)
oled.pixel(x0 - x, y0 + y, color)
oled.pixel(x0 + x, y0 - y, color)
oled.pixel(x0 - x, y0 - y, color)
oled.pixel(x0 + y, y0 + x, color)
oled.pixel(x0 - y, y0 + x, color)
oled.pixel(x0 + y, y0 - x, color)
oled.pixel(x0 - y, y0 - x, color)
if d < 0:
d = d + 4 * x + 6
else:
d = d + 4 * (x - y) + 10
y -= 1
x += 1
draw_circle(oled, 64, 32, 20, 1)
oled.show()
4.5.2 显示图片(BMP转数组)
更复杂的图形,比如商品图标,可以用图片转数组的方式。我常用的是将128x64的BMP图片转换成字节数组。
# 假设有一个128x64的图片数据
image_data = [
0xFF, 0x81, 0x81, 0x81, ... # 共1024字节
]
# 将图片数据写入屏幕
for page in range(8): # SSD1306分8页,每页8行
for col in range(128):
oled.buffer[page * 128 + col] = image_data[page * 128 + col]
oled.show()
提示:SSD1306的显存是1024字节(128x64/8)。直接操作buffer是最快的方式。我一般把常用图标预先转成数组,放在代码里,需要时直接刷进去。
4.6 避坑指南
最后,分享几个我踩过的坑:
- 电源问题:OLED屏幕对电源纹波敏感。如果屏幕闪烁,先检查电源,别急着改代码。
- I2C上拉电阻:I2C总线需要上拉电阻(4.7kΩ左右)。有些模块内置了,有些没有。没上拉的话,通信会失败。
- 刷新率:不要频繁调用
show()。每次调用都会刷新整个屏幕,太频繁会导致闪烁。我一般控制在30-60Hz。 - 字库存储:中文字库很大(一个16x16的字需要32字节)。如果显示很多汉字,建议用外部Flash存储字库,别全塞进单片机Flash里。
好了,这一章就到这里。下一章我们聊聊「触摸输入与按键交互」。到时候见。