Rockchip RK3399 – ASoC Sound Card Jack Device

—————————————————————————————————————————-

Development Board: NanoPC-T4 Development BoardeMMC: 16GBLPDDR3: 4GBDisplay: 15.6 inch HDMI Interface Displayu-boot: 2023.04Linux: 6.3—————————————————————————————————————————-

The term “jack” literally refers to a socket. What is a socket? I understand it to be the socket for audio devices such as microphones and headphones.

So what is the function of the jack device? Its main purpose is to detect the insertion and removal of devices such as headphones and microphones.

Taking headphone detection as an example, general headphone detection includes two types: regular headphone detection and detection for headphones with a microphone. These two types are collectively referred to as Headset, while headphones without a microphone are generally referred to as Headphone.

For the detection of Headset devices, it is generally completed through the jack. The basic principle is to use a headphone socket with a detection mechanical structure, connecting the detection pin to the SoC’s GPIO port. As shown in the figure below, when the headphone is inserted, the metal of the headphone plug touches the detection pin, causing a change in the level of the detection pin, thus triggering an interrupt. This allows the interrupt handler to read the value of the GPIO port and further determine whether the headphone is inserted or removed.

Rockchip RK3399 - ASoC Sound Card Jack Device

ALSA CORE has implemented a jack intermediate layer, providing APIs to access the jack intermediate layer code in the file include/sound/jack.h.

1. ALSA Core Data Structures

1.1 struct snd_jack

The jack uses the struct snd_jack data structure to describe itself;

struct snd_jack {
        struct list_head kctl_list;
        struct snd_card *card;
        const char *id;
#ifdef CONFIG_SND_JACK_INPUT_DEV
        struct input_dev *input_dev;
        struct mutex input_dev_lock;
        int registered;
        int type;
        char name[100];
        unsigned int key[6];   /* Keep in sync with definitions above */
#endif /* CONFIG_SND_JACK_INPUT_DEV */
        int hw_status_cache;
        void *private_data;
        void (*private_free)(struct snd_jack *);
};

Among them:

  • • kctl_list: Saves the list of jack kcontrols, where each element is struct snd_jack_kctl;
  • • card: The associated sound card device;
  • • id: The unique identifier for the jack;
  • • input_dev: Pointer to the input device structure (struct input_dev), used to handle input events related to the jack;
  • • input_dev_lock: Lock for the input device, used to protect operations on the input device;
  • • registered: Flag indicating whether the jack has been registered;
  • • type: The type of jack that can be reported, referring to enum snd_jack_types;
  • • name: The name string of the jack, with a length of 100 bytes;
  • • key: If the jack can report SND_JACK_BTN_0, SND_JACK_BTN_1, etc., the array element key[x] stores the event code for the corresponding EV_KEY event;
  • • hw_status_cache: Cached value of the hardware status;
  • • private_data: Pointer to private data, which can be used to store other data related to the jack;
  • • private_free: Pointer to the private data release function, used to release related private data when freeing snd_jack;

1.2 enum snd_jack_types

The types that the jack can report are described in the structure definition;

/**
 * enum snd_jack_types - Jack types which can be reported
 * @SND_JACK_HEADPHONE: Headphone
 * @SND_JACK_MICROPHONE: Microphone
 * @SND_JACK_HEADSET: Headset
 * @SND_JACK_LINEOUT: Line out
 * @SND_JACK_MECHANICAL: Mechanical switch
 * @SND_JACK_VIDEOOUT: Video out
 * @SND_JACK_AVOUT: AV (Audio Video) out
 * @SND_JACK_LINEIN:  Line in
 * @SND_JACK_BTN_0: Button 0
 * @SND_JACK_BTN_1: Button 1
 * @SND_JACK_BTN_2: Button 2
 * @SND_JACK_BTN_3: Button 3
 * @SND_JACK_BTN_4: Button 4
 * @SND_JACK_BTN_5: Button 5
 *
 * These values are used as a bitmask.
 *
 * Note that this must be kept in sync with the lookup table in
 * sound/core/jack.c.
 */
enum snd_jack_types {
        SND_JACK_HEADPHONE      = 0x0001,  // If jack->type & 0x001=0x001, then set the input device to report EV_SW event, event code is 0x02
        SND_JACK_MICROPHONE     = 0x0002,  // Same as above, set the input device to report EV_SW event, event code is 0x04
        SND_JACK_HEADSET        = SND_JACK_HEADPHONE | SND_JACK_MICROPHONE,
        SND_JACK_LINEOUT        = 0x0004,  // Same as above, set the input device to report EV_SW event, event code is 0x04
        SND_JACK_MECHANICAL     = 0x0008, /* If detected separately */ Same as above, set the input device to report EV_SW event, event code is 0x07
        SND_JACK_VIDEOOUT       = 0x0010,  // Same as above, set the input device to report EV_SW event, event code is 0x08
        SND_JACK_AVOUT          = SND_JACK_LINEOUT | SND_JACK_VIDEOOUT,
        SND_JACK_LINEIN         = 0x0020,  // Same as above, set the input device to report EV_SW event, event code is 0x0d

        /* Kept separate from switches to facilitate implementation */
        SND_JACK_BTN_0          = 0x4000,   // If jack->type & 0x4000=0x4000, then set the input device to report EV_KEY event, event code is 0x100
        SND_JACK_BTN_1          = 0x2000,   // Same as above, set the input device to report EV_KEY event, event code is 0x101
        SND_JACK_BTN_2          = 0x1000,   // Same as above, set the input device to report EV_KEY event, event code is 0x102
        SND_JACK_BTN_3          = 0x0800,   // Same as above, set the input device to report EV_KEY event, event code is 0x103
        SND_JACK_BTN_4          = 0x0400,   // Same as above, set the input device to report EV_KEY event, event code is 0x104
        SND_JACK_BTN_5          = 0x0200,   // Same as above. Set the input device to report EV_KEY event, event code is 0x105
};

1.3 struct snd_jack_kctl

In ALSA, the struct snd_jack_kctl data structure is used to describe the kcontrol in the jack, defined in sound/core/jack.c:

struct snd_jack_kctl {
        struct snd_kcontrol *kctl;
        struct list_head list;  /* list of controls belong to the same jack */
        unsigned int mask_bits; /* only masked status bits are reported via kctl */
        struct snd_jack *jack;  /* pointer to struct snd_jack */
        bool sw_inject_enable;  /* allow to inject plug event via debugfs */
#ifdef CONFIG_SND_JACK_INJECTION_DEBUG
        struct dentry *jack_debugfs_root; /* jack_kctl debugfs root */
#endif
};

Among them:

  • • kctl: Pointer to struct snd_kcontrol;
  • • list: List node used to link the current node to the snd_jack’s kctl_list;
  • • mask_bits: The type that the current jack control can report, referring to enum snd_jack_types; for example, if we set the mask_bits value to SND_JACK_HEADPHONE, when the headphone is inserted or removed, the HP_DET pin will receive an interrupt, the interrupt handler reads the pin level and reports the SND_JACK_HEADPHONE corresponding EV_SW event, event code is 0x02, value is insert/remove;
  • • jack: Pointer to struct snd_jack, indicating the jack associated with snd_jack_kctl;
  • • sw_inject_enable: Allows injecting plug events via debugfs;
  • • jack_debugfs_root: Jack_kctl debugfs root directory;

1.4 Relationship Diagram

To better understand the relationships among struct snd_jack, struct snd_kcontrol, struct snd_jack_kctl, etc., we have drawn the following relationship diagram:

Rockchip RK3399 - ASoC Sound Card Jack Device

Points we are particularly concerned about are:

  • • The jack is a snd_device that hangs under the devices list of the snd_card member;
  • • The field kctl_list in snd_jack saves all jack kcontrols, and each jack kcontrol is associated with a kcontrol;
  • • The field input_dev in snd_jack is an input device used to report headphone insertion/removal, microphone insertion/removal events.

2. ALSA Core API

2.1 Create Jack Device

The creation of the jack device can be completed through the snd_jack_new function, which takes 6 parameters:

  • • card: ALSA sound card device;
  • • id: Unique identifier for the jack device;
  • • type: A bitmask composed of the enum snd_jack_type values, indicating the types that this jack can report; here we can understand it as the types that the jack can detect, such as headphone insertion/removal;
  • • jack: Pointer to the jack pointer, used to save the allocated snd_jack;
  • • initial_kctl: If true, create a jack kcontrol and add it to the jack’s kctl_list;
  • • phantom_jack: For virtual jacks, do not create an input device;

The function is defined in sound/core/jack.c;

/**
 * snd_jack_new - Create a new jack
 * @card:  the card instance
 * @id:    an identifying string for this jack
 * @type:  a bitmask of enum snd_jack_type values that can be detected by
 *         this jack
 * @jjack: Used to provide the allocated jack object to the caller.
 * @initial_kctl: if true, create a kcontrol and add it to the jack list.
 * @phantom_jack: Don't create an input device for phantom jacks.
 *
 * Creates a new jack object.
 *
 * Return: Zero if successful, or a negative error code on failure.
 * On success @jjack will be initialised.
 */
int snd_jack_new(struct snd_card *card, const char *id, int type,
                 struct snd_jack **jjack, bool initial_kctl, bool phantom_jack)
{
        struct snd_jack *jack;
        struct snd_jack_kctl *jack_kctl = NULL;
        int err;
        static const struct snd_device_ops ops = {     // jack device operation set
                .dev_free = snd_jack_dev_free,
#ifdef CONFIG_SND_JACK_INPUT_DEV  // This macro is configured by default
                .dev_register = snd_jack_dev_register,
                .dev_disconnect = snd_jack_dev_disconnect,
#endif /* CONFIG_SND_JACK_INPUT_DEV */
        };

        if (initial_kctl) {  // If true, dynamically create a jack kcontrol, with a name of id
                jack_kctl = snd_jack_kctl_new(card, id, type); // The mask field of the jack kcontrol is set to type
                if (!jack_kctl)
                        return -ENOMEM;
        }

        jack = kzalloc(sizeof(struct snd_jack), GFP_KERNEL);  // Dynamically allocate memory for an snd_jack data structure
        if (jack == NULL)
                return -ENOMEM;

        jack->id = kstrdup(id, GFP_KERNEL); // Copy id and return
        if (jack->id == NULL) {
                kfree(jack);
                return -ENOMEM;
        }
#ifdef CONFIG_SND_JACK_INPUT_DEV
        mutex_init(&jack->input_dev_lock); // Get mutex lock

        /* don't create input device for phantom jack */
        if (!phantom_jack) {  // False, create input device
                int i;

                jack->input_dev = input_allocate_device(); // Dynamically create input device
                if (jack->input_dev == NULL) {
                        err = -ENOMEM;
                        goto fail_input;
                }

                jack->input_dev->phys = "ALSA";

                jack->type = type;  // Initialize jack type

                for (i = 0; i < SND_JACK_SWITCH_TYPES; i++) // 6
                        if (type & (1 << i))
                                input_set_capability(jack->input_dev, EV_SW, // Set the input device to report which input events EV_SW represents switch events
                                                     jack_switch_types[i]);   // Event encoding

        }
#endif /* CONFIG_SND_JACK_INPUT_DEV */
    //  Create a new snd_device instance and add it to the sound card device's devices list
        err = snd_device_new(card, SNDRV_DEV_JACK, jack, &ops);  // device_data is set to jack
        if (err < 0)
                goto fail_input;

        jack->card = card;  // Set sound card device
        INIT_LIST_HEAD(&jack->kctl_list); // Initialize list node

        if (initial_kctl)  // If true, add the jack kcontrol to the jack's kctl_list
                snd_jack_kctl_add(jack, jack_kctl);

        *jjack = jack;  // Write back

        return 0;

fail_input:
#ifdef CONFIG_SND_JACK_INPUT_DEV
        input_free_device(jack->input_dev);
#endif
        kfree(jack->id);
        kfree(jack);
        return err;
}

The jack_switch_types is a global array that stores the encoding of EV_SW type events, with a length of 6, defined in sound/core/jack.c:

static const int jack_switch_types[SND_JACK_SWITCH_TYPES] = {
        SW_HEADPHONE_INSERT,     // 0x02
        SW_MICROPHONE_INSERT,    // 0x04  
        SW_LINEOUT_INSERT,       // 0x06 
        SW_JACK_PHYSICAL_INSERT, // 0x07
        SW_VIDEOOUT_INSERT,      // 0x80
        SW_LINEIN_INSERT,        // 0x0d
};
2.1.1 snd_jack_kctl_new

The snd_jack_kctl_new function is defined in sound/core/jack.c, used to dynamically create a jack kcontrol, containing a kcontrol with a name of name; the function parameters include:

  • • card: Pointer to the snd_card structure, which points to the ALSA sound card device;
  • • name: The name of the kcontrol contained in the jack kcontrol;
  • • mask: The type that the newly created jack kcontrol can report, its value taken from enum snd_jack_type; for example, if we set the mask value to SND_JACK_HEADPHONE, when the headphone is inserted or removed, the HP_DET pin will receive an interrupt, the interrupt handler reads the pin level and reports the SND_JACK_HEADPHONE corresponding EV_SW event, event code is 0x02, value is insert/remove;

The function is defined as follows:

static struct snd_jack_kctl * snd_jack_kctl_new(struct snd_card *card, const char *name, unsigned int mask)
{
        struct snd_kcontrol *kctl;
        struct snd_jack_kctl *jack_kctl;
        int err;

        kctl = snd_kctl_jack_new(name, card); // Dynamically create a kcontrol
        if (!kctl)
                return NULL;

        err = snd_ctl_add(card, kctl); // Add the kcontrol to the sound card's controls list
        if (err < 0)
                return NULL;

        jack_kctl = kzalloc(sizeof(*jack_kctl), GFP_KERNEL); // Dynamically create snd_jack_kctl

        if (!jack_kctl)
                goto error;

        jack_kctl->kctl = kctl;       // Initialize members, set associated kcontrol
        jack_kctl->mask_bits = mask;  // Set the events to be reported

        kctl->private_data = jack_kctl;  // Set private data
        kctl->private_free = snd_jack_kctl_private_free;

        return jack_kctl;
error:
        snd_ctl_free_one(kctl);
        return NULL;
}

Where snd_kctl_jack_new is defined in sound/core/ctljack.c, used to dynamically create a kcontrol;

struct snd_kcontrol *
snd_kctl_jack_new(const char *name, struct snd_card *card)
{
        struct snd_kcontrol *kctl;

        kctl = snd_ctl_new1(&jack_detect_kctl, NULL);  // Allocates a kcontrol and copies corresponding values from the kcontrol template to this kcontrol
        if (!kctl)
                return NULL;

        jack_kctl_name_gen(kctl->id.name, name, sizeof(kctl->id.name));
        kctl->id.index = get_available_index(card, kctl->id.name);
        kctl->private_value = 0;   // Save the state of the kcontrol
        return kctl;
}

The jack_detect_kctl is a global variable of type struct snd_kcontrol_new, i.e., the kcontrol template:

static const struct snd_kcontrol_new jack_detect_kctl = {
        /* name is filled later */
        .iface = SNDRV_CTL_ELEM_IFACE_CARD,
        .access = SNDRV_CTL_ELEM_ACCESS_READ,
        .info = jack_detect_kctl_info,
        .get = jack_detect_kctl_get,
};
2.1.2 snd_jack_kctl_add

The snd_jack_kctl_add function is defined in sound/core/jack.c, used to add the jack kcontrol to the jack’s kctl_list;

static void snd_jack_kctl_add(struct snd_jack *jack, struct snd_jack_kctl *jack_kctl)
{
        jack_kctl->jack = jack;  // Set the associated jack
        list_add_tail(&jack_kctl->list, &jack->kctl_list); // Add to the jack's kctl_list
        snd_jack_debugfs_add_inject_node(jack, jack_kctl);
}

2.2 snd_jack_add_new_kctl

The snd_jack_add_new_kctl function is defined in sound/soc/jack.c, this function actually wraps snd_jack_kctl_new and snd_jack_kctl_add functions, used to create a new snd_jack_kctl and add it to the jack;

/**
 * snd_jack_add_new_kctl - Create a new snd_jack_kctl and add it to jack
 * @jack:  the jack instance which the kctl will attaching to
 * @name:  the name for the snd_kcontrol object
 * @mask:  a bitmask of enum snd_jack_type values that can be detected
 *         by this snd_jack_kctl object.
 *
 * Creates a new snd_kcontrol object and adds it to the jack kctl_list.
 *
 * Return: Zero if successful, or a negative error code on failure.
 */
int snd_jack_add_new_kctl(struct snd_jack *jack, const char * name, int mask)
{
        struct snd_jack_kctl *jack_kctl;

        jack_kctl = snd_jack_kctl_new(jack->card, name, mask);  // Dynamically create a jack kcontrol, containing a kcontrol with a name of name
        if (!jack_kctl)
                return -ENOMEM;

        snd_jack_kctl_add(jack, jack_kctl);  // Used to add the jack kcontrol to the jack's kctl_list;
        return 0;
}

2.3 snd_jack_dev_register

When registering the sound card device, it will traverse the logical device list of the sound card device and call the dev_register function in the sound card logical device operation set, for the jack device, it is the snd_jack_dev_register function.

The snd_jack_dev_register function is defined in sound/core/jack.c:

static int snd_jack_dev_register(struct snd_device *device)
{
        struct snd_jack *jack = device->device_data;  // Get jack
        struct snd_card *card = device->card;    // Get sound card
        int err, i;

        snprintf(jack->name, sizeof(jack->name), "%s %s",  // Set jack name 
                 card->shortname, jack->id);

        mutex_lock(&jack->input_dev_lock);  // Get mutex lock
        if (!jack->input_dev) {
                mutex_unlock(&jack->input_dev_lock);
                return 0;
        }

        jack->input_dev->name = jack->name;  // Set the name of the input device

        /* Default to the sound card device. */
        if (!jack->input_dev->dev.parent)
                jack->input_dev->dev.parent = snd_card_get_device_link(card);  // Set the parent device of the input device

        /* Add capabilities for any keys that are enabled */
        for (i = 0; i < ARRAY_SIZE(jack->key); i++) { // Set which EV_KEY types the jack can report, and save the event encoding in jack->key[i]
                int testbit = SND_JACK_BTN_0 >> i;  // 0x4000 >> i  

                if (!(jack->type & testbit)) // If the reporting of BTN_x type has been set, it will not enter
                        continue;

                if (!jack->key[i])
                        jack->key[i] = BTN_0 + i;  //  Convert to EV_KEY type event encoding

                input_set_capability(jack->input_dev, EV_KEY, jack->key[i]); // Set which input events the input device can report EV_KEY represents key events
        }

        err = input_register_device(jack->input_dev);   // Register the input device, will create input%d file in /sys/class/input
        if (err == 0)
                jack->registered = 1;  // Registration flag

        mutex_unlock(&jack->input_dev_lock);   // Release mutex lock
        return err;
}

2.4 snd_jack_report

The snd_jack_report function is defined in sound/core/jack.c, used to report the current status of a jack; the function internally calls input_report_key/input_report_switch to report EV_KEY and EV_SW events based on the jack’s reporting type;

/**
 * snd_jack_report - Report the current status of a jack
 * Note: This function uses mutexes and should be called from a
 * context which can sleep (such as a workqueue).
 *
 * @jack:   The jack to report status for
 * @status: The current status of the jack
 */
void snd_jack_report(struct snd_jack *jack, int status)
{
        struct snd_jack_kctl *jack_kctl;
        unsigned int mask_bits = 0;
#ifdef CONFIG_SND_JACK_INPUT_DEV
        int i;
#endif

        if (!jack)
                return;

        jack->hw_status_cache = status;   // Cache status

        list_for_each_entry(jack_kctl, &jack->kctl_list, list) // Traverse the jack kctl_list linked list, assign to jack_kctl
                if (jack_kctl->sw_inject_enable)   // This should not be set
                        mask_bits |= jack_kctl->mask_bits;
                else
                        snd_kctl_jack_report(jack->card, jack_kctl->kctl,
                                             status & jack_kctl->mask_bits); // The state of the kcontrol that needs to be set

#ifdef CONFIG_SND_JACK_INPUT_DEV
        mutex_lock(&jack->input_dev_lock);    // Get mutex lock
        if (!jack->input_dev) {
                mutex_unlock(&jack->input_dev_lock);
                return;
        }

        for (i = 0; i < ARRAY_SIZE(jack->key); i++) {  // Traverse EV_KEY event encoding
                int testbit = ((SND_JACK_BTN_0 >> i) & ~mask_bits);   // 0x4000 >> i  ....

                if (jack->type & testbit)   // If the reporting of SND_JACK_HEADPHONE type has been set
                        input_report_key(jack->input_dev, jack->key[i],  // Report EV_KEY type, event encoding is jack->key[i], value is status & testbit
                                         status & testbit);
        }

        for (i = 0; i < ARRAY_SIZE(jack_switch_types); i++) {  // Traverse EV_SW event encoding
                int testbit = ((1 << i) & ~mask_bits);  

                if (jack->type & testbit)    // If the reporting of SND_JACK_HEADPHONE type has been set
                        input_report_switch(jack->input_dev,    // Report EV_SW type, event encoding is jack_switch_types[i], value is status & testbit
                                            jack_switch_types[i],
                                            status & testbit);
        }

        input_sync(jack->input_dev);
        mutex_unlock(&jack->input_dev_lock);  // Release mutex lock
#endif /* CONFIG_SND_JACK_INPUT_DEV */
}

Where the function snd_kctl_jack_report is defined in sound/core/ctljack.c, used to send kcontrol state change notifications;

void snd_kctl_jack_report(struct snd_card *card,
                          struct snd_kcontrol *kctl, bool status)
{
        if (kctl->private_value == status)  // If the state has not changed
                return;
        kctl->private_value = status;  // Update the state of the kcontrol
        snd_ctl_notify(card, SNDRV_CTL_EVENT_MASK_VALUE, &kctl->id);
}

The function calls snd_ctl_notify, where the second parameter is the event mask (event-mask), and the third parameter is the pointer to the id of the kcontrol element for this notification; the event mask SNDRV_CTL_EVENT_MASK_VALUE defined by the following code means that the change of the kcontrol value is notified;

3. ASoC Core Data Structures

3.1 struct snd_soc_jack

In ASoC, struct snd_soc_jack is used to describe the jack and provides functions to manage and notify its status and pins, defined in include/sound/soc-jack.h:

struct snd_soc_jack {
        struct mutex mutex;
        struct snd_jack *jack;
        struct snd_soc_card *card;
        struct list_head pins;
        int status;
        struct blocking_notifier_head notifier;
        struct list_head jack_zones;
};

Among them:

  • • mutex: A mutex used to protect concurrent access to the current structure;
  • • jack: Pointer to the snd_jack structure, which points to the ALSA jack;
  • • card: Pointer to the snd_soc_card structure, representing the ASoC sound card device;
  • • pins: A linked list that saves the information of the jack pins, where each element is struct snd_soc_jack_pin;
  • • status: Indicates the insertion/removal status of the jack;
  • • notifier: A blocking notifier head used to notify associated observers when the jack status changes;
  • • jack_zones: A linked list header used to save the area information of the jack;

3.2 struct snd_soc_jack_gpio

In ASoC, struct snd_soc_jack_gpio is used to describe the properties and configurations of GPIO pins used for jack detection, including GPIO number, reported value, debounce time, etc.;

/**
 * struct snd_soc_jack_gpio - Describes a gpio pin for jack detection
 *
 * @gpio:         legacy gpio number
 * @idx:          gpio descriptor index within the function of the GPIO
 *                consumer device
 * @gpiod_dev:    GPIO consumer device
 * @name:         gpio name. Also as connection ID for the GPIO consumer
 *                device function name lookup
 * @report:       value to report when jack detected
 * @invert:       report presence in low state
 * @debounce_time: debounce time in ms
 * @wake:         enable as wake source
 * @jack_status_check: callback function which overrides the detection
 *                     to provide more complex checks (eg, reading an
 *                     ADC).
 */
struct snd_soc_jack_gpio {
        unsigned int gpio;
        unsigned int idx;
        struct device *gpiod_dev;
        const char *name;
        int report;
        int invert;
        int debounce_time;
        bool wake;

        /* private: */
        struct snd_soc_jack *jack;
        struct delayed_work work;
        struct notifier_block pm_notifier;
        struct gpio_desc *desc;

        void *data;
        /* public: */
        int (*jack_status_check)(void *data);
};

This structure contains the following members:

  • • gpio: GPIO number, globally unique;
  • • idx: Indicates the index of the GPIO descriptor within the function of the GPIO consumer device;
  • • gpiod_dev: Pointer to the GPIO consumer device;
  • • name: The name of the GPIO, also used as the connection ID for the GPIO consumer device function name lookup;
  • • report: The type that the GPIO can report, for example, the value for headphone insertion/removal is SND_JACK_HEADPHONE;
  • • invert: Indicates inversion; normally, a high level on the GPIO indicates that a device is inserted, and if set to invert, a low level indicates that a device is inserted;
  • • debounce_time: Debounce time, in milliseconds;
  • • wake: Whether to enable this GPIO pin as a wake source;
  • • jack: Pointer to the snd_soc_jack structure, representing the ASoC jack;
  • • work: Delayed work used for handling delayed jack detection;
  • • pm_notifier: Power management notifier block used for handling system power events;
  • • desc: Pointer to the GPIO descriptor;
  • • data: Pointer to arbitrary data, used to store private data;
  • • jack_status_check: Callback function used to override the detection process, providing more complex detection methods (e.g., reading ADC);

3.3 struct snd_soc_jack_pin

struct snd_soc_jack_pin is defined in include/sound/soc-jack.h, used to describe the widget to be powered on or off based on jack detection;

/**
 * struct snd_soc_jack_pin - Describes a pin to update based on jack detection
 *
 * @pin:    name of the pin to update
 * @mask:   bits to check for in reported jack status
 * @invert: if non-zero then pin is enabled when status is not reported
 * @list:   internal list entry
 */
struct snd_soc_jack_pin {
        struct list_head list;
        const char *pin;
        int mask;
        bool invert;
};

Structure members include:

  • • pin: The name of the pin, which is actually the name of the widget; when setting the pin state, it will look up the widget with the name pin in the sound card widgets list and then perform power on/off operations;
  • • mask: The type that the current jack pin can report, its value taken from enum snd_jack_type;
  • • invert: When setting the pin state, the default is 0: power off, 1: power on; if invert is set to non-zero, it means 1: power off, 0: power on;
  • • list: List node used to add this node to the ASoC Jack’s pins list;

4. ASoC Core API

4.1 snd_soc_card_jack_new_pins

The snd_soc_card_jack_new_pins function is defined in sound/soc/soc-card.c, used to create a jack with pins in the ASoC sound card. The function parameters include:

  • • card: Pointer to the ASoC sound card structure;
  • • id: A string used to identify this jack;
  • • type: A bitmask indicating the snd_jack_type values that can be detected by this jack;
  • • jack: Pointer to the ASoC jack structure;
  • • pins: The array of pins to be added to the ASoC jack, or NULL;
  • • num_pins: The number of elements in the pins array.

The function is defined as follows:

/**
 * snd_soc_card_jack_new_pins - Create a new jack with pins
 * @card:  ASoC card
 * @id:    an identifying string for this jack
 * @type:  a bitmask of enum snd_jack_type values that can be detected by
 *         this jack
 * @jack:  structure to use for the jack
 * @pins:  Array of jack pins to be added to the jack or NULL
 * @num_pins: Number of elements in the @pins array
 *
 * Creates a new jack object with pins. If not adding pins,
 * snd_soc_card_jack_new() should be used instead.
 *
 * Returns zero if successful, or a negative error code on failure.
 * On success jack will be initialised.
 */
int snd_soc_card_jack_new_pins(struct snd_soc_card *card, const char *id,
                               int type, struct snd_soc_jack *jack, // ASoC jack
                               struct snd_soc_jack_pin *pins,   // jack pins
                               unsigned int num_pins)      // number of jack pins
{
        int ret;

        ret = jack_new(card, id, type, jack, false);  // Create a new snd_jack instance, assign to jack->jack
        if (ret)
                goto end;

        if (num_pins)
                ret = snd_soc_jack_add_pins(jack, num_pins, pins);  // For each jack pin, create a new snd_jack_kctl and add it to jack->jack's kctl_list.
end:
        return soc_card_ret(card, ret);
}

The function internally first calls the jack_new function to create a new snd_jack instance, assigning it to jack->jack and associating it with the ASoC sound card device.

Then, if pins are provided, it calls snd_soc_jack_add_pins to create a new snd_jack_kctl for each jack pin and add it to jack->jack’s kctl_list.

4.1.1 jack_new

The jack_new is defined as follows:

static int jack_new(struct snd_soc_card *card, const char *id, int type,
                    struct snd_soc_jack *jack, bool initial_kctl)
{
        mutex_init(&jack->mutex);         // Initialize mutex lock
        jack->card = card;                // Set ASoC sound card device
        INIT_LIST_HEAD(&jack->pins);      // Initialize list node
        INIT_LIST_HEAD(&jack->jack_zones);  // Initialize list node
        BLOCKING_INIT_NOTIFIER_HEAD(&jack->notifier);

        return snd_jack_new(card->snd_card, id, type, &jack->jack, initial_kctl, false);  // Create an snd_jack device, assign to jack->jack
}
4.1.2 snd_soc_jack_add_pins

The snd_soc_jack_add_pins function is defined in sound/soc/soc-jack.c, used to create a new snd_jack_kctl for each jack pin and add it to the jack’s kctl_list, the function is defined as follows:

/**
 * snd_soc_jack_add_pins - Associate DAPM pins with an ASoC jack
 *
 * @jack:  ASoC jack created with snd_soc_card_jack_new_pins()
 * @count: Number of pins
 * @pins:  Array of pins
 *
 * After this function has been called the DAPM pins specified in the
 * pins array will have their status updated to reflect the current
 * state of the jack whenever the jack status is updated.
 */
int snd_soc_jack_add_pins(struct snd_soc_jack *jack, int count,
                          struct snd_soc_jack_pin *pins)
{
        int i;

        for (i = 0; i < count; i++) {  // Traverse jack pin
                if (!pins[i].pin) {
                        dev_err(jack->card->dev, "ASoC: No name for pin %d\n",
                                i);
                        return -EINVAL;
                }
                if (!pins[i].mask) {
                        dev_err(jack->card->dev, "ASoC: No mask for pin %d"
                                " (%s)\n", i, pins[i].pin);
                        return -EINVAL;
                }

                INIT_LIST_HEAD(&pins[i].list);  // Initialize list node
                list_add(&pins[i].list, &jack->pins); // Add the current jack pin node to jack->pins list
                snd_jack_add_new_kctl(jack->jack, pins[i].pin, pins[i].mask);  // Create a new snd_jack_kctl for each jack pin and add it to jack's kctl_list.
        }

        /* Update to reflect the last reported status; canned jack
         * implementations are likely to set their state before the
         * card has an opportunity to associate pins.
         */
        snd_soc_jack_report(jack, 0, 0);

        return 0;
}

The function first traverses the pins array and performs the following operations for each pin:

  • • Check if the name of the pin is empty; if empty, return an error;
  • • Check if the mask of the pin is empty; if empty, return an error;
  • • Initialize the list node pins[i].list and add it to the jack->pins list;
  • • Call snd_jack_add_new_kctl function to create a new snd_jack_kctl for this pin and add it to jack->jack’s kctl_list;
  • • Finally, the function calls snd_soc_jack_report function to reflect the last reported jack status;

4.2 snd_soc_jack_report

The snd_soc_jack_report function is defined in sound/soc/soc-jack.c, used to report the insertion/removal status of the jack, the function parameters include:

  • • jack: Pointer to the ASoC jack structure;
  • • status: Indicates the current detected insertion/removal status of the jack;
  • • mask: A bitmask composed of the enum snd_jack_type values, indicating the types that need to be reported;

For example, status = 0010 0010b, mask= 0000 0011, which means the 0th and 1st bits in status are valid, and all other bits are invalid;

The function is defined as follows:

/**
 * snd_soc_jack_report - Report the current status for a jack
 *
 * @jack:   the jack
 * @status: a bitmask of enum snd_jack_type values that are currently detected.
 * @mask:   a bitmask of enum snd_jack_type values that being reported.
 *
 * If configured using snd_soc_jack_add_pins() then the associated
 * DAPM pins will be enabled or disabled as appropriate and DAPM
 * synchronised.
 *
 * Note: This function uses mutexes and should be called from a
 * context which can sleep (such as a workqueue).
 */
void snd_soc_jack_report(struct snd_soc_jack *jack, int status, int mask)
{
        struct snd_soc_dapm_context *dapm;
        struct snd_soc_jack_pin *pin;
        unsigned int sync = 0;

        if (!jack)
                return;
        trace_snd_soc_jack_report(jack, mask, status);

        dapm = &jack->card->dapm;  // The dapm domain of the sound card device

        mutex_lock(&jack->mutex);   // Get mutex lock

        jack->status &= ~mask;          // Clear old status (mask points to the bits)
        jack->status |= status & mask;  // Set new status (mask points to the bits)

        trace_snd_soc_jack_notify(jack, status);

        list_for_each_entry(pin, &jack->pins, list) {     // Traverse each jack pin and manage power on/off
                int enable = pin->mask & jack->status;    // Get the status value of the current jack pin that can report, 1: power on 0: power off  
        
                if (pin->invert)                          // Power on/power off value is inverted
                        enable = !enable;

                if (enable)
                        snd_soc_dapm_enable_pin(dapm, pin->pin);  // Set the widget named pin->pin to connected state
                else
                        snd_soc_dapm_disable_pin(dapm, pin->pin); // Set the widget named pin->pin to disconnected state

                /* we need to sync for this case only */
                sync = 1;
        }

        /* Report before the DAPM sync to help users updating micbias status */
        blocking_notifier_call_chain(&jack->notifier, jack->status, jack);

        if (sync)
                snd_soc_dapm_sync(dapm);  // Unified processing of widgets whose connection status has changed, perform power on/off management

        snd_jack_report(jack->jack, jack->status);  // Report the current jack insertion/removal status through input_report_key/input_report_switch

        mutex_unlock(&jack->mutex);   // Release the mutex lock
}

The function mainly accomplishes the following two tasks:

(1) The function updates the status of the jack pins added to the jack pin array through snd_soc_jack_add_pins based on the jack insertion/removal status, specifically as follows:

  • • Calls snd_soc_dapm_enable_pin to find the widget with the name pin->pin in the sound card widgets list; if the widget connection status has changed, it will also add the widget to the sound card’s dapm_dirty list; and update the widget.connected field value; the snd_soc_dapm_enable_pin function will be introduced inRockchip RK3399 – DAPM Widget&Route&Path;
  • • Finally, calls snd_soc_dapm_sync to uniformly process the widgets whose connection status has changed, performing power on/off management (in fact, it operates registers to control their power status; of course, if there are no power control-related registers, nothing will be done);

(2) Calls snd_jack_report, which reports the current jack insertion/removal status through input_report_key/input_report_switch;

4.3 snd_soc_jack_add_gpios

Based on the above introduction, the following approach can be used to implement headphone insertion/removal detection based on the input event mechanism:

  • • Call snd_soc_card_jack_new_pins to create a jack object with pins;
  • • Use request irq to apply for headphone insertion/removal interrupts, and in the interrupt handler, determine whether the headphone is inserted or removed by detecting the rising/falling edge;
  • • Based on the judgment result, call snd_soc_jack_report to report EV_KEY, EV_SW events to the upper layer;

For the latter two steps, ASoC also provides a packaged function to achieve:

int snd_soc_jack_add_gpios(struct snd_soc_jack *jack, int count, struct snd_soc_jack_gpio *gpios)

This function applies for GPIO and the corresponding interrupts through standard GPIO drivers, and provides a unified interrupt handler to report events; this function is suitable for headphone interrupts connected to GPIO and where the GPIO driver is the standard Linux driver. The Machine driver registration introduced in the articleRockchip RK3399 – ASoC Machine Driver Basicswill call this function:

gpio.name        = "Headphone detection";
gpio.report      = SND_JACK_HEADPHONE;
gpio.desc        = desc;
gpio.debounce_time = 150;                           
snd_soc_jack_add_gpios(jack, 1, &gpio);

The function is defined in sound/soc/soc-jack.c;

/**
 * snd_soc_jack_add_gpios - Associate GPIO pins with an ASoC jack
 *
 * @jack:  ASoC jack
 * @count: number of pins
 * @gpios: array of gpio pins
 *
 * This function will request gpio, set data direction and request irq
 * for each gpio in the array.
 */
int snd_soc_jack_add_gpios(struct snd_soc_jack *jack, int count,
                        struct snd_soc_jack_gpio *gpios)
{
        int i, ret;
        struct jack_gpio_tbl *tbl;

        tbl = devres_alloc(jack_devres_free_gpios, sizeof(*tbl), GFP_KERNEL); // Dynamically allocate memory, data structure type is struct jack_gpio_tbl
        if (!tbl)
                return -ENOMEM;
        tbl->jack = jack;
tbl->count = count;
tbl->gpios = gpios;

        for (i = 0; i < count; i++) {  // Number of GPIOs
                if (!gpios[i].name) {
                        dev_err(jack->card->dev,
                                "ASoC: No name for gpio at index %d\n", i);
                        ret = -EINVAL;
                        goto undo;
                }

                if (gpios[i].desc) {   // If desc has already been initialized, it will go here; for example, in the Machine driver instance introduced later,
                                       // when calling the snd_soc_jack_add_gpios function, the headphone detection pin has already initialized the member desc
                        /* Already have a GPIO descriptor. */
                        goto got_gpio;
                } else if (gpios[i].gpiod_dev) {
                        /* Get a GPIO descriptor */
                        gpios[i].desc = gpiod_get_index(gpios[i].gpiod_dev,
                                                        gpios[i].name,
                                                        gpios[i].idx, GPIOD_IN);
                        if (IS_ERR(gpios[i].desc)) {
                                ret = PTR_ERR(gpios[i].desc);
                                dev_err(gpios[i].gpiod_dev,
                                        "ASoC: Cannot get gpio at index %d: %d",
                                        i, ret);
                                goto undo;
                        }
                } else {
                        /* legacy GPIO number */
                        if (!gpio_is_valid(gpios[i].gpio)) {
                                dev_err(jack->card->dev,
                                        "ASoC: Invalid gpio %d\n",
                                        gpios[i].gpio);
                                ret = -EINVAL;
                                goto undo;
                        }

                        ret = gpio_request_one(gpios[i].gpio, GPIOF_IN,
                                               gpios[i].name);
                        if (ret)
                                goto undo;

                        gpios[i].desc = gpio_to_desc(gpios[i].gpio);
                }
got_gpio:
                INIT_DELAYED_WORK(&gpios[i].work, gpio_work);  // Initialize delayed work, the work function is set to gpio_work
                gpios[i].jack = jack;

                ret = request_any_context_irq(gpiod_to_irq(gpios[i].desc),   // Convert GPIO descriptor to IRQ number and then apply for interrupts, rising/falling edge triggers
                                              gpio_handler,                  // The interrupt handler is gpio_handler 
                                              IRQF_TRIGGER_RISING |
                                              IRQF_TRIGGER_FALLING,
                                              gpios[i].name,  // "Headphone detection"
                                              &gpios[i]);
                if (ret < 0)
                        goto err;

                if (gpios[i].wake) {   // If this GPIO pin is enabled as a wake source
                        ret = irq_set_irq_wake(gpiod_to_irq(gpios[i].desc), 1);
                        if (ret != 0)
                                dev_err(jack->card->dev,
                                        "ASoC: Failed to mark GPIO at index %d as wake source: %d\n",
                                        i, ret);
                }

                /* Register PM notifier so we do not miss state transitions
                 * happening while system is asleep.
                 */
                gpios[i].pm_notifier.notifier_call = snd_soc_jack_pm_notifier;
                register_pm_notifier(&gpios[i].pm_notifier);

                /* Expose GPIO value over sysfs for diagnostic purposes */
                gpiod_export(gpios[i].desc, false);

                /* Update initial jack status */
                schedule_delayed_work(&gpios[i].work,
                                      msecs_to_jiffies(gpios[i].debounce_time)); // Submit work after debounce_time, handled by the default work queue and work thread
        }

        devres_add(jack->card->dev, tbl);
        return 0;

err:
        gpio_free(gpios[i].gpio);
undo:
        jack_free_gpios(jack, i, gpios);
        devres_free(tbl);

        return ret;
}
4.3.1 gpio_handler

Let’s take a look at the GPIO interrupt handler gpio_handler, which is relatively simple, mainly pushing the GPIO work delay to the work queue schedule_delayed_work for execution;

/* irq handler for gpio pin */
static irqreturn_t gpio_handler(int irq, void *data)
{
        struct snd_soc_jack_gpio *gpio = data;
        struct device *dev = gpio->jack->card->dev;

        trace_snd_soc_jack_irq(gpio->name);

        if (device_may_wakeup(dev))
                pm_wakeup_event(dev, gpio->debounce_time + 50);

        queue_delayed_work(system_power_efficient_wq, &gpio->work, // Similar to schedule_delayed_work, the difference is that queue_work submits the given work to the created work queue system_power_efficient_wq  
                                                                   // instead of the default queue
                              msecs_to_jiffies(gpio->debounce_time));

        return IRQ_HANDLED;
}
4.3.2 gpio_work

The GPIO work function is set to gpio_work, and this function calls snd_soc_jack_report to report the current GPIO insertion/removal status, for example, 1: indicates a device is inserted, 0: indicates no device is inserted;

/* gpio detect */
static void snd_soc_jack_gpio_detect(struct snd_soc_jack_gpio *gpio)
{
        struct snd_soc_jack *jack = gpio->jack;
        int enable;
        int report;

        enable = gpiod_get_value_cansleep(gpio->desc);  // Get the GPIO value
        if (gpio->invert)   // Whether to invert; here, 0 is passed
                enable = !enable;

        if (enable)        // Device inserted
                report = gpio->report;  // SND_JACK_HEADPHONE
        else               // Device removed
                report = 0;

        if (gpio->jack_status_check)
                report = gpio->jack_status_check(gpio->data);  // Check the GPIO status

        snd_soc_jack_report(jack, report, gpio->report);      // Report the current GPIO insertion/removal status
}  

/* gpio work */
static void gpio_work(struct work_struct *work)
{
        struct snd_soc_jack_gpio *gpio;

        gpio = container_of(work, struct snd_soc_jack_gpio, work.work);
        snd_soc_jack_gpio_detect(gpio);
}

References

[1] RK3399 Exploration Journey / Audio Driver Layer Quick Read

[2] Linux ALSA Audio Driver Part 1: Framework Overview

[3] Linux Audio Subsystem

[4] http://www.alsa-project.org/。

[5] Linux ALSA Driver Part 2: The Sound Card Creation Process

[6] Linked Lists in the Linux Kernel – struct list_head

[7] Linux ALSA Audio System: Physical Link Part

[8] Sound Subsystem Documentation

[9] Linux ALSA Part 2: ALSA Sound Cards and Devices

[10] Audio Headphone (Part 3) SoC-Jack

[11] Adding Headphone Insertion Detection Function to wm8524 on Android 10 Platform and Analysis

Leave a Comment