ssd1306驱动要点

Creative Commons
本作品采用知识共享署名

本文说明i2c的ssd1306驱动要点,帮助理解zephyr的ssd3306驱动。

地址

ssd1306的i2c地址和一般的i2c slave地址一样是7bit,在7bit后跟1bit读写标志,如下图:
addr
其中SA0可以通过硬件选择是0还是1对应地址0x3c和0x3d,一般我们购买的模块都是选择的0,所以ssd1306的i2c salve地址是0x3c。
需要注意的是一些ssd1306的示例程序按照另外一种情况将1bit读写标志都算入地址,代码中就会写成0x78和0x7A。来至于买的模块上也这样丝印,如下图
sela
在zephyr中使用的标准的i2c slave地址,也就是0x3c

命令

当主机发送地址ssd1306回复ACK后就可以发送命令或数据了,如下:
sq
对于命令数据发送的格式是一个Control byte对应一个命令数据,如下
[Control byte+1 byte data]+[Control byte+1 byte data0]….
对应图像数据发送的格式是一个Control byte对应多个图像数据,如下
Control byte+1 byte data+1 byte data+1 byte data…

Control Byte

Co位,1表示后面还有数据,0表示是最后一个数据
D/C位, 1表示传输的图像数据,0表示的传输的是命令数据
剩余的6bit为全0
因此在Zephyr的ssd1306_reg中有如下定义

1
2
3
4
#define SSD1306_CONTROL_LAST_BYTE_CMD		0x00
#define SSD1306_CONTROL_LAST_BYTE_DATA 0x40
#define SSD1306_CONTROL_BYTE_CMD 0x80
#define SSD1306_CONTROL_BYTE_DATA 0xc0

写命令

发多条命令

[Control byte+1 byte data]+[Control byte+1 byte data0]…. 在zephyr的sd1306中就体现为

1
2
3
4
5
6
7
8
9
10
u8_t cmd_buf[] = {
SSD1306_CONTROL_BYTE_CMD, //一个control byte,后续还有其它的control byte+DATA要发送
SSD1306_SET_CONTRAST_CTRL, //一个数据
SSD1306_CONTROL_LAST_BYTE_CMD, //一个control byte, 后续无其它的数据要发
contrast, //一个数据
};

//i2c发数据,会先发地址出去,收到ack后按顺序将cmd_buf内容发到sd1306
return i2c_write(driver->i2c, cmd_buf, sizeof(cmd_buf),
DT_INST_0_SOLOMON_SSD1306FB_BASE_ADDRESS);

发一条命令

Control byte直接使用SSD1306_CONTROL_LAST_BYTE_CMD,然后发命令数据即可

1
2
return i2c_reg_write_byte(driver->i2c, DT_INST_0_SOLOMON_SSD1306FB_BASE_ADDRESS,
SSD1306_CONTROL_LAST_BYTE_CMD, SSD1306_DISPLAY_ON);

写数据

因为ssd1306在写数据后一般不会再执行其它命令,所以大多都是使用SSD1306_CONTROL_LAST_BYTE_DATA,下面是zephyr填一个page的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
	u8_t cmd_buf[] = {
SSD1306_CONTROL_BYTE_CMD,
SSD1306_SET_MEM_ADDRESSING_MODE,
SSD1306_CONTROL_BYTE_CMD,
SSD1306_SET_MEM_ADDRESSING_PAGE,
SSD1306_CONTROL_BYTE_CMD,
SSD1306_SET_LOWER_COL_ADDRESS |
(col & SSD1306_SET_LOWER_COL_ADDRESS_MASK),
SSD1306_CONTROL_BYTE_CMD,
SSD1306_SET_HIGHER_COL_ADDRESS |
((col >> 4) & SSD1306_SET_LOWER_COL_ADDRESS_MASK),
SSD1306_CONTROL_LAST_BYTE_CMD,
SSD1306_SET_PAGE_START_ADDRESS | page
};

//写命令数据,设置ssd1306的显存填写方式和起始地址
if (i2c_write(driver->i2c, cmd_buf, sizeof(cmd_buf),
DT_INST_0_SOLOMON_SSD1306FB_BASE_ADDRESS)) {
return -1;
}

//使用Control byte SSD1306_CONTROL_LAST_BYTE_DATA 一次性将长度为length的显存数据送到显存data
return i2c_burst_write(driver->i2c, DT_INST_0_SOLOMON_SSD1306FB_BASE_ADDRESS,
SSD1306_CONTROL_LAST_BYTE_DATA,
data, length);
}

SSD1306命令

本文只是帮助理解zephyr的SSD1306驱动,具体命令意义请直接参考SSD1306规格书
http://www.gabotronics.com/download/datasheets/ssd1306.pdf

显存

ssd1306是128 64的单色显示屏,其内部显存大小为128 64 bit

显存结构

128 * 64的大小,将其在垂直方向分为8个page,每个page8行,将其在水平方向分为128列,如下图:
gdram
当一个数据字节被写入显存时,当前列的同一页面的所有行图像数据被填充。数据位D0写入顶行,而数据位D7写入底行,如下图:
page

显存填充方式

ssd1306有3中填充方式,无论那种填充方式,起点地址都是已page和列来指定,可以任意指定page和列,但不能指定page内的某一行。

page模式

在page模式下,读取/写入显示RAM后,列地址指针自动增加1. 如果列地址指针到达列结束地址,则列地址指针将复位为列起始地址,而page地址指针则不会改变。 用户必须设置新的page和列地址才能访问下一页RAM内容。
pm

水平模式

在水平寻址模式下,读/写显示RAM后,列地址指针自动增加1.如果列地址指针到达列结束地址,则列地址指针复位为列起始地址,页地址指针增加 当列和页地址指针都到达结束地址时,指针将重置为列起始地址和页面起始地址,如下图:
vm

垂直模式

在垂直寻址模式下,在读/写显示RAM后,页面地址指针自动增加1.如果页面地址指针到达页面结束地址,则页面地址指针被重置为页面起始地址,列地址指针为 增加1. 当列和页地址指针都到达结束地址时,指针将重置为列起始地址和页面起始地址,如下图:
hm

Zephyr display driver 模型

接口

zephyr的dirsplayer driver模型参看文件display.h,对上层使用的接口定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//写framebuffer
static inline int display_write(const struct device *dev, const u16_t x,
const u16_t y,
const struct display_buffer_descriptor *desc,
const void *buf);
/读framebuffer
static inline int display_read(const struct device *dev, const u16_t x,
const u16_t y,
const struct display_buffer_descriptor *desc,
void *buf);
//获取display device的framebuffer
static inline void *display_get_framebuffer(const struct device *dev);
//关屏
static inline int display_blanking_on(const struct device *dev);
//开屏
static inline int display_blanking_off(const struct device *dev);
//设置亮度
static inline int display_set_brightness(const struct device *dev,
u8_t brightness);
//设置对比度
static inline int display_set_contrast(const struct device *dev, u8_t contrast);
//获取display driver的能力,例如宽,高,色彩等
static inline void display_get_capabilities(const struct device *dev,
struct display_capabilities *
capabilities);
//设置pixel format
static inline int
display_set_pixel_format(const struct device *dev,
const enum display_pixel_format pixel_format);
//设置显示方向
static inline int display_set_orientation(const struct device *dev,
const enum display_orientation
orientation);

实现

参考ssd1306.c,实现的方法就是在驱动中按照ssd1306的硬件特性实现struct display_driver_api 内函数指针对应的函数即可,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static struct display_driver_api ssd1306_driver_api = {
.blanking_on = ssd1306_suspend,
.blanking_off = ssd1306_resume,
.write = ssd1306_write,
.read = ssd1306_read,
.get_framebuffer = ssd1306_get_framebuffer,
.set_brightness = ssd1306_set_brightness,
.set_contrast = ssd1306_set_contrast,
.get_capabilities = ssd1306_get_capabilities,
.set_pixel_format = ssd1306_set_pixel_format,
.set_orientation = ssd1306_set_orientation,
};

DEVICE_AND_API_INIT(ssd1306, DT_INST_0_SOLOMON_SSD1306FB_LABEL, ssd1306_init,
&ssd1306_driver, NULL,
POST_KERNEL, CONFIG_APPLICATION_INIT_PRIORITY,
&ssd1306_driver_api);

由于display硬件的不同除了write和get_capabilities是必须的外,其它实现函数内可以直接留空函数不支援对应功能。对于ssd1306的write函数我们再详细展开一下

write

write函数指针原型如下

1
2
3
4
5
6
7
8
9
10
11
struct display_buffer_descriptor {
u32_t buf_size;
u16_t width;
u16_t height;
u16_t pitch;
};

typedef int (*display_write_api)(const struct device *dev, const u16_t x,
const u16_t y,
const struct display_buffer_descriptor *desc,
const void *buf);

从write的参数可以看到希望能写任意矩形区域x,y为起点 desc->width和desc->height为宽高, 再来看下Zephyr的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int ssd1306_write(const struct device *dev, const u16_t x, const u16_t y,
const struct display_buffer_descriptor *desc,
const void *buf){
if (x != 0U && y != 0U) {
LOG_ERR("Unsupported origin");
return -1;
}
u8_t cmd_buf[] = {
SSD1306_CONTROL_BYTE_CMD,
SSD1306_SET_MEM_ADDRESSING_MODE,
SSD1306_CONTROL_BYTE_CMD,
SSD1306_ADDRESSING_MODE,
SSD1306_CONTROL_BYTE_CMD,
SSD1306_SET_COLUMN_ADDRESS,
SSD1306_CONTROL_BYTE_CMD,
0,
SSD1306_CONTROL_BYTE_CMD,
(SSD1306_PANEL_NUMOF_COLUMS - 1),
SSD1306_CONTROL_BYTE_CMD,
SSD1306_SET_PAGE_ADDRESS,
SSD1306_CONTROL_BYTE_CMD,
0,
SSD1306_CONTROL_LAST_BYTE_CMD,
(SSD1306_PANEL_NUMOF_PAGES - 1)
};

if (i2c_write(driver->i2c, cmd_buf, sizeof(cmd_buf),
DT_INST_0_SOLOMON_SSD1306FB_BASE_ADDRESS)) {
LOG_ERR("Failed to write command");
return -1;
}

return i2c_burst_write(driver->i2c, DT_INST_0_SOLOMON_SSD1306FB_BASE_ADDRESS,
SSD1306_CONTROL_LAST_BYTE_DATA,
(u8_t *)buf, desc->buf_size);
}

很遗憾,限制了只能从0,0开始写,大多数情况下ssd1306会在驱动外部在建立一个1K的全framebuffer缓存,每次都写全屏。但我想在zephyr上让lvgl能跑在ssd1306就必须支援写任意矩形,改造如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if ((y & 0x7) != 0U) {
LOG_ERR("Unsupported origin");
return -1;
}

u8_t cmd_buf[] = {
SSD1306_CONTROL_BYTE_CMD,
SSD1306_SET_MEM_ADDRESSING_MODE,
SSD1306_CONTROL_BYTE_CMD,
SSD1306_ADDRESSING_MODE,
SSD1306_CONTROL_BYTE_CMD,
SSD1306_SET_COLUMN_ADDRESS,
SSD1306_CONTROL_BYTE_CMD,
x,
SSD1306_CONTROL_BYTE_CMD,
(x + desc->width - 1),
SSD1306_CONTROL_BYTE_CMD,
SSD1306_SET_PAGE_ADDRESS,
SSD1306_CONTROL_BYTE_CMD,
y/8,
SSD1306_CONTROL_LAST_BYTE_CMD,
((y + desc->height)/8 - 1)
};

这样可以写指定的矩形了,不过可能你已经看到了代码里还是要求了page对齐(y&0x7),这会对lvgl工作造成影响吗? lvgl已经考虑了display driver的特殊性,可以看zephyr的lib/gui/lvgl/lvgl_display.c, 下面两个函数就是解决这个问题的,这里不做详细分析了,有兴趣可以看看代码

1
2
disp_drv->rounder_cb = lvgl_rounder_cb_mono;
disp_drv->set_px_cb = lvgl_set_px_cb_mono;