ST7567 I²C帧缓冲图形库:支持比例字体与有序抖动
1. 项目概述ST7567_FB_I2C 是一款专为 ST7567 控制器驱动的 128×64 点阵单色 LCD 显示模块设计的 I²C 接口图形库。该库采用全内存帧缓冲Framebuffer架构所有绘图操作均在 RAM 中的显存副本上完成最终通过一次批量 I²C 传输将整帧数据刷新至 LCD 控制器显存。这种设计彻底规避了传统“边画边传”模式下频繁读-修改-写显存带来的总线开销与延迟显著提升复杂图形绘制效率尤其适用于需动态刷新、动画渲染或文本混排的嵌入式人机界面场景。本库并非从零构建其核心绘图引擎继承自 Pawel A. Hernik 开发的 SPI 版本 ST7567 图形库并针对 I²C 协议特性进行了深度重构优化地址指针自动递增机制、重写显存页/列寻址序列、引入双缓冲区切换策略以消除画面撕裂。在保持原有高性能绘图能力的基础上新增对比例字体Proportional Fonts与有序抖动Ordered Dithering的原生支持使单色 LCD 在有限灰度表现力下仍能呈现层次丰富的视觉效果。该库已在 ESP32-S3 平台完成完整验证但其硬件抽象层HAL设计具备高度可移植性。只要目标 MCU 支持标准 I²C 主机模式含 7 位地址、重复起始、ACK/NACK 处理并提供 GPIO 控制能力用于复位引脚即可通过修改底层 I²C 驱动适配函数快速迁移至 STM32HAL/LL、nRF52、RP2040 等主流平台。2. 硬件接口与电气连接2.1 ST7567 模块引脚定义与连接规范ST7567 是一款内置行驱动器与列驱动器的 COGChip-on-GlassLCD 控制器其 I²C 接口为标准 2 线制SCL/SDA无需额外片选信号。模块典型引脚布局如下以常见 128×64 COG 模组为例LCD 引脚功能说明推荐 MCU 连接电气要求备注#01 SDAI²C 数据线MCU I²C SDA 引脚如 ESP32 GPIO213.3V LVTTL需上拉4.7kΩ必接#02 SCLI²C 时钟线MCU I²C SCL 引脚如 ESP32 GPIO223.3V LVTTL需上拉4.7kΩ必接#03 RST硬件复位输入MCU 任意 GPIO如 ESP32 GPIO4低电平有效内部弱上拉可选若悬空则需软件复位#07 VCC逻辑供电MCU 3.3V 电源轨严格 3.3V±5%纹波 50mV严禁接 5V#08 GND地MCU GND共地必接关键电气约束ST7567 芯片内核与 I/O 均为 3.3V 逻辑电平。若 MCU 为 5V 系统如经典 Arduino Uno必须使用双向电平转换器如 TXB0104隔离 SDA/SCL 信号否则将永久损坏 LCD 模块。2.2 I²C 地址配置与多设备共存ST7567 的 I²C 从机地址由硬件引脚A0决定部分模块标注为SA0或ADDR。根据官方数据手册其地址编码规则如下A0 引脚状态7 位 I²C 地址十六进制8 位写地址W/R08 位读地址W/R1悬空或接 VCC0x3F0x7E0x7F接 GND0x3E0x7C0x7D在Quick Start示例中使用的#define I2C_ADDR 0x3F即对应 A0 悬空/高电平配置。若需在同一 I²C 总线上挂载多个 ST7567 显示屏如双屏显示系统可通过独立控制各模块的 A0 引脚电平实现地址区分。此时每个ST7567_FB_I2C实例需传入对应地址// 双屏配置示例ESP32 #define LCD1_RST 4 #define LCD2_RST 5 #define LCD1_ADDR 0x3F // A0 HIGH #define LCD2_ADDR 0x3E // A0 LOW ST7567_FB_I2C lcd1(LCD1_RST, LCD1_ADDR); ST7567_FB_I2C lcd2(LCD2_RST, LCD2_ADDR); void setup() { lcd1.init(); // 初始化屏1 lcd2.init(); // 初始化屏2 lcd1.cls(); // 清屏1 lcd2.cls(); // 清屏2 }3. 核心功能与 API 详解3.1 帧缓冲区Framebuffer架构库的核心是uint8_t fb[1024]的静态显存缓冲区128×64 / 8 1024 字节。该缓冲区按 LCD 的物理存储结构组织每 8 行为一页Page共 8 页Page 0–7每页包含 128 字节对应 128 列。缓冲区索引fb[y/8 * 128 x]直接映射到 LCD 显存地址其中x ∈ [0,127],y ∈ [0,63]。所有绘图函数drawPixel,drawLine,fillRect等均操作此 RAM 缓冲区不触发任何 I²C 通信。这带来两大优势零延迟响应UI 交互如按钮高亮、进度条更新即时生效原子性保证避免因 I²C 传输中断导致画面局部错乱。缓冲区刷新通过display()函数完成其内部执行以下原子操作禁用全局中断防止刷新过程中缓冲区被修改按页Page 0→7顺序向 LCD 发送0xB0 page设置页地址0x10 (col4)设置高位列地址0x00 (col0x0F)设置低位列地址连续发送当前页全部 128 字节数据利用 I²C 的自动地址递增特性恢复中断。// display() 函数关键片段简化示意 void ST7567_FB_I2C::display() { noInterrupts(); // 关中断确保缓冲区一致性 for (uint8_t page 0; page 8; page) { // 设置页地址 sendCommand(0xB0 | page); // 设置列地址高位0x10和低位0x00 sendCommand(0x10); // 高4位 sendCommand(0x00); // 低4位 // 发送整页128字节数据 sendBuffer(fb[page * 128], 128); } interrupts(); // 开中断 }3.2 基础绘图 API所有绘图函数均遵循统一坐标系原点(0,0)位于左上角X 向右递增0–127Y 向下递增0–63。函数返回void错误通过断言assert()或编译时检查捕获。函数签名功能说明参数详解典型用法void drawPixel(uint8_t x, uint8_t y, uint8_t color)绘制单个像素x: X 坐标 (0–127)y: Y 坐标 (0–63)color:BLACK(1) 或WHITE(0)lcd.drawPixel(64, 32, BLACK); // 屏幕中心点void drawLine(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t color)绘制直线Bresenham 算法(x0,y0): 起点(x1,y1): 终点color: 像素颜色lcd.drawLine(0,0,127,63,BLACK); // 对角线void drawRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t color)绘制空心矩形x,y: 左上角坐标w,h: 宽高像素color: 边框颜色lcd.drawRect(10,10,100,40,WHITE); // 白色边框void fillRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t color)绘制实心矩形同drawRect填充整个区域lcd.fillRect(20,20,80,20,BLACK); // 黑色背景块void drawCircle(uint8_t x0, uint8_t y0, uint8_t r, uint8_t color)绘制空心圆中点圆算法(x0,y0): 圆心r: 半径color: 圆周颜色lcd.drawCircle(64,32,20,BLACK); // 圆心居中void fillCircle(uint8_t x0, uint8_t y0, uint8_t r, uint8_t color)绘制实心圆同drawCircle填充圆内lcd.fillCircle(64,32,10,BLACK); // 实心圆点性能提示drawLine和fillRect经过汇编级优化水平/垂直线绘制速度比斜线快 3–5 倍fillRect使用memset()加速128×64 全屏填充仅需约 1.2msESP32 240MHz。3.3 文本渲染比例字体支持库本身不内置字体数据而是通过printStr()函数调用外部字体资源。其设计依赖于PropFonts 库https://github.com/adafruit/Adafruit-GFX-Library/tree/master/Fonts该库提供多种开源比例字体如FreeMono9pt7b、TomThumb每个字符的宽度glyph-width与字形数据glyph-bitmap均独立存储。printStr()执行流程遍历字符串每个字符c查询字体描述符font-glyph[c - font-first]获取字形信息计算字符起始 X 坐标x_pos x glyph-xOffset逐行glyph-height行将字形位图OR到帧缓冲区对应位置更新x坐标x glyph-xAdvance非固定宽度体现比例特性。// 使用 PropFonts 的完整示例 #include ST7567_FB_I2C.h #include Fonts/FreeMono9pt7b.h // 需提前安装 Adafruit GFX Fonts ST7567_FB_I2C lcd(4, 0x3F); void setup() { lcd.init(); lcd.cls(); // 设置字体必须在 printStr 前调用 lcd.setFont(FreeMono9pt7b); // 在 (0,0) 位置打印比例字体文本 lcd.printStr(0, 0, Hello, World!); lcd.display(); // 刷新到屏幕 }关键参数表FreeMono9pt7b 字体特性属性值说明first/last32 / 126ASCII 可显示字符范围空格到‘~’glyph[0].width5字符 空格宽度glyph[W].width12字符 W 宽度宽于平均值glyph[i].width3字符 i 宽度窄于平均值xAdvance10默认字符间距可被glyph-xAdvance覆盖3.4 高级特性有序抖动Ordered Dithering单色 LCD 无法显示灰度但通过快速切换像素明暗状态即抖动可在人眼视觉暂留效应下模拟中间色调。ST7567_FB_I2C 实现的是有序抖动Ordered Dithering其核心是 4×4 的阈值矩阵Threshold Matrix[ 0, 8, 2, 10 ] [12, 4,14, 6 ] [ 3,11, 1, 9 ] [15, 7,13, 5 ]当处理一幅 8 位灰度图像时对每个像素(x,y)计算其在 4×4 矩阵中的偏移tx x 0x03,ty y 0x03获取阈值T matrix[ty][tx]若原始灰度值gray T则在 LCD 上点亮该像素BLACK否则熄灭WHITE。库预置 17 种抖动模式DITHER_1x1至DITHER_16x16实际使用ditherImage()函数// 抖动处理函数原型 void ditherImage(const uint8_t* src, uint16_t width, uint16_t height, uint8_t ditherMode DITHER_4x4); // 使用示例将 128x64 灰度图 data[] 抖动后绘制 extern const uint8_t grayscale_img[1024]; // 8-bit per pixel lcd.ditherImage(grayscale_img, 128, 64, DITHER_4x4); lcd.display();模式选择指南DITHER_1x1: 无抖动纯二值化最快但细节丢失严重DITHER_2x2: 基础抖动适合小图标DITHER_4x4:推荐默认值平衡速度与质量4×4 矩阵匹配人眼分辨率DITHER_8x8/DITHER_16x16: 用于高质量静态图像计算量剧增O(n²)需权衡实时性。4. 初始化与底层驱动适配4.1 初始化流程与寄存器配置init()函数执行 ST7567 的完整上电初始化序列关键步骤如下按 LCD 数据手册时序硬件复位若RST引脚已连接拉低RST≥ 1μs再拉高并等待 ≥ 5ms软件复位发送命令0xE2Reset基础配置0xA2: 偏压比Bias设为 1/90xA3为 1/70xA1为 1/80xC0: COM 输出方向设为正常0xC8为反向0xA0: SEG 输出方向设为正常0xA1为反向电源控制0x2F: 开启全部电源Regulator, Follower, Booster0x27: 设置电子音量V0为中等0x20–0x2F可调显示控制0xAF: 开启显示0xAE为关闭0xA6: 正常显示模式0xA7为反相。// init() 关键寄存器设置片段 void ST7567_FB_I2C::init() { if (_rst_pin ! 255) { pinMode(_rst_pin, OUTPUT); digitalWrite(_rst_pin, LOW); delayMicroseconds(1); digitalWrite(_rst_pin, HIGH); delay(5); } sendCommand(0xE2); // Software Reset sendCommand(0xA2); // Bias 1/9 sendCommand(0xC0); // COM Dir Normal sendCommand(0xA0); // SEG Dir Normal sendCommand(0x2F); // Power Control: All ON sendCommand(0x27); // V0 0x27 (mid) sendCommand(0xAF); // Display ON sendCommand(0xA6); // Normal Display cls(); // Clear framebuffer }4.2 I²C 底层驱动移植指南库的 I²C 通信封装在sendCommand()和sendBuffer()两个函数中。若需移植到非 Arduino 平台如 STM32 HAL只需重写这两个函数// STM32 HAL 移植示例需在 .cpp 文件中定义 extern I2C_HandleTypeDef hi2c1; // 假设使用 I2C1 void ST7567_FB_I2C::sendCommand(uint8_t cmd) { uint8_t data[2] {0x00, cmd}; // 控制字节 0x00 命令 HAL_I2C_Master_Transmit(hi2c1, _i2c_addr 1, data, 2, HAL_MAX_DELAY); } void ST7567_FB_I2C::sendBuffer(const uint8_t* buf, uint16_t len) { uint8_t data[1] {0x40}; // 控制字节 0x40 (连续数据) HAL_I2C_Master_Transmit(hi2c1, _i2c_addr 1, data, 1, HAL_MAX_DELAY); HAL_I2C_Master_Transmit(hi2c1, _i2c_addr 1, (uint8_t*)buf, len, HAL_MAX_DELAY); }I²C 时序关键点ST7567 要求命令/数据传输前必须发送控制字节Control Byte0x00命令或0x40数据sendBuffer()必须分两次调用先发0x40再发数据流不可合并I²C 时钟频率建议 ≤ 400kHzFast Mode过高可能导致 LCD 采样失败。5. 实际工程应用案例5.1 基于 FreeRTOS 的双任务 UI 系统在资源受限的 ESP32-S3 上可利用 FreeRTOS 创建分离的任务一个任务负责传感器数据采集与处理另一个任务专注 UI 渲染通过队列传递数据。#include freertos/FreeRTOS.h #include freertos/queue.h #include ST7567_FB_I2C.h ST7567_FB_I2C lcd(4, 0x3F); QueueHandle_t ui_queue; // UI 任务消费队列数据并刷新屏幕 void ui_task(void* pvParameters) { struct ui_data_t { int temp; float humidity; }; struct ui_data_t data; while(1) { if (xQueueReceive(ui_queue, data, portMAX_DELAY) pdTRUE) { lcd.cls(); lcd.printStr(0, 0, Sensor Data:); char buf[16]; sprintf(buf, Temp: %d C, data.temp); lcd.printStr(0, 12, buf); sprintf(buf, Humi: %.1f%%, data.humidity); lcd.printStr(0, 24, buf); lcd.display(); } } } // 传感器任务生产数据 void sensor_task(void* pvParameters) { struct ui_data_t data; while(1) { data.temp read_temperature(); // 伪代码 data.humidity read_humidity(); // 伪代码 xQueueSend(ui_queue, data, 0); vTaskDelay(2000 / portTICK_PERIOD_MS); } } void setup() { ui_queue xQueueCreate(5, sizeof(struct ui_data_t)); xTaskCreate(ui_task, UI, 2048, NULL, 1, NULL); xTaskCreate(sensor_task, Sensor, 2048, NULL, 1, NULL); }5.2 低功耗待机模式实现ST7567 支持睡眠模式Sleep Mode可将功耗降至 1μA。通过sleep()和wake()函数控制// 进入睡眠关闭显示保留显存 void sleep() { sendCommand(0xAE); // Display OFF sendCommand(0xAC); // Static indicator OFF sendCommand(0xA4); // Normal display (not all off) sendCommand(0x10); // Set high column 0 sendCommand(0x00); // Set low column 0 sendCommand(0xB0); // Set page 0 sendCommand(0xAF); // Display ON (redundant, but safe) } // 唤醒恢复显示 void wake() { sendCommand(0xAF); // Display ON }在电池供电设备中可在loop()空闲时调用lcd.sleep()检测到按键中断后再调用lcd.wake()并刷新 UI实现毫微安级待机。6. 常见问题与调试技巧6.1 屏幕全黑/全白/花屏故障排查现象最可能原因解决方案全黑VCC未接或电压不足RST未释放持续低电平用万用表测 VCC 是否为 3.3V检查RST引脚电平是否为高全白V0电压过低sendCommand(0x20)COM方向配置错误0xC8误设增大V0值如0x27检查sendCommand(0xC0)是否正确花屏/错位I²C 地址错误sendBuffer()未发送控制字节0x40帧缓冲区越界写入用 I²C 扫描工具确认地址审查sendBuffer()实现启用#define DEBUG_FRAMEBUFFER检查缓冲区访问6.2 性能瓶颈分析与优化瓶颈 1display()刷新慢原因I²C 速率默认 100kHz。解决在Wire.begin()后调用Wire.setClock(400000)提升至 400kHz需确保线路质量。瓶颈 2printStr()卡顿原因比例字体查询开销大。解决对固定文本预渲染为位图用drawBitmap()直接绘制。瓶颈 3抖动计算耗时原因ditherImage()为纯 C 实现。解决对静态图像预先离线抖动仅存储结果位图。该库的工程价值在于其确定性行为——所有函数执行时间可精确预估drawPixel: 0.8μsfillRect(128,64): 1.2msdisplay(): 8.5ms 400kHz这使得它成为硬实时嵌入式 UI 开发的可靠基石。