[alsa-devel] Audio DMA buffer layout
Hi,
I'm working with a custom sound chip with its own DMA hardware. The figure shows how the I2S DMA expects data to be prepared:
IRQ1 +----------------+ | page_3 (right) |\ +----------------+ > double buffer 1 | page_2 (left) |/ IRQ0 +----------------+ | page_1 (right) |\ +----------------+ > double buffer 0 | page_0 (left) |/ +----------------+
The hardware has 2 double buffers, each containing left and right sample regions, i.e. it alternately transmits samples from page_0 and page_1, i.e. double buffer 0, then generates an interrupt and starts transmitting page_2 and page_3. After that another interrupt is generated and the process begins again with double buffer 0.
So my question is, how to tell ALSA about this memory layout? Is it possible to operate in MMAP mode with SNDRV_PCM_INFO_NONINTERLEAVED? But then, how to tell the application that it should either fill double buffer 0 or 1? Or should I use RW mode and copy samples manually to another buffer? Or something else? SG-DMA? page()-callback?
Best regards, Markus Korber
At Tue, 08 May 2007 13:47:47 +0200, Markus Korber wrote:
Hi,
I'm working with a custom sound chip with its own DMA hardware. The figure shows how the I2S DMA expects data to be prepared:
IRQ1 +----------------+ | page_3 (right) |\ +----------------+ > double buffer 1 | page_2 (left) |/ IRQ0 +----------------+ | page_1 (right) |\ +----------------+ > double buffer 0 | page_0 (left) |/ +----------------+
The hardware has 2 double buffers, each containing left and right sample regions, i.e. it alternately transmits samples from page_0 and page_1, i.e. double buffer 0, then generates an interrupt and starts transmitting page_2 and page_3. After that another interrupt is generated and the process begins again with double buffer 0.
So my question is, how to tell ALSA about this memory layout? Is it possible to operate in MMAP mode with SNDRV_PCM_INFO_NONINTERLEAVED? But then, how to tell the application that it should either fill double buffer 0 or 1? Or should I use RW mode and copy samples manually to another buffer? Or something else? SG-DMA? page()-callback?
If I understand correctly, the samples for a channel (e.g. left) is practically split to two regions, page 0 and page 2 in the above? If yes, this h/w design is hard to implement mmap mode straightforwardly because the alsa-lib (and apps) assume "linear" buffers. For normal read/write without mmap, this shouldn't be that hard as you guess, though.
A workaround would be to use SG-buffer (if possible). The constraint is thta both double buffers above have to be aligned in PAGE_SIZE (depending on architecture, 4KB mostly). For implementing a SG buffer, you can use either mmap or page pcm_ops. The mmap is to do the whole mmap operation, so you can do all what you'd like. The page is a callback used by the default ALSA mmap handler and you can return the page pointer corresponding to the given buffer offset.
Takashi
Thus spake Takashi Iwai:
First of all, thanks for your answer. However,
At Tue, 08 May 2007 13:47:47 +0200, Markus Korber wrote:
IRQ1 +----------------+ | page_3 (right) |\ +----------------+ > double buffer 1 | page_2 (left) |/ IRQ0 +----------------+ | page_1 (right) |\ +----------------+ > double buffer 0 | page_0 (left) |/ +----------------+
If I understand correctly, the samples for a channel (e.g. left) is practically split to two regions, page 0 and page 2 in the above?
Correct.
If yes, this h/w design is hard to implement mmap mode straightforwardly because the alsa-lib (and apps) assume "linear" buffers. For normal read/write without mmap, this shouldn't be that hard as you guess, though.
Are there any drawbacks if I use the R/W mode without mmap, regarding applications (mplayer), dmix, etc.?
Furthermore, in mmap mode, I think I could use an intermediate buffer (with the layout as above), where I copy the data from the (linear) mmap'd buffer to, could I? Maybe using helpers from struct pcm-indirect.h? But this is not very beautiful.
A workaround would be to use SG-buffer (if possible). The constraint is thta both double buffers above have to be aligned in PAGE_SIZE (depending on architecture, 4KB mostly).
It's a custom chip with an ARM core and an AMBA-AHB bus. PAGE_SIZE alignment wouldn't be a problem, although then buffer_size would be fixed to multiples of PAGE_SIZE, i guess?
I'm not quite sure how to implement this:
chip_pcm_new - snd_pcm_lib_preallocate_pages_for_all(pcm, SNDRV_DMA_TYPE_CONTINUOUS, snd_dma_continuous_data(GFP_KERNEL), PAGE_SIZE, PAGE_SIZE)
chip_hw_params - snd_pcm_lib_malloc_pages(substream, params_buffer_bytes(hw_params)) - map sg-pages with snd_pcm_sgbuf_get_addr()
Why/How?
I mean, my hardware DMA layout is fixed (apart from the double buffer page sizes, which should be a multiple of PAGE_SIZE). So do I have to call snd_pcm_lib_malloc_pages() and/or do I need to call dma_map_sg() with a scatterlist? Or how do I tell ALSA about my DMA layout? And how is the application informed about this?
For implementing a SG buffer, you can use either mmap or page pcm_ops. The mmap is to do the whole mmap operation, so you can do all what you'd like.
My understanding is, that the application mmap's my allocated buffer (currently allocated with dma_alloc_writecombine) and the ALSA library tells it, where it is allowed to put its data. (ALSA gets this info through the pointer() callback and calls to snd_pcm_period_elapsed.) Thus, this mmap() callback is only called once in normal mmap mode.
Now, if I would use a SG approach, how would I handle this mmap operation in my callback? I guess, I have to provide similar functionality as in dma_mmap() but with 2 different base addresses?
The page is a callback used by the default ALSA mmap handler and you can return the page pointer corresponding to the given buffer offset.
This approach looks easier to me. Am I right, that here I would just call snd_pcm_sgbuf_ops_page() from my page() callback handler? What else do I need to do in this case to let the application know which buffer it should fill?
Regards, Markus Korber
At Thu, 10 May 2007 12:52:20 +0200, Markus Korber wrote:
Thus spake Takashi Iwai:
First of all, thanks for your answer. However,
At Tue, 08 May 2007 13:47:47 +0200, Markus Korber wrote:
IRQ1 +----------------+ | page_3 (right) |\ +----------------+ > double buffer 1 | page_2 (left) |/ IRQ0 +----------------+ | page_1 (right) |\ +----------------+ > double buffer 0 | page_0 (left) |/ +----------------+
If I understand correctly, the samples for a channel (e.g. left) is practically split to two regions, page 0 and page 2 in the above?
Correct.
If yes, this h/w design is hard to implement mmap mode straightforwardly because the alsa-lib (and apps) assume "linear" buffers. For normal read/write without mmap, this shouldn't be that hard as you guess, though.
Are there any drawbacks if I use the R/W mode without mmap, regarding applications (mplayer), dmix, etc.?
Exactly. mplayer shouldn't be a problem, but dmix won't work without mmap.
Furthermore, in mmap mode, I think I could use an intermediate buffer (with the layout as above), where I copy the data from the (linear) mmap'd buffer to, could I? Maybe using helpers from struct pcm-indirect.h? But this is not very beautiful.
Yeah, it's possible but not sexy. That's what I meant "straightforwardly".
However, if the dmix is the main reason to support mmap, it would end up with the intermediate buffer because dmix doesn't work well with two-periods.
Thus the following might be not worth to consider. Anyhow...
A workaround would be to use SG-buffer (if possible). The constraint is thta both double buffers above have to be aligned in PAGE_SIZE (depending on architecture, 4KB mostly).
It's a custom chip with an ARM core and an AMBA-AHB bus. PAGE_SIZE alignment wouldn't be a problem, although then buffer_size would be fixed to multiples of PAGE_SIZE, i guess?
Yes.
I'm not quite sure how to implement this:
chip_pcm_new - snd_pcm_lib_preallocate_pages_for_all(pcm, SNDRV_DMA_TYPE_CONTINUOUS, snd_dma_continuous_data(GFP_KERNEL), PAGE_SIZE, PAGE_SIZE)
chip_hw_params - snd_pcm_lib_malloc_pages(substream, params_buffer_bytes(hw_params)) - map sg-pages with snd_pcm_sgbuf_get_addr()
Why/How?
I mean, my hardware DMA layout is fixed (apart from the double buffer page sizes, which should be a multiple of PAGE_SIZE). So do I have to call snd_pcm_lib_malloc_pages() and/or do I need to call dma_map_sg() with a scatterlist? Or how do I tell ALSA about my DMA layout? And how is the application informed about this?
Does the hardware support the "real" SG buffers? That is, you can compose a buffer from the different memory chunks, or do you need a linear buffer but split to two inside?
In the latter case, I'd do like the following: - preallocate the buffer as TYPE_CONTINUOUS - Use snd_pcm_lib_malloc_pages() in hw_params callback as if it's a normal linear buffer. - Then linearize the virtual buffer via vmap() in hw_params. (It has to be vfree'd in the corresponding hw_free or succeeding call of hw_params.) - Implement page PCM ops for mmap, too, so that it can map correctly. Or implement mmap PCM ops, especially for ARM, and co. In this case, map two regions by dma_mmap_cohernt() call. - This would require a constraint to align period_bytes PAGE_SIZE. snd_pcm_hw_constraint_step() can be used for this purpose.
Takashi
Thus spake Takashi Iwai:
At Thu, 10 May 2007 12:52:20 +0200, Markus Korber wrote:
Thus spake Takashi Iwai:
At Tue, 08 May 2007 13:47:47 +0200, Markus Korber wrote:
IRQ1 +----------------+ | page_3 (right) |\ +----------------+ > double buffer 1 | page_2 (left) |/ IRQ0 +----------------+ | page_1 (right) |\ +----------------+ > double buffer 0 | page_0 (left) |/ +----------------+
If I understand correctly, the samples for a channel (e.g. left) is practically split to two regions, page 0 and page 2 in the above?
Correct.
Furthermore, in mmap mode, I think I could use an intermediate buffer (with the layout as above), where I copy the data from the (linear) mmap'd buffer to, could I? Maybe using helpers from struct pcm-indirect.h? But this is not very beautiful.
Yeah, it's possible but not sexy. That's what I meant "straightforwardly".
Thanks again for your answer. I'm trying to implement the solution with the intermediate buffer first, however, since it seems easier to me :)
Driver settings are as follows:
I use 2* in buffer_bytes_max because I copy the data from the first half of the buffer (where the application writes to) into the second half with the layout as shown above (this is my intermediate buffer where the hardware works upon). So the intermediate buffer lies right at the end of the ALSA buffer.
,---- | static const struct snd_pcm_hardware chip_pcm_hardware = { | .info = SNDRV_PCM_INFO_MMAP | | SNDRV_PCM_INFO_MMAP_VALID | | SNDRV_PCM_INFO_BLOCK_TRANSFER | | SNDRV_PCM_INFO_PAUSE | | SNDRV_PCM_INFO_RESUME | | SNDRV_PCM_INFO_NONINTERLEAVED, | ... | .period_bytes_min = 8*1024, | .period_bytes_max = 8*1024, | .periods_min = 2, | .periods_max = 2, | .buffer_bytes_max = 2*2*8*1024, | } `----
And here some driver fragments:
· bufnum refers to the active (i.e. currently transmitted) double buffer 0 or 1 as defined above. · Buffer is allocated with dma_alloc_writecombine(). Does this make sense? · prtd->period_bytes = params_period_bytes(params)
So here I copy the provided source data to either double buffer 0 or 1. ,---- | static void chip_playback_copy(struct snd_pcm_substream *substream, | struct snd_pcm_indirect *rec, size_t bytes) | { | unsigned char *src = substream->runtime->dma_area + rec->sw_data; | ... | DBG("Entered %s (%d -> src: %#08x, bytes: %#04x, data_l: %#04x)\n", | __FUNCTION__, bufnum, src, bytes, *(u16*)src); | ... | dst = bufnum == 0 ? bus_to_virt(prtd->i2s_buffer_1) : | bus_to_virt(prtd->i2s_buffer_0); | memcpy(dst, src, bytes/2); | } `----
Here I setup the hardware buffer size to be the same as the software buffer size (0x4000). ,---- | static int chip_pcm_prepare(struct snd_pcm_substream *substream) | { | ... | chip->playback.hw_buffer_size = prtd->period_bytes * 2; /* byte size */ | chip->playback.sw_buffer_size = snd_pcm_lib_buffer_bytes(substream); | chip->playback_hw_ptr = 0; | ... | } `----
The IRQ is generated at each double buffer boundary. Thus I just set the playback_hw_ptr to either 0 or hw_buffer_size/2. ,---- | static void chip_pcm_dma_irq(u32 tx_intstat, u32 rx_intstat, | struct snd_pcm_substream *substream) | { | ... | if (bufnum == 0) | prtd->playback_hw_ptr = 0; | else | prtd->playback_hw_ptr = prtd->period_bytes; | ... | if (substream) | snd_pcm_period_elapsed(substream); | | } `---- ,---- | static snd_pcm_uframes_t | chip_pcm_pointer(struct snd_pcm_substream *substream) | { | return snd_pcm_indirect_playback_pointer(substream, &chip->playback, | prtd->playback_hw_ptr); | } `----
The logging output from the chip_playback_copy() function shows this: 'aplay -M -D hw:0,0 /usr/share/sounds/alsa/test.wav'
(1) chip_pcm_trigger (command 0x1) (2) chip_playback_copy (0 -> src: 0xffd00000, bytes: 0x4000, data_l: 0xff9a) (3) chip_i2s_trigger (command 0x1) (4) chip_playback_copy (1 -> src: 0xffd00000, bytes: 0x2000, data_l: 0x183) (5) chip_playback_copy (0 -> src: 0xffd02000, bytes: 0x2000, data_l: 0x9c6) (6) chip_playback_copy (1 -> src: 0xffd00000, bytes: 0x2000, data_l: 0xd719) (7) chip_playback_copy (0 -> src: 0xffd02000, bytes: 0x2000, data_l: 0xdd1d)
My questions are:
(1) I receive 2 copy calls (lines 4+5), where line 4 gives me 0x2000 bytes of left channel samples and line 5 0x2000 right channel samples.
Shouldn't I receive 0x1000 bytes of left AND right samples at each copy call and not either left or right samples from pcm-indirect?
(2) How can I prevent the usage of bus_to_virt() in chip_playback_copy, if I just want to copy samples from one physical address to another?
Finally, if I use SNDRV_PCM_INFO_INTERLEAVED and manually partition left and right samples into the corresponding double buffers, everything works fine.
Thanks for any hints.
Regards, Markus Korber
participants (2)
-
Markus Korber
-
Takashi Iwai