💬

STM32でZephyrRTOS入門~I2Cカラーセンサ(TCS34725)を自作ドライバとして制御~

に公開

はじめに

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

今回は、I2Cを使って手持ちの以下カラーセンサを制御するサンプルを作成し、I2C動作の仕組みの理解に取り組みたい。手持ちのカラーセンサにはTCS34725が使われているため、これを対象にする。
Unit Color

ESP32やArduinoならライブラリを取り込んで簡単に動かせるが、Zephyrでは公式でサポートされていない場合、ドライバを自作する必要があるようだ。そこで今回は、カラーセンサの制御ドライバを自作し、Zephyr上で動かすことに挑戦する。

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

下記の動画を参考に、Zephyrで自作ドライバを作成する手順を確認した。
Introduction to Zephyr Part 6: How to Write a Device Driver | DigiKey

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

  1. 自作ドライバのコード(tcs34725.h, tcs34725.c)を作成
  2. ビルドシステム用の設定ファイル(CMakeLists.txt, Kconfig)を作成
  3. バインディングファイル(ams,tcs34725.yaml)を作成
  4. モジュール定義ファイル(module.yml)を作成

これにより利用側のアプリケーションコードから自作ドライバを利用できるようになる。
以下それぞれの手順の詳細を説明する。

1. 自作ドライバのコード(tcs34725.h, tcs34725.c)を作成

まず以下構成のディレクトリおよびドライバのファイルを作成する。

modules/
  tcs34725/
    drivers/
      sensor/
        tcs34725/
          tcs34725.c
          tcs34725.h

カラーセンサ(TCS34725)のドライバ実装ついてはArduino向けライブラリを参考にし、以下の観点で修正を加えた。

  • DeviceTreeへの登録などZephyr向けの初期化コードの追加
  • ZephyrのI2C APIへの置き換え
  • ZephyrのセンサAPIへの対応

また、同メーカー型番違いのセンサ(TCS3400)についてはZephyr公式のドライバが提供されており、そちらも参考にした。

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

tcs34725.h
tcs34725.h
#ifndef ZEPHYR_DRIVERS_SENSOR_TCS34725_H_
#define ZEPHYR_DRIVERS_SENSOR_TCS34725_H_

#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/i2c.h>
#include <zephyr/drivers/sensor.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/sys/util.h>

#define TCS34725_ID_1 (0x4d)
#define TCS34725_ID_2 (0x44)
#define TCS34725_ID_3 (0x10)

#define TCS34725_ADDRESS (0x29)     /**< I2C address **/
#define TCS34725_COMMAND_BIT (0x80) /**< Command bit **/
#define TCS34725_ENABLE (0x00)      /**< Interrupt Enable register */
#define TCS34725_ENABLE_AIEN (0x10) /**< RGBC Interrupt Enable */
#define TCS34725_ENABLE_WEN \
    (0x08) /**< Wait Enable - Writing 1 activates the wait timer */
#define TCS34725_ENABLE_AEN \
    (0x02) /**< RGBC Enable - Writing 1 actives the ADC, 0 disables it */
#define TCS34725_ENABLE_PON                                                                  \
    (0x01)                    /**< Power on - Writing 1 activates the internal oscillator, 0 \
                                 disables it */
#define TCS34725_ATIME (0x01) /**< Integration time */
#define TCS34725_WTIME \
    (0x03)                          /**< Wait time (if TCS34725_ENABLE_WEN is asserted) */
#define TCS34725_WTIME_2_4MS (0xFF) /**< WLONG0 = 2.4ms   WLONG1 = 0.029s */
#define TCS34725_WTIME_204MS (0xAB) /**< WLONG0 = 204ms   WLONG1 = 2.45s  */
#define TCS34725_WTIME_614MS (0x00) /**< WLONG0 = 614ms   WLONG1 = 7.4s   */
#define TCS34725_AILTL \
    (0x04) /**< Clear channel lower interrupt threshold (lower byte) */
#define TCS34725_AILTH \
    (0x05) /**< Clear channel lower interrupt threshold (higher byte) */
#define TCS34725_AIHTL \
    (0x06) /**< Clear channel upper interrupt threshold (lower byte) */
#define TCS34725_AIHTH \
    (0x07) /**< Clear channel upper interrupt threshold (higher byte) */
#define TCS34725_PERS                                                   \
    (0x0C) /**< Persistence register - basic SW filtering mechanism for \
              interrupts */
#define TCS34725_PERS_NONE \
    (0b0000) /**< Every RGBC cycle generates an interrupt */
#define TCS34725_PERS_1_CYCLE                                                \
    (0b0001) /**< 1 clean channel value outside threshold range generates an \
                interrupt */
#define TCS34725_PERS_2_CYCLE                                                 \
    (0b0010) /**< 2 clean channel values outside threshold range generates an \
                interrupt */
#define TCS34725_PERS_3_CYCLE                                                 \
    (0b0011) /**< 3 clean channel values outside threshold range generates an \
                interrupt */
#define TCS34725_PERS_5_CYCLE                                                 \
    (0b0100) /**< 5 clean channel values outside threshold range generates an \
                interrupt */
#define TCS34725_PERS_10_CYCLE                                                 \
    (0b0101) /**< 10 clean channel values outside threshold range generates an \
                interrupt*/
#define TCS34725_PERS_15_CYCLE                                                 \
    (0b0110) /**< 15 clean channel values outside threshold range generates an \
                interrupt*/
#define TCS34725_PERS_20_CYCLE                                                 \
    (0b0111) /**< 20 clean channel values outside threshold range generates an \
                interrupt*/
#define TCS34725_PERS_25_CYCLE                                                 \
    (0b1000) /**< 25 clean channel values outside threshold range generates an \
                interrupt*/
#define TCS34725_PERS_30_CYCLE                                                 \
    (0b1001) /**< 30 clean channel values outside threshold range generates an \
                interrupt*/
#define TCS34725_PERS_35_CYCLE                                                 \
    (0b1010) /**< 35 clean channel values outside threshold range generates an \
                interrupt*/
#define TCS34725_PERS_40_CYCLE                                                 \
    (0b1011) /**< 40 clean channel values outside threshold range generates an \
                interrupt*/
#define TCS34725_PERS_45_CYCLE                                                 \
    (0b1100) /**< 45 clean channel values outside threshold range generates an \
                interrupt*/
#define TCS34725_PERS_50_CYCLE                                                 \
    (0b1101) /**< 50 clean channel values outside threshold range generates an \
                interrupt*/
#define TCS34725_PERS_55_CYCLE                                                 \
    (0b1110) /**< 55 clean channel values outside threshold range generates an \
                interrupt*/
#define TCS34725_PERS_60_CYCLE                                                                   \
    (0b1111)                   /**< 60 clean channel values outside threshold range generates an \
                                  interrupt*/
#define TCS34725_CONFIG (0x0D) /**< Configuration **/
#define TCS34725_CONFIG_WLONG                                                           \
    (0x02)                      /**< Choose between short and long (12x) wait times via \
                                   TCS34725_WTIME */
#define TCS34725_CONTROL (0x0F) /**< Set the gain level for the sensor */
#define TCS34725_ID \
    (0x12)                          /**< 0x44 = TCS34721/TCS34725, 0x4D = TCS34723/TCS34727 */
#define TCS34725_STATUS (0x13)      /**< Device status **/
#define TCS34725_STATUS_AINT (0x10) /**< RGBC Clean channel interrupt */
#define TCS34725_STATUS_AVALID                                                                     \
    (0x01)                     /**< Indicates that the RGBC channels have completed an integration \
                                  cycle */
#define TCS34725_CDATAL (0x14) /**< Clear channel data low byte */
#define TCS34725_CDATAH (0x15) /**< Clear channel data high byte */
#define TCS34725_RDATAL (0x16) /**< Red channel data low byte */
#define TCS34725_RDATAH (0x17) /**< Red channel data high byte */
#define TCS34725_GDATAL (0x18) /**< Green channel data low byte */
#define TCS34725_GDATAH (0x19) /**< Green channel data high byte */
#define TCS34725_BDATAL (0x1A) /**< Blue channel data low byte */
#define TCS34725_BDATAH (0x1B) /**< Blue channel data high byte */

/** Integration time settings for TCS34725 */
/*
 * 60-Hz period: 16.67ms, 50-Hz period: 20ms
 * 100ms is evenly divisible by 50Hz periods and by 60Hz periods
 */
#define TCS34725_INTEGRATIONTIME_2_4MS \
    (0xFF) /**< 2.4ms - 1 cycle - Max Count: 1024 */
#define TCS34725_INTEGRATIONTIME_24MS \
    (0xF6) /**< 24.0ms - 10 cycles - Max Count: 10240 */
#define TCS34725_INTEGRATIONTIME_50MS \
    (0xEB) /**< 50.4ms - 21 cycles - Max Count: 21504 */
#define TCS34725_INTEGRATIONTIME_60MS \
    (0xE7) /**< 60.0ms - 25 cycles - Max Count: 25700 */
#define TCS34725_INTEGRATIONTIME_101MS \
    (0xD6) /**< 100.8ms - 42 cycles - Max Count: 43008 */
#define TCS34725_INTEGRATIONTIME_120MS \
    (0xCE) /**< 120.0ms - 50 cycles - Max Count: 51200 */
#define TCS34725_INTEGRATIONTIME_154MS \
    (0xC0) /**< 153.6ms - 64 cycles - Max Count: 65535 */
#define TCS34725_INTEGRATIONTIME_180MS \
    (0xB5) /**< 180.0ms - 75 cycles - Max Count: 65535 */
#define TCS34725_INTEGRATIONTIME_199MS \
    (0xAD) /**< 199.2ms - 83 cycles - Max Count: 65535 */
#define TCS34725_INTEGRATIONTIME_240MS \
    (0x9C) /**< 240.0ms - 100 cycles - Max Count: 65535 */
#define TCS34725_INTEGRATIONTIME_300MS \
    (0x83) /**< 300.0ms - 125 cycles - Max Count: 65535 */
#define TCS34725_INTEGRATIONTIME_360MS \
    (0x6A) /**< 360.0ms - 150 cycles - Max Count: 65535 */
#define TCS34725_INTEGRATIONTIME_401MS \
    (0x59) /**< 400.8ms - 167 cycles - Max Count: 65535 */
#define TCS34725_INTEGRATIONTIME_420MS \
    (0x51) /**< 420.0ms - 175 cycles - Max Count: 65535 */
#define TCS34725_INTEGRATIONTIME_480MS \
    (0x38) /**< 480.0ms - 200 cycles - Max Count: 65535 */
#define TCS34725_INTEGRATIONTIME_499MS \
    (0x30) /**< 499.2ms - 208 cycles - Max Count: 65535 */
#define TCS34725_INTEGRATIONTIME_540MS \
    (0x1F) /**< 540.0ms - 225 cycles - Max Count: 65535 */
#define TCS34725_INTEGRATIONTIME_600MS \
    (0x06) /**< 600.0ms - 250 cycles - Max Count: 65535 */
#define TCS34725_INTEGRATIONTIME_614MS \
    (0x00) /**< 614.4ms - 256 cycles - Max Count: 65535 */

/** Gain settings for TCS34725  */
typedef enum
{
    TCS34725_GAIN_1X = 0x00,  /**<  No gain  */
    TCS34725_GAIN_4X = 0x01,  /**<  4x gain  */
    TCS34725_GAIN_16X = 0x02, /**<  16x gain */
    TCS34725_GAIN_60X = 0x03  /**<  60x gain */
} tcs34725Gain_t;

#define TCS34725_DEFAULT_INTEGRATIONTIME TCS34725_INTEGRATIONTIME_240MS
#define TCS34725_DEFAULT_INTEGRATIONTIME_MS 240
#define TCS34725_DEFAULT_GAIN TCS34725_GAIN_1X


enum sensor_attribute_tcs34725
{
    /** RGBC Integration Cycles */
    SENSOR_ATTR_TCS34725_INTEGRATION_CYCLES = SENSOR_ATTR_PRIV_START,
};

struct tcs34725_config
{
    struct i2c_dt_spec i2c;
    struct gpio_dt_spec int_gpio;
};

struct tcs34725_data
{
    // uint16_t r, g, b, c;
    struct gpio_callback gpio_cb;
    const struct device *dev;

    uint16_t sample_crgb[4];

    struct k_sem data_sem;
};

#endif /* ZEPHYR_DRIVERS_SENSOR_TCS34725_H_ */
tcs34725.c
tcs34725.c
#define DT_DRV_COMPAT ams_tcs34725

#include "tcs34725.h"

#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(tcs34725, CONFIG_SENSOR_LOG_LEVEL);

static int write8(const struct device *dev, uint8_t reg, uint8_t value)
{
    const struct tcs34725_config *cfg = dev->config;
    uint8_t buf[2] = {(uint8_t)(TCS34725_COMMAND_BIT | reg), value};

    return i2c_write_dt(&cfg->i2c, buf, sizeof(buf));
}

static int read8(const struct device *dev, uint8_t reg, uint8_t *value)
{
    const struct tcs34725_config *cfg = dev->config;
    uint8_t reg_addr = (uint8_t)(TCS34725_COMMAND_BIT | reg);

    int ret = i2c_write_read_dt(&cfg->i2c, &reg_addr, 1, value, 1);
    return ret;
}

static int read16(const struct device *dev, uint8_t reg, uint16_t *value)
{
    const struct tcs34725_config *cfg = dev->config;
    uint8_t addr = (uint8_t)(TCS34725_COMMAND_BIT | reg);

    uint8_t buf[2];
    int ret = i2c_burst_read_dt(&cfg->i2c, addr, buf, sizeof(buf));
    if (ret < 0)
    {
        return ret;
    }

    *value = (uint16_t)buf[0] | ((uint16_t)buf[1] << 8);
    return 0;
}

static int enable(const struct device *dev)
{
    int ret = write8(dev, TCS34725_ENABLE, TCS34725_ENABLE_PON);
    if (ret) {
        return ret;
    }

    k_msleep(3);

    ret = write8(dev, TCS34725_ENABLE, TCS34725_ENABLE_PON | TCS34725_ENABLE_AEN);
    if (ret) {
        return ret;
    }

    return ret;
}

static int disable(const struct device *dev)
{
    uint8_t reg = 0;

    /* Turn the device off to save power */
    int ret = read8(dev, TCS34725_ENABLE, &reg);
    ret = write8(dev, TCS34725_ENABLE, reg & ~(TCS34725_ENABLE_PON | TCS34725_ENABLE_AEN));

    return ret;
}

static int setIntegrationTime(const struct device *dev, uint8_t it)
{
    int ret = write8(dev, TCS34725_ATIME, it);
    return ret;
}

static int setGain(const struct device *dev, tcs34725Gain_t gain)
{
    int ret = write8(dev, TCS34725_CONTROL, gain);
    return ret;
}

static void tcs34725_setup_int(const struct tcs34725_config *config, bool enable)
{
    unsigned int flags = enable ? GPIO_INT_EDGE_TO_ACTIVE : GPIO_INT_DISABLE;

    gpio_pin_interrupt_configure_dt(&config->int_gpio, flags);
}

static void tcs34725_gpio_callback(const struct device *dev,
                                   struct gpio_callback *cb, uint32_t pins)
{
    struct tcs34725_data *data = CONTAINER_OF(cb, struct tcs34725_data,
                                              gpio_cb);

    tcs34725_setup_int(data->dev->config, false);

    k_sem_give(&data->data_sem);
}

static int tcs34725_sample_fetch(const struct device *dev,
                                 enum sensor_channel chan)
{
    struct tcs34725_data *data = dev->data;
    int ret;
    uint8_t status;

    if (chan != SENSOR_CHAN_ALL)
    {
        LOG_ERR("Unsupported sensor channel");
        return -ENOTSUP;
    }

    // 割り込みは一旦無効化
    // LOG_DBG("tcs34725_setup_int");
    // tcs34725_setup_int(cfg, true);

    LOG_DBG("Enabling sensor...");
    ret = enable(dev);
    if (ret)
    {
        LOG_ERR("enable failed (%d)", ret);
        return ret;
    }

    // 割り込みでの待ちは一旦無効化
    // LOG_DBG("Waiting for data ready interrupt...");
    // k_sem_take(&data->data_sem, K_FOREVER);
    // LOG_DBG("Interrupt received!");

    // 積算時間分待機
    int32_t delay_ms = TCS34725_DEFAULT_INTEGRATIONTIME_MS;
    k_msleep(delay_ms);

    // 待ち時間が足りなかった場合に備えたリトライ処理
    int retry = 10;

    do {
        read8(dev, TCS34725_STATUS, &status);
        if (status & TCS34725_STATUS_AVALID)
        {
            break;
        }
        k_msleep(10);
    }
    while (retry--);

    if (!(status & TCS34725_STATUS_AVALID))
    {
        LOG_ERR("AVALID not set, status=0x%02x", status);
        return -EIO;
    }

    ret = read16(dev, TCS34725_CDATAL, &data->sample_crgb[0]);
    if (ret)
    {
        return ret;
    }

    ret = read16(dev, TCS34725_RDATAL, &data->sample_crgb[1]);
    if (ret)
    {
        return ret;
    }

    ret = read16(dev, TCS34725_GDATAL, &data->sample_crgb[2]);
    if (ret)
    {
        return ret;
    }

    ret = read16(dev, TCS34725_BDATAL, &data->sample_crgb[3]);
    if (ret)
    {
        return ret;
    }

    ret = disable(dev);
    if (ret)
    {
        return ret;
    }

    return 0;
}

static int tcs34725_channel_get(const struct device *dev,
                                enum sensor_channel chan,
                                struct sensor_value *val)
{
    struct tcs34725_data *data = dev->data;

    switch (chan)
    {
    case SENSOR_CHAN_LIGHT:
        val->val1 = sys_le16_to_cpu(data->sample_crgb[0]);
        val->val2 = 0;
        break;
    case SENSOR_CHAN_RED:
        val->val1 = sys_le16_to_cpu(data->sample_crgb[1]);
        val->val2 = 0;
        break;
    case SENSOR_CHAN_GREEN:
        val->val1 = sys_le16_to_cpu(data->sample_crgb[2]);
        val->val2 = 0;
        break;
    case SENSOR_CHAN_BLUE:
        val->val1 = sys_le16_to_cpu(data->sample_crgb[3]);
        val->val2 = 0;
        break;
    default:
        return -ENOTSUP;
    }

    return 0;
}

static int tcs34725_attr_set(const struct device *dev,
                             enum sensor_channel chan,
                             enum sensor_attribute attr,
                             const struct sensor_value *val)
{
    int ret;
    uint8_t reg_val;

    switch (attr)
    {
    case SENSOR_ATTR_TCS34725_INTEGRATION_CYCLES:
        if (!IN_RANGE(val->val1, 1, 256))
        {
            return -EINVAL;
        }
        reg_val = UINT8_MAX - val->val1 + 1;
        ret = setIntegrationTime(dev, reg_val);
        if (ret)
        {
            return ret;
        }
        break;
    default:
        return -ENOTSUP;
    }

    return 0;
}

static int tcs34725_sensor_setup(const struct device *dev)
{
    uint8_t chip_id;
    int ret;

    // チップID照合
    ret = read8(dev, TCS34725_ID, &chip_id);
    if (ret)
    {
        LOG_DBG("Failed to read chip id: %d", ret);
        return ret;
    }

    if (!((chip_id == TCS34725_ID_1) || (chip_id == TCS34725_ID_2) || (chip_id == TCS34725_ID_3)))
    {
        LOG_DBG("Invalid chip id: %02x", chip_id);
        return -EIO;
    }

    LOG_INF("chip id: 0x%x", chip_id);

    /* Set default integration time and gain */
    setIntegrationTime(dev, TCS34725_DEFAULT_INTEGRATIONTIME);
    setGain(dev, TCS34725_DEFAULT_GAIN);

    return 0;
}

static DEVICE_API(sensor, tcs34725_api) = {
    .sample_fetch = tcs34725_sample_fetch,
    .channel_get = tcs34725_channel_get,
    .attr_set = tcs34725_attr_set,
};

static int tcs34725_init(const struct device *dev)
{
    const struct tcs34725_config *cfg = dev->config;
    struct tcs34725_data *data = dev->data;
    int ret;

    k_sem_init(&data->data_sem, 0, K_SEM_MAX_LIMIT);
    data->dev = dev;

    if (!i2c_is_ready_dt(&cfg->i2c))
    {
        LOG_ERR("I2C bus is not ready");
        return -ENODEV;
    }

    ret = tcs34725_sensor_setup(dev);
    if (ret < 0)
    {
        LOG_ERR("Failed to setup device");
        return ret;
    }

    if (!gpio_is_ready_dt(&cfg->int_gpio))
    {
        LOG_ERR("Interrupt GPIO device not ready");
        return -ENODEV;
    }

    ret = gpio_pin_configure_dt(&cfg->int_gpio, GPIO_INPUT);
    if (ret < 0)
    {
        LOG_ERR("Failed to configure interrupt pin");
        return ret;
    }

    gpio_init_callback(&data->gpio_cb, tcs34725_gpio_callback,
                       BIT(cfg->int_gpio.pin));

    ret = gpio_add_callback(cfg->int_gpio.port, &data->gpio_cb);
    if (ret < 0)
    {
        LOG_ERR("Failed to set GPIO callback");
        return ret;
    }

    return 0;
}

#define TCS34725_INIT(n)                                                   \
    static struct tcs34725_data tcs34725_data_##n;                         \
    static const struct tcs34725_config tcs34725_config_##n = {            \
        .i2c = I2C_DT_SPEC_INST_GET(n),                                    \
        .int_gpio = GPIO_DT_SPEC_INST_GET(n, int_gpios),                   \
    };                                                                     \
    SENSOR_DEVICE_DT_INST_DEFINE(n, &tcs34725_init, NULL,                  \
                                 &tcs34725_data_##n, &tcs34725_config_##n, \
                                 POST_KERNEL, CONFIG_SENSOR_INIT_PRIORITY, \
                                 &tcs34725_api);

DT_INST_FOREACH_STATUS_OKAY(TCS34725_INIT)

2. ビルドシステム用の設定ファイル(CMakeLists.txt, Kconfig)を作成

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

modules/tcs34725/drivers/sensor/tcs34725 以下
CMakeLists.txt
zephyr_library()

zephyr_library_sources(tcs34725.c)

zephyr_library_include_directories(.)
Kconfig
config TCS34725
	bool "TCS34725 Sensor"
	default n
	depends on DT_HAS_AMS_TCS34725_ENABLED
	select I2C
	help
	  Enable driver for TCS34725 sensors.

modules/tcs34725/drivers/sensor 以下
CMakeLists.txt
add_subdirectory_ifdef(CONFIG_TCS34725 tcs34725)
Kconfig
rsource "tcs34725/Kconfig"
modules/tcs34725/drivers 以下
CMakeLists.txt
add_subdirectory(sensor)

zephyr_include_directories(sensor)
Kconfig
rsource "sensor/Kconfig"
modules/tcs34725 以下
CMakeLists.txt
add_subdirectory(drivers)

zephyr_include_directories(drivers)
Kconfig
rsource "drivers/Kconfig"

3. バインディングファイル(ams,tcs34725.yaml)を作成

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

modules/
  tcs34725/
    bindings/
      sensor/
        ams,tcs34725.yaml
ams,tcs34725.yaml
ams,tcs34725.yaml
description: "AMS TCS34725 color sensor"
compatible: "ams,tcs34725"

include: [sensor-device.yaml, i2c-device.yaml]

properties:
  int-gpios:
    type: phandle-array
    required: true
    description: |
      INT pin GPIO identifier, open-drain, active low.

4. モジュール定義ファイル(module.yml)を作成

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

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

これらの手順により、自作ドライバをZephyrのビルドシステムに認識させることができる。

アプリケーションコード

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

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

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

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})

set(ZEPHYR_EXTRA_MODULES "${CMAKE_SOURCE_DIR}/../modules/tcs34725")
この記述により、自作ドライバをビルドシステムに認識させている。

zephyr/prj.conf
CONFIG_I2C=y
CONFIG_SENSOR=y
CONFIG_LOG=y
CONFIG_SENSOR_LOG_LEVEL_DBG=y
CONFIG_TCS34725=y

CONFIG_TCS34725=y
この記述により、自作ドライバを有効化している。

zephyr/boards/nucleo_f103rb.overlay
&i2c1 {
    status = "okay";

    tcs34725: tcs34725@29 {
        compatible = "ams,tcs34725";
        reg = <0x29>;
        int-gpios = <&gpioa 0 GPIO_ACTIVE_LOW>;
        status = "okay";
    };
};

TCS34725のI2Cアドレス(0x29)および割り込みピン(GPIOA0)をデバイスツリーに登録している。
ただし今回使用するカラーセンサモジュールは割り込み機能を使用しないため、int-gpiosの設定はダミーである。

カラーセンサの値を読み取るメインプログラム

ここまでで自作ドライバをZephyrのビルドシステムに認識させ、アプリケーションコードから利用できるようになった。
※ここに至るまで何度もビルドエラーに遭遇し非常に苦労した。

以下はカラーセンサの値をポーリングで読み取り、シリアルコンソールに表示するメインプログラムのコードである。

src/main.c
main.c
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/sensor.h>

static const struct device *dev = DEVICE_DT_GET_ANY(ams_tcs34725);

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

    if (dev == NULL)
    {
        printk("No device found.\n");
        return 0;
    }

    if (!device_is_ready(dev))
    {
        printk("TCS34725 device not ready!\n");
        return 0;
    }

    printk("TCS34725 device ready!\n");

    while (1)
    {
        struct sensor_value r, g, b, c;
        int ret;

        ret = sensor_sample_fetch(dev);
        if (ret < 0)
        {
            printk("sensor_sample_fetch failed: %d\n", ret);
            k_sleep(K_MSEC(1000));
            continue;
        }

        /* Red */
        sensor_channel_get(dev, SENSOR_CHAN_RED, &r);
        /* Green */
        sensor_channel_get(dev, SENSOR_CHAN_GREEN, &g);
        /* Blue */
        sensor_channel_get(dev, SENSOR_CHAN_BLUE, &b);
        /* Clear (ライト) */
        sensor_channel_get(dev, SENSOR_CHAN_LIGHT, &c);

        printk("R=%d, G=%d, B=%d, C=%d\n",
               r.val1, g.val1, b.val1, c.val1);

        k_sleep(K_MSEC(200));
    }

    return 0;
}

sensor_sample_fetchでセンサの値を取得し、sensor_channel_getで各色成分(R,G,B)およびライト成分(Clear)の値を取得している。
アプリケーションからはデバイスの初期化やセンサ読み取りのためのレジスタ制御を一切意識せずに利用できるのが分かる。

トラブルシューティングで参照したページ

上述の通り、自作ドライバの作成およびビルドシステムへの組み込みは非常に手間がかかり、何度もビルドエラーに遭遇した。その際、以下の公式ドキュメントが非常に参考になったため、紹介しておく。

https://docs.zephyrproject.org/latest/build/dts/troubleshooting.html

特に「undefined reference to __device_dts_ord_68」のエラーの原因解明は難航した。
上記サイトを参考に順番に確認したところ、tcs34725.c内の以下マクロ定義が抜けていたことが原因であった。

#define DT_DRV_COMPAT ams_tcs34725

おわりに

以上、Zephyrで自作ドライバを作成し、アプリケーションコードからI2Cカラーセンサを制御する手順を説明した。
自作ドライバの作成は非常に手間がかかるが、Zephyrのビルドシステムに慣れる良い機会となった。
今後は自作ドライバの改良や他のI2Cデバイスの制御にも挑戦していきたい。

Discussion