The Freescale SGTL5000 codec is the first chip I've put de novo into a circuit design, skipping breadboarding and relying completely on reading the datasheet and using the example schematics. I will have to master two new communication protocols/wiring/programming - I2C (for controlling the chip) and I2S (for sound) as well as analog connections for headphones, audio line-in and line-out. I'm nervous because it's a lot to get right on the first try, but excited because it is faster and less expensive to go direct from schematic to design without an intermediate breadboarding/evaluation board step.
It took me about a week to get the chip working, in three major steps:
- Writing the I2C software driver
- Setting the internal PLL and master clock
- Configuring the digital mixer
1. I2C software driver
The I2C driver is the interface between my application code and the chip. My application juggles sound, display and memory processes, so it important not to block the application when sending an I2C message. It is also desirable to abstract the interface with the chip, so that changing the hardware does not require a rewrite of the entire application.
To solve these problems, I wrote a device driver. The driver is a set of C function calls, which define an interface to a continuously running daemon that talks with the I2C peripheral on the MCU. As an example, to read the chip ID of the SGTL5000:
To solve these problems, I wrote a device driver. The driver is a set of C function calls, which define an interface to a continuously running daemon that talks with the I2C peripheral on the MCU. As an example, to read the chip ID of the SGTL5000:
- The application calls the driver function GetChipID(). These functions are written at a high level that will not change even if the underlying chip and driver change. The application also supplies a semaphore to signal when the transfer is complete.
- The function fills in a structure with the target register (in this case, register 0x0000) and value (0x0000) and a handle to the transfer complete semaphore, and calls the I2CDriver_AddTransferRequest function to add the structure to the message queue of the daemon. The execution returns to the application thread, which then goes to sleep, allowing other application threads to execute.
- The daemon wakes up, reads the message in the queue and calls the lower level function CI2C2_MasterSendBlock provided by the manufacturer to send a command via the on-chip I2C peripheral.
- The lower level function sets the registers on the I2C peripheral and starts the data transfer. The execution thread then returns to the daemon, which goes to sleep, waiting for the command to complete.
- During the transfer, the I2C peripheral generates interrupts, calling the low level interrupt handler (CI2C2_Interrupt) The interrupt handler handles details like advancing the byte count, setting read and write bits, checking for acknowledgement bits, and calling hook functions on completion of the transfer, or on errors.
- When the transfer is done, the low level interrupt handler calls the callback function CI2C2_OnMasterBlockSent.
- The callback function signals to the driver (using the driver's semaphore) that the transfer is done.
- The driver wakes up, prepares for the next transfer in the queue, and signals to the application that the transfer is done.
- The application thread wakes up and continues.
In this scheme the RTOS manages the message queues, semaphores and the execution of the application and daemon threads. Using semaphores and a message queue ensures that commands are executed in sequence and do not run over one another, eliminating many of the more common race conditions and other nasty bugs. However, this coordination comes at the cost of increased memory and CPU overhead to manage the separate threads.
Because I've used a similar structure for a SPI driver that I previously written, it was relatively quick to get a basic I2C driver running. The next step was to debug the I2C calls.
The I2C protocol differs from the SPI protocol in a few ways. It uses a 7 bit address (instead of a chip select), has a read and write mode (instead of simultaneous read/write), uses an acknowledgement bit (vs no acknowledgement) and variable timing (e.g. the slave can pull lines low and pause the entire process). This additional complexity creates several pitfalls that I promptly discovered:
Because I've used a similar structure for a SPI driver that I previously written, it was relatively quick to get a basic I2C driver running. The next step was to debug the I2C calls.
The I2C protocol differs from the SPI protocol in a few ways. It uses a 7 bit address (instead of a chip select), has a read and write mode (instead of simultaneous read/write), uses an acknowledgement bit (vs no acknowledgement) and variable timing (e.g. the slave can pull lines low and pause the entire process). This additional complexity creates several pitfalls that I promptly discovered:
- The I2C address is seven bits, not eight - it must be must be properly bit-shifted. Still not exactly sure when it should be shifted one to the left, but using a logic analyzer like the Saleae Logic will dispel all doubt.
- Check the byte ordering when writing to chip registers. My compiler generates little-endian words, which must be byte-reversed if I want to send the big-endian word commands the chip uses.
- The read/write bit must be set (in SPI there is no read-write). It took me a little while to figure out that to read a register, I had to first write the register address, then read the data - two separate transactions, each preceded by the I2C address.
- I learned to pay close attention to the acknowledgement (ACK) bit - it has several uses. First, the slave will ACK (i.e. pull the SDA line low) if it has received its I2C address - and so you can tell if your chip is working. Then, after every byte of a block (either read or write) it will also ACK - except after the last byte of a read. And as I would also discover, if I write an invalid value, it also doesn't ACK - in which case the driver would hang, waiting for the ACK.
- Unlike in SPI, the has control over the bus timing (e.g. can 'stretch' timing if it is not ready to respond), which can also cause confusion and problems. The slave can pull both data and clock lines low, stalling the bus and hanging the I2C driver. Both lines low is a telltale sign that the slave has stopped working.
- Pull-up resistors need to be used on both lines of the I2C bus. The internal pull-ups on the chip are sufficient.
2. Setting the PLL and master clock
The SGTL5000, like many audio codecs, has an optional mode in which an internal PLL generates the bitclocks needed for the I2S protocol (e.g. for a 44.1kHz sample rate, the bitclocks run at 44.1kHz*4096 = 180.6336 mHz) as well as the master clock for the chip from an arbitrary (between 2mHz and 27mHz) external master clock signal. This saves the designer from having to generate the bitclocks externally. In this mode the codec chip acts as the I2S master.
However, shutting down the PLL also shuts down the master clock and the I2C circuit, cutting off communication with the chip. Also, the PLL must be programmed (e.g. the PLL's clock multiplier needs to be set based upon the external clock rate and the desired master clock frequency). But, the PLL can only be programmed when it is shut down. This has two consequences for the driver:
However, shutting down the PLL also shuts down the master clock and the I2C circuit, cutting off communication with the chip. Also, the PLL must be programmed (e.g. the PLL's clock multiplier needs to be set based upon the external clock rate and the desired master clock frequency). But, the PLL can only be programmed when it is shut down. This has two consequences for the driver:
- If you want to try to reprogram the PLL frequency (e.g. switch sampling rates) without shutting down the PLL, it won't work. You have to first configure the chip to -not- use the PLL as the master clock, turn off the PLL, reprogram the PLL frequency, turn on the PLL, then re-configure the chip to use the PLL as master clock.
- In the startup code, you cannot simply set the PLL power bit to zero (as in the example code). Although this may work if the chip is being powered up the first time (i.e. it is not using the PLL as a master clock), it causes the I2C circuit to shut down, and the system to hang, if the chip is already using the PLL as the master clock. To avoid this, initialization code must first configure the chip to -not- use the PLL as the master clock, then shut down the PLL.
3. Digital mixer
I began with the simplest function: turn on the headphone amplifier and connect the analog line-in to it. I was rewarded with clear music from my MP3 player playing through my headphones.
It took longer to get the digital mixer working. It depends on having the ADC, DAC, DAP (digital audio processor/mixer) working.
It took longer to get the digital mixer working. It depends on having the ADC, DAC, DAP (digital audio processor/mixer) working.
- It took me a while to get the PLL up and running and the chip configured to use it as a master clock. I had done the frequency programming calculation correctly but it took some time to sort out the procedure (above) for setting the PLL frequency.
- Even though I was not using the I2S interface, I had made a bit-translation mistake and misconfigured the interface. This caused the interface to run in slave mode, where it uses an external clock signal for timing. My hypothesis is that the DAC/ADC block also runs off of the I2S clock - and since I was not supplying a clock signal, the DAC/ADC did not run. It took me a while to find this error because I was not using the I2S interface yet, and did not check the I2S settings carefully.
Next steps
Although the chip is now up and running, there are several remaining steps:
- Set up I2S communication and play sounds through the I2S bus.
- Experiment with a slower sample frequency (e.g. 20kHz) as this will reduce the amount of memory my samples need.
- Track down source of analog/digital crosstalk in the system, likely from coupling of the analog and digital power supplies.
- Add volume control, power up and power down commands to the driver and integrate into the main application
- Understand how the 'short circuit' detection works - for capless headphone drivers, this prevents a short to ground from burning out the chip.
- Implement a headphone detection circuit - I may have to use a 4 pin headphone jack, rather than the three pin jack I currently have.
- With a 500mAH/3.7V battery, an analog bypass (about 2mA) should run about 250 hours on a single charge. Running the PLL continuously drops the lifetime to about 85 hours. Definitely the digital mixer should only be enabled when it is being used.
- I will consider using a 1.8V supply - the power consumption goes down by about 50%.
- Look at using the line-in as battery voltage reader as in the Maxim 98XX series, as well as headphone detection.
- Design the power management scheme - whether the user will have an 'on-off' switch, how to let the user know that power is being drained through the bypass, etc.
Overall I am impressed by the SGTL5000. It packs many features into a simple, low power package, and the audio quality is very good at first impression. I also now have a working I2C driver and confidence in my ability to incorporate quickly these components into my designs.