👏

STM32でZephyrRTOS入門~SPIでOLED(SH1107)制御~

に公開

はじめに

これまでWindows上にZephyrRTOSの開発環境を構築し、NucleoF103RBボードを用いて学習を進めてきた。
https://zenn.dev/gotoooo/articles/894213f95372bd

今回はSPI接続のOLEDディスプレイ制御に挑戦する。使用したOLEDは以下の通りである。
Grove - OLED Display 1.12" SH1107 V3.0

このOLEDは購入時点ではI2C接続で動作する配線となっているが、パターンカットとはんだ付けを行うことでSPI接続に変更できる。
また、このOLEDに搭載されるSH1107コントローラはZephyrでは公式でサポートされていないため、今回もドライバの自作から取り組むことにした。

NucleoF103RBとOLEDの接続

OLED NucleoF103RB
CS PB6
D/C PA9
RES PA8
SI MOSI(PA7)
SCL SCK(PA5)
5V 5V
GND GND

Zephyrで自作ドライバを作成する手順

大まかな流れは以下の通り。

  1. 自作ドライバのコードを作成
  2. ビルドシステム用の設定ファイルを作成
  3. バインディングファイルを作成
  4. モジュール定義ファイルを作成

1. 自作ドライバのコードを作成

以前自作したカラーセンサ(TCS34725)ドライバ同様、以下ディレクトリ構成でSH1107ドライバを作成した。

modules/
  sh1107/
    drivers/
      display/
        sh1107/
          sh1107.c
          sh1107_reg.h

「sh1107.c」、「sh1107_reg.h」はZephyr公式のSSD1306ドライバをベースとし、SH1107特有の初期化シーケンスはArduino向けライブラリを参考に実装を進めた。

ソースコードは以下の通りである。

sh1107_reg.h
sh1107_reg.h
#ifndef __SH1107_REGS_H__
#define __SH1107_REGS_H__

/* All following bytes will contain commands */
#define SH1107_CONTROL_ALL_BYTES_CMD		0x00
/* All following bytes will contain data */
#define SH1107_CONTROL_ALL_BYTES_DATA		0x40
/* The next byte will contain a command */
#define SH1107_CONTROL_BYTE_CMD		0x80
/* The next byte will contain data */
#define SH1107_CONTROL_BYTE_DATA		0xc0
#define SH1107_READ_STATUS_MASK		0xc0
#define SH1107_READ_STATUS_BUSY		0x80
#define SH1107_READ_STATUS_ON			0x40

/*
 * Fundamental Command Table
 */
#define SH1107_SET_CONTRAST_CTRL		0x81 /* double byte command */

#define SH1107_SET_ENTIRE_DISPLAY_OFF		0xa4
#define SH1107_SET_ENTIRE_DISPLAY_ON		0xa5

/* RAM data of 1 indicates an "ON" pixel */
#define SH1107_SET_NORMAL_DISPLAY		0xa6
/* RAM data of 0 indicates an "ON" pixel */
#define SH1107_SET_REVERSE_DISPLAY		0xa7

#define SH1107_DISPLAY_OFF			0xae
#define SH1107_DISPLAY_ON			0xaf

/*
 * Addressing Setting Command Table
 */
#define SH1107_SET_LOWER_COL_ADDRESS		0x00
#define SH1107_SET_LOWER_COL_ADDRESS_MASK	0x0f

#define SH1107_SET_HIGHER_COL_ADDRESS		0x10
#define SH1107_SET_HIGHER_COL_ADDRESS_MASK	0x0f

#define SH1107_SET_MEM_ADDRESSING_MODE		0x20 /* double byte command */
#define SH1107_SET_MEM_ADDRESSING_HORIZONTAL	0x00
#define SH1107_SET_MEM_ADDRESSING_VERTICAL	0x01
#define SH1107_SET_MEM_ADDRESSING_PAGE		0x02

#define SH1107_SET_COLUMN_ADDRESS		0x21 /* triple byte command */

#define SH1107_SET_PAGE_ADDRESS		0x22 /* triple byte command */

#define SH1107_SET_PAGE_START_ADDRESS		0xb0
#define SH1107_SET_PAGE_START_ADDRESS_MASK	0x07


/*
 * Hardware Configuration Command Table
 */
// #define SH1107_SET_START_LINE			0x40
#define SH1107_SET_START_LINE			0xdc
#define SH1107_SET_START_LINE_MASK		0x3f

#define SH1107_SET_SEGMENT_MAP_NORMAL		0xa0
#define SH1107_SET_SEGMENT_MAP_REMAPED		0xa1

#define SH1107_SET_MULTIPLEX_RATIO		0xa8 /* double byte command */

#define SH1107_SET_COM_OUTPUT_SCAN_NORMAL	0xc0
#define SH1107_SET_COM_OUTPUT_SCAN_FLIPPED	0xc8

#define SH1107_SET_DISPLAY_OFFSET		0xd3 /* double byte command */

#define SH1107_SET_PADS_HW_CONFIG		0xda /* double byte command */
#define SH1107_SET_PADS_HW_SEQUENTIAL		0x02
#define SH1107_SET_PADS_HW_ALTERNATIVE         0x12

#define SH1107_SET_IREF_MODE			0xad
#define SH1107_SET_IREF_MODE_INTERNAL		0x30
#define SH1107_SET_IREF_MODE_EXTERNAL		0x00


/*
 * Timing and Driving Scheme Setting Command Table
 */
#define SH1107_SET_CLOCK_DIV_RATIO		0xd5 /* double byte command */

#define SH1107_SET_CHARGE_PERIOD		0xd9 /* double byte command */

#define SH1107_SET_VCOM_DESELECT_LEVEL		0xdb /* double byte command */

#define SH1107_NOP				0xe3

/*
 * Charge Pump Command Table
 */
#define SH1107_SET_CHARGE_PUMP_ON		0x8d /* double byte command */
#define SH1107_SET_CHARGE_PUMP_ON_DISABLED	0x10
#define SH1107_SET_CHARGE_PUMP_ON_ENABLED	0x14

#define SH1106_SET_DCDC_MODE			0xad /* double byte command */
#define SH1106_SET_DCDC_DISABLED		0x8a
#define SH1106_SET_DCDC_ENABLED			0x8b

#define SH1107_SET_PUMP_VOLTAGE_64		0x30
#define SH1107_SET_PUMP_VOLTAGE_74		0x31
#define SH1107_SET_PUMP_VOLTAGE_80		0x32
#define SH1107_SET_PUMP_VOLTAGE_90		0x33

/*
 * Read modify write
 */
#define SH1107_READ_MODIFY_WRITE_START		0xe0
#define SH1107_READ_MODIFY_WRITE_END		0xee

/* time constants in ms */
#define SH1107_RESET_DELAY			10
#define SH1107_SUPPLY_DELAY			20

#endif

sh1107.c

基本的にはsh1306.cをコピーしてsh1306をsh1107に置換したものである。大きく変更したのは以下の関数。

  • sh1107_init_device
    SH1107の初期化シーケンスに基づき実装を変更。

  • sh1107_write
    もともとconfig->sh1106_compatibleのフラグを見てssd1306_write_sh1106()を呼び出していたが、フラグによらず、ssd1306_write_sh1106()相当の処理を行うように変更。

sh1107.c
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(sh1107, CONFIG_DISPLAY_LOG_LEVEL);

#include <string.h>
#include <zephyr/device.h>
#include <zephyr/init.h>
#include <zephyr/drivers/display.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/i2c.h>
#include <zephyr/drivers/spi.h>
#include <zephyr/kernel.h>

#include "sh1107_regs.h"

#define SH1107_CLOCK_DIV_RATIO		0x0
#define SH1107_CLOCK_FREQUENCY		0x8
#define SH1107_PANEL_VCOM_DESEL_LEVEL	0x20
#define SH1107_PANEL_PUMP_VOLTAGE	SH1107_SET_PUMP_VOLTAGE_90

#define SH1107_PANEL_VCOM_DESEL_LEVEL_SSD1309  0x34

#ifndef SH1107_ADDRESSING_MODE
#define SH1107_ADDRESSING_MODE		(SH1107_SET_MEM_ADDRESSING_HORIZONTAL)
#endif

union sh1107_bus {
	struct i2c_dt_spec i2c;
	struct spi_dt_spec spi;
};

typedef bool (*sh1107_bus_ready_fn)(const struct device *dev);
typedef int (*sh1107_write_bus_fn)(const struct device *dev, uint8_t *buf, size_t len,
				    bool command);
typedef const char *(*sh1107_bus_name_fn)(const struct device *dev);

struct sh1107_config {
	union sh1107_bus bus;
	struct gpio_dt_spec data_cmd;
	struct gpio_dt_spec reset;
	struct gpio_dt_spec supply;
	sh1107_bus_ready_fn bus_ready;
	sh1107_write_bus_fn write_bus;
	sh1107_bus_name_fn bus_name;
	uint16_t height;
	uint16_t width;
	uint8_t segment_offset;
	uint8_t page_offset;
	uint8_t display_offset;
	uint8_t multiplex_ratio;
	uint8_t prechargep;
	bool segment_remap;
	bool com_invdir;
	bool com_sequential;
	bool color_inversion;
	bool ssd1309_compatible;
	bool sh1106_compatible;
	int ready_time_ms;
	bool use_internal_iref;
};

struct sh1107_data {
	enum display_pixel_format pf;
};

#if DT_HAS_COMPAT_ON_BUS_STATUS_OKAY(sinowealth_sh1107, i2c)
static bool sh1107_bus_ready_i2c(const struct device *dev)
{
	const struct sh1107_config *config = dev->config;

	return i2c_is_ready_dt(&config->bus.i2c);
}

static int sh1107_write_bus_i2c(const struct device *dev, uint8_t *buf, size_t len, bool command)
{
	const struct sh1107_config *config = dev->config;

	return i2c_burst_write_dt(&config->bus.i2c,
				  command ? SH1107_CONTROL_ALL_BYTES_CMD :
				  SH1107_CONTROL_ALL_BYTES_DATA,
				  buf, len);
}

static const char *sh1107_bus_name_i2c(const struct device *dev)
{
	const struct sh1107_config *config = dev->config;

	return config->bus.i2c.bus->name;
}
#endif

#if DT_HAS_COMPAT_ON_BUS_STATUS_OKAY(sinowealth_sh1107, spi)
static bool sh1107_bus_ready_spi(const struct device *dev)
{
	const struct sh1107_config *config = dev->config;

	if (gpio_pin_configure_dt(&config->data_cmd, GPIO_OUTPUT_INACTIVE) < 0) {
		return false;
	}

	return spi_is_ready_dt(&config->bus.spi);
}

static int sh1107_write_bus_spi(const struct device *dev, uint8_t *buf, size_t len, bool command)
{
	const struct sh1107_config *config = dev->config;
	int ret;

	gpio_pin_set_dt(&config->data_cmd, command ? 0 : 1);
	struct spi_buf tx_buf = {
		.buf = buf,
		.len = len
	};

	struct spi_buf_set tx_bufs = {
		.buffers = &tx_buf,
		.count = 1
	};

	ret = spi_write_dt(&config->bus.spi, &tx_bufs);

	return ret;
}

static const char *sh1107_bus_name_spi(const struct device *dev)
{
	const struct sh1107_config *config = dev->config;

	return config->bus.spi.bus->name;
}
#endif

static inline bool sh1107_bus_ready(const struct device *dev)
{
	const struct sh1107_config *config = dev->config;

	return config->bus_ready(dev);
}

static inline int sh1107_write_bus(const struct device *dev, uint8_t *buf, size_t len,
				    bool command)
{
	const struct sh1107_config *config = dev->config;

	return config->write_bus(dev, buf, len, command);
}

static inline int sh1107_set_panel_orientation(const struct device *dev)
{
	const struct sh1107_config *config = dev->config;
	uint8_t cmd_buf[] = {(config->segment_remap ? SH1107_SET_SEGMENT_MAP_REMAPED
						    : SH1107_SET_SEGMENT_MAP_NORMAL),
			     (config->com_invdir ? SH1107_SET_COM_OUTPUT_SCAN_FLIPPED
						 : SH1107_SET_COM_OUTPUT_SCAN_NORMAL)};

	return sh1107_write_bus(dev, cmd_buf, sizeof(cmd_buf), true);
}

static inline int sh1107_set_timing_setting(const struct device *dev)
{
	const struct sh1107_config *config = dev->config;
	uint8_t cmd_buf[] = {SH1107_SET_CLOCK_DIV_RATIO,
			     (SH1107_CLOCK_FREQUENCY << 4) | SH1107_CLOCK_DIV_RATIO,
			     SH1107_SET_CHARGE_PERIOD,
			     config->prechargep,
			     SH1107_SET_VCOM_DESELECT_LEVEL,
			     config->ssd1309_compatible ? SH1107_PANEL_VCOM_DESEL_LEVEL_SSD1309 :
				SH1107_PANEL_VCOM_DESEL_LEVEL};

	return sh1107_write_bus(dev, cmd_buf, sizeof(cmd_buf), true);
}

static inline int sh1107_set_hardware_config(const struct device *dev)
{
	const struct sh1107_config *config = dev->config;
	uint8_t cmd_buf[] = {
		SH1107_SET_START_LINE,
		SH1107_SET_DISPLAY_OFFSET,
		config->display_offset,
		SH1107_SET_PADS_HW_CONFIG,
		(config->com_sequential ? SH1107_SET_PADS_HW_SEQUENTIAL
					: SH1107_SET_PADS_HW_ALTERNATIVE),
		SH1107_SET_MULTIPLEX_RATIO,
		config->multiplex_ratio,
	};

	return sh1107_write_bus(dev, cmd_buf, sizeof(cmd_buf), true);
}

static inline int sh1107_set_charge_pump(const struct device *dev)
{
	const struct sh1107_config *config = dev->config;
	uint8_t cmd_buf[] = {
		(config->sh1106_compatible ? SH1106_SET_DCDC_MODE : SH1107_SET_CHARGE_PUMP_ON),
		(config->sh1106_compatible ? SH1106_SET_DCDC_ENABLED
					   : SH1107_SET_CHARGE_PUMP_ON_ENABLED),
		SH1107_PANEL_PUMP_VOLTAGE,
	};

	return sh1107_write_bus(dev, cmd_buf, sizeof(cmd_buf), true);
}

static inline int sh1107_set_iref_mode(const struct device *dev)
{
	int ret = 0;
	const struct sh1107_config *config = dev->config;
	uint8_t cmd_buf[] = {
		SH1107_SET_IREF_MODE,
		SH1107_SET_IREF_MODE_INTERNAL,
	};

	if (config->use_internal_iref) {
		ret = sh1107_write_bus(dev, cmd_buf, sizeof(cmd_buf), true);
	}

	return ret;
}

static int sh1107_resume(const struct device *dev)
{
	const struct sh1107_config *config = dev->config;
	uint8_t cmd_buf[] = {
		SH1107_DISPLAY_ON,
	};

	/* Turn on supply if pin connected */
	if (config->supply.port) {
		gpio_pin_set_dt(&config->supply, 1);
		k_sleep(K_MSEC(SH1107_SUPPLY_DELAY));
	}

	return sh1107_write_bus(dev, cmd_buf, sizeof(cmd_buf), true);
}

static int sh1107_suspend(const struct device *dev)
{
	const struct sh1107_config *config = dev->config;
	uint8_t cmd_buf[] = {
		SH1107_DISPLAY_OFF,
	};

	/* Turn off supply if pin connected */
	if (config->supply.port) {
		gpio_pin_set_dt(&config->supply, 0);
		k_sleep(K_MSEC(SH1107_SUPPLY_DELAY));
	}

	return sh1107_write_bus(dev, cmd_buf, sizeof(cmd_buf), true);
}

static int sh1107_write(const struct device *dev, const uint16_t x, const uint16_t y,
			 const struct display_buffer_descriptor *desc, const void *buf)
{
	const struct sh1107_config *config = dev->config;
	size_t buf_len;

	if (desc->pitch < desc->width) {
		LOG_ERR("Pitch is smaller than width");
		return -1;
	}

	buf_len = MIN(desc->buf_size, desc->height * desc->width / 8);
	if (buf == NULL || buf_len == 0U) {
		LOG_ERR("Display buffer is not available");
		return -1;
	}

	if (desc->pitch > desc->width) {
		LOG_ERR("Unsupported mode");
		return -1;
	}

	if ((y & 0x7) != 0U) {
		LOG_ERR("Unsupported origin");
		return -1;
	}

	LOG_DBG("x %u, y %u, pitch %u, width %u, height %u, buf_len %u", x, y, desc->pitch,
		desc->width, desc->height, buf_len);

	uint8_t x_offset = x + config->segment_offset;
	uint8_t cmd_buf[] = {
		SH1107_SET_LOWER_COL_ADDRESS |
			(x_offset & SH1107_SET_LOWER_COL_ADDRESS_MASK),
		SH1107_SET_HIGHER_COL_ADDRESS |
			((x_offset >> 4) & SH1107_SET_LOWER_COL_ADDRESS_MASK),
		SH1107_SET_PAGE_START_ADDRESS | (y / 8)
	};
	uint8_t *buf_ptr = (uint8_t *)buf;

	for (uint8_t n = 0; n < desc->height / 8; n++) {
		cmd_buf[sizeof(cmd_buf) - 1] =
			SH1107_SET_PAGE_START_ADDRESS | (n + (y / 8));
		LOG_HEXDUMP_DBG(cmd_buf, sizeof(cmd_buf), "cmd_buf");

		if (sh1107_write_bus(dev, cmd_buf, sizeof(cmd_buf), true)) {
			return -1;
		}

		if (sh1107_write_bus(dev, buf_ptr, desc->width, false)) {
			return -1;
		}

		buf_ptr = buf_ptr + desc->width;
		if (buf_ptr > ((uint8_t *)buf + buf_len)) {
			LOG_ERR("Exceeded buffer length");
			return -1;
		}
	}

	return 0;
}

static int sh1107_set_contrast(const struct device *dev, const uint8_t contrast)
{
	uint8_t cmd_buf[] = {
		SH1107_SET_CONTRAST_CTRL,
		contrast,
	};

	return sh1107_write_bus(dev, cmd_buf, sizeof(cmd_buf), true);
}

static void sh1107_get_capabilities(const struct device *dev,
				     struct display_capabilities *caps)
{
	const struct sh1107_config *config = dev->config;
	struct sh1107_data *data = dev->data;

	caps->x_resolution = config->width;
	caps->y_resolution = config->height;
	caps->supported_pixel_formats = PIXEL_FORMAT_MONO10 | PIXEL_FORMAT_MONO01;
	caps->current_pixel_format = data->pf;
	caps->screen_info = SCREEN_INFO_MONO_VTILED;
	caps->current_orientation = DISPLAY_ORIENTATION_NORMAL;
}

static int sh1107_set_pixel_format(const struct device *dev,
				    const enum display_pixel_format pf)
{
	struct sh1107_data *data = dev->data;
	uint8_t cmd;
	int ret;

	if (pf == data->pf) {
		return 0;
	}

	if (pf == PIXEL_FORMAT_MONO10) {
		cmd = SH1107_SET_REVERSE_DISPLAY;
	} else if (pf == PIXEL_FORMAT_MONO01) {
		cmd = SH1107_SET_NORMAL_DISPLAY;
	} else {
		LOG_WRN("Unsupported pixel format");
		return -ENOTSUP;
	}

	ret = sh1107_write_bus(dev, &cmd, 1, true);
	if (ret) {
		return ret;
	}

	data->pf = pf;

	return 0;
}

static int sh1107_init_device(const struct device *dev)
{
    const struct sh1107_config *config = dev->config;
    int ret;

    /* RESET pulse */
    if (config->reset.port) {
        gpio_pin_set_dt(&config->reset, 0);
        k_msleep(10);
        gpio_pin_set_dt(&config->reset, 1);
        k_msleep(10);
        gpio_pin_set_dt(&config->reset, 0);
        k_msleep(10);
    }

    /* === SH1107 Initial Command Sequence === */
    uint8_t init_cmds[] = {
        SH1107_DISPLAY_OFF,

        SH1107_SET_CLOCK_DIV_RATIO, 0x51,
        SH1107_SET_MEM_ADDRESSING_MODE,

        SH1107_SET_CONTRAST_CTRL, 0x4F,

        SH1106_SET_DCDC_MODE, 0x8A,

		SH1107_SET_SEGMENT_MAP_NORMAL,
        SH1107_SET_COM_OUTPUT_SCAN_NORMAL,

        SH1107_SET_START_LINE, 0x00,
        SH1107_SET_DISPLAY_OFFSET, 0x60,

        SH1107_SET_CHARGE_PERIOD, 0x22,
        SH1107_SET_VCOM_DESELECT_LEVEL, 0x35,

        SH1107_SET_MULTIPLEX_RATIO, 0x3F,

        SH1107_SET_ENTIRE_DISPLAY_OFF,
        SH1107_SET_NORMAL_DISPLAY,
    };

    ret = sh1107_write_bus(dev, init_cmds, sizeof(init_cmds), true);
    if (ret) return ret;

    /* === Additional 128x128 setup === */
    uint8_t init_128x128[] = {
        SH1107_SET_DISPLAY_OFFSET, config->display_offset,
        SH1107_SET_MULTIPLEX_RATIO, 0x7F,
    };

    ret = sh1107_write_bus(dev, init_128x128, sizeof(init_128x128), true);
    if (ret) return ret;

    k_msleep(100);
    uint8_t disp_on = SH1107_DISPLAY_ON;
    ret = sh1107_write_bus(dev, &disp_on, 1, true);

    return ret;
}

static int sh1107_init(const struct device *dev)
{
	const struct sh1107_config *config = dev->config;
	int ret;

	k_sleep(K_TIMEOUT_ABS_MS(config->ready_time_ms));

	if (!sh1107_bus_ready(dev)) {
		LOG_ERR("Bus device %s not ready!", config->bus_name(dev));
		return -EINVAL;
	}

	if (config->supply.port) {
		ret = gpio_pin_configure_dt(&config->supply,
					    GPIO_OUTPUT_INACTIVE);
		if (ret < 0) {
			return ret;
		}
		if (!gpio_is_ready_dt(&config->supply)) {
			LOG_ERR("Supply GPIO device not ready");
			return -ENODEV;
		}
	}

	if (config->reset.port) {
		ret = gpio_pin_configure_dt(&config->reset,
					    GPIO_OUTPUT_INACTIVE);
		if (ret < 0) {
			return ret;
		}
		if (!gpio_is_ready_dt(&config->reset)) {
			LOG_ERR("Reset GPIO device not ready");
			return -ENODEV;
		}
	}

	if (sh1107_init_device(dev)) {
		LOG_ERR("Failed to initialize device!");
		return -EIO;
	}

	return 0;
}

static DEVICE_API(display, sh1107_driver_api) = {
	.blanking_on = sh1107_suspend,
	.blanking_off = sh1107_resume,
	.write = sh1107_write,
	.set_contrast = sh1107_set_contrast,
	.get_capabilities = sh1107_get_capabilities,
	.set_pixel_format = sh1107_set_pixel_format,
};

#define SH1107_CONFIG_SPI(node_id)                                                                \
	.bus = {.spi = SPI_DT_SPEC_GET(                                                            \
			node_id, SPI_OP_MODE_MASTER | SPI_TRANSFER_MSB | SPI_WORD_SET(8), 0)},     \
	.bus_ready = sh1107_bus_ready_spi,                                                        \
	.write_bus = sh1107_write_bus_spi,                                                        \
	.bus_name = sh1107_bus_name_spi,                                                          \
	.data_cmd = GPIO_DT_SPEC_GET(node_id, data_cmd_gpios),

#define SH1107_CONFIG_I2C(node_id)                                                                \
	.bus = {.i2c = I2C_DT_SPEC_GET(node_id)},                                                  \
	.bus_ready = sh1107_bus_ready_i2c,                                                        \
	.write_bus = sh1107_write_bus_i2c,                                                        \
	.bus_name = sh1107_bus_name_i2c,                                                          \
	.data_cmd = {0},

#define SH1107_DEFINE(node_id)                                                                    \
	static struct sh1107_data data##node_id;                                                  \
	static const struct sh1107_config config##node_id = {                                     \
		.reset = GPIO_DT_SPEC_GET_OR(node_id, reset_gpios, {0}),                           \
		.supply = GPIO_DT_SPEC_GET_OR(node_id, supply_gpios, {0}),                         \
		.height = DT_PROP(node_id, height),                                                \
		.width = DT_PROP(node_id, width),                                                  \
		.segment_offset = DT_PROP(node_id, segment_offset),                                \
		.page_offset = DT_PROP(node_id, page_offset),                                      \
		.display_offset = DT_PROP(node_id, display_offset),                                \
		.multiplex_ratio = DT_PROP(node_id, multiplex_ratio),                              \
		.segment_remap = DT_PROP(node_id, segment_remap),                                  \
		.com_invdir = DT_PROP(node_id, com_invdir),                                        \
		.com_sequential = DT_PROP(node_id, com_sequential),                                \
		.prechargep = DT_PROP(node_id, prechargep),                                        \
		.color_inversion = DT_PROP(node_id, inversion_on),                                 \
		.ssd1309_compatible = DT_NODE_HAS_COMPAT(node_id, solomon_ssd1309fb),              \
		.sh1106_compatible = DT_NODE_HAS_COMPAT(node_id, sinowealth_sh1106),               \
		.ready_time_ms = DT_PROP(node_id, ready_time_ms),                                  \
		.use_internal_iref = DT_PROP(node_id, use_internal_iref),                          \
		COND_CODE_1(DT_ON_BUS(node_id, spi), (SH1107_CONFIG_SPI(node_id)),                \
			    (SH1107_CONFIG_I2C(node_id)))                                         \
	};                                                                                         \
                                                                                                   \
	DEVICE_DT_DEFINE(node_id, sh1107_init, NULL, &data##node_id, &config##node_id,            \
			 POST_KERNEL, CONFIG_DISPLAY_INIT_PRIORITY, &sh1107_driver_api);

DT_FOREACH_STATUS_OKAY(sinowealth_sh1107, SH1107_DEFINE)

2. ビルドシステム用の設定ファイルを作成

Zephryのビルドシステムで自作ドライバを認識させるため、modules以下の各ディレクトリにCMakeLists.txt, Kconfigを作成する。

modules/sh1107/drivers/display/sh1107 以下
CMakeLists.txt
zephyr_library()

zephyr_library_sources(sh1107.c)

zephyr_library_include_directories(.)
Kconfig
config SH1107
	bool "SH1107 display driver"
	default n
	depends on DT_HAS_SINOWEALTH_SH1107_ENABLED
	select I2C if $(dt_compat_on_bus,$(DT_COMPAT_SINOWEALTH_SH1107),i2c)
	select SPI if $(dt_compat_on_bus,$(DT_COMPAT_SINOWEALTH_SH1107),spi)
	help
	  Enable driver for SH1107 display driver.

if SH1107

config SH1107_DEFAULT_CONTRAST
	int "SH1107 default contrast"
	default 47
	range 0 $(UINT8_MAX)
	help
	  SH1107 default contrast.

endif # SH1107

modules/sh1107/drivers/display 以下
CMakeLists.txt
add_subdirectory_ifdef(CONFIG_SH1107 sh1107)
Kconfig
rsource "sh1107/Kconfig"
modules/sh1107/drivers 以下
CMakeLists.txt
add_subdirectory(display)

zephyr_include_directories(display)
Kconfig
rsource "display/Kconfig"
modules/sh1107 以下
CMakeLists.txt
add_subdirectory(drivers)

zephyr_include_directories(drivers)
Kconfig
rsource "drivers/Kconfig"

3. バインディングファイルを作成

続いて、デバイスツリーで自作ドライバを認識させるため、以下のディレクトリ構成でバインディングファイルを作成する。

modules/
  sh1107/
    bindings/
      display/
        sinowealth,sh1107-spi.yaml
sinowealth,sh1107-spi.yaml
sinowealth,sh1107-spi.yaml
description: SH1107 128x128 dot-matrix display controller on SPI bus

compatible: "sinowealth,sh1107"

include: ["solomon,ssd1306fb-common.yaml", "spi-device.yaml"]

properties:
  data-cmd-gpios:
    type: phandle-array
    required: true
    description: D/C# pin.

4. モジュール定義ファイルを作成

最後に、以下ディレクトリ構成でモジュール定義ファイルを作成する。

modules/
  zephyr/
    module.yml
module.yml
module.yml
name: sh1107
build:
  cmake: .
  kconfig: Kconfig
  settings:
    dts_root: .

アプリケーションコード

自作ドライバを利用するための設定

zephyr/CMakeLists.txt
CMakeLists.txt
cmake_minimum_required(VERSION 3.13.1)

set(ZEPHYR_EXTRA_MODULES "${CMAKE_SOURCE_DIR}/../modules/sh1107")

include($ENV{ZEPHYR_BASE}/cmake/app/boilerplate.cmake NO_POLICY_SCOPE)
project(nucleof103rb_zephyr)

FILE(GLOB app_sources ../src/*.c*)
target_sources(app PRIVATE ${app_sources})

zephyr/prj.conf
CONFIG_LOG=y
CONFIG_SPI=y
CONFIG_DISPLAY=y
CONFIG_SH1107=y
CONFIG_SH1107_DEFAULT_CONTRAST=47
CONFIG_CHARACTER_FRAMEBUFFER=y
CONFIG_NEWLIB_LIBC=y
CONFIG_STDOUT_CONSOLE=y
CONFIG_HEAP_MEM_POOL_SIZE=4096
CONFIG_COMMON_LIBC_MALLOC_ARENA_SIZE=4096
zephyr/boards/nucleo_f103rb.overlay
&spi1 {
	status = "okay";
	pinctrl-0 = <&spi1_sck_master_pa5 &spi1_miso_master_pa6 &spi1_mosi_master_pa7>;
	pinctrl-names = "default";
	cs-gpios = <&gpiob 6 (GPIO_ACTIVE_LOW)>;

	oled: sh1107@0 {
		compatible = "sinowealth,sh1107";
		reg = <0>;
		spi-max-frequency = <10000000>;
		width = <128>;
		height = <128>;
		segment-offset = <0>;
		page-offset = <0>;
		display-offset = <0x00>;
		multiplex-ratio = <0x7F>;
		prechargep = <0x22>;
		segment-remap;
		com-invdir;
		inversion-on;
		reset-gpios = <&gpioa 8 GPIO_ACTIVE_LOW>;
		data-cmd-gpios = <&gpioa 9 GPIO_ACTIVE_HIGH>;
		ready-time-ms = <100>;
	};
};

OLED表示のサンプルコード

動作確認にあたっては以下動画を参考に進めた。
https://www.youtube.com/watch?v=ddZ-04IVrak

src/logo_image.h

上記動画の概要欄から取得したものをベースとし、128x128の解像度に合わせて配列サイズを2048バイトに変更している。

logo_image.h
#ifndef __LOGO_IMAGE_H__
#define __LOGO_IMAGE_H__

#include <zephyr/kernel.h>

uint8_t buf[2048] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x80, 0x80, 0x00, 
0x00, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 
0xc0, 0xe0, 0xf0, 0xf8, 0xfc, 0xfc, 0xfe, 0xfe, 0xff, 0xff, 0xff, 0x1f, 0x1f, 0x0f, 0x0f, 0x00, 
0x00, 0x0f, 0x0f, 0x1f, 0x1f, 0xff, 0xff, 0xff, 0xfe, 0xfe, 0xfc, 0xfc, 0xf8, 0xf0, 0xe0, 0xc0, 
0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xf8, 0xfe, 0xff, 
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x86, 0x86, 0x86, 0x86, 0x86, 
0x86, 0x86, 0x86, 0x86, 0x86, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
0xff, 0xfe, 0xf8, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x21, 0x21, 0x21, 0x21, 0x21, 
0x21, 0x21, 0x21, 0x21, 0x21, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x1f, 0x7f, 
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x18, 0x18, 0x18, 0x18, 0x18, 
0x18, 0x18, 0x18, 0x18, 0x18, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
0x7f, 0x1f, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x01, 0x03, 0x07, 0x0f, 0x0f, 0x1f, 0x1f, 0x3f, 0x3f, 0x3f, 0x7e, 0x7e, 0x7c, 0x7c, 0x00, 
0x00, 0x7c, 0x7c, 0x7e, 0x7e, 0x3f, 0x3f, 0x3f, 0x1f, 0x1f, 0x0f, 0x0f, 0x07, 0x03, 0x01, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0xf0, 0x10, 
0x10, 0x00, 0xf0, 0x80, 0x80, 0x80, 0xf0, 0x00, 0xf0, 0x90, 0x90, 0x90, 0x10, 0x00, 0x00, 0x00, 
0xf0, 0x90, 0x90, 0x90, 0x60, 0x00, 0xf0, 0x00, 0x00, 0x00, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0x00, 
0x00, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x80, 0x00, 0xf0, 0x00, 0x00, 0x00, 0xf0, 
0x00, 0xf0, 0x90, 0x90, 0x90, 0x60, 0x00, 0x00, 0x00, 0xf0, 0x90, 0x90, 0x90, 0x60, 0x00, 0xf0, 
0x90, 0x90, 0x10, 0x10, 0x00, 0x60, 0x90, 0x90, 0x90, 0x10, 0x00, 0xf0, 0x00, 0x60, 0x90, 0x90, 
0x90, 0x10, 0x00, 0x10, 0x10, 0xf0, 0x10, 0x10, 0x00, 0xe0, 0x10, 0x10, 0x10, 0xe0, 0x00, 0xf0, 
0x90, 0x90, 0x90, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 
0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x07, 0x00, 0x07, 0x04, 0x04, 0x04, 0x04, 0x00, 0x00, 0x00, 
0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x04, 0x04, 0x04, 0x03, 0x00, 0x07, 0x04, 0x04, 0x04, 
0x04, 0x00, 0x07, 0x04, 0x04, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x03, 0x04, 0x04, 0x04, 0x03, 
0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x01, 0x06, 0x00, 0x07, 
0x04, 0x04, 0x04, 0x04, 0x00, 0x04, 0x04, 0x04, 0x04, 0x03, 0x00, 0x07, 0x00, 0x04, 0x04, 0x04, 
0x04, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x04, 0x04, 0x04, 0x03, 0x00, 0x07, 
0x00, 0x00, 0x01, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};

#endif /* __LOGO_IMAGE_H__ */
src/main.c
main.c
#include <stdint.h>
#include <zephyr/device.h>
#include <zephyr/kernel.h>
#include <zephyr/drivers/display.h>
#include <zephyr/display/cfb.h>
#include <zephyr/logging/log.h>

#include "logo_image.h"

#define DISPLAY_BUFFER_PITCH 128

LOG_MODULE_REGISTER(display);

static const struct device *display = DEVICE_DT_GET(DT_NODELABEL(oled));

int main(void)
{
    printk("Debug start\n");

    if (display == NULL)
    {
        LOG_ERR("device pointer is NULL");
        return 0;
    }

    if (!device_is_ready(display))
    {
        LOG_ERR("display device is not ready");
        return 0;
    }

    printk("Device is ready.\n");

#if 1
    struct display_capabilities capabilities;
    display_get_capabilities(display, &capabilities);
    LOG_INF("Display resolution: %ux%u", capabilities.x_resolution, capabilities.y_resolution);
    LOG_INF("Display scrreen info: %u", capabilities.screen_info);
    LOG_INF("Display supported pixel formats: 0x%08x", capabilities.supported_pixel_formats);
    LOG_INF("Display pixel format: %u", capabilities.current_pixel_format);
    LOG_INF("Display orientation: %u", capabilities.current_orientation);

    const struct display_buffer_descriptor buf_desc = {
        .width = capabilities.x_resolution,
        .height = capabilities.y_resolution,
        .buf_size = capabilities.x_resolution * capabilities.y_resolution,
        .pitch = DISPLAY_BUFFER_PITCH,
    };

    if (display_write(display, 0, 0, &buf_desc, buf) != 0)
    {
        LOG_ERR("Failed to write to display");
        return 0;
    };
#endif

#if 0
    int ret;
    ret = cfb_framebuffer_init(display);
    if (ret != 0)
    {
        LOG_ERR("cfb_framebuffer_init failed: %d", ret);
        return 0;
    }
    printk("Framebuffer initialized.\n");

    ret = cfb_print(display, "Hello,world!", 0, 64);
    if (ret != 0)
    {
        LOG_ERR("cfb_print failed: %d", ret);
        return 0;
    }
    printk("Printed to framebuffer.\n");

    ret = cfb_framebuffer_finalize(display);
    if (ret != 0)
    {
        LOG_ERR("cfb_framebuffer_finalize failed: %d", ret);
        return 0;
    }
    printk("Framebuffer finalized.\n");
#endif

    while (1)
    {
        k_sleep(K_MSEC(200));
    }

    return 0;
}

おわりに

以上、SH1107コントローラ用のZephyrディスプレイドライバを自作し、Nucleo-F103RBボードで動作させる手順を紹介した。
今回もドライバを自作することとなり、なかなか骨の折れる作業であった。
ZephyrではLVGLなどのGUIライブラリも利用できるようなので、今後はそちらも試してみたいと思う。

Discussion