Class D AVR

Class D AVR

This is a project that I’ve been working on for some time now, a good part of my life, but more recently for the past month. I’m not going to go through everything as to how I arrived at this point, but I can assure you it was through a lot of trial and error. I now present to you, the class D avr amp.

This uses an ATtiny461, with a few discrete components as shown in the schematic below.

If I may be brief, I used 2-470 ohm resistors to combine a stereo signal, followed by a decoupling cap, using a pot to adjust the voltage bias before heading into the AtTiny. I have a .1uF cap to the input of the aref pin to help with noise reduction. I have 2 LEDs which proved to be very useful for debugging and troubleshooting as well. You’ll notice there is a filter on the avcc pin, and a pullup resistor on the reset, as well as an isp for programming.

I used a (shown left) cheapie eBay h-bridge motor driver to drive the speakers. This is based off of the L298 chip. In hindsight, this probably was not the greatest choice because I ultimately had to reduce the switching frequency of the drive signal to 32.5khz, anything higher and the L298 could not turn on/off fast enough, causing loss of sound quality and distortion. Conveniently, though, the L298 board shown above also had a 5vdc output, which is what I powered the AtTiny off of.

I’m not going to go into the specifics of how a class d is supposed to work. Frankly, I’m tired of reading “pulse width modulation is…” posts from other people. But the gist is that your audio signal is compared to a triangle (or sawtooth) wave, and the output (usually through a comparator) is one state if your signal is greater in amplitude, and in another state (high or low) if the signal is lower in amplitude. I won’t hotlink, so if you would kindly visit and see figure 2.

Moving forward, the below is the complete code, after which I will explain each part in sections.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/*
 * classd_word.cpp
 *  Author: George Gardner
 */


//Includes
#include <avr/io.h>
#include <avr/interrupt.h>
volatile bool centerFlag = 0;
volatile uint8_t clipFlag = 0;

int main(void)
{    
      //Initialize Timer
      TCCR1A = 0b01000010; //oc1a complimentary. pwma  enabled
      TCCR1B = 0b00000010; //pck = ck/2 = 32mhz, giving 32.25khz with 1024 steps
      PLLCSR = 0b00000100; //enable pll clock 64mhz
      TC1H = 0x03; //set top value of pwm to 1023
      OCR1C = 0xff;
     
      //Initialize ADC
      ADCSRA = 0b11101101; //adc enable, adc start conversion, auto trigger enable, adc interrupt enable, prescaler of 32 (bit 2:0 = 101) prescaler of 16 = 100
      ADMUX |= (1<<REFS1) | (1<<REFS0);
      ADCSRB |= (1<<REFS2);
     
      //Init pins
      DDRB |= 0b01000011;  //PB1 = OC1A, PB0 = /OC1A, PB6 = clipping led
      DDRA |= (1<<PA7);  //LED output if needed
           
      //enable global interrupts
      sei();
     
      while(1)
    {
            if(centerFlag){
                  PORTA |= (1<<7);
            } else {
                  PORTA &= ~(1<<7);
            }
            if(clipFlag > 0){
                  clipFlag--;
                  PORTB |= (1<<6);
            } else {
                  PORTB &= ~(1<<6);
            }
      }
}

ISR(ADC_vect){
      uint8_t lowbyte = ADCL;
      TC1H = ADCH;
      OCR1A = lowbyte;
     
      //the below not needed
      int16_t result = (ADC);
      if(result == 511){
            centerFlag = 1;
      } else {
            centerFlag = 0;
      }
      if((result > 1000) || (result < 22)){
            clipFlag = 254;
      }
}

First I’d like to explain my logic behind all this. I’ve experimented with many versions of code and types of output, and found that as the code got easier and smaller, the amp sounded better. The first version was a monster, and sounded like one too! The version in the code shown above worked the best for my setup. Again, keeping in mind that the switching frequency could not be too high due to the L298 board that I used.

With that being said, here is the general operation of the code/AtTiny: The ADC is setup to run in free running mode, continuously taking samples and firing an interrupt at the end of every conversion. The ADC clock is prescaled from the system clock of 16mhz (yes, I used PLL fuse to run at 16mhz) by a factor of 16, making the ADC clock 1mhz, which makes me question the precision since it should only run at 200mhz for 10-bit precision (this is something that I need to revisit). In any case, after a 10-bit ADC conversion takes place, an interrupt is called to set the OCR1A (a.k.a. the pwm duty cycle) to the value of ADC. Timer1 is setup in fast pwm mode with pins OCR1A and OCR1A to be complimentary (one is on while the other is off). 10-bit precision is achieved at a speed of 31.25khz by using PLL clock for timer1. Timer1 is setup to use PLL clock with a prescaler of 2, 64mhz / 2 = 32mhz! A count of the timer up to the top value of 1023, yields a pwm cycle speed of 32mhz / 1024 = 31.250khz! Note that this value could be doubled by setting pck=ck (i.e. no prescaler).

And that’s it. The code is super simple. All the code in the main loop is actually not needed, I just put it there to control the LEDs. The two variables at the top are not needed, and in essence, the only code you really need for the device to work is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*
 * classd_word.cpp
 *  Author: George Gardner
 */


//Includes
#include <avr/io.h>
#include <avr/interrupt.h>

int main(void)
{    
      //Initialize Timer
      TCCR1A = 0b01000010; //oc1a complimentary. pwma  enabled
      TCCR1B = 0b00000010; //pck = ck/2 = 32mhz, giving 32.25khz with 1024 steps
      PLLCSR = 0b00000100; //enable pll clock 64mhz
      TC1H = 0x03; //set top value of pwm to 1023
      OCR1C = 0xff;
     
      //Initialize ADC
      ADCSRA = 0b11101101; //adc enable, adc start conversion, auto trigger enable, adc interrupt enable, prescaler of 32 (bit 2:0 = 101) prescaler of 16 = 100
      ADMUX |= (1<<REFS1) | (1<<REFS0);
      ADCSRB |= (1<<REFS2);
     
      //Init pins
      DDRB |= 0b01000011;  //PB1 = OC1A, PB0 = /OC1A, PB6 = clipping led
      DDRA |= (1<<PA7);  //LED output if needed
           
      //enable global interrupts
      sei();
     
      while(1)
    {
           
      }
}

ISR(ADC_vect){
      uint8_t lowbyte = ADCL;
      TC1H = ADCH;
      OCR1A = lowbyte; 
}

Pretty simple, huh?

Now for the breakdown.

1
2
3
4
5
6
//Initialize Timer
TCCR1A = 0b01000010; //oc1a complimentary. pwma enabled
TCCR1B = 0b00000010; //pck = ck/2 = 32mhz, giving 31.25khz with 1024 steps
PLLCSR = 0b00000100; //enable pll clock 64mhz
TC1H = 0x03; //set top value of pwm to 1023
OCR1C = 0xff;

TCCR1A sets the OC1A pins to complimentary, while enabling PWM1A. WMG bits, elsewhere, are at zero so you know you’re in FASTPWM mode (check your data sheet!) TCCR1B just sets the prescaler to be a division of 2 from the clock that is given, which is defined to use the pll clock of 64mhz (fuse must be set to do this at time of programming as well) by means of the PLLCSR register. The OCR1C (the top value) is set by first setting the tc1h register, then the ocr1c. The combined value 0x03FF is equal to 1023.

It’s important to realize that TC1H is a temporary register that is shared amongst all OCRX registers. This is why you set TC1H first, because when you write 0CR1C, the AVR will check the value of TC1H and write these bits to the ninth and tenth bit of the OCR1C behind the scenes. The 9th and 10th bits cannot be accessed directly, but only through the TC1H register. This is also why when you read the bit, you must read OCR1C first. When you read OCR1C, at the same time the value is read, if there is a 9th and 10th bit present, the AVR will copy the value of that to the TC1H register. It took me a minute to wrap my head around that one.

1
2
3
4
//Initialize ADC
ADCSRA = 0b11101100; //adc enable, adc start conversion, auto trigger enable, adc interrupt enable, prescaler of 32 (bit 2:0 = 101) prescaler of 16 = 100
ADMUX |= (1&lt;&lt;REFS1) | (1&lt;&lt;REFS0);
ADCSRB |= (1&lt;&lt;REFS2);

Next we initialize the adc. ADCSRA high bits from left to right enable the adc, start the first conversion to get the ball rolling, enable the auto trigger mode (free running is active by defualt – see datasheet for adcsrb), enable the interrupt for ADC, and finally set the prescaler to 16. The ADMUX and ADCSRB in combination set the voltage reference to 2.56v internal reference with an external bypass capacitor on the aref pin, which we have.

The ADC takes 13 ADC clock cycles to make a conversion via successive approximation. With our clock running at 1mhz, this will yeild us a result (1mhz / 13) at a rate of 76khz. I need to try the adc with a prescaler of 32. This should increase our result accuracy, and would be better matched to the pwm frequency of 31.25khz.

1
2
3
4
5
6
      //Init pins
      DDRB |= 0b01000011;  //PB1 = OC1A, PB0 = /OC1A, PB6 = clipping led
      DDRA |= (1<<PA7);  //LED output if needed
           
      //enable global interrupts
      sei();

Above you have your simple direction register declarations, in addition to enabling global interrups required for ADC interrupt.

The heart of the code is shown below, in all its simplicity.

1
2
3
4
5
ISR(ADC_vect){
      uint8_t lowbyte = ADCL;
      TC1H = ADCH;
      OCR1A = lowbyte;
}

The above code gets executed approximately 76,000 times per second, setting the OCR1A, and therefore the pulse width, equal to the ADC input. I can do this because of the bias pot on the input (which needs to be 1/2 of the 2.56v reference). One LED helps me by turning on when the ADC value is 511 (one half), so I can set the center point, giving a 50% pwm by tweaking the pot until the LED appears to be fully on. The other LED is used to light up when the signal approaches the very top or bottom of the input, kind of like a clipping light so you know if your signal is too hot).

It’s important to see in the code above, that ADCL is read before ACDH, and that TC1H is written before OCR1A is written, as explained earlier.

I’ve experimented with a version that outputs nothing when the voltage level is half of the reference (no audio signal) and increases from 0 to 100% on one channel on the upper portion of the wave, and 0 to 100% on the other channel on the lower portion of the wave, but the result was too distorted at low volumes. The current version, as stated, outputs 50% duty cycle with no signal.

I found that a filter was not needed, but ended up using one due to a better sound quality.

I would really appreciate if anyone could suggest a better method or improved schematic/code. I would love to play with the project more in the future. Perhaps we can find a way to give it feedback to make it a proper amp?

Share your thoughts!