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).
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.
Read more: One-chip sound player.