SSD1306 显示驱动与字体渲染
[中文正文内容]
SSD1306 显示驱动与字体渲染
Drew
2025年4月14日 • 阅读时长5分钟
最初在我的原型机上实现 SSD1306 OLED 时,我选择了最快、最容易实现的驱动程序——一个 Espressif 提供的驱动程序,作为 ESP-BSP 的一部分,但后来已被移除。它工作得很好,以大约 40Hz 的频率更新屏幕,并且资源占用非常少。然而,它只支持一种字体,并且不容易添加其他字体,需要大量工作才能将每个字形转换为特定的 C 数组格式。我想添加另一种字体,所以我开始寻找其他选择。
该驱动程序已不再受支持,Espressif 已将其替换为较低级别的驱动程序,该驱动程序实际上不支持任何字体,仅支持直接位图绘制。现在的建议是使用 LVGL,这是一个功能齐全的图形堆栈,具有小部件、按钮和各种各样的东西。所以我开始实现 LVGL,它确实很好地支持添加您自己的字体,但在 ESP32 上(以全速 I2C,400khz 运行)无法超过大约 18 - 20 Hz。它还设置了自己的定时器和绘图循环,经过一段时间的摆弄后,我无法使显示器更快地更新。此外,无论有多少工作要做,绘图循环始终占用 ESP32 核心的约 5%——这对我来说感觉不太合适。另外,我没有对此进行太多研究,当我使用 LVGL 时,我的 SSD1306 显示器会发出可听见的呜呜声。也许我可以调试一下,但我已经计划好继续前进了。
LVGL 对于我的用例来说...有点太多了
U8G2 是一个流行的库,支持数十种小型显示器,包括 SSD1306。我以前使用过它,所以我想我会再次尝试一下。它内置了大量字体,并且有一个系统可以将其他字体导入到其内部格式中。它本身不支持 ESP-IDF,但有一个 wrapper 效果很好。然而,在实现这一点后,我再次遇到了更新速度慢的问题——我能达到的最快速度约为 18 Hz(在 400khz I2C 下)。经过一些研究,其他人也注意到了同样的事情,但共识似乎是这已经足够好了。对我来说不够好!
我发现了 另一个有希望的 SSD1306 驱动程序,当单独运行一个简单的文本测试时,可以达到 30+ Hz。它还有一个支持 BDF(一种流行的旧字体格式)字体的示例,因此看起来很有希望。然而,由于我 能够修复的原因,该示例的速度非常慢,这让我有点困惑。此外,BDF 示例的字距微调不太正确,我不太热衷于尝试修复它。最后,当我在我的完整合成器项目中包含该驱动程序时,它使用的资源比我预期的要多,并且绘制速度不够快。
字距微调很重要
此时我几乎绝望了。我知道 AdafruitGFX 是一个流行的图形库,但它本身不支持 ESP-IDF(仅支持 Arduino)。为了在我的项目中实现它,我必须引入一个 Arduino 兼容层,即使这样,我也不知道性能或资源使用情况,所以感觉像是一个可能不会有回报的风险。
我决定回到我所知的唯一一个工作得很好的驱动程序——已被弃用的 ESP-BSP 驱动程序。此后,我已将我的 ESP-IDF 版本升级到 5.4.x,并且实际上无法再按原样使用该驱动程序,因为它仅支持现在称为“传统” I2C 驱动程序。所以我将代码分叉到我自己的存储库中,并替换了所有 I2c API 调用(我已经用我的 ES8388 驱动程序完成了此操作),它就可以工作了。而且它仍然很快!比我使用过的任何其他驱动程序都快。为什么它更快?我的直觉是因为它以单个事务将帧缓冲区数据推送到 I2C 总线的方式,而 U8G2 以块的形式将字节推送到显示器,我猜是因为它支持许多不同的显示器变体(我不确定 LVGL 或其他驱动程序,但可能类似)。
static esp_err_t ssd1306_write_data(ssd1306_handle_t dev, const uint8_t *const data, const uint16_t data_len)
{
ssd1306_dev_t *device = (ssd1306_dev_t *) dev;
esp_err_t ret;
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
ret = i2c_master_start(cmd);
assert(ESP_OK == ret);
ret = i2c_master_write_byte(cmd, device->dev_addr | I2C_MASTER_WRITE, true);
assert(ESP_OK == ret);
ret = i2c_master_write_byte(cmd, SSD1306_WRITE_DAT, true);
assert(ESP_OK == ret);
ret = i2c_master_write(cmd, data, data_len, true);
assert(ESP_OK == ret);
ret = i2c_master_stop(cmd);
assert(ESP_OK == ret);
ret = i2c_master_cmd_begin(device->bus, cmd, 1000 / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
return ret;
}
...
static esp_err_t ssd1306_write_data(ssd1306_handle_t dev, const uint8_t *const data, const uint16_t data_len) {
ssd1306_dev_t *device = (ssd1306_dev_t *)dev;
esp_err_t ret;
uint8_t *out_buf = (uint8_t *)calloc(data_len + 1, sizeof(uint8_t));
out_buf[0] = SSD1306_WRITE_DAT;
memcpy(out_buf + 1, data, data_len);
ret = i2c_master_transmit(device->i2c_dev_handle, out_buf, data_len + 1, 1000);
free(out_buf);
return ret;
}
传统 I2C API 与新的 I2C API
但我基本上回到了原点。我有一个工作得很好且速度很快的显示驱动程序,但它只支持一种字体。
此时,我开始更多地考虑自己处理字体绘制 - 我可以添加一个库来将字体渲染成位图,然后使用我的驱动程序直接绘制位图吗?经过一番研究,我遇到了 nvbdflib,它实际上直接解析 BDF 字体,并允许您提供自己的绘图函数!这似乎很有希望 - 也许我可以包含这个库,给它一个 BDF 字体和一个直接绘制到我的帧缓冲区的函数,完全跳过中间位图。
void bdf_drawing_function(int x, int y, int c, void *ctx) {
ssd1306_dev_t *device = (ssd1306_dev_t *)ctx;
ssd1306_fill_point(device, x, y, c);
}
...
bdfSetDrawingFunction(bdf_drawing_function, (void *)device);
nvbdflib 允许您直接传入绘图函数
这花了我一点时间 - 我的设备上还没有文件系统,所以为了将 BDF 文件加载到缓冲区中,我使用了 这个(或者可以编译为目标文件,但不确定如何使用 ESP-IDF 执行此操作)。它奏效了!它确实将整个字体加载到内存中,但 BDF 格式只是纯文本,所以我编辑并将其修剪到我需要的 94 个字符。我加载的修剪后的字体,即 1987 年的 8x16 IBM VGA 字体,大约为 10 kb。我的项目没有内存限制 - CPU 是我的主要限制 - 因此对于能够非常轻松地插入另一个字体而无需编译成中间格式来说,这是一个完全可以接受的折衷方案(提高内存使用率的方法是添加该编译步骤,但这对于我的用例并不重要)。在对 nvbdflib 进行一些调整以将使用者的上下文传递到提供的绘图函数中之后,我使该库在显示驱动程序内部运行良好。
STARTFONT 2.1
FONT -IBM-VGA-Normal-R-Normal--16-120-96-96-C-80-ISO10646-1
SIZE 12 96 96
FONTBOUNDINGBOX 8 16 0 -4
STARTPROPERTIES 34
FOUNDRY "IBM"
FAMILY_NAME "VGA 8x16"
WEIGHT_NAME "Normal"
SLANT "R"
SETWIDTH_NAME "Normal"
ADD_STYLE_NAME ""
PIXEL_SIZE 16
POINT_SIZE 120
RESOLUTION_X 96
RESOLUTION_Y 96
BDF 字体只是纯文本,这使得编辑它们变得容易
这就是我现在的状态 - 我有一个 SSD1306 显示驱动程序,能够达到全速 (40 Hz),但同时也支持 BDF 格式的任何字体!我将继续改进驱动程序并添加我需要的东西 - 例如,计算字符串的边界框 - 但我对它现在的状态感觉良好。没有大的依赖项,没有兼容性层,并且使用现代 I2C API。真棒!