Configuration

Our configuration structure for the DM163 driver will look like this:

struct dm163_config {
  const struct gpio_dt_spec en;
  const struct gpio_dt_spec gck;
  const struct gpio_dt_spec lat;
  const struct gpio_dt_spec rst;
  const struct gpio_dt_spec selbk;
  const struct gpio_dt_spec sin;
};

Those names correspond to the name of the pins in the DM163 documentation. We want to fill this structure from data coming from the device tree.

Create a dts/bindings/siti,dm163.yaml file containing the following description:

description: SITI DM163 LED controller node

compatible: "siti,dm163"

properties:
  sin-gpios:
    type: phandle-array
    required: true
  selbk-gpios:
    type: phandle-array
    required: true
  lat-gpios:
    type: phandle-array
    required: true
  gck-gpios:
    type: phandle-array
    required: true
  rst-gpios:
    type: phandle-array
    required: true
  en-gpios:
    type: phandle-array
    required: false

This file contains information about what may be and must be present in a device-tree node marked as being compatible with the "siti,dm163" device driver. The phandle-array type corresponds to a list of phandles and 32-bit cells (usually specifiers), for example <&dma0 2>,<&dma0 3> (according to Zephyr documentation). In our case, we will have only one phandle and its corresponding information, such as <&gpioc 5 0>.

Some pins names end with "_b" in the documentation. It means that they are active low (when pulled to ground) instead of active high (when pulled to VCC). In our case, we wil note those pins as GPIO_ACTIVE_LOW in the device tree: setting them to ACTIVE will pull them to ground and setting them to INACTIVE will pull them to VCC.

Also note that we named the properties -gpios; even though we only have one GPIO, it is customary to end the name in s if the type allows you to enter several values.

Populate the boards/disco_l475_iot1.overlay with the following hardware information which corresponds to how the led matrix DM163 is connected to our microcontroller:

/ {
  dm163: dm163 {
    compatible = "siti,dm163";
    selbk-gpios = <&gpioc 5 0>;
    lat-gpios = <&gpioc 4 GPIO_ACTIVE_LOW>;
    rst-gpios = <&gpioc 3 GPIO_ACTIVE_LOW>;
    gck-gpios = <&gpiob 1 0>;
    sin-gpios = <&gpioa 4 0>;
  };
};

Here, we define a node /dm163 with node label dm163. As we declare it to be compatible with the siti,dm163 device driver (which we still have to write), the node content is checked against the bindings file we created just before (dts/bindings/siti,dm163.yaml).

Note that en-gpios is absent. It was marked as required: false in the bindings description. This pin is sometimes connected directly to the ground, which means that the DM163 output is always enabled. If it is present in the device tree, we will have to configure it as an output and make it low, if it is absent we have nothing to do to enable the DM163 outputs.

Reading the device-tree into the struct dm163_config

Let's create dm163_module/zephyr/dm163.c with the following content:

// Enter the driver name so that when we initialize the (maybe numerous)
// DM163 peripherals we can designated them by index.
#define DT_DRV_COMPAT siti_dm163

#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/led.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>

LOG_MODULE_REGISTER(dm163, LOG_LEVEL_DBG);

struct dm163_config {
  const struct gpio_dt_spec en;
  const struct gpio_dt_spec gck;
  const struct gpio_dt_spec lat;
  const struct gpio_dt_spec rst;
  const struct gpio_dt_spec selbk;
  const struct gpio_dt_spec sin;
};

#define CONFIGURE_PIN(dt, flags)                                               \
  do {                                                                         \
    if (!device_is_ready((dt)->port)) {                                        \
      LOG_ERR("device %s is not ready", (dt)->port->name);                     \
      return -ENODEV;                                                          \
    }                                                                          \
    gpio_pin_configure_dt(dt, flags);                                          \
  } while (0)

static int dm163_init(const struct device *dev) {
  const struct dm163_config *config = dev->config;

  LOG_DBG("starting initialization of device %s", dev->name);

  // Disable DM163 outputs while configuring if this pin
  // is connected.
  if (config->en.port) {
    CONFIGURE_PIN(&config->en, GPIO_OUTPUT_INACTIVE);
  }
  // Configure all pins. Make reset active so that the DM163
  // initiates a reset. We want the clock (gck) and latch (lat)
  // to be inactive at start. selbk will select bank 1 by default.
  CONFIGURE_PIN(&config->rst, GPIO_OUTPUT_ACTIVE);
  CONFIGURE_PIN(&config->gck, GPIO_OUTPUT_INACTIVE);
  CONFIGURE_PIN(&config->lat, GPIO_OUTPUT_INACTIVE);
  CONFIGURE_PIN(&config->selbk, GPIO_OUTPUT_ACTIVE);
  CONFIGURE_PIN(&config->sin, GPIO_OUTPUT);
  k_usleep(1); // 100ns min
  // Cancel reset by making it inactive.
  gpio_pin_set_dt(&config->rst, 0);
  // Enable the outputs if this pin is connected.
  if (config->en.port) {
    gpio_pin_set_dt(&config->en, 1);
  }
  LOG_INF("device %s initialized", dev->name);
  return 0;
}

// Macro to initialize the DM163 peripheral with index i
#define DM163_DEVICE(i)                                                        \
                                                                               \
  /* Build a dm163_config for DM163 peripheral with index i, named          */ \
  /* dm163_config_/i/ (for example dm163_config_0 for the first peripheral) */ \
  static const struct dm163_config dm163_config_##i = {                        \
      .en = GPIO_DT_SPEC_GET_OR(DT_DRV_INST(i), en_gpios, {0}),                \
      .gck = GPIO_DT_SPEC_GET(DT_DRV_INST(i), gck_gpios),                      \
      .lat = GPIO_DT_SPEC_GET(DT_DRV_INST(i), lat_gpios),                      \
      .rst = GPIO_DT_SPEC_GET(DT_DRV_INST(i), rst_gpios),                      \
      .selbk = GPIO_DT_SPEC_GET(DT_DRV_INST(i), selbk_gpios),                  \
      .sin = GPIO_DT_SPEC_GET(DT_DRV_INST(i), sin_gpios),                      \
  };                                                                           \
                                                                               \
  DEVICE_DT_INST_DEFINE(i, &dm163_init, NULL, NULL, &dm163_config_##i,         \
                        POST_KERNEL, CONFIG_LED_INIT_PRIORITY, NULL);

// Apply the DM163_DEVICE to all DM163 peripherals not marked "disabled"
// in the device tree and pass it the corresponding index.
DT_INST_FOREACH_STATUS_OKAY(DM163_DEVICE)

Since you can have several identical peripheral on a board, they will be identified by an index. The DT*_INST_* macros work with peripherals of type DT_DRV_COMPAT (which we defined at the top of the file). Here, the DM163_DEVICE macro will be applied to each peripheral marked compatible with the siti_dm163 driver. Note how the , from the device tree (in "siti,dm163") as been replaced by a _ for compatibility with the C language.

You have probably understood arleady what DT_DRV_INST(i) does: it designates the node in the device tree containing instance i of driver DT_DRV_COMPAT. Also, DEVICE_DT_INST_DEFINE() creates an instance of the struct device. After those macros have executed, you end up with having statically created a struct device for every instance of the DM163 peripherals.

The other interesting parameters are:

  • POST_KERNEL: the driver must be initialized after the kernel has started because we use the k_sleep() kernel service in dm163_init().
  • CONFIG_LED_INIT_PRIORITY: amongst all the drivers initialized in the POST_KERNEL phase, initialize this one at the same time as the other led drivers. We could have defined our own initialization piority but there is no need to do so.

Enabling the device driver

We need to create a Kconfig file in dm163_module/zephyr which adds a new CONFIG_DM163_DRIVER option. Let's make this option be selected by default as soon as there is a peripheral wanting to use the siti,dm163 driver in the device tree. In this case, CONFIG_DT_HAS_SITI_DM163_ENABLED will be defined automatically by the device tree compiler. Also, we need to ensure that CONFIG_GPIO and CONFIG_LED are selected when we use this driver, as we use gpios and will make our driver compatible with the led driver. Here is the content of dm163_module/zephyr/Kconfig:

config DM163_DRIVER
  bool "Support for the DM163 led driver"
  default y
  depends on DT_HAS_SITI_DM163_ENABLED
  select GPIO
  select LED

There are two steps needed to complete our module. Create a file for cmake in dm163_module/zephyr/CMakeLists.txt indicating where the module sources are:

if(CONFIG_DM163_DRIVER)
  # Unused for now as we do not have any .h, but we might need to add
  # .h files later for this driver
  zephyr_include_directories(.)

  zephyr_library()
  zephyr_library_sources(dm163.c)
endif()

and a dm163_module/zephyr/module.yml which indicates where the directory with the cmake file and the Kconfig file are located relative to the module directory:

build:
  cmake: zephyr
  kconfig: zephyr/Kconfig

We must now indicate to the top-level cmake where our module can be found if we need it by appending the dm163_module to the list of Zephyr extra modules. Use the following content in CMakeLists.txt:

cmake_minimum_required(VERSION 3.20.0)

list(APPEND ZEPHYR_EXTRA_MODULES
  ${CMAKE_CURRENT_SOURCE_DIR}/dm163_module
)

find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(dm163_example)

target_sources(app PRIVATE src/main.c)

Let's now configure our project in prj.conf and add some logging capabilities:

CONFIG_LOG=y

Our main program in src/main.c does not need to be long:

int main() {
    return 0;
}

It is now time to build our project with west build -b disco_l475_iot1 and to flash it with west flash --runner jlink --reset. If everything went well, you should see the following on the USB serial port (for example /dev/ttyACM0):

*** Booting Zephyr OS build zephyr-v3.2.0-714-g7288df4c41d3  ***
[00:00:00.000,000] <dbg> dm163: dm163_init: starting initialization of device dm163
[00:00:00.000,000] <inf> dm163: device dm163 initialized

Yeah, we have the beginning of a device driver. Let's continue.