| // 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 <asm/prom.h> | |
| #include <linux/list.h> | |
| #include <linux/module.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 int 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; | |
| return 0; | |
| } | |
| #ifdef CONFIG_PM_SLEEP | |
| 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 SIMPLE_DEV_PM_OPS(aoa_fabric_layout_pm_ops, | |
| aoa_fabric_layout_suspend, aoa_fabric_layout_resume); | |
| #endif | |
| 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, | |
| #ifdef CONFIG_PM_SLEEP | |
| .pm = &aoa_fabric_layout_pm_ops, | |
| #endif | |
| } | |
| }; | |
| 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); |