[alsa-devel] [PATCH 0/2] ASoC: stm32: Add I2S driver
This patch-set handles the SPI/I2S IP on STM32 platforms.
It applies to STM32 platforms implementing version 2 of SPI/I2S IP like STM32H7 series.
The SPI/I2S block is a serial peripheral interface (SPI), which can also be configured to work on I2S/PCM mode. ASoC STM32 I2S driver only supports this I2S/PCM mode.
The I2S/PCM interface can either support full duplex or half-duplex communication (transmitter or receiver only).
I2S driver exhibits 3 DAIs corresponding to a playback, capture or full duplex PCM device. When the interface is configured as full duplex, the related PCM device must be opened twice. The PCM streams are started when both playback and capture devices are opened. The relevant DAI can be selected through Device Tree using sound-dai-cells parameter.
The I2S/PCM interface supports four audio standards: I2S Philips standard, MSB justified standard, LSB justified standard, PCM standard PCM standard is declined in two versions, PCM short and long, according frame synchronization duration.
These standards can be mapped to ASoC standards as follows: - I2S: i2s - MSB justified standard: left_j - LSB justified standard: right_j - PCM short: dsp_a PCM long format is not exposed as it does not match supported ASoC standards.
olivier moysan (2): dt-bindings: Document STM32 I2S bindings ASoC: stm32: Add I2S driver
.../devicetree/bindings/sound/st,stm32h7-i2s.txt | 71 ++ sound/soc/Kconfig | 1 + sound/soc/Makefile | 1 + sound/soc/stm/Kconfig | 8 + sound/soc/stm/Makefile | 2 + sound/soc/stm/stm32_i2s.c | 1069 ++++++++++++++++++++ 6 files changed, 1152 insertions(+) create mode 100644 Documentation/devicetree/bindings/sound/st,stm32h7-i2s.txt create mode 100644 sound/soc/stm/Kconfig create mode 100644 sound/soc/stm/Makefile create mode 100644 sound/soc/stm/stm32_i2s.c
Add documentation of device tree bindings for STM32 SPI/I2S.
Signed-off-by: olivier moysan olivier.moysan@st.com --- .../devicetree/bindings/sound/st,stm32h7-i2s.txt | 71 ++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 Documentation/devicetree/bindings/sound/st,stm32h7-i2s.txt
diff --git a/Documentation/devicetree/bindings/sound/st,stm32h7-i2s.txt b/Documentation/devicetree/bindings/sound/st,stm32h7-i2s.txt new file mode 100644 index 0000000..b99467a --- /dev/null +++ b/Documentation/devicetree/bindings/sound/st,stm32h7-i2s.txt @@ -0,0 +1,71 @@ +STMicroelectronics STM32 SPI/I2S Controller + +The SPI/I2S block supports I2S/PCM protocols when configured on I2S mode. +Only some SPI instances support I2S. + +Required properties: + - compatible: Must be "st,stm32h7-i2s" + - #sound-dai-cells: Must be 1. (one parameter) + This parameter allows to specify CPU DAI index in soundcard CPU dai link. + index 0: playback DAI + index 1: capture DAI + index 2: full duplex DAI + - reg: Offset and length of the device's register set. + - interrupts: Must contain the interrupt line id. + - clocks: Must contain phandle and clock specifier pairs for each entry + in clock-names. + - clock-names: Must contain "i2sclk", "pclk", "x8k" and "x11k". + "i2sclk": clock which feeds the internal clock generator + "pclk": clock which feeds the peripheral bus interface + "x8k": I2S parent clock for sampling rates multiple of 8kHz. + "x11k": I2S parent clock for sampling rates multiple of 11.025kHz. + - dmas: DMA specifiers for tx and rx dma. + See Documentation/devicetree/bindings/dma/stm32-dma.txt. + - dma-names: Identifier for each DMA request line. Must be "tx" and "rx". + - pinctrl-names: should contain only value "default" + - pinctrl-0: see Documentation/devicetree/bindings/pinctrl/pinctrl-stm32.txt + +Optional properties: + - resets: Reference to a reset controller asserting the reset controller + +Example: +sound_card { + compatible = "audio-graph-card"; + dais = <&i2s2_port 0>; +}; + +i2s2: audio-controller@40003800 { + compatible = "st,stm32h7-i2s"; + #sound-dai-cells = <1>; + reg = <0x40003800 0x400>; + interrupts = <36>; + clocks = <&rcc PCLK1>, <&rcc SPI2_CK>, <&rcc PLL1_Q>, <&rcc PLL2_P>; + clock-names = "pclk", "i2sclk", "x8k", "x11k"; + dmas = <&dmamux2 2 39 0x400 0x1>, + <&dmamux2 3 40 0x400 0x1>; + dma-names = "rx", "tx"; + pinctrl-names = "default"; + pinctrl-0 = <&pinctrl_i2s2>; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + i2s2_port: port@0 { + reg = <0>; + cpu_endpoint: endpoint { + remote-endpoint = <&codec_endpoint>; + audio-graph-card,format = "i2s"; + audio-graph-card,bitclock-master = <&codec_endpoint>; + audio-graph-card,frame-master = <&codec_endpoint>; + }; + }; +}; + +audio-codec { + codec_port: port { + codec_endpoint: endpoint { + remote-endpoint = <&cpu_endpoint>; + }; + }; +};
On Thu, Apr 06, 2017 at 05:40:35PM +0200, olivier moysan wrote:
Add documentation of device tree bindings for STM32 SPI/I2S.
Signed-off-by: olivier moysan olivier.moysan@st.com
.../devicetree/bindings/sound/st,stm32h7-i2s.txt | 71 ++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 Documentation/devicetree/bindings/sound/st,stm32h7-i2s.txt
diff --git a/Documentation/devicetree/bindings/sound/st,stm32h7-i2s.txt b/Documentation/devicetree/bindings/sound/st,stm32h7-i2s.txt new file mode 100644 index 0000000..b99467a --- /dev/null +++ b/Documentation/devicetree/bindings/sound/st,stm32h7-i2s.txt @@ -0,0 +1,71 @@ +STMicroelectronics STM32 SPI/I2S Controller
+The SPI/I2S block supports I2S/PCM protocols when configured on I2S mode. +Only some SPI instances support I2S.
+Required properties:
- compatible: Must be "st,stm32h7-i2s"
- #sound-dai-cells: Must be 1. (one parameter)
- This parameter allows to specify CPU DAI index in soundcard CPU dai link.
- index 0: playback DAI
- index 1: capture DAI
- index 2: full duplex DAI
Is this still needed for graph-card?
- reg: Offset and length of the device's register set.
- interrupts: Must contain the interrupt line id.
- clocks: Must contain phandle and clock specifier pairs for each entry
- in clock-names.
- clock-names: Must contain "i2sclk", "pclk", "x8k" and "x11k".
- "i2sclk": clock which feeds the internal clock generator
- "pclk": clock which feeds the peripheral bus interface
- "x8k": I2S parent clock for sampling rates multiple of 8kHz.
- "x11k": I2S parent clock for sampling rates multiple of 11.025kHz.
- dmas: DMA specifiers for tx and rx dma.
- See Documentation/devicetree/bindings/dma/stm32-dma.txt.
- dma-names: Identifier for each DMA request line. Must be "tx" and "rx".
- pinctrl-names: should contain only value "default"
- pinctrl-0: see Documentation/devicetree/bindings/pinctrl/pinctrl-stm32.txt
+Optional properties:
- resets: Reference to a reset controller asserting the reset controller
+Example: +sound_card {
- compatible = "audio-graph-card";
- dais = <&i2s2_port 0>;
+};
+i2s2: audio-controller@40003800 {
- compatible = "st,stm32h7-i2s";
- #sound-dai-cells = <1>;
- reg = <0x40003800 0x400>;
- interrupts = <36>;
- clocks = <&rcc PCLK1>, <&rcc SPI2_CK>, <&rcc PLL1_Q>, <&rcc PLL2_P>;
- clock-names = "pclk", "i2sclk", "x8k", "x11k";
- dmas = <&dmamux2 2 39 0x400 0x1>,
<&dmamux2 3 40 0x400 0x1>;
- dma-names = "rx", "tx";
- pinctrl-names = "default";
- pinctrl-0 = <&pinctrl_i2s2>;
- ports {
#address-cells = <1>;
#size-cells = <0>;
i2s2_port: port@0 {
reg = <0>;
cpu_endpoint: endpoint {
remote-endpoint = <&codec_endpoint>;
audio-graph-card,format = "i2s";
audio-graph-card,bitclock-master = <&codec_endpoint>;
audio-graph-card,frame-master = <&codec_endpoint>;
The 'audio-graph-card,' part has been dropped.
};
};
+};
+audio-codec {
codec_port: port {
codec_endpoint: endpoint {
remote-endpoint = <&cpu_endpoint>;
};
- };
+};
1.9.1
On Mon, Apr 10, 2017 at 02:48:32PM -0500, Rob Herring wrote:
On Thu, Apr 06, 2017 at 05:40:35PM +0200, olivier moysan wrote:
+Required properties:
- compatible: Must be "st,stm32h7-i2s"
- #sound-dai-cells: Must be 1. (one parameter)
- This parameter allows to specify CPU DAI index in soundcard CPU dai link.
- index 0: playback DAI
- index 1: capture DAI
- index 2: full duplex DAI
Is this still needed for graph-card?
The graph card is blocked on your review... I'm also not clear how without something like this we'd be able to identify a specific DAI within a device if we don't have a way of identifying them.
Hello Rob, Mark
Thanks for your review. Please find my comments below.
On 04/11/2017 04:32 PM, Mark Brown wrote:
On Mon, Apr 10, 2017 at 02:48:32PM -0500, Rob Herring wrote:
On Thu, Apr 06, 2017 at 05:40:35PM +0200, olivier moysan wrote:
+Required properties:
- compatible: Must be "st,stm32h7-i2s"
- #sound-dai-cells: Must be 1. (one parameter)
- This parameter allows to specify CPU DAI index in soundcard CPU dai link.
- index 0: playback DAI
- index 1: capture DAI
- index 2: full duplex DAI
Is this still needed for graph-card?
The graph card is blocked on your review... I'm also not clear how without something like this we'd be able to identify a specific DAI within a device if we don't have a way of identifying them.
Yes, it seems that audio graph card does not allow to select a specific DAI. This will probably be necessary.
However, regarding STM32 I2S driver, I'm wondering if selecting DAI is the best way to configure interface as tx, rx or fd. Maybe, it is more relevant to configure DAI according to DMA configuration from I2S node. This would moreover avoid to allocate 2 dmas channels when not necessary (tx or rx only). If you agree with this, I will implement this change in a v2.
BRs olivier
On Tue, Apr 11, 2017 at 03:44:52PM +0000, Olivier MOYSAN wrote:
However, regarding STM32 I2S driver, I'm wondering if selecting DAI is the best way to configure interface as tx, rx or fd.
Why do you even need to configure this?
Maybe, it is more relevant to configure DAI according to DMA configuration from I2S node. This would moreover avoid to allocate 2 dmas channels when not necessary (tx or rx only). If you agree with this, I will implement this change in a v2.
That sounds wrong, I'd expect this wiring to be done statically as part of the .dtsi for the SoC (or just grabbed as needed at runtime if things are flexbile enough) rather than being a configuration thing done per board... I had thought that this was configuration reflecting different ways of taping out the IP with different feature sets, is that not the case?
Hello Mark,
On 04/11/2017 11:10 PM, Mark Brown wrote:
On Tue, Apr 11, 2017 at 03:44:52PM +0000, Olivier MOYSAN wrote:
However, regarding STM32 I2S driver, I'm wondering if selecting DAI is the best way to configure interface as tx, rx or fd.
Why do you even need to configure this?
The IP provides two data wires, SD in and SD out. So it can be configured either as capture or playback only, or full-duplex. This corresponds to a mode selection through a register configuration.
Maybe, it is more relevant to configure DAI according to DMA configuration from I2S node. This would moreover avoid to allocate 2 dmas channels when not necessary (tx or rx only). If you agree with this, I will implement this change in a v2.
That sounds wrong, I'd expect this wiring to be done statically as part of the .dtsi for the SoC (or just grabbed as needed at runtime if things are flexbile enough) rather than being a configuration thing done per board... I had thought that this was configuration reflecting different ways of taping out the IP with different feature sets, is that not the case?
This configuration is board dependent. The IP may be used as rx, tx or fd depending on board. So I think it can make sense to have a DMA configuration linked to board, and to set IP mode accordingly.
BRs olivier
On Wed, Apr 12, 2017 at 08:30:31AM +0000, Olivier MOYSAN wrote:
On 04/11/2017 11:10 PM, Mark Brown wrote:
That sounds wrong, I'd expect this wiring to be done statically as part of the .dtsi for the SoC (or just grabbed as needed at runtime if things are flexbile enough) rather than being a configuration thing done per board... I had thought that this was configuration reflecting different ways of taping out the IP with different feature sets, is that not the case?
This configuration is board dependent. The IP may be used as rx, tx or fd depending on board. So I think it can make sense to have a DMA configuration linked to board, and to set IP mode accordingly.
It is totally normal to just not use one direction in a given system, we don't normally need to do anything special to handle things. I'm a bit confused as to what's different here and needs configuring?
Hello Mark,
On 04/12/2017 01:32 PM, Mark Brown wrote:
On Wed, Apr 12, 2017 at 08:30:31AM +0000, Olivier MOYSAN wrote:
On 04/11/2017 11:10 PM, Mark Brown wrote:
That sounds wrong, I'd expect this wiring to be done statically as part of the .dtsi for the SoC (or just grabbed as needed at runtime if things are flexbile enough) rather than being a configuration thing done per board... I had thought that this was configuration reflecting different ways of taping out the IP with different feature sets, is that not the case?
This configuration is board dependent. The IP may be used as rx, tx or fd depending on board. So I think it can make sense to have a DMA configuration linked to board, and to set IP mode accordingly.
It is totally normal to just not use one direction in a given system, we don't normally need to do anything special to handle things. I'm a bit confused as to what's different here and needs configuring?
The IP does not provide an audio channel configured either as rx or tx. I agree, that in such case, the cpu driver does not generally need to worry about direction and there is nothing special required to handle it.
Here the IP provides 2 channels, 1 tx and 1 rx, which may be active or not. The driver has to know which channel is used, and if both channels have to be managed. The affected registers depend on selected channel. Moreover specific processing has to be performed if both channels are used.
BRs olivier
On Wed, Apr 12, 2017 at 12:58:16PM +0000, Olivier MOYSAN wrote:
On 04/12/2017 01:32 PM, Mark Brown wrote:
It is totally normal to just not use one direction in a given system, we don't normally need to do anything special to handle things. I'm a bit confused as to what's different here and needs configuring?
The IP does not provide an audio channel configured either as rx or tx. I agree, that in such case, the cpu driver does not generally need to worry about direction and there is nothing special required to handle it.
No, that's not what I'm saying - such hardware would be extremely unusual.
Here the IP provides 2 channels, 1 tx and 1 rx, which may be active or not. The driver has to know which channel is used, and if both channels have to be managed. The affected registers depend on selected channel.
This sounds like essentially every audio controller out there. The overwhelming majority of controllers do exactly as you describe and have both directions in the same IP, this really doesn't seem at all unusual. Off the top of my head I can only think of one SoC family which combines multiple IPs to do bidirectional audio (though I didn't check).
It really feels like there is something different here and I'm just missing it.
Moreover specific processing has to be performed if both channels are used.
Given that this case has to be supported anyway I'd be more inclined to ask the question the other way around TBH.
Hello Mark,
On 04/12/2017 05:38 PM, Mark Brown wrote:
On Wed, Apr 12, 2017 at 12:58:16PM +0000, Olivier MOYSAN wrote:
On 04/12/2017 01:32 PM, Mark Brown wrote:
It is totally normal to just not use one direction in a given system, we don't normally need to do anything special to handle things. I'm a bit confused as to what's different here and needs configuring?
The IP does not provide an audio channel configured either as rx or tx. I agree, that in such case, the cpu driver does not generally need to worry about direction and there is nothing special required to handle it.
No, that's not what I'm saying - such hardware would be extremely unusual.
Here the IP provides 2 channels, 1 tx and 1 rx, which may be active or not. The driver has to know which channel is used, and if both channels have to be managed. The affected registers depend on selected channel.
This sounds like essentially every audio controller out there. The overwhelming majority of controllers do exactly as you describe and have both directions in the same IP, this really doesn't seem at all unusual. Off the top of my head I can only think of one SoC family which combines multiple IPs to do bidirectional audio (though I didn't check).
It really feels like there is something different here and I'm just missing it.
Yes, I think I need to give more details on design choices.
The IP provides 2 channels. I can see mainly 3 solutions to link dais to these channels:
1) 2 static dais NOT exclusive - dai tx - dai rx The IP exhibits a mode register, where you select mode TX, RX or FD. There are 2 two options to manage this register. option 1: start first channel with mode RX or TX when second channel is started, mode has to be changed to FD. Transfers have to be stopped before changing configuration registers, so this leads to cuts in audio stream. option 2: start a first channel with mode FD. In this case, we may have unpredictable behavior for the stream which is not already started. probably underrun/overrun. So, this solution rises problem for full-duplex management.
2) 3 static dais exclusive - dai tx - dai rx - dai rx-tx (fd) This is the current implementation. The choice of the dai is done at probe time. It is provided by DT through sound-dai parameter. When dai fd is selected, after starting first stream, we assume that second stream will be started. In this case we wait for second stream to be available before enabling IP and starting transfers.
3) 1 dynamic dai - dai rx or tx or fd (according to dma conf in IP node) Here the driver exposes only a single dai constructed from dma configuration provided by IP DT node. This allows to get ride of sound-dai parameter. This was my suggestion after Rob comments on DT bindings.
Hope this will help clarifying design constraints.
Moreover specific processing has to be performed if both channels are used.
Given that this case has to be supported anyway I'd be more inclined to ask the question the other way around TBH.
Best regards olivier
On Thu, Apr 13, 2017 at 08:01:34AM +0000, Olivier MOYSAN wrote:
- 2 static dais NOT exclusive
- dai tx
- dai rx
The IP exhibits a mode register, where you select mode TX, RX or FD. There are 2 two options to manage this register. option 1: start first channel with mode RX or TX when second channel is started, mode has to be changed to FD. Transfers have to be stopped before changing configuration registers, so this leads to cuts in audio stream. option 2: start a first channel with mode FD. In this case, we may have unpredictable behavior for the stream which is not already started. probably underrun/overrun. So, this solution rises problem for full-duplex management.
- 3 static dais exclusive
- dai tx
- dai rx
- dai rx-tx (fd)
This is the current implementation. The choice of the dai is done at probe time. It is provided by DT through sound-dai parameter. When dai fd is selected, after starting first stream, we assume that second stream will be started. In this case we wait for second stream to be available before enabling IP and starting transfers.
- 1 dynamic dai
- dai rx or tx or fd (according to dma conf in IP node)
Here the driver exposes only a single dai constructed from dma configuration provided by IP DT node. This allows to get ride of sound-dai parameter.
None of these options reflect how normal I2S controllers present themselves in DT. To repeat, you should present a single bidirectional DAI for the single physical bidirectional I2S controller that your hardware has.
If it's not possible to figure out a way to make the controller support simultaneous playback and record with the two started independently then the driver should just return an error if userspace tries to start the second direction up. This will severely limit the utility of the driver as Linux generally treats playback and record independently but that's going to apply just as much with any of the options involving multiple DAIs or configuration in DT. You might be able to do something with feeding it dummy data I guess?
Hello Mark,
On 04/26/2017 06:15 PM, Mark Brown wrote:
On Thu, Apr 13, 2017 at 08:01:34AM +0000, Olivier MOYSAN wrote:
- 2 static dais NOT exclusive
- dai tx
- dai rx
The IP exhibits a mode register, where you select mode TX, RX or FD. There are 2 two options to manage this register. option 1: start first channel with mode RX or TX when second channel is started, mode has to be changed to FD. Transfers have to be stopped before changing configuration registers, so this leads to cuts in audio stream. option 2: start a first channel with mode FD. In this case, we may have unpredictable behavior for the stream which is not already started. probably underrun/overrun. So, this solution rises problem for full-duplex management.
- 3 static dais exclusive
- dai tx
- dai rx
- dai rx-tx (fd)
This is the current implementation. The choice of the dai is done at probe time. It is provided by DT through sound-dai parameter. When dai fd is selected, after starting first stream, we assume that second stream will be started. In this case we wait for second stream to be available before enabling IP and starting transfers.
- 1 dynamic dai
- dai rx or tx or fd (according to dma conf in IP node)
Here the driver exposes only a single dai constructed from dma configuration provided by IP DT node. This allows to get ride of sound-dai parameter.
None of these options reflect how normal I2S controllers present themselves in DT. To repeat, you should present a single bidirectional DAI for the single physical bidirectional I2S controller that your hardware has.
If it's not possible to figure out a way to make the controller support simultaneous playback and record with the two started independently then the driver should just return an error if userspace tries to start the second direction up. This will severely limit the utility of the driver as Linux generally treats playback and record independently but that's going to apply just as much with any of the options involving multiple DAIs or configuration in DT. You might be able to do something with feeding it dummy data I guess?
Ok. I will implement a single bidirectional DAI in v2.
Best regards Olivier
On Tue, Apr 11, 2017 at 9:32 AM, Mark Brown broonie@kernel.org wrote:
On Mon, Apr 10, 2017 at 02:48:32PM -0500, Rob Herring wrote:
On Thu, Apr 06, 2017 at 05:40:35PM +0200, olivier moysan wrote:
+Required properties:
- compatible: Must be "st,stm32h7-i2s"
- #sound-dai-cells: Must be 1. (one parameter)
- This parameter allows to specify CPU DAI index in soundcard CPU dai link.
- index 0: playback DAI
- index 1: capture DAI
- index 2: full duplex DAI
Is this still needed for graph-card?
The graph card is blocked on your review...
Maybe if there were more reviewers it would move faster. I don't know ASoC that well.
I'm also not clear how without something like this we'd be able to identify a specific DAI within a device if we don't have a way of identifying them.
Isn't that what the graph does? "dais" points to a list of ports which are the specific DAIs whether there are multiple ones in a single device or multiple devices with a single DAI each.
Rob
On Tue, Apr 11, 2017 at 11:02:57AM -0500, Rob Herring wrote:
On Tue, Apr 11, 2017 at 9:32 AM, Mark Brown broonie@kernel.org wrote:
The graph card is blocked on your review...
Maybe if there were more reviewers it would move faster. I don't know ASoC that well.
The times I've looked at it recently it's been stuck in DT style issues rather than anything substantially ASoC related, as far as I can tell the binding is essentially empty from an ASoC point of view and inherited from the of_graph binding. There's bits in that are a bit random like specifically listing the CPU DAIs and only them but that just looks like one of these random DT things that's predetermined. I really can't see anything at all in there to review from an ASoC point of view, I've applied the changes that don't seem blocked on the binding. If there's something you're looking for then please say...
We never seem to make any progress on the generic changes in drivers/of at the start of the series either...
I'm also not clear how without something like this we'd be able to identify a specific DAI within a device if we don't have a way of identifying them.
Isn't that what the graph does? "dais" points to a list of ports which are the specific DAIs whether there are multiple ones in a single device or multiple devices with a single DAI each.
But the ports can still have indexes AFAICT (the examples show port@0, port@1 and so on) so we still need to define what those indexes mean which is what this is doing?
Add I2S ASoC driver for STM32.
Signed-off-by: olivier moysan olivier.moysan@st.com --- sound/soc/Kconfig | 1 + sound/soc/Makefile | 1 + sound/soc/stm/Kconfig | 8 + sound/soc/stm/Makefile | 2 + sound/soc/stm/stm32_i2s.c | 1069 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1081 insertions(+) create mode 100644 sound/soc/stm/Kconfig create mode 100644 sound/soc/stm/Makefile create mode 100644 sound/soc/stm/stm32_i2s.c
diff --git a/sound/soc/Kconfig b/sound/soc/Kconfig index 182d92e..3836ebe 100644 --- a/sound/soc/Kconfig +++ b/sound/soc/Kconfig @@ -63,6 +63,7 @@ source "sound/soc/sh/Kconfig" source "sound/soc/sirf/Kconfig" source "sound/soc/spear/Kconfig" source "sound/soc/sti/Kconfig" +source "sound/soc/stm/Kconfig" source "sound/soc/sunxi/Kconfig" source "sound/soc/tegra/Kconfig" source "sound/soc/txx9/Kconfig" diff --git a/sound/soc/Makefile b/sound/soc/Makefile index 9a30f21..5440cf7 100644 --- a/sound/soc/Makefile +++ b/sound/soc/Makefile @@ -43,6 +43,7 @@ obj-$(CONFIG_SND_SOC) += sh/ obj-$(CONFIG_SND_SOC) += sirf/ obj-$(CONFIG_SND_SOC) += spear/ obj-$(CONFIG_SND_SOC) += sti/ +obj-$(CONFIG_SND_SOC) += stm/ obj-$(CONFIG_SND_SOC) += sunxi/ obj-$(CONFIG_SND_SOC) += tegra/ obj-$(CONFIG_SND_SOC) += txx9/ diff --git a/sound/soc/stm/Kconfig b/sound/soc/stm/Kconfig new file mode 100644 index 0000000..972970f --- /dev/null +++ b/sound/soc/stm/Kconfig @@ -0,0 +1,8 @@ +menuconfig SND_SOC_STM32 + tristate "STMicroelectronics STM32 SOC audio support" + depends on ARCH_STM32 || COMPILE_TEST + depends on SND_SOC + select SND_SOC_GENERIC_DMAENGINE_PCM + select REGMAP_MMIO + help + Say Y if you want to enable ASoC-support for STM32 diff --git a/sound/soc/stm/Makefile b/sound/soc/stm/Makefile new file mode 100644 index 0000000..090836b --- /dev/null +++ b/sound/soc/stm/Makefile @@ -0,0 +1,2 @@ +snd-soc-stm32-i2s-objs := stm32_i2s.o +obj-$(CONFIG_SND_SOC_STM32) += snd-soc-stm32-i2s.o diff --git a/sound/soc/stm/stm32_i2s.c b/sound/soc/stm/stm32_i2s.c new file mode 100644 index 0000000..2ca9719 --- /dev/null +++ b/sound/soc/stm/stm32_i2s.c @@ -0,0 +1,1069 @@ +/* + * STM32 ALSA SoC Digital Audio Interface (I2S) driver. + * + * Copyright (C) 2017, STMicroelectronics - All Rights Reserved + * Author(s): Olivier Moysan olivier.moysan@st.com for STMicroelectronics. + * + * License terms: GPL V2.0. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 as published by + * the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + */ + +#include <linux/clk.h> +#include <linux/delay.h> +#include <linux/module.h> +#include <linux/of_irq.h> +#include <linux/of_platform.h> +#include <linux/regmap.h> +#include <linux/reset.h> +#include <linux/spinlock.h> + +#include <sound/dmaengine_pcm.h> +#include <sound/pcm_params.h> + +#define STM32_I2S_CR1_REG 0x0 +#define STM32_I2S_CFG1_REG 0x08 +#define STM32_I2S_CFG2_REG 0x0C +#define STM32_I2S_IER_REG 0x10 +#define STM32_I2S_SR_REG 0x14 +#define STM32_I2S_IFCR_REG 0x18 +#define STM32_I2S_TXDR_REG 0X20 +#define STM32_I2S_RXDR_REG 0x30 +#define STM32_I2S_CGFR_REG 0X50 + +/* Bit definition for SPI2S_CR1 register */ +#define I2S_CR1_SPE BIT(0) +#define I2S_CR1_CSTART BIT(9) +#define I2S_CR1_CSUSP BIT(10) +#define I2S_CR1_HDDIR BIT(11) +#define I2S_CR1_SSI BIT(12) +#define I2S_CR1_CRC33_17 BIT(13) +#define I2S_CR1_RCRCI BIT(14) +#define I2S_CR1_TCRCI BIT(15) + +/* Bit definition for SPI_CFG2 register */ +#define I2S_CFG2_IOSWP_SHIFT 15 +#define I2S_CFG2_IOSWP BIT(I2S_CFG2_IOSWP_SHIFT) +#define I2S_CFG2_LSBFRST BIT(23) +#define I2S_CFG2_AFCNTR BIT(31) + +/* Bit definition for SPI_CFG1 register */ +#define I2S_CFG1_FTHVL_SHIFT 5 +#define I2S_CFG1_FTHVL_MASK GENMASK(8, I2S_CFG1_FTHVL_SHIFT) +#define I2S_CFG1_FTHVL_SET(x) ((x) << I2S_CFG1_FTHVL_SHIFT) + +#define I2S_CFG1_TXDMAEN BIT(15) +#define I2S_CFG1_RXDMAEN BIT(14) + +/* Bit definition for SPI2S_IER register */ +#define I2S_IER_RXPIE BIT(0) +#define I2S_IER_TXPIE BIT(1) +#define I2S_IER_DPXPIE BIT(2) +#define I2S_IER_EOTIE BIT(3) +#define I2S_IER_TXTFIE BIT(4) +#define I2S_IER_UDRIE BIT(5) +#define I2S_IER_OVRIE BIT(6) +#define I2S_IER_CRCEIE BIT(7) +#define I2S_IER_TIFREIE BIT(8) +#define I2S_IER_MODFIE BIT(9) +#define I2S_IER_TSERFIE BIT(10) + +/* Bit definition for SPI2S_SR register */ +#define I2S_SR_RXP BIT(0) +#define I2S_SR_TXP BIT(1) +#define I2S_SR_DPXP BIT(2) +#define I2S_SR_EOT BIT(3) +#define I2S_SR_TXTF BIT(4) +#define I2S_SR_UDR BIT(5) +#define I2S_SR_OVR BIT(6) +#define I2S_SR_CRCERR BIT(7) +#define I2S_SR_TIFRE BIT(8) +#define I2S_SR_MODF BIT(9) +#define I2S_SR_TSERF BIT(10) +#define I2S_SR_SUSP BIT(11) +#define I2S_SR_TXC BIT(12) +#define I2S_SR_RXPLVL GENMASK(14, 13) +#define I2S_SR_RXWNE BIT(15) + +#define I2S_SR_MASK GENMASK(15, 0) + +/* Bit definition for SPI_IFCR register */ +#define I2S_IFCR_EOTC BIT(3) +#define I2S_IFCR_TXTFC BIT(4) +#define I2S_IFCR_UDRC BIT(5) +#define I2S_IFCR_OVRC BIT(6) +#define I2S_IFCR_CRCEC BIT(7) +#define I2S_IFCR_TIFREC BIT(8) +#define I2S_IFCR_MODFC BIT(9) +#define I2S_IFCR_TSERFC BIT(10) +#define I2S_IFCR_SUSPC BIT(11) + +#define I2S_IFCR_MASK GENMASK(11, 3) + +/* Bit definition for SPI_I2SCGFR register */ +#define I2S_CGFR_I2SMOD BIT(0) + +#define I2S_CGFR_I2SCFG_SHIFT 1 +#define I2S_CGFR_I2SCFG_MASK GENMASK(3, I2S_CGFR_I2SCFG_SHIFT) +#define I2S_CGFR_I2SCFG_SET(x) ((x) << I2S_CGFR_I2SCFG_SHIFT) + +#define I2S_CGFR_I2SSTD_SHIFT 4 +#define I2S_CGFR_I2SSTD_MASK GENMASK(5, I2S_CGFR_I2SSTD_SHIFT) +#define I2S_CGFR_I2SSTD_SET(x) ((x) << I2S_CGFR_I2SSTD_SHIFT) + +#define I2S_CGFR_PCMSYNC BIT(7) + +#define I2S_CGFR_DATLEN_SHIFT 8 +#define I2S_CGFR_DATLEN_MASK GENMASK(9, I2S_CGFR_DATLEN_SHIFT) +#define I2S_CGFR_DATLEN_SET(x) ((x) << I2S_CGFR_DATLEN_SHIFT) + +#define I2S_CGFR_CHLEN_SHIFT 10 +#define I2S_CGFR_CHLEN BIT(I2S_CGFR_CHLEN_SHIFT) +#define I2S_CGFR_CKPOL BIT(11) +#define I2S_CGFR_FIXCH BIT(12) +#define I2S_CGFR_WSINV BIT(13) +#define I2S_CGFR_DATFMT BIT(14) + +#define I2S_CGFR_I2SDIV_SHIFT 16 +#define I2S_CGFR_I2SDIV_BIT_H 23 +#define I2S_CGFR_I2SDIV_MASK GENMASK(I2S_CGFR_I2SDIV_BIT_H,\ + I2S_CGFR_I2SDIV_SHIFT) +#define I2S_CGFR_I2SDIV_SET(x) ((x) << I2S_CGFR_I2SDIV_SHIFT) +#define I2S_CGFR_I2SDIV_MAX ((1 << (I2S_CGFR_I2SDIV_BIT_H -\ + I2S_CGFR_I2SDIV_SHIFT)) - 1) + +#define I2S_CGFR_ODD_SHIFT 24 +#define I2S_CGFR_ODD BIT(I2S_CGFR_ODD_SHIFT) +#define I2S_CGFR_MCKOE BIT(25) + +enum i2s_master_mode { + I2S_MS_NOT_SET, + I2S_MS_MASTER, + I2S_MS_SLAVE, +}; + +enum i2s_mode { + I2S_I2SMOD_TX_SLAVE, + I2S_I2SMOD_RX_SLAVE, + I2S_I2SMOD_TX_MASTER, + I2S_I2SMOD_RX_MASTER, + I2S_I2SMOD_FD_SLAVE, + I2S_I2SMOD_FD_MASTER, +}; + +enum i2s_fifo_th { + I2S_FIFO_TH_NONE, + I2S_FIFO_TH_ONE_QUARTER, + I2S_FIFO_TH_HALF, + I2S_FIFO_TH_THREE_QUARTER, + I2S_FIFO_TH_FULL, +}; + +enum i2s_std { + I2S_STD_I2S, + I2S_STD_LEFT_J, + I2S_STD_RIGHT_J, + I2S_STD_DSP, +}; + +enum i2s_dir { + I2S_DIR_TX, + I2S_DIR_RX, + I2S_DIR_FD, +}; + +enum i2s_datlen { + I2S_I2SMOD_DATLEN_16, + I2S_I2SMOD_DATLEN_24, + I2S_I2SMOD_DATLEN_32, +}; + +#define STM32_I2S_DAI_NAME_SIZE 20 +#define STM32_I2S_DAIS_NB 3 +#define STM32_I2S_IP_NAME_LENGTH 5 +#define STM32_I2S_FIFO_SIZE 16 + +#define STM32_I2S_IS_MASTER(x) ((x)->ms_flg == I2S_MS_MASTER) +#define STM32_I2S_IS_SLAVE(x) ((x)->ms_flg == I2S_MS_SLAVE) + +#define STM32_I2S_IS_PLAYBACK(x) ((x)->dir == I2S_DIR_TX) +#define STM32_I2S_IS_CAPTURE(x) ((x)->dir == I2S_DIR_RX) +#define STM32_I2S_IS_FULL_DUPLEX(x) ((x)->dir == I2S_DIR_FD) + +/** + * @regmap_conf: I2S register map configuration pointer + * @egmap: I2S register map pointer + * @pdev: device data pointer + * @dai_drv: DAI driver pointer + * @dma_data_tx: dma configuration data for tx channel + * @dma_data_rx: dma configuration data for tx channel + * @substream: PCM substream data pointer + * @i2sclk: kernel clock feeding the I2S clock generator + * @pclk: peripheral clock driving bus interface + * @x8kclk: I2S parent clock for sampling frequencies multiple of 8kHz + * @x11kclk: I2S parent clock for sampling frequencies multiple of 11kHz + * @base: mmio register base virtual address + * @phys_addr: I2S registers physical base address + * @lock_fd: lock to manage race conditions in full duplex mode + * @ip_name: I2S name + * @dais_name: playback, capture and fd DAI names + * @fifo_th: fifo threshold setting + * @mclk_rate: master clock frequency (Hz) + * @fmt: DAI protocol + * @refcount: keep count of opened streams on I2S + * @startcount: keep count of started streams on I2S + * @dir: I2S direction. tx, rx or full duplex. + * @ms_flg: master mode flag. + * @format: pcm stream width (16/32 bits), used for consistency check in fd mode + * @rate: pcm stream rate, used for consistency check in fd mode + */ +struct stm32_i2s_data { + const struct regmap_config *regmap_conf; + struct regmap *regmap; + struct platform_device *pdev; + struct snd_soc_dai_driver *dai_drv; + struct snd_dmaengine_dai_dma_data dma_data_tx; + struct snd_dmaengine_dai_dma_data dma_data_rx; + struct snd_pcm_substream *substream; + struct clk *i2sclk; + struct clk *pclk; + struct clk *x8kclk; + struct clk *x11kclk; + void __iomem *base; + dma_addr_t phys_addr; + spinlock_t lock_fd; /* Manage race conditions for full duplex */ + const char *ip_name; + char dais_name[STM32_I2S_DAIS_NB][STM32_I2S_DAI_NAME_SIZE]; + unsigned int fifo_th; + unsigned int mclk_rate; + unsigned int fmt; + int refcount; + int startcount; + int dir; + int ms_flg; + int format; + unsigned int rate; +}; + +static irqreturn_t stm32_i2s_isr(int irq, void *devid) +{ + struct stm32_i2s_data *i2s = (struct stm32_i2s_data *)devid; + struct platform_device *pdev = i2s->pdev; + u32 sr, ier; + unsigned long flags; + int err = 0; + + regmap_read(i2s->regmap, STM32_I2S_SR_REG, &sr); + regmap_read(i2s->regmap, STM32_I2S_IER_REG, &ier); + + flags = sr & ier; + if (!flags) { + dev_dbg(&pdev->dev, "Spurious IT sr=0x%08x, ier=0x%08x\n", + sr, ier); + return IRQ_NONE; + } + + /* Clear ITs */ + regmap_update_bits(i2s->regmap, STM32_I2S_IFCR_REG, + I2S_IFCR_MASK, flags); + + if (flags & I2S_SR_OVR) { + dev_dbg(&pdev->dev, "Overrun: received value discarded\n"); + err = 1; + } + + if (flags & I2S_SR_UDR) { + dev_dbg(&pdev->dev, "Underrun\n"); + err = 1; + } + + if (flags & I2S_SR_TIFRE) + dev_dbg(&pdev->dev, "Frame error\n"); + + if (err) + snd_pcm_stop_xrun(i2s->substream); + + return IRQ_HANDLED; +} + +static bool stm32_i2s_readable_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case STM32_I2S_CR1_REG: + case STM32_I2S_CFG1_REG: + case STM32_I2S_CFG2_REG: + case STM32_I2S_IER_REG: + case STM32_I2S_SR_REG: + case STM32_I2S_IFCR_REG: + case STM32_I2S_TXDR_REG: + case STM32_I2S_RXDR_REG: + case STM32_I2S_CGFR_REG: + return true; + default: + return false; + } +} + +static bool stm32_i2s_volatile_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case STM32_I2S_TXDR_REG: + case STM32_I2S_RXDR_REG: + return true; + default: + return false; + } +} + +static bool stm32_i2s_writeable_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case STM32_I2S_CR1_REG: + case STM32_I2S_CFG1_REG: + case STM32_I2S_CFG2_REG: + case STM32_I2S_IER_REG: + case STM32_I2S_IFCR_REG: + case STM32_I2S_TXDR_REG: + case STM32_I2S_CGFR_REG: + return true; + default: + return false; + } +} + +static int stm32_i2s_set_dai_fmt(struct snd_soc_dai *cpu_dai, unsigned int fmt) +{ + struct stm32_i2s_data *i2s = snd_soc_dai_get_drvdata(cpu_dai); + u32 cgfr; + u32 cgfr_mask = I2S_CGFR_I2SSTD_MASK | I2S_CGFR_CKPOL | + I2S_CGFR_WSINV | I2S_CGFR_I2SCFG_MASK; + + dev_dbg(cpu_dai->dev, "fmt %x\n", fmt); + + /* + * winv = 0 : default behavior (high/low) for all standards + * ckpol 0 for all standards. + */ + switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) { + case SND_SOC_DAIFMT_I2S: + cgfr = I2S_CGFR_I2SSTD_SET(I2S_STD_I2S); + break; + case SND_SOC_DAIFMT_MSB: + cgfr = I2S_CGFR_I2SSTD_SET(I2S_STD_LEFT_J); + break; + case SND_SOC_DAIFMT_LSB: + cgfr = I2S_CGFR_I2SSTD_SET(I2S_STD_RIGHT_J); + break; + case SND_SOC_DAIFMT_DSP_A: + cgfr = I2S_CGFR_I2SSTD_SET(I2S_STD_DSP); + break; + /* DSP_B not mapped on I2S PCM long format. 1 bit offset does not fit */ + default: + dev_err(cpu_dai->dev, "Unsupported protocol %#x\n", + fmt & SND_SOC_DAIFMT_FORMAT_MASK); + return -EINVAL; + } + + /* DAI clock strobing */ + switch (fmt & SND_SOC_DAIFMT_INV_MASK) { + case SND_SOC_DAIFMT_NB_NF: + break; + case SND_SOC_DAIFMT_IB_NF: + cgfr |= I2S_CGFR_CKPOL; + break; + case SND_SOC_DAIFMT_NB_IF: + cgfr |= I2S_CGFR_WSINV; + break; + case SND_SOC_DAIFMT_IB_IF: + cgfr |= I2S_CGFR_CKPOL; + cgfr |= I2S_CGFR_WSINV; + break; + default: + dev_err(cpu_dai->dev, "Unsupported strobing %#x\n", + fmt & SND_SOC_DAIFMT_INV_MASK); + return -EINVAL; + } + + /* DAI clock master masks */ + switch (fmt & SND_SOC_DAIFMT_MASTER_MASK) { + case SND_SOC_DAIFMT_CBM_CFM: + if (STM32_I2S_IS_MASTER(i2s)) { + dev_err(cpu_dai->dev, "previous DAI set master mode\n"); + return -EINVAL; + } + i2s->ms_flg = I2S_MS_SLAVE; + + if (STM32_I2S_IS_FULL_DUPLEX(i2s)) + cgfr |= I2S_CGFR_I2SCFG_SET(I2S_I2SMOD_FD_SLAVE); + else if (STM32_I2S_IS_PLAYBACK(i2s)) + cgfr |= I2S_CGFR_I2SCFG_SET(I2S_I2SMOD_TX_SLAVE); + else + cgfr |= I2S_CGFR_I2SCFG_SET(I2S_I2SMOD_RX_SLAVE); + break; + case SND_SOC_DAIFMT_CBS_CFS: + if (STM32_I2S_IS_SLAVE(i2s)) { + dev_err(cpu_dai->dev, "previous DAI set slave mode\n"); + return -EINVAL; + } + i2s->ms_flg = I2S_MS_MASTER; + + if (STM32_I2S_IS_FULL_DUPLEX(i2s)) + cgfr |= I2S_CGFR_I2SCFG_SET(I2S_I2SMOD_FD_MASTER); + else if (STM32_I2S_IS_PLAYBACK(i2s)) + cgfr |= I2S_CGFR_I2SCFG_SET(I2S_I2SMOD_TX_MASTER); + else + cgfr |= I2S_CGFR_I2SCFG_SET(I2S_I2SMOD_RX_MASTER); + break; + default: + dev_err(cpu_dai->dev, "Unsupported mode %#x\n", + fmt & SND_SOC_DAIFMT_MASTER_MASK); + return -EINVAL; + } + + i2s->fmt = fmt; + return regmap_update_bits(i2s->regmap, STM32_I2S_CGFR_REG, + cgfr_mask, cgfr); +} + +static int stm32_i2s_set_sysclk(struct snd_soc_dai *cpu_dai, + int clk_id, unsigned int freq, int dir) +{ + struct stm32_i2s_data *i2s = snd_soc_dai_get_drvdata(cpu_dai); + + dev_dbg(cpu_dai->dev, "I2S MCLK frequency is %uHz\n", freq); + + if ((dir == SND_SOC_CLOCK_OUT) && STM32_I2S_IS_MASTER(i2s)) { + i2s->mclk_rate = freq; + + /* Enable master clock if master mode and mclk-fs are set */ + return regmap_update_bits(i2s->regmap, STM32_I2S_CGFR_REG, + I2S_CGFR_MCKOE, I2S_CGFR_MCKOE); + } + + return 0; +} + +static int stm32_i2s_configure_clock(struct snd_soc_dai *cpu_dai, + struct snd_pcm_hw_params *params) +{ + struct stm32_i2s_data *i2s = snd_soc_dai_get_drvdata(cpu_dai); + unsigned long i2s_clock_rate; + unsigned int tmp, div, real_div, nb_bits, frame_len; + unsigned int rate = params_rate(params); + int ret; + u32 cgfr, cgfr_mask; + bool odd; + + if (i2s->refcount > 1) { + if (rate != i2s->rate) { + dev_err(cpu_dai->dev, + "rate not compatible with active stream"); + return -EINVAL; + } + return 0; + } + i2s->rate = rate; + + if (!(rate % 11025)) + clk_set_parent(i2s->i2sclk, i2s->x11kclk); + else + clk_set_parent(i2s->i2sclk, i2s->x8kclk); + i2s_clock_rate = clk_get_rate(i2s->i2sclk); + + /* + * mckl = mclk_ratio x ws + * i2s mode : mclk_ratio = 256 + * dsp mode : mclk_ratio = 128 + * + * mclk on + * i2s mode : div = i2s_clk / (mclk_ratio * ws) + * dsp mode : div = i2s_clk / (mclk_ratio * ws) + * mclk off + * i2s mode : div = i2s_clk / (nb_bits x ws) + * dsp mode : div = i2s_clk / (nb_bits x ws) + */ + if (i2s->mclk_rate) { + tmp = DIV_ROUND_CLOSEST(i2s_clock_rate, i2s->mclk_rate); + } else { + frame_len = 32; + if ((i2s->fmt & SND_SOC_DAIFMT_FORMAT_MASK) == + SND_SOC_DAIFMT_DSP_A) + frame_len = 16; + + /* master clock not enabled */ + ret = regmap_read(i2s->regmap, STM32_I2S_CGFR_REG, &cgfr); + if (ret < 0) + return ret; + + nb_bits = frame_len * ((cgfr & I2S_CGFR_CHLEN) + 1); + tmp = DIV_ROUND_CLOSEST(i2s_clock_rate, (nb_bits * rate)); + } + + /* Check the parity of the divider */ + odd = tmp & 0x1; + + /* Compute the div prescaler */ + div = tmp >> 1; + + cgfr = I2S_CGFR_I2SDIV_SET(div) | (odd << I2S_CGFR_ODD_SHIFT); + cgfr_mask = I2S_CGFR_I2SDIV_MASK | I2S_CGFR_ODD; + + real_div = ((2 * div) + odd); + dev_dbg(cpu_dai->dev, "I2S clk: %ld, SCLK: %d\n", + i2s_clock_rate, rate); + dev_dbg(cpu_dai->dev, "Divider: 2*%d(div)+%d(odd) = %d\n", + div, odd, real_div); + + if (((div == 1) && odd) || (div > I2S_CGFR_I2SDIV_MAX)) { + dev_err(cpu_dai->dev, "Wrong divider setting\n"); + return -EINVAL; + } + + if (!div && !odd) + dev_warn(cpu_dai->dev, "real divider forced to 1\n"); + + ret = regmap_update_bits(i2s->regmap, STM32_I2S_CGFR_REG, + cgfr_mask, cgfr); + if (ret < 0) + return ret; + + /* Set bitclock and frameclock to their inactive state */ + return regmap_update_bits(i2s->regmap, STM32_I2S_CFG2_REG, + I2S_CFG2_AFCNTR, I2S_CFG2_AFCNTR); +} + +static int stm32_i2s_configure_channel(struct snd_soc_dai *cpu_dai, + struct snd_pcm_hw_params *params) +{ + struct stm32_i2s_data *i2s = snd_soc_dai_get_drvdata(cpu_dai); + int format = params_width(params); + u32 cfgr, cfgr_mask; + + if (i2s->refcount > 1) { + if (format != i2s->format) { + dev_err(cpu_dai->dev, + "format not compatible with active stream"); + return -EINVAL; + } + return 0; + } + i2s->format = format; + + switch (format) { + case 16: + cfgr = I2S_CGFR_DATLEN_SET(I2S_I2SMOD_DATLEN_16); + cfgr_mask = I2S_CGFR_DATLEN_MASK; + break; + case 32: + cfgr = I2S_CGFR_DATLEN_SET(I2S_I2SMOD_DATLEN_32) | + I2S_CGFR_CHLEN; + cfgr_mask = I2S_CGFR_DATLEN_MASK | I2S_CGFR_CHLEN; + break; + default: + dev_err(cpu_dai->dev, "Unexpected format %d", format); + return -EINVAL; + } + + if (STM32_I2S_IS_SLAVE(i2s)) { + /* As data length is either 16 or 32 bits, fixch always set */ + cfgr |= I2S_CGFR_FIXCH; + cfgr_mask |= I2S_CGFR_FIXCH; + } + + return regmap_update_bits(i2s->regmap, STM32_I2S_CGFR_REG, + cfgr_mask, cfgr); +} + +static int stm32_i2s_startup(struct snd_pcm_substream *substream, + struct snd_soc_dai *cpu_dai) +{ + struct stm32_i2s_data *i2s = snd_soc_dai_get_drvdata(cpu_dai); + int ret, ier; + + i2s->substream = substream; + + spin_lock(&i2s->lock_fd); + i2s->refcount++; + spin_unlock(&i2s->lock_fd); + + ret = regmap_update_bits(i2s->regmap, STM32_I2S_IFCR_REG, + I2S_IFCR_MASK, 0); + if (ret < 0) + return ret; + + /* Enable ITs */ + ier = I2S_IER_OVRIE | I2S_IER_UDRIE; + if (STM32_I2S_IS_SLAVE(i2s)) + ier |= I2S_IER_TIFREIE; + + return regmap_update_bits(i2s->regmap, STM32_I2S_IER_REG, ier, ier); +} + +static int stm32_i2s_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *cpu_dai) +{ + struct stm32_i2s_data *i2s = snd_soc_dai_get_drvdata(cpu_dai); + int format = params_width(params); + unsigned int fthlv; + int ret; + + if ((params_channels(params) == 1) && + ((i2s->fmt & SND_SOC_DAIFMT_FORMAT_MASK) != SND_SOC_DAIFMT_DSP_A)) { + dev_err(cpu_dai->dev, "Mono mode supported only by DSP_A\n"); + return -EINVAL; + } + + /* + * fthlv is fifo threshold expressed as sample number + * fthlv = fifo size * threshold_ratio / (format / 8) + * where threshold_ratio = fifo_th / 4 + */ + fthlv = STM32_I2S_FIFO_SIZE * 8 * i2s->fifo_th / (format * 4); + + regmap_update_bits(i2s->regmap, STM32_I2S_CFG1_REG, I2S_CFG1_FTHVL_MASK, + I2S_CFG1_FTHVL_SET(fthlv - 1)); + + spin_lock(&i2s->lock_fd); + + ret = stm32_i2s_configure_channel(cpu_dai, params); + if (ret < 0) { + spin_unlock(&i2s->lock_fd); + return ret; + } + + if (STM32_I2S_IS_MASTER(i2s)) { + ret = stm32_i2s_configure_clock(cpu_dai, params); + if (ret < 0) { + spin_unlock(&i2s->lock_fd); + return ret; + } + } + + spin_unlock(&i2s->lock_fd); + + return 0; +} + +static int stm32_i2s_trigger(struct snd_pcm_substream *substream, int cmd, + struct snd_soc_dai *cpu_dai) +{ + struct stm32_i2s_data *i2s = snd_soc_dai_get_drvdata(cpu_dai); + int ret; + bool stream_is_playback = false; + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) + stream_is_playback = true; + + /* This lock protects both start flag and SPE bit accesses */ + spin_lock(&i2s->lock_fd); + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + if (stream_is_playback) { + ret = regmap_update_bits(i2s->regmap, + STM32_I2S_CFG1_REG, + I2S_CFG1_TXDMAEN, + I2S_CFG1_TXDMAEN); + } else { + ret = regmap_update_bits(i2s->regmap, + STM32_I2S_CFG1_REG, + I2S_CFG1_RXDMAEN, + I2S_CFG1_RXDMAEN); + } + if (ret < 0) + goto err_trigger; + + i2s->startcount++; + if (STM32_I2S_IS_FULL_DUPLEX(i2s) && (i2s->startcount < 2)) { + dev_dbg(cpu_dai->dev, + "Full duplex device waiting for %s stream", + stream_is_playback ? "capture" : "playback"); + spin_unlock(&i2s->lock_fd); + return 0; + } + + /* Enable i2s */ + dev_dbg(cpu_dai->dev, "start I2S\n"); + + ret = regmap_update_bits(i2s->regmap, STM32_I2S_CR1_REG, + I2S_CR1_SPE, I2S_CR1_SPE); + if (ret < 0) { + dev_err(cpu_dai->dev, "Error %d enabling I2S\n", ret); + goto err_trigger; + } + + ret = regmap_update_bits(i2s->regmap, STM32_I2S_CR1_REG, + I2S_CR1_CSTART, I2S_CR1_CSTART); + if (ret < 0) { + dev_err(cpu_dai->dev, "Error %d starting I2S\n", ret); + goto err_trigger; + } + break; + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + dev_dbg(cpu_dai->dev, "stop I2S\n"); + + ret = regmap_update_bits(i2s->regmap, STM32_I2S_CR1_REG, + I2S_CR1_SPE, 0); + if (ret < 0) { + dev_err(cpu_dai->dev, "Error %d disabling I2S\n", ret); + goto err_trigger; + } + i2s->startcount = 0; + + if (stream_is_playback) { + ret = regmap_update_bits(i2s->regmap, + STM32_I2S_CFG1_REG, + I2S_CFG1_TXDMAEN, 0); + } else { + ret = regmap_update_bits(i2s->regmap, + STM32_I2S_CFG1_REG, + I2S_CFG1_RXDMAEN, 0); + } + if (ret < 0) + goto err_trigger; + + break; + default: + return -EINVAL; + } + + spin_unlock(&i2s->lock_fd); + return 0; + +err_trigger: + spin_unlock(&i2s->lock_fd); + return ret; +} + +static void stm32_i2s_shutdown(struct snd_pcm_substream *substream, + struct snd_soc_dai *cpu_dai) +{ + struct stm32_i2s_data *i2s = snd_soc_dai_get_drvdata(cpu_dai); + + i2s->substream = NULL; + + spin_lock(&i2s->lock_fd); + if (i2s->refcount > 0) + i2s->refcount--; + spin_unlock(&i2s->lock_fd); + + regmap_update_bits(i2s->regmap, STM32_I2S_CGFR_REG, + I2S_CGFR_MCKOE, (unsigned int)~I2S_CGFR_MCKOE); +} + +static int stm32_i2s_dai_probe(struct snd_soc_dai *cpu_dai) +{ + struct stm32_i2s_data *i2s = dev_get_drvdata(cpu_dai->dev); + struct snd_dmaengine_dai_dma_data *dma_data_tx = &i2s->dma_data_tx; + struct snd_dmaengine_dai_dma_data *dma_data_rx = &i2s->dma_data_rx; + + /* Buswidth will be set by framework */ + dma_data_tx->addr_width = DMA_SLAVE_BUSWIDTH_UNDEFINED; + dma_data_tx->addr = (dma_addr_t)(i2s->phys_addr) + STM32_I2S_TXDR_REG; + dma_data_tx->maxburst = 1; + dma_data_rx->addr_width = DMA_SLAVE_BUSWIDTH_UNDEFINED; + dma_data_rx->addr = (dma_addr_t)(i2s->phys_addr) + STM32_I2S_RXDR_REG; + dma_data_rx->maxburst = 1; + + i2s->dir = cpu_dai->id; + switch (i2s->dir) { + case I2S_DIR_TX: + i2s->fifo_th = I2S_FIFO_TH_FULL; + snd_soc_dai_init_dma_data(cpu_dai, dma_data_tx, NULL); + break; + case I2S_DIR_RX: + i2s->fifo_th = I2S_FIFO_TH_HALF; + snd_soc_dai_init_dma_data(cpu_dai, NULL, dma_data_rx); + break; + case I2S_DIR_FD: + i2s->fifo_th = I2S_FIFO_TH_NONE; + snd_soc_dai_init_dma_data(cpu_dai, dma_data_tx, dma_data_rx); + break; + } + + return 0; +} + +static const struct regmap_config stm32_h7_i2s_regmap_conf = { + .reg_bits = 32, + .reg_stride = 4, + .val_bits = 32, + .max_register = STM32_I2S_CGFR_REG, + .readable_reg = stm32_i2s_readable_reg, + .volatile_reg = stm32_i2s_volatile_reg, + .writeable_reg = stm32_i2s_writeable_reg, + .fast_io = true, +}; + +static const struct snd_soc_dai_ops stm32_i2s_pcm_dai_ops = { + .set_sysclk = stm32_i2s_set_sysclk, + .set_fmt = stm32_i2s_set_dai_fmt, + .startup = stm32_i2s_startup, + .hw_params = stm32_i2s_hw_params, + .trigger = stm32_i2s_trigger, + .shutdown = stm32_i2s_shutdown, +}; + +static const struct snd_pcm_hardware stm32_i2s_pcm_hw = { + .info = SNDRV_PCM_INFO_INTERLEAVED | SNDRV_PCM_INFO_MMAP, + .buffer_bytes_max = 8 * PAGE_SIZE, + .period_bytes_max = 2048, + .periods_min = 2, + .periods_max = 8, +}; + +static const struct snd_dmaengine_pcm_config stm32_i2s_pcm_config = { + .pcm_hardware = &stm32_i2s_pcm_hw, + .prepare_slave_config = snd_dmaengine_pcm_prepare_slave_config, + .prealloc_buffer_size = PAGE_SIZE * 8, +}; + +static const struct snd_soc_component_driver stm32_i2s_component = { + .name = "stm32-i2s", +}; + +static void stm32_i2s_dai_init(struct snd_soc_pcm_stream *stream, + char *stream_name) +{ + stream->stream_name = stream_name; + stream->channels_min = 1; + stream->channels_max = 2; + stream->rates = SNDRV_PCM_RATE_8000_192000; + stream->formats = SNDRV_PCM_FMTBIT_S16_LE | + SNDRV_PCM_FMTBIT_S32_LE; +} + +static int stm32_i2s_dais_init(struct platform_device *pdev, + struct stm32_i2s_data *i2s) +{ + struct snd_soc_dai_driver *dai_ptr; + int i; + + dai_ptr = devm_kzalloc(&pdev->dev, STM32_I2S_DAIS_NB * + sizeof(struct snd_soc_dai_driver), GFP_KERNEL); + if (!dai_ptr) + return -ENOMEM; + + i2s->dai_drv = dai_ptr; + for (i = 0; i < STM32_I2S_DAIS_NB; i++) { + dai_ptr->probe = stm32_i2s_dai_probe; + dai_ptr->ops = &stm32_i2s_pcm_dai_ops; + if (i == 0) { + snprintf(i2s->dais_name[i], STM32_I2S_DAI_NAME_SIZE, + "%s-playback", i2s->ip_name); + dai_ptr->id = I2S_DIR_TX; + stm32_i2s_dai_init(&dai_ptr->playback, "CPU-Playback"); + } + if (i == 1) { + snprintf(i2s->dais_name[i], STM32_I2S_DAI_NAME_SIZE, + "%s-capture", i2s->ip_name); + dai_ptr->id = I2S_DIR_RX; + stm32_i2s_dai_init(&dai_ptr->capture, "CPU-Capture"); + } + if (i == 2) { + snprintf(i2s->dais_name[i], STM32_I2S_DAI_NAME_SIZE, + "%s-full-duplex", i2s->ip_name); + dai_ptr->id = I2S_DIR_FD; + stm32_i2s_dai_init(&dai_ptr->playback, + "CPU-FD-Playback"); + stm32_i2s_dai_init(&dai_ptr->capture, + "CPU-FD-Capture"); + } + dai_ptr->name = i2s->dais_name[i]; + dai_ptr++; + } + + return 0; +} + +static const struct of_device_id stm32_i2s_ids[] = { + { + .compatible = "st,stm32h7-i2s", + .data = &stm32_h7_i2s_regmap_conf + }, + {}, +}; + +static int stm32_i2s_parse_dt(struct platform_device *pdev, + struct stm32_i2s_data *i2s) +{ + struct device_node *np = pdev->dev.of_node; + const struct of_device_id *of_id; + struct reset_control *rst; + struct resource *res; + int irq, ret; + + if (!np) + return -ENODEV; + + of_id = of_match_device(stm32_i2s_ids, &pdev->dev); + if (of_id) + i2s->regmap_conf = (const struct regmap_config *)of_id->data; + else + return -EINVAL; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + i2s->base = devm_ioremap_resource(&pdev->dev, res); + if (IS_ERR(i2s->base)) + return PTR_ERR(i2s->base); + + i2s->phys_addr = res->start; + + /* Get clocks */ + i2s->pclk = devm_clk_get(&pdev->dev, "pclk"); + if (IS_ERR(i2s->pclk)) { + dev_err(&pdev->dev, "Could not get pclk\n"); + return PTR_ERR(i2s->pclk); + } + + i2s->i2sclk = devm_clk_get(&pdev->dev, "i2sclk"); + if (IS_ERR(i2s->i2sclk)) { + dev_err(&pdev->dev, "Could not get i2sclk\n"); + return PTR_ERR(i2s->i2sclk); + } + + i2s->x8kclk = devm_clk_get(&pdev->dev, "x8k"); + if (IS_ERR(i2s->x8kclk)) { + dev_err(&pdev->dev, "missing x8k parent clock\n"); + return PTR_ERR(i2s->x8kclk); + } + + i2s->x11kclk = devm_clk_get(&pdev->dev, "x11k"); + if (IS_ERR(i2s->x11kclk)) { + dev_err(&pdev->dev, "missing x11k parent clock\n"); + return PTR_ERR(i2s->x11kclk); + } + + /* Get irqs */ + irq = platform_get_irq(pdev, 0); + if (irq < 0) { + dev_err(&pdev->dev, "no irq for node %s\n", pdev->name); + return -ENOENT; + } + + ret = devm_request_irq(&pdev->dev, irq, stm32_i2s_isr, IRQF_ONESHOT, + dev_name(&pdev->dev), i2s); + if (ret) { + dev_err(&pdev->dev, "irq request returned %d\n", ret); + return ret; + } + + /* Reset */ + rst = devm_reset_control_get(&pdev->dev, NULL); + if (!IS_ERR(rst)) { + reset_control_assert(rst); + udelay(2); + reset_control_deassert(rst); + } + + return 0; +} + +static int stm32_i2s_probe(struct platform_device *pdev) +{ + struct stm32_i2s_data *i2s; + int ret; + + i2s = devm_kzalloc(&pdev->dev, sizeof(*i2s), GFP_KERNEL); + if (!i2s) + return -ENOMEM; + + i2s->pdev = pdev; + i2s->ms_flg = I2S_MS_NOT_SET; + i2s->ip_name = strstr(dev_name(&pdev->dev), ".") + 1; + spin_lock_init(&i2s->lock_fd); + platform_set_drvdata(pdev, i2s); + + ret = stm32_i2s_parse_dt(pdev, i2s); + if (ret) + return ret; + + ret = stm32_i2s_dais_init(pdev, i2s); + if (ret) + return ret; + + i2s->regmap = devm_regmap_init_mmio(&pdev->dev, i2s->base, + i2s->regmap_conf); + if (IS_ERR(i2s->regmap)) { + dev_err(&pdev->dev, "regmap init failed\n"); + return PTR_ERR(i2s->regmap); + } + + ret = clk_prepare_enable(i2s->pclk); + if (ret) { + dev_err(&pdev->dev, "Enable pclk failed: %d\n", ret); + return ret; + } + + ret = clk_prepare_enable(i2s->i2sclk); + if (ret) { + dev_err(&pdev->dev, "Enable i2sclk failed: %d\n", ret); + goto err_pclk_disable; + } + + ret = devm_snd_soc_register_component(&pdev->dev, + &stm32_i2s_component, + i2s->dai_drv, STM32_I2S_DAIS_NB); + if (ret) + goto err_clocks_disable; + + ret = devm_snd_dmaengine_pcm_register(&pdev->dev, + &stm32_i2s_pcm_config, 0); + if (ret) + goto err_clocks_disable; + + /* Set SPI/I2S in i2s mode */ + ret = regmap_update_bits(i2s->regmap, STM32_I2S_CGFR_REG, + I2S_CGFR_I2SMOD, I2S_CGFR_I2SMOD); + if (ret) + goto err_clocks_disable; + + return ret; + +err_clocks_disable: + clk_disable_unprepare(i2s->i2sclk); +err_pclk_disable: + clk_disable_unprepare(i2s->pclk); + + return ret; +} + +static int stm32_i2s_remove(struct platform_device *pdev) +{ + struct stm32_i2s_data *i2s = platform_get_drvdata(pdev); + + clk_disable_unprepare(i2s->i2sclk); + clk_disable_unprepare(i2s->pclk); + + return 0; +} + +MODULE_DEVICE_TABLE(of, stm32_i2s_ids); + +static struct platform_driver stm32_i2s_driver = { + .driver = { + .name = "st,stm32-i2s", + .of_match_table = stm32_i2s_ids, + }, + .probe = stm32_i2s_probe, + .remove = stm32_i2s_remove, +}; + +module_platform_driver(stm32_i2s_driver); + +MODULE_DESCRIPTION("STM32 Soc i2s Interface"); +MODULE_AUTHOR("Olivier Moysan, olivier.moysan@st.com"); +MODULE_ALIAS("platform:stm32-i2s"); +MODULE_LICENSE("GPL v2");
participants (3)
-
Mark Brown
-
olivier moysan
-
Rob Herring