On 1/26/22 15:15, Javier Martinez Canillas wrote: > On 1/26/22 15:10, Andy Shevchenko wrote: >> On Wed, Jan 26, 2022 at 04:08:32PM +0200, Andy Shevchenko wrote: >>> On Wed, Jan 26, 2022 at 02:46:08PM +0100, Javier Martinez Canillas wrote: >>>> On 1/26/22 14:12, Andy Shevchenko wrote: >> >> ... >> >>>> I've just bought a SSD1306 (I2C) based one and will attempt to write a DRM >>>> driver using drivers/staging/fbtft/fb_ssd1306.c as a reference. >>> >>> You should take ssd1307fb.c instead. And basically create a MIPI based driver >>> for I2C. Then we won't go same road again for other similar devices. >> >> For the record it supports your device: >> >> static const struct i2c_device_id ssd1307fb_i2c_id[] = { >> { "ssd1305fb", 0 }, >> { "ssd1306fb", 0 }, >> { "ssd1307fb", 0 }, >> { "ssd1309fb", 0 }, >> >> > > Thanks a lot for the pointer. I was only looking at drivers/staging > and didn't check drivers/video. I'll try to convert that one then > once I get the display. > I got some time this weekend and was able to port the ssd1306 fbdev driver to DRM [0]. I'm not that familiar with the simple display pipe helpers, so there may be some wrong things there. But it does work and all the fbtests from https://git.kernel.org/pub/scm/linux/kernel/git/geert/fbtest.git pass. There are some hacks in the driver though. For example it exposes an XRGB8888 format even thought the OLED display is monochromatic and has 1 bit per pixel. The driver then goes and converts the XRGB8888 pixels first to grayscale and then to reverse mono. I took that idea from the repaper driver but that gives us the multiple copies that Geert was complaining about. Another hack is that I am just hardcoding the {width, height}_mm, but I don't know what DPI could be used for these panels nor how I could calculate the mm. Best regards, Javier [0]: >From 5ec4b468b66022d4c48ae6bec8a68926a01a6785 Mon Sep 17 00:00:00 2001 From: Javier Martinez Canillas <javierm@xxxxxxxxxx> Date: Sun, 30 Jan 2022 12:16:34 +0100 Subject: [RFC] drm/tiny: Add driver for Solomon SSD130X OLED displays Add a DRM driver for SSD1305, SSD1306, SSD1307 and SSD1309 Solomon OLED controllers that can be programmed via an I2C interface. This is a port of the ssd1307fb driver that already supports these devices. A Device Tree binding is not added because the DRM driver is compatible with the existing binding for the ssd1307fb driver. Signed-off-by: Javier Martinez Canillas <javierm@xxxxxxxxxx> --- MAINTAINERS | 7 + drivers/gpu/drm/tiny/Kconfig | 12 + drivers/gpu/drm/tiny/Makefile | 1 + drivers/gpu/drm/tiny/ssd130x.c | 944 +++++++++++++++++++++++++++++++++ 4 files changed, 964 insertions(+) create mode 100644 drivers/gpu/drm/tiny/ssd130x.c diff --git a/MAINTAINERS b/MAINTAINERS index d03ad8da1f36..87334676ce07 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -6102,6 +6102,13 @@ T: git git://anongit.freedesktop.org/drm/drm-misc F: Documentation/devicetree/bindings/display/repaper.txt F: drivers/gpu/drm/tiny/repaper.c +DRM DRIVER FOR SOLOMON SSD130X DISPLAYS +M: Javier Martinez Canillas <javierm@xxxxxxxxxx> +S: Maintained +T: git git://anongit.freedesktop.org/drm/drm-misc +F: Documentation/devicetree/bindings/display/solomon,ssd1307fb.yaml +F: drivers/gpu/drm/tiny/ssd130x.c + DRM DRIVER FOR QEMU'S CIRRUS DEVICE M: Dave Airlie <airlied@xxxxxxxxxx> M: Gerd Hoffmann <kraxel@xxxxxxxxxx> diff --git a/drivers/gpu/drm/tiny/Kconfig b/drivers/gpu/drm/tiny/Kconfig index 712e0004e96e..358ceb7354f5 100644 --- a/drivers/gpu/drm/tiny/Kconfig +++ b/drivers/gpu/drm/tiny/Kconfig @@ -157,6 +157,18 @@ config TINYDRM_REPAPER If M is selected the module will be called repaper. +config TINYDRM_SSD130X + tristate "DRM support for Solomon SSD130X OLED displays" + depends on DRM && OF && I2C + select DRM_KMS_HELPER + select DRM_GEM_SHMEM_HELPER + select BACKLIGHT_CLASS_DEVICE + help + DRM driver for the SSD1305, SSD1306, SSD1307 and SSD1309 Solomon + OLED controllers that can be programmed via an I2C interface. + + If M is selected the module will be called ssd130x. + config TINYDRM_ST7586 tristate "DRM support for Sitronix ST7586 display panels" depends on DRM && SPI diff --git a/drivers/gpu/drm/tiny/Makefile b/drivers/gpu/drm/tiny/Makefile index 5d5505d40e7b..93a1d70155f0 100644 --- a/drivers/gpu/drm/tiny/Makefile +++ b/drivers/gpu/drm/tiny/Makefile @@ -12,5 +12,6 @@ obj-$(CONFIG_TINYDRM_ILI9341) += ili9341.o obj-$(CONFIG_TINYDRM_ILI9486) += ili9486.o obj-$(CONFIG_TINYDRM_MI0283QT) += mi0283qt.o obj-$(CONFIG_TINYDRM_REPAPER) += repaper.o +obj-$(CONFIG_TINYDRM_SSD130X) += ssd130x.o obj-$(CONFIG_TINYDRM_ST7586) += st7586.o obj-$(CONFIG_TINYDRM_ST7735R) += st7735r.o diff --git a/drivers/gpu/drm/tiny/ssd130x.c b/drivers/gpu/drm/tiny/ssd130x.c new file mode 100644 index 000000000000..88d88caeb37d --- /dev/null +++ b/drivers/gpu/drm/tiny/ssd130x.c @@ -0,0 +1,944 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * DRM driver for Solomon SSD130X OLED displays + * + * Copyright 2022 Red Hat Inc. + * + * Based on drivers/video/fbdev/ssd1307fb.c + * Copyright 2012 Free Electrons + * + */ + +#include <linux/backlight.h> +#include <linux/delay.h> +#include <linux/gpio/consumer.h> +#include <linux/i2c.h> +#include <linux/module.h> +#include <linux/property.h> +#include <linux/pwm.h> +#include <linux/regulator/consumer.h> + +#include <drm/drm_atomic_helper.h> +#include <drm/drm_damage_helper.h> +#include <drm/drm_drv.h> +#include <drm/drm_fb_cma_helper.h> +#include <drm/drm_fb_helper.h> +#include <drm/drm_format_helper.h> +#include <drm/drm_gem_atomic_helper.h> +#include <drm/drm_gem_framebuffer_helper.h> +#include <drm/drm_gem_shmem_helper.h> +#include <drm/drm_managed.h> +#include <drm/drm_modes.h> +#include <drm/drm_rect.h> +#include <drm/drm_probe_helper.h> +#include <drm/drm_simple_kms_helper.h> + +#define DRIVER_NAME "ssd130x" +#define DRIVER_DESC "DRM driver for Solomon SSD130X OLED displays" +#define DRIVER_DATE "20220130" +#define DRIVER_MAJOR 1 +#define DRIVER_MINOR 0 + +#define SSD130X_DATA 0x40 +#define SSD130X_COMMAND 0x80 + +#define SSD130X_SET_ADDRESS_MODE 0x20 +#define SSD130X_SET_ADDRESS_MODE_HORIZONTAL (0x00) +#define SSD130X_SET_ADDRESS_MODE_VERTICAL (0x01) +#define SSD130X_SET_ADDRESS_MODE_PAGE (0x02) +#define SSD130X_SET_COL_RANGE 0x21 +#define SSD130X_SET_PAGE_RANGE 0x22 +#define SSD130X_CONTRAST 0x81 +#define SSD130X_SET_LOOKUP_TABLE 0x91 +#define SSD130X_CHARGE_PUMP 0x8d +#define SSD130X_SEG_REMAP_ON 0xa1 +#define SSD130X_DISPLAY_OFF 0xae +#define SSD130X_SET_MULTIPLEX_RATIO 0xa8 +#define SSD130X_DISPLAY_ON 0xaf +#define SSD130X_START_PAGE_ADDRESS 0xb0 +#define SSD130X_SET_DISPLAY_OFFSET 0xd3 +#define SSD130X_SET_CLOCK_FREQ 0xd5 +#define SSD130X_SET_AREA_COLOR_MODE 0xd8 +#define SSD130X_SET_PRECHARGE_PERIOD 0xd9 +#define SSD130X_SET_COM_PINS_CONFIG 0xda +#define SSD130X_SET_VCOMH 0xdb + +#define MAX_CONTRAST 255 + +struct ssd130x_deviceinfo { + u32 default_vcomh; + u32 default_dclk_div; + u32 default_dclk_frq; + int need_pwm; + int need_chargepump; +}; + +struct ssd130x_device { + struct drm_device drm; + struct drm_simple_display_pipe pipe; + struct drm_display_mode mode; + struct drm_connector connector; + struct i2c_client *client; + + const struct ssd130x_deviceinfo *device_info; + + unsigned area_color_enable : 1; + unsigned com_invdir : 1; + unsigned com_lrremap : 1; + unsigned com_seq : 1; + unsigned lookup_table_set : 1; + unsigned low_power : 1; + unsigned seg_remap : 1; + u32 com_offset; + u32 contrast; + u32 dclk_div; + u32 dclk_frq; + u32 height; + u8 lookup_table[4]; + u32 page_offset; + u32 col_offset; + u32 prechargep1; + u32 prechargep2; + + struct backlight_device *bl_dev; + struct pwm_device *pwm; + struct gpio_desc *reset; + struct regulator *vbat_reg; + u32 vcomh; + u32 width; + /* Cached address ranges */ + u8 col_start; + u8 col_end; + u8 page_start; + u8 page_end; +}; + +struct ssd130x_array { + u8 type; + u8 data[]; +}; + +static inline struct ssd130x_device *drm_to_ssd130x(struct drm_device *drm) +{ + return container_of(drm, struct ssd130x_device, drm); +} + +static struct ssd130x_array *ssd130x_alloc_array(u32 len, u8 type) +{ + struct ssd130x_array *array; + + array = kzalloc(sizeof(struct ssd130x_array) + len, GFP_KERNEL); + if (!array) + return NULL; + + array->type = type; + + return array; +} + +static int ssd130x_write_array(struct i2c_client *client, + struct ssd130x_array *array, u32 len) +{ + int ret; + + len += sizeof(struct ssd130x_array); + + ret = i2c_master_send(client, (u8 *)array, len); + if (ret != len) { + dev_err(&client->dev, "Couldn't send I2C command.\n"); + return ret; + } + + return 0; +} + +static inline int ssd130x_write_cmd(struct i2c_client *client, u8 cmd) +{ + struct ssd130x_array *array; + int ret; + + array = ssd130x_alloc_array(1, SSD130X_COMMAND); + if (!array) + return -ENOMEM; + + array->data[0] = cmd; + + ret = ssd130x_write_array(client, array, 1); + kfree(array); + + return ret; +} + +static int ssd130x_set_col_range(struct ssd130x_device *ssd130x, + u8 col_start, u8 cols) +{ + u8 col_end = col_start + cols - 1; + int ret; + + if (col_start == ssd130x->col_start && col_end == ssd130x->col_end) + return 0; + + ret = ssd130x_write_cmd(ssd130x->client, SSD130X_SET_COL_RANGE); + if (ret < 0) + return ret; + + ret = ssd130x_write_cmd(ssd130x->client, col_start); + if (ret < 0) + return ret; + + ret = ssd130x_write_cmd(ssd130x->client, col_end); + if (ret < 0) + return ret; + + ssd130x->col_start = col_start; + ssd130x->col_end = col_end; + return 0; +} + +static int ssd130x_set_page_range(struct ssd130x_device *ssd130x, + u8 page_start, u8 pages) +{ + u8 page_end = page_start + pages - 1; + int ret; + + if (page_start == ssd130x->page_start && page_end == ssd130x->page_end) + return 0; + + ret = ssd130x_write_cmd(ssd130x->client, SSD130X_SET_PAGE_RANGE); + if (ret < 0) + return ret; + + ret = ssd130x_write_cmd(ssd130x->client, page_start); + if (ret < 0) + return ret; + + ret = ssd130x_write_cmd(ssd130x->client, page_end); + if (ret < 0) + return ret; + + ssd130x->page_start = page_start; + ssd130x->page_end = page_end; + return 0; +} + +static int ssd130x_update_display(struct ssd130x_device *ssd130x, u8 *buf, + u32 width, u32 height) +{ + struct ssd130x_array *array; + unsigned int line_length = DIV_ROUND_UP(width, 8); + unsigned int pages = DIV_ROUND_UP(height, 8); + u32 array_idx = 0; + int ret, i, j, k; + + array = ssd130x_alloc_array(width * pages, SSD130X_DATA); + if (!array) + return -ENOMEM; + + /* + * The screen is divided in pages, each having a height of 8 + * pixels, and the width of the screen. When sending a byte of + * data to the controller, it gives the 8 bits for the current + * column. I.e, the first byte are the 8 bits of the first + * column, then the 8 bits for the second column, etc. + * + * + * Representation of the screen, assuming it is 5 bits + * wide. Each letter-number combination is a bit that controls + * one pixel. + * + * A0 A1 A2 A3 A4 + * B0 B1 B2 B3 B4 + * C0 C1 C2 C3 C4 + * D0 D1 D2 D3 D4 + * E0 E1 E2 E3 E4 + * F0 F1 F2 F3 F4 + * G0 G1 G2 G3 G4 + * H0 H1 H2 H3 H4 + * + * If you want to update this screen, you need to send 5 bytes: + * (1) A0 B0 C0 D0 E0 F0 G0 H0 + * (2) A1 B1 C1 D1 E1 F1 G1 H1 + * (3) A2 B2 C2 D2 E2 F2 G2 H2 + * (4) A3 B3 C3 D3 E3 F3 G3 H3 + * (5) A4 B4 C4 D4 E4 F4 G4 H4 + */ + + ret = ssd130x_set_col_range(ssd130x, ssd130x->col_offset, width); + if (ret < 0) + goto out_free; + + ret = ssd130x_set_page_range(ssd130x, ssd130x->page_offset / 8, pages); + if (ret < 0) + goto out_free; + + for (i = 0; i < pages; i++) { + int m = 8; + + /* Last page may be partial */ + if (8 * (i + 1) > ssd130x->height) + m = ssd130x->height % 8; + for (j = 0; j < width; j++) { + u8 data = 0; + + for (k = 0; k < m; k++) { + u8 byte = buf[(8 * i + k) * line_length + + j / 8]; + u8 bit = (byte >> (j % 8)) & 1; + + data |= bit << k; + } + array->data[array_idx++] = data; + } + } + + ret = ssd130x_write_array(ssd130x->client, array, width * pages); + +out_free: + kfree(array); + return ret; +} + +static void ssd130x_gray8_to_mono_reversed(u8 *buf, u32 width, u32 height) +{ + u8 *gray8 = buf, *mono = buf; + int y, xb, i; + + for (y = 0; y < height; y++) + for (xb = 0; xb < width / 8; xb++) { + u8 byte = 0x00; + + for (i = 0; i < 8; i++) { + int x = xb * 8 + i; + + byte >>= 1; + if (gray8[y * width + x] >> 7) + byte |= BIT(7); + } + *mono++ = byte; + } +} + +static int ssd130x_fb_blit_rect(struct drm_framebuffer *fb, const struct dma_buf_map *map, + struct drm_rect *rect) +{ + struct ssd130x_device *ssd130x = drm_to_ssd130x(fb->dev); + void *vmap = map->vaddr; /* TODO: Use mapping abstraction properly */ + int idx, ret = 0; + u8 *buf = NULL; + + if (!drm_dev_enter(fb->dev, &idx)) + return -ENODEV; + + buf = kmalloc_array(fb->width, fb->height, GFP_KERNEL); + if (!buf) { + ret = -ENOMEM; + goto out_exit; + } + + drm_fb_xrgb8888_to_gray8(buf, 0, vmap, fb, rect); + + ssd130x_gray8_to_mono_reversed(buf, fb->width, fb->height); + + ssd130x_update_display(ssd130x, buf, fb->width, fb->height); + + kfree(buf); +out_exit: + drm_dev_exit(idx); + + return ret; +} + +static void ssd130x_display_pipe_enable(struct drm_simple_display_pipe *pipe, + struct drm_crtc_state *crtc_state, + struct drm_plane_state *plane_state) +{ + struct ssd130x_device *ssd130x = drm_to_ssd130x(pipe->crtc.dev); + int idx; + + if (!drm_dev_enter(pipe->crtc.dev, &idx)) + return; + + backlight_enable(ssd130x->bl_dev); + + ssd130x_write_cmd(ssd130x->client, SSD130X_DISPLAY_ON); +} + +static void ssd130x_display_pipe_disable(struct drm_simple_display_pipe *pipe) +{ + struct ssd130x_device *ssd130x = drm_to_ssd130x(pipe->crtc.dev); + + ssd130x_write_cmd(ssd130x->client, SSD130X_DISPLAY_OFF); + + backlight_disable(ssd130x->bl_dev); +} + +static void ssd130x_display_pipe_update(struct drm_simple_display_pipe *pipe, + struct drm_plane_state *old_plane_state) +{ + struct drm_plane_state *state = pipe->plane.state; + struct drm_shadow_plane_state *shadow_plane_state = to_drm_shadow_plane_state(state); + struct drm_rect rect; + + if (!pipe->crtc.state->active) + return; + + if (drm_atomic_helper_damage_merged(old_plane_state, state, &rect)) + ssd130x_fb_blit_rect(state->fb, &shadow_plane_state->data[0], &rect); +} + +static const struct drm_simple_display_pipe_funcs ssd130x_pipe_funcs = { + .enable = ssd130x_display_pipe_enable, + .disable = ssd130x_display_pipe_disable, + .update = ssd130x_display_pipe_update, + DRM_GEM_SIMPLE_DISPLAY_PIPE_SHADOW_PLANE_FUNCS, +}; + +static int ssd130x_connector_get_modes(struct drm_connector *connector) +{ + struct ssd130x_device *ssd130x = drm_to_ssd130x(connector->dev); + struct drm_display_mode *mode; + + mode = drm_mode_duplicate(connector->dev, &ssd130x->mode); + if (!mode) { + DRM_ERROR("Failed to duplicate mode\n"); + return 0; + } + + connector->display_info.width_mm = mode->width_mm; + connector->display_info.height_mm = mode->height_mm; + connector->display_info.bpc = 32; + + drm_mode_set_name(mode); + mode->type |= DRM_MODE_TYPE_PREFERRED; + drm_mode_probed_add(connector, mode); + + return 1; +} + +static const struct drm_connector_helper_funcs ssd130x_connector_helper_funcs = { + .get_modes = ssd130x_connector_get_modes, +}; + +static const struct drm_connector_funcs ssd130x_connector_funcs = { + .reset = drm_atomic_helper_connector_reset, + .fill_modes = drm_helper_probe_single_connector_modes, + .destroy = drm_connector_cleanup, + .atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state, + .atomic_destroy_state = drm_atomic_helper_connector_destroy_state, +}; + +static const struct drm_mode_config_funcs ssd130x_mode_config_funcs = { + .fb_create = drm_gem_fb_create_with_dirty, + .atomic_check = drm_atomic_helper_check, + .atomic_commit = drm_atomic_helper_commit, +}; + +static const uint32_t ssd130x_formats[] = { + DRM_FORMAT_XRGB8888, +}; + +DEFINE_DRM_GEM_FOPS(ssd130x_fops); + +static const struct drm_driver ssd130x_drm_driver = { + DRM_GEM_SHMEM_DRIVER_OPS, + .name = DRIVER_NAME, + .desc = DRIVER_DESC, + .date = DRIVER_DATE, + .major = DRIVER_MAJOR, + .minor = DRIVER_MINOR, + .driver_features = DRIVER_ATOMIC | DRIVER_GEM | DRIVER_MODESET, + .fops = &ssd130x_fops, +}; + +static int ssd130x_pwm_enable(struct ssd130x_device *ssd130x) +{ + struct pwm_state pwmstate; + + ssd130x->pwm = pwm_get(&ssd130x->client->dev, NULL); + if (IS_ERR(ssd130x->pwm)) { + dev_err(&ssd130x->client->dev, "Could not get PWM from device tree!\n"); + return PTR_ERR(ssd130x->pwm); + } + + pwm_init_state(ssd130x->pwm, &pwmstate); + pwm_set_relative_duty_cycle(&pwmstate, 50, 100); + pwm_apply_state(ssd130x->pwm, &pwmstate); + + /* Enable the PWM */ + pwm_enable(ssd130x->pwm); + + dev_dbg(&ssd130x->client->dev, "Using PWM%d with a %lluns period.\n", + ssd130x->pwm->pwm, pwm_get_period(ssd130x->pwm)); + + return 0; +} + +static int ssd130x_init(struct ssd130x_device *ssd130x) +{ + u32 precharge, dclk, com_invdir, compins; + int ret; + + /* Set initial contrast */ + ret = ssd130x_write_cmd(ssd130x->client, SSD130X_CONTRAST); + if (ret < 0) + return ret; + + ret = ssd130x_write_cmd(ssd130x->client, ssd130x->contrast); + if (ret < 0) + return ret; + + /* Set segment re-map */ + if (ssd130x->seg_remap) { + ret = ssd130x_write_cmd(ssd130x->client, SSD130X_SEG_REMAP_ON); + if (ret < 0) + return ret; + } + + /* Set COM direction */ + com_invdir = 0xc0 | ssd130x->com_invdir << 3; + ret = ssd130x_write_cmd(ssd130x->client, com_invdir); + if (ret < 0) + return ret; + + /* Set multiplex ratio value */ + ret = ssd130x_write_cmd(ssd130x->client, SSD130X_SET_MULTIPLEX_RATIO); + if (ret < 0) + return ret; + + ret = ssd130x_write_cmd(ssd130x->client, ssd130x->height - 1); + if (ret < 0) + return ret; + + /* set display offset value */ + ret = ssd130x_write_cmd(ssd130x->client, SSD130X_SET_DISPLAY_OFFSET); + if (ret < 0) + return ret; + + ret = ssd130x_write_cmd(ssd130x->client, ssd130x->com_offset); + if (ret < 0) + return ret; + + /* Set clock frequency */ + ret = ssd130x_write_cmd(ssd130x->client, SSD130X_SET_CLOCK_FREQ); + if (ret < 0) + return ret; + + dclk = ((ssd130x->dclk_div - 1) & 0xf) | (ssd130x->dclk_frq & 0xf) << 4; + ret = ssd130x_write_cmd(ssd130x->client, dclk); + if (ret < 0) + return ret; + + /* Set Area Color Mode ON/OFF & Low Power Display Mode */ + if (ssd130x->area_color_enable || ssd130x->low_power) { + u32 mode; + + ret = ssd130x_write_cmd(ssd130x->client, + SSD130X_SET_AREA_COLOR_MODE); + if (ret < 0) + return ret; + + mode = (ssd130x->area_color_enable ? 0x30 : 0) | + (ssd130x->low_power ? 5 : 0); + ret = ssd130x_write_cmd(ssd130x->client, mode); + if (ret < 0) + return ret; + } + + /* Set precharge period in number of ticks from the internal clock */ + ret = ssd130x_write_cmd(ssd130x->client, SSD130X_SET_PRECHARGE_PERIOD); + if (ret < 0) + return ret; + + precharge = (ssd130x->prechargep1 & 0xf) | (ssd130x->prechargep2 & 0xf) << 4; + ret = ssd130x_write_cmd(ssd130x->client, precharge); + if (ret < 0) + return ret; + + /* Set COM pins configuration */ + ret = ssd130x_write_cmd(ssd130x->client, SSD130X_SET_COM_PINS_CONFIG); + if (ret < 0) + return ret; + + compins = 0x02 | !ssd130x->com_seq << 4 | ssd130x->com_lrremap << 5; + ret = ssd130x_write_cmd(ssd130x->client, compins); + if (ret < 0) + return ret; + + /* Set VCOMH */ + ret = ssd130x_write_cmd(ssd130x->client, SSD130X_SET_VCOMH); + if (ret < 0) + return ret; + + ret = ssd130x_write_cmd(ssd130x->client, ssd130x->vcomh); + if (ret < 0) + return ret; + + /* Turn on the DC-DC Charge Pump */ + ret = ssd130x_write_cmd(ssd130x->client, SSD130X_CHARGE_PUMP); + if (ret < 0) + return ret; + + ret = ssd130x_write_cmd(ssd130x->client, + BIT(4) | (ssd130x->device_info->need_chargepump ? BIT(2) : 0)); + if (ret < 0) + return ret; + + /* Set lookup table */ + if (ssd130x->lookup_table_set) { + int i; + + ret = ssd130x_write_cmd(ssd130x->client, + SSD130X_SET_LOOKUP_TABLE); + if (ret < 0) + return ret; + + for (i = 0; i < ARRAY_SIZE(ssd130x->lookup_table); ++i) { + u8 val = ssd130x->lookup_table[i]; + + if (val < 31 || val > 63) + dev_warn(&ssd130x->client->dev, + "lookup table index %d value out of range 31 <= %d <= 63\n", + i, val); + ret = ssd130x_write_cmd(ssd130x->client, val); + if (ret < 0) + return ret; + } + } + + /* Switch to horizontal addressing mode */ + ret = ssd130x_write_cmd(ssd130x->client, SSD130X_SET_ADDRESS_MODE); + if (ret < 0) + return ret; + + ret = ssd130x_write_cmd(ssd130x->client, + SSD130X_SET_ADDRESS_MODE_HORIZONTAL); + if (ret < 0) + return ret; + + /* Turn on the display */ + ret = ssd130x_write_cmd(ssd130x->client, SSD130X_DISPLAY_ON); + if (ret < 0) + return ret; + + return 0; +} + +static int ssd130x_update_bl(struct backlight_device *bdev) +{ + struct ssd130x_device *ssd130x = bl_get_data(bdev); + int brightness = bdev->props.brightness; + int ret; + + ssd130x->contrast = brightness; + + ret = ssd130x_write_cmd(ssd130x->client, SSD130X_CONTRAST); + if (ret < 0) + return ret; + + ret = ssd130x_write_cmd(ssd130x->client, ssd130x->contrast); + if (ret < 0) + return ret; + + return 0; +} + +static int ssd130x_get_brightness(struct backlight_device *bdev) +{ + struct ssd130x_device *ssd130x = bl_get_data(bdev); + + return ssd130x->contrast; +} + +static const struct backlight_ops ssd130xfb_bl_ops = { + .update_status = ssd130x_update_bl, + .get_brightness = ssd130x_get_brightness, +}; + +static void ssd130x_reset(struct ssd130x_device *ssd130x) +{ + /* Reset the screen */ + gpiod_set_value_cansleep(ssd130x->reset, 1); + udelay(4); + gpiod_set_value_cansleep(ssd130x->reset, 0); + udelay(4); +} + +static void ssd130x_parse_properties(struct ssd130x_device *ssd130x) +{ + struct device *dev = &ssd130x->client->dev; + + if (device_property_read_u32(dev, "solomon,width", &ssd130x->width)) + ssd130x->width = 96; + + if (device_property_read_u32(dev, "solomon,height", &ssd130x->height)) + ssd130x->height = 16; + + if (device_property_read_u32(dev, "solomon,page-offset", &ssd130x->page_offset)) + ssd130x->page_offset = 1; + + if (device_property_read_u32(dev, "solomon,col-offset", &ssd130x->col_offset)) + ssd130x->col_offset = 0; + + if (device_property_read_u32(dev, "solomon,com-offset", &ssd130x->com_offset)) + ssd130x->com_offset = 0; + + if (device_property_read_u32(dev, "solomon,prechargep1", &ssd130x->prechargep1)) + ssd130x->prechargep1 = 2; + + if (device_property_read_u32(dev, "solomon,prechargep2", &ssd130x->prechargep2)) + ssd130x->prechargep2 = 2; + + if (!device_property_read_u8_array(dev, "solomon,lookup-table", + ssd130x->lookup_table, + ARRAY_SIZE(ssd130x->lookup_table))) + ssd130x->lookup_table_set = 1; + + ssd130x->seg_remap = !device_property_read_bool(dev, "solomon,segment-no-remap"); + ssd130x->com_seq = device_property_read_bool(dev, "solomon,com-seq"); + ssd130x->com_lrremap = device_property_read_bool(dev, "solomon,com-lrremap"); + ssd130x->com_invdir = device_property_read_bool(dev, "solomon,com-invdir"); + ssd130x->area_color_enable = + device_property_read_bool(dev, "solomon,area-color-enable"); + ssd130x->low_power = device_property_read_bool(dev, "solomon,low-power"); + + ssd130x->contrast = 127; + ssd130x->vcomh = ssd130x->device_info->default_vcomh; + + /* Setup display timing */ + if (device_property_read_u32(dev, "solomon,dclk-div", &ssd130x->dclk_div)) + ssd130x->dclk_div = ssd130x->device_info->default_dclk_div; + if (device_property_read_u32(dev, "solomon,dclk-frq", &ssd130x->dclk_frq)) + ssd130x->dclk_frq = ssd130x->device_info->default_dclk_frq; +} + +static void ssd130x_set_mode(struct ssd130x_device *ssd130x) +{ + struct drm_display_mode *mode = &ssd130x->mode; + struct drm_device *drm = &ssd130x->drm; + + mode->type = DRM_MODE_TYPE_DRIVER; + mode->clock = 1; + mode->hdisplay = mode->htotal = ssd130x->width; + mode->hsync_start = mode->hsync_end = ssd130x->width; + mode->vdisplay = mode->vtotal = ssd130x->height; + mode->vsync_start = mode->vsync_end = ssd130x->height; + mode->width_mm = 27; + mode->height_mm = 27; + + drm->mode_config.min_width = ssd130x->width; + drm->mode_config.max_width = ssd130x->width; + drm->mode_config.min_height = ssd130x->height; + drm->mode_config.max_height = ssd130x->height; + drm->mode_config.preferred_depth = 32; + drm->mode_config.funcs = &ssd130x_mode_config_funcs; +} + +static int ssd130x_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct ssd130x_device *ssd130x; + struct backlight_device *bl; + struct drm_device *drm; + char bl_name[12]; + int ret; + + ssd130x = devm_drm_dev_alloc(dev, &ssd130x_drm_driver, + struct ssd130x_device, drm); + if (IS_ERR(ssd130x)) + return PTR_ERR(ssd130x); + + drm = &ssd130x->drm; + + ret = drmm_mode_config_init(drm); + if (ret) + return ret; + + ssd130x->client = client; + + ssd130x->device_info = device_get_match_data(dev); + + ssd130x_parse_properties(ssd130x); + + ssd130x_set_mode(ssd130x); + + ssd130x->reset = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_LOW); + if (IS_ERR(ssd130x->reset)) { + dev_err(dev, "failed to get reset gpio: %ld\n", + PTR_ERR(ssd130x->reset)); + return PTR_ERR(ssd130x->reset); + } + + ssd130x->vbat_reg = devm_regulator_get_optional(dev, "vbat"); + if (IS_ERR(ssd130x->vbat_reg)) { + ret = PTR_ERR(ssd130x->vbat_reg); + if (ret == -ENODEV) { + ssd130x->vbat_reg = NULL; + } else { + dev_err(dev, "failed to get VBAT regulator: %d\n", ret); + return ret; + } + } + + drm_connector_helper_add(&ssd130x->connector, &ssd130x_connector_helper_funcs); + ret = drm_connector_init(drm, &ssd130x->connector, &ssd130x_connector_funcs, + DRM_MODE_CONNECTOR_Unknown); + if (ret) + return ret; + + i2c_set_clientdata(client, ssd130x); + + if (ssd130x->reset) + ssd130x_reset(ssd130x); + + if (ssd130x->vbat_reg) { + ret = regulator_enable(ssd130x->vbat_reg); + if (ret) { + dev_err(dev, "failed to enable VBAT: %d\n", ret); + return ret; + } + } + + ret = ssd130x_init(ssd130x); + if (ret) + goto regulator_disable; + + if (ssd130x->device_info->need_pwm) { + ret = ssd130x_pwm_enable(ssd130x); + if (ret) + goto regulator_disable; + } + + snprintf(bl_name, sizeof(bl_name), "ssd130x%d", drm->primary->index); + bl = backlight_device_register(bl_name, dev, ssd130x, &ssd130xfb_bl_ops, + NULL); + if (IS_ERR(bl)) { + ret = PTR_ERR(bl); + dev_err(dev, "unable to register backlight device: %d\n", ret); + goto pwm_disable; + } + + bl->props.brightness = ssd130x->contrast; + bl->props.max_brightness = MAX_CONTRAST; + ssd130x->bl_dev = bl; + + ret = drm_simple_display_pipe_init(drm, &ssd130x->pipe, &ssd130x_pipe_funcs, + ssd130x_formats, ARRAY_SIZE(ssd130x_formats), + NULL, &ssd130x->connector); + if (ret) + goto pwm_disable; + + drm_plane_enable_fb_damage_clips(&ssd130x->pipe.plane); + + drm_mode_config_reset(drm); + + ret = drm_dev_register(drm, 0); + if (ret) + goto backlight_unregister; + + drm_fbdev_generic_setup(drm, 0); + + return 0; + +backlight_unregister: + backlight_device_unregister(ssd130x->bl_dev); +pwm_disable: + if (ssd130x->device_info->need_pwm) { + pwm_disable(ssd130x->pwm); + pwm_put(ssd130x->pwm); + } +regulator_disable: + if (ssd130x->vbat_reg) + regulator_disable(ssd130x->vbat_reg); + return ret; +} + +static int ssd130x_remove(struct i2c_client *client) +{ + struct ssd130x_device *ssd130x = i2c_get_clientdata(client); + + drm_dev_unplug(&ssd130x->drm); + + ssd130x_write_cmd(ssd130x->client, SSD130X_DISPLAY_OFF); + + backlight_device_unregister(ssd130x->bl_dev); + + if (ssd130x->device_info->need_pwm) { + pwm_disable(ssd130x->pwm); + pwm_put(ssd130x->pwm); + } + if (ssd130x->vbat_reg) + regulator_disable(ssd130x->vbat_reg); + + return 0; +} + +static void ssd130x_shutdown(struct i2c_client *client) +{ + struct ssd130x_device *ssd130x = i2c_get_clientdata(client); + + drm_atomic_helper_shutdown(&ssd130x->drm); +} + +static struct ssd130x_deviceinfo ssd130x_ssd1305_deviceinfo = { + .default_vcomh = 0x34, + .default_dclk_div = 1, + .default_dclk_frq = 7, +}; + +static struct ssd130x_deviceinfo ssd130x_ssd1306_deviceinfo = { + .default_vcomh = 0x20, + .default_dclk_div = 1, + .default_dclk_frq = 8, + .need_chargepump = 1, +}; + +static struct ssd130x_deviceinfo ssd130x_ssd1307_deviceinfo = { + .default_vcomh = 0x20, + .default_dclk_div = 2, + .default_dclk_frq = 12, + .need_pwm = 1, +}; + +static struct ssd130x_deviceinfo ssd130x_ssd1309_deviceinfo = { + .default_vcomh = 0x34, + .default_dclk_div = 1, + .default_dclk_frq = 10, +}; + +static const struct of_device_id ssd130x_of_match[] = { + { + .compatible = "solomon,ssd1305fb-i2c", + .data = (void *)&ssd130x_ssd1305_deviceinfo, + }, + { + .compatible = "solomon,ssd1306fb-i2c", + .data = (void *)&ssd130x_ssd1306_deviceinfo, + }, + { + .compatible = "solomon,ssd1307fb-i2c", + .data = (void *)&ssd130x_ssd1307_deviceinfo, + }, + { + .compatible = "solomon,ssd1309fb-i2c", + .data = (void *)&ssd130x_ssd1309_deviceinfo, + }, + {}, +}; +MODULE_DEVICE_TABLE(of, ssd130x_of_match); + + +static struct i2c_driver ssd130x_i2c_driver = { + .driver = { + .name = DRIVER_NAME, + .of_match_table = ssd130x_of_match, + }, + .probe_new = ssd130x_probe, + .remove = ssd130x_remove, + .shutdown = ssd130x_shutdown, +}; + +module_i2c_driver(ssd130x_i2c_driver); + +MODULE_DESCRIPTION(DRIVER_DESC); +MODULE_AUTHOR("Javier Martinez Canillas <javierm@xxxxxxxxxx>"); +MODULE_LICENSE("GPL v2"); -- 2.34.1