3. 驱动库封装:Python基础驱动类设计、写命令与写数据函数、全屏填充与清屏函数
好,咱们进入第三章。这一章,说白了就是给OLED屏幕写一个“翻译官”。
你想想看,屏幕本身只认识高低电平、I2C时序。我们想让屏幕显示“Hello World”,总不能直接跟它说“嘿,显示个H”吧?得通过驱动库,把我们的意图翻译成屏幕能懂的指令和数据。
我个人习惯,写驱动库之前,先想清楚一件事:这个库到底要提供什么功能? 对于SSD1306这种OLED,核心就三件事:写命令、写数据、控制显示区域。其他的画点、画线、显示文字,都是在这三个基础上搭积木。
3.1 基础驱动类设计
我们先搭个架子。Python里,我习惯用类来封装。为什么?因为一个屏幕就是一个对象,它有状态(比如当前光标位置、是否翻转),用类管理最自然。
import smbus
import time
class SSD1306_Driver:
"""SSD1306 OLED 基础驱动类
封装了I2C通信、写命令、写数据等底层操作。
"""
# 屏幕参数常量
OLED_WIDTH = 128
OLED_HEIGHT = 64
# I2C地址(常见0x3C或0x3D,取决于SA0引脚电平)
I2C_ADDR = 0x3C
def __init__(self, i2c_bus=1, addr=0x3C):
"""初始化I2C总线,保存地址"""
self.addr = addr
self.bus = smbus.SMBus(i2c_bus)
# 我习惯在初始化时顺便做一次复位
self._reset()
def _reset(self):
"""软件复位(部分模块需要硬件复位引脚,这里简化)"""
# 发送复位命令序列
self.write_cmd(0xAE) # 关闭显示
time.sleep(0.01)
self.write_cmd(0xAF) # 开启显示
time.sleep(0.01)
小提示: 我在项目中遇到过I2C地址搞错的情况,屏幕死活不亮。后来发现是模块的SA0引脚接了高电平,地址变成了0x3D。所以,初始化时把地址作为参数传进来,是个好习惯。
3.2 写命令与写数据函数
这是驱动库的“心脏”。SSD1306通过I2C通信时,第一个字节是控制字节,用来告诉屏幕:后面跟着的是命令还是数据。
- 控制字节 = 0x00:后面是命令
- 控制字节 = 0x40:后面是数据
说白了,就是加个“标签”。
def write_cmd(self, cmd):
"""写一个命令字节到OLED"""
try:
self.bus.write_byte_data(self.addr, 0x00, cmd)
except OSError as e:
# 我曾经遇到过I2C总线被占用的情况,加个重试机制
print(f"I2C写命令失败: {e},尝试重试...")
time.sleep(0.001)
self.bus.write_byte_data(self.addr, 0x00, cmd)
def write_data(self, data):
"""写一个数据字节到OLED"""
try:
self.bus.write_byte_data(self.addr, 0x40, data)
except OSError as e:
print(f"I2C写数据失败: {e},尝试重试...")
time.sleep(0.001)
self.bus.write_byte_data(self.addr, 0x40, data)
def write_cmd_list(self, cmd_list):
"""批量写命令,提高效率"""
for cmd in cmd_list:
self.write_cmd(cmd)
def write_data_list(self, data_list):
"""批量写数据,常用于填充显存"""
for data in data_list:
self.write_data(data)
注意: 有些教程喜欢用 write_i2c_block_data 一次发多个字节,速度更快。但我个人习惯用单字节函数,因为调试时更容易定位问题。等驱动稳定了,再优化性能不迟。
3.3 全屏填充与清屏函数
这两个函数,是UI开发中最常用的“刷墙”工具。全屏填充就是把整个屏幕刷成一种颜色,清屏就是刷成黑色。
你想想看,SSD1306的显存是128x64位,也就是1024个字节。每个字节的8个位,对应屏幕上8个像素点(垂直排列)。所以,全屏填充0xFF,就是所有像素点亮;填充0x00,就是全黑。
def fill(self, data=0xFF):
"""全屏填充
Args:
data: 填充的数据,0xFF为全亮,0x00为全黑
"""
# 设置显示区域为整个屏幕
self.write_cmd_list([
0x21, 0, 127, # 设置列范围:0~127
0x22, 0, 7 # 设置页范围:0~7(共8页)
])
# 填充1024个字节
for i in range(1024):
self.write_data(data)
def clear(self):
"""清屏,等效于全屏填充0x00"""
self.fill(0x00)
def fill_area(self, x_start, y_start, width, height, data=0xFF):
"""填充指定区域(矩形区域)
这个函数我在做菜单高亮时经常用
"""
# 计算页范围(y坐标除以8)
page_start = y_start // 8
page_end = (y_start + height - 1) // 8
# 设置显示区域
self.write_cmd(0x21) # 设置列范围命令
self.write_cmd(x_start)
self.write_cmd(x_start + width - 1)
self.write_cmd(0x22) # 设置页范围命令
self.write_cmd(page_start)
self.write_cmd(page_end)
# 填充数据
total_bytes = width * (page_end - page_start + 1)
for i in range(total_bytes):
self.write_data(data)
核心要点:
- 写命令:控制屏幕行为(如设置坐标、开关显示)
- 写数据:填充显存内容(决定哪些像素亮)
- 全屏填充:本质是连续写1024个相同的数据字节
- 清屏:就是全屏填充0x00
3.4 完整示例:点亮所有像素
光说不练假把式。咱们写个完整的测试代码,看看驱动能不能正常工作。
if __name__ == "__main__":
# 创建驱动对象
oled = SSD1306_Driver()
print("开始测试OLED驱动...")
# 第一步:初始化屏幕(发送初始化命令序列)
init_cmds = [
0xAE, # 关闭显示
0xD5, 0x80, # 设置时钟分频
0xA8, 0x3F, # 设置多路复用比
0xD3, 0x00, # 设置显示偏移
0x40, # 设置显示起始行
0x8D, 0x14, # 启用电荷泵
0x20, 0x00, # 设置内存地址模式(水平模式)
0xA1, # 段重映射(左右翻转)
0xC8, # COM扫描方向(上下翻转)
0xDA, 0x12, # 设置COM引脚配置
0x81, 0xCF, # 设置对比度
0xD9, 0xF1, # 设置预充电周期
0xDB, 0x40, # 设置VCOMH电压
0xA4, # 全局显示(不忽略RAM内容)
0xA6, # 正常显示(非反色)
0xAF # 开启显示
]
oled.write_cmd_list(init_cmds)
print("初始化完成")
# 第二步:全屏填充(点亮所有像素)
print("全屏填充...")
oled.fill(0xFF)
time.sleep(2)
# 第三步:清屏
print("清屏...")
oled.clear()
time.sleep(1)
# 第四步:填充一个矩形区域(做个简单的测试图案)
print("绘制矩形区域...")
oled.fill_area(10, 10, 50, 30, 0xFF)
time.sleep(2)
# 最后清屏
oled.clear()
print("测试完成!")
避坑指南: 我曾经在初始化序列上栽过跟头。不同的OLED模块,初始化命令可能略有不同。比如有的模块需要设置电荷泵,有的不需要。我的建议是:先确认你的模块型号,再找对应的初始化序列。网上很多教程直接复制粘贴,很容易翻车。
3.5 性能优化小技巧
写驱动库,不能光想着“能用”,还得考虑“好用”。尤其是全屏填充这种高频操作,如果逐字节发送,速度会很慢。
我常用的优化手段有两个:
- 批量发送:用
write_i2c_block_data一次发32个字节,减少I2C通信次数 - 预计算数据:比如全屏填充0xFF,直接生成一个1024字节的列表,一次性发送
def fill_fast(self, data=0xFF):
"""快速全屏填充(使用块传输)"""
# 设置显示区域
self.write_cmd_list([0x21, 0, 127, 0x22, 0, 7])
# 生成填充数据块
block = [data] * 32 # 每次发32字节
# 分块发送(1024 / 32 = 32次)
for i in range(32):
self.bus.write_i2c_block_data(self.addr, 0x40, block)
实测数据: 在我的树莓派上,逐字节发送全屏填充需要约200ms,而块传输只需要约30ms。性能提升了6倍多。所以,能用块传输就别用单字节。
好了,这一章的内容就到这里。驱动库的架子搭好了,下一章咱们就可以在上面画点、画线、显示文字了。记住,底层驱动是UI的基石,基础打牢了,上层建筑才稳当。