drivers: display: Add support for LED matrix driven by nRF SoC GPIOs
Add a display driver and the corresponding devicetree binding for a LED
matrix with rows and columns driven by nRF SoCs GPIOs. Such matrix can
be found, for example, in the BBC micro:bit boards.
Signed-off-by: Andrzej Głąbek <andrzej.glabek@nordicsemi.no>
diff --git a/drivers/display/CMakeLists.txt b/drivers/display/CMakeLists.txt
index 2c72d93..492ebad 100644
--- a/drivers/display/CMakeLists.txt
+++ b/drivers/display/CMakeLists.txt
@@ -2,6 +2,7 @@
zephyr_library()
zephyr_library_sources_ifdef(CONFIG_DISPLAY_MCUX_ELCDIF display_mcux_elcdif.c)
+zephyr_library_sources_ifdef(CONFIG_DISPLAY_NRF_LED_MATRIX display_nrf_led_matrix.c)
zephyr_library_sources_ifdef(CONFIG_DUMMY_DISPLAY display_dummy.c)
zephyr_library_sources_ifdef(CONFIG_FRAMEBUF_DISPLAY display_framebuf.c)
zephyr_library_sources_ifdef(CONFIG_GD7965 gd7965.c)
diff --git a/drivers/display/Kconfig b/drivers/display/Kconfig
index 674bebd..c9a9f95 100644
--- a/drivers/display/Kconfig
+++ b/drivers/display/Kconfig
@@ -23,6 +23,7 @@
source "drivers/display/Kconfig.grove"
source "drivers/display/Kconfig.mcux_elcdif"
source "drivers/display/Kconfig.microbit"
+source "drivers/display/Kconfig.nrf_led_matrix"
source "drivers/display/Kconfig.ili9xxx"
source "drivers/display/Kconfig.sdl"
source "drivers/display/Kconfig.ssd1306"
diff --git a/drivers/display/Kconfig.nrf_led_matrix b/drivers/display/Kconfig.nrf_led_matrix
new file mode 100644
index 0000000..b89b9af
--- /dev/null
+++ b/drivers/display/Kconfig.nrf_led_matrix
@@ -0,0 +1,19 @@
+# Copyright (c) 2021, Nordic Semiconductor ASA
+# SPDX-License-Identifier: Apache-2.0
+
+config DISPLAY_NRF_LED_MATRIX
+ bool "LED matrix driven by GPIOs"
+ depends on SOC_FAMILY_NRF
+ select NRFX_GPIOTE
+ select NRFX_PPI if HAS_HW_NRF_PPI
+ help
+ Enable driver for a LED matrix with rows and columns driven by
+ GPIOs. The matrix is refreshed pixel by pixel (only one LED is
+ turned on in particular time slots) and each pixel can have one
+ of 256 levels of brightness (0 means off completely).
+ Assignment of GPIOs to rows and columns and the mapping of those
+ to pixels are specified in properties of a "nordic,nrf-led-matrix"
+ compatible node in devicetree.
+ The driver uses one TIMER instance and, depending on what is set
+ in devicetree, one PWM instance or one PPI channel and one GPIOTE
+ channel.
diff --git a/drivers/display/display_nrf_led_matrix.c b/drivers/display/display_nrf_led_matrix.c
new file mode 100644
index 0000000..ac81b5e
--- /dev/null
+++ b/drivers/display/display_nrf_led_matrix.c
@@ -0,0 +1,450 @@
+/*
+ * Copyright (c) 2021, Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <drivers/display.h>
+#include <devicetree.h>
+#include <dt-bindings/gpio/gpio.h>
+#include <hal/nrf_timer.h>
+#ifdef PWM_PRESENT
+#include <hal/nrf_pwm.h>
+#endif
+#include <nrfx_gpiote.h>
+#ifdef PPI_PRESENT
+#include <nrfx_ppi.h>
+#endif
+
+#define MATRIX_NODE DT_INST(0, nordic_nrf_led_matrix)
+#define TIMER_NODE DT_PHANDLE(MATRIX_NODE, timer)
+#define USE_PWM DT_NODE_HAS_PROP(MATRIX_NODE, pwm)
+#define ROW_COUNT DT_PROP_LEN(MATRIX_NODE, row_gpios)
+#define COL_COUNT DT_PROP_LEN(MATRIX_NODE, col_gpios)
+
+#define X_PIXELS DT_PROP(MATRIX_NODE, width)
+#define Y_PIXELS DT_PROP(MATRIX_NODE, height)
+#define PIXEL_COUNT DT_PROP_LEN(MATRIX_NODE, pixel_mapping)
+BUILD_ASSERT(PIXEL_COUNT == (X_PIXELS * Y_PIXELS),
+ "Invalid length of pixel-mapping");
+
+#define PIXEL_MAPPING(idx) DT_PROP_BY_IDX(MATRIX_NODE, pixel_mapping, idx)
+#define CHECK_PIXEL(node_id, pha, idx) \
+ BUILD_ASSERT((PIXEL_MAPPING(idx) >> 4) < ROW_COUNT, \
+ "Invalid row index in pixel-mapping["#idx"]"); \
+ BUILD_ASSERT((PIXEL_MAPPING(idx) & 0xF) < COL_COUNT, \
+ "Invalid column index in pixel-mapping["#idx"]");
+DT_FOREACH_PROP_ELEM(MATRIX_NODE, pixel_mapping, CHECK_PIXEL)
+
+#define REFRESH_FREQUENCY DT_PROP(MATRIX_NODE, refresh_frequency)
+#define BASE_FREQUENCY 16000000
+#define TIMER_CLK_CONFIG NRF_TIMER_FREQ_16MHz
+#define PWM_CLK_CONFIG NRF_PWM_CLK_16MHz
+#define BRIGHTNESS_MAX 255
+
+#define QUANTUM (BASE_FREQUENCY / (REFRESH_FREQUENCY * PIXEL_COUNT * \
+ BRIGHTNESS_MAX))
+#define PIXEL_PERIOD (BRIGHTNESS_MAX * QUANTUM)
+BUILD_ASSERT(PIXEL_PERIOD <= BIT_MASK(16));
+#if USE_PWM
+BUILD_ASSERT(PIXEL_PERIOD <= PWM_COUNTERTOP_COUNTERTOP_Msk);
+#endif
+
+#define ACTIVE_LOW_MASK 0x80
+#define PSEL_MASK 0x7F
+
+struct display_drv_config {
+ NRF_TIMER_Type *timer;
+#if USE_PWM
+ NRF_PWM_Type *pwm;
+#endif
+ uint8_t rows[ROW_COUNT];
+ uint8_t cols[COL_COUNT];
+ uint8_t pixel_mapping[PIXEL_COUNT];
+};
+
+struct display_drv_data {
+#if USE_PWM
+ uint16_t seq;
+#else
+ uint8_t gpiote_ch;
+#endif
+ uint8_t pixel_idx;
+ uint8_t framebuf[PIXEL_COUNT];
+ uint8_t brightness;
+ bool blanking;
+};
+
+static void set_pin(uint8_t pin_info, bool active)
+{
+ uint32_t value = active ? 1 : 0;
+
+ if (pin_info & ACTIVE_LOW_MASK) {
+ value = !value;
+ }
+ nrf_gpio_pin_write(pin_info & PSEL_MASK, value);
+}
+
+static int api_blanking_on(const struct device *dev)
+{
+ struct display_drv_data *dev_data = dev->data;
+ const struct display_drv_config *dev_config = dev->config;
+
+ if (!dev_data->blanking) {
+ nrf_timer_task_trigger(dev_config->timer, NRF_TIMER_TASK_STOP);
+ for (uint8_t i = 0; i < ROW_COUNT; ++i) {
+ set_pin(dev_config->rows[i], false);
+ }
+ for (uint8_t i = 0; i < COL_COUNT; ++i) {
+ set_pin(dev_config->cols[i], false);
+ }
+
+ dev_data->blanking = true;
+ }
+
+ return 0;
+}
+
+static int api_blanking_off(const struct device *dev)
+{
+ struct display_drv_data *dev_data = dev->data;
+ const struct display_drv_config *dev_config = dev->config;
+
+ if (dev_data->blanking) {
+ dev_data->pixel_idx = PIXEL_COUNT - 1;
+
+ nrf_timer_task_trigger(dev_config->timer, NRF_TIMER_TASK_CLEAR);
+ nrf_timer_task_trigger(dev_config->timer, NRF_TIMER_TASK_START);
+
+ dev_data->blanking = false;
+ }
+
+ return 0;
+}
+
+static void *api_get_framebuffer(const struct device *dev)
+{
+ struct display_drv_data *dev_data = dev->data;
+
+ return dev_data->framebuf;
+}
+
+static int api_set_brightness(const struct device *dev,
+ const uint8_t brightness)
+{
+ struct display_drv_data *dev_data = dev->data;
+ uint8_t new_brightness = CLAMP(brightness, 1, BRIGHTNESS_MAX);
+ int16_t delta = (int16_t)new_brightness - dev_data->brightness;
+
+ dev_data->brightness = new_brightness;
+
+ for (uint8_t i = 0; i < PIXEL_COUNT; ++i) {
+ uint8_t old_val = dev_data->framebuf[i];
+
+ if (old_val) {
+ int16_t new_val = old_val + delta;
+
+ dev_data->framebuf[i] =
+ (uint8_t)CLAMP(new_val, 1, BRIGHTNESS_MAX);
+ }
+ }
+
+ return 0;
+}
+
+static int api_set_contrast(const struct device *dev,
+ const uint8_t contrast)
+{
+ return -ENOTSUP;
+}
+
+static int api_set_pixel_format(const struct device *dev,
+ const enum display_pixel_format format)
+{
+ switch (format) {
+ case PIXEL_FORMAT_MONO01:
+ return 0;
+ default:
+ return -ENOTSUP;
+ }
+}
+
+static int api_set_orientation(const struct device *dev,
+ const enum display_orientation orientation)
+{
+ switch (orientation) {
+ case DISPLAY_ORIENTATION_NORMAL:
+ return 0;
+ default:
+ return -ENOTSUP;
+ }
+}
+
+static void api_get_capabilities(const struct device *dev,
+ struct display_capabilities *caps)
+{
+ caps->x_resolution = X_PIXELS;
+ caps->y_resolution = Y_PIXELS;
+ caps->supported_pixel_formats = PIXEL_FORMAT_MONO01;
+ caps->screen_info = 0;
+ caps->current_pixel_format = PIXEL_FORMAT_MONO01;
+ caps->current_orientation = DISPLAY_ORIENTATION_NORMAL;
+}
+
+static inline void move_to_next_pixel(uint8_t *mask, uint8_t *data,
+ const uint8_t **byte_buf)
+{
+ *mask <<= 1;
+ if (!*mask) {
+ *mask = 0x01;
+ *data = *(*byte_buf)++;
+ }
+}
+
+static int api_write(const struct device *dev,
+ const uint16_t x, const uint16_t y,
+ const struct display_buffer_descriptor *desc,
+ const void *buf)
+{
+ struct display_drv_data *dev_data = dev->data;
+ const uint8_t *byte_buf = buf;
+ uint16_t end_x = x + desc->width;
+ uint16_t end_y = y + desc->height;
+
+ if (x >= X_PIXELS || end_x > X_PIXELS ||
+ y >= Y_PIXELS || end_y > Y_PIXELS) {
+ return -EINVAL;
+ }
+
+ if (desc->pitch < desc->width) {
+ return -EINVAL;
+ }
+
+ uint16_t to_skip = desc->pitch - desc->width;
+ uint8_t mask = 0;
+ uint8_t data = 0;
+
+ for (uint16_t py = y; py < end_y; ++py) {
+ for (uint16_t px = x; px < end_x; ++px) {
+ move_to_next_pixel(&mask, &data, &byte_buf);
+ dev_data->framebuf[px + (py * X_PIXELS)] =
+ (data & mask) ? dev_data->brightness : 0;
+ }
+
+ if (to_skip) {
+ uint16_t cnt = to_skip;
+
+ do {
+ move_to_next_pixel(&mask, &data, &byte_buf);
+ } while (--cnt);
+ }
+ }
+
+ return 0;
+}
+
+static int api_read(const struct device *dev,
+ const uint16_t x, const uint16_t y,
+ const struct display_buffer_descriptor *desc,
+ void *buf)
+{
+ return -ENOTSUP;
+}
+
+const struct display_driver_api driver_api = {
+ .blanking_on = api_blanking_on,
+ .blanking_off = api_blanking_off,
+ .write = api_write,
+ .read = api_read,
+ .get_framebuffer = api_get_framebuffer,
+ .set_brightness = api_set_brightness,
+ .set_contrast = api_set_contrast,
+ .get_capabilities = api_get_capabilities,
+ .set_pixel_format = api_set_pixel_format,
+ .set_orientation = api_set_orientation,
+};
+
+static void timer_irq_handler(void *arg)
+{
+ const struct device *dev = arg;
+ struct display_drv_data *dev_data = dev->data;
+ const struct display_drv_config *dev_config = dev->config;
+ uint8_t prev_row_idx, pixel_mapping, row_pin_info, col_pin_info;
+ uint16_t pulse;
+
+ /* The timer is automagically stopped and cleared by shortcuts
+ * on the same event (COMPARE0) that generates this interrupt.
+ * But the event itself needs to be cleared here.
+ */
+ nrf_timer_event_clear(dev_config->timer, NRF_TIMER_EVENT_COMPARE0);
+
+ /* Disable the row that contains the previously handled pixel. */
+ prev_row_idx = dev_config->pixel_mapping[dev_data->pixel_idx] >> 4;
+ set_pin(dev_config->rows[prev_row_idx], false);
+ /* Disconnect that pixel column pin from the peripheral driving it. */
+#if USE_PWM
+ nrf_pwm_disable(dev_config->pwm);
+#else
+ NRF_GPIOTE->CONFIG[dev_data->gpiote_ch] = 0;
+#endif
+
+ /* Switch to the next pixel. */
+ ++dev_data->pixel_idx;
+ if (dev_data->pixel_idx >= PIXEL_COUNT) {
+ dev_data->pixel_idx = 0;
+ }
+ pixel_mapping = dev_config->pixel_mapping[dev_data->pixel_idx];
+ row_pin_info = dev_config->rows[pixel_mapping >> 4];
+ col_pin_info = dev_config->cols[pixel_mapping & 0xF];
+
+ /* Prepare the low pulse on the column pin for the current pixel. */
+ pulse = dev_data->framebuf[dev_data->pixel_idx] * QUANTUM;
+#if USE_PWM
+ dev_config->pwm->PSEL.OUT[0] = col_pin_info & PSEL_MASK;
+ dev_data->seq = pulse
+ | ((col_pin_info & ACTIVE_LOW_MASK) ? 0 : BIT(15));
+ nrf_pwm_enable(dev_config->pwm);
+ nrf_pwm_task_trigger(dev_config->pwm, NRF_PWM_TASK_SEQSTART0);
+#else
+ uint32_t gpiote_cfg = GPIOTE_CONFIG_MODE_Task
+ | ((col_pin_info & PSEL_MASK) << GPIOTE_CONFIG_PSEL_Pos);
+
+ if (col_pin_info & ACTIVE_LOW_MASK) {
+ gpiote_cfg |= (GPIOTE_CONFIG_POLARITY_LoToHi
+ << GPIOTE_CONFIG_POLARITY_Pos)
+ /* If there should be no pulse at all for a given
+ * pixel, its column GPIO needs to be configured
+ * as initially inactive.
+ */
+ | ((pulse == 0 ? GPIOTE_CONFIG_OUTINIT_High
+ : GPIOTE_CONFIG_OUTINIT_Low)
+ << GPIOTE_CONFIG_OUTINIT_Pos);
+ } else {
+ gpiote_cfg |= (GPIOTE_CONFIG_POLARITY_HiToLo
+ << GPIOTE_CONFIG_POLARITY_Pos)
+ | ((pulse == 0 ? GPIOTE_CONFIG_OUTINIT_Low
+ : GPIOTE_CONFIG_OUTINIT_High)
+ << GPIOTE_CONFIG_OUTINIT_Pos);
+ }
+ nrf_timer_cc_set(dev_config->timer, 1, pulse);
+ NRF_GPIOTE->CONFIG[dev_data->gpiote_ch] = gpiote_cfg;
+#endif
+
+ /* Enable the row drive for the current pixel and restart the timer. */
+ set_pin(row_pin_info, true);
+ nrf_timer_task_trigger(dev_config->timer, NRF_TIMER_TASK_START);
+}
+
+static int instance_init(const struct device *dev)
+{
+ struct display_drv_data *dev_data = dev->data;
+ const struct display_drv_config *dev_config = dev->config;
+
+#if USE_PWM
+ uint32_t out_psels[NRF_PWM_CHANNEL_COUNT] = {
+ NRF_PWM_PIN_NOT_CONNECTED,
+ NRF_PWM_PIN_NOT_CONNECTED,
+ NRF_PWM_PIN_NOT_CONNECTED,
+ NRF_PWM_PIN_NOT_CONNECTED,
+ };
+ nrf_pwm_sequence_t sequence = {
+ .values.p_raw = &dev_data->seq,
+ .length = 1,
+ };
+
+ nrf_pwm_pins_set(dev_config->pwm, out_psels);
+ nrf_pwm_configure(dev_config->pwm,
+ PWM_CLK_CONFIG, NRF_PWM_MODE_UP, PIXEL_PERIOD);
+ nrf_pwm_decoder_set(dev_config->pwm,
+ NRF_PWM_LOAD_COMMON, NRF_PWM_STEP_TRIGGERED);
+ nrf_pwm_sequence_set(dev_config->pwm, 0, &sequence);
+ nrf_pwm_loop_set(dev_config->pwm, 0);
+ nrf_pwm_shorts_set(dev_config->pwm, NRF_PWM_SHORT_SEQEND0_STOP_MASK);
+#else
+ nrfx_err_t err;
+ nrf_ppi_channel_t ppi_ch;
+
+ err = nrfx_ppi_channel_alloc(&ppi_ch);
+ if (err != NRFX_SUCCESS) {
+ return -ENOMEM;
+ }
+
+ err = nrfx_gpiote_channel_alloc(&dev_data->gpiote_ch);
+ if (err != NRFX_SUCCESS) {
+ nrfx_ppi_channel_free(ppi_ch);
+ return -ENOMEM;
+ }
+
+ nrf_ppi_channel_endpoint_setup(NRF_PPI, ppi_ch,
+ nrf_timer_event_address_get(dev_config->timer,
+ nrf_timer_compare_event_get(1)),
+ nrf_gpiote_event_address_get(NRF_GPIOTE,
+ nrf_gpiote_out_task_get(dev_data->gpiote_ch)));
+ nrf_ppi_channel_enable(NRF_PPI, ppi_ch);
+#endif /* USE_PWM */
+
+ for (uint8_t i = 0; i < ROW_COUNT; ++i) {
+ uint8_t row_pin_info = dev_config->rows[i];
+
+ set_pin(row_pin_info, false);
+ nrf_gpio_cfg(row_pin_info & PSEL_MASK,
+ NRF_GPIO_PIN_DIR_OUTPUT,
+ NRF_GPIO_PIN_INPUT_DISCONNECT,
+ NRF_GPIO_PIN_NOPULL,
+ NRF_GPIO_PIN_S0S1,
+ NRF_GPIO_PIN_NOSENSE);
+ }
+
+ for (uint8_t i = 0; i < COL_COUNT; ++i) {
+ uint8_t col_pin_info = dev_config->cols[i];
+
+ set_pin(col_pin_info, false);
+ nrf_gpio_cfg(col_pin_info & PSEL_MASK,
+ NRF_GPIO_PIN_DIR_OUTPUT,
+ NRF_GPIO_PIN_INPUT_DISCONNECT,
+ NRF_GPIO_PIN_NOPULL,
+ NRF_GPIO_PIN_S0S1,
+ NRF_GPIO_PIN_NOSENSE);
+ }
+
+ nrf_timer_bit_width_set(dev_config->timer, NRF_TIMER_BIT_WIDTH_16);
+ nrf_timer_frequency_set(dev_config->timer, TIMER_CLK_CONFIG);
+ nrf_timer_cc_set(dev_config->timer, 0, PIXEL_PERIOD);
+ nrf_timer_shorts_set(dev_config->timer,
+ NRF_TIMER_SHORT_COMPARE0_STOP_MASK |
+ NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK);
+ nrf_timer_event_clear(dev_config->timer, NRF_TIMER_EVENT_COMPARE0);
+ nrf_timer_int_enable(dev_config->timer, NRF_TIMER_INT_COMPARE0_MASK);
+
+ IRQ_CONNECT(DT_IRQN(TIMER_NODE), DT_IRQ(TIMER_NODE, priority),
+ timer_irq_handler, DEVICE_DT_GET(MATRIX_NODE), 0);
+ irq_enable(DT_IRQN(TIMER_NODE));
+
+ return 0;
+}
+
+static struct display_drv_data instance_data = {
+ .brightness = 0xFF,
+ .blanking = true,
+};
+
+#define GET_PIN_INFO(node_id, pha, idx) \
+ (DT_GPIO_PIN_BY_IDX(node_id, pha, idx) | \
+ (DT_PROP_BY_PHANDLE_IDX(node_id, pha, idx, port) << 5) | \
+ ((DT_GPIO_FLAGS_BY_IDX(node_id, pha, idx) & GPIO_ACTIVE_LOW) ? \
+ ACTIVE_LOW_MASK : 0)),
+
+static const struct display_drv_config instance_config = {
+ .timer = (NRF_TIMER_Type *)DT_REG_ADDR(TIMER_NODE),
+#if USE_PWM
+ .pwm = (NRF_PWM_Type *)DT_REG_ADDR(DT_PHANDLE(MATRIX_NODE, pwm)),
+#endif
+ .rows = { DT_FOREACH_PROP_ELEM(MATRIX_NODE, row_gpios, GET_PIN_INFO) },
+ .cols = { DT_FOREACH_PROP_ELEM(MATRIX_NODE, col_gpios, GET_PIN_INFO) },
+ .pixel_mapping = DT_PROP(MATRIX_NODE, pixel_mapping),
+};
+
+DEVICE_DT_DEFINE(MATRIX_NODE,
+ instance_init, NULL,
+ &instance_data, &instance_config,
+ POST_KERNEL, CONFIG_DISPLAY_INIT_PRIORITY, &driver_api);
diff --git a/dts/bindings/display/nordic,nrf-led-matrix.yaml b/dts/bindings/display/nordic,nrf-led-matrix.yaml
new file mode 100644
index 0000000..a14c772
--- /dev/null
+++ b/dts/bindings/display/nordic,nrf-led-matrix.yaml
@@ -0,0 +1,65 @@
+# Copyright (c) 2021, Nordic Semiconductor ASA
+# SPDX-License-Identifier: Apache-2.0
+
+description: Generic LED matrix driven by nRF SoC GPIOs
+
+compatible: "nordic,nrf-led-matrix"
+
+include: display-controller.yaml
+
+properties:
+ row-gpios:
+ type: phandle-array
+ required: true
+ description: |
+ Array of GPIOs to be used as rows of the matrix.
+
+ col-gpios:
+ type: phandle-array
+ required: true
+ description: |
+ Array of GPIOs to be used as columns of the matrix.
+
+ pixel-mapping:
+ type: uint8-array
+ required: true
+ description: |
+ Array of bytes that specify which rows and columns of the matrix
+ control its particular pixels, line by line. Each byte in this
+ array corresponds to one pixel of the matrix and specifies the row
+ index in the high nibble and the column index in the low nibble.
+
+ For example, the following snippet (from the bbc_microbit board DTS):
+
+ width = <5>;
+ height = <5>;
+ pixel-mapping = [00 13 01 14 02
+ 23 24 25 26 27
+ ...
+
+ specifies that:
+ - pixel (0,0) is controlled by row 0 and column 0
+ - pixel (1,0) is controlled by row 1 and column 3
+ - pixel (0,1) is controlled by row 2 and column 3
+ - pixel (1,1) is controlled by row 2 and column 4
+ and so on.
+
+ refresh-frequency:
+ type: int
+ required: true
+ description: |
+ Frequency of refreshing the matrix, in Hz.
+
+ timer:
+ type: phandle
+ required: true
+ description: |
+ Reference to a TIMER instance for controlling refreshing of the matrix.
+
+ pwm:
+ type: phandle
+ required: false
+ description: |
+ Reference to a PWM instance for generating pulse signals on column
+ GPIOs. If not provided, one PPI and one GPIOTE channel are allocated
+ and used instead for generating those pulses.