Zephyr OS驱动0.96OLED
0.前言
本节在之前搭建的Zephyr环境下,实现合宙ESP32C3开发板驱动0.96寸OLED的功能,旨在熟悉zephyr的开发流程。
一、文件添加
按照官方推荐的目录结构,在zephyr同级目录下,创建app目录,用于顶层的应用开发。并创建相关的源文件、cmake文件、工程配置文件和设备树插件:

二、添加板级支持及编译逻辑
在CMakeLists.txt中,添加如下内容,将顶层源代码依赖于中间层的zyphyr os:
bash
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(ssd1306_oled_demo)
target_sources(app PRIVATE src/main.c)
修改 esp32c3_luatos_core.overlay 文件,对开发板上的管脚功能进行配置。
这里将 esp32c3 的 i2c0(GPIO4,GPIO5)进行使能,并在节点下添加 ssd1306 的子节点,compatible 属性用于和源码中的驱动进行匹配。
bash
/ {
chosen {
zephyr,display = &ssd1306;
};
};
&i2c0 {
status = "okay";
ssd1306: ssd1306@3c {
compatible = "solomon,ssd1306fb";
reg = <0x3c>;
width = <128>;
height = <64>;
segment-offset = <0>;
page-offset = <0>;
display-offset = <0>;
multiplex-ratio = <63>;
segment-remap;
com-invdir;
prechargep = <0x22>;
};
};
修改 prj.conf ,添加相关的依赖库支持包进行编译:
bash
# 显示驱动配置
CONFIG_DISPLAY=y
CONFIG_SSD1306=y
# I2C 驱动配置
CONFIG_I2C=y
# 日志配置
CONFIG_LOG=y
CONFIG_LOG_MODE_IMMEDIATE=y
CONFIG_PRINTK=y
CONFIG_CONSOLE=y
CONFIG_SERIAL=y
CONFIG_UART_CONSOLE=y
# 显示驱动日志
CONFIG_DISPLAY_LOG_LEVEL_INF=y
也可以通过 west build -t menuconfig 命令到图形化界面中进行查找和添加。
三、业务代码
在main.c中,手动实现 ssd1306 的一些特定功能代码:
c
/*
* ESP32-C3 LuatOS Core SSD1306 OLED 示例程序
*
* 硬件连接:
* - SSD1306 SDA -> GPIO4
* - SSD1306 SCL -> GPIO5
* - SSD1306 VCC -> 3.3V
* - SSD1306 GND -> GND
*/
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/display.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(ssd1306_demo, LOG_LEVEL_INF);
/* 在屏幕上绘制一个填充矩形 */
static void draw_filled_rect(const struct device *dev, uint16_t x, uint16_t y,
uint16_t width, uint16_t height, bool fill)
{
struct display_buffer_descriptor buf_desc;
uint8_t *buf;
size_t buf_size;
/* SSD1306 使用 MONO01 像素格式,每个像素1位 */
buf_size = (width * height + 7) / 8;
buf = k_malloc(buf_size);
if (!buf) {
LOG_ERR("Failed to allocate buffer");
return;
}
/* 填充缓冲区 */
memset(buf, fill ? 0xFF : 0x00, buf_size);
buf_desc.buf_size = buf_size;
buf_desc.width = width;
buf_desc.height = height;
buf_desc.pitch = width;
display_write(dev, x, y, &buf_desc, buf);
k_free(buf);
}
/* 清空整个屏幕 */
static void clear_display(const struct device *dev,
struct display_capabilities *caps)
{
draw_filled_rect(dev, 0, 0, caps->x_resolution, caps->y_resolution, false);
}
/* 绘制简单的图案 */
static void draw_pattern(const struct device *dev,
struct display_capabilities *caps)
{
/* 绘制边框 */
draw_filled_rect(dev, 0, 0, caps->x_resolution, 2, true); /* 顶部 */
draw_filled_rect(dev, 0, caps->y_resolution - 8,
caps->x_resolution, 2, true); /* 底部 */
draw_filled_rect(dev, 0, 0, 2, caps->y_resolution, true); /* 左侧 */
draw_filled_rect(dev, caps->x_resolution - 2, 0,
2, caps->y_resolution, true); /* 右侧 */
/* 在中心绘制一个小矩形 */
uint16_t center_x = caps->x_resolution / 2 - 16;
uint16_t center_y = caps->y_resolution / 2 - 8;
draw_filled_rect(dev, center_x, center_y, 32, 16, true);
/* 绘制四个角的小方块 */
draw_filled_rect(dev, 8, 8, 8, 8, true); /* 左上 */
draw_filled_rect(dev, caps->x_resolution - 16, 8, 8, 8, true); /* 右上 */
draw_filled_rect(dev, 8, caps->y_resolution - 16, 8, 8, true); /* 左下 */
draw_filled_rect(dev, caps->x_resolution - 16,
caps->y_resolution - 16, 8, 8, true); /* 右下 */
}
/* 绘制动画效果 - 移动的方块 */
static void draw_moving_box(const struct device *dev,
struct display_capabilities *caps,
uint16_t pos)
{
const uint16_t box_size = 8;
uint16_t x = pos % (caps->x_resolution - box_size);
uint16_t y = 24;
/* 清除上一帧的方块区域 */
draw_filled_rect(dev, 0, y, caps->x_resolution, box_size, false);
/* 绘制新位置的方块 */
draw_filled_rect(dev, x, y, box_size, box_size, true);
}
int main(void)
{
const struct device *display_dev;
struct display_capabilities caps;
uint16_t frame = 0;
LOG_INF("ESP32-C3 LuatOS Core - SSD1306 OLED Demo");
/* 获取显示设备 */
display_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_display));
if (!device_is_ready(display_dev)) {
LOG_ERR("Display device not ready");
return -1;
}
LOG_INF("Display device: %s", display_dev->name);
/* 获取显示能力 */
display_get_capabilities(display_dev, &caps);
LOG_INF("Resolution: %dx%d", caps.x_resolution, caps.y_resolution);
LOG_INF("Pixel format: %d", caps.current_pixel_format);
/* 清空屏幕 */
clear_display(display_dev, &caps);
/* 关闭屏幕消隐(使能显示) */
display_blanking_off(display_dev);
/* 显示静态图案 */
LOG_INF("Drawing pattern...");
draw_pattern(display_dev, &caps);
/* 等待3秒 */
k_sleep(K_SECONDS(3));
/* 清空屏幕准备动画 */
clear_display(display_dev, &caps);
/* 显示动画 - 移动的方块 */
LOG_INF("Starting animation...");
while (1) {
draw_moving_box(display_dev, &caps, frame);
frame += 2;
/* 每50ms更新一次 */
k_sleep(K_MSEC(50));
/* 每隔一段时间重新绘制静态图案 */
if (frame >= 500) {
clear_display(display_dev, &caps);
draw_pattern(display_dev, &caps);
k_sleep(K_SECONDS(2));
clear_display(display_dev, &caps);
frame = 0;
}
}
return 0;
}
这段代码中主要封装了一个填充矩形的代码,其他的代码以此为基础,进行矩形的平移、指定位置填充等操作。
显示节点的主要使用方法为:
在main函数中通过 display_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_display));函数,查找在设备树中指定的 display 显示节点,然后通过 display_write 接口,将缓冲区数据写入到这个显示节点中。