ssd1306驱动要点

本文说明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中有如下定义

#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中就体现为

	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,然后发命令数据即可

	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的代码

	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,对上层使用的接口定义如下

//写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 内函数指针对应的函数即可,如下

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函数指针原型如下

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的实现:

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就必须支援写任意矩形,改造如下:

	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, 下面两个函数就是解决这个问题的,这里不做详细分析了,有兴趣可以看看代码

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