Reputation: 111
I am trying to use a DMA engine on a Zynq-7000 based platform to transfer a PCM stream to a custom I2S controller in the Zynq PL. My I2S controller interfaces to an external amp. I want to use DMA through an AXI-DMA Controller. This is currently my datapath:
I am using a Linux 4.10 kernel on the Zynq PS. I use Linux ASoC subsystem to generate pcm streams and control my external audio amp. I have 512MB of DDR RAM connected to Zynq. I would like to use a section of this RAM to run my DMA engine. My I2S controller runs off an AXI-Lite control interface and uses an AXI4-Stream interface for audio streaming. This IP has been tested and can be assumed to work well with these interfaces.
In the past, I have used the PL330 in the Zynq PS to drive the DMA engine. My I2S controller used to have a FIFO built into it's AXI-Lite register space, so all DMA transfers went through AXI-Lite interface. I simply pointed the DMA engine to this FIFO address like so:
struct axi_i2s {
struct snd_dmaengine_dai_dma_data playback_dma_data;
struct snd_dmaengine_dai_dma_data capture_dma_data;
};
static int axi_i2s_dai_probe(struct snd_soc_dai *dai)
{
struct axi_i2s *i2s = snd_soc_dai_get_drvdata(dai);
snd_soc_dai_init_dma_data(dai, &i2s->playback_dma_data,
&i2s->capture_dma_data);
return 0;
}
static struct snd_soc_dai_driver axi_i2s_dai = {
.probe = axi_i2s_dai_probe,
.playback = {
.channels_min = 1,
.channels_max = 8,
.rates = I2S_RATES,
.formats = SNDRV_PCM_FMTBIT_S16_LE |
SNDRV_PCM_FMTBIT_S24_LE |
SNDRV_PCM_FMTBIT_S32_LE,
},
};
static int axi_i2s_probe(struct platform_device *pdev)
{
axi_i2s *i2s;
i2s = devm_kzalloc(&pdev->dev, sizeof(*i2s), GFP_KERNEL);
if (!i2s)
return -ENOMEM;
platform_set_drvdata(pdev, i2s);
i2s->playback_dma_data.addr = I2S_BASE_ADDRESS + TX_FIFO_OFFSET;
i2s->playback_dma_data.addr_width = 4;
i2s->playback_dma_data.maxburst = 1;
i2s->capture_dma_data.addr = I2S_BASE_ADDRESS + RX_FIFO_OFFSET;
i2s->capture_dma_data.addr_width = 4;
i2s->capture_dma_data.maxburst = 1;
devm_snd_dmaengine_pcm_register(&pdev->dev, NULL, 0);
return 0;
}
Devicetree:
dmac_s: dmac@f8003000 {
compatible = "arm,pl330", "arm,primecell";
reg = <0xf8003000 0x1000>;
interrupt-parent = <&intc>;
interrupt-names = "abort", "dma0", "dma1", "dma2", "dma3",
"dma4", "dma5", "dma6", "dma7";
interrupts = <0 13 4>,
<0 14 4>, <0 15 4>,
<0 16 4>, <0 17 4>,
<0 40 4>, <0 41 4>,
<0 42 4>, <0 43 4>;
#dma-cells = <1>;
#dma-channels = <8>;
#dma-requests = <4>;
clocks = <&clkc 27>;
clock-names = "apb_pclk";
};
axi_i2s@0x43C00000 {
#sound-dai-cells = <1>;
compatible = "my,driver";
reg = <0x43C00000 0x10000>;
clocks = <&clkc 15>;
clock-names = "axi";
dmas = <&dmac_s 0>, <&dmac_s 1>;
dma-names = "tx", "rx";
xlnx,dma-type = <0x1>;
};
New set up:
/* AXI DMA */
axi_dma_0: axidma@40400000 {
compatible = "xlnx,axi-dma-1.00.a";
#dma-cells = <1>;
reg = < 0x40400000 0x10000 >;
xlnx,addrwidth = <0x20>;
clocks = <&clkc 15>, <&clkc 15>, <&clkc 15>, <&clkc 15>;
clock-names = "s_axi_lite_aclk", "m_axi_sg_aclk", "m_axi_mm2s_aclk", "m_axi_s2mm_aclk";
interrupt-parent = <&intc>;
interrupts = < 0 33 4 0 34 4>;
dma-ranges = <0x00000000 0x00000000 0x20000000>;
//xlnx,include-sg ;
dma-channel@40400000 {
compatible = "xlnx,axi-dma-mm2s-channel";
dma-channels = <0x1>;
interrupts = < 0 33 4 >;
xlnx,datawidth = <0x20>;
xlnx,device-id = <0x0>;
//xlnx,include-dre ;
} ;
dma-channel@40400030 {
compatible = "xlnx,axi-dma-s2mm-channel";
dma-channels = <0x1>;
interrupts = < 0 34 4 >;
xlnx,datawidth = <0x20>;
xlnx,device-id = <0x0>;
//xlnx,include-dre ;
} ;
};
/* New stream version */
axi_i2s@0x43C10000 {
#sound-dai-cells = <1>;
compatible = "my,driver";
reg = <0x43C10000 0x10000>;
clocks = <&clkc 15>;
clock-names = "axi";
dmas = <&axi_dma_0 0
&axi_dma_0 1>;
dma-names = "axidma0", "axidma1";
xlnx,dma-type = <0x1>;
};
Obviously, some details are left out, but these are the relevant bits.
Now, I can't quite figure out how to change this driver to DMA using the AXI-DMA IP instead of the PL330. Since the DMA transfers will be done in a different memory region without a FIFO, how do I set up the snd_dmaengine_dai_dma_data structs to write to the AXI-DMA memory? Specifically this section:
i2s->playback_dma_data.addr = I2S_BASE_ADDRESS + TX_FIFO_OFFSET;
i2s->playback_dma_data.addr_width = 4;
i2s->playback_dma_data.maxburst = 1;
i2s->capture_dma_data.addr = I2S_BASE_ADDRESS + RX_FIFO_OFFSET;
i2s->capture_dma_data.addr_width = 4;
i2s->capture_dma_data.maxburst = 1;
The AXI-DMA IP has access to all 512MB of my DDR, but I don't know where the kernel will allocate memory for my DMA transfers.
Upvotes: 3
Views: 1957
Reputation: 334
You will need a linux kernel driver to allocate memory which will be used as a DMA buffer. If you need a large continuous buffer you will need to enable CMA in your linux kernel. In your driver you can use kmalloc to allocate the memory.
As a reference for the kernel driver i would suggest using udmabuf (https://github.com/ikwzm/udmabuf).
The udmabuf shows up as a device in /sys/class/udmabuf and the physical address of the DMA buffer can be read in userspace. Pass this address to your AXI DMA buffer as the target region to push data to.
More info on DMA in linux on Zynq: https://forums.xilinx.com/xlnx/attachments/xlnx/ELINUX/10658/1/drivers-session4-dma-4public.pdf
EDIT: You can find an example linux driver for AXI DMA here https://forums.xilinx.com/xlnx/attachments/xlnx/ELINUX/10658/2/axidma.c.golden
Upvotes: 1