7.1 channel Hi-Fi microprocessor/PC controlled Preamp

Note: This project has been rebuilt and full information is available here.
I suggest reading about the rebuild first. Information below is for reference purposes.

Volume

This page includes the description and code required to manage 4 PGA2310 volume controls, and a total of 8 channels!

First lets look at the hardware...

PGA2310
From Datasheet

/MUTE always remains high in my case, since to mute these chips you can also send the data value of 0x00h - minimum volume level. ZCEN also remains high since this keeps the volume control from making any pops on volume changes.

SDI, SCLK and /CS are connected to the microprocessor directly, and I label them as DATA, CLOCK and LATCH respectively from here onwards.

SDO goes to additional PGA2310's. Each addition one still needs a CLOCK and DATA signal, so these wires are paralleled with the ones connecting to the PGA2310 before.

If you want more information on how to connect additional PGA2310's, the datasheet explains this clearly. Don't forget the pull-up resistor!

C2C Code for the volume

I will dive straight into showing you the routine that outputs code to all four of my PGA2310 chips...

void pga2310() {
    char n; // Loop counter
    
    // Set latch to low
    clear_bit(portc, 2);
    
    short bitSelect = 10000000b; // byte used to select a bit in voll/vol2 to send - MSB first
    
    for (n=0; n<64; n++) { // Check bit
        // Clear clock for next bit
        clear_bit(portc, 1);
        
        char sn;
        sn = n/8;
        // if n = 4, sn = 0
        // if n = 8, sn = 1
        // if n = 16, sn = 2
        // if n = 24, sn = 3
        // if n = 32, sn = 4
        // if n = 40, sn = 5
        // if n = 48, sn = 6
        // if n = 56, sn = 7
        // if n = 64, sn = 8
        
        char vol = 0;
        if (mute == 0)
            vol = volumes[sn];
        
        if ((vol & bitSelect) != 0) // the set bit position in bitSelect in vol is set, output high
            set_bit(portc, 0);
        else
            clear_bit(portc, 0);
        // Raise clock so serial bit output is sent
        set_bit(portc, 1);
        // Shift set bit in bitSelect one position to the left
        bitSelect = bitSelect >> 1;
        if (bitSelect == 0)
            bitSelect = 10000000b;
    }
    // Set latch to high
    set_bit(portc, 2);
    
}

8 bytes in total needs to be sent. One for each channel, representing a volume level from 0-255 (or -96dB to +31.5dB).

Each volume level is stored in an array, with position 0 being the subwoofer channel and 7 being front left (it works in reverse order from what you would suspect). Essentially, one 64-bit word is sent out, meaning this whole process can last a few hundred micro seconds (us) - but it is fast enough as in pratice I notice no delay from turning the encoder to having a change in volume.

Connections in the above routine are all on port c. Pin 0 is serial Data (SDI), pin 1 is Clock (SCLK) and pin 2 is Latch (/CS). They can be changed as you see fit.

Calculating the volume in dB for the LCD

This is quite a complex process, as this C compiler, and PIC's in general do not support floating point numbers natively - so having a decimal point becomes slightly difficult!

However, I worked a way around it, and the code is shown below for your convenience.

void showVolume(char vol) {
    lcdLine(3);
    lcdText('V');
    lcdText('o');
    lcdText('l');
    lcdText(' ');
    int modulus = 0;
    
    // Gain is 0dB
    if (vol == 192) {
        gain = 0;
        gainDec = 0;
        lcdText(' ');
    }
    // Gain is postive
    else if (vol > 192) {
        gain = 31 - ((255-vol) / 2);
        modulus = (254-vol) % 2; // Work out modulus - the remainder of the division

        lcdText('+');
    }
    // Gain is negative
    else if (vol < 192) {
        gain = ((254-vol) / 2) - 31;
        modulus = (254-vol) % 2; // Work out modulus - the remainder of the division
        lcdText('-');
    }
    // Work out the gain decimal
    if (modulus)
        gainDec = 5;
    else

        gainDec = 0;
    lcdText('0' + (gain / 10) % 10 );
    lcdText('0' + gain % 10 );
    lcdText('.');
    lcdText('0' + gainDec % 10);
    lcdText('d');
    lcdText('B');
    
}

Its not tooo complex once you've scratched your head and studied it a bit, but in case you don't care, there are a few things to point out if you want to use this code in your project. lcdText(' ') is an important routine - this just takes a simple character and writes it to an LCD - and it will of course vary depending on the LCD you have. To make sure the text is wrote to the last line of my display, I call a routine called lcdLine(3). This again will vary according to your LCD.

Note this routine only takes one volume character - a sort of universal volume level unaffected by rear/sub/center volume cut or boost, or balance if you add that too. Since I have no balance, the character sent to this routine is one of the front levels (i.e. left).

Levels - for cut/boost

Another array of 8 chars is used to represent cut/boost to certain speakers, such as rear, center or subwoofer.

void setLevels(char vol) {
    int y;
    for (y=0; y<8; y++) {
        volumes[y] = vol + levels[y];
        // Prevent overflow

        if (levels[y] & 0x80) { // Negative, new level should always be less than overall
            if (volumes[y] > vol)
                volumes[y] = 0;
        }
        else { // Positive, new level should be greater than overall

            if (volumes[y] < vol)
                volumes[y] = 255;
        }
    }
}

This routine takes a universal volume level, and automatically applies the levels from the levels array. It deals with overflow, preventing any sudden change from min to max and vice versa.

The levels array is set from byte codes sent from PIC#1 using:

            case 'L': // Rear, Cen, Sub level modifiers
                // All rears go to first byte
                int m;
                for (m=2; m<6; m++)
                    levels[m] = serialRxBuffer[1];
                levels[0] = serialRxBuffer[2]; // Cen

                levels[1] = serialRxBuffer[3]; // Sub
                setLevels(volumes[7]);
                pga2310();
                break;

Balance is ignored, hence the loop counter starts at 2, and applies the first byte to every rear channel. More details on how each level is set on PIC#1 is described later in the PIC control page.

Onto page 4 - LCD