| /* |
| * Copyright (c) 2017 Intel Corporation |
| * Copyright (c) 2021, Nordic Semiconductor ASA |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| /* |
| * This tool uses display controller driver API and requires |
| * a suitable LED matrix controller driver. |
| */ |
| |
| #include <zephyr/zephyr.h> |
| #include <zephyr/init.h> |
| #include <string.h> |
| #include <zephyr/sys/printk.h> |
| |
| #include <zephyr/display/mb_display.h> |
| #include <zephyr/drivers/display.h> |
| |
| #include "mb_font.h" |
| |
| #include <zephyr/logging/log.h> |
| LOG_MODULE_REGISTER(mb_disp, CONFIG_DISPLAY_LOG_LEVEL); |
| |
| #define MODE_MASK BIT_MASK(16) |
| |
| #define SCROLL_OFF 0 |
| #define SCROLL_START 1 |
| #define SCROLL_DEFAULT_DURATION_MS 80 |
| |
| #define MB_DISP_XRES 5 |
| #define MB_DISP_YRES 5 |
| |
| struct mb_display { |
| const struct device *lm_dev; /* LED matrix display device */ |
| |
| struct k_work_delayable dwork; /* Delayable work item */ |
| |
| uint8_t img_count; /* Image count */ |
| |
| uint8_t cur_img; /* Current image or character to show */ |
| |
| uint8_t scroll:3, /* Scroll shift */ |
| first:1, /* First frame of a scroll sequence */ |
| loop:1, /* Loop to beginning */ |
| text:1, /* We're showing a string (not image) */ |
| img_sep:1, /* One column image separation */ |
| msb:1; /* MSB represents the first pixel */ |
| |
| int32_t duration; /* Duration for each shown image */ |
| |
| union { |
| const struct mb_image *img; /* Array of images to show */ |
| const char *str; /* String to be shown */ |
| }; |
| |
| /* Buffer for printed strings */ |
| char str_buf[CONFIG_MICROBIT_DISPLAY_STR_MAX]; |
| }; |
| |
| static inline const struct mb_image *get_font(char ch) |
| { |
| if (ch < MB_FONT_START || ch > MB_FONT_END) { |
| return &mb_font[' ' - MB_FONT_START]; |
| } |
| |
| return &mb_font[ch - MB_FONT_START]; |
| } |
| |
| static ALWAYS_INLINE uint8_t flip_pixels(uint8_t b) |
| { |
| b = (b & 0xf0) >> 4 | (b & 0x0f) << 4; |
| b = (b & 0xcc) >> 2 | (b & 0x33) << 2; |
| b = (b & 0xaa) >> 1 | (b & 0x55) << 1; |
| |
| return b; |
| } |
| |
| static int update_content(struct mb_display *disp, const struct mb_image *img) |
| { |
| const struct display_buffer_descriptor buf_desc = { |
| .buf_size = sizeof(struct mb_image), |
| .width = MB_DISP_XRES, |
| .height = MB_DISP_YRES, |
| .pitch = 8, |
| }; |
| struct mb_image tmp_img; |
| int ret; |
| |
| if (disp->msb) { |
| for (int i = 0; i < sizeof(struct mb_image); i++) { |
| tmp_img.row[i] = flip_pixels(img->row[i]); |
| } |
| |
| ret = display_write(disp->lm_dev, 0, 0, &buf_desc, &tmp_img); |
| } else { |
| ret = display_write(disp->lm_dev, 0, 0, &buf_desc, img); |
| } |
| |
| if (ret < 0) { |
| LOG_ERR("Write to display controller failed"); |
| return ret; |
| } |
| |
| LOG_DBG("Image duration %d", disp->duration); |
| if (disp->duration != SYS_FOREVER_MS) { |
| k_work_reschedule(&disp->dwork, K_MSEC(disp->duration)); |
| } |
| |
| return ret; |
| } |
| |
| static int start_image(struct mb_display *disp, const struct mb_image *img) |
| { |
| int ret; |
| |
| ret = display_blanking_off(disp->lm_dev); |
| if (ret < 0) { |
| LOG_ERR("Set blanking off failed"); |
| return ret; |
| } |
| |
| return update_content(disp, img); |
| } |
| |
| static int reset_display(struct mb_display *disp) |
| { |
| int ret; |
| |
| disp->str = NULL; |
| disp->cur_img = 0U; |
| disp->img = NULL; |
| disp->img_count = 0U; |
| disp->scroll = SCROLL_OFF; |
| |
| ret = display_blanking_on(disp->lm_dev); |
| if (ret < 0) { |
| LOG_ERR("Set blanking on failed"); |
| } |
| |
| return ret; |
| } |
| |
| static const struct mb_image *current_img(struct mb_display *disp) |
| { |
| if (disp->scroll && disp->first) { |
| return get_font(' '); |
| } |
| |
| if (disp->text) { |
| return get_font(disp->str[disp->cur_img]); |
| } else { |
| return &disp->img[disp->cur_img]; |
| } |
| } |
| |
| static const struct mb_image *next_img(struct mb_display *disp) |
| { |
| if (disp->text) { |
| if (disp->first) { |
| return get_font(disp->str[0]); |
| } else if (disp->str[disp->cur_img]) { |
| return get_font(disp->str[disp->cur_img + 1]); |
| } else { |
| return get_font(' '); |
| } |
| } else { |
| if (disp->first) { |
| return &disp->img[0]; |
| } else if (disp->cur_img < (disp->img_count - 1)) { |
| return &disp->img[disp->cur_img + 1]; |
| } else { |
| return get_font(' '); |
| } |
| } |
| } |
| |
| static inline bool last_frame(struct mb_display *disp) |
| { |
| if (disp->text) { |
| return (disp->str[disp->cur_img] == '\0'); |
| } else { |
| return (disp->cur_img >= disp->img_count); |
| } |
| } |
| |
| static inline uint8_t scroll_steps(struct mb_display *disp) |
| { |
| return MB_DISP_XRES + disp->img_sep; |
| } |
| |
| static int update_scroll(struct mb_display *disp) |
| { |
| if (disp->scroll < scroll_steps(disp)) { |
| struct mb_image img; |
| |
| for (int i = 0; i < MB_DISP_XRES; i++) { |
| const struct mb_image *i1 = current_img(disp); |
| const struct mb_image *i2 = next_img(disp); |
| |
| img.row[i] = ((i1->row[i] >> disp->scroll) | |
| (i2->row[i] << (scroll_steps(disp) - |
| disp->scroll))); |
| } |
| |
| disp->scroll++; |
| return update_content(disp, &img); |
| } else { |
| if (disp->first) { |
| disp->first = 0U; |
| } else { |
| disp->cur_img++; |
| } |
| |
| if (last_frame(disp)) { |
| if (!disp->loop) { |
| return reset_display(disp); |
| } |
| |
| disp->cur_img = 0U; |
| disp->first = 1U; |
| } |
| |
| disp->scroll = SCROLL_START; |
| return update_content(disp, current_img(disp)); |
| } |
| } |
| |
| static int update_image(struct mb_display *disp) |
| { |
| disp->cur_img++; |
| |
| if (last_frame(disp)) { |
| if (!disp->loop) { |
| return reset_display(disp); |
| } |
| |
| disp->cur_img = 0U; |
| } |
| |
| return update_content(disp, current_img(disp)); |
| } |
| |
| static void update_display_work(struct k_work *work) |
| { |
| struct k_work_delayable *dwork = k_work_delayable_from_work(work); |
| struct mb_display *disp = CONTAINER_OF(dwork, struct mb_display, dwork); |
| int ret; |
| |
| if (disp->scroll) { |
| ret = update_scroll(disp); |
| } else { |
| ret = update_image(disp); |
| } |
| |
| __ASSERT(ret == 0, "Failed to update display"); |
| } |
| |
| static int start_scroll(struct mb_display *disp, int32_t duration) |
| { |
| /* Divide total duration by number of scrolling steps */ |
| if (duration) { |
| disp->duration = duration / scroll_steps(disp); |
| } else { |
| disp->duration = SCROLL_DEFAULT_DURATION_MS; |
| } |
| |
| disp->scroll = SCROLL_START; |
| disp->first = 1U; |
| disp->cur_img = 0U; |
| return start_image(disp, get_font(' ')); |
| } |
| |
| static int start_single(struct mb_display *disp, int32_t duration) |
| { |
| disp->duration = duration; |
| |
| if (disp->text) { |
| return start_image(disp, get_font(disp->str[0])); |
| } else { |
| return start_image(disp, disp->img); |
| } |
| } |
| |
| void mb_display_stop(struct mb_display *disp) |
| { |
| struct k_work_sync sync; |
| int ret; |
| |
| k_work_cancel_delayable_sync(&disp->dwork, &sync); |
| LOG_DBG("delayable work stopped %p", disp); |
| ret = reset_display(disp); |
| __ASSERT(ret == 0, "Failed to reset display"); |
| } |
| |
| void mb_display_image(struct mb_display *disp, uint32_t mode, int32_t duration, |
| const struct mb_image *img, uint8_t img_count) |
| { |
| int ret; |
| |
| mb_display_stop(disp); |
| |
| __ASSERT(img && img_count > 0, "Invalid parameters"); |
| |
| disp->text = 0U; |
| disp->img_count = img_count; |
| disp->img = img; |
| disp->img_sep = 0U; |
| disp->cur_img = 0U; |
| disp->loop = !!(mode & MB_DISPLAY_FLAG_LOOP); |
| |
| switch (mode & MODE_MASK) { |
| case MB_DISPLAY_MODE_DEFAULT: |
| case MB_DISPLAY_MODE_SINGLE: |
| ret = start_single(disp, duration); |
| __ASSERT(ret == 0, "Failed to start single mode"); |
| break; |
| case MB_DISPLAY_MODE_SCROLL: |
| ret = start_scroll(disp, duration); |
| __ASSERT(ret == 0, "Failed to start scroll mode"); |
| break; |
| default: |
| __ASSERT(0, "Invalid display mode"); |
| } |
| } |
| |
| void mb_display_print(struct mb_display *disp, uint32_t mode, |
| int32_t duration, const char *fmt, ...) |
| { |
| va_list ap; |
| int ret; |
| |
| mb_display_stop(disp); |
| |
| va_start(ap, fmt); |
| vsnprintk(disp->str_buf, sizeof(disp->str_buf), fmt, ap); |
| va_end(ap); |
| |
| if (disp->str_buf[0] == '\0') { |
| return; |
| } |
| |
| disp->str = disp->str_buf; |
| disp->text = 1U; |
| disp->img_sep = 1U; |
| disp->cur_img = 0U; |
| disp->loop = !!(mode & MB_DISPLAY_FLAG_LOOP); |
| |
| switch (mode & MODE_MASK) { |
| case MB_DISPLAY_MODE_DEFAULT: |
| case MB_DISPLAY_MODE_SCROLL: |
| ret = start_scroll(disp, duration); |
| __ASSERT(ret == 0, "Failed to start scroll mode"); |
| break; |
| case MB_DISPLAY_MODE_SINGLE: |
| ret = start_single(disp, duration); |
| __ASSERT(ret == 0, "Failed to start single mode"); |
| break; |
| default: |
| __ASSERT(0, "Invalid display mode"); |
| } |
| } |
| |
| static int mb_display_init(struct mb_display *disp) |
| { |
| struct display_capabilities caps; |
| int ret; |
| |
| display_get_capabilities(disp->lm_dev, &caps); |
| if (caps.x_resolution != MB_DISP_XRES || |
| caps.y_resolution != MB_DISP_YRES) { |
| LOG_ERR("Not supported display resolution"); |
| return -ENOTSUP; |
| } |
| |
| if (caps.screen_info & SCREEN_INFO_MONO_MSB_FIRST) { |
| disp->msb = 1U; |
| } |
| |
| ret = display_set_brightness(disp->lm_dev, 0xFF); |
| if (ret < 0) { |
| LOG_ERR("Failed to set brightness"); |
| return ret; |
| } |
| |
| k_work_init_delayable(&disp->dwork, update_display_work); |
| |
| return 0; |
| } |
| |
| static struct mb_display display; |
| |
| struct mb_display *mb_display_get(void) |
| { |
| return &display; |
| } |
| |
| static int mb_display_init_on_boot(const struct device *dev) |
| { |
| ARG_UNUSED(dev); |
| |
| display.lm_dev = DEVICE_DT_GET_ONE(nordic_nrf_led_matrix); |
| if (!device_is_ready(display.lm_dev)) { |
| LOG_ERR("Display controller device not ready"); |
| return -ENODEV; |
| } |
| |
| return mb_display_init(&display); |
| } |
| |
| SYS_INIT(mb_display_init_on_boot, APPLICATION, CONFIG_DISPLAY_INIT_PRIORITY); |