| // SPDX-License-Identifier: GPL-2.0-or-later |
| /* |
| * MIPI Display Bus Interface (DBI) LCD controller support |
| * |
| * Copyright 2016 Noralf Trønnes |
| */ |
| |
| #include <linux/debugfs.h> |
| #include <linux/delay.h> |
| #include <linux/dma-buf.h> |
| #include <linux/gpio/consumer.h> |
| #include <linux/module.h> |
| #include <linux/regulator/consumer.h> |
| #include <linux/spi/spi.h> |
| |
| #include <drm/drm_connector.h> |
| #include <drm/drm_damage_helper.h> |
| #include <drm/drm_drv.h> |
| #include <drm/drm_gem_cma_helper.h> |
| #include <drm/drm_format_helper.h> |
| #include <drm/drm_fourcc.h> |
| #include <drm/drm_gem_framebuffer_helper.h> |
| #include <drm/drm_mipi_dbi.h> |
| #include <drm/drm_modes.h> |
| #include <drm/drm_probe_helper.h> |
| #include <drm/drm_rect.h> |
| #include <drm/drm_vblank.h> |
| #include <video/mipi_display.h> |
| |
| #define MIPI_DBI_MAX_SPI_READ_SPEED 2000000 /* 2MHz */ |
| |
| #define DCS_POWER_MODE_DISPLAY BIT(2) |
| #define DCS_POWER_MODE_DISPLAY_NORMAL_MODE BIT(3) |
| #define DCS_POWER_MODE_SLEEP_MODE BIT(4) |
| #define DCS_POWER_MODE_PARTIAL_MODE BIT(5) |
| #define DCS_POWER_MODE_IDLE_MODE BIT(6) |
| #define DCS_POWER_MODE_RESERVED_MASK (BIT(0) | BIT(1) | BIT(7)) |
| |
| /** |
| * DOC: overview |
| * |
| * This library provides helpers for MIPI Display Bus Interface (DBI) |
| * compatible display controllers. |
| * |
| * Many controllers for tiny lcd displays are MIPI compliant and can use this |
| * library. If a controller uses registers 0x2A and 0x2B to set the area to |
| * update and uses register 0x2C to write to frame memory, it is most likely |
| * MIPI compliant. |
| * |
| * Only MIPI Type 1 displays are supported since a full frame memory is needed. |
| * |
| * There are 3 MIPI DBI implementation types: |
| * |
| * A. Motorola 6800 type parallel bus |
| * |
| * B. Intel 8080 type parallel bus |
| * |
| * C. SPI type with 3 options: |
| * |
| * 1. 9-bit with the Data/Command signal as the ninth bit |
| * 2. Same as above except it's sent as 16 bits |
| * 3. 8-bit with the Data/Command signal as a separate D/CX pin |
| * |
| * Currently mipi_dbi only supports Type C options 1 and 3 with |
| * mipi_dbi_spi_init(). |
| */ |
| |
| #define MIPI_DBI_DEBUG_COMMAND(cmd, data, len) \ |
| ({ \ |
| if (!len) \ |
| DRM_DEBUG_DRIVER("cmd=%02x\n", cmd); \ |
| else if (len <= 32) \ |
| DRM_DEBUG_DRIVER("cmd=%02x, par=%*ph\n", cmd, (int)len, data);\ |
| else \ |
| DRM_DEBUG_DRIVER("cmd=%02x, len=%zu\n", cmd, len); \ |
| }) |
| |
| static const u8 mipi_dbi_dcs_read_commands[] = { |
| MIPI_DCS_GET_DISPLAY_ID, |
| MIPI_DCS_GET_RED_CHANNEL, |
| MIPI_DCS_GET_GREEN_CHANNEL, |
| MIPI_DCS_GET_BLUE_CHANNEL, |
| MIPI_DCS_GET_DISPLAY_STATUS, |
| MIPI_DCS_GET_POWER_MODE, |
| MIPI_DCS_GET_ADDRESS_MODE, |
| MIPI_DCS_GET_PIXEL_FORMAT, |
| MIPI_DCS_GET_DISPLAY_MODE, |
| MIPI_DCS_GET_SIGNAL_MODE, |
| MIPI_DCS_GET_DIAGNOSTIC_RESULT, |
| MIPI_DCS_READ_MEMORY_START, |
| MIPI_DCS_READ_MEMORY_CONTINUE, |
| MIPI_DCS_GET_SCANLINE, |
| MIPI_DCS_GET_DISPLAY_BRIGHTNESS, |
| MIPI_DCS_GET_CONTROL_DISPLAY, |
| MIPI_DCS_GET_POWER_SAVE, |
| MIPI_DCS_GET_CABC_MIN_BRIGHTNESS, |
| MIPI_DCS_READ_DDB_START, |
| MIPI_DCS_READ_DDB_CONTINUE, |
| 0, /* sentinel */ |
| }; |
| |
| static bool mipi_dbi_command_is_read(struct mipi_dbi *dbi, u8 cmd) |
| { |
| unsigned int i; |
| |
| if (!dbi->read_commands) |
| return false; |
| |
| for (i = 0; i < 0xff; i++) { |
| if (!dbi->read_commands[i]) |
| return false; |
| if (cmd == dbi->read_commands[i]) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| /** |
| * mipi_dbi_command_read - MIPI DCS read command |
| * @dbi: MIPI DBI structure |
| * @cmd: Command |
| * @val: Value read |
| * |
| * Send MIPI DCS read command to the controller. |
| * |
| * Returns: |
| * Zero on success, negative error code on failure. |
| */ |
| int mipi_dbi_command_read(struct mipi_dbi *dbi, u8 cmd, u8 *val) |
| { |
| if (!dbi->read_commands) |
| return -EACCES; |
| |
| if (!mipi_dbi_command_is_read(dbi, cmd)) |
| return -EINVAL; |
| |
| return mipi_dbi_command_buf(dbi, cmd, val, 1); |
| } |
| EXPORT_SYMBOL(mipi_dbi_command_read); |
| |
| /** |
| * mipi_dbi_command_buf - MIPI DCS command with parameter(s) in an array |
| * @dbi: MIPI DBI structure |
| * @cmd: Command |
| * @data: Parameter buffer |
| * @len: Buffer length |
| * |
| * Returns: |
| * Zero on success, negative error code on failure. |
| */ |
| int mipi_dbi_command_buf(struct mipi_dbi *dbi, u8 cmd, u8 *data, size_t len) |
| { |
| u8 *cmdbuf; |
| int ret; |
| |
| /* SPI requires dma-safe buffers */ |
| cmdbuf = kmemdup(&cmd, 1, GFP_KERNEL); |
| if (!cmdbuf) |
| return -ENOMEM; |
| |
| mutex_lock(&dbi->cmdlock); |
| ret = dbi->command(dbi, cmdbuf, data, len); |
| mutex_unlock(&dbi->cmdlock); |
| |
| kfree(cmdbuf); |
| |
| return ret; |
| } |
| EXPORT_SYMBOL(mipi_dbi_command_buf); |
| |
| /* This should only be used by mipi_dbi_command() */ |
| int mipi_dbi_command_stackbuf(struct mipi_dbi *dbi, u8 cmd, u8 *data, size_t len) |
| { |
| u8 *buf; |
| int ret; |
| |
| buf = kmemdup(data, len, GFP_KERNEL); |
| if (!buf) |
| return -ENOMEM; |
| |
| ret = mipi_dbi_command_buf(dbi, cmd, buf, len); |
| |
| kfree(buf); |
| |
| return ret; |
| } |
| EXPORT_SYMBOL(mipi_dbi_command_stackbuf); |
| |
| /** |
| * mipi_dbi_buf_copy - Copy a framebuffer, transforming it if necessary |
| * @dst: The destination buffer |
| * @fb: The source framebuffer |
| * @clip: Clipping rectangle of the area to be copied |
| * @swap: When true, swap MSB/LSB of 16-bit values |
| * |
| * Returns: |
| * Zero on success, negative error code on failure. |
| */ |
| int mipi_dbi_buf_copy(void *dst, struct drm_framebuffer *fb, |
| struct drm_rect *clip, bool swap) |
| { |
| struct drm_gem_object *gem = drm_gem_fb_get_obj(fb, 0); |
| struct drm_gem_cma_object *cma_obj = to_drm_gem_cma_obj(gem); |
| struct dma_buf_attachment *import_attach = gem->import_attach; |
| struct drm_format_name_buf format_name; |
| void *src = cma_obj->vaddr; |
| int ret = 0; |
| |
| if (import_attach) { |
| ret = dma_buf_begin_cpu_access(import_attach->dmabuf, |
| DMA_FROM_DEVICE); |
| if (ret) |
| return ret; |
| } |
| |
| switch (fb->format->format) { |
| case DRM_FORMAT_RGB565: |
| if (swap) |
| drm_fb_swab16(dst, src, fb, clip); |
| else |
| drm_fb_memcpy(dst, src, fb, clip); |
| break; |
| case DRM_FORMAT_XRGB8888: |
| drm_fb_xrgb8888_to_rgb565(dst, src, fb, clip, swap); |
| break; |
| default: |
| dev_err_once(fb->dev->dev, "Format is not supported: %s\n", |
| drm_get_format_name(fb->format->format, |
| &format_name)); |
| return -EINVAL; |
| } |
| |
| if (import_attach) |
| ret = dma_buf_end_cpu_access(import_attach->dmabuf, |
| DMA_FROM_DEVICE); |
| return ret; |
| } |
| EXPORT_SYMBOL(mipi_dbi_buf_copy); |
| |
| static void mipi_dbi_fb_dirty(struct drm_framebuffer *fb, struct drm_rect *rect) |
| { |
| struct drm_gem_object *gem = drm_gem_fb_get_obj(fb, 0); |
| struct drm_gem_cma_object *cma_obj = to_drm_gem_cma_obj(gem); |
| struct mipi_dbi_dev *dbidev = drm_to_mipi_dbi_dev(fb->dev); |
| unsigned int height = rect->y2 - rect->y1; |
| unsigned int width = rect->x2 - rect->x1; |
| struct mipi_dbi *dbi = &dbidev->dbi; |
| bool swap = dbi->swap_bytes; |
| int idx, ret = 0; |
| bool full; |
| void *tr; |
| |
| if (!dbidev->enabled) |
| return; |
| |
| if (!drm_dev_enter(fb->dev, &idx)) |
| return; |
| |
| full = width == fb->width && height == fb->height; |
| |
| DRM_DEBUG_KMS("Flushing [FB:%d] " DRM_RECT_FMT "\n", fb->base.id, DRM_RECT_ARG(rect)); |
| |
| if (!dbi->dc || !full || swap || |
| fb->format->format == DRM_FORMAT_XRGB8888) { |
| tr = dbidev->tx_buf; |
| ret = mipi_dbi_buf_copy(dbidev->tx_buf, fb, rect, swap); |
| if (ret) |
| goto err_msg; |
| } else { |
| tr = cma_obj->vaddr; |
| } |
| |
| mipi_dbi_command(dbi, MIPI_DCS_SET_COLUMN_ADDRESS, |
| (rect->x1 >> 8) & 0xff, rect->x1 & 0xff, |
| ((rect->x2 - 1) >> 8) & 0xff, (rect->x2 - 1) & 0xff); |
| mipi_dbi_command(dbi, MIPI_DCS_SET_PAGE_ADDRESS, |
| (rect->y1 >> 8) & 0xff, rect->y1 & 0xff, |
| ((rect->y2 - 1) >> 8) & 0xff, (rect->y2 - 1) & 0xff); |
| |
| ret = mipi_dbi_command_buf(dbi, MIPI_DCS_WRITE_MEMORY_START, tr, |
| width * height * 2); |
| err_msg: |
| if (ret) |
| dev_err_once(fb->dev->dev, "Failed to update display %d\n", ret); |
| |
| drm_dev_exit(idx); |
| } |
| |
| /** |
| * mipi_dbi_pipe_update - Display pipe update helper |
| * @pipe: Simple display pipe |
| * @old_state: Old plane state |
| * |
| * This function handles framebuffer flushing and vblank events. Drivers can use |
| * this as their &drm_simple_display_pipe_funcs->update callback. |
| */ |
| void mipi_dbi_pipe_update(struct drm_simple_display_pipe *pipe, |
| struct drm_plane_state *old_state) |
| { |
| struct drm_plane_state *state = pipe->plane.state; |
| struct drm_crtc *crtc = &pipe->crtc; |
| struct drm_rect rect; |
| |
| if (drm_atomic_helper_damage_merged(old_state, state, &rect)) |
| mipi_dbi_fb_dirty(state->fb, &rect); |
| |
| if (crtc->state->event) { |
| spin_lock_irq(&crtc->dev->event_lock); |
| drm_crtc_send_vblank_event(crtc, crtc->state->event); |
| spin_unlock_irq(&crtc->dev->event_lock); |
| crtc->state->event = NULL; |
| } |
| } |
| EXPORT_SYMBOL(mipi_dbi_pipe_update); |
| |
| /** |
| * mipi_dbi_enable_flush - MIPI DBI enable helper |
| * @dbidev: MIPI DBI device structure |
| * @crtc_state: CRTC state |
| * @plane_state: Plane state |
| * |
| * This function sets &mipi_dbi->enabled, flushes the whole framebuffer and |
| * enables the backlight. Drivers can use this in their |
| * &drm_simple_display_pipe_funcs->enable callback. |
| * |
| * Note: Drivers which don't use mipi_dbi_pipe_update() because they have custom |
| * framebuffer flushing, can't use this function since they both use the same |
| * flushing code. |
| */ |
| void mipi_dbi_enable_flush(struct mipi_dbi_dev *dbidev, |
| struct drm_crtc_state *crtc_state, |
| struct drm_plane_state *plane_state) |
| { |
| struct drm_framebuffer *fb = plane_state->fb; |
| struct drm_rect rect = { |
| .x1 = 0, |
| .x2 = fb->width, |
| .y1 = 0, |
| .y2 = fb->height, |
| }; |
| int idx; |
| |
| if (!drm_dev_enter(&dbidev->drm, &idx)) |
| return; |
| |
| dbidev->enabled = true; |
| mipi_dbi_fb_dirty(fb, &rect); |
| backlight_enable(dbidev->backlight); |
| |
| drm_dev_exit(idx); |
| } |
| EXPORT_SYMBOL(mipi_dbi_enable_flush); |
| |
| static void mipi_dbi_blank(struct mipi_dbi_dev *dbidev) |
| { |
| struct drm_device *drm = &dbidev->drm; |
| u16 height = drm->mode_config.min_height; |
| u16 width = drm->mode_config.min_width; |
| struct mipi_dbi *dbi = &dbidev->dbi; |
| size_t len = width * height * 2; |
| int idx; |
| |
| if (!drm_dev_enter(drm, &idx)) |
| return; |
| |
| memset(dbidev->tx_buf, 0, len); |
| |
| mipi_dbi_command(dbi, MIPI_DCS_SET_COLUMN_ADDRESS, 0, 0, |
| ((width - 1) >> 8) & 0xFF, (width - 1) & 0xFF); |
| mipi_dbi_command(dbi, MIPI_DCS_SET_PAGE_ADDRESS, 0, 0, |
| ((height - 1) >> 8) & 0xFF, (height - 1) & 0xFF); |
| mipi_dbi_command_buf(dbi, MIPI_DCS_WRITE_MEMORY_START, |
| (u8 *)dbidev->tx_buf, len); |
| |
| drm_dev_exit(idx); |
| } |
| |
| /** |
| * mipi_dbi_pipe_disable - MIPI DBI pipe disable helper |
| * @pipe: Display pipe |
| * |
| * This function disables backlight if present, if not the display memory is |
| * blanked. The regulator is disabled if in use. Drivers can use this as their |
| * &drm_simple_display_pipe_funcs->disable callback. |
| */ |
| void mipi_dbi_pipe_disable(struct drm_simple_display_pipe *pipe) |
| { |
| struct mipi_dbi_dev *dbidev = drm_to_mipi_dbi_dev(pipe->crtc.dev); |
| |
| if (!dbidev->enabled) |
| return; |
| |
| DRM_DEBUG_KMS("\n"); |
| |
| dbidev->enabled = false; |
| |
| if (dbidev->backlight) |
| backlight_disable(dbidev->backlight); |
| else |
| mipi_dbi_blank(dbidev); |
| |
| if (dbidev->regulator) |
| regulator_disable(dbidev->regulator); |
| } |
| EXPORT_SYMBOL(mipi_dbi_pipe_disable); |
| |
| static int mipi_dbi_connector_get_modes(struct drm_connector *connector) |
| { |
| struct mipi_dbi_dev *dbidev = drm_to_mipi_dbi_dev(connector->dev); |
| struct drm_display_mode *mode; |
| |
| mode = drm_mode_duplicate(connector->dev, &dbidev->mode); |
| if (!mode) { |
| DRM_ERROR("Failed to duplicate mode\n"); |
| return 0; |
| } |
| |
| if (mode->name[0] == '\0') |
| drm_mode_set_name(mode); |
| |
| mode->type |= DRM_MODE_TYPE_PREFERRED; |
| drm_mode_probed_add(connector, mode); |
| |
| if (mode->width_mm) { |
| connector->display_info.width_mm = mode->width_mm; |
| connector->display_info.height_mm = mode->height_mm; |
| } |
| |
| return 1; |
| } |
| |
| static const struct drm_connector_helper_funcs mipi_dbi_connector_hfuncs = { |
| .get_modes = mipi_dbi_connector_get_modes, |
| }; |
| |
| static const struct drm_connector_funcs mipi_dbi_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 int mipi_dbi_rotate_mode(struct drm_display_mode *mode, |
| unsigned int rotation) |
| { |
| if (rotation == 0 || rotation == 180) { |
| return 0; |
| } else if (rotation == 90 || rotation == 270) { |
| swap(mode->hdisplay, mode->vdisplay); |
| swap(mode->hsync_start, mode->vsync_start); |
| swap(mode->hsync_end, mode->vsync_end); |
| swap(mode->htotal, mode->vtotal); |
| swap(mode->width_mm, mode->height_mm); |
| return 0; |
| } else { |
| return -EINVAL; |
| } |
| } |
| |
| static const struct drm_mode_config_funcs mipi_dbi_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 mipi_dbi_formats[] = { |
| DRM_FORMAT_RGB565, |
| DRM_FORMAT_XRGB8888, |
| }; |
| |
| /** |
| * mipi_dbi_dev_init_with_formats - MIPI DBI device initialization with custom formats |
| * @dbidev: MIPI DBI device structure to initialize |
| * @funcs: Display pipe functions |
| * @formats: Array of supported formats (DRM_FORMAT\_\*). |
| * @format_count: Number of elements in @formats |
| * @mode: Display mode |
| * @rotation: Initial rotation in degrees Counter Clock Wise |
| * @tx_buf_size: Allocate a transmit buffer of this size. |
| * |
| * This function sets up a &drm_simple_display_pipe with a &drm_connector that |
| * has one fixed &drm_display_mode which is rotated according to @rotation. |
| * This mode is used to set the mode config min/max width/height properties. |
| * |
| * Use mipi_dbi_dev_init() if you don't need custom formats. |
| * |
| * Note: |
| * Some of the helper functions expects RGB565 to be the default format and the |
| * transmit buffer sized to fit that. |
| * |
| * Returns: |
| * Zero on success, negative error code on failure. |
| */ |
| int mipi_dbi_dev_init_with_formats(struct mipi_dbi_dev *dbidev, |
| const struct drm_simple_display_pipe_funcs *funcs, |
| const uint32_t *formats, unsigned int format_count, |
| const struct drm_display_mode *mode, |
| unsigned int rotation, size_t tx_buf_size) |
| { |
| static const uint64_t modifiers[] = { |
| DRM_FORMAT_MOD_LINEAR, |
| DRM_FORMAT_MOD_INVALID |
| }; |
| struct drm_device *drm = &dbidev->drm; |
| int ret; |
| |
| if (!dbidev->dbi.command) |
| return -EINVAL; |
| |
| dbidev->tx_buf = devm_kmalloc(drm->dev, tx_buf_size, GFP_KERNEL); |
| if (!dbidev->tx_buf) |
| return -ENOMEM; |
| |
| drm_mode_copy(&dbidev->mode, mode); |
| ret = mipi_dbi_rotate_mode(&dbidev->mode, rotation); |
| if (ret) { |
| DRM_ERROR("Illegal rotation value %u\n", rotation); |
| return -EINVAL; |
| } |
| |
| drm_connector_helper_add(&dbidev->connector, &mipi_dbi_connector_hfuncs); |
| ret = drm_connector_init(drm, &dbidev->connector, &mipi_dbi_connector_funcs, |
| DRM_MODE_CONNECTOR_SPI); |
| if (ret) |
| return ret; |
| |
| ret = drm_simple_display_pipe_init(drm, &dbidev->pipe, funcs, formats, format_count, |
| modifiers, &dbidev->connector); |
| if (ret) |
| return ret; |
| |
| drm_plane_enable_fb_damage_clips(&dbidev->pipe.plane); |
| |
| drm->mode_config.funcs = &mipi_dbi_mode_config_funcs; |
| drm->mode_config.min_width = dbidev->mode.hdisplay; |
| drm->mode_config.max_width = dbidev->mode.hdisplay; |
| drm->mode_config.min_height = dbidev->mode.vdisplay; |
| drm->mode_config.max_height = dbidev->mode.vdisplay; |
| dbidev->rotation = rotation; |
| |
| DRM_DEBUG_KMS("rotation = %u\n", rotation); |
| |
| return 0; |
| } |
| EXPORT_SYMBOL(mipi_dbi_dev_init_with_formats); |
| |
| /** |
| * mipi_dbi_dev_init - MIPI DBI device initialization |
| * @dbidev: MIPI DBI device structure to initialize |
| * @funcs: Display pipe functions |
| * @mode: Display mode |
| * @rotation: Initial rotation in degrees Counter Clock Wise |
| * |
| * This function sets up a &drm_simple_display_pipe with a &drm_connector that |
| * has one fixed &drm_display_mode which is rotated according to @rotation. |
| * This mode is used to set the mode config min/max width/height properties. |
| * Additionally &mipi_dbi.tx_buf is allocated. |
| * |
| * Supported formats: Native RGB565 and emulated XRGB8888. |
| * |
| * Returns: |
| * Zero on success, negative error code on failure. |
| */ |
| int mipi_dbi_dev_init(struct mipi_dbi_dev *dbidev, |
| const struct drm_simple_display_pipe_funcs *funcs, |
| const struct drm_display_mode *mode, unsigned int rotation) |
| { |
| size_t bufsize = mode->vdisplay * mode->hdisplay * sizeof(u16); |
| |
| dbidev->drm.mode_config.preferred_depth = 16; |
| |
| return mipi_dbi_dev_init_with_formats(dbidev, funcs, mipi_dbi_formats, |
| ARRAY_SIZE(mipi_dbi_formats), mode, |
| rotation, bufsize); |
| } |
| EXPORT_SYMBOL(mipi_dbi_dev_init); |
| |
| /** |
| * mipi_dbi_release - DRM driver release helper |
| * @drm: DRM device |
| * |
| * This function finalizes and frees &mipi_dbi. |
| * |
| * Drivers can use this as their &drm_driver->release callback. |
| */ |
| void mipi_dbi_release(struct drm_device *drm) |
| { |
| struct mipi_dbi_dev *dbidev = drm_to_mipi_dbi_dev(drm); |
| |
| DRM_DEBUG_DRIVER("\n"); |
| |
| drm_mode_config_cleanup(drm); |
| drm_dev_fini(drm); |
| kfree(dbidev); |
| } |
| EXPORT_SYMBOL(mipi_dbi_release); |
| |
| /** |
| * mipi_dbi_hw_reset - Hardware reset of controller |
| * @dbi: MIPI DBI structure |
| * |
| * Reset controller if the &mipi_dbi->reset gpio is set. |
| */ |
| void mipi_dbi_hw_reset(struct mipi_dbi *dbi) |
| { |
| if (!dbi->reset) |
| return; |
| |
| gpiod_set_value_cansleep(dbi->reset, 0); |
| usleep_range(20, 1000); |
| gpiod_set_value_cansleep(dbi->reset, 1); |
| msleep(120); |
| } |
| EXPORT_SYMBOL(mipi_dbi_hw_reset); |
| |
| /** |
| * mipi_dbi_display_is_on - Check if display is on |
| * @dbi: MIPI DBI structure |
| * |
| * This function checks the Power Mode register (if readable) to see if |
| * display output is turned on. This can be used to see if the bootloader |
| * has already turned on the display avoiding flicker when the pipeline is |
| * enabled. |
| * |
| * Returns: |
| * true if the display can be verified to be on, false otherwise. |
| */ |
| bool mipi_dbi_display_is_on(struct mipi_dbi *dbi) |
| { |
| u8 val; |
| |
| if (mipi_dbi_command_read(dbi, MIPI_DCS_GET_POWER_MODE, &val)) |
| return false; |
| |
| val &= ~DCS_POWER_MODE_RESERVED_MASK; |
| |
| /* The poweron/reset value is 08h DCS_POWER_MODE_DISPLAY_NORMAL_MODE */ |
| if (val != (DCS_POWER_MODE_DISPLAY | |
| DCS_POWER_MODE_DISPLAY_NORMAL_MODE | DCS_POWER_MODE_SLEEP_MODE)) |
| return false; |
| |
| DRM_DEBUG_DRIVER("Display is ON\n"); |
| |
| return true; |
| } |
| EXPORT_SYMBOL(mipi_dbi_display_is_on); |
| |
| static int mipi_dbi_poweron_reset_conditional(struct mipi_dbi_dev *dbidev, bool cond) |
| { |
| struct device *dev = dbidev->drm.dev; |
| struct mipi_dbi *dbi = &dbidev->dbi; |
| int ret; |
| |
| if (dbidev->regulator) { |
| ret = regulator_enable(dbidev->regulator); |
| if (ret) { |
| DRM_DEV_ERROR(dev, "Failed to enable regulator (%d)\n", ret); |
| return ret; |
| } |
| } |
| |
| if (cond && mipi_dbi_display_is_on(dbi)) |
| return 1; |
| |
| mipi_dbi_hw_reset(dbi); |
| ret = mipi_dbi_command(dbi, MIPI_DCS_SOFT_RESET); |
| if (ret) { |
| DRM_DEV_ERROR(dev, "Failed to send reset command (%d)\n", ret); |
| if (dbidev->regulator) |
| regulator_disable(dbidev->regulator); |
| return ret; |
| } |
| |
| /* |
| * If we did a hw reset, we know the controller is in Sleep mode and |
| * per MIPI DSC spec should wait 5ms after soft reset. If we didn't, |
| * we assume worst case and wait 120ms. |
| */ |
| if (dbi->reset) |
| usleep_range(5000, 20000); |
| else |
| msleep(120); |
| |
| return 0; |
| } |
| |
| /** |
| * mipi_dbi_poweron_reset - MIPI DBI poweron and reset |
| * @dbidev: MIPI DBI device structure |
| * |
| * This function enables the regulator if used and does a hardware and software |
| * reset. |
| * |
| * Returns: |
| * Zero on success, or a negative error code. |
| */ |
| int mipi_dbi_poweron_reset(struct mipi_dbi_dev *dbidev) |
| { |
| return mipi_dbi_poweron_reset_conditional(dbidev, false); |
| } |
| EXPORT_SYMBOL(mipi_dbi_poweron_reset); |
| |
| /** |
| * mipi_dbi_poweron_conditional_reset - MIPI DBI poweron and conditional reset |
| * @dbidev: MIPI DBI device structure |
| * |
| * This function enables the regulator if used and if the display is off, it |
| * does a hardware and software reset. If mipi_dbi_display_is_on() determines |
| * that the display is on, no reset is performed. |
| * |
| * Returns: |
| * Zero if the controller was reset, 1 if the display was already on, or a |
| * negative error code. |
| */ |
| int mipi_dbi_poweron_conditional_reset(struct mipi_dbi_dev *dbidev) |
| { |
| return mipi_dbi_poweron_reset_conditional(dbidev, true); |
| } |
| EXPORT_SYMBOL(mipi_dbi_poweron_conditional_reset); |
| |
| #if IS_ENABLED(CONFIG_SPI) |
| |
| /** |
| * mipi_dbi_spi_cmd_max_speed - get the maximum SPI bus speed |
| * @spi: SPI device |
| * @len: The transfer buffer length. |
| * |
| * Many controllers have a max speed of 10MHz, but can be pushed way beyond |
| * that. Increase reliability by running pixel data at max speed and the rest |
| * at 10MHz, preventing transfer glitches from messing up the init settings. |
| */ |
| u32 mipi_dbi_spi_cmd_max_speed(struct spi_device *spi, size_t len) |
| { |
| if (len > 64) |
| return 0; /* use default */ |
| |
| return min_t(u32, 10000000, spi->max_speed_hz); |
| } |
| EXPORT_SYMBOL(mipi_dbi_spi_cmd_max_speed); |
| |
| static bool mipi_dbi_machine_little_endian(void) |
| { |
| #if defined(__LITTLE_ENDIAN) |
| return true; |
| #else |
| return false; |
| #endif |
| } |
| |
| /* |
| * MIPI DBI Type C Option 1 |
| * |
| * If the SPI controller doesn't have 9 bits per word support, |
| * use blocks of 9 bytes to send 8x 9-bit words using a 8-bit SPI transfer. |
| * Pad partial blocks with MIPI_DCS_NOP (zero). |
| * This is how the D/C bit (x) is added: |
| * x7654321 |
| * 0x765432 |
| * 10x76543 |
| * 210x7654 |
| * 3210x765 |
| * 43210x76 |
| * 543210x7 |
| * 6543210x |
| * 76543210 |
| */ |
| |
| static int mipi_dbi_spi1e_transfer(struct mipi_dbi *dbi, int dc, |
| const void *buf, size_t len, |
| unsigned int bpw) |
| { |
| bool swap_bytes = (bpw == 16 && mipi_dbi_machine_little_endian()); |
| size_t chunk, max_chunk = dbi->tx_buf9_len; |
| struct spi_device *spi = dbi->spi; |
| struct spi_transfer tr = { |
| .tx_buf = dbi->tx_buf9, |
| .bits_per_word = 8, |
| }; |
| struct spi_message m; |
| const u8 *src = buf; |
| int i, ret; |
| u8 *dst; |
| |
| if (drm_debug & DRM_UT_DRIVER) |
| pr_debug("[drm:%s] dc=%d, max_chunk=%zu, transfers:\n", |
| __func__, dc, max_chunk); |
| |
| tr.speed_hz = mipi_dbi_spi_cmd_max_speed(spi, len); |
| spi_message_init_with_transfers(&m, &tr, 1); |
| |
| if (!dc) { |
| if (WARN_ON_ONCE(len != 1)) |
| return -EINVAL; |
| |
| /* Command: pad no-op's (zeroes) at beginning of block */ |
| dst = dbi->tx_buf9; |
| memset(dst, 0, 9); |
| dst[8] = *src; |
| tr.len = 9; |
| |
| return spi_sync(spi, &m); |
| } |
| |
| /* max with room for adding one bit per byte */ |
| max_chunk = max_chunk / 9 * 8; |
| /* but no bigger than len */ |
| max_chunk = min(max_chunk, len); |
| /* 8 byte blocks */ |
| max_chunk = max_t(size_t, 8, max_chunk & ~0x7); |
| |
| while (len) { |
| size_t added = 0; |
| |
| chunk = min(len, max_chunk); |
| len -= chunk; |
| dst = dbi->tx_buf9; |
| |
| if (chunk < 8) { |
| u8 val, carry = 0; |
| |
| /* Data: pad no-op's (zeroes) at end of block */ |
| memset(dst, 0, 9); |
| |
| if (swap_bytes) { |
| for (i = 1; i < (chunk + 1); i++) { |
| val = src[1]; |
| *dst++ = carry | BIT(8 - i) | (val >> i); |
| carry = val << (8 - i); |
| i++; |
| val = src[0]; |
| *dst++ = carry | BIT(8 - i) | (val >> i); |
| carry = val << (8 - i); |
| src += 2; |
| } |
| *dst++ = carry; |
| } else { |
| for (i = 1; i < (chunk + 1); i++) { |
| val = *src++; |
| *dst++ = carry | BIT(8 - i) | (val >> i); |
| carry = val << (8 - i); |
| } |
| *dst++ = carry; |
| } |
| |
| chunk = 8; |
| added = 1; |
| } else { |
| for (i = 0; i < chunk; i += 8) { |
| if (swap_bytes) { |
| *dst++ = BIT(7) | (src[1] >> 1); |
| *dst++ = (src[1] << 7) | BIT(6) | (src[0] >> 2); |
| *dst++ = (src[0] << 6) | BIT(5) | (src[3] >> 3); |
| *dst++ = (src[3] << 5) | BIT(4) | (src[2] >> 4); |
| *dst++ = (src[2] << 4) | BIT(3) | (src[5] >> 5); |
| *dst++ = (src[5] << 3) | BIT(2) | (src[4] >> 6); |
| *dst++ = (src[4] << 2) | BIT(1) | (src[7] >> 7); |
| *dst++ = (src[7] << 1) | BIT(0); |
| *dst++ = src[6]; |
| } else { |
| *dst++ = BIT(7) | (src[0] >> 1); |
| *dst++ = (src[0] << 7) | BIT(6) | (src[1] >> 2); |
| *dst++ = (src[1] << 6) | BIT(5) | (src[2] >> 3); |
| *dst++ = (src[2] << 5) | BIT(4) | (src[3] >> 4); |
| *dst++ = (src[3] << 4) | BIT(3) | (src[4] >> 5); |
| *dst++ = (src[4] << 3) | BIT(2) | (src[5] >> 6); |
| *dst++ = (src[5] << 2) | BIT(1) | (src[6] >> 7); |
| *dst++ = (src[6] << 1) | BIT(0); |
| *dst++ = src[7]; |
| } |
| |
| src += 8; |
| added++; |
| } |
| } |
| |
| tr.len = chunk + added; |
| |
| ret = spi_sync(spi, &m); |
| if (ret) |
| return ret; |
| } |
| |
| return 0; |
| } |
| |
| static int mipi_dbi_spi1_transfer(struct mipi_dbi *dbi, int dc, |
| const void *buf, size_t len, |
| unsigned int bpw) |
| { |
| struct spi_device *spi = dbi->spi; |
| struct spi_transfer tr = { |
| .bits_per_word = 9, |
| }; |
| const u16 *src16 = buf; |
| const u8 *src8 = buf; |
| struct spi_message m; |
| size_t max_chunk; |
| u16 *dst16; |
| int ret; |
| |
| if (!spi_is_bpw_supported(spi, 9)) |
| return mipi_dbi_spi1e_transfer(dbi, dc, buf, len, bpw); |
| |
| tr.speed_hz = mipi_dbi_spi_cmd_max_speed(spi, len); |
| max_chunk = dbi->tx_buf9_len; |
| dst16 = dbi->tx_buf9; |
| |
| if (drm_debug & DRM_UT_DRIVER) |
| pr_debug("[drm:%s] dc=%d, max_chunk=%zu, transfers:\n", |
| __func__, dc, max_chunk); |
| |
| max_chunk = min(max_chunk / 2, len); |
| |
| spi_message_init_with_transfers(&m, &tr, 1); |
| tr.tx_buf = dst16; |
| |
| while (len) { |
| size_t chunk = min(len, max_chunk); |
| unsigned int i; |
| |
| if (bpw == 16 && mipi_dbi_machine_little_endian()) { |
| for (i = 0; i < (chunk * 2); i += 2) { |
| dst16[i] = *src16 >> 8; |
| dst16[i + 1] = *src16++ & 0xFF; |
| if (dc) { |
| dst16[i] |= 0x0100; |
| dst16[i + 1] |= 0x0100; |
| } |
| } |
| } else { |
| for (i = 0; i < chunk; i++) { |
| dst16[i] = *src8++; |
| if (dc) |
| dst16[i] |= 0x0100; |
| } |
| } |
| |
| tr.len = chunk * 2; |
| len -= chunk; |
| |
| ret = spi_sync(spi, &m); |
| if (ret) |
| return ret; |
| } |
| |
| return 0; |
| } |
| |
| static int mipi_dbi_typec1_command(struct mipi_dbi *dbi, u8 *cmd, |
| u8 *parameters, size_t num) |
| { |
| unsigned int bpw = (*cmd == MIPI_DCS_WRITE_MEMORY_START) ? 16 : 8; |
| int ret; |
| |
| if (mipi_dbi_command_is_read(dbi, *cmd)) |
| return -EOPNOTSUPP; |
| |
| MIPI_DBI_DEBUG_COMMAND(*cmd, parameters, num); |
| |
| ret = mipi_dbi_spi1_transfer(dbi, 0, cmd, 1, 8); |
| if (ret || !num) |
| return ret; |
| |
| return mipi_dbi_spi1_transfer(dbi, 1, parameters, num, bpw); |
| } |
| |
| /* MIPI DBI Type C Option 3 */ |
| |
| static int mipi_dbi_typec3_command_read(struct mipi_dbi *dbi, u8 *cmd, |
| u8 *data, size_t len) |
| { |
| struct spi_device *spi = dbi->spi; |
| u32 speed_hz = min_t(u32, MIPI_DBI_MAX_SPI_READ_SPEED, |
| spi->max_speed_hz / 2); |
| struct spi_transfer tr[2] = { |
| { |
| .speed_hz = speed_hz, |
| .tx_buf = cmd, |
| .len = 1, |
| }, { |
| .speed_hz = speed_hz, |
| .len = len, |
| }, |
| }; |
| struct spi_message m; |
| u8 *buf; |
| int ret; |
| |
| if (!len) |
| return -EINVAL; |
| |
| /* |
| * Support non-standard 24-bit and 32-bit Nokia read commands which |
| * start with a dummy clock, so we need to read an extra byte. |
| */ |
| if (*cmd == MIPI_DCS_GET_DISPLAY_ID || |
| *cmd == MIPI_DCS_GET_DISPLAY_STATUS) { |
| if (!(len == 3 || len == 4)) |
| return -EINVAL; |
| |
| tr[1].len = len + 1; |
| } |
| |
| buf = kmalloc(tr[1].len, GFP_KERNEL); |
| if (!buf) |
| return -ENOMEM; |
| |
| tr[1].rx_buf = buf; |
| gpiod_set_value_cansleep(dbi->dc, 0); |
| |
| spi_message_init_with_transfers(&m, tr, ARRAY_SIZE(tr)); |
| ret = spi_sync(spi, &m); |
| if (ret) |
| goto err_free; |
| |
| if (tr[1].len == len) { |
| memcpy(data, buf, len); |
| } else { |
| unsigned int i; |
| |
| for (i = 0; i < len; i++) |
| data[i] = (buf[i] << 1) | !!(buf[i + 1] & BIT(7)); |
| } |
| |
| MIPI_DBI_DEBUG_COMMAND(*cmd, data, len); |
| |
| err_free: |
| kfree(buf); |
| |
| return ret; |
| } |
| |
| static int mipi_dbi_typec3_command(struct mipi_dbi *dbi, u8 *cmd, |
| u8 *par, size_t num) |
| { |
| struct spi_device *spi = dbi->spi; |
| unsigned int bpw = 8; |
| u32 speed_hz; |
| int ret; |
| |
| if (mipi_dbi_command_is_read(dbi, *cmd)) |
| return mipi_dbi_typec3_command_read(dbi, cmd, par, num); |
| |
| MIPI_DBI_DEBUG_COMMAND(*cmd, par, num); |
| |
| gpiod_set_value_cansleep(dbi->dc, 0); |
| speed_hz = mipi_dbi_spi_cmd_max_speed(spi, 1); |
| ret = mipi_dbi_spi_transfer(spi, speed_hz, 8, cmd, 1); |
| if (ret || !num) |
| return ret; |
| |
| if (*cmd == MIPI_DCS_WRITE_MEMORY_START && !dbi->swap_bytes) |
| bpw = 16; |
| |
| gpiod_set_value_cansleep(dbi->dc, 1); |
| speed_hz = mipi_dbi_spi_cmd_max_speed(spi, num); |
| |
| return mipi_dbi_spi_transfer(spi, speed_hz, bpw, par, num); |
| } |
| |
| /** |
| * mipi_dbi_spi_init - Initialize MIPI DBI SPI interface |
| * @spi: SPI device |
| * @dbi: MIPI DBI structure to initialize |
| * @dc: D/C gpio (optional) |
| * |
| * This function sets &mipi_dbi->command, enables &mipi_dbi->read_commands for the |
| * usual read commands. It should be followed by a call to mipi_dbi_dev_init() or |
| * a driver-specific init. |
| * |
| * If @dc is set, a Type C Option 3 interface is assumed, if not |
| * Type C Option 1. |
| * |
| * If the SPI master driver doesn't support the necessary bits per word, |
| * the following transformation is used: |
| * |
| * - 9-bit: reorder buffer as 9x 8-bit words, padded with no-op command. |
| * - 16-bit: if big endian send as 8-bit, if little endian swap bytes |
| * |
| * Returns: |
| * Zero on success, negative error code on failure. |
| */ |
| int mipi_dbi_spi_init(struct spi_device *spi, struct mipi_dbi *dbi, |
| struct gpio_desc *dc) |
| { |
| struct device *dev = &spi->dev; |
| int ret; |
| |
| /* |
| * Even though it's not the SPI device that does DMA (the master does), |
| * the dma mask is necessary for the dma_alloc_wc() in |
| * drm_gem_cma_create(). The dma_addr returned will be a physical |
| * address which might be different from the bus address, but this is |
| * not a problem since the address will not be used. |
| * The virtual address is used in the transfer and the SPI core |
| * re-maps it on the SPI master device using the DMA streaming API |
| * (spi_map_buf()). |
| */ |
| if (!dev->coherent_dma_mask) { |
| ret = dma_coerce_mask_and_coherent(dev, DMA_BIT_MASK(32)); |
| if (ret) { |
| dev_warn(dev, "Failed to set dma mask %d\n", ret); |
| return ret; |
| } |
| } |
| |
| dbi->spi = spi; |
| dbi->read_commands = mipi_dbi_dcs_read_commands; |
| |
| if (dc) { |
| dbi->command = mipi_dbi_typec3_command; |
| dbi->dc = dc; |
| if (mipi_dbi_machine_little_endian() && !spi_is_bpw_supported(spi, 16)) |
| dbi->swap_bytes = true; |
| } else { |
| dbi->command = mipi_dbi_typec1_command; |
| dbi->tx_buf9_len = SZ_16K; |
| dbi->tx_buf9 = devm_kmalloc(dev, dbi->tx_buf9_len, GFP_KERNEL); |
| if (!dbi->tx_buf9) |
| return -ENOMEM; |
| } |
| |
| mutex_init(&dbi->cmdlock); |
| |
| DRM_DEBUG_DRIVER("SPI speed: %uMHz\n", spi->max_speed_hz / 1000000); |
| |
| return 0; |
| } |
| EXPORT_SYMBOL(mipi_dbi_spi_init); |
| |
| /** |
| * mipi_dbi_spi_transfer - SPI transfer helper |
| * @spi: SPI device |
| * @speed_hz: Override speed (optional) |
| * @bpw: Bits per word |
| * @buf: Buffer to transfer |
| * @len: Buffer length |
| * |
| * This SPI transfer helper breaks up the transfer of @buf into chunks which |
| * the SPI controller driver can handle. |
| * |
| * Returns: |
| * Zero on success, negative error code on failure. |
| */ |
| int mipi_dbi_spi_transfer(struct spi_device *spi, u32 speed_hz, |
| u8 bpw, const void *buf, size_t len) |
| { |
| size_t max_chunk = spi_max_transfer_size(spi); |
| struct spi_transfer tr = { |
| .bits_per_word = bpw, |
| .speed_hz = speed_hz, |
| }; |
| struct spi_message m; |
| size_t chunk; |
| int ret; |
| |
| spi_message_init_with_transfers(&m, &tr, 1); |
| |
| while (len) { |
| chunk = min(len, max_chunk); |
| |
| tr.tx_buf = buf; |
| tr.len = chunk; |
| buf += chunk; |
| len -= chunk; |
| |
| ret = spi_sync(spi, &m); |
| if (ret) |
| return ret; |
| } |
| |
| return 0; |
| } |
| EXPORT_SYMBOL(mipi_dbi_spi_transfer); |
| |
| #endif /* CONFIG_SPI */ |
| |
| #ifdef CONFIG_DEBUG_FS |
| |
| static ssize_t mipi_dbi_debugfs_command_write(struct file *file, |
| const char __user *ubuf, |
| size_t count, loff_t *ppos) |
| { |
| struct seq_file *m = file->private_data; |
| struct mipi_dbi_dev *dbidev = m->private; |
| u8 val, cmd = 0, parameters[64]; |
| char *buf, *pos, *token; |
| int i, ret, idx; |
| |
| if (!drm_dev_enter(&dbidev->drm, &idx)) |
| return -ENODEV; |
| |
| buf = memdup_user_nul(ubuf, count); |
| if (IS_ERR(buf)) { |
| ret = PTR_ERR(buf); |
| goto err_exit; |
| } |
| |
| /* strip trailing whitespace */ |
| for (i = count - 1; i > 0; i--) |
| if (isspace(buf[i])) |
| buf[i] = '\0'; |
| else |
| break; |
| i = 0; |
| pos = buf; |
| while (pos) { |
| token = strsep(&pos, " "); |
| if (!token) { |
| ret = -EINVAL; |
| goto err_free; |
| } |
| |
| ret = kstrtou8(token, 16, &val); |
| if (ret < 0) |
| goto err_free; |
| |
| if (token == buf) |
| cmd = val; |
| else |
| parameters[i++] = val; |
| |
| if (i == 64) { |
| ret = -E2BIG; |
| goto err_free; |
| } |
| } |
| |
| ret = mipi_dbi_command_buf(&dbidev->dbi, cmd, parameters, i); |
| |
| err_free: |
| kfree(buf); |
| err_exit: |
| drm_dev_exit(idx); |
| |
| return ret < 0 ? ret : count; |
| } |
| |
| static int mipi_dbi_debugfs_command_show(struct seq_file *m, void *unused) |
| { |
| struct mipi_dbi_dev *dbidev = m->private; |
| struct mipi_dbi *dbi = &dbidev->dbi; |
| u8 cmd, val[4]; |
| int ret, idx; |
| size_t len; |
| |
| if (!drm_dev_enter(&dbidev->drm, &idx)) |
| return -ENODEV; |
| |
| for (cmd = 0; cmd < 255; cmd++) { |
| if (!mipi_dbi_command_is_read(dbi, cmd)) |
| continue; |
| |
| switch (cmd) { |
| case MIPI_DCS_READ_MEMORY_START: |
| case MIPI_DCS_READ_MEMORY_CONTINUE: |
| len = 2; |
| break; |
| case MIPI_DCS_GET_DISPLAY_ID: |
| len = 3; |
| break; |
| case MIPI_DCS_GET_DISPLAY_STATUS: |
| len = 4; |
| break; |
| default: |
| len = 1; |
| break; |
| } |
| |
| seq_printf(m, "%02x: ", cmd); |
| ret = mipi_dbi_command_buf(dbi, cmd, val, len); |
| if (ret) { |
| seq_puts(m, "XX\n"); |
| continue; |
| } |
| seq_printf(m, "%*phN\n", (int)len, val); |
| } |
| |
| drm_dev_exit(idx); |
| |
| return 0; |
| } |
| |
| static int mipi_dbi_debugfs_command_open(struct inode *inode, |
| struct file *file) |
| { |
| return single_open(file, mipi_dbi_debugfs_command_show, |
| inode->i_private); |
| } |
| |
| static const struct file_operations mipi_dbi_debugfs_command_fops = { |
| .owner = THIS_MODULE, |
| .open = mipi_dbi_debugfs_command_open, |
| .read = seq_read, |
| .llseek = seq_lseek, |
| .release = single_release, |
| .write = mipi_dbi_debugfs_command_write, |
| }; |
| |
| /** |
| * mipi_dbi_debugfs_init - Create debugfs entries |
| * @minor: DRM minor |
| * |
| * This function creates a 'command' debugfs file for sending commands to the |
| * controller or getting the read command values. |
| * Drivers can use this as their &drm_driver->debugfs_init callback. |
| * |
| * Returns: |
| * Zero on success, negative error code on failure. |
| */ |
| int mipi_dbi_debugfs_init(struct drm_minor *minor) |
| { |
| struct mipi_dbi_dev *dbidev = drm_to_mipi_dbi_dev(minor->dev); |
| umode_t mode = S_IFREG | S_IWUSR; |
| |
| if (dbidev->dbi.read_commands) |
| mode |= S_IRUGO; |
| debugfs_create_file("command", mode, minor->debugfs_root, dbidev, |
| &mipi_dbi_debugfs_command_fops); |
| |
| return 0; |
| } |
| EXPORT_SYMBOL(mipi_dbi_debugfs_init); |
| |
| #endif |
| |
| MODULE_LICENSE("GPL"); |