How to Get I2S working on an STM32 MCU

David Ramsay
7 min readFeb 17, 2018

--

I’ve been at this one for a while this week, and after finally succeeding I figured I’d make an ultra-quick outline to help other folks get this up and working. I don’t like giant/opaque IDEs and build environments, so this is how to set up a simple makefile-based solution.

I couldn’t find a working I2S example anywhere in the MBED community for any STM32 processor, despite ST having written libraries that support it. The move towards ‘arduino-esque’ development is great if you use popular features on popular processors, but if you want full feature support for less common variants in the family, things get too difficult to quickly in the heavily abstracted world of MBED. Life is better with full control and full visibility of the build process.

Setting up the Toolchain

The secret to your success is the STM32CubeMX software, which is available only on windows (groan). I’m a mac user, so my final setup required:

1. an installation of the latest arm-none-eabi-gcc on mac (https://developer.arm.com/open-source/gnu-toolchain/gnu-rm/downloads — I think this is available with brew, but I ended up downloading directly from GNU because the when I tried it with brew it was out-of-date)

2. an installation of the stlink utility on mac (https://github.com/texane/stlink — up-to-date install with brew)

3. STM32CubeMX on a windows VM (http://www.st.com/en/development-tools/stm32cubemx.html at the bottom)

Hardware

To start things off, we’re going to get basic I2S audio into the STM32. I started with the SPH0645 I2S MEMs Mic breakout board from Adafruit, and the STM32F767ZI Nucleo Board from ST (which has an embedded STLink programmer on board, and was chosen because there’s some cool ongoing work on porting tensorflow to it I wanted to check out). To set up the board properly, we need to connect our BCLK to PA5 (CN7–10), our WS/LRCLK to PA4 (CN7–17), and our SD/DOUT to PA7 (CN7–14). On this board we have to watch out — PA7 gets reused for a few different things. You must remove the default jumper at JP6 or else you won’t get any data coming in. Finally, we hook the SEL line and GND to Ground, and our power pin up to 3.3V on the Nucleo headers.

The Cube

Now it’s time to fire up the Cube, select our processor, and start generating our default project. Here are the three steps:

Turn that I2S on! Put it in Master Mode!
Configure that thing. For a MEMs mic input, be sure to put it in Receive Mode. These are the working settings for the SPH0645 (Phillips, 24bit/32bit frame MSB, 16k).
Go to ‘Project > Settings’ and make your selection a Makefile. Pick a location for your project as well. It’s also nice to go to the Code Generator tab and choose whether or not you want all possible library files included in your project (which can be overwhelming), or just the ones you’ve chosen during setup.

Once you’re done with the above steps, you can simply click ‘Project > Settings > Generate Code’ and we’re off to the races.

The Flash

The only edit I’ve needed to make is the path prefix for arm/gcc tools in the Makefile (I personally have them on my default path). Otherwise, you should be able to navigate to the folder, type ‘make’, and successfully compile the project.

Once this is working, we can edit our main.c while loop with the simple code:

uint16_t data;

while(1){

HAL_StatusTypeDef result = HAL_I2S_Receive(&hi2s1, &data, 1, 100);

}

With any luck, you can then simply type:

st-flash write build/<yourproject>.bin 0x8000000

Audio is flowing!

Success!

But Really? Is it working?

Ok, maybe ‘success’ is a little premature. We’re seeing data on the audio lines, but that doesn’t mean we’re getting the data in a useful format on board in our firmware. Let’s actually take a look at what’s going on.

Zoomed, typical frame from the SPH0645, from our digital logic analyzer.
SPH0645 Datasheet. 2’s compliment, MSB left-aligned 24-bit data on a 32 bit frame, transitions on rising edges.

First let’s see what to expect. When things are quiet, the microphone reports a pretty consistent value shown above. We’re expecting 24 MSB-aligned bits (though only 18 have information per the data sheet), which should conveniently be handled correctly by a standard 32 bit signed int in C (a long or typedef-ed int32_t). Yeah the last 8–14 bits are garbage, but that doesn’t really matter. For our first value we see 1111 1000 1011 1001 0100 0000 = 0xF8 0xB9 0x40 on the logic analyzer, so that’s what we’ll be looking for in our debugger.

Let’s edit our while loop to actually read in the full data we need:

uint16_t data_in[2];

while(1){

volatile HAL_StatusTypeDef result = HAL_I2S_Receive(&hi2s1, data_in, 2, 100);

if (result == HAL_OK) {
volatile int32_t data_full = (int32_t) data_in[0] << 16 | data_in[1];
volatile int16_t data_short = (int16_t) data_in[0];

volatile uint32_t counter = 10;
while(counter — );
}

}

I’m not the greatest C coder, so I sprinkle in ‘volatile’ liberally when running these sorts of initial tests, because frequently the variables we’re testing aren’t actually getting used meaningfully (we’re just reading them in at this point) and thus there is some danger of the compiler optimizing them out. You could also add a flag in the compilation step of your makefile to prevent this. After we make and flash this new version, let’s fire up our debugging tools. In one window, we start our GDB debugging server. The stlink tool takes care of all the messy work for us.

st-util

Now we have a GDB server running on our local port 4242!

In a new window, we need to connect to our GDB server to start an interactive debugging session:

arm-none-eabi-gdb -tui -eval-command=”target extended-remote localhost:4242" build/<your-project>.elf

Once in GDB, type ctl+x, then a if you don’t see the nice code visualization. (ctl+x, then 2 adds a nice visualization of the assembly instructions, if you’re interested in that as well).

I then type b main, which will put a breakpoint as soon as you enter the main function call. From there it’s easy to identify lines you might want to break at in the top console, and add more breakpoints by line number (b 116 or b main.c:116 for line 116). Typing c will continue to the next breakpoint, and cntl-c will stop things if you get in a loop (but will bring you to the SIGTRAP interrupt vector, not the most useful place to be).

Once we’re in main, I find myself using n to step through each command. To check our variables, we first type set output-radix 16 to make them print in hex (only once), and then info locals.

What’s that you say? It doesn’t look like it’s working? Well of course it’s not. Nothing works on the first try, this is embedded programming.

What we do notice is that it seems to call I2S receive function once, but then we get sucked into an I2S timeout I2S library function call on every consecutive loop. This isn’t wholly unexpected — things like timeouts and watchdog timers have a propensity to muck things up when we start stepping through our code at a snail’s pace.

Luckily, STM32 has a feature called ‘debug freeze’ to fix this. Inside your main function before the HAL_INIT call, add the following macro:

__HAL_DBGMCU_UNFREEZE_IWDG();

After you recompile, reflash, and restart your debugging tools, you should find that everything is much better behaved.

It works!

Now that we can reasonably step through our function, we should expect our I2S to return some timeouts, some zero values (when reading the right word of our left/right word clock, since I2S is inherently stereo), and some values close to our expected 0xF8 0xB9 0x40 we saw on the data line with our logic analyzer. After a bit of stepping through, we finally get the confirmation we’ve been looking for:

look at that beautiful data!

0xF8 EB C0 is very close to what we expect! Looks like things are working, and we’re able to read in the data in a few appropriate formats (signed 16- and 32- bit integers).

On Pain

This example took more hours and more my sanity to get running than I care to admit. I hope it saves you some of yours.

Here’s a nice collection of someone else’s hard-earned STM32 tricks. This guy knows what’s up. Do us all a favor and contribute when you find your own STM32 idiosyncrasies and stumbling blocks.

--

--