Dynamic data
As we defined the struct dm163_config
to hold read-only configuration information about a peripheral, we now need to define a struct dm163_data
to hold dynamic information.
The DM163 has two data banks:
- Bank 0 contains 6 bits per channel (a channel is the red, green, or blue color for one RGB led).
- Bank 1 contains 8 bits per channel.
The PWM value presented on a given channel will be proportional to (bank0÷64)×(bank1÷256). We will use bank 0 to represent the brightness of the led and bank 1 to represent the color intensity.
Typically, all 24 channels (for 8 leds) will have the same brightness. It is useful to reduce the overall power.
This gives us the structure of our struct dm163_data
:
#define NUM_LEDS 8
#define NUM_CHANNELS (NUM_LEDS * 3)
struct dm163_data {
uint8_t brightness[NUM_CHANNELS];
uint8_t channels[NUM_CHANNELS];
};
We can now extend our DM163_DEVICE
macro so that every struct device
also contains a struct dm163_data
to represent the dynamic data of a given DM163 peripheral:
// 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), \
}; \
\
/* Build a new dm163_data_/i/ structure for dynamic data */ \
static struct dm163_data dm163_data_##i = {}; \
\
DEVICE_DT_INST_DEFINE(i, &dm163_init, NULL, &dm163_data_##i, \
&dm163_config_##i, POST_KERNEL, \
CONFIG_LED_INIT_PRIORITY, NULL);
Let's initialize our data by adding the following fragments in dm163_init()
. Right after the definition of config
, we'll define data
and retrieve it from the const struct device *dev
that we received:
struct dm163_data *data = dev->data;
Then right after we mark the config->rst
pin as inactive and before we enable config->en
, we'll initialize the data and send it to the DM163 peripheral:
memset(&data->brightness, 0x3f, sizeof(data->brightness));
memset(&data->channels, 0x00, sizeof(data->channels));
flush_brightness(dev);
flush_channels(dev);
Here we set the brightness of every channel to its maximum 0x3f, or 2⁶-1, and the color value to 0. Then we send all the brightness and channel information to the DM163: all leds are off.
Of course we'll have to write those flush_brightness()
and flush_channels()
functions.
Utility functions
Write the following functions.
Sending data to the DM163
static void pulse_data(const struct dm163_config *config, uint8_t data, int bits);
This function sends bits
bits of data
, MSB first. Sending a bit means settings its value on config->sin
, and making config->gck
active then inactive (clock pulse).
For example pulse_data(config, data, 6)
will send bit 5 of data, then bit 4, …, and end with bit 0. pulse_data(config, data, 8)
will send bit 7 of data, then bit 6, …, and end with bit 0.
Sending channel information
static void flush_channels(const struct device *dev) {
const struct dm163_config *config = dev->config;
struct dm163_data *data = dev->data;
for (int i = NUM_CHANNELS - 1; i >= 0; i--)
pulse_data(config, data->channels[i], 8);
gpio_pin_set_dt(&config->lat, 1);
gpio_pin_set_dt(&config->lat, 0);
}
This function sends all channel information (in reverse order) to bank 1 and then does a latch pulse by setting config->lat
as active (ground, because this pin is active low) then inactive (VCC). We also assume that the current selected bank (on config->sb
) is 1 when we enter this function.
The code of the function is given to show the standard way of working with the config
and data
members of the struct device
structure. By storing them into correctly typed variable (while they are of type void *
in struct device
), we can easily reference their fields, such as config->lat
or data->channels
.
Sending brightness information
static void flush_brightness(const struct device *dev);
This function sends the brightness information for all the channels (in reverse order) to bank 0. Bank 0 must be selected first, and bank 1 must be selected back at the end of the function so that when we return bank 1 is selected again. The rationale here is that we are more likely to frequently change colors rather than change the brightness of the leds.
Setting brightness for a led
static int dm163_set_brightness(const struct device *dev, uint32_t led, uint8_t value);
This function receives a brightness between 0 and 100 (inclusive) in value
. It must set it in data->brightness
for the three channels of the corresponding led
after converting the value
to the [0-63] interval. Once the brightness has been set, flush_brightness()
must be called for this to be reflected on the actual leds.
The return value is 0 if all goes well, -EINVAL
if the led number is incorrect.
Turning a led on and off
static int dm163_on(const struct device *dev, uint32_t led);
static int dm163_off(const struct device *dev, uint32_t led);
Those functions respectively turn a led on (full white) and off. Once the channels
have been updated, flush_channels()
must be called for this to be reflected on the actual leds.
Being compatible with the led API
We now have enough functions to be able to pretend to be compatible with Zephyr led driver API. Before the DM163_DEVICE()
macro, build a read-only struct led_driver_api
named dm163_api
:
static const struct led_driver_api dm163_api = {
.on = dm163_on,
.off = dm163_off,
.set_brightness = dm163_set_brightness,
};
Here we define three functions that can be called by the LED api functions (respectively led_on()
, led_off()
and led_set_brightness()
). We let other functions be undefined (NULL) for the time being.
Update the DEFINE_DT_INST_DEFINE()
call to include this newly defined structure as the last argument:
DEVICE_DT_INST_DEFINE(i, &dm163_init, NULL, &dm163_data_##i, \
&dm163_config_##i, POST_KERNEL, \
CONFIG_LED_INIT_PRIORITY, &dm163_api);
Using the LED API and testing our code
We can now build a real main program:
#include <zephyr/device.h>
#include <zephyr/kernel.h>
#include <zephyr/drivers/led.h>
#define DM163_NODE DT_NODELABEL(dm163)
static const struct device *dm163_dev = DEVICE_DT_GET(DM163_NODE);
int main() {
if (!device_is_ready(dm163_dev)) {
return -ENODEV;
}
// Set brightness to 5% for all leds so that we don't become blind
for (int i = 0; i < 8; i++)
led_set_brightness(dm163_dev, i, 5);
// Animate the leds
for (;;) {
for (int col = 0; col < 8; col++) {
led_on(dm163_dev, col);
k_sleep(K_MSEC(100));
led_off(dm163_dev, col);
}
}
}
Execute the program. Does it work?
If it does, think about how strange this is: you haven't enabled any line transistor and yet, one line is enabled. How comes? Since CONFIG_DM163_DRIVER
, which is implicit because you have declared one such peripheral is the device tree, implies CONFIG_LED
, then the GPIO leds are configured. And one of those leds correspond to the same pin as one of the lines. You got lucky.
Using the shell
If you activate the CONFIG_SHELL
and CONFIG_LED_SHELL
options, you'll see a led
menu in the shell that can be used to turn the leds on or off:
uart:~$ led on dm163 4
dm163: turning on LED 4
uart:~$ led off dm163 4
dm163: turning off LED 4