/* * H-PreAmp.c * * Created: 18/09/2016 * Author: Tomi Nihtila * * This is a test software for Addon H-PreAmp. It works and it is in use but * is not well tested or documented. * * Functionality is very simple - control PGA4311 volume based on potentiometer * position and input and DAC filter settings based on mechanical switch * positions. All functionality is interrupt-based. * */ #include #include #define F_CPU 16000000 #include // ADC circular buffer length. Average is calculated from these values, after // excluding the smallest and largest values. #define ADCVAL_LEN 5 // Hysteresis size. ADC range 1024 is divided into approx 200 steps, meaning // one step (0.5 dB) corresponds ~5 change in ADC value. Hysteresis should be // around this (or a bit smaller?). Comparison is made between average values // so small variations do not change the behavior. #define ADC_HYSTERESIS 3 // Ultimate maximum volume level, decimal value sent to the PGA4311. // 1 = -95.5 dB, 255 = 31.5 dB. 192 = 0 dB. #define MAX_VOL 192 // 204 +6 dB // Led definitions, partly related to one old design. LED_CNT controls how long // time a led is lit after changing volume. #define LED_MINUS 0 #define LED_PLUS 1 #define LED_CNT 4 static unsigned short prusCalcAvg(void); static void prvChangeSettings(void); static void prvChangeVolume(void); static void prvErrorHandler(void); static void prvLedOFF(void); static void prvLedON(signed char scSign); static void prvSendVolume(unsigned char ucVolume); // typedef struct TABLE_STRUCT { unsigned short usBuf[ADCVAL_LEN]; unsigned short usAvg; unsigned short usOldAvg; } xTablestruct; typedef struct VOLUME_STRUCT { unsigned char ucTrueValue; unsigned char ucICValue; } xVolumestruct; xTablestruct xADC; xVolumestruct xVolume; unsigned char ucLedCounter[2]; // AD-conversion complete -interrupt // // ADC-conversion is triggered by timer0 overflow and this ISR is triggered // when conversion is completed. It then processes the data. // ISR(ADC_vect) { static unsigned char ucCInd; unsigned short usAnVal; // Read conversion result usAnVal = ADC; // This happens in case of error or if pot is removed // (and resistor R70 pulls the input to Vcc) if(usAnVal > 1010) { prvErrorHandler(); } xADC.usBuf[ucCInd] = usAnVal; // Ensure buffer is filled once completely before using values if(xADC.usBuf[ADCVAL_LEN-1] != 0xFFFF) { // Otherwise update current average value and change volume if necessary xADC.usAvg = prusCalcAvg(); prvChangeVolume(); } // Index for circular buffer ucCInd++; if(ucCInd >= ADCVAL_LEN) { ucCInd = 0; } // Turn a led off if necessary prvLedOFF(); // Timer0 overflow triggers ADC, flag needs to be reseted manually TIFR0 |= 0x01; } // Pin change interrupts // // Triggers when input or filter switch is toggled. // Filter switch was toggled ISR(PCINT1_vect) { prvChangeSettings(); } // Input switch was toggled ISR(PCINT2_vect) { prvChangeSettings(); } // prvChangeSettings(void) // // Changes input and digital filter setting based on the mechanical switches. // static void prvChangeSettings(void) { // ucData is data prepared to be sent to the shift register via SPI-bus. unsigned char ucData, ucPort; // De-emphasis is always off but PDUR can be controlled with jumper link // on PC4. If it is open, PC4 is high - setting on H-DAC is high, meaning // high rate and 192 kHz sample rate may not work. // PDUR normal, de-emphasis off if(PINC & 0x10) { ucData = 0x1F; } else // PDUR high, de-emphasis off { ucData = 0x9F; } // After this ucPort is 1, 2, or 3 ucPort = (PIND & 0x18) >> 3; // coax if(ucPort == 1) { ucData &= ~0x08; } // USB else if(ucPort == 2) { ucData &= ~0x01; } // opt else if(ucPort == 3) { ucData &= ~0x02; } else { prvErrorHandler(); } // DAC filter switch if(PINC & 0x20) { ucData |= 0x20; } // disable interrupts for a while, just in case cli(); // Chip select low PORTC &= ~0x04; SPDR = ucData; // Wait for sending to complete while(!(SPSR & 0x80)); // Chip select high PORTC |= 0x04; sei(); } // oprvErrorHandler(void) // // ERROR: Mute output, light led and halt program // static void prvErrorHandler(void) { // Hardware mute PORTC &= ~0x01; // Light led PORTB &= ~0x04; while(1); } // prvChangeVolume(void) // // Check if volume knob has been rotated since the last check. // static void prvChangeVolume(void) { signed short ssTemp; unsigned char ucTemp; unsigned long ulCalc, ulCalc2; // This is for the first check after startup if(xADC.usOldAvg == 0xFFFF) { // hysteresis to cause volume update after startup = turns the device on xADC.usOldAvg = xADC.usAvg-ADC_HYSTERESIS; } // Difference between previous and current value ssTemp = (signed short)xADC.usAvg - (signed short)xADC.usOldAvg; if( ssTemp < 0 ) { ucTemp = -(unsigned char)ssTemp; } else if( ssTemp > 0 ) { ucTemp = (unsigned char)ssTemp; } // no change else { return; } // not large enough change if(ucTemp < ADC_HYSTERESIS) { return; } // ACTION: Volume needs to be changed else { // This calculates new volume value based on the ADC setting. ADC // returns value of approximately 0...1000 (it does not go to 1023 // due to series resistor) which is converted to 0...MAX_VOL. // This scales the value ulCalc2 = 10000/MAX_VOL; ulCalc = 10*xADC.usAvg/ulCalc2; // Send new volume, light a led prvSendVolume((unsigned char)ulCalc); prvLedON((signed char)ssTemp); // Update old average value, comparison is always made to the value // which led to the volume change before. xADC.usOldAvg = xADC.usAvg; } } // prvLedON(signed char scSign) // // Led is lit for a while after volume is changed. This was mostly for // indicating that volume value is not oscillating. // static void prvLedON(signed char scSign) { // There are two counters because I used to have separate leds for // volume up and down. Now it is the same one. if(scSign < 0) { // Led for lowering volume PORTB &= ~0x04; ucLedCounter[LED_MINUS] = LED_CNT; } else { // Led for raising volume PORTB &= ~0x04; ucLedCounter[LED_PLUS] = LED_CNT; } } // prvLedOFF(void) // // Led is used to denote volume change. This turns off the led after a // period of time. // static void prvLedOFF(void) { // Control LEDs // There are two counters because I used to have separate leds for // volume up and down. if(ucLedCounter[LED_MINUS] > 0) { ucLedCounter[LED_MINUS]--; } else if(ucLedCounter[LED_PLUS] > 0) { ucLedCounter[LED_PLUS]--; } else { // Turn the led off PORTB |= 0x04; } } // prvSendVolume(unsigned char ucVolume) // // Send new volume data to PGA4311 via SPI bus. // static void prvSendVolume(unsigned char ucVolume) { unsigned char ucInd; // If this happens, something went seriously wrong if( ucVolume > MAX_VOL ) { prvErrorHandler(); } xVolume.ucTrueValue = ucVolume; // I used to have some software limitations for volume here, those were // set with extra jumper links. xVolume.ucICValue = xVolume.ucTrueValue; // disable interrupts just in case cli(); // chip select low PORTC &= ~0x02; // send value 4 times (4 channels - no balance control is implemented) for(ucInd=0; ucInd<4; ucInd++) { // Set data and send SPDR = xVolume.ucICValue; // Wait for sending to complete while(!(SPSR & 0x80)); } // chip select high PORTC |= 0x02; // enable interrupts sei(); } // prusCAlcAvg(void) // // Calculate ADC-reading using values in the buffer. Reading is calculated by // average of the buffer values but excluding the smallest and the // largest values. This should give pretty good protection for sudden weird // volume behavior. // static unsigned short prusCalcAvg(void) { unsigned short usSum; unsigned char ucInd, ucMin, ucMax; ucMax = 0; ucMin = 0; // Determine the smallest value (index) for(ucInd=1; ucInd xADC.usBuf[ucMax] ) { ucMax = ucInd; } } // Add the values together except the largest and smallest usSum = 0; for(ucInd=0; ucInd