Andy Peters said:
Advanced homework assignment: do the whole thing in a small FPGA (like Lattice XO2). Five bucks, and you get a lot of pins to drive your metering LEDs without the external shift registers and without Charlieplexing or other muxing technique.
OK, in the Money-Where-Your-Mouth-Is Department ...
Like pretty much everyone else here, I think I can design an ADC and DAC that's as good as anything we can buy. So I designed one. Two of the parts on the board require some intelligence for configuration, plus it has a simple user interface (buttons for operation control, LEDs for status), so the first thought was to use an SiLabs 8051. That's the simple solution. The 8051 has an on-board ADC, perfect for the input meter function (for the audio ADC side), and it can scan buttons and with a muxing scheme drive LEDs or maybe use SPI LED drivers.
But then I started to get into the weeds. What is the audio ADC's reference, so one can do a proper measure of input level as seen by that ADC? Do I need a reference for the micro's ADC that is "better" than what is built in? What's the gain structure of the whole thing anyhow?
That's when I remembered that the SiLab's ARM devices (EFM32 and Si3M, which are all pretty cheap) have USARTs which can be configured to work as an I2S slave. Just route the ADC BCLK, LRCLK and data lines to the micro and there it is. Except when configured as an I2S slave, the maximum sample rate supported (actually the limit is in the BCLK) by these micros is 48 kHz. Grrr!
What about using programmable logic for the I2S part? Easy. But then why glue that part to a micro when the whole thing can be rolled up into a small FPGA? Oh, wow, Xilinx Spartan3AN (XC3S50AN) is too expensive at $15. Lattice has the MachXO2 devices, and the moderate size part (LCMXO2-1200) in a 100-pin TQFP is $7. And unlike the Xilinx, it doesn't need a separate 1.2 V core supply, as it has an onboard regulator. Bonus! MachXO2 has an onboard "Embedded Function Block" with two I2C ports, an SPI port, a timer and a good amount of user flash memory. Bonus 2! Lattice has a small microcontroller core called Mico8 you can put into an FPGA!
After spending too much time trying to work out the Mico8 toolchain, I gave up, realizing that what I was doing really didn't need a micro. Squirting configuration data into a peripheral chip over I2C or SPI can be done with a hardware state machine. Plus why make the design bigger, fitting in the micro?
Next up: EFB. Cool, it has a timer, which maybe I can use to detect the frequency of incoming word clock. Except, no, it doesn't have that kind of capture feature. So it's easy enough to write that code in VHDL. A counter runs on the system clock. Word clock input is synchronized to that clock, and on every rising edge of word clock you capture the counter value and reset it. The counter value tells you your word clock frequency. If the counter rolls over, you flag that as "no word clock present."
Cool, EFB has I2C and SPI ports which I can use to configure those peripherals. Except: the access to those ports is through a WISHBONE (open-source) bus interface, which just adds a level of crap to the whole thing. And basically the idea of a "general purpose SPI core" is dumb, as it's always easier to just embed the shift registers into the larger control logic instead of trying to go through some standard bus interface. Once you realize that there's no such thing as an "SPI standard," and that some slaves have really idiotic requirements, and that the number of bits you need to shift is design dependent, you just end up forgetting about a core and just writing an interface to the part you're controlling. So, EFB out, and two interfaces to the two peripherals that need configuration.
User interface operation is straightforward. Buttons are debounced. There is a button that selects the ADC sample rate, another button that enables external word clock sync (only if word clock is present) and a third button chooses which digital input is used by the DAC section. Status LEDs show the current operating state.
Back to the metering.
An I2S slave receiver deserializes the data from the ADC into the left and right channel data, which are then synchronized to the global clock. Data are rectified and I throw away the eight LSbs, because 16 bits is more than enough for a meter.
I wrote a low-pass filter (averager) and a peak detector (with peak hold) in VHDL. The output of each hit a bank of comparators, one per meter segment. (I chose 32 segments per meter.) The peak meter and the average meter were ORed to get the final meter drive.
At first I instantiated two meter circuits (averager, peak detect, comparator bank) but that was wasteful, since the I2S data gives me one channel at a time. So I removed one of the meter circuits, fed the deserialized/rectified data into the remaining one, and LRCLK tells me which channel sample is in the meter at a time. (Remember the meter calculations take much less than a sample time.) Then to save some FPGA pins, the metering is multiplexed into four banks of 16 segments.
The FPGA design uses 62 of the 80 I/O pins available, about half of the registers, 84% of the slices, and three of the seven block RAMs present in the LCMXO2-1200 device.
I'm laying out the PCB now and I hope to have that done by the end of the week. So maybe by the first of the year I'll have it all built up and I'll post details and a video of the meter as well as the VHDL source.