How the CLASSiC DAC firmware works As you might expect, the software for the CLASSiC DAC is quite complicated and took some time to create. Quite a bit of effort also went into ensuring that it is (as nearly as possible) free of “bugs” (ie, mistakes in the code). The firmware consists of around 9,000 lines of C code which compile into a HEX file which nearly fills the 128KB of available flash storage. Partly this is due to the high level of speed optimisation required to get the code to run fast enough to play back 96kHz, 24 bit WAV files from the SD card despite the a fairly low-powered processor (40MHz, 16-bit). The software consists of a number of module which work together to provide the necessary functions. Direct Memory Access (DMA) and interrupt handler routines are used where possible to allow several different tasks to operate simultaneously for maximum efficiency. The major modules are: * Biphase.c/DCI.c: Biphase encoder and S/PDIF output streamer – this set of routines uses DMA and interrupts to read pulse code modulated (PCM) audio data from a set of memory buffers and convert it to a biphase-encoded S/PDIF stream on the fly. This stream is continuously output using the data converter interface (DCI) to feed the digital audio receiver. This unit operates while playing back WAV files and effectively forms the sound output driver for the microcontroller. It makes heavy use of pre-calculated tables to allow the biphase encoding to be fast enough that it only consumes a few percent of the available CPU time, even at the maximum supported sampling rate for the DCI (96kHz in this mode). This code was originally written for the Digital Audio Signal Generator project (March-May 2010) and subsequently improved for the Digital Audio Delay unit (December 2011). * Clock.c: routines to switch between PLL operation (39.6MHz) during normal operation and Fast RC Oscillator-based clock at 8MHz while in standby, to reduce power consumption (CPU goes into “sleep” mode in standby to reduce power consumption further). * Control_SPI.c/CS4398.c/CS8416.c/PLL1708.c: routines to send and receive SPI commands so that the micro can control IC1, IC3 and IC7. Mostly, these routines simply provide an API (application programming interface) to make setting the ICs into their various modes simpler to do. Accompanying header files give human-readable names to the various numeric SPI command tokens. * Infrared.c: interrupt-based routines to receive and decode remote control commands, including Philips RC5 and NEC type. When a valid command is decoded, it is placed in a global variable to be handled by the main loop when it is ready (ie, asynchronously). This file has been re-used for a number of SILICON CHIP projects. * main.c: initialisation code plus general operations such as input switching, scanning, LED updating and so on. Most of the main loop deals with handling remote control commands, updating LEDs and calling the routines to handle WAV file playback, one chunk at a time, as buffers require it. * playback.c: playback logic handling. This file contains high-level subroutines to figure out which file to play next when changing tracks or folders, keep track of the current playback mode, skip forwards or backwards within an audio file and so on. WAV decoding and audio playback takes place elsewhere. This file was originally written for the Digital Lighting Controller (October-December 2010) and then improved to add folder support for the LED Musicolour (October-November 2012). We have made further improvements to make it handle various situations better (eg, to skip over folders within the directory structure with only non-audio files in them). Much of the complexity involves “walking” the file system tree to handle nested folders, sorting directory entries to play files back in the correct order (which doesn't necessarily match the order in which they are physically stored on the SD card) and so on. This is made more difficult by the micro's limited amount of RAM (64KB) which must be shared with audio buffers. As such, when traversing the directory tree, generally playback is stopped and the audio buffers re-used to store and sort file names. They are then wiped before playback resumes. * sd.c/spi.c/ff.c/diskio.c/crc.c: SD and MMC card I/O routines. Due to the evolution of MMC, SD, SDHC, SDXC, etc card standards and the number of different manufacturers, interfacing with these devices is quite involved. These files deal with both the low level SPI interface and also decoding the various kinds of FAT file systems (FAT12, FAT16, FAT32) and also basic file handling such as keeping track of which clusters the file is located on as it is progressively read. This code was originally written largely by Mauro Grassi has been used in a number of our past projects – essentially, all of those involving SD cards. It has been gradually improved to give better performance and to handle certain quirky SD card controllers. We had to do quite a bit of work for this project, to get it to run fast enough to read a 24bit 96kHz stereo WAV file fast enough so that there is enough time left to decode it, re-encode it as S/PDIF and do other housekeeping tasks (eg, infrared remote control decoding) so that the audio doesn't skip or break up if you are pressing remote control buttons during playback at the maximum sampling rate. The data rate for such a file is 24 x 2 x 96k bits/s = 576KB/s. We had trouble using DMA for SD card I/O for two reasons. One, certain DMA hardware errors (some not documented) conspire to make doing this a fairly wasteful exercise. Two, the file I/O routines are designed for synchronous access – ie, you call a function to read a file and it returns when it has the data – and changing them to operate in an asynchronous manner would be a lot of work. Since virtually everything else is asynchronous (eg, audio playback), this turns out to not be too much of a problem. The main loop simply waits for data to come in while the other tasks operate simultaneously, based on DMA channels or interrupt handlers. We had to make some changes in order to minimise the amount of time the SPI controller spends idle while reading from the SD card – unfortunately, due to its design, there are always some delays between bytes or words read using the SPI peripheral. But having tweaked the SD card code, it is now more than fast enough for our purposes and the limitation becomes the rate at which the DCI peripheral can stream S/PDIF audio data to IC1. * Timer.c: being able to have delays with a consistent period and being able to measure time between events is not always easy, especially when the processor clock rate can change dynamically. This file contains routines to set up a timer running at a fixed ratio to the processor clock, then figure out the rate at which that timer increments relative to real time. Fixed-length delays and accurate timing are then possible. This is necessary for correct infrared command decoding and for properly setting up various chips, which may require reset delays, delays between commands, etc. However since the microcontroller's speed is based on its internal RC oscillator (whether operating with PLL or not) and not a more accurate crystal source, these timers are not without error. Thus, an external crystal-locked clock generator (IC7) is used to generate audio sampling rates for the most accurate reproduction. * WAV.c: routines to decode the header of a WAV file and determine the format and location of the data within. The main restrictions in this code are that the header must fit within a 512-byte block and it only plays back the first DATA chunk, which must be in stereo or mono PCM format, 16-bit or 24-bit, with a sampling rate between 32kHz and 192kHz. Main loop After initialising the various other ICs, the firmware enters the main loop, which runs endlessly. This loop performs a number of tasks including scanning the eight inputs for active signals, updating the input and sampling rate LEDs, changing the DAC's oversampling rate to match the sampling rate of the received audio, checking for remote control commands or a button press and checking to see if an SD card has been inserted or removed. IR command decoding happens asynchronously, as described earlier. While an SD card with WAV files is present, the main loop also calls the routine to read WAV data and feed it into the audio buffers. When there is no more WAV data to read, this routine automatically scans for the next WAV file (depending on the playback order selected) and then starts playing that back. While WAV playback is active, the interrupt-driven S/PDIF audio output routines are also frequently active, emptying those audio buffers at the sampling rate The file reader keeps the buffers as full as possible since if they empty, an audible glitch will result. Microchip's EEPROM emulation library is used to store settings in the chip's flash. We've allocated as much flash as possible for this purpose as it increases the lifespan (each block is erased less often). Given that a write is only necessary when a setting such as the volume or the current input is changed, this memory should last for many decades and realistically, for the life of the hardware. Bootloader The bootloader re-uses the same SD card code as described above but adds a few new files. These works as follows: * flash_operations.c: code to read and write flash memory. Also includes some routines to progressively erase and write words to flash, only erasing each flash block once, as the HEX files don't necessarily contain data in monotonically increasing order. * hexfile.c: code to read and parse a HEX file and either confirm that it is valid (so that we don't start writing to flash until we know the whole file is good), compare the contents of flash against what is in the HEX file or write the contents of the HEX file to flash. * main.c: main loop, error feedback (via LED flash codes) and code to start main program when the time comes. Note that the bootloader sits in the upper 8KB or so of flash and the reset vector always points to it. If the flash doesn't need to be updated, or after it has been updated, it resets the microcontroller state back to a valid power-up state and then jumps to the main program, located at the start of flash. The bootloader does not use any interrupts so that the main program's interrupt table can be updated without any trickery.