ADC In An FPGA, Part 2

Note: This article was first published on June 5th, 2011 in a long abandoned blog. I have republished it here, with minor edits, because it is still very relevant and useful.

This is part 2 of “ADC In An FPGA”. If you have not read part 1, here it is.

Part 1 covered an overview of the ADC design. Part 2 is going to cover a small part of it, but in more detail. Specifically, the math around the RC filter model and how to optimize it to almost nothing.

The RC filter model is basically a chunk of logic inside the FPGA that attempts to predict what the external RC filter is actually doing, without directly measuring it. The math behind this is conceptually easy, so here’s the formula.

New_Voltage = Current_Voltage + (RC_Input_Voltage – Current_Voltage) * (1-exp(-T/RC))

Where:

T = Clock period, in seconds
R = Resistor, in ohms
C = Capacitor, in Farads
Current_Voltage = The current output voltage of the RC filter
RC_Input_Voltage = The output of the FPGA, going into the RC filter
New_Voltage = The output voltage of the RC filter after T seconds
exp() = The exponential function.

At first glance, this looks like it would be hard to implement in an FPGA, but rest assured that it isn’t.

The first optimization that we’re going to do is change “voltage” to be a value from 0.0 to 1.0. Basically, a percentage of the power rail voltage. So if our I/O Voltage is 3.3v then a value of 1.0 would represent a voltage of 3.3, and a value of 0.5 would represent 1.65 volts. This is super easy to do, all you do is change the definitions of the variables in the above formula. The new formula is:

New_Value = Current_Value + (RC_Input_Value – Current_Value) * (1-exp(-T/RC))

Where all of the “values” have a range of 0.0 to 1.0. And since the FPGA can only output ‘0’ or ‘1’, the acceptable values for RC_Input_Value can only be 0.0 or 1.0 and nothing in between.

Of course in the FPGA we would not be doing the math for this as floating point (“real” numbers in VHDL). That would be silly. Instead we’ll use the standard fixed point notation where (using a 4-bit vector for example) “0000” would be a value of 0.0. “0111” would be, essentially 1.0. “1000” would be -1.0. And “0010” would be +0.5.

The next thing to optimize is the ” * (1-exp(-T/RC))” part. It is important to note that since T, R, and C do not change once the design is done that this expression simplifies down to multiplication by a constant. In an FPGA, multiplication by an arbitrary constant is easy but can take up a lot of logic. Either it will use up some integer multiplier inside the FPGA, or end up as some crazy adder tree. Sometimes that’s appropriate, but we can do better.

So we don’t want to multiply by an arbitrary constant. But if our constant had only a single bit set in it, then our multiplication turns into a simple bit-shift, and a bit-shift in an FPGA takes no logic at all! We do have control over what R and C are, and to a lesser extent T. If we choose our R and C carefully then we can find a constant with only 1 bit set– and our multiplication becomes dirt simple.

Finding the appropriate R and C is not simple. If you did it by hand it would take lots of tedious calculations. Fortunately, computers are good at that sort of thing. I wrote a program that, when given T, will calculate all R’s and C’s that have a nice constant value associated with them.

It works like this: Let’s assume that R and C are 3.16K and 200 pF, and our T is 10 nS. From that we can calculate that our constant is 0.015698261559. When we convert it to our fixed point representation, we get 0x0202668F, assuming 32 bit signed numbers. If we then chop off all but the first ‘1’ bit, we get 0x02000000. Now, 0x02000000 looks a lot different than 0x0202668F, but if you convert it back into normal floating point (0.015625000000) you’ll see that it is about 0.47% off from the original number. In the grand scheme of things 0.47% is not a lot considering the resistor has a tolerance of 1% and the cap is much worse.

My program does this for all combinations of R and C, and spits out the combinations that are within 1% of the ideal value. Click on adc_constants to get an Excel file with the most popular clock frequencies already calculated. Inside the file are multiple sheets, one for each clock frequency. Each sheet has the following columns:

R — The resistor value
C — The capacitor value
Const(ideal) — The ideal value for our (1-exp(-T/RC)) constant, without rounding and stuff.
Const(int) — The 32-bit integer representation of our constant, in hex, and after all the bit mangling.
Const(float) — The floating point version of Const(int), after bit mangling.
Const Err– The error between Const(float) and Const(ideal).
n_bits — The number of ‘1’ bits in Const(int)
rc — The RC time constant for R and C

The easiest way to use this file is to go to the sheet with your clock frequency, and pick out an RC combination that is close to what you want. I strongly suggest using the Excel sort data function to put the data into an order that makes life easier. By default, it is sorted by R and then C. But sorting by rc and then by Const Err could be useful too. Either way, pick out an RC and go to town!

I should also note that the table does not include resistors smaller than 3K ohms. This is intentional. If the resistor is any smaller then the load on the FPGA output pin is too high to allow the output to switch rail to rail. Rail to rail outputs is essential to keeping everyone sane.

Once the dust settles, our original formula has turned into a simple bit shift and addition. Ya can’t get much simpler than that!