| /* |
| * Copyright (c) 2021, Nordic Semiconductor ASA |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #include <zephyr/drivers/display.h> |
| #include <zephyr/devicetree.h> |
| #include <zephyr/dt-bindings/gpio/gpio.h> |
| #include <soc.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 |
| #include <nrf_peripherals.h> |
| #include <zephyr/logging/log.h> |
| #include <zephyr/irq.h> |
| LOG_MODULE_REGISTER(nrf_led_matrix, CONFIG_DISPLAY_LOG_LEVEL); |
| |
| #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 PWM_NODE DT_PHANDLE(MATRIX_NODE, pwm) |
| #define ROW_COUNT DT_PROP_LEN(MATRIX_NODE, row_gpios) |
| #define COL_COUNT DT_PROP_LEN(MATRIX_NODE, col_gpios) |
| #define GROUP_SIZE DT_PROP(MATRIX_NODE, pixel_group_size) |
| #if (GROUP_SIZE > DT_PROP(TIMER_NODE, cc_num) - 1) || \ |
| (USE_PWM && GROUP_SIZE > PWM0_CH_NUM) |
| #error "Invalid pixel-group-size configured." |
| #endif |
| |
| #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) |
| #if (PIXEL_COUNT != (X_PIXELS * Y_PIXELS)) |
| #error "Invalid length of pixel-mapping." |
| #endif |
| |
| #define PIXEL_MAPPING(idx) DT_PROP_BY_IDX(MATRIX_NODE, pixel_mapping, idx) |
| |
| #define GET_DT_ROW_IDX(pixel_idx) \ |
| _GET_ROW_IDX(PIXEL_MAPPING(pixel_idx)) |
| #define GET_ROW_IDX(dev_config, pixel_idx) \ |
| _GET_ROW_IDX(dev_config->pixel_mapping[pixel_idx]) |
| #define _GET_ROW_IDX(byte) (byte >> 4) |
| |
| #define GET_DT_COL_IDX(pixel_idx) \ |
| _GET_COL_IDX(PIXEL_MAPPING(pixel_idx)) |
| #define GET_COL_IDX(dev_config, pixel_idx) \ |
| _GET_COL_IDX(dev_config->pixel_mapping[pixel_idx]) |
| #define _GET_COL_IDX(byte) (byte & 0xF) |
| |
| #define CHECK_PIXEL(node_id, pha, idx) \ |
| BUILD_ASSERT(GET_DT_ROW_IDX(idx) < ROW_COUNT, \ |
| "Invalid row index in pixel-mapping["#idx"]."); \ |
| BUILD_ASSERT(GET_DT_COL_IDX(idx) < 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 8000000 |
| #define TIMER_CLK_CONFIG NRF_TIMER_FREQ_8MHz |
| #define PWM_CLK_CONFIG NRF_PWM_CLK_8MHz |
| #define BRIGHTNESS_MAX 255 |
| |
| /* Always round up, as even a partially filled group uses the full time slot. */ |
| #define PIXEL_SLOTS (ROW_COUNT * NRFX_CEIL_DIV(COL_COUNT, GROUP_SIZE)) |
| #define QUANTUM (BASE_FREQUENCY \ |
| / (REFRESH_FREQUENCY * PIXEL_SLOTS * BRIGHTNESS_MAX)) |
| #define PIXEL_PERIOD (BRIGHTNESS_MAX * QUANTUM) |
| #if (PIXEL_PERIOD > BIT_MASK(16)) || \ |
| (USE_PWM && PIXEL_PERIOD > PWM_COUNTERTOP_COUNTERTOP_Msk) |
| #error "Invalid pixel period. Change refresh-frequency or pixel-group-size." |
| #endif |
| |
| #define ACTIVE_LOW_MASK 0x80 |
| #define PSEL_MASK 0x7F |
| |
| #if (GROUP_SIZE > 1) |
| #define ITERATION_COUNT ROW_COUNT * COL_COUNT |
| #else |
| #define ITERATION_COUNT PIXEL_COUNT |
| #endif |
| |
| struct display_drv_config { |
| NRF_TIMER_Type *timer; |
| #if USE_PWM |
| NRF_PWM_Type *pwm; |
| #else |
| nrfx_gpiote_t gpiote; |
| #endif |
| uint8_t rows[ROW_COUNT]; |
| uint8_t cols[COL_COUNT]; |
| uint8_t pixel_mapping[PIXEL_COUNT]; |
| #if (GROUP_SIZE > 1) |
| uint8_t refresh_order[ITERATION_COUNT]; |
| #endif |
| }; |
| |
| struct display_drv_data { |
| #if USE_PWM |
| uint16_t seq[PWM0_CH_NUM]; |
| #else |
| uint8_t gpiote_ch[GROUP_SIZE]; |
| #endif |
| uint8_t framebuf[PIXEL_COUNT]; |
| uint8_t iteration; |
| uint8_t prev_row_idx; |
| 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->iteration = ITERATION_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_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; |
| } |
| |
| const struct display_driver_api driver_api = { |
| .blanking_on = api_blanking_on, |
| .blanking_off = api_blanking_off, |
| .write = api_write, |
| .get_framebuffer = api_get_framebuffer, |
| .set_brightness = api_set_brightness, |
| .get_capabilities = api_get_capabilities, |
| .set_pixel_format = api_set_pixel_format, |
| .set_orientation = api_set_orientation, |
| }; |
| |
| static void prepare_pixel_pulse(const struct device *dev, |
| uint8_t pixel_idx, |
| uint8_t channel_idx) |
| { |
| struct display_drv_data *dev_data = dev->data; |
| const struct display_drv_config *dev_config = dev->config; |
| |
| uint8_t col_idx = GET_COL_IDX(dev_config, pixel_idx); |
| uint8_t col_pin_info = dev_config->cols[col_idx]; |
| uint8_t col_psel = (col_pin_info & PSEL_MASK); |
| bool col_active_low = (col_pin_info & ACTIVE_LOW_MASK); |
| uint16_t pulse = dev_data->framebuf[pixel_idx] * QUANTUM; |
| |
| #if USE_PWM |
| dev_config->pwm->PSEL.OUT[channel_idx] = col_psel; |
| dev_data->seq[channel_idx] = pulse | (col_active_low ? 0 : BIT(15)); |
| #else |
| uint32_t gpiote_cfg = GPIOTE_CONFIG_MODE_Task |
| | (col_psel << GPIOTE_CONFIG_PSEL_Pos); |
| |
| if (col_active_low) { |
| 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); |
| } |
| |
| /* First timer channel is used for timing the period of pulses. */ |
| nrf_timer_cc_set(dev_config->timer, 1 + channel_idx, pulse); |
| dev_config->gpiote.p_reg->CONFIG[dev_data->gpiote_ch[channel_idx]] = gpiote_cfg; |
| #endif /* USE_PWM */ |
| } |
| |
| |
| 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 iteration = dev_data->iteration; |
| uint8_t pixel_idx; |
| uint8_t row_idx; |
| |
| /* 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 was enabled in the previous iteration. */ |
| set_pin(dev_config->rows[dev_data->prev_row_idx], false); |
| /* Disconnect used column pins from the peripheral that drove them. */ |
| #if USE_PWM |
| nrf_pwm_disable(dev_config->pwm); |
| for (int i = 0; i < GROUP_SIZE; ++i) { |
| dev_config->pwm->PSEL.OUT[i] = NRF_PWM_PIN_NOT_CONNECTED; |
| } |
| #else |
| for (int i = 0; i < GROUP_SIZE; ++i) { |
| dev_config->gpiote.p_reg->CONFIG[dev_data->gpiote_ch[i]] = 0; |
| } |
| #endif |
| |
| for (int i = 0; i < GROUP_SIZE; ++i) { |
| #if (GROUP_SIZE > 1) |
| do { |
| ++iteration; |
| if (iteration >= ITERATION_COUNT) { |
| iteration = 0; |
| } |
| |
| pixel_idx = dev_config->refresh_order[iteration]; |
| } while (pixel_idx >= PIXEL_COUNT); |
| #else |
| ++iteration; |
| if (iteration >= ITERATION_COUNT) { |
| iteration = 0; |
| } |
| |
| pixel_idx = iteration; |
| #endif /* (GROUP_SIZE > 1) */ |
| |
| if (i == 0) { |
| row_idx = GET_ROW_IDX(dev_config, pixel_idx); |
| } else { |
| /* If the next pixel is in a different row, it cannot |
| * be lit within this group. |
| */ |
| if (row_idx != GET_ROW_IDX(dev_config, pixel_idx)) { |
| break; |
| } |
| } |
| |
| dev_data->iteration = iteration; |
| |
| prepare_pixel_pulse(dev, pixel_idx, i); |
| } |
| |
| /* Enable the row drive for the current pixel. */ |
| set_pin(dev_config->rows[row_idx], true); |
| |
| dev_data->prev_row_idx = row_idx; |
| |
| #if USE_PWM |
| /* Now that all the channels are configured, the PWM can be started. */ |
| nrf_pwm_enable(dev_config->pwm); |
| nrf_pwm_task_trigger(dev_config->pwm, NRF_PWM_TASK_SEQSTART0); |
| #endif |
| |
| /* Restart the timer. */ |
| 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 = PWM0_CH_NUM, |
| }; |
| |
| 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_INDIVIDUAL, 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; |
| |
| for (int i = 0; i < GROUP_SIZE; ++i) { |
| uint8_t *gpiote_ch = &dev_data->gpiote_ch[i]; |
| |
| err = nrfx_ppi_channel_alloc(&ppi_ch); |
| if (err != NRFX_SUCCESS) { |
| LOG_ERR("Failed to allocate PPI channel."); |
| /* Do not bother with freeing resources allocated |
| * so far. The application needs to be reconfigured |
| * anyway. |
| */ |
| return -ENOMEM; |
| } |
| |
| err = nrfx_gpiote_channel_alloc(&dev_config->gpiote, gpiote_ch); |
| if (err != NRFX_SUCCESS) { |
| LOG_ERR("Failed to allocate GPIOTE channel."); |
| /* Do not bother with freeing resources allocated |
| * so far. The application needs to be reconfigured |
| * anyway. |
| */ |
| 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 + i)), |
| nrf_gpiote_event_address_get(dev_config->gpiote.p_reg, |
| nrf_gpiote_out_task_get(*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_H0H1, |
| 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_prescaler_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, |
| }; |
| |
| #if !USE_PWM |
| #define CHECK_GPIOTE_INST(node_id, prop, idx) \ |
| BUILD_ASSERT(NRF_DT_GPIOTE_INST_BY_IDX(node_id, prop, idx) == \ |
| NRF_DT_GPIOTE_INST_BY_IDX(node_id, prop, 0), \ |
| "All column GPIOs must use the same GPIOTE instance"); |
| DT_FOREACH_PROP_ELEM(MATRIX_NODE, col_gpios, CHECK_GPIOTE_INST) |
| #endif |
| |
| #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)), |
| |
| #define ADD_FF(i, _) 0xFF |
| #define FILL_ROW_WITH_FF(node_id, pha, idx) LISTIFY(COL_COUNT, ADD_FF, (,)), |
| #define GET_PIXEL_ORDINAL(node_id, pha, idx) \ |
| [GET_DT_ROW_IDX(idx) * COL_COUNT + \ |
| GET_DT_COL_IDX(idx)] = idx, |
| |
| 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(PWM_NODE), |
| #else |
| .gpiote = NRFX_GPIOTE_INSTANCE( |
| NRF_DT_GPIOTE_INST_BY_IDX(MATRIX_NODE, col_gpios, 0)), |
| #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), |
| #if (GROUP_SIZE > 1) |
| /* The whole array is by default filled with FFs, then the elements |
| * for the actually used row/columns pairs are overwritten (using |
| * designators) with the proper ordinal values for pixels. |
| */ |
| .refresh_order = { DT_FOREACH_PROP_ELEM(MATRIX_NODE, row_gpios, |
| FILL_ROW_WITH_FF) |
| DT_FOREACH_PROP_ELEM(MATRIX_NODE, pixel_mapping, |
| GET_PIXEL_ORDINAL) }, |
| #endif |
| }; |
| |
| DEVICE_DT_DEFINE(MATRIX_NODE, |
| instance_init, NULL, |
| &instance_data, &instance_config, |
| POST_KERNEL, CONFIG_DISPLAY_INIT_PRIORITY, &driver_api); |