diff options
Diffstat (limited to 'drivers/video/fbdev/mxc/mxsfb_sii902x_audio.c')
-rw-r--r-- | drivers/video/fbdev/mxc/mxsfb_sii902x_audio.c | 361 |
1 files changed, 361 insertions, 0 deletions
diff --git a/drivers/video/fbdev/mxc/mxsfb_sii902x_audio.c b/drivers/video/fbdev/mxc/mxsfb_sii902x_audio.c new file mode 100644 index 000000000000..a4f1d58d8c43 --- /dev/null +++ b/drivers/video/fbdev/mxc/mxsfb_sii902x_audio.c @@ -0,0 +1,361 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright 2019 NXP + +#include <linux/clk.h> +#include <linux/kthread.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/component.h> +#include <linux/mfd/syscon.h> +#include <linux/of.h> +#include <linux/irq.h> +#include <linux/of_device.h> +#include <sound/hdmi-codec.h> +#include <drm/drm_edid.h> + +#include "mxsfb_sii902x.h" + +static const unsigned int audio_rates[] = { + 32000, + 44100, + 48000, + 88200, + 96000, + 176400, + 192000, +}; + +/* + * HDMI audio codec callbacks + */ +static int sii902x_audio_hw_params(struct device *dev, void *data, + struct hdmi_codec_daifmt *daifmt, + struct hdmi_codec_params *params) +{ + struct sii902x_data *sii902x = dev_get_drvdata(dev); + unsigned char reg; + + /* sii90sx hdmi audio setup */ + i2c_smbus_write_byte_data(sii902x->client, 0x26, 0x90); + i2c_smbus_write_byte_data(sii902x->client, 0x20, 0x2d); + i2c_smbus_write_byte_data(sii902x->client, 0x1f, 0x88); + i2c_smbus_write_byte_data(sii902x->client, 0x1f, 0x91); + i2c_smbus_write_byte_data(sii902x->client, 0x1f, 0xa2); + i2c_smbus_write_byte_data(sii902x->client, 0x1f, 0xb3); + i2c_smbus_write_byte_data(sii902x->client, 0x27, 0); + + switch (params->sample_rate) { + case 44100: + reg = 0; + break; + case 48000: + reg = 0x2; + break; + case 32000: + reg = 0x3; + break; + case 88200: + reg = 0x8; + break; + case 96000: + reg = 0xa; + break; + case 176400: + reg = 0xc; + break; + case 192000: + reg = 0xe; + break; + default: + reg = 0x1; + break; + } + + i2c_smbus_write_byte_data(sii902x->client, 0x24, reg); + i2c_smbus_write_byte_data(sii902x->client, 0x25, 0x0b); + i2c_smbus_write_byte_data(sii902x->client, 0x26, 0x80); + + return 0; +} + +static void sii902x_audio_shutdown(struct device *dev, void *data) +{ + struct sii902x_data *sii902x = dev_get_drvdata(dev); + + i2c_smbus_write_byte_data(sii902x->client, 0x26, 0x10); +} + + +/* Most of these function is copy from the drivers/gpu/drm/drm_edid.c */ +typedef void detailed_cb(struct detailed_timing *timing, void *closure); +static void +cea_for_detailed_block(u8 *ext, detailed_cb *cb, void *closure) +{ + int i, n = 0; + u8 d = ext[0x02]; + u8 *det_base = ext + d; + + n = (127 - d) / 18; + for (i = 0; i < n; i++) + cb((struct detailed_timing *)(det_base + 18 * i), closure); +} + +static void +vtb_for_detailed_block(u8 *ext, detailed_cb *cb, void *closure) +{ + unsigned int i, n = min_t(int, ext[0x02], 6); + u8 *det_base = ext + 5; + + if (ext[0x01] != 1) + return; /* unknown version */ + + for (i = 0; i < n; i++) + cb((struct detailed_timing *)(det_base + 18 * i), closure); +} + +#define EDID_DETAILED_TIMINGS 4 +#define AUDIO_BLOCK 0x01 +#define VIDEO_BLOCK 0x02 +#define VENDOR_BLOCK 0x03 +#define SPEAKER_BLOCK 0x04 + +static void +eld_for_detailed_block(u8 *raw_edid, detailed_cb *cb, void *closure) +{ + int i; + struct edid *edid = (struct edid *)raw_edid; + + if (edid == NULL) + return; + + for (i = 0; i < EDID_DETAILED_TIMINGS; i++) + cb(&(edid->detailed_timings[i]), closure); + + for (i = 1; i <= raw_edid[0x7e]; i++) { + u8 *ext = raw_edid + (i * EDID_LENGTH); + + switch (*ext) { + case CEA_EXT: + cea_for_detailed_block(ext, cb, closure); + break; + case VTB_EXT: + vtb_for_detailed_block(ext, cb, closure); + break; + default: + break; + } + } +} + +static void +monitor_name(struct detailed_timing *t, void *data) +{ + if (t->data.other_data.type == EDID_DETAIL_MONITOR_NAME) + *(u8 **)data = t->data.other_data.data.str.str; +} + +static int get_monitor_name(struct edid *edid, char name[13]) +{ + char *edid_name = NULL; + int mnl; + + if (!edid || !name) + return 0; + + eld_for_detailed_block((u8 *)edid, monitor_name, &edid_name); + for (mnl = 0; edid_name && mnl < 13; mnl++) { + if (edid_name[mnl] == 0x0a) + break; + + name[mnl] = edid_name[mnl]; + } + + return mnl; +} + +static int +cea_revision(const u8 *cea) +{ + return cea[1]; +} + +static int +cea_db_offsets(const u8 *cea, int *start, int *end) +{ + /* Data block offset in CEA extension block */ + *start = 4; + *end = cea[2]; + if (*end == 0) + *end = 127; + if (*end < 4 || *end > 127) + return -ERANGE; + return 0; +} + +static int +cea_db_payload_len(const u8 *db) +{ + return db[0] & 0x1f; +} + +static int +cea_db_tag(const u8 *db) +{ + return db[0] >> 5; +} + +#define HDMI_IEEE_OUI 0x000c03 +static bool cea_db_is_hdmi_vsdb(const u8 *db) +{ + int hdmi_id; + + if (cea_db_tag(db) != VENDOR_BLOCK) + return false; + + if (cea_db_payload_len(db) < 5) + return false; + + hdmi_id = db[1] | (db[2] << 8) | (db[3] << 16); + + return hdmi_id == HDMI_IEEE_OUI; +} + +#define for_each_cea_db(cea, i, start, end) \ + for ((i) = (start); (i) < (end) && (i) + cea_db_payload_len(&(cea)[(i)]) < (end); (i) += cea_db_payload_len(&(cea)[(i)]) + 1) + +static u8 *find_cea_extension(const struct edid *edid, int ext_id) +{ + u8 *edid_ext = NULL; + int i; + + /* No EDID or EDID extensions */ + if (edid == NULL || edid->extensions == 0) + return NULL; + + /* Find CEA extension */ + for (i = 0; i < edid->extensions; i++) { + edid_ext = (u8 *)edid + EDID_LENGTH * (i + 1); + if (edid_ext[0] == ext_id) + break; + } + + if (i == edid->extensions) + return NULL; + + return edid_ext; +} + +static void edid_to_eld(char *eld, struct edid *edid) +{ + u8 *cea; + u8 *db; + int total_sad_count = 0; + int mnl; + int dbl; + + if (!edid) + return; + + cea = find_cea_extension(edid, CEA_EXT); + if (!cea) + return; + + mnl = get_monitor_name(edid, &eld[DRM_ELD_MONITOR_NAME_STRING]); + + eld[DRM_ELD_CEA_EDID_VER_MNL] = cea[1] << DRM_ELD_CEA_EDID_VER_SHIFT; + eld[DRM_ELD_CEA_EDID_VER_MNL] |= mnl; + + eld[DRM_ELD_VER] = DRM_ELD_VER_CEA861D; + + eld[DRM_ELD_MANUFACTURER_NAME0] = edid->mfg_id[0]; + eld[DRM_ELD_MANUFACTURER_NAME1] = edid->mfg_id[1]; + eld[DRM_ELD_PRODUCT_CODE0] = edid->prod_code[0]; + eld[DRM_ELD_PRODUCT_CODE1] = edid->prod_code[1]; + + if (cea_revision(cea) >= 3) { + int i, start, end; + + if (cea_db_offsets(cea, &start, &end)) { + start = 0; + end = 0; + } + + for_each_cea_db(cea, i, start, end) { + db = &cea[i]; + dbl = cea_db_payload_len(db); + + switch (cea_db_tag(db)) { + int sad_count; + + case AUDIO_BLOCK: + /* Audio Data Block, contains SADs */ + sad_count = min(dbl / 3, 15 - total_sad_count); + if (sad_count >= 1) + memcpy(&eld[DRM_ELD_CEA_SAD(mnl, total_sad_count)], + &db[1], sad_count * 3); + total_sad_count += sad_count; + break; + case SPEAKER_BLOCK: + /* Speaker Allocation Data Block */ + if (dbl >= 1) + eld[DRM_ELD_SPEAKER] = db[1]; + break; + case VENDOR_BLOCK: + /* HDMI Vendor-Specific Data Block */ + if (cea_db_is_hdmi_vsdb(db)) { + u8 len = cea_db_payload_len(db); + + if (len >= 6 && (db[6] & (1 << 7))) + eld[DRM_ELD_SAD_COUNT_CONN_TYPE] |= DRM_ELD_SUPPORTS_AI; + + } + break; + default: + break; + } + } + } + + eld[DRM_ELD_SAD_COUNT_CONN_TYPE] |= total_sad_count << DRM_ELD_SAD_COUNT_SHIFT; + + eld[DRM_ELD_SAD_COUNT_CONN_TYPE] |= DRM_ELD_CONN_TYPE_HDMI; + + eld[DRM_ELD_BASELINE_ELD_LEN] = + DIV_ROUND_UP(drm_eld_calc_baseline_block_size(eld), 4); +} + +static int sii902x_audio_get_eld(struct device *dev, void *data, uint8_t *buf, size_t len) +{ + struct sii902x_data *sii902x = dev_get_drvdata(dev); + uint8_t eld[128]; + + memset(eld, 0, 128); + + edid_to_eld(eld, (struct edid *)sii902x->edid); + + memcpy(buf, eld, min(sizeof(eld), len)); + + return 0; +} + +static const struct hdmi_codec_ops sii902x_audio_codec_ops = { + .hw_params = sii902x_audio_hw_params, + .audio_shutdown = sii902x_audio_shutdown, + .get_eld = sii902x_audio_get_eld, +}; + +void sii902x_register_audio_driver(struct device *dev) +{ + struct hdmi_codec_pdata codec_data = { + .ops = &sii902x_audio_codec_ops, + .max_i2s_channels = 8, + .i2s = 1, + }; + struct platform_device *pdev; + + pdev = platform_device_register_data(dev, HDMI_CODEC_DRV_NAME, + 1, &codec_data, + sizeof(codec_data)); + if (IS_ERR(pdev)) + return; +} |