PWM (Pulse Width Modulation) is a widely used technique for creating analog voltages using digital hardware. It is very simple, and for many uses sufficient solution. However, if the application at hand requires high precision and fast update rate, then it may turn out to be unusable. Especially if the clock frequency of the driving device (assuming FPGA) is limited or restricted. To provide a solution for such cases, the PWM engine has been folded to several subcycles in each cycle. This results in higher frequency for the noise, while maintaining the resolution. High frequency noise is easier to filter out using normal RC filters, and as such many applications can consider it to be insignificant.
Basic operation of a PWM is just to keep the output high for a given number of clock cycles, and then low for the rest of the cycle. Connecting this output signal to an RC filter provides an analog voltage that can be controlled by adjusting the time the output is high, compared to the full cycle. In this folded scheme the full cycle is split in sub cycles, each operating like a full cycle. This alone would lead to reduction in resolution, as only part of the control bits can be assigned to a shorter sequence. Here the sub cycles get their value from the top bits for the control word. To reach the full resolution, the contribution from the low bits is assigned to the subcycles individually. For example lets consider a 4 bit PWM with 2 bit folding. That would give us 4 folded subcycles, each with the top 2 bits as their initial value. Then the contribution from the two remaining low bits would be assigned to the four subcycles as +1, until the sum matches. Now the frequency of the output is 4 times higher, with the same resolution. Input word "0110" would produce a sequence "10", "10", "01", "01" for a total of 6 clocks spend in high output state. Exactly what the input word required. However, this clearly gives us a small lower frequency contribution, since the +1's are all packed in the front of the sequence. So, in order to disperse them bether dithering is used. The sequence is then "10", "01", "10", "01". Still the summ over the whole cycle remain correct, but now the lowest frequency componen has been doubled in frequency and halved in intensity. Both result in RC filter being more effective in removing the noise.
There are many ways for generating the dithering patterns, here one of the most elegant solutions was chosen. The idea is to use a permutaion of the bits in the high part of the main counter. The most efficient permutation is simply turning the bit positions around. In VHDL this means just simply renaming the existing wires. Thus, the solution causes no logic to be generated. Using the previous example, the counter would be 4 bits wide, with lowest 2 bits used inside each subcycle. The remaining 2 high bits would be permutated by reversing their bit positions, so after renaming their sequence would be "00", "10", "01", "11". Using this sequence to assign the input words low bit contributions gives a balanced spread across the full cycle. Note, that the spread is not fully optimal for all values, but it is a very good approximation, considering that no logic is needed.
All of the implementation variants provided will do the same algorithm. The resulting maximum clock speeds vary from 211MHz to 483MHz. Details can be found in Implementation_results.txt.
Also, a testbench with a few simulated analog components is provided. By changing the UUT, one can easily test the variants. Setting the bits and dithering genererics in the testbench top allows different combinations as needed. The RC parameters can be changed and input patterns generated. Feel free to add additional RC filter stages, if you wish.
Please notice, that simulation of analog RC filters using VHDL is questionable at best. RC filters should be simulated with continuous time and voltage, but digital simulations are intended to be used for simulation of discreet time and discreet voltage. To provide some accuracy, data type real is used for the voltages and automatic refresh timers are added in the analog models. Using short self refresh periods in the first analog model will also update all of the models after it. Feel free to play with the settings.
Monitoring modules provide digital read back, from the filtered analog value back to the digital domain. Additionally, during analyze period, a maximum and minimum of the filtered output are tracked, and peak to peak noise value is calculated. Comparing the merits of each solution should be easy.
For example the dithering pattern for 5 bit dithering would be:
00000000000000000000000000000000 = 0
10000000000000000000000000000000 = 1
10000000000000001000000000000000 = 2
10000000100000001000000000000000 = 3
10000000100000001000000010000000 = 4
10001000100000001000000010000000 = 5
10001000100000001000100010000000 = 6
10001000100010001000100010000000 = 7
10001000100010001000100010001000 = 8
10101000100010001000100010001000 = 9
10101000100010001010100010001000 = 10
10101000101010001010100010001000 = 11
10101000101010001010100010101000 = 12
10101010101010001010100010101000 = 13
10101010101010001010101010101000 = 14
10101010101010101010101010101000 = 15
10101010101010101010101010101010 = 16
11101010101010101010101010101010 = 17
11101010101010101110101010101010 = 18
11101010111010101110101010101010 = 19
11101010111010101110101011101010 = 20
11101110111010101110101011101010 = 21
11101110111010101110111011101010 = 22
11101110111011101110111011101010 = 23
11101110111011101110111011101110 = 24
11111110111011101110111011101110 = 25
11111110111011101111111011101110 = 26
11111110111111101111111011101110 = 27
11111110111111101111111011111110 = 28
11111111111111101111111011111110 = 29
11111111111111101111111111111110 = 30
11111111111111111111111111111110 = 31
Finally, a note on the coding style. In the code positional precedence has been used. If you prefer, you can easily transform the code to use fully written version. Example:
if rising_edge(clk) then
if cnt="0000" then o<='1'; end if;
if cnt="1010" then o<='0'; end if;
Produces the same logic as:
if rising_edge(clk) then
if cnt="1010" then o<='0';
elsif cnt="0000" then o<='1';
I just prefer the shorter version. All modern tools should be able to handle both styles.
The url of the svn repository is: https://opencores.org/websvn/listing/pwm_with_dithering/pwm_with_dithering