blob: 76c92686d842fbc183855c1f9a443a28dac26d74 [file] [log] [blame]
/*
* Copyright (c) 2022 Jimmy Ou <yanagiis@gmail.com>
*
* SPDX-License-Identifier: Apache-2.0
*/
#define DT_DRV_COMPAT maxim_max7219
/**
* @file
* @brief MAX7219 LED display driver
*
* This driver map the segment as x, digit as y.
*
* A MAX7219 has 8x8 pixels.
* Two MAX7219s (with cascading) have 8x16 pixels.
* So on and so forth.
*
* Datasheet: https://datasheets.maximintegrated.com/en/ds/MAX7219-MAX7221.pdf
*
* Limitations:
* 1. This driver only implements no-decode mode.
*/
#include <stddef.h>
#include <zephyr/drivers/display.h>
#include <zephyr/drivers/spi.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/sys/util.h>
#include <zephyr/kernel.h>
LOG_MODULE_REGISTER(max7219, CONFIG_DISPLAY_LOG_LEVEL);
#define MAX7219_SEGMENTS_PER_DIGIT 8
#define MAX7219_DIGITS_PER_DEVICE 8
/* clang-format off */
/* MAX7219 registers and fields */
#define MAX7219_REG_NOOP 0x00
#define MAX7219_NOOP 0x00
#define MAX7219_REG_DECODE_MODE 0x09
#define MAX7219_NO_DECODE 0x00
#define MAX7219_REG_INTENSITY 0x0A
#define MAX7219_REG_SCAN_LIMIT 0x0B
#define MAX7219_REG_SHUTDOWN 0x0C
#define MAX7219_SHUTDOWN_MODE 0x00
#define MAX7219_LEAVE_SHUTDOWN_MODE 0x01
#define MAX7219_REG_DISPLAY_TEST 0x0F
#define MAX7219_LEAVE_DISPLAY_TEST_MODE 0x00
#define MAX7219_DISPLAY_TEST_MODE 0x01
/* clang-format on */
struct max7219_config {
struct spi_dt_spec spi;
uint32_t num_cascading;
uint8_t intensity;
uint8_t scan_limit;
};
struct max7219_data {
/* Keep all digit_buf for all cascading MAX7219 */
uint8_t *digit_buf;
uint8_t *tx_buf;
};
static int max7219_transmit_all(const struct device *dev, const uint8_t addr, const uint8_t value)
{
const struct max7219_config *dev_config = dev->config;
struct max7219_data *dev_data = dev->data;
const struct spi_buf tx_buf = {
.buf = dev_data->tx_buf,
.len = dev_config->num_cascading * 2,
};
const struct spi_buf_set tx_bufs = {
.buffers = &tx_buf,
.count = 1U,
};
for (int i = 0; i < dev_config->num_cascading; i++) {
dev_data->tx_buf[i * 2] = addr;
dev_data->tx_buf[i * 2 + 1] = value;
}
return spi_write_dt(&dev_config->spi, &tx_bufs);
}
static int max7219_transmit_one(const struct device *dev, const uint8_t max7219_idx,
const uint8_t addr, const uint8_t value)
{
const struct max7219_config *dev_config = dev->config;
struct max7219_data *dev_data = dev->data;
const struct spi_buf tx_buf = {
.buf = dev_data->tx_buf,
.len = dev_config->num_cascading * 2,
};
const struct spi_buf_set tx_bufs = {
.buffers = &tx_buf,
.count = 1U,
};
for (int i = 0; i < dev_config->num_cascading; i++) {
if (i != (dev_config->num_cascading - 1 - max7219_idx)) {
dev_data->tx_buf[i * 2] = MAX7219_REG_NOOP;
dev_data->tx_buf[i * 2 + 1] = MAX7219_NOOP;
continue;
}
dev_data->tx_buf[i * 2] = addr;
dev_data->tx_buf[i * 2 + 1] = value;
}
return spi_write_dt(&dev_config->spi, &tx_bufs);
}
static inline uint8_t next_pixel(uint8_t *mask, uint8_t *data, const uint8_t **buf)
{
*mask <<= 1;
if (!*mask) {
*mask = 0x01;
*data = *(*buf)++;
}
return *data & *mask;
}
static inline void skip_pixel(uint8_t *mask, uint8_t *data, const uint8_t **buf, uint16_t count)
{
while (count--) {
next_pixel(mask, data, buf);
}
}
static int max7219_blanking_on(const struct device *dev)
{
ARG_UNUSED(dev);
return -ENOTSUP;
}
static int max7219_blanking_off(const struct device *dev)
{
ARG_UNUSED(dev);
return -ENOTSUP;
}
static int max7219_write(const struct device *dev, const uint16_t x, const uint16_t y,
const struct display_buffer_descriptor *desc, const void *buf)
{
const struct max7219_config *dev_config = dev->config;
struct max7219_data *dev_data = dev->data;
const uint16_t max_width = MAX7219_SEGMENTS_PER_DIGIT;
const uint16_t max_height = dev_config->num_cascading * MAX7219_DIGITS_PER_DEVICE;
/*
* MAX7219 only supports PIXEL_FORMAT_MONO01. 1 bit stands for 1 pixel.
*/
__ASSERT((desc->pitch * desc->height) <= (desc->buf_size * 8U), "Input buffer to small");
__ASSERT(desc->width <= desc->pitch, "Pitch is smaller then width");
__ASSERT(desc->pitch <= max_width, "Pitch in descriptor is larger than screen size");
__ASSERT(desc->height <= max_height, "Height in descriptor is larger than screen size");
__ASSERT(x + desc->pitch <= max_width,
"Writing outside screen boundaries in horizontal direction");
__ASSERT(y + desc->height <= max_height,
"Writing outside screen boundaries in vertical direction");
if (desc->width > desc->pitch || (desc->pitch * desc->height) > (desc->buf_size * 8U)) {
return -EINVAL;
}
if ((x + desc->pitch) > max_width || (y + desc->height) > max_height) {
return -EINVAL;
}
const uint16_t end_x = x + desc->width;
const uint16_t end_y = y + desc->height;
const uint8_t *byte_buf = buf;
const 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) {
const uint8_t max7219_idx = py / MAX7219_DIGITS_PER_DEVICE;
const uint8_t digit_idx = py % MAX7219_DIGITS_PER_DEVICE;
uint8_t segment = dev_data->digit_buf[py];
int ret;
for (uint16_t px = x; px < end_x; ++px) {
WRITE_BIT(segment, px, next_pixel(&mask, &data, &byte_buf));
}
skip_pixel(&mask, &data, &byte_buf, to_skip);
/* led register address begins from 1 */
ret = max7219_transmit_one(dev, max7219_idx, digit_idx + 1, segment);
if (ret < 0) {
return ret;
}
dev_data->digit_buf[y] = segment;
}
return 0;
}
static int max7219_read(const struct device *dev, const uint16_t x, const uint16_t y,
const struct display_buffer_descriptor *desc, void *buf)
{
ARG_UNUSED(dev);
ARG_UNUSED(x);
ARG_UNUSED(y);
ARG_UNUSED(desc);
ARG_UNUSED(buf);
return -ENOTSUP;
}
static void *max7219_get_framebuffer(const struct device *dev)
{
ARG_UNUSED(dev);
return NULL;
}
static int max7219_set_brightness(const struct device *dev, const uint8_t brightness)
{
int ret;
/*
* max7219 supports intensity value from 0x0 to 0xF.
* map the brightness from [0, 255] to [0, 15]
*/
ret = max7219_transmit_all(dev, MAX7219_REG_INTENSITY, brightness >> 4);
if (ret < 0) {
LOG_ERR("Failed to set brightness");
return ret;
}
return 0;
}
static int max7219_set_contrast(const struct device *dev, const uint8_t contrast)
{
ARG_UNUSED(dev);
ARG_UNUSED(contrast);
return -ENOTSUP;
}
static int max7219_set_pixel_format(const struct device *dev,
const enum display_pixel_format format)
{
ARG_UNUSED(dev);
switch (format) {
case PIXEL_FORMAT_MONO01:
return 0;
default:
return -ENOTSUP;
}
}
static int max7219_set_orientation(const struct device *dev,
const enum display_orientation orientation)
{
ARG_UNUSED(dev);
switch (orientation) {
case DISPLAY_ORIENTATION_NORMAL:
return 0;
default:
return -ENOTSUP;
}
}
static void max7219_get_capabilities(const struct device *dev, struct display_capabilities *caps)
{
const struct max7219_config *dev_config = dev->config;
caps->x_resolution = MAX7219_SEGMENTS_PER_DIGIT;
caps->y_resolution = MAX7219_DIGITS_PER_DEVICE * dev_config->num_cascading;
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 const struct display_driver_api max7219_api = {
.blanking_on = max7219_blanking_on,
.blanking_off = max7219_blanking_off,
.write = max7219_write,
.read = max7219_read,
.get_framebuffer = max7219_get_framebuffer,
.set_brightness = max7219_set_brightness,
.set_contrast = max7219_set_contrast,
.get_capabilities = max7219_get_capabilities,
.set_pixel_format = max7219_set_pixel_format,
.set_orientation = max7219_set_orientation,
};
static int max7219_init(const struct device *dev)
{
const struct max7219_config *dev_config = dev->config;
struct max7219_data *dev_data = dev->data;
int ret;
if (!spi_is_ready(&dev_config->spi)) {
LOG_ERR("SPI device not ready");
return -ENODEV;
}
/* turn off all leds */
memset(dev_data->digit_buf, 0,
dev_config->num_cascading * MAX7219_DIGITS_PER_DEVICE * sizeof(uint8_t));
ret = max7219_transmit_all(dev, MAX7219_REG_DISPLAY_TEST, MAX7219_LEAVE_DISPLAY_TEST_MODE);
if (ret < 0) {
LOG_ERR("Failed to disable display test");
return ret;
}
ret = max7219_transmit_all(dev, MAX7219_REG_DECODE_MODE, MAX7219_NO_DECODE);
if (ret < 0) {
LOG_ERR("Failed to set decode mode");
return ret;
}
ret = max7219_transmit_all(dev, MAX7219_REG_INTENSITY, dev_config->intensity);
if (ret < 0) {
LOG_ERR("Failed to set global brightness");
return ret;
}
ret = max7219_transmit_all(dev, MAX7219_REG_SCAN_LIMIT, dev_config->scan_limit);
if (ret < 0) {
LOG_ERR("Failed to set scan limit");
return ret;
}
ret = max7219_transmit_all(dev, MAX7219_REG_SHUTDOWN, MAX7219_LEAVE_SHUTDOWN_MODE);
if (ret < 0) {
LOG_ERR("Failed to leave shutdown state");
return ret;
}
const struct display_buffer_descriptor desc = {
.buf_size = dev_config->num_cascading * MAX7219_DIGITS_PER_DEVICE,
.height = dev_config->num_cascading * MAX7219_DIGITS_PER_DEVICE,
.width = MAX7219_DIGITS_PER_DEVICE,
.pitch = MAX7219_DIGITS_PER_DEVICE,
};
ret = max7219_write(dev, 0, 0, &desc, dev_data->digit_buf);
if (ret < 0) {
return ret;
}
return 0;
}
#define DISPLAY_MAX7219_INIT(n) \
static uint8_t max7219_digit_data_##n[DT_INST_PROP(n, num_cascading) * \
MAX7219_DIGITS_PER_DEVICE]; \
static uint8_t max7219_tx_buf##n[DT_INST_PROP(n, num_cascading) * 2]; \
static struct max7219_data max7219_data_##n = { \
.digit_buf = max7219_digit_data_##n, \
.tx_buf = max7219_tx_buf##n, \
}; \
static const struct max7219_config max7219_config_##n = { \
.spi = SPI_DT_SPEC_INST_GET( \
n, SPI_OP_MODE_MASTER | SPI_WORD_SET(8U), 0U), \
.num_cascading = DT_INST_PROP(n, num_cascading), \
.intensity = DT_INST_PROP(n, intensity), \
.scan_limit = DT_INST_PROP(n, scan_limit), \
}; \
DEVICE_DT_INST_DEFINE(n, max7219_init, NULL, &max7219_data_##n, \
&max7219_config_##n, POST_KERNEL, \
CONFIG_DISPLAY_INIT_PRIORITY, &max7219_api);
DT_INST_FOREACH_STATUS_OKAY(DISPLAY_MAX7219_INIT)