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 thek_sleep()
kernel service indm163_init()
.CONFIG_LED_INIT_PRIORITY
: amongst all the drivers initialized in thePOST_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.