What made the STM32F4-Discovery board so attractive for me was the fact that it comes with a nice on-board Audio-DAC with integrated amplifier, the Cirrus Logic CS43L22. However, getting the combination of STM32F4 and CS43L22 to produce any sound is anything but trivial for someone just starting out with ARM microcontroller development (like me). After having spend quite a few hours in the last week making it happen, and I finally did, I thought it might be worthwhile writing it down here in case others want to follow along.
This tutorial assumes that you set up your development environment already, and have at least managed to get an LED to blink. If not, there are several tutorials on the web that should be able to show you how to do this. This tutorial will make heavy use of the “Standard Peripheral Library” that ST provides for it’s microcontrollers. It is integrated into the IDE I am using (CooCox CoIDE), and I found it incredibly useful, especially in combination with the documentation that comes with in in form of a .chm (compiled HTML help file).
A little big of background
The way chips like the CS43L22 typically work is that they have one port for the digital audio signal, and one port for control signals. Both of these ports are essentially serial interfaces, the audio portion a fairly standard I2S interface, the control port a standard I2C interface. Looking at the schematic for the STM32F4-Discovery, the I2S lines connect to pins of the SPI3 peripheral, and the I2C lines to pins of the I2C1 peripheral. Therefore we’ll need to configure both of these peripherals. In addition to these two peripherals, the different GPIO peripherals for the various pins need to be configured. Lastly, or maybe firstly(!), all of these peripherals need to receive the proper clock inputs, otherwise they won’t function at all. Once everything is set up on the STM32F4 side, the CS43L22 also needs a bit of initializing via the control port.
Let’s get started with the clocks
As mentioned, the different peripherals on the STM32F4 that are involved in getting data to and from the CS43L22 will need to be configured and clocked. Let’s first enable the clocks. The GPIO peripherals that we need to configure are GPIOA (I2S_WS signal), GPIOB (I2C_SDA & I2C_SCL), GPIOC (I2S_MCK, I2S_SCK, I2S_SD) and GPIOD (Reset pin on CS43L22). Configuring and enabling clocks is done via the “Reset and Clock Control” (RCC) registers, which is best done via the standard peripheral library: #include “stm32f4xx_rcc.h”. To enable the clock signal for these GPIO peripherals, a single command is required:
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA | RCC_AHB1Periph_GPIOB | RCC_AHB1Periph_GPIOC | RCC_AHB1Periph_GPIOD, ENABLE);
The reason this can be done via one command is that all GPIO peripherals are connected to the same system bus (AHB1), which you can see on page 50 of the STM32F4 Reference Manual, and therefore receive the same clock signal. Similarly, the two serial peripherals that we need (SPI3, I2C1) also share the same system bus (APB1) and can therefore be enabled with a single command:
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1 | RCC_APB1Periph_SPI3, ENABLE);
Lastly, the I2S peripherals have their own PLL module to provide accurate standard audio sampling frequencies. To enable this clock signal, the following command is required:
Having dealt with the clocks, it’s time to configure the pins. Here it will be necessary to set mode of the pin (input, output, analog or alternate function), output driver configuration (push-pull vs. open-drain), speed, any pull-up or pull-down resistors. The Standard Peripheral Library provides a nice type-structure to deal with all those things: GPIO_InitTypeDef (make sure to #include “stm32f4xx_gpio.h”). For the reset signal this structure should look like this:
PinInitStruct.GPIO_Pin = GPIO_Pin_4;
PinInitStruct.GPIO_Mode = GPIO_Mode_OUT;
PinInitStruct.GPIO_OType = GPIO_OType_PP;
PinInitStruct.GPIO_PuPd = GPIO_PuPd_DOWN;
PinInitStruct.GPIO_Speed = GPIO_Speed_50MHz;
The first command declares the structure, then we fill the individual fields; the last command actually configures the specific pin (by setting the appropriate values in the registers). Here you have to specify which of the GPIO ports A-I you are configuring . In the case of the reset signal pin it is GPIOD. You can re-use the same structure to configure the other pins as well, making any necessary adjustments. If pins on the same GPIO port share the same configuration you can specify multiple pins in the structure, e.g.:
PinInitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_9; //I2S SCL and SDA pins
The mode for all I2S and I2C pins will be “Alternate function” (GPIO_Mode_AF ). The output type for the I2C pins are “open drain” (GPIO_OType_OD) with no pull (GPIO_PuPd_NOPULL). The output type for the I2S pins is “push-pull” (GPIO_OType_PP), also with no pull. Now the pins are readily configured for their intended purpose. However, since almost all pins are multiplexed between multiple alternate functions, we still need to map it to the appropriate one. This is done with the following command from the library:
GPIO_PinAFConfig(GPIOA, GPIO_PinSource4, GPIO_AF_SPI3); //connecting pin 4 of port A to the SPI3 peripheral
Notice that the second parameter for this function is different from the values we used for the GPIO_Pin field of the initialization structure. Here you can’t run a single mapping command for multiple pins – it has to be run individually for each pin.
The next step is to configure the actual peripherals. As with the GPIO configuration, the library provides convenient structures and functions to do just that. Since the I2S function is part of an SPI peripheral we will need to #include “stm32f4xx_spi.h”
I2S_InitType.I2S_AudioFreq = I2S_AudioFreq_48k;
I2S_InitType.I2S_MCLKOutput = I2S_MCLKOutput_Enable;
I2S_InitType.I2S_Mode = I2S_Mode_MasterTx;
I2S_InitType.I2S_DataFormat = I2S_DataFormat_16b;
I2S_InitType.I2S_Standard = I2S_Standard_Phillips;
I2S_InitType.I2S_CPOL = I2S_CPOL_Low;
Most of the structure fields should be fairly self-explanatory: we’re specifying the audio frequency, whether or not we’re sending the master clock signal, that the module is a transmitter in master mode, the number of bits per sample, the data protocol, and the clock polarity. The last command again actually implements these changes. Once the initialization command is completed we can turn on the peripheral itself:
I2C initialization and connecting to the CS43L22
Alright, almost there! Just as the GPIOs and the I2S, we need to configure our I2C interface to talk to the external DAC. By this time you may already have guessed that the Standard Peripheral Library again provides structures and functions to achieve this. You will need to #include “stm32f4xx_i2c.h”. Again, the fields of the structure should be fairly self-explanatory. I set up my I2S module in this way:
I2C_InitType.I2C_ClockSpeed = 100000;
I2C_InitType.I2C_Mode = I2C_Mode_I2C;
I2C_InitType.I2C_OwnAddress1 = 99;
I2C_InitType.I2C_Ack = I2C_Ack_Enable;
I2C_InitType.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_InitType.I2C_DutyCycle = I2C_DutyCycle_2;
The OwnAddress field can be any valid I2C address (see restrictions here), but make sure it doesn’t collide with other addresses on the bus (e.g. the DAC’s I2C address). For the specific case here it actually shouldn’t matter too much as the STM32F4 is the only “master” on that bus. Once again, do the actual initialization with:
I2C_Init(I2C1, &I2C_InitType); //initialize the I2C peripheral ...
I2C_Cmd(I2C1, ENABLE); //... and turn it on
At this point we’re done configuring everything on the STM32F4 side, and we’re just left with configuring the DAC itself. To do that we first need to turn it on by bringing the reset signal high:
We should now give it a bit of time to go through its startup routines before proceeding. Just like a microcontroller, the CS43L22 has various registers to control its operation, and we use the I2C interface we just initialized to read from and write to these registers. This is a fairly standard way of doing this, which you’ll find on many other devices. Section 5 of the datasheet of the CS43L22 describes the general procedures for reading and writing registers via I2C. In short, each access begins by sending the register address (the data sheet calls it a “map byte” since the most significant bit actually codes what should happen for subsequent reads or writes). When writing to a register you can immediately send the value of the register (or registers when writing to multiple consecutive registers). When reading from registers, you now have to stop communicating as “Transmitter” and switch to “Receiver” Mode, to now receive the contents of the register previously addressed with the map byte. The standard peripheral library again as convenient functions to do all this:
- I2C_GenerateSTART to generate the so-called start condition
- I2C_Send7bitAddress to send the address to the “slave” device (here the CS43L22)
- I2C_SendData to send the actual data (mapbyte, register value)
- I2C_ReceiveData to receive register values from the DAC
- I2C_GenerateStop to terminate the transmission
Of course we can’t simply fire off these commands one after the other. Why not? Because the speed with which the I2C bus can transmit the data is much lower than we can set bits in the I2C peripheral’s registers or write data to the output buffers (which is all these commands are doing). We should also check if the CS43L22 has actually responded to our address call (and to subsequent transmissions) with the “Acknowledge” bit. Two functions of the library can help with this:
- I2C_CheckEvent is useful for checking if the various I2C events have occured
- I2C_GetFlagStatus to check for other conditions (e.g. to make sure the I2C bus is not busy before attempting a transmission)
If all of this makes absolutely no sense to you, I suggest to read up at least a little bit on the basics of the I2C protocol as this would require its own tutorial. A very basic summary of this can be found in chapter 23.3 of the “STM32F4 Reference Manual” (starting at page 576). There’s a few diagrams that are helpful; for our case the ones on page 582 (master transmitter) and page 584 (master receiver) are most relevant. But let’s return to the registers of the DAC.
After the CS43L22 has come out of reset state, it is still not doing very much, because the default state of the “Power Control Register 1” (address 0x02) is in an “off” state. The datasheet recommends to keep it this way until all the other registers are set to the desired values. The datasheet then recommends to “load the required initialization settings”, a series of reads and writes to undocumented registers. Surprisingly, the “waveplayer” demo that comes with STM32F4-Discovery firmware examples doesn’t go through this sequence. I implemented the sequence in my own code, but maybe it would work without it as well. In any case, you should set a few of the other registers, such as “Power Ctl 2” (0x04), “Clocking Control” (0x05) and “Interface Control 1” (0x06). The descriptions of the registers in the datasheet are fairly good, and for many of the other registers, the default settings are quite sensible and do not have to be changed. Once all the appropriate settings were made you can actually turn on the device by sending the byte 0x9E to register 0x02 (Power Ctl 1).
If all went well, both of your devices are now ready to produce some sound. To do that you simply have to fill the transmit buffer of the SPI3 peripheral on the STM32F4 with the desired data. There are multiple ways of doing this, such as using the DMA controller. Another, simpler way is to manually transfer the data with one of the functions from the Standard Peripheral Library:
Here, SPI3 is of course our SPI peripheral that we put into I2S mode, and theData is a 16-bit unsigned integer value that will be send to the DAC. Between repeated calls of this function, the SPI peripheral automatically switches the WS signal (to indicate left or right audio channel), so you don’t have to worry about it. However, before making repeated writes to the transmit buffer, you should make sure it is empty (i.e. the contents of the transmit buffer have been written into the shift register and are being transmitted). This can be done by setting up an interrupt, or by checking manually with:
We should now be able to hear something on the headphone output of the STM32F4-Discovery. If not, it’s time to debug a little. I was struggling a long time getting everything to work. Even after I verified that the STM32F4 keeps properly sending data I still couldn’t hear anything other then a little ‘plop’ at the very beginning. Once I probed the signals, I noticed that the WS signal was not changing – I had forgotten to turn on the clock for GPIOA. Duh!!! After that, everything worked fine. I will now attempt to clean up the code a bit, and convert many of the manual checks into interrupts and possibly set up the DMA controller for the transfer of the audio data to the SPI peripheral.
You can now download the code I describe in this post here: simpleAudioExample.zip. To have samples to be sent to the codec, it generates (and crudely filters) a very simple saw wave. The code is probably not very pretty, and certainly not very efficient, but should be enough to produce some output from the codec. Some things may need to be adapted to your development environment.