| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Apple Onboard Audio driver -- layout/machine id fabric |
| * |
| * Copyright 2006-2008 Johannes Berg <johannes@sipsolutions.net> |
| * |
| * This fabric module looks for sound codecs based on the |
| * layout-id or device-id property in the device tree. |
| */ |
| #include <linux/list.h> |
| #include <linux/module.h> |
| #include <linux/of.h> |
| #include <linux/platform_device.h> |
| #include <linux/slab.h> |
| #include "../aoa.h" |
| #include "../soundbus/soundbus.h" |
| |
| MODULE_AUTHOR("Johannes Berg <johannes@sipsolutions.net>"); |
| MODULE_LICENSE("GPL"); |
| MODULE_DESCRIPTION("Layout-ID fabric for snd-aoa"); |
| |
| #define MAX_CODECS_PER_BUS 2 |
| |
| /* These are the connections the layout fabric |
| * knows about. It doesn't really care about the |
| * input ones, but I thought I'd separate them |
| * to give them proper names. The thing is that |
| * Apple usually will distinguish the active output |
| * by GPIOs, while the active input is set directly |
| * on the codec. Hence we here tell the codec what |
| * we think is connected. This information is hard- |
| * coded below ... */ |
| #define CC_SPEAKERS (1<<0) |
| #define CC_HEADPHONE (1<<1) |
| #define CC_LINEOUT (1<<2) |
| #define CC_DIGITALOUT (1<<3) |
| #define CC_LINEIN (1<<4) |
| #define CC_MICROPHONE (1<<5) |
| #define CC_DIGITALIN (1<<6) |
| /* pretty bogus but users complain... |
| * This is a flag saying that the LINEOUT |
| * should be renamed to HEADPHONE. |
| * be careful with input detection! */ |
| #define CC_LINEOUT_LABELLED_HEADPHONE (1<<7) |
| |
| struct codec_connection { |
| /* CC_ flags from above */ |
| int connected; |
| /* codec dependent bit to be set in the aoa_codec.connected field. |
| * This intentionally doesn't have any generic flags because the |
| * fabric has to know the codec anyway and all codecs might have |
| * different connectors */ |
| int codec_bit; |
| }; |
| |
| struct codec_connect_info { |
| char *name; |
| struct codec_connection *connections; |
| }; |
| |
| #define LAYOUT_FLAG_COMBO_LINEOUT_SPDIF (1<<0) |
| |
| struct layout { |
| unsigned int layout_id, device_id; |
| struct codec_connect_info codecs[MAX_CODECS_PER_BUS]; |
| int flags; |
| |
| /* if busname is not assigned, we use 'Master' below, |
| * so that our layout table doesn't need to be filled |
| * too much. |
| * We only assign these two if we expect to find more |
| * than one soundbus, i.e. on those machines with |
| * multiple layout-ids */ |
| char *busname; |
| int pcmid; |
| }; |
| |
| MODULE_ALIAS("sound-layout-36"); |
| MODULE_ALIAS("sound-layout-41"); |
| MODULE_ALIAS("sound-layout-45"); |
| MODULE_ALIAS("sound-layout-47"); |
| MODULE_ALIAS("sound-layout-48"); |
| MODULE_ALIAS("sound-layout-49"); |
| MODULE_ALIAS("sound-layout-50"); |
| MODULE_ALIAS("sound-layout-51"); |
| MODULE_ALIAS("sound-layout-56"); |
| MODULE_ALIAS("sound-layout-57"); |
| MODULE_ALIAS("sound-layout-58"); |
| MODULE_ALIAS("sound-layout-60"); |
| MODULE_ALIAS("sound-layout-61"); |
| MODULE_ALIAS("sound-layout-62"); |
| MODULE_ALIAS("sound-layout-64"); |
| MODULE_ALIAS("sound-layout-65"); |
| MODULE_ALIAS("sound-layout-66"); |
| MODULE_ALIAS("sound-layout-67"); |
| MODULE_ALIAS("sound-layout-68"); |
| MODULE_ALIAS("sound-layout-69"); |
| MODULE_ALIAS("sound-layout-70"); |
| MODULE_ALIAS("sound-layout-72"); |
| MODULE_ALIAS("sound-layout-76"); |
| MODULE_ALIAS("sound-layout-80"); |
| MODULE_ALIAS("sound-layout-82"); |
| MODULE_ALIAS("sound-layout-84"); |
| MODULE_ALIAS("sound-layout-86"); |
| MODULE_ALIAS("sound-layout-90"); |
| MODULE_ALIAS("sound-layout-92"); |
| MODULE_ALIAS("sound-layout-94"); |
| MODULE_ALIAS("sound-layout-96"); |
| MODULE_ALIAS("sound-layout-98"); |
| MODULE_ALIAS("sound-layout-100"); |
| |
| MODULE_ALIAS("aoa-device-id-14"); |
| MODULE_ALIAS("aoa-device-id-22"); |
| MODULE_ALIAS("aoa-device-id-31"); |
| MODULE_ALIAS("aoa-device-id-35"); |
| MODULE_ALIAS("aoa-device-id-44"); |
| |
| /* onyx with all but microphone connected */ |
| static struct codec_connection onyx_connections_nomic[] = { |
| { |
| .connected = CC_SPEAKERS | CC_HEADPHONE | CC_LINEOUT, |
| .codec_bit = 0, |
| }, |
| { |
| .connected = CC_DIGITALOUT, |
| .codec_bit = 1, |
| }, |
| { |
| .connected = CC_LINEIN, |
| .codec_bit = 2, |
| }, |
| {} /* terminate array by .connected == 0 */ |
| }; |
| |
| /* onyx on machines without headphone */ |
| static struct codec_connection onyx_connections_noheadphones[] = { |
| { |
| .connected = CC_SPEAKERS | CC_LINEOUT | |
| CC_LINEOUT_LABELLED_HEADPHONE, |
| .codec_bit = 0, |
| }, |
| { |
| .connected = CC_DIGITALOUT, |
| .codec_bit = 1, |
| }, |
| /* FIXME: are these correct? probably not for all the machines |
| * below ... If not this will need separating. */ |
| { |
| .connected = CC_LINEIN, |
| .codec_bit = 2, |
| }, |
| { |
| .connected = CC_MICROPHONE, |
| .codec_bit = 3, |
| }, |
| {} /* terminate array by .connected == 0 */ |
| }; |
| |
| /* onyx on machines with real line-out */ |
| static struct codec_connection onyx_connections_reallineout[] = { |
| { |
| .connected = CC_SPEAKERS | CC_LINEOUT | CC_HEADPHONE, |
| .codec_bit = 0, |
| }, |
| { |
| .connected = CC_DIGITALOUT, |
| .codec_bit = 1, |
| }, |
| { |
| .connected = CC_LINEIN, |
| .codec_bit = 2, |
| }, |
| {} /* terminate array by .connected == 0 */ |
| }; |
| |
| /* tas on machines without line out */ |
| static struct codec_connection tas_connections_nolineout[] = { |
| { |
| .connected = CC_SPEAKERS | CC_HEADPHONE, |
| .codec_bit = 0, |
| }, |
| { |
| .connected = CC_LINEIN, |
| .codec_bit = 2, |
| }, |
| { |
| .connected = CC_MICROPHONE, |
| .codec_bit = 3, |
| }, |
| {} /* terminate array by .connected == 0 */ |
| }; |
| |
| /* tas on machines with neither line out nor line in */ |
| static struct codec_connection tas_connections_noline[] = { |
| { |
| .connected = CC_SPEAKERS | CC_HEADPHONE, |
| .codec_bit = 0, |
| }, |
| { |
| .connected = CC_MICROPHONE, |
| .codec_bit = 3, |
| }, |
| {} /* terminate array by .connected == 0 */ |
| }; |
| |
| /* tas on machines without microphone */ |
| static struct codec_connection tas_connections_nomic[] = { |
| { |
| .connected = CC_SPEAKERS | CC_HEADPHONE | CC_LINEOUT, |
| .codec_bit = 0, |
| }, |
| { |
| .connected = CC_LINEIN, |
| .codec_bit = 2, |
| }, |
| {} /* terminate array by .connected == 0 */ |
| }; |
| |
| /* tas on machines with everything connected */ |
| static struct codec_connection tas_connections_all[] = { |
| { |
| .connected = CC_SPEAKERS | CC_HEADPHONE | CC_LINEOUT, |
| .codec_bit = 0, |
| }, |
| { |
| .connected = CC_LINEIN, |
| .codec_bit = 2, |
| }, |
| { |
| .connected = CC_MICROPHONE, |
| .codec_bit = 3, |
| }, |
| {} /* terminate array by .connected == 0 */ |
| }; |
| |
| static struct codec_connection toonie_connections[] = { |
| { |
| .connected = CC_SPEAKERS | CC_HEADPHONE, |
| .codec_bit = 0, |
| }, |
| {} /* terminate array by .connected == 0 */ |
| }; |
| |
| static struct codec_connection topaz_input[] = { |
| { |
| .connected = CC_DIGITALIN, |
| .codec_bit = 0, |
| }, |
| {} /* terminate array by .connected == 0 */ |
| }; |
| |
| static struct codec_connection topaz_output[] = { |
| { |
| .connected = CC_DIGITALOUT, |
| .codec_bit = 1, |
| }, |
| {} /* terminate array by .connected == 0 */ |
| }; |
| |
| static struct codec_connection topaz_inout[] = { |
| { |
| .connected = CC_DIGITALIN, |
| .codec_bit = 0, |
| }, |
| { |
| .connected = CC_DIGITALOUT, |
| .codec_bit = 1, |
| }, |
| {} /* terminate array by .connected == 0 */ |
| }; |
| |
| static struct layout layouts[] = { |
| /* last PowerBooks (15" Oct 2005) */ |
| { .layout_id = 82, |
| .flags = LAYOUT_FLAG_COMBO_LINEOUT_SPDIF, |
| .codecs[0] = { |
| .name = "onyx", |
| .connections = onyx_connections_noheadphones, |
| }, |
| .codecs[1] = { |
| .name = "topaz", |
| .connections = topaz_input, |
| }, |
| }, |
| /* PowerMac9,1 */ |
| { .layout_id = 60, |
| .codecs[0] = { |
| .name = "onyx", |
| .connections = onyx_connections_reallineout, |
| }, |
| }, |
| /* PowerMac9,1 */ |
| { .layout_id = 61, |
| .codecs[0] = { |
| .name = "topaz", |
| .connections = topaz_input, |
| }, |
| }, |
| /* PowerBook5,7 */ |
| { .layout_id = 64, |
| .flags = LAYOUT_FLAG_COMBO_LINEOUT_SPDIF, |
| .codecs[0] = { |
| .name = "onyx", |
| .connections = onyx_connections_noheadphones, |
| }, |
| }, |
| /* PowerBook5,7 */ |
| { .layout_id = 65, |
| .codecs[0] = { |
| .name = "topaz", |
| .connections = topaz_input, |
| }, |
| }, |
| /* PowerBook5,9 [17" Oct 2005] */ |
| { .layout_id = 84, |
| .flags = LAYOUT_FLAG_COMBO_LINEOUT_SPDIF, |
| .codecs[0] = { |
| .name = "onyx", |
| .connections = onyx_connections_noheadphones, |
| }, |
| .codecs[1] = { |
| .name = "topaz", |
| .connections = topaz_input, |
| }, |
| }, |
| /* PowerMac8,1 */ |
| { .layout_id = 45, |
| .codecs[0] = { |
| .name = "onyx", |
| .connections = onyx_connections_noheadphones, |
| }, |
| .codecs[1] = { |
| .name = "topaz", |
| .connections = topaz_input, |
| }, |
| }, |
| /* Quad PowerMac (analog in, analog/digital out) */ |
| { .layout_id = 68, |
| .codecs[0] = { |
| .name = "onyx", |
| .connections = onyx_connections_nomic, |
| }, |
| }, |
| /* Quad PowerMac (digital in) */ |
| { .layout_id = 69, |
| .codecs[0] = { |
| .name = "topaz", |
| .connections = topaz_input, |
| }, |
| .busname = "digital in", .pcmid = 1 }, |
| /* Early 2005 PowerBook (PowerBook 5,6) */ |
| { .layout_id = 70, |
| .codecs[0] = { |
| .name = "tas", |
| .connections = tas_connections_nolineout, |
| }, |
| }, |
| /* PowerBook 5,4 */ |
| { .layout_id = 51, |
| .codecs[0] = { |
| .name = "tas", |
| .connections = tas_connections_nolineout, |
| }, |
| }, |
| /* PowerBook6,1 */ |
| { .device_id = 31, |
| .codecs[0] = { |
| .name = "tas", |
| .connections = tas_connections_nolineout, |
| }, |
| }, |
| /* PowerBook6,5 */ |
| { .device_id = 44, |
| .codecs[0] = { |
| .name = "tas", |
| .connections = tas_connections_all, |
| }, |
| }, |
| /* PowerBook6,7 */ |
| { .layout_id = 80, |
| .codecs[0] = { |
| .name = "tas", |
| .connections = tas_connections_noline, |
| }, |
| }, |
| /* PowerBook6,8 */ |
| { .layout_id = 72, |
| .codecs[0] = { |
| .name = "tas", |
| .connections = tas_connections_nolineout, |
| }, |
| }, |
| /* PowerMac8,2 */ |
| { .layout_id = 86, |
| .codecs[0] = { |
| .name = "onyx", |
| .connections = onyx_connections_nomic, |
| }, |
| .codecs[1] = { |
| .name = "topaz", |
| .connections = topaz_input, |
| }, |
| }, |
| /* PowerBook6,7 */ |
| { .layout_id = 92, |
| .codecs[0] = { |
| .name = "tas", |
| .connections = tas_connections_nolineout, |
| }, |
| }, |
| /* PowerMac10,1 (Mac Mini) */ |
| { .layout_id = 58, |
| .codecs[0] = { |
| .name = "toonie", |
| .connections = toonie_connections, |
| }, |
| }, |
| { |
| .layout_id = 96, |
| .codecs[0] = { |
| .name = "onyx", |
| .connections = onyx_connections_noheadphones, |
| }, |
| }, |
| /* unknown, untested, but this comes from Apple */ |
| { .layout_id = 41, |
| .codecs[0] = { |
| .name = "tas", |
| .connections = tas_connections_all, |
| }, |
| }, |
| { .layout_id = 36, |
| .codecs[0] = { |
| .name = "tas", |
| .connections = tas_connections_nomic, |
| }, |
| .codecs[1] = { |
| .name = "topaz", |
| .connections = topaz_inout, |
| }, |
| }, |
| { .layout_id = 47, |
| .codecs[0] = { |
| .name = "onyx", |
| .connections = onyx_connections_noheadphones, |
| }, |
| }, |
| { .layout_id = 48, |
| .codecs[0] = { |
| .name = "topaz", |
| .connections = topaz_input, |
| }, |
| }, |
| { .layout_id = 49, |
| .codecs[0] = { |
| .name = "onyx", |
| .connections = onyx_connections_nomic, |
| }, |
| }, |
| { .layout_id = 50, |
| .codecs[0] = { |
| .name = "topaz", |
| .connections = topaz_input, |
| }, |
| }, |
| { .layout_id = 56, |
| .codecs[0] = { |
| .name = "onyx", |
| .connections = onyx_connections_noheadphones, |
| }, |
| }, |
| { .layout_id = 57, |
| .codecs[0] = { |
| .name = "topaz", |
| .connections = topaz_input, |
| }, |
| }, |
| { .layout_id = 62, |
| .codecs[0] = { |
| .name = "onyx", |
| .connections = onyx_connections_noheadphones, |
| }, |
| .codecs[1] = { |
| .name = "topaz", |
| .connections = topaz_output, |
| }, |
| }, |
| { .layout_id = 66, |
| .codecs[0] = { |
| .name = "onyx", |
| .connections = onyx_connections_noheadphones, |
| }, |
| }, |
| { .layout_id = 67, |
| .codecs[0] = { |
| .name = "topaz", |
| .connections = topaz_input, |
| }, |
| }, |
| { .layout_id = 76, |
| .codecs[0] = { |
| .name = "tas", |
| .connections = tas_connections_nomic, |
| }, |
| .codecs[1] = { |
| .name = "topaz", |
| .connections = topaz_inout, |
| }, |
| }, |
| { .layout_id = 90, |
| .codecs[0] = { |
| .name = "tas", |
| .connections = tas_connections_noline, |
| }, |
| }, |
| { .layout_id = 94, |
| .codecs[0] = { |
| .name = "onyx", |
| /* but it has an external mic?? how to select? */ |
| .connections = onyx_connections_noheadphones, |
| }, |
| }, |
| { .layout_id = 98, |
| .codecs[0] = { |
| .name = "toonie", |
| .connections = toonie_connections, |
| }, |
| }, |
| { .layout_id = 100, |
| .codecs[0] = { |
| .name = "topaz", |
| .connections = topaz_input, |
| }, |
| .codecs[1] = { |
| .name = "onyx", |
| .connections = onyx_connections_noheadphones, |
| }, |
| }, |
| /* PowerMac3,4 */ |
| { .device_id = 14, |
| .codecs[0] = { |
| .name = "tas", |
| .connections = tas_connections_noline, |
| }, |
| }, |
| /* PowerMac3,6 */ |
| { .device_id = 22, |
| .codecs[0] = { |
| .name = "tas", |
| .connections = tas_connections_all, |
| }, |
| }, |
| /* PowerBook5,2 */ |
| { .device_id = 35, |
| .codecs[0] = { |
| .name = "tas", |
| .connections = tas_connections_all, |
| }, |
| }, |
| {} |
| }; |
| |
| static struct layout *find_layout_by_id(unsigned int id) |
| { |
| struct layout *l; |
| |
| l = layouts; |
| while (l->codecs[0].name) { |
| if (l->layout_id == id) |
| return l; |
| l++; |
| } |
| return NULL; |
| } |
| |
| static struct layout *find_layout_by_device(unsigned int id) |
| { |
| struct layout *l; |
| |
| l = layouts; |
| while (l->codecs[0].name) { |
| if (l->device_id == id) |
| return l; |
| l++; |
| } |
| return NULL; |
| } |
| |
| static void use_layout(struct layout *l) |
| { |
| int i; |
| |
| for (i=0; i<MAX_CODECS_PER_BUS; i++) { |
| if (l->codecs[i].name) { |
| request_module("snd-aoa-codec-%s", l->codecs[i].name); |
| } |
| } |
| /* now we wait for the codecs to call us back */ |
| } |
| |
| struct layout_dev; |
| |
| struct layout_dev_ptr { |
| struct layout_dev *ptr; |
| }; |
| |
| struct layout_dev { |
| struct list_head list; |
| struct soundbus_dev *sdev; |
| struct device_node *sound; |
| struct aoa_codec *codecs[MAX_CODECS_PER_BUS]; |
| struct layout *layout; |
| struct gpio_runtime gpio; |
| |
| /* we need these for headphone/lineout detection */ |
| struct snd_kcontrol *headphone_ctrl; |
| struct snd_kcontrol *lineout_ctrl; |
| struct snd_kcontrol *speaker_ctrl; |
| struct snd_kcontrol *master_ctrl; |
| struct snd_kcontrol *headphone_detected_ctrl; |
| struct snd_kcontrol *lineout_detected_ctrl; |
| |
| struct layout_dev_ptr selfptr_headphone; |
| struct layout_dev_ptr selfptr_lineout; |
| |
| u32 have_lineout_detect:1, |
| have_headphone_detect:1, |
| switch_on_headphone:1, |
| switch_on_lineout:1; |
| }; |
| |
| static LIST_HEAD(layouts_list); |
| static int layouts_list_items; |
| /* this can go away but only if we allow multiple cards, |
| * make the fabric handle all the card stuff, etc... */ |
| static struct layout_dev *layout_device; |
| |
| #define control_info snd_ctl_boolean_mono_info |
| |
| #define AMP_CONTROL(n, description) \ |
| static int n##_control_get(struct snd_kcontrol *kcontrol, \ |
| struct snd_ctl_elem_value *ucontrol) \ |
| { \ |
| struct gpio_runtime *gpio = snd_kcontrol_chip(kcontrol); \ |
| if (gpio->methods && gpio->methods->get_##n) \ |
| ucontrol->value.integer.value[0] = \ |
| gpio->methods->get_##n(gpio); \ |
| return 0; \ |
| } \ |
| static int n##_control_put(struct snd_kcontrol *kcontrol, \ |
| struct snd_ctl_elem_value *ucontrol) \ |
| { \ |
| struct gpio_runtime *gpio = snd_kcontrol_chip(kcontrol); \ |
| if (gpio->methods && gpio->methods->set_##n) \ |
| gpio->methods->set_##n(gpio, \ |
| !!ucontrol->value.integer.value[0]); \ |
| return 1; \ |
| } \ |
| static const struct snd_kcontrol_new n##_ctl = { \ |
| .iface = SNDRV_CTL_ELEM_IFACE_MIXER, \ |
| .name = description, \ |
| .access = SNDRV_CTL_ELEM_ACCESS_READWRITE, \ |
| .info = control_info, \ |
| .get = n##_control_get, \ |
| .put = n##_control_put, \ |
| } |
| |
| AMP_CONTROL(headphone, "Headphone Switch"); |
| AMP_CONTROL(speakers, "Speakers Switch"); |
| AMP_CONTROL(lineout, "Line-Out Switch"); |
| AMP_CONTROL(master, "Master Switch"); |
| |
| static int detect_choice_get(struct snd_kcontrol *kcontrol, |
| struct snd_ctl_elem_value *ucontrol) |
| { |
| struct layout_dev *ldev = snd_kcontrol_chip(kcontrol); |
| |
| switch (kcontrol->private_value) { |
| case 0: |
| ucontrol->value.integer.value[0] = ldev->switch_on_headphone; |
| break; |
| case 1: |
| ucontrol->value.integer.value[0] = ldev->switch_on_lineout; |
| break; |
| default: |
| return -ENODEV; |
| } |
| return 0; |
| } |
| |
| static int detect_choice_put(struct snd_kcontrol *kcontrol, |
| struct snd_ctl_elem_value *ucontrol) |
| { |
| struct layout_dev *ldev = snd_kcontrol_chip(kcontrol); |
| |
| switch (kcontrol->private_value) { |
| case 0: |
| ldev->switch_on_headphone = !!ucontrol->value.integer.value[0]; |
| break; |
| case 1: |
| ldev->switch_on_lineout = !!ucontrol->value.integer.value[0]; |
| break; |
| default: |
| return -ENODEV; |
| } |
| return 1; |
| } |
| |
| static const struct snd_kcontrol_new headphone_detect_choice = { |
| .iface = SNDRV_CTL_ELEM_IFACE_MIXER, |
| .name = "Headphone Detect Autoswitch", |
| .info = control_info, |
| .access = SNDRV_CTL_ELEM_ACCESS_READWRITE, |
| .get = detect_choice_get, |
| .put = detect_choice_put, |
| .private_value = 0, |
| }; |
| |
| static const struct snd_kcontrol_new lineout_detect_choice = { |
| .iface = SNDRV_CTL_ELEM_IFACE_MIXER, |
| .name = "Line-Out Detect Autoswitch", |
| .info = control_info, |
| .access = SNDRV_CTL_ELEM_ACCESS_READWRITE, |
| .get = detect_choice_get, |
| .put = detect_choice_put, |
| .private_value = 1, |
| }; |
| |
| static int detected_get(struct snd_kcontrol *kcontrol, |
| struct snd_ctl_elem_value *ucontrol) |
| { |
| struct layout_dev *ldev = snd_kcontrol_chip(kcontrol); |
| int v; |
| |
| switch (kcontrol->private_value) { |
| case 0: |
| v = ldev->gpio.methods->get_detect(&ldev->gpio, |
| AOA_NOTIFY_HEADPHONE); |
| break; |
| case 1: |
| v = ldev->gpio.methods->get_detect(&ldev->gpio, |
| AOA_NOTIFY_LINE_OUT); |
| break; |
| default: |
| return -ENODEV; |
| } |
| ucontrol->value.integer.value[0] = v; |
| return 0; |
| } |
| |
| static const struct snd_kcontrol_new headphone_detected = { |
| .iface = SNDRV_CTL_ELEM_IFACE_MIXER, |
| .name = "Headphone Detected", |
| .info = control_info, |
| .access = SNDRV_CTL_ELEM_ACCESS_READ, |
| .get = detected_get, |
| .private_value = 0, |
| }; |
| |
| static const struct snd_kcontrol_new lineout_detected = { |
| .iface = SNDRV_CTL_ELEM_IFACE_MIXER, |
| .name = "Line-Out Detected", |
| .info = control_info, |
| .access = SNDRV_CTL_ELEM_ACCESS_READ, |
| .get = detected_get, |
| .private_value = 1, |
| }; |
| |
| static int check_codec(struct aoa_codec *codec, |
| struct layout_dev *ldev, |
| struct codec_connect_info *cci) |
| { |
| const u32 *ref; |
| char propname[32]; |
| struct codec_connection *cc; |
| |
| /* if the codec has a 'codec' node, we require a reference */ |
| if (of_node_name_eq(codec->node, "codec")) { |
| snprintf(propname, sizeof(propname), |
| "platform-%s-codec-ref", codec->name); |
| ref = of_get_property(ldev->sound, propname, NULL); |
| if (!ref) { |
| printk(KERN_INFO "snd-aoa-fabric-layout: " |
| "required property %s not present\n", propname); |
| return -ENODEV; |
| } |
| if (*ref != codec->node->phandle) { |
| printk(KERN_INFO "snd-aoa-fabric-layout: " |
| "%s doesn't match!\n", propname); |
| return -ENODEV; |
| } |
| } else { |
| if (layouts_list_items != 1) { |
| printk(KERN_INFO "snd-aoa-fabric-layout: " |
| "more than one soundbus, but no references.\n"); |
| return -ENODEV; |
| } |
| } |
| codec->soundbus_dev = ldev->sdev; |
| codec->gpio = &ldev->gpio; |
| |
| cc = cci->connections; |
| if (!cc) |
| return -EINVAL; |
| |
| printk(KERN_INFO "snd-aoa-fabric-layout: can use this codec\n"); |
| |
| codec->connected = 0; |
| codec->fabric_data = cc; |
| |
| while (cc->connected) { |
| codec->connected |= 1<<cc->codec_bit; |
| cc++; |
| } |
| |
| return 0; |
| } |
| |
| static int layout_found_codec(struct aoa_codec *codec) |
| { |
| struct layout_dev *ldev; |
| int i; |
| |
| list_for_each_entry(ldev, &layouts_list, list) { |
| for (i=0; i<MAX_CODECS_PER_BUS; i++) { |
| if (!ldev->layout->codecs[i].name) |
| continue; |
| if (strcmp(ldev->layout->codecs[i].name, codec->name) == 0) { |
| if (check_codec(codec, |
| ldev, |
| &ldev->layout->codecs[i]) == 0) |
| return 0; |
| } |
| } |
| } |
| return -ENODEV; |
| } |
| |
| static void layout_remove_codec(struct aoa_codec *codec) |
| { |
| int i; |
| /* here remove the codec from the layout dev's |
| * codec reference */ |
| |
| codec->soundbus_dev = NULL; |
| codec->gpio = NULL; |
| for (i=0; i<MAX_CODECS_PER_BUS; i++) { |
| } |
| } |
| |
| static void layout_notify(void *data) |
| { |
| struct layout_dev_ptr *dptr = data; |
| struct layout_dev *ldev; |
| int v, update; |
| struct snd_kcontrol *detected, *c; |
| struct snd_card *card = aoa_get_card(); |
| |
| ldev = dptr->ptr; |
| if (data == &ldev->selfptr_headphone) { |
| v = ldev->gpio.methods->get_detect(&ldev->gpio, AOA_NOTIFY_HEADPHONE); |
| detected = ldev->headphone_detected_ctrl; |
| update = ldev->switch_on_headphone; |
| if (update) { |
| ldev->gpio.methods->set_speakers(&ldev->gpio, !v); |
| ldev->gpio.methods->set_headphone(&ldev->gpio, v); |
| ldev->gpio.methods->set_lineout(&ldev->gpio, 0); |
| } |
| } else if (data == &ldev->selfptr_lineout) { |
| v = ldev->gpio.methods->get_detect(&ldev->gpio, AOA_NOTIFY_LINE_OUT); |
| detected = ldev->lineout_detected_ctrl; |
| update = ldev->switch_on_lineout; |
| if (update) { |
| ldev->gpio.methods->set_speakers(&ldev->gpio, !v); |
| ldev->gpio.methods->set_headphone(&ldev->gpio, 0); |
| ldev->gpio.methods->set_lineout(&ldev->gpio, v); |
| } |
| } else |
| return; |
| |
| if (detected) |
| snd_ctl_notify(card, SNDRV_CTL_EVENT_MASK_VALUE, &detected->id); |
| if (update) { |
| c = ldev->headphone_ctrl; |
| if (c) |
| snd_ctl_notify(card, SNDRV_CTL_EVENT_MASK_VALUE, &c->id); |
| c = ldev->speaker_ctrl; |
| if (c) |
| snd_ctl_notify(card, SNDRV_CTL_EVENT_MASK_VALUE, &c->id); |
| c = ldev->lineout_ctrl; |
| if (c) |
| snd_ctl_notify(card, SNDRV_CTL_EVENT_MASK_VALUE, &c->id); |
| } |
| } |
| |
| static void layout_attached_codec(struct aoa_codec *codec) |
| { |
| struct codec_connection *cc; |
| struct snd_kcontrol *ctl; |
| int headphones, lineout; |
| struct layout_dev *ldev = layout_device; |
| |
| /* need to add this codec to our codec array! */ |
| |
| cc = codec->fabric_data; |
| |
| headphones = codec->gpio->methods->get_detect(codec->gpio, |
| AOA_NOTIFY_HEADPHONE); |
| lineout = codec->gpio->methods->get_detect(codec->gpio, |
| AOA_NOTIFY_LINE_OUT); |
| |
| if (codec->gpio->methods->set_master) { |
| ctl = snd_ctl_new1(&master_ctl, codec->gpio); |
| ldev->master_ctrl = ctl; |
| aoa_snd_ctl_add(ctl); |
| } |
| while (cc->connected) { |
| if (cc->connected & CC_SPEAKERS) { |
| if (headphones <= 0 && lineout <= 0) |
| ldev->gpio.methods->set_speakers(codec->gpio, 1); |
| ctl = snd_ctl_new1(&speakers_ctl, codec->gpio); |
| ldev->speaker_ctrl = ctl; |
| aoa_snd_ctl_add(ctl); |
| } |
| if (cc->connected & CC_HEADPHONE) { |
| if (headphones == 1) |
| ldev->gpio.methods->set_headphone(codec->gpio, 1); |
| ctl = snd_ctl_new1(&headphone_ctl, codec->gpio); |
| ldev->headphone_ctrl = ctl; |
| aoa_snd_ctl_add(ctl); |
| ldev->have_headphone_detect = |
| !ldev->gpio.methods |
| ->set_notify(&ldev->gpio, |
| AOA_NOTIFY_HEADPHONE, |
| layout_notify, |
| &ldev->selfptr_headphone); |
| if (ldev->have_headphone_detect) { |
| ctl = snd_ctl_new1(&headphone_detect_choice, |
| ldev); |
| aoa_snd_ctl_add(ctl); |
| ctl = snd_ctl_new1(&headphone_detected, |
| ldev); |
| ldev->headphone_detected_ctrl = ctl; |
| aoa_snd_ctl_add(ctl); |
| } |
| } |
| if (cc->connected & CC_LINEOUT) { |
| if (lineout == 1) |
| ldev->gpio.methods->set_lineout(codec->gpio, 1); |
| ctl = snd_ctl_new1(&lineout_ctl, codec->gpio); |
| if (cc->connected & CC_LINEOUT_LABELLED_HEADPHONE) |
| strscpy(ctl->id.name, |
| "Headphone Switch", sizeof(ctl->id.name)); |
| ldev->lineout_ctrl = ctl; |
| aoa_snd_ctl_add(ctl); |
| ldev->have_lineout_detect = |
| !ldev->gpio.methods |
| ->set_notify(&ldev->gpio, |
| AOA_NOTIFY_LINE_OUT, |
| layout_notify, |
| &ldev->selfptr_lineout); |
| if (ldev->have_lineout_detect) { |
| ctl = snd_ctl_new1(&lineout_detect_choice, |
| ldev); |
| if (cc->connected & CC_LINEOUT_LABELLED_HEADPHONE) |
| strscpy(ctl->id.name, |
| "Headphone Detect Autoswitch", |
| sizeof(ctl->id.name)); |
| aoa_snd_ctl_add(ctl); |
| ctl = snd_ctl_new1(&lineout_detected, |
| ldev); |
| if (cc->connected & CC_LINEOUT_LABELLED_HEADPHONE) |
| strscpy(ctl->id.name, |
| "Headphone Detected", |
| sizeof(ctl->id.name)); |
| ldev->lineout_detected_ctrl = ctl; |
| aoa_snd_ctl_add(ctl); |
| } |
| } |
| cc++; |
| } |
| /* now update initial state */ |
| if (ldev->have_headphone_detect) |
| layout_notify(&ldev->selfptr_headphone); |
| if (ldev->have_lineout_detect) |
| layout_notify(&ldev->selfptr_lineout); |
| } |
| |
| static struct aoa_fabric layout_fabric = { |
| .name = "SoundByLayout", |
| .owner = THIS_MODULE, |
| .found_codec = layout_found_codec, |
| .remove_codec = layout_remove_codec, |
| .attached_codec = layout_attached_codec, |
| }; |
| |
| static int aoa_fabric_layout_probe(struct soundbus_dev *sdev) |
| { |
| struct device_node *sound = NULL; |
| const unsigned int *id; |
| struct layout *layout = NULL; |
| struct layout_dev *ldev = NULL; |
| int err; |
| |
| /* hm, currently we can only have one ... */ |
| if (layout_device) |
| return -ENODEV; |
| |
| /* by breaking out we keep a reference */ |
| for_each_child_of_node(sdev->ofdev.dev.of_node, sound) { |
| if (of_node_is_type(sound, "soundchip")) |
| break; |
| } |
| if (!sound) |
| return -ENODEV; |
| |
| id = of_get_property(sound, "layout-id", NULL); |
| if (id) { |
| layout = find_layout_by_id(*id); |
| } else { |
| id = of_get_property(sound, "device-id", NULL); |
| if (id) |
| layout = find_layout_by_device(*id); |
| } |
| |
| if (!layout) { |
| printk(KERN_ERR "snd-aoa-fabric-layout: unknown layout\n"); |
| goto outnodev; |
| } |
| |
| ldev = kzalloc(sizeof(struct layout_dev), GFP_KERNEL); |
| if (!ldev) |
| goto outnodev; |
| |
| layout_device = ldev; |
| ldev->sdev = sdev; |
| ldev->sound = sound; |
| ldev->layout = layout; |
| ldev->gpio.node = sound->parent; |
| switch (layout->layout_id) { |
| case 0: /* anything with device_id, not layout_id */ |
| case 41: /* that unknown machine no one seems to have */ |
| case 51: /* PowerBook5,4 */ |
| case 58: /* Mac Mini */ |
| ldev->gpio.methods = ftr_gpio_methods; |
| printk(KERN_DEBUG |
| "snd-aoa-fabric-layout: Using direct GPIOs\n"); |
| break; |
| default: |
| ldev->gpio.methods = pmf_gpio_methods; |
| printk(KERN_DEBUG |
| "snd-aoa-fabric-layout: Using PMF GPIOs\n"); |
| } |
| ldev->selfptr_headphone.ptr = ldev; |
| ldev->selfptr_lineout.ptr = ldev; |
| dev_set_drvdata(&sdev->ofdev.dev, ldev); |
| list_add(&ldev->list, &layouts_list); |
| layouts_list_items++; |
| |
| /* assign these before registering ourselves, so |
| * callbacks that are done during registration |
| * already have the values */ |
| sdev->pcmid = ldev->layout->pcmid; |
| if (ldev->layout->busname) { |
| sdev->pcmname = ldev->layout->busname; |
| } else { |
| sdev->pcmname = "Master"; |
| } |
| |
| ldev->gpio.methods->init(&ldev->gpio); |
| |
| err = aoa_fabric_register(&layout_fabric, &sdev->ofdev.dev); |
| if (err && err != -EALREADY) { |
| printk(KERN_INFO "snd-aoa-fabric-layout: can't use," |
| " another fabric is active!\n"); |
| goto outlistdel; |
| } |
| |
| use_layout(layout); |
| ldev->switch_on_headphone = 1; |
| ldev->switch_on_lineout = 1; |
| return 0; |
| outlistdel: |
| /* we won't be using these then... */ |
| ldev->gpio.methods->exit(&ldev->gpio); |
| /* reset if we didn't use it */ |
| sdev->pcmname = NULL; |
| sdev->pcmid = -1; |
| list_del(&ldev->list); |
| layouts_list_items--; |
| kfree(ldev); |
| outnodev: |
| of_node_put(sound); |
| layout_device = NULL; |
| return -ENODEV; |
| } |
| |
| static void aoa_fabric_layout_remove(struct soundbus_dev *sdev) |
| { |
| struct layout_dev *ldev = dev_get_drvdata(&sdev->ofdev.dev); |
| int i; |
| |
| for (i=0; i<MAX_CODECS_PER_BUS; i++) { |
| if (ldev->codecs[i]) { |
| aoa_fabric_unlink_codec(ldev->codecs[i]); |
| } |
| ldev->codecs[i] = NULL; |
| } |
| list_del(&ldev->list); |
| layouts_list_items--; |
| of_node_put(ldev->sound); |
| |
| ldev->gpio.methods->set_notify(&ldev->gpio, |
| AOA_NOTIFY_HEADPHONE, |
| NULL, |
| NULL); |
| ldev->gpio.methods->set_notify(&ldev->gpio, |
| AOA_NOTIFY_LINE_OUT, |
| NULL, |
| NULL); |
| |
| ldev->gpio.methods->exit(&ldev->gpio); |
| layout_device = NULL; |
| kfree(ldev); |
| sdev->pcmid = -1; |
| sdev->pcmname = NULL; |
| } |
| |
| static int aoa_fabric_layout_suspend(struct device *dev) |
| { |
| struct layout_dev *ldev = dev_get_drvdata(dev); |
| |
| if (ldev->gpio.methods && ldev->gpio.methods->all_amps_off) |
| ldev->gpio.methods->all_amps_off(&ldev->gpio); |
| |
| return 0; |
| } |
| |
| static int aoa_fabric_layout_resume(struct device *dev) |
| { |
| struct layout_dev *ldev = dev_get_drvdata(dev); |
| |
| if (ldev->gpio.methods && ldev->gpio.methods->all_amps_restore) |
| ldev->gpio.methods->all_amps_restore(&ldev->gpio); |
| |
| return 0; |
| } |
| |
| static DEFINE_SIMPLE_DEV_PM_OPS(aoa_fabric_layout_pm_ops, |
| aoa_fabric_layout_suspend, aoa_fabric_layout_resume); |
| |
| static struct soundbus_driver aoa_soundbus_driver = { |
| .name = "snd_aoa_soundbus_drv", |
| .owner = THIS_MODULE, |
| .probe = aoa_fabric_layout_probe, |
| .remove = aoa_fabric_layout_remove, |
| .driver = { |
| .owner = THIS_MODULE, |
| .pm = &aoa_fabric_layout_pm_ops, |
| } |
| }; |
| |
| static int __init aoa_fabric_layout_init(void) |
| { |
| return soundbus_register_driver(&aoa_soundbus_driver); |
| } |
| |
| static void __exit aoa_fabric_layout_exit(void) |
| { |
| soundbus_unregister_driver(&aoa_soundbus_driver); |
| aoa_fabric_unregister(&layout_fabric); |
| } |
| |
| module_init(aoa_fabric_layout_init); |
| module_exit(aoa_fabric_layout_exit); |