Qualcomm 8155 audio data from HAL to DSP

Posted by WakeAngel on Thu, 09 Dec 2021 17:31:17 +0100

From the perspective of data flow, sort out the process of Android platform audio data from HAL layer to DSP.
With multimedia22 -- > QUIN_ TDM_ RX_ 0 playback as an example.
It mainly focuses on writing pcm data to dsp and informing dsp of front and rear routing information.

<!-- more -->

[Platform: Qualcomm 8155 gvmq Android 11]
[Kernel:msm-5.4]
Code reference codeaurora Download as follows

repo init -u https://source.codeaurora.org/quic/la/platform/manifest.git -b release -m LA.AU.1.3.2.r2-02600-sa8155_gvmq.0.xml --depth 1
repo sync -c kernel/msm-5.4 platform/vendor/opensource/audio-kernel platform/hardware/qcom/audio platform/external/tinyalsa

After reading this article, you'd better understand ALSA or ASOC(ALSA System on Chip). You can see the relevant documents

<<Kernel Didr>>/Documentation/sound/soc/overview.rst

And the documents in the directory, or search on the Internet, there are a lot of materials.
In short, the Linux ASOC architecture proposes a set of advanced and power-saving (DAPM) Audio Architecture for the purpose of XXX. Of course, there are brand-new playing methods and many terms. From the perspective of driving, there are three important parts:

  • Codec partial driver
    Codec is related to the codec chip, such as its Mixer, control, DAI interface, A/D,D/A, etc. this part is only the general part of codec and does not contain any platform or machine related code, so as to run on any architecture and machine.
  • Platform partial drive
    Including audio DMA, digital audio interface (DAI) drivers (such as I2S, AC97, PCM) and DSP drivers (some Qualcomm documents take out the DSP driver separately, which is equivalent to CPU driver). This part is only for soc cpu and shall not contain specific board level related codes, that is, it is also independent of the machine.
  • Machine partial drive
    Codec and platform have nothing to do with the machine. They are two relatively independent parts. Who binds them together? This task falls on the machine, which describes and binds these components and instantiates them as sound cards, including codec and platform specific related code. It can handle any machine specific controls (GPIO, interrupts, clocking, jacks, voltage, etc.) and machine level audio events (for example, turn on the speaker/hp amplifier at the beginning of playback).

From the perspective of data flow, there are two important concepts:

  • FE-DAI
    Front end Dai, front-end, visible to the user space, is a pcm device, which can be routed to the back-end through mixer operation, connected to the back-end, and can be routed to multiple back-end DAIs.
  • BE-DAI
    Back end Dai, the back end, is invisible to user space and can be routed to multiple front-end DAIs.

There will be a routing table for the front-end and back-end routable, which specifies which routable can be connected.

When referring to BE and FE DAI, we have to say that Dynamic PCM is a concept. See the document < < kernel didr > > / documentation / sound / SOC / DPCM Rst, the following figure is also from this document,

  | Front End PCMs    |  SoC DSP  | Back End DAIs | Audio devices |
  
                      *************
  PCM0 <------------> *           * <----DAI0-----> Codec Headset
                      *           *
  PCM1 <------------> *           * <----DAI1-----> Codec Speakers
                      *   DSP     *
  PCM2 <------------> *           * <----DAI2-----> MODEM
                      *           *
  PCM3 <------------> *           * <----DAI3-----> BT
                      *           *
                      *           * <----DAI4-----> DMIC
                      *           *
                      *           * <----DAI5-----> FM
                      *************

The figure above shows the audio schematic diagram of smart phone, which supports Headset, Speakers, MODEM, Bluetooth, digital microphone and FM. It defines 4 front-end pcm devices and 6 back-end DAIs.
Each front-end PCM can route data to one or more back ends. For example, PCM0 data can be sent to DAI3 BT, DAI1 speaker and DAI3 BT at the same time.
Multiple front ends can also be routed to one back end at the same time. For example, pcm0 and PCM1 all send data to DAI0.
It should be noted that the back-end DAI and peripherals usually have a one-to-one correspondence, that is, a back-end DAI represents a peripheral; The front-end pcm and HAL layer use case s usually correspond to each other.

Qualcomm platform adsp driver in order to realize these, the software is divided into ASM ADM AFE

  • ASM
    Stream management can be simply understood as a part of Fe operation (FE data finally interacts with dsp by q6asm sending apr packets), and also includes the processing of audio streams, such as sound effects.
  • ADM
    Device management also includes routing management, that is, which streams are written to which devices, and there is device level audio processing (such as common sound effect processing after mixing multiple streams).
  • AFE
    It can BE simply understood as the end operation part of BE, and the name acquisition is confusing. Operation of DSP equipment, such as clock, pll, etc.

HAL layer operation

8155 qcom audio HAL has been moved to vendor / qcom / opensource / audio Hal / primary Hal and is no longer located in the hardware directory.
The HAL layer has a lot of logic processing, routing enabling and closing, and also considers various use cases, acdb information, etc. a lot of codes are dizzy for us to analyze the data flow. Fortunately, we can play through the tinymixer and tinyplay command lines. Watching the tinyplay playback process can greatly simplify the difficulty of analyzing the code, but before playing, We have to use tinymixer to control the channel and open the whole link before we can write data.

Here I choose a rarely used usecase_ AUDIO_ PLAYBACK_ REAR_ Seat (rear seat playback) to perform command line operations.

By looking up the code, the front and rear end routing path s corresponding to the reference design are as follows, and the BE used is QUIN_TDM_RX_0

vendor/qcom/opensource/audio-hal/primary-hal/configs/msmnile_au/mixer_paths_adp.xml
<path name="rear-seat-playback">
    <ctl name="QUIN_TDM_RX_0 Channels" value="Sixteen" />
    <ctl name="QUIN_TDM_RX_0 Audio Mixer MultiMedia22" value="1" />
</path>

The pcm device number corresponding to the use case is 54 (strictly speaking, pcm 54 corresponding to multimedia 22)

msm8974/platform.h
#define REAR_SEAT_PCM_DEVICE 54

msm8974/platform.c
static int pcm_device_table[AUDIO_USECASE_MAX][2] = {
...// pcm device 54 corresponding to use case rear seat
    [USECASE_AUDIO_PLAYBACK_REAR_SEAT] = {REAR_SEAT_PCM_DEVICE,
                                          REAR_SEAT_PCM_DEVICE},

So finally, we can use the command line to do the following operations for playback

tinymix "QUIN_TDM_RX_0 Channels" "Six" # Set the number of channels
tinymix "QUIN_TDM_RX_0 Audio Mixer MultiMedia22" "1" # dpcm, connect the front and rear ends
tinyplay /data/a.wav -D 0 -d 54 # Using sound card 0, the 54th pcm device plays

The first command sets the number of channel s, which is not the focus of this article and is ignored;
The second command sets the dpcm route and connects the front and rear ends;
The third command is played through the 54th device of sound card 0. In fact, it is to write data to the kernel through the / dev/snd/pcmC0D54p node.

tinyplay playback process is very simple, which is sorted as follows:

external/tinyalsa/tinyplay.c
main()
  + Parameter analysis
  + play_sample()
      + pcm_open()
      |   + snprintf(fn, sizeof(fn), "/dev/snd/pcmC%uD%u%c", card, device,
      |   |          flags & PCM_IN ? 'c' : 'p');
      |   + pcm->fd = open(fn, O_RDWR|O_NONBLOCK); // Open / dev/snd equipment
      |   | 
      |   + ioctl(pcm->fd, SNDRV_PCM_IOCTL_HW_PARAMS, &params)
      |   + ioctl(pcm->fd, SNDRV_PCM_IOCTL_SW_PARAMS, &sparams)
      |
      + do { pcm_write() } while()
      |   + // pcm_write()
      |   + if (!pcm->running) {
      |   |   pcm_prepare(pcm); // ioctl(pcm->fd, SNDRV_PCM_IOCTL_PREPARE)
      |   |   ioctl(pcm->fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, &x)
      |   |   return 0;
      |   | }
      |   |
      |   + // Write data through ioctl
      +   + ioctl(pcm->fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, &x)

For this article, we mainly focus on three points:

  • pcm_open() open the device and set the software and hardware parameters;
  • pcm_prepare();
  • pcm_write() write data after preparation;

Of course, these three functions interact with the kernel mainly through ioctl().

Intuitively, we follow the analysis of pcm_write() can know the data flow direction, but before analyzing the function, we have to make clear which device in the kernel the write is going to?

Viewing pcm device information

pcm device information can be viewed with the following commands

# View pcm device information
$ cat /proc/asound/pcm
00-00: MultiMedia1 (*) :  : playback 1 : capture 1
00-01: MultiMedia2 (*) :  : playback 1 : capture 1
...
00-54: MultiMedia22 (*) :  : playback 1 : capture 1
...

As in the PCM 54 of our example above, the ID is MultiMedia22 and supports one playback and one recording.

The details of the PCM equipment can also be viewed with the following command

# View pcm54 capture information
$ cat /proc/asound/card0/pcm54c/info
card: 0
device: 54
subdevice: 0
stream: CAPTURE
id: MultiMedia22 (*)
name: 
subname: subdevice #0
class: 0
subclass: 0
subdevices_count: 1
subdevices_avail: 1

Before continuing to analyze which device to write data to, we have to ask
Which is 54 in the kernel? Why is multimedia 22 54 instead of 55 or 53?

BE

pcm device creation

The device number 54 is actually the 54th device in the dai link (note that it is in the dai link rather than the dai definition), which is the 54th device registered according to the dai link information when the sound card is registered,
Therefore, if there is a pcm added by yourself, it's best to add it to the back, or just change the corresponding relationship of these equipment numbers.

kernel/msm-5.4/techpack/audio/asoc/sa8155.c
static struct snd_soc_dai_link msm_common_dai_links[] = {
    /* FrontEnd DAI Links */
    ...
    {
        .name = MSM_DAILINK_NAME(Media22), // dai_link name, expanded to "SA8155 Media22"
        .stream_name = "MultiMedia22",
        .dynamic = 1, // Dynamic routing
#if IS_ENABLED(CONFIG_AUDIO_QGKI)
        .async_ops = ASYNC_DPCM_SND_SOC_PREPARE,
#endif /* CONFIG_AUDIO_QGKI */
        .dpcm_playback = 1, // Playback support dpcm
        .dpcm_capture = 1,
        .trigger = {SND_SOC_DPCM_TRIGGER_POST,
            SND_SOC_DPCM_TRIGGER_POST},
        .ignore_suspend = 1,
        .ignore_pmdown_time = 1,
        .id = MSM_FRONTEND_DAI_MULTIMEDIA22,
        SND_SOC_DAILINK_REG(multimedia22), // Macro, defining cpu codec platform
    },

msm_dailink.h
SND_SOC_DAILINK_DEFS(multimedia22,
    // cpu component name, soc_bind_dai_link() when Binding_ node, dai_ Name, wait and see SND_ soc_ find_ Of of dai()_ Node, Dai, and msm_populate_dai_link_component_of_node() pair of_node processing
    // The driver is in MSM Dai Fe c
    DAILINK_COMP_ARRAY(COMP_CPU("MultiMedia22")),
    // codec component is dummy because of dynamic pcm
    DAILINK_COMP_ARRAY(COMP_CODEC("snd-soc-dummy", "snd-soc-dummy-dai")),
    // The platform component corresponding to multimedia 22 is driven in msm-pcm-q6-v2 c
    DAILINK_COMP_ARRAY(COMP_PLATFORM("msm-pcm-dsp.0")));

By the way, we can take a look at the corresponding dai definition, which defines the information of playback/capture, such as name, supported sampling rate, format and supported channel
And some operations ops and probe functions of the dai

kernel/msm-5.4/techpack/audio/asoc/msm-dai-fe.c
static struct snd_soc_dai_driver msm_fe_dais[] = {
  ...
  {
    .playback = {
      .stream_name = "MultiMedia22 Playback",
      .aif_name = "MM_DL22",
      .rates = (SNDRV_PCM_RATE_8000_384000 |
          SNDRV_PCM_RATE_KNOT),
      .formats = (SNDRV_PCM_FMTBIT_S16_LE |
            SNDRV_PCM_FMTBIT_S24_LE |
            SNDRV_PCM_FMTBIT_S24_3LE |
            SNDRV_PCM_FMTBIT_S32_LE),
      .channels_min = 1,
      .channels_max = 32,
      .rate_min = 8000,
      .rate_max = 384000,
    },
    .capture = {
      .stream_name = "MultiMedia22 Capture",
      .aif_name = "MM_UL22",
      .rates = (SNDRV_PCM_RATE_8000_48000|
          SNDRV_PCM_RATE_KNOT),
      .formats = (SNDRV_PCM_FMTBIT_S16_LE |
            SNDRV_PCM_FMTBIT_S24_LE |
            SNDRV_PCM_FMTBIT_S24_3LE |
            SNDRV_PCM_FMTBIT_S32_LE),
      .channels_min = 1,
      .channels_max = 32,
      .rate_min =     8000,
      .rate_max =     48000,
    },
    .ops = &msm_fe_Multimedia_dai_ops, // At present, there is only the startup method
    .name = "MultiMedia22", // The name of the cpu component is the same as that in dai link
    .probe = fe_dai_probe,
  },

These dai links will send the dai links information to the sound card - > Dai when the machine drives the probe_ Link, when the sound card is registered, the corresponding pcm device will be created according to this information,

// machine driven probe
msm_asoc_machine_probe() / sa8155.c
  + populate_snd_card_dailinks(&pdev->dev) // dai links information
  + msm_populate_dai_link_component_of_node() // Assign value of according to dai link_ Node, if found, CPUs - > Dai_ name = NULL;  platforms->name = NULL;
  + devm_snd_soc_register_card() // Register sound card


static struct snd_soc_card *populate_snd_card_dailinks(struct device *dev)
{...
    if (!strcmp(match->data, "adp_star_codec")) {
        card = &snd_soc_card_auto_msm;
        ...
        memcpy(msm_auto_dai_links,
               msm_common_dai_links, // Multimedia 22 ranks 54th among these dai links
               sizeof(msm_common_dai_links));
    ...
        dailink = msm_auto_dai_links;
    }
    ...     // dai link to the sound card_ link
        card->dai_link = dailink;
        card->num_links = total_links;
    ...
}

The sound card registration process is very long. Although there are no major changes in recent versions, it may be changed in the future. We mainly focus on the PCM device creation process.

Here is a brief list of the sound card registration process. If you are interested, you can see it. For details, you can find some articles on the Internet.

Sound card registration process
devm_snd_soc_register_card()
+ snd_soc_register_card()
  + snd_soc_bind_card()
    + snd_soc_instantiate_card()
      + for_each_card_links(card, dai_link) {
      |   soc_bind_dai_link() // Bind dai link
      |     + snd_soc_find_dai(dai_link->cpus); // cpus dai match, match of first_ node
      |     |   + strcmp(..., dlc->dai_name) // Then if dai_name is not empty. Compare component driver name with Dai_ CPU in link_ dai_name
      |     + for_each_link_codecs(dai_link, i, codec) // codec dai matching
      |     + for_each_link_platforms(dai_link, i, platform) // platform dai matching
      |     |
      |     + soc_add_pcm_runtime() // Add RTD - > list to card - > RTD_ In the list,
      |        + rtd->num = card->num_rtd; // Equipment number, the num is 54 in our example
      |        + card->num_rtd++; // Sound card runtime example + 1
      + }
      + snd_card_register()
      | + snd_device_register_all()
      |   + list_for_each_entry(dev, &card->devices, list) {
      |   |   __snd_device_register()
      |   |     + dev->ops->dev_register(dev); // Traverse registered devices
      +   + }

In the above code, we can first focus on RTD - > num, which is the pcm device number 54 in our example.

The final device registration is to call dev - > Ops - > dev_ Register (DEV) is registered, so which method is this?
Different devices have different registration methods. These may be useful for future viewing.

Device driver filedev_register method
rawmidi.csnd_rawmidi_dev_register()
seq_device.csnd_seq_device_dev_register()
jack.csnd_jack_dev_register()
hwdep.csnd_hwdep_dev_register()
pcm.csnd_pcm_dev_register()
compress_offload.csnd_compress_dev_register()
timer.csnd_timer_dev_register()
control.csnd_ctl_dev_register()
ac97_codec.csnd_ac97_dev_register()

For PCM equipment, its definition and calling process are as follows, which can be skipped and directly to the next step snd_pcm_dev_register()

# technological process
kernel/msm-5.4/sound/core/pcm.c
snd_soc_instantiate_card()
  for_each_card_rtds(card, rtd)
    soc_link_init(card, rtd);
      + soc_new_pcm()
          + snd_pcm_new()
              + _snd_pcm_new() // Create two streams of pcm and add pcm devices to card - > devices list


# dev_register definition
static int _snd_pcm_new(struct snd_card *card, const char *id, int device,
   int playback_count, int capture_count, bool internal,
   struct snd_pcm **rpcm)
{...
    static struct snd_device_ops ops = {
        .dev_free = snd_pcm_dev_free,
        .dev_register =    snd_pcm_dev_register,
        .dev_disconnect = snd_pcm_dev_disconnect,
    };
    ...
    // Information creation of playback / recording stream and its sub streams, currently playback_count capture_count is 1. See SOC for details_ new_ PCM () rule
    // Assign stream information to SND_ pcm pcm->streams[stream];
    err = snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_PLAYBACK,
                 playback_count);
    ...
    err = snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_CAPTURE, capture_count);
};

int snd_pcm_new_stream(struct snd_pcm *pcm, int stream, int substream_count)
{...
    // The stream name, as in our example, plays pcmC0D54p, and PCM - > device is the device number, as in 54 of our example
    dev_set_name(&pstr->dev, "pcmC%iD%i%c", pcm->card->number, pcm->device,
             stream == SNDRV_PCM_STREAM_PLAYBACK ? 'p' : 'c');
    ... Subflow information, omitted
    for (idx = 0, prev = NULL; idx < substream_count; idx++) {

_ snd_pcm_new() only creates the playback / recording stream and its sub stream information (for example, our example name is pcmC0D54p), and then adds the PCM device to the sound card devices list, without creating a device node,
The real device node is snd_pcm_dev_register(),

static int snd_pcm_dev_register(struct snd_device *device)
{...
    // cid stands for SNDRV_PCM_STREAM_PLAYBACK SNDRV_PCM_STREAM_CAPTURE
    for (cidx = 0; cidx < 2; cidx++) {
        ...// Registering pcm devices
        /* register pcm */
        err = snd_register_device(devtype, pcm->card, pcm->device,
                      &snd_pcm_f_ops[cidx], pcm,
                      &pcm->streams[cidx].dev);
sound/core/sound.c
int snd_register_device(int type, struct snd_card *card, int dev,
            const struct file_operations *f_ops,
            void *private_data, struct device *device)
{...
    // Find idle minor
    minor = snd_find_free_minor(type, card, dev);
    ...// Register device node
    err = device_add(device);
    ...
}

snd_register_device() by calling device_add() creates the device node, that is

/dev/snd/pcmC0D54p

After that, we can pass
pcm_write() --> ioctl(pcm->fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, &x)
Data has been written to the front PCM device

PCM open

When we know which device to write data to, we should intuitively continue to analyze pcm_write() looks at the write process,
However, in general, some important data structures will be initialized when open ing, so in this section, the points needing attention can be written, or you can skip looking at the writing process directly.

The process of open is as follows:

sound card
--> Play stream
  --> pcm subflow 
    --> dpcm front end dai
      --> All back-end components open
        --> All front-end components open (according to fe dai, codec Components, cpu Component sequence)

Corresponding code

chrdev_open()
+ snd_open()
  + file->f_op->open() // snd_pcm_f_ops see Note 1 
    + snd_pcm_playback_open()
      + snd_pcm_open()
        + snd_pcm_open_file()
          + snd_pcm_open_substream()
            + substream->ops->open // dpcm_ fe_ dai_ See Note 2
              + dpcm_fe_dai_startup()
                + dpcm_be_dai_startup() // BE component open
                | + soc_pcm_open() // Same as below, open and omit
                |
                + soc_pcm_open() // Here is the FE assembly open
                  + soc_pcm_components_open()
                    + for_each_rtdcom(rtd, rtdcom) 
                        snd_soc_component_open(component, substream);
                        + component->driver->ops->open(substream) // Open Fe, Dai, codec and cpu components

# Note 1
// pcm playback recording file_operations
const struct file_operations snd_pcm_f_ops[2] = {
  {
    .owner =    THIS_MODULE,
    .write =    snd_pcm_write,
    .write_iter =   snd_pcm_writev,
    .open =     snd_pcm_playback_open,
    .release =    snd_pcm_release,
    .llseek =   no_llseek,
    .poll =     snd_pcm_poll,
    .unlocked_ioctl = snd_pcm_ioctl,
    .compat_ioctl =   snd_pcm_ioctl_compat,
    .mmap =     snd_pcm_mmap,
    .fasync =   snd_pcm_fasync,
    .get_unmapped_area =  snd_pcm_get_unmapped_area,
  },
  {
    .owner =    THIS_MODULE,
    .read =     snd_pcm_read,
    .read_iter =    snd_pcm_readv,
    .open =     snd_pcm_capture_open,
    .release =    snd_pcm_release,
    .llseek =   no_llseek,
    .poll =     snd_pcm_poll,
    .unlocked_ioctl = snd_pcm_ioctl,
    .compat_ioctl =   snd_pcm_ioctl_compat,
    .mmap =     snd_pcm_mmap,
    .fasync =   snd_pcm_fasync,
    .get_unmapped_area =  snd_pcm_get_unmapped_area,
  }
};


# Note 2 substream - > Ops

substream->ops When the sound card is registered soc_new_pcm() --> snd_pcm_set_ops(), 
Based on runtime flow rtd Whether to adopt dynamic pcm Assigned
soc_new_pcm()
  + snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &rtd->ops);
      + struct snd_pcm_str *stream = &pcm->streams[direction];
      | for (substream = stream->substream; substream != NULL; substream = substream->next)
      +   substream->ops = ops; // That is RTD - > ops

It is defined as follows
/* create a new pcm */
int soc_new_pcm(struct snd_soc_pcm_runtime *rtd, int num)
{
  ...// If dynamic pcm is used, its method
  /* ASoC PCM operations */
  if (rtd->dai_link->dynamic) {
    rtd->ops.open   = dpcm_fe_dai_open;
    rtd->ops.hw_params  = dpcm_fe_dai_hw_params;
    rtd->ops.prepare  = dpcm_fe_dai_prepare;
    rtd->ops.trigger  = dpcm_fe_dai_trigger;
    rtd->ops.hw_free  = dpcm_fe_dai_hw_free;
    rtd->ops.close    = dpcm_fe_dai_close;
    rtd->ops.pointer  = soc_pcm_pointer;
    rtd->ops.ioctl    = snd_soc_pcm_component_ioctl;
    ...
  } else {// dpcm is not used
    rtd->ops.open   = soc_pcm_open;
    rtd->ops.hw_params  = soc_pcm_hw_params;
    rtd->ops.prepare  = soc_pcm_prepare;
    rtd->ops.trigger  = soc_pcm_trigger;
    rtd->ops.hw_free  = soc_pcm_hw_free;
    rtd->ops.close    = soc_pcm_close;
    ...
  }


At the same time, pay attention copy_user Assignment, which will be used later
for_each_rtdcom(rtd, rtdcom) {
  const struct snd_pcm_ops *ops = rtdcom->component->driver->ops;
  ....
  if (ops->copy_user)
    rtd->ops.copy_user  = snd_soc_pcm_component_copy_user;
  if (ops->page)
    rtd->ops.page   = snd_soc_pcm_component_page;
  if (ops->mmap)
    rtd->ops.mmap   = snd_soc_pcm_component_mmap;
}
Attention:
Pay attention copy_user  = snd_soc_pcm_component_copy_user, It will be used when writing data later. I won't talk about it later

For the opening of BE, see the following chapters,
When all front-end components are opened, there is no open operation in the dai definition, and the codec component is dummy, so we only look at the open of the cpu component.
For our example, the driver is in msm-pcm-q6-v2 C (for voip/voice/compress or other drivers, it is not extended here)

Its open function (msm_pcm_open()) is mainly through q6asm_audio_client_alloc() for audio_client's application,
The information that interacts with dsp is basically stored here, q6asm_audio_client_alloc() mainly performs session application and session registration.

msm_pcm_open() / msm-pcm-q6-v2.c
+ prtd->audio_client = q6asm_audio_client_alloc(
|                       (app_cb)event_handler, prtd);
| + q6asm_audio_client_alloc() / kernel/msm-5.4/techpack/audio/dsp/q6asm.c
|   + n = q6asm_session_alloc(ac);
|   | + for (n = 1; n <= ASM_ACTIVE_STREAMS_ALLOWED; n++) {
|   |       if (!(session[n].ac)) { // Find idle session s
|   |           session[n].ac = ac;q6
|   + ac->cb = cb; // Incoming callback, event callback processing
|   + rc = q6asm_session_register(ac);
+     +  apr_register("ADSP", "ASM",...)

For a session application, it is mainly to find a free session from session[ASM_ACTIVE_STREAMS_ALLOWED+1]. The number of sessions allowed for audio client is [1,15], and 0 is reserved; In addition, we see a common_client whose id is ASM_CONTROL_SESSION, used for all session memory mapping calibrations.
A session in dsp corresponds to the concept of port. Both session and port have fixed conversion formulas. Session and port are determined

kernel/msm-5.4/techpack/audio/include/dsp/q6asm-v2.h
/* Control session is used for mapping calibration memory */
  #define ASM_CONTROL_SESSION    (ASM_ACTIVE_STREAMS_ALLOWED + 1)

For session registration, it mainly calls apr_register() registers the information, and then sends it to the audio client's apr. when there is apr information to be processed, it passes q6asm_callback callback to further call audio client callback processing

static int q6asm_session_register(struct audio_client *ac)
{
    ac->apr = apr_register("ADSP", "ASM",
            (apr_fn)q6asm_callback,
            ((ac->session) << 8 | 0x0001),
            ac);
    ...
    ac->apr2 = apr_register("ADSP", "ASM",
            (apr_fn)q6asm_callback,
            ((ac->session) << 8 | 0x0002),
            ac);
    ...
    // Runtime session apr handle, rtac_asm_apr_data[session_id].apr_handle = handle;
    rtac_set_asm_handle(ac->session, ac->apr);

    pr_debug("%s: Registering the common port with APR\n", __func__);
    ac->mmap_apr = q6asm_mmap_apr_reg(); // Also call apr_register

apr_register in apr_vm.c is implemented in apr.c_ vm. C is used for 8155 hypervisor scheme, that is, one chip runs Android + instrument QNX scheme at the same time. You can see the apr.c of Android.
apr_register is mainly used to fill in apr_svc information, such as dest_id,client_id and so on. In addition to confirming whether chanel is turned on, it seems that there is no additional information exchange with dsp. Which port does the session data write to and how do you tell dsp? Let's continue to look at the writing process.

apr (Asynchronous Packet Router), For and Qualcomm dsp For interaction, it has its own set of protocols. In short, it is nothing more than packet header and load information.

Write write data to dsp

User space write data through PCM_ write() --> ioctl(pcm->fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, &x)

The kernel alsa layer process code is listed as follows

kernel/msm-5.4/sound/core/pcm_native.c
snd_pcm_common_ioctl()
+ case SNDRV_PCM_IOCTL_WRITEI_FRAMES:
  case SNDRV_PCM_IOCTL_READI_FRAMES:
    + return snd_pcm_xferi_frames_ioctl(substream, arg);
      + copy_from_user(&xferi, _xferi, sizeof(xferi)) // frames and buf address information
      + snd_pcm_lib_write(substream, xferi.buf, xferi.frames)
        + __snd_pcm_lib_xfer(substream, (void __force *)buf, true, frames, false);
          + writer(...transfer); // transfer is substream - > Ops - > copy_ user
            + interleaved_copy()
              + transfer(substream, 0, hwoff, data + off, frames);
                + substream->ops->copy_user()

For our example, substream - > Ops - > copy_ User is defined in the following file

kernel/msm-5.4/techpack/audio/asoc/msm-pcm-q6-v2.c
static const struct snd_pcm_ops msm_pcm_ops = {
  .open           = msm_pcm_open,
  .copy_user  = msm_pcm_copy,

msm_pcm_copy() calls different functions according to read / write, and write is msm_pcm_playback_copy(), the main process is:

  • Check for available CPUs_ buf;
  • Copy the user space data to the buffer, that is, the audio client's port [dir] - [buf [IDX] In data;
  • The port information and data address information corresponding to the session are sent to dsp through apr.
msm_pcm_copy()
+ msm_pcm_playback_copy()
  + while ((fbytes > 0) && (retries < MAX_PB_COPY_RETRIES)) {
  |   + data = q6asm_is_cpu_buf_avail(IN, prtd->audio_client, &size, // Is there a CPU available_ buf
  |   + copy_from_user(bufptr, buf, xfer) // Copy from user space to bufptr, bufptr = data;
  |   |
  |   + q6asm_write(prtd->audio_client, xfer,
  |   |   |               0, 0, NO_TIMESTAMP);
  |   |   + q6asm_add_hdr()
  |   |   | + __q6asm_add_hdr()
  |   |   |   + hdr->src_port = ((ac->session << 8) & 0xFF00) | (stream_id);
  |   |   |   + hdr->dest_port = ((ac->session << 8) & 0xFF00) | (stream_id);
  |   |   |
  |   |   + write.hdr.opcode = ASM_DATA_CMD_WRITE_V2;
  |   |   + write.buf_addr_lsw = lower_32_bits(ab->phys); // The current port address of audio client, that is, the address of valid data
  |   +   + apr_send_pkt(ac->apr, (uint32_t *) &write);
  + }
Tips:
1.about cpu buf Application can be viewed q6asm_audio_client_buf_alloc_contiguous(), adopt msm_audio_ion_alloc()Application, here also involves and dsp Address mapping q6asm_memory_map_regions()
2.port[dir] dir by IN/OUT,Is aimed at dsp See, play is IN, Recording is OUT. 

So far, the process of writing pcm data to dsp is completed, that is, the front-end process is completed,
The data is sent to dsp for processing. This is a black box. There is a source code to analyze the next process. What we can do is to specify which BE the data is output from through its interface. Next, let's look at the relevant contents of the back end.

BE

In the above analysis, we know that the pcm stream applies for a free session, and finally sends the data to the DSP through the apr packet,
However, the hardware output interface of DSP has five groups of TDMS for 8155 platform, and each group of TDMS also has RX0, RX1 and other functions. How can our pcm stream data tell DSP which TDM and which function to output?

Before announcing the answer, let's take a look at BE dai link and related definitions (which can be skipped).

dai link and definition

// dai link
static struct snd_soc_dai_link msm_common_be_dai_links[] = {
    /* Backend AFE DAI Links */
    ...
    {
        .name = LPASS_BE_QUIN_TDM_RX_0, // "QUIN_TDM_RX_0"
        .stream_name = "Quinary TDM0 Playback",
        .no_pcm = 1,
        .dpcm_playback = 1,
        .id = MSM_BACKEND_DAI_QUIN_TDM_RX_0,
        .be_hw_params_fixup = msm_tdm_be_hw_params_fixup,
        .ops = &sa8155_tdm_be_ops,
        .ignore_suspend = 1,
        .ignore_pmdown_time = 1,
        SND_SOC_DAILINK_REG(quin_tdm_rx_0),
    },

// quin_tdm_rx_0 definition
SND_SOC_DAILINK_DEFS(quin_tdm_rx_0,
    DAILINK_COMP_ARRAY(COMP_CPU("msm-dai-q6-tdm.36928")), // cpu component msm-dai-q6-v2 c
    DAILINK_COMP_ARRAY(COMP_CODEC("msm-stub-codec.1", "msm-stub-rx")),
    DAILINK_COMP_ARRAY(COMP_PLATFORM("msm-pcm-routing"))); // platform component msm-pcm-routing-v2 c

The cpu component "msm-dai-q6-tdm.36928" 36928 corresponds to AFE_PORT_ID_QUINARY_TDM_RX, i.e. 0x9040

// 36928 -> AFE_PORT_ID_QUINARY_TDM_RX
kernel/msm-5.4/techpack/audio/include/dsp/apr_audio-v2.h

/* Start of the range of port IDs for TDM devices. */
#define AFE_PORT_ID_TDM_PORT_RANGE_START    0x9000

#define AFE_PORT_ID_QUINARY_TDM_RX \
    (AFE_PORT_ID_TDM_PORT_RANGE_START + 0x40)

It has only one Dai, comp_ The Dai corresponding to the CPU ("msm-dai-q6-tdm. 36928") is,

static struct snd_soc_dai_driver msm_dai_q6_tdm_dai[] = {...
    {
        .playback = {
            .stream_name = "Quinary TDM0 Playback",
            .aif_name = "QUIN_TDM_RX_0",
            .rates = SNDRV_PCM_RATE_48000 | SNDRV_PCM_RATE_8000 |
                SNDRV_PCM_RATE_16000 | SNDRV_PCM_RATE_48000 |
                SNDRV_PCM_RATE_176400 | SNDRV_PCM_RATE_352800,
            .formats = SNDRV_PCM_FMTBIT_S16_LE |
                   SNDRV_PCM_FMTBIT_S24_LE |
                   SNDRV_PCM_FMTBIT_S32_LE,
            .channels_min = 1,
            .channels_max = 16,
            .rate_min = 8000,
            .rate_max = 352800,
        },
        .name = "QUIN_TDM_RX_0",
        .ops = &msm_dai_q6_tdm_ops, // prepare hw_params set_tdm_slot set_sysclk et al
        .id = AFE_PORT_ID_QUINARY_TDM_RX,
        .probe = msm_dai_q6_dai_tdm_probe,
        .remove = msm_dai_q6_dai_tdm_remove,
    },

Front and rear end connection

The above episode describes some things defined by the back-end dai, where you need to check the code in the future.

Back to the cpu side, how to tell the dsp which pcm stream (corresponding session) to write to which device.

In the chapter of HAL layer operation, we use the command line to play, and only
`
tinymix "QUIN_TDM_RX_0 Audio Mixer MultiMedia22" "1"
`

After connecting the front and rear ends, I guess the information will be told to the dsp here. Is this the case? Let's continue:

tinymix is actually controlled through the sound card / dev/snd/controlC0 (0 means sound card 0),

QUIN_TDM_RX_0 Audio Mixer related information is as follows:,

kernel/msm-5.4/techpack/audio/asoc/msm-pcm-routing-v2.c

static const struct snd_soc_dapm_widget msm_qdsp6_widgets_tdm[] = {...
    SND_SOC_DAPM_MIXER("QUIN_TDM_RX_0 Audio Mixer", SND_SOC_NOPM, 0, 0,
                quin_tdm_rx_0_mixer_controls,
                ARRAY_SIZE(quin_tdm_rx_0_mixer_controls)),

static const struct snd_kcontrol_new quin_tdm_rx_0_mixer_controls[] = {...
    SOC_DOUBLE_EXT("MultiMedia22", SND_SOC_NOPM,
    MSM_BACKEND_DAI_QUIN_TDM_RX_0, // be dai, shift_left, .shift
    // fe dai, shift_right, .rshift
    MSM_FRONTEND_DAI_MULTIMEDIA22, 1, 0, msm_routing_get_audio_mixer,
    msm_routing_put_audio_mixer),

That is, when "quin_tdm_rx_0 audio mixer multimedia 22" is set, MSM will be called_ routing_ put_ audio_ Mixer()

static int msm_routing_put_audio_mixer(struct snd_kcontrol *kcontrol,
            struct snd_ctl_elem_value *ucontrol)
{...
    // Set to 1
    if (ucontrol->value.integer.value[0] &&
       msm_pcm_routing_route_is_set(mc->shift, mc->rshift) == false) {
        // Routing processing   
        msm_pcm_routing_process_audio(mc->shift, mc->rshift, 1);
        // dapm update power status
        snd_soc_dapm_mixer_update_power(widget->dapm, kcontrol, 1,
            update);
    ...
}

msm_bedais and fe_dai_map records the front and rear information,
msm_ pcm_ routing_ process_ In audio (), if the back-end dai is active and the front-end stream ID (i.e. the session of audio_client) is valid,
Through adm_matrix_map() sends the session information to DSP as an apr attachment. After receiving the information, DSP knows which device the pcm stream should be written to.

reg -> be dai, val -> fe dai, set -> 0/1
msm_pcm_routing_process_audio(u16 reg, u16 val, int set)
+ if (set) {
  + fdai = &fe_dai_map[val][session_type];
  | // The back-end Dai is active and the front-end session is not - 1
  + if (msm_bedais[reg].active && fdai->strm_id !=
    |       INVALID_SESSION) {
    | // devices enabling
    + copp_idx = adm_open(port_id, ..., acdb_dev_id,
    | + // kernel/msm-5.4/techpack/audio/dsp/q6adm.c
    | + ellipsis... open_v8.hdr.dest_svc = APR_SVC_ADM;
    |
    | // Update routing information
    + msm_pcm_routing_build_matrix(val, ...);
    | + int port_id = get_port_id(msm_bedais[i].port_id);
    | + payload.port_id[num_copps] = port_id; // payload.port_id [] is the back end
    | |
    | // ** fe_ dai_ Strm found in map_ ID, that is, the session of the audio client corresponding to the pcm stream**
    | + payload.session_id = fe_dai_map[fedai_id][sess_type].strm_id;
    | + adm_matrix_map(fedai_id, path_type, payload, perf_mode, passthr_mode);
    |   + // kernel/msm-5.4/techpack/audio/dsp/q6adm.c
    |   + route_set_opcode_matrix_id(&route, path, passthr_mode);
    |   |  + case ADM_PATH_PLAYBACK:
    |   |      route->hdr.opcode = ADM_CMD_MATRIX_MAP_ROUTINGS_V5; // Update routing matrix opcode
    |   |
    |   | session Update to matrix_map,As apr The package is attached and sent
    |   + node->session_id = payload_map.session_id;
    |   + ret = apr_send_pkt(this_adm.apr, (uint32_t *)matrix_map);
    + }

When we use tinymix to connect the front and rear ends, we do not perform any pcm open/write operation, so strm_ The ID is not assigned. The above code will be executed only after the route update is played.

At which stage do we update the routing information for the first time?
The answer is in the prepare phase.
Take a look at Adm_ matrix_ Dump of map()_ stack():

dump_stack+0xb8/0x114
adm_matrix_map+0x58/0x5c4 [q6_dlkm]
msm_pcm_routing_reg_phy_stream+0x7c0/0x8f8 [platform_dlkm]
msm_pcm_playback_prepare+0x2ec/0x48c [platform_dlkm]
msm_pcm_prepare+0x20/0x3c [platform_dlkm]
snd_soc_component_prepare+0x44/0x80
soc_pcm_prepare+0xa0/0x28c
dpcm_fe_dai_prepare+0x110/0x2f4
snd_pcm_do_prepare+0x40/0xfc
snd_pcm_action_single.llvm.4985077898353288322+0x70/0x168
snd_pcm_common_ioctl+0x1030/0x1320
snd_pcm_ioctl_compat+0x234/0x3b4
__arm64_compat_sys_ioctl+0x10c/0x41c

In addition, snd_soc_dapm_mixer_update_power(), compress playback setting hw parameter stage or other scenarios will also update the routing information.

summary

For the HAL layer, all you have to do is play,
First, set the route, connect the front and back ends,
When pcm is open, an audio will be found in session []_ Session is an idle session, which corresponds to the pcm front-end stream,
When preparing, the back-end routing information corresponding to the front-end session will be sent to the DSP,
Then, when PCM writes data, the front end sends the data to DSP through q6asm, and DSP will output the data to the back end according to the routing information.