Raspberry Pi I2S Driver (Slave)

Created by Taylor Roorda, last modified on Mar 30, 2017

Introduction

Disclaimer: This is my first non-trivial Linux project after coming from a bare metal microcontroller background. This article is intended for people in a similar position. Experienced Linux developers will probably have a different approach.

After a recent interest in audio processing, I decided to purchase MikroElektronika’s Audio Codec Proto (Digi-Key: 1471-1228-ND), a breakout board for the Cirrus Logic WM8731 codec. The codec uses I2S for digital audio data and I2C for the control interface. For a host processor, I chose the Raspberry Pi Zero which I had lying around unused. This was tested exclusively with the Pi Zero so it may not work on other version of the Pi without modification.

My hope was that there would be a driver within the Raspberry Pi community that would allow me to work with the raw audio data. Most seemed to focus on playing music files. The first option that looked promising was the use of the snd_soc_wm8731 and snd_soc_rpi_proto modules which use the codec like a sound card through ALSA (Advanced Linux Sound Architecture). I experimented with this for a while, but was unable to get audio in or out in the end or find a way to work with raw data.

Second, I came across a driver made by philpoole on the Raspberry Pi forums. Looking through his source code gave me a better idea of the inner workings of the Pi’s I2S interface, but it was still ALSA based, transmit only, and still wasn’t quite what I wanted.

After all my searching, I was inspired by a comment I saw on Stack Overflow to make my own driver that did only what I wanted it to. My main requirement was to transmit and receive raw audio samples with my setup. Ideally, I would have liked to do both at the same time, but didn’t think the Pi Zero would handle that very well. I also wanted it to be generic and only handle the communication protocol. Codec specific requirements can be easily handled externally.

Operation

The Software

At its core, this program is a type of loadable kernel module called a character driver. “Character” because it transfers data from the Linux kernel to the user space through a stream of characters (or bytes). For a more detailed look at kernel modules and character drivers, I highly recommend looking at The Linux Kernel Module Programming Guide. It’s a bit dated, but still very useful for getting started.

Essentially the driver only describes how file operations like open, close, read, and write work when called on a specific file, or node, in /dev that represents the I2S interface we want to use. An ioctl function can also be used to change device settings or retrieve other information from the driver. A kernel module only has two required functions: init_module and cleanup_module. When the driver is first loaded into Linux, the init_module function is called to assign the driver a major number and run any setup code the driver needs. The major number is then used to link the device file and driver together. This number is also required when the driver is unloaded during cleanup_module to properly remove the module.

Most of the action takes place in the device_read and device_write functions. These functions transfer data between sample buffers in the kernel and a program running in user space. The size of the kernel buffers was chosen arbitrarily to be 16K samples. When the transmitter interrupt triggers, it pulls data from the corresponding buffer and fills up the hardware FIFO. Likewise when the receiver interrupt triggers, it empties the hardware FIFO into the kernel buffer.

The Hardware

The ARM peripheral data sheet describes all the registers used by the I2S peripheral and their physical and bus addresses. These addresses can’t be used directly by the processor so they need to be remapped. The ioremap function takes the physical address and a size and returns a virtual address usable by the processor for reading and writing to the hardware. The beginning of the ARM data sheet has more information on the differences between virtual, physical, and bus addresses.

Hardware setup is done in the i2s_init_default() function. The major things to set up are the communication mode (master/slave), channel settings, and interrupt or DMA usage. By default, the Pi is the slave making the clock and frame sync signals inputs from the codec. The reason for this is because there were a lot of complaints on the forums about the Pi not being able to generate the correct frequencies for audio. The codec board has its own oscillator and is better able to generate the correct clock frequency. Since my codec is 24 bits, the default channel width is also 24 bits with the most significant bit positioned on the second rising clock edge each frame. Finally, interrupts are enabled to signal when data needs to be written to the transmitter or read from the receiver. Using the DMA would be more efficient for the processor, but interrupts were easier to set up. A future revision may incorporate the DMA, but interrupt-driven version works well enough for now.

Supporting Scripts

In addition to the module itself, there are two simple Bash scripts to for loading and removing the module. The i2s-install.sh script sets the relevant GPIO pins to use their I2S functions and builds the module. Setting the GPIO alt functions is done using a wrapper from Tim Giles from the Raspberry Pi forums. The i2s-uninstall.sh script returns the GPIO to their default function as inputs with WiringPi and removes the module.

As a side note: configuring the GPIO pins could be done in the driver’s initialization code, but the GPIO memory is claimed by a different driver. I could access it anyway, but it’s just as easy to use a small, separate program.

I also use a very simple Python script to set up the WM8731 codec over I2C. The source for this is included in the repo for others that might be using this codec.

Usage

In order to compile the module, you’ll need to have the kernel source somewhere on the Pi. Typically this is in /lib/modules. I got stuck on this part for quite a while and I believe rpi-update was what eventually got me what I needed.

Update firmware:

# Run these if you don't have the kernel source on your device
sudo apt-get update && sudo apt-get upgrade
 
# Verify that there is an directory in /lib/modules that matches what you get from uname -r
uname -r
ls /lib/modules
 
# You may need to run this too if the above doesn't work
# sudo apt-get install rpi-update
# rpi-update

Compile gpio_alt:

# Credit to TimG for this program
gcc -o gpio_alt gpio_alt.c
sudo chown root:root gpio_alt
sudo chmod u+s gpio_alt
sudo mv gpio_alt /usr/local/bin/

This program is used in the install script.

Check for WiringPi:

gpio -v

This is used in the uninstall script. If you get nothing, see the install instructions at http://wiringpi.com/download-and-install/

Clone the driver repo from Github (https://github.com/taylor-roorda/raspberry-pi-i2s-driver.git). Replace ${shell pwd} with the directory of your choice if desired.

git clone git@github.com:taylor-roorda/raspberry-pi-i2s-driver.git ${shell pwd}

Build and load the module:

./i2s-install.sh

The driver should now be installed and ready to use. You should be able to read and write to /dev/i2s like you would a normal file. Simplified example of basic operations:

#include <uinstd.h>
 
int i2s = open("/dev/i2s", O_RDWR);
 
// Read a single sample
// All samples are stored as 32 bit values so a single sample transfer is always 4 bytes.
int32_t sample;
read(i2s, &sample, 4);
 
// Write a single sample
write(i2s, &sample, 4);
 
close(i2s);

The readme on Github has additional details on using the driver and there is an example C program, driver_test, in the repo for reference.

To remove the module:

./i2s-uninstall.sh

Observations

One of the shortcomings of this driver is its heavy CPU usage. During my tests, I was using a sample rate of 48kHz and I struggled with buffer overflow and underflow if my test program was doing anything other than reading or writing samples. Usually this resulted in the kernel hanging so the current version will disable the interface after a timeout period. Using the DMA should fix this since the amount of time spent in interrupts could be reduced. The bigger Raspberry Pi models should handle it better since they have more processing power. Ultimately, this sort of approach would be better suited for a bare metal microcontroller or something running an RTOS.

There is a lot of examples and documentation for the Raspberry Pi when working from user space, but little when working at the kernel level. And the little that can be found can be confusing. This makes getting started a little difficult if you’re not already a Linux veteran. Finding answers to fairly simple questions like “Which interrupt number is X assigned to?” or “Which DMA channels are available for me to use?” can be a real challenge.

For example, the ARM data sheet says the I2S interrupt is IRQ 55. In the Linux headers for the Broadcom chip, platform.h says the I2S interrupt is IRQ 81. After trying both and getting no results, I looked at the output of cat /proc/interrupts. Since I had I2C and SPI enabled, I saw they were assigned to IRQs 77 and 78 respectively. The I2S interrupt should be the next one. IRQ 79 ended up working and I’m still not sure why exactly that is. This ended up being the deciding factor on the choice between interrupts and DMA. Interrupts were easier to get started with since I was actually able to determine the right IRQ number.

Conclusion

The driver presented here isn’t meant to compete with the default drivers you might find already in Linux. It simply provides low level access to the Pi’s I2S interface for those that might want to work with it. While you won’t be doing any real-time audio processing, it can be used for playing or recording audio files, arbitrary waveform generation, or other simple audio applications. It may also be useful for those interested in developing their own drivers for I2S or otherwise.

External Links

Here are a few of the useful documents I came across while working on this driver:

Questions/Comments

Any questions or comments please go to Digi-Key’s TechForum