Lab 4: Kernel Driver for Playing Audio

In this lab, you will add a new dimension to Space Invaders: sound! The PYNQ board contains a codec chip that can play sounds under software control. With this chip, and a little effort, you will be able to hear those aliens marching back and forth; hear the strange sounds of the alien space ship as it appears and tries to get away, and hear the explosion when those evil aliens blast your tank.

This lab will provide your first experience writing kernel code. You will write a kernel device driver for the audio module that transmits data to the codec chip.

Preliminary

  • Make sure you have completed the assigned reading from the LDD3 textbook.
  • Read the documentation on Linux Platform Devices and the Device Tree. Note that, for this lab, this is informational only. The Device Tree is already setup for this lab. Still, it is important to know how the device tree works, so study this.
  • Read the documentation on Linux Device Drivers.
  • Read the documentation on the Audio Hardware.

Github Repository

You will go back to working individually for this lab and the remaining labs. Both lab 3 team members should copy their lab3 game code to their individual repositories. Do not commit your lab 4, 5, or 6 code to your shared repository.

Overview

You will be creating a loadable kernel module, which allows you to write code that runs in kernel space. This kernel module will be a driver for the audio codec.

Your driver code will need to do several things. Some of the major tasks:

Registering with Linux

You need to register your driver with Linux, so that it calls the functions in your driver at appropriate times:

  • The module_init() and module_exit() macros will register your kernel module with Linux so that your init and exit functions are called when Linux loads and unloads your module (Milestone 1).
  • You need to inform Linux that your module is a hardware driver that supports platform devices found in the device tree (platform_driver_register(), Milestone 1).
  • You will need to request major/minor numbers from Linux to use for your devices (alloc_chrdev_region(), Milestone 1).

Communicating with the Hardware

You driver will need to communicate with the audio hardware using register reads/writes. Before you can do that, you will need to:

  • Retrieve the base address from the device tree (platform_get_resource(), Milestone 1).
  • Reserve this address space with Linux (request_mem_region(), Milestone 1).
  • Map this physical address space to a virtual address pointer (ioremap(), Milestone 1).

Creating a Character Device

Your driver can’t be used from user space applications until you create some sort of interface that is accessible to user programs. To do this, you will create a character device, accessible to user space as a device file in /dev. This requires you to do the following:

  • Before you can create a character device, you to first create a device class. You can create this class once when your driver loads (class_create(), Milestone 1).
  • Create a new character device (cdev_init(), then cdev_add(), Milestone 1).
    • When you create your character device, you will need to point Linux to functions in your module that can be called when user code performs a read() or write() on your device.
    • In Milestone 1, the read() and write() functions will only print log messages.
    • In Milestone 2, the read() and write() functions will interact with the audio device.
    • In Milestone 3, you will add another file operation, ioctl.
  • Create a device file in /dev (device_create(), Milestone 1).

Handling Interrupts

The audio hardware will send an interrupt when it needs more audio data. Unlike previous labs, this interrupt line is not connected to an interrupt controller that you manage from user space. Instead, it is connected directly to the Global Interrupt Controller (GIC) that is part of the ARM processor. The GIC is managed by Linux. As such, you will be interacting directly with the Linux interrupt system.

To handle and respond to interrupts generated by the audio hardware, you will do the following:

  • Query the device tree and get the virtual IRQ number. Note, the device tree lists the physical interrupt line; however, Linux will provide you with a virtual number to use instead (platform_get_resource(), Milestone 1).
  • Register your interrupt service routine with Linux (request_irq(), Milestone 1)

Driver vs. Device

Make sure you recognize the distinction between the driver (kernel module) and the device. Typically, one Linux driver would support multiple devices of the same type. To be clear:

  • The driver/module is loaded and unloaded once, regardless of the number of devices that it manages. Module (driver) loading and unloading is done using functions provided to module_init() and module_exit().
  • Each time Linux finds a device managed by your driver, it will call the functions you provided in the .probe and .remove entries of the struct you provided when registering your module as a platform device driver (platform_driver_register()).

Based on the above, it is important to recognize which of the driver features should be done once when the module is loaded, and which should be done for each device.

That said, the driver you make in this lab is only going to support one device. However, as good coders, we still want to organize our code in such a way that it could more easily be extended in the future.

Milestone 1

Implementation

In this milestone you will create the skeleton of a kernel driver for sending audio data to the codec.

Your kernel driver should:

  • Contain code in audio_init() and audio_exit() to init and unload the driver, and register itself as a platform driver, with audio_probe() and audio_remove() functions to init and unload a device.
  • Upon device probe, the driver should:
    • Create a character device as described above. The character device should implement read() and write() functions; however, for this milestone, these functions should simply print a message to the kernel log.
    • Setup a virtual address pointer as described above.
    • Setup an interrupt handler as described above.

Make sure that you also:

  • Create helper functions to read and write registers in the audio device.
  • When the driver is unloaded, undo all appropriate actions that were perform when the driver was loaded.
  • When a device is removed, undo all appropriate actions that were done when the device was added.

Set up your code so that the TAs can test it in the following way:

  • Loading and unload your driver (insmod and rmmod) TWICE should work without error and have appropriate logging messages. For example, you should at minimum print the major number, physical and virtual address and IRQ number. It must also print a message to the log when read() and write() are called. You may choose to add more messages to help you debug your driver. A simple script is provided that loads and unloads the module twice and prints recent entries in the log.
  • Add code at the end of your probe function that enables the interrupt output of the audio core. Since there is no data in the FIFOs, this should immediately trigger your ISR. In your ISR print a message to the kernel log and then disable the interrupt output of the audio core (or you will be stuck in an endless loop). This ISR message must show up in the log when the TA loads your module.

Pass Off

To ensure the TAs can run and grade your driver, you should:

  • Create a new user space program in ‘‘userspace/apps/lab4_m1’’, with appropriate CMake file to create an executable named ‘‘lab4_m1’’. Thus, when the TAs build your user space code, it should produce an executable ‘‘userspace/build/apps/lab4_m1/lab4_m1’’ (the TAs grade using scripts so be sure to get this name correct).
  • This program should open your device file, perform a read() and write(), and then close it. The TAs will run this program and inspect the kernel logs to see that the read() and write() functions in your driver were executed.

Milestone 2

In this milestone you will write code to read and parse WAVE files, and update your kernel driver to play sound data.

Specifications

  • Initialize the audio codec chip via I2C from userspace using the provided functions. See Audio Hardware.
  • Update your kernel driver such that:
    • Your device struct contains a statically-sized buffer (ie, an array) to store audio samples the user will write() to your driver (512KB should be large enough).
    • In your write() function, you accept a buffer containing an audio clip. When this occurs your driver will stop playing any existing audio clip, and immediately begin playing the newly provided audio clip. In the write() function you will need to:
      • Immediately disable interrupts from the audio core.
      • Copy the audio data from user space to your buffer (including safety checks on the user space pointer) - LDD page 64.
      • Make sure the audio core has interrupts enabled.
    • Your ISR will be responsible for actually playing the audio clip. In your ISR:
      • Determine how much free space is in the audio FIFOs and fill them up with the next audio samples to be played.
      • Once the end of the audio clip is reached, disable interrupts on the audio core.
    • In your read() function, return one byte of data, with value 0 or 1, indicating whether an audio sample is currently being played.
  • Write a user space program that parses WAVE files and sends the PCM data to the audio driver.
    • The audio data you read from the WAVE files (in user space) will be an array of 16-bit samples.
    • The audio data you write to the hardware FIFOs (in kernel space) needs to be 24-bit samples (there’s no 24-bit data type in C, so use a 32-bit type)
    • This means you will need to convert each sample. For example, for a 16-bit PCM sample (int16_t), you will want to left-shift the data by 8 bits and then store it in a 32-bit data type (int32_t). This should be done in user space as you read the file into memory. You can do this conversion in user space or kernel space.
    • This means that your kernel write() function will take an array of 32-bit samples.

Passing Off

  • Create a user space executable that takes a WAVE file path as a command-line argument, and plays the audio clip, twice in a row, on the speakers using your audio driver. The TAs will run this executable when doing your pass-off.
  • This new user space program should be located in userspace/apps/lab4_m2, with an appropriate CMake file to create an executable named lab4_m2. Thus, when the TAs build your userspace code, it should produce an executable userspace/build/apps/lab4_m2/lab4_m2, which takes a single command-line argument.

Milestone 3

In this milestone you will add an ioctl interface to your driver to allow userspace to send special commands to your audio driver. You will also update your Space Invaders code to play sound effects. This is demonstrated in this video.

Specifications

  • Extend your kernel driver to add ioctl to the list of file operations supported by your character device. You should support two ioctl commands:
    • Turn on looping for the current audio clip.
    • Turn off looping for the current audio clip.
  • Integrate sound into Space Invaders by generating the following sounds during game operation:
    • WAVE files are provided here.
    • the “marching” sound the aliens make as they move back and forth across the screen is comprised of four separate walk1, walk2, walk3, and walk4 sounds. You start with walk1 the first time an alien moves and then play the next sound in the sequence on each successive move, cycling back to wave1.
    • the sound that the red flying saucer makes as it flies across the screen (use your looping functionality for this)
    • the explosion noise that occurs when your tank is hit by an alien bullet,
    • the explosion noise that occurs when an alien is hit by a tank bullet,
    • the “ping” sound that the tank makes when you fire a bullet, and
    • the sound the flying saucer makes if you hit it with a bullet.
  • Implement volume control in the following manner:
    • To increase volume, slide sw0 up, press btn3. Each press increases the volume a preset amount, such as 10%.
    • To decrease volume, slide sw0 down, press btn3. Each press decreases the volume a preset amount, such as 10%.

Passing Off

Your Space Invaders game should be operating with all of the sound effects. As a reminder, the coding standard requires that your code compile without warnings. The TAs will verify this at pass off (both kernel and user code).

Important: All necessary WAVE files should be committed as part of your repository. Your game should not rely on these files being located at a specific absolute path, as the TAs will likely clone your repo to a different path. You can use read_link to get the path to your space_invaders executable, and then use a relative path from that location to access the WAVE files in your repository.

Code Submission

Follow the Submission Instructions.

Resources, Tips, and Hints

Milestone 1

audio_codec.c provides a starting framework for your driver software. Read over it carefully before coding anything.

A few notes about the provided code:

  • Your driver only needs to support a single audio device, so we have allocated a single struct audio_device as a global variable at compile time.
    • You might want to add a check in your probe function to make sure it isn’t being called more than once and overwriting the data in your struct.
    • If you need to add new variables related to the device they should be placed within this struct. Variables related to the driver should be placed as static global variables.
  • In our driver we are going to assume that the character device is only ever opened by one user program at a time. Otherwise, we would need to handle parallel accesses and deal with race conditions. LDD3, Ch 6, Single-Open Devices discusses how you can enforce this property. You can implement this enforcement if you like.

Things to remember:

  • MAKE SURE you pay close attention to the return values of every function you call, and handle any errors. Not all functions return a negative number on error. Functions that return pointers often encode errors differently. (See LDD3, Ch 11 Pointers and Error Values)
  • Add lots of kernel logging messages to help with debugging

Error handling:

  • In init and probe, if you encounter an error, you will need to roll back any changes you have made. A common technique to do this is using labels and gotos as shown in the pseudocode below:
  int err;
  
  err = doTask1();
  if (err)
    goto errTask1;
  
  err = doTask2();
  if (err)
    goto errTask2;
    
  err = doTask3();
  if (err)
    goto errTask3;
    
  errTask3:
    undoTask2();
  errTask2:
    undoTask1();
  errTask1:
    return err;

Some resources to help you with the kernel function calls:

  • alloc_chrdev_region:
    • see LDD3, Ch3, Major and Minor Numbers and Allocating and Freeing Device Numbers.
  • class_create:
    • All devices need to belong to a class, so you will need to create a device class for your audio code. This class pointer will be used when you create the device file.
    • You can store the struct class* as a static global variable.
  • platform_driver_register
    • There is quite a bit of background you need to read before understanding the significance of this call. Read the course documentation on linux platform devices.
    • You will need to pass in a pointer to a struct platform_driver. You can create a static global variable for this struct.
    • xillybus.com provides a good example of how your struct should be initialized. Remember that the .compatible field needs to exactly match the string in our device tree.
    • Read about and study device-tree source files for this project here. Search for a compatible string that looks like it is related to an audio codec.
  • cdev_init and cdev_add
    • LDD3, Ch3 discusses these functions in the section Char Device Registration.
  • device_create
  • platform_get_resource
  • request_mem_region and ioremap
    • See LDD3, Ch 9, I/O Memory Allocation and Mapping.
  • request_irq
    • See LDD3, Ch 10, Installing an Interrupt Handler.
  • iowrite32
    • See LDD3, CH 9, Accessing I/O Memory.
    • Don’t forget the rules of Pointer Arithmetic.
    • Basically, the register offsets are byte-aligned, but if you’re not careful, the program will calculate addresses as if they were word-aligned.

Milestone 2

  • Consult the documentation on the Audio Hardware.
  • Consult the LDD3 textbook.

Milestone 3

  • Read LDD3 chapter 6, the first section on ioctl.
  • Use .unlocked_ioctl in the struct file_operations (.ioctl as the text suggests is out of date). Even with this update, you still use the ioctl() system call in your user-space code for space invaders.
  • The ioctl interface must be implemented as described in LDD3. Make sure the ioctl command values are created using the _IO* macros. An example of these are given on pages 138-139 of LDD3.
  • You probably want to read all of the WAVE files and store them in arrays at the startup of you game. Then each time you want to play a sound effect you can pass the appropriate array buffer to the driver. This avoids repeatedly reading the WAVE files each time you play a sound.
  • No sound mixing is required for this lab, simply play one sound at a time.

Other