| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * AU Optronics A030JTN01.0 TFT LCD panel driver |
| * |
| * Copyright (C) 2023, Paul Cercueil <paul@crapouillou.net> |
| * Copyright (C) 2023, Christophe Branchereau <cbranchereau@gmail.com> |
| */ |
| |
| #include <linux/bitfield.h> |
| #include <linux/delay.h> |
| #include <linux/device.h> |
| #include <linux/gpio/consumer.h> |
| #include <linux/media-bus-format.h> |
| #include <linux/mod_devicetable.h> |
| #include <linux/module.h> |
| #include <linux/regmap.h> |
| #include <linux/regulator/consumer.h> |
| #include <linux/spi/spi.h> |
| |
| #include <drm/drm_modes.h> |
| #include <drm/drm_panel.h> |
| |
| #define REG05 0x05 |
| #define REG06 0x06 |
| #define REG07 0x07 |
| |
| #define REG05_STDBY BIT(0) |
| #define REG06_VBLK GENMASK(4, 0) |
| #define REG07_HBLK GENMASK(7, 0) |
| |
| |
| struct a030jtn01_info { |
| const struct drm_display_mode *display_modes; |
| unsigned int num_modes; |
| u16 width_mm, height_mm; |
| u32 bus_format, bus_flags; |
| }; |
| |
| struct a030jtn01 { |
| struct drm_panel panel; |
| struct spi_device *spi; |
| struct regmap *map; |
| |
| const struct a030jtn01_info *panel_info; |
| |
| struct regulator *supply; |
| struct gpio_desc *reset_gpio; |
| }; |
| |
| static inline struct a030jtn01 *to_a030jtn01(struct drm_panel *panel) |
| { |
| return container_of(panel, struct a030jtn01, panel); |
| } |
| |
| static int a030jtn01_prepare(struct drm_panel *panel) |
| { |
| struct a030jtn01 *priv = to_a030jtn01(panel); |
| struct device *dev = &priv->spi->dev; |
| unsigned int dummy; |
| int err; |
| |
| err = regulator_enable(priv->supply); |
| if (err) { |
| dev_err(dev, "Failed to enable power supply: %d\n", err); |
| return err; |
| } |
| |
| usleep_range(1000, 8000); |
| |
| /* Reset the chip */ |
| gpiod_set_value_cansleep(priv->reset_gpio, 1); |
| usleep_range(100, 8000); |
| gpiod_set_value_cansleep(priv->reset_gpio, 0); |
| usleep_range(2000, 8000); |
| |
| /* |
| * No idea why, but a register read (doesn't matter which) is needed to |
| * properly initialize the chip after a reset; otherwise, the colors |
| * will be wrong. It doesn't seem to be timing-related as a msleep(200) |
| * doesn't fix it. |
| */ |
| err = regmap_read(priv->map, REG05, &dummy); |
| if (err) |
| goto err_disable_regulator; |
| |
| /* Use (24 + 6) == 0x1e as the vertical back porch */ |
| err = regmap_write(priv->map, REG06, FIELD_PREP(REG06_VBLK, 0x1e)); |
| if (err) |
| goto err_disable_regulator; |
| |
| /* Use (42 + 30) * 3 == 0xd8 as the horizontal back porch */ |
| err = regmap_write(priv->map, REG07, FIELD_PREP(REG07_HBLK, 0xd8)); |
| if (err) |
| goto err_disable_regulator; |
| |
| return 0; |
| |
| err_disable_regulator: |
| gpiod_set_value_cansleep(priv->reset_gpio, 1); |
| regulator_disable(priv->supply); |
| return err; |
| } |
| |
| static int a030jtn01_unprepare(struct drm_panel *panel) |
| { |
| struct a030jtn01 *priv = to_a030jtn01(panel); |
| |
| gpiod_set_value_cansleep(priv->reset_gpio, 1); |
| regulator_disable(priv->supply); |
| |
| return 0; |
| } |
| |
| static int a030jtn01_enable(struct drm_panel *panel) |
| { |
| struct a030jtn01 *priv = to_a030jtn01(panel); |
| int ret; |
| |
| ret = regmap_set_bits(priv->map, REG05, REG05_STDBY); |
| if (ret) |
| return ret; |
| |
| /* Wait for the picture to be stable */ |
| if (panel->backlight) |
| msleep(100); |
| |
| return 0; |
| } |
| |
| static int a030jtn01_disable(struct drm_panel *panel) |
| { |
| struct a030jtn01 *priv = to_a030jtn01(panel); |
| |
| return regmap_clear_bits(priv->map, REG05, REG05_STDBY); |
| } |
| |
| static int a030jtn01_get_modes(struct drm_panel *panel, |
| struct drm_connector *connector) |
| { |
| struct a030jtn01 *priv = to_a030jtn01(panel); |
| const struct a030jtn01_info *panel_info = priv->panel_info; |
| struct drm_display_mode *mode; |
| unsigned int i; |
| |
| for (i = 0; i < panel_info->num_modes; i++) { |
| mode = drm_mode_duplicate(connector->dev, |
| &panel_info->display_modes[i]); |
| if (!mode) |
| return -ENOMEM; |
| |
| drm_mode_set_name(mode); |
| |
| mode->type = DRM_MODE_TYPE_DRIVER; |
| if (panel_info->num_modes == 1) |
| mode->type |= DRM_MODE_TYPE_PREFERRED; |
| |
| drm_mode_probed_add(connector, mode); |
| } |
| |
| connector->display_info.bpc = 8; |
| connector->display_info.width_mm = panel_info->width_mm; |
| connector->display_info.height_mm = panel_info->height_mm; |
| |
| drm_display_info_set_bus_formats(&connector->display_info, |
| &panel_info->bus_format, 1); |
| connector->display_info.bus_flags = panel_info->bus_flags; |
| |
| return panel_info->num_modes; |
| } |
| |
| static const struct drm_panel_funcs a030jtn01_funcs = { |
| .prepare = a030jtn01_prepare, |
| .unprepare = a030jtn01_unprepare, |
| .enable = a030jtn01_enable, |
| .disable = a030jtn01_disable, |
| .get_modes = a030jtn01_get_modes, |
| }; |
| |
| static bool a030jtn01_has_reg(struct device *dev, unsigned int reg) |
| { |
| static const u32 a030jtn01_regs_mask = 0x001823f1fb; |
| |
| return a030jtn01_regs_mask & BIT(reg); |
| }; |
| |
| static const struct regmap_config a030jtn01_regmap_config = { |
| .reg_bits = 8, |
| .val_bits = 8, |
| .read_flag_mask = 0x40, |
| .max_register = 0x1c, |
| .readable_reg = a030jtn01_has_reg, |
| .writeable_reg = a030jtn01_has_reg, |
| }; |
| |
| static int a030jtn01_probe(struct spi_device *spi) |
| { |
| struct device *dev = &spi->dev; |
| struct a030jtn01 *priv; |
| int err; |
| |
| spi->mode |= SPI_MODE_3 | SPI_3WIRE; |
| |
| priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); |
| if (!priv) |
| return -ENOMEM; |
| |
| priv->spi = spi; |
| spi_set_drvdata(spi, priv); |
| |
| priv->map = devm_regmap_init_spi(spi, &a030jtn01_regmap_config); |
| if (IS_ERR(priv->map)) |
| return dev_err_probe(dev, PTR_ERR(priv->map), "Unable to init regmap"); |
| |
| priv->panel_info = spi_get_device_match_data(spi); |
| if (!priv->panel_info) |
| return -EINVAL; |
| |
| priv->supply = devm_regulator_get(dev, "power"); |
| if (IS_ERR(priv->supply)) |
| return dev_err_probe(dev, PTR_ERR(priv->supply), "Failed to get power supply"); |
| |
| priv->reset_gpio = devm_gpiod_get(dev, "reset", GPIOD_OUT_HIGH); |
| if (IS_ERR(priv->reset_gpio)) |
| return dev_err_probe(dev, PTR_ERR(priv->reset_gpio), "Failed to get reset GPIO"); |
| |
| drm_panel_init(&priv->panel, dev, &a030jtn01_funcs, |
| DRM_MODE_CONNECTOR_DPI); |
| |
| err = drm_panel_of_backlight(&priv->panel); |
| if (err) |
| return err; |
| |
| drm_panel_add(&priv->panel); |
| |
| return 0; |
| } |
| |
| static void a030jtn01_remove(struct spi_device *spi) |
| { |
| struct a030jtn01 *priv = spi_get_drvdata(spi); |
| |
| drm_panel_remove(&priv->panel); |
| drm_panel_disable(&priv->panel); |
| drm_panel_unprepare(&priv->panel); |
| } |
| |
| static const struct drm_display_mode a030jtn01_modes[] = { |
| { /* 60 Hz */ |
| .clock = 14400, |
| .hdisplay = 320, |
| .hsync_start = 320 + 8, |
| .hsync_end = 320 + 8 + 42, |
| .htotal = 320 + 8 + 42 + 30, |
| .vdisplay = 480, |
| .vsync_start = 480 + 90, |
| .vsync_end = 480 + 90 + 24, |
| .vtotal = 480 + 90 + 24 + 6, |
| .flags = DRM_MODE_FLAG_NHSYNC | DRM_MODE_FLAG_NVSYNC, |
| }, |
| { /* 50 Hz */ |
| .clock = 12000, |
| .hdisplay = 320, |
| .hsync_start = 320 + 8, |
| .hsync_end = 320 + 8 + 42, |
| .htotal = 320 + 8 + 42 + 30, |
| .vdisplay = 480, |
| .vsync_start = 480 + 90, |
| .vsync_end = 480 + 90 + 24, |
| .vtotal = 480 + 90 + 24 + 6, |
| .flags = DRM_MODE_FLAG_NHSYNC | DRM_MODE_FLAG_NVSYNC, |
| }, |
| }; |
| |
| static const struct a030jtn01_info a030jtn01_info = { |
| .display_modes = a030jtn01_modes, |
| .num_modes = ARRAY_SIZE(a030jtn01_modes), |
| .width_mm = 70, |
| .height_mm = 51, |
| .bus_format = MEDIA_BUS_FMT_RGB888_3X8_DELTA, |
| .bus_flags = DRM_BUS_FLAG_PIXDATA_DRIVE_NEGEDGE, |
| }; |
| |
| static const struct spi_device_id a030jtn01_id[] = { |
| { "a030jtn01", (kernel_ulong_t) &a030jtn01_info }, |
| { /* sentinel */ } |
| }; |
| MODULE_DEVICE_TABLE(spi, a030jtn01_id); |
| |
| static const struct of_device_id a030jtn01_of_match[] = { |
| { .compatible = "auo,a030jtn01" }, |
| { /* sentinel */ } |
| }; |
| MODULE_DEVICE_TABLE(of, a030jtn01_of_match); |
| |
| static struct spi_driver a030jtn01_driver = { |
| .driver = { |
| .name = "auo-a030jtn01", |
| .of_match_table = a030jtn01_of_match, |
| }, |
| .id_table = a030jtn01_id, |
| .probe = a030jtn01_probe, |
| .remove = a030jtn01_remove, |
| }; |
| module_spi_driver(a030jtn01_driver); |
| |
| MODULE_AUTHOR("Paul Cercueil <paul@crapouillou.net>"); |
| MODULE_AUTHOR("Christophe Branchereau <cbranchereau@gmail.com>"); |
| MODULE_LICENSE("GPL"); |