One-chip sound player.

The purpose of this project was to create a sound player that can play high quality sound using nothing but a single chip (plus an SD card for data storage).

The chosen microcontroller was the PIC12F1840. It was chosen because of its fast clock rate (up to 32 MHz). Since microchip PICs have this atrocious property of executing one instruction every four clock cycles, this results in 8MIPS performance. The second reason for the choice was the hardware SPI module - bit-banging SPI is not hard, BUT doing it fast enough to sustain the sound playback would be - the hardware SPI module helps by allowing the SPI clock to be as high at 8MHz without using nearly as much CPU time as bit-banging would. The reset of the circuit design was easy: the PIC outputs a PWM waveform to drive the speaker, which is amplified by a MOSFET, PIC's SPI controller talks to the SD card, and another GPIO is used to provide power to te SD card allowing it to be powered off and thus letting the circuit sleep in very low power mode (nanowatts of power used). The pic is overclocked using the OSCTUNE register to 33MHz from the stock speed of 32MHz.

The SD card driver is my own work, and supports SD/MMC/SDHC/RS-MMC/HS-MMC/miniSD/microSD/transFlash and whatever others are in that family. Card is initialized at 375KHz (which is within spec), and then the bus is clocked at 8MHz for data reads/writes. The driver is peculiar for a few reasons, mainly because it does not try to be an abstraction between the SPI bus and the caller, thus the driver will init the card, and set it up to stream sectors to the caller. The caller then calls spiByte() directly to read sector data. Every 512 calls, a call to sdNextSector() is needed to notify the SD driver that we want the next sector & properly wait for it to start.

Since there is some time between the end of streaming of one sector and beginning of streaming the other, buffering is used for audio data. The buffer is a simple circular buffer that the audio thread reads from, and the SD thread writes to. When buffer is full, SD thread busy-waits for a slot to open to write data into. Since SD cards do not care how much time passes between clock ticks, this wait can be as long as needed.

The filesystem driver (uFAT) is also my own work, and is a tiny tiny FAT16 implementation. It supports only: short filenames, only FAT16, only read, only root directory. It can: enumerate files' names/sizes/flags and return the set of sector extents a file occupies. A sector extent is defined as a contiguous range of sectors (a "fragment" in FAT terms). The caller then reads those sectors by itself (allowing the caller to thus avoid the needless layer of abstraction and thus time waste).

Since the FAT filesystem can be arbitrarily fragmented, any file can be made up of more than one sector extent. We need to store them ahead of playing the file, since during playback we do not have the luxury of reading non-sound sectors without underflowing the playback buffer. Storing them in RAM is painful since we need a rather large buffer for them. EEPROM is a better place, BUT it is rather small. Program memory is thus used. A range of it is allocated as a "const static" array, and then the code uses its address and size to find where it is stored, and erases the area. Each 2 14-bit words are thus used to store a sector number or a number-of-sectors value (28-bit each) for a sector extent that makes up the chosen audio file. Since I do not forsee needing to support cards with over 2^24 sectors (>8GB) for this projecgt, the 28-bit size does not bother me. This area is then accessed using readSecList() and writeSecList() functions. In the latest version EEPROM is used afterall, for space saving.

Audio playback itself is relatively simple. PWM module is used to output the waveworm. Its frequency is 1MHz or so (adjustable) and the duty cycle is the needed audio amplitude. Currently 6 bits of data are used (bottom 2 are truncated). This produces pretty good quality sound with deep lows and nice crisp highs. Timer0 overflow interrupt is used to determine when to change the PWM duty cycle to the next audio sample's value. Duty cycles are kept at below 50% always, since anything above that causes distortion. Audio thread does NOT check for buffer underflow, and instead just keeps re-playing the buffer over and over. Needless to say we avoid this by filling the buffer in a timely fashion. The function audioOn() turns on the audio thread and audioOff() turns it off.

The random number geneartor used is a very simple 32-bit multiplication-addition-modulus type, where Xn = (Xn-1 * 0xDEECE66D + 0x0B) % 0x100000000. The seed is stored in the last 4 bytes of EEPROM, so that on each power-on the sequence of sonds is not repated, but continued from the last time. The top 16 bits of the modified seed are returned as the "random" number to the caller.

Given all of the above, the method of operation becomes simple:

  1. Power on SD card
  2. Init SD card
  3. List files in root directory
  4. Count number of non-hidden files with "WAV" extension
  5. Loop:
    1. Select a file randomly
    2. Use uFAT to find the sector extents file occupies (fragmentation is properly handled)
    3. Store those sectors in the sectorList storage
    4. Init audio playback
    5. Play data from sectors in each extent until there is no more
    6. turn audio off
    7. enter low power sleep for a determined time (30 sec currently, random length in the future)
    8. power card on
    9. [Re-] init card

Detailed PIC resource usage:

  • Clocks: 33MHz when playing, 500KHz when waking from sleep before deciding to sleep more
  • WDT: Used to wake from sleep
  • Reference Clock Module: Unused
  • Interrupts: Timer0 overflow interrupt used, others unused
  • Data EEPROM: 4 bytes at end used, others unused
  • Program Flash: Code uses 40%, sectorList uses 45%
  • GPIOs: 3 used for SPI, 1 used for output, 1 used for SD card power control. RA3 unused.
  • Fixed VoltageReference: Unused
  • Temperature indicator: Unused
  • ADC: Unused
  • DAC: Unused
  • SR Latch: Unused
  • Comparator: Unused
  • Timer0: Used for sound playback timing, overflows 22050 times a second, with proper reload value
  • Timer1: Unused
  • Timer2: Used for PWM module's timing
  • Data Signal Modulator: Unused
  • PWM: used to produce output sound
  • MSSP: used in SPI master mode to talk to the SD card
  • EUSART: Unused
  • Capacitative Sensing Module: Unused

As can be seen in the video here, it all works rather well. To address a few final questions: Audio files are uncompressed WAV files. The cde attempts to figure out the specific format and play nice with it, but 8 bit samples work best. SD card must be formatted FAT16 since uFAT does not support FAT12 or FAT32. This may (and likely will) change in the future. The maximum number of file fragments is set in the code, and is something like 128 currently, any more than that will not be played, or will cause data corruption, so defragment your card sometimes, if you plan to replace sound files often. Code cleanup is on the way, and the code will be released after cleanup is complete. For now, here's the HEX file for the PIC12F1840: [DOWNLOAD]. Source (current snapshot) can be downloaded [HERE]. Feedback is always welcome in [EMAIL] form.

© 2012-2024