/*************************************************************************************************** * HIFIDUINO v. B03 * * November 02, 2010 * Arduino code for Buffalo II DAC and other DACs based on the Sabre32 DAC chip. This code is adapted * from an earlier version developed for the Wolfson 8741 DAC chip. For more information visit * www.hifiduino.wordpress.com or www.hifiduino.blogspot.com * * Although this code is based on a confidential data sheet for which I've signed an NDA, I've also * requested and have received permission to publish this code. * * Change log: * v. B03 11/02/10: Supports s/w debounce, added setting of FIR filter, DPLL bandwidth, jitter reduction and adjustment of LCD brightness. * v. B021 10/18/10: No new functionality, but cleaned up the code and comments. * v. B02 10/15/10: Added reading of sample rate. * v. B01 10/11/10: Volume control, LCD, Rotary Encoder. ***************************************************************************************************/
// LIBRARIES #include <LiquidCrystal.h> // For LCD // Initialize the library with the numbers of the interface pins LiquidCrystal lcd(12, 11, 10, 9, 8, 7); #include <Wire.h> // For I2C // CONSTANT DEFINITION // (The digital volume for Sabre32 is 0 to -127 db in .5 db steps) #define DEFAULTATTNU 0x64 //-50 dB this is 50x2=100 or 0x64 #define MAXATTNU 0xC6 //-99dB this is 99X2=198 or 0xC6 #define MINATTNU 0x00 //-0 dB // #define DIMVOL 0x3C //-60dB The volume level when dimming the volume #define VOLUPPIN 4 // Button to increase volume or RotEnc A terminal #define VOLDOWNPIN 2 // Button to decrease volume or RotEnc B terminal #define SELECTPIN 5 // Switch to select function #define BRIPIN 6 // Pin to control the LCD brightness with analogWrite #define INTERVAL_SAMPLE 2 // Time interval in SECONDS for refreshing the sample rate #define INTERVAL_BOUNCE 2 // Time in milliseconds to debounce the rotary encoder #define INTERVAL_SWITCHBOUNCE 200 // Time in milliseconds to debounce switch #define INTERVAL_SELECT 5 // Time to exit select mode when no activity #define VOL 0 // The order of selection when clicking the select switch #define FIL 1 #define JIT 2 #define BRI 4 #define DPL 3 // VARIABLE DECLARATION byte regVal; // Variable to pass register value byte regAddr; // Variable to pass register value byte currAttnu=DEFAULTATTNU; // Variable to hold the current attenuation value unsigned long displayMillis = 0; // Stores last recorded time for display interval unsigned long debounceMillis = 0; // Stores last recorded time for switch debounce interval unsigned long selectMillis = 0; // Stores last recorded time for being in select mode byte dpllBW; // To record DPLL bandwidth setting byte brightness=0; byte select; // To record current select option boolean sharp; // FIR filter mode boolean jitter; // Jitter reduction boolean selectMode; /* LCD For the LCD, I am using a "standard" HD44780 20x4 display, and I am using the official Arduino LiquidCrystal library that comes with the standard installation. This is as standard as it can be and this type of LCD is available everywhere The pin assignment is different from the example code and it is as follows: * LCD RS pin to digital pin 12 * LCD Enable pin to digital pin 11 * LCD D4 pin to digital pin 10 * LCD D5 pin to digital pin 9 * LCD D6 pin to digital pin 8 * LCD D7 pin to digital pin 7 */ /* ROTARY ENCODER The rotary encoder is connected to digital pin 2 (A) and digital pin 4 (B). pin 2 (and pin 3) is an interrupt line in Arduino. It does not matter which terminal is connected to which pin. The third terminal is connected to GND. At each cycle, the rotary encoder will pull the pins LOW or HIGH and the transition is detected by the interrupt line. The interrupt service routine is specified below. Debounce In this version, the code implements debouncing by adding a few millisecond delay that pauses the code from evaluating the state of the pins. Typically, all the switching noise –the “bouncing” is generated in the first few milliseconds after the switch event. The code is optimized by calling the delay only if there is any activity in the rotary encoder. Activity in the rotary encoder is detected with an interrupt. The interrupt service routine is minimalist in design and determines if there is any motion in the rotary encoder. In addition, in order to minimize spurious interrupts –those generated by the switching noise, hardware debounce can be optionally implemented in the rotary encoder. Although the code will ignore all the interrupts generated during the debounce delay time, interrupts are still generated and it is still a good idea to minimize those spurious interrupts. There is just a slight change in the software if H/W debouncing is implemented, namely the pull up resistors in the pins are to be disabled. Further, the rotary encoder has an on-off switch, and the debouncing of the switch is also done in s/w (again the same h/w debouncing can be implemented, but it is optional). It is not based on interrupts and because pushing the switch manually will generate not only noise during the switching, but the switch can remain pressed for 100 milliseconds or more because one cannot lift the finger that fast. In this implementation and with my way of pusshing down the switch, 200 msec is an appropriate value. INTERRUPT SERVICE ROUTINE FOR ROTARY ENCODER The interrupt service routine has been designed to be minimalist in nature. The code just sets a flag indicating that some activity has been detected in the rotary encoder. */ // Rotary encoder interrupt service routine static boolean rotating=false; void rotEncoder() { rotating=true; // If motion is detected in the rotary encoder, set the flag to true } /* READING THE SAMPLE RATE The sample rate can be calculated by reading the DPLL 32-bit register. For SPDIF DPLL value is divided by (2^32/Crystal-Frequency). In Buffalo II, the Crystal frequency is 80,000,000 Hz. In Arduino (and other small microprocessors) it is NOT advisable to do floating point math because "it is very slow"; therefore integer math will be used to calculate the sample rate. The value of 2^32/80,000,000 is 53.687091 (which is a floating point number). If we use the integer part (53 or 54) we get the following results for a 44.1K sample rate signal: divided by 53 the result is 44.677K; divided by 54, the result is 43.849K. Clearly there are large errors from being confined to integer math. The actual result, if we use floating point math and use all the significant digits is 44,105 Hz (the 5 Hz deviation from ideal 44100 Hz is within the specification of SPDIF and the tolerances of the crystals involved) In order to increase the accuracy of the integer calculation, we can use more of the significant digits of the divisor. I did some evaluation of the DPLL register values for sample rates ranging from 44.1K to 192K and noticed that I could multiply the value of the DPLL number by up to 400 without overflowing the 32-bits. Therefore, since we have 32 bit number to work with, we can multiply the DPLL number by 400 and then divide by 400X53.687091=21475. If we do this, we obtain 44.105K which is the same as the exact value. */ // Sample rate reading routines volatile unsigned long DPLLNum; // Variable to hold DPLL value byte readDPLL(byte regAddr) { Wire.beginTransmission(0x48); // Hard coded the Sabre/Buffalo device address Wire.send(regAddr); // Specify the DPLL register from which to read value Wire.endTransmission(); Wire.requestFrom(0x48,1); // Hard coded to Buffalo, request one byte from address // specified with Wire.send() //while(!Wire.available()) { // Wait for byte to be available on the bus if(Wire.available()) { } return Wire.receive(); // Return the value returned by specified register } unsigned long sampleRate() { DPLLNum=0; // Reading the 4 registers of DPLL one byte at a time and stuffing into a single 32-bit number DPLLNum|=readDPLL(31); DPLLNum<<=8; DPLLNum|=readDPLL(30); DPLLNum<<=8; DPLLNum|=readDPLL(29); DPLLNum<<=8; DPLLNum|=readDPLL(28); // Calculating the sample rate for SPDIF DPLLNum*=400; DPLLNum/=21475; return DPLLNum; } /* CONTROLLING THE DIGITAL ATTENUATION (VOLUME) IN THE DAC The device address of Sabre DAC Datasheet specifies the address as 0x90 which is an 8-bit value. The wire library in Arduino uses 7-bit device addresses and the 8th R/W bit is added automatically depending on whether you use the write call [beginTransmission()] or the read call [requestFrom()]. Therefore, you will use the 7 most significant bits of the 8-bit address. In our example, 0x90 becomes 0x48 as follows: 0x90: 0101000 (we eliminate the rightmost bit) 0x48: 0010100 */ void writeSabreReg(byte regAddr, byte regVal) { Wire.beginTransmission(0x48); //Hard coded to the the Sabre/Buffalo device address Wire.send(regAddr); // Specifying the address of volume register Wire.send(regVal); // Writing the volume value into the register Wire.endTransmission(); } void setSabreVolume(byte regVal) { writeSabreReg(0, regVal); // set up volume in DAC1 writeSabreReg(1, regVal); // set up volume in DAC2 writeSabreReg(2, regVal); // set up volume in DAC3 writeSabreReg(3, regVal); // set up volume in DAC4 writeSabreReg(4, regVal); // set up volume in DAC5 writeSabreReg(5, regVal); // set up volume in DAC6 writeSabreReg(6, regVal); // set up volume in DAC7 writeSabreReg(7, regVal); // set up volume in DAC8 } /* PRINTING (DISPLAYING) THE VAVLUE OF VOLUME "NICELY FORMATTED" For now, it is hardcoded to print on line 3 (4th line) of the LCD and with the Volume Label specified below in setup(). This routine takes in the actual value of the volume level and displays it with proper format in the LDC. */ void printVol(byte regVal) { lcd.setCursor(15,3); if (regVal<10) // transition between two digit and one digit display { lcd.print("0"); // Add a leading zero lcd.print(regVal, DEC); } else { lcd.print(regVal, DEC); } } void printSelectBar(){ // PRINTING SELECTION INDICATOR lcd.setCursor(0,0); lcd.print(255,BYTE); lcd.setCursor(0,1); lcd.print(255,BYTE); lcd.setCursor(0,2); lcd.print(255,BYTE); lcd.setCursor(0,3); lcd.print(255,BYTE); } void clearSelectBar(){ // CLEARING SELECTION INDICATOR lcd.setCursor(0,0); lcd.print(" "); lcd.setCursor(0,1); lcd.print(" "); lcd.setCursor(0,2); lcd.print(" "); lcd.setCursor(0,3); lcd.print(" "); } void setAndPrintDPLL(byte regVal){ switch(regVal){ case 0: writeSabreReg(0x0B, 0x81); // Mode control 2 register lcd.setCursor(7,3); lcd.print("ZERO "); break; case 1: writeSabreReg(0x0B, 0x85); lcd.setCursor(7,3); lcd.print("LOWEST"); break; case 2: writeSabreReg(0x0B, 0x89); lcd.setCursor(7,3); lcd.print("LOW "); break; case 3: writeSabreReg(0x0B, 0x8D); lcd.setCursor(7,3); lcd.print("LO-MED"); break; case 4: writeSabreReg(0x0B, 0x91); lcd.setCursor(7,3); lcd.print("MEDIUM"); break; case 5: writeSabreReg(0x0B, 0x95); lcd.setCursor(7,3); lcd.print("MED-HI"); break; case 6: writeSabreReg(0x0B, 0x99); lcd.setCursor(7,3); lcd.print("HIGH "); break; case 7: writeSabreReg(0x0B, 0x9D); lcd.setCursor(7,3); lcd.print("HI-EST"); break; } } /*************************MAIN PROGRAM*************************************************************/ void setup() { // Set up the LCD's number of columns and rows: lcd.begin(20, 4); // Join the I2C bus as a master Wire.begin(); // Attach Interrupts attachInterrupt(0, rotEncoder, CHANGE); // ISR for rotary encoder // Set up the pin modes pinMode(VOLUPPIN, INPUT); // Button for Encoder pin for volume up digitalWrite(VOLUPPIN, HIGH); // If H/W debouncing is implemented, set to LOW pinMode(VOLDOWNPIN, INPUT); // Button for Encoder pin for volume down digitalWrite(VOLDOWNPIN, HIGH); // If H/W debouncing is implemented, set to LOW pinMode(SELECTPIN, INPUT); // Button for encoder switch digitalWrite(SELECTPIN, HIGH); // Enable pull-up resistor pinMode(6, OUTPUT); // Controls the brightness of LCD // Print the welcome message and other labels to the LCD lcd.clear(); lcd.setCursor(0,1); lcd.print(" H I F I D U I N O"); lcd.setCursor(0,2); lcd.print(" v.B03 "); delay(1000); lcd.clear(); delay(500); lcd.home(); lcd.setCursor(16,0); lcd.print("sr"); lcd.setCursor(2,1); lcd.print("Filt:SHARP"); // Default is sharp roll-off lcd.setCursor(2,2); lcd.print("Jitt:ON"); // Default is jitter reduction enabled lcd.setCursor (2,3); lcd.print("DPLL:LOWEST"); // Default is lowest bandwidth lcd.setCursor(16,2); lcd.print("vol"); lcd.setCursor(14,3); lcd.print("- dB"); dpllBW=1; // This is the default value of dpll bandwidth (lowest) sharp=true; // This is the default value of roll-off filter (sharp) jitter=true; // This is the default value for jitter reduction (ON) setSabreVolume(DEFAULTATTNU); // Set volume at startup otherwise // default is full volume printVol(DEFAULTATTNU/2); writeSabreReg(25,0); // Allow all DPLL settings } void loop() { // Print the sample rate (once every "INTERVAL_SAMPLE" time) if(millis() - displayMillis > INTERVAL_SAMPLE*1000) { displayMillis = millis(); // Saving last time we display sample rate lcd.setCursor(14,1); lcd.print(sampleRate(), DEC); } /* To debounce the switch, we detect the first instance of the "click" and then ignore the switch: first during the switch "bouncing" time and then while the switch remains pressed because one cannot lift the finger so fast. We want to register a single "click" per push of the switch */ if((digitalRead(SELECTPIN)==LOW)&&((millis()-debounceMillis)>INTERVAL_SWITCHBOUNCE)) { selectMode=true; // Now we are in select mode printSelectBar(); // Indicate we are in select mode debounceMillis=millis(); // Start debounce timer selectMillis=millis(); // Start being in select mode timer select++; // Move to next select option switch(select%5){ case VOL: printSelectBar(); break; case BRI: printSelectBar(); lcd.setCursor(0,0); lcd.print(126,BYTE); lcd.print(" Brightness"); break; case FIL: printSelectBar(); lcd.setCursor(0,1); lcd.print(126,BYTE); break; case JIT: printSelectBar(); lcd.setCursor(0,2); lcd.print(126,BYTE); break; case DPL: printSelectBar(); lcd.setCursor(0,3); lcd.print(126,BYTE); break; } // End of switch } if(rotating) { delay(INTERVAL_BOUNCE); // debounce by waiting INTERVAL_BOUNCE time switch(select%5){ case VOL: if (digitalRead(4) == digitalRead(2)) // CCW { if (currAttnu<MAXATTNU) // Check if not already at maximum attenuation (minimum Volume) { currAttnu+=2; // Increase 1 dB attenuation (decrease volume 1 db) setSabreVolume(currAttnu); // Write value into registers printVol(currAttnu/2); // Divide by 2 to print in whole dBs } } else // If not CCW, then it is CW { if (currAttnu>MINATTNU) // Check if not already at minimum attenuation (max volume) { currAttnu-=2; // Decrease attenuation 1 dB (increase volume 1 db) setSabreVolume(currAttnu); // Write value into registers printVol(currAttnu/2); // Divide by 2 to print in whole dBs } } break; case BRI: if (digitalRead(4) == digitalRead(2)) { // CCW, decrease brightness brightness-=5; analogWrite(BRIPIN,brightness); } else { // CW, increase brightness brightness+=5; analogWrite(BRIPIN,brightness); } selectMillis=millis(); // Update being-in-select-mode timer break; case FIL: // Toggle between Sharp and Slow filter if (sharp){ // If currently in sharp, toggle to slow roll off writeSabreReg(14, 6); // Register value for slow is 6 lcd.setCursor(7,1); lcd.print("SLOW "); sharp=false; // Now in slow roll off filter } else { // Toggle to sharp roll off filter writeSabreReg(14, 7); // register value for sharp roll off is 7 lcd.setCursor(7,1); lcd.print("SHARP"); sharp=true; // Now in sharp roll off filter } selectMillis=millis(); // Update being-in-select-mode timer break; case JIT: if (jitter){ // If jitter reduction is on, turn off writeSabreReg(0x0A, 0xCA); // Turn off Jitter reduction lcd.setCursor(7,2); lcd.print("OFF"); jitter=false; // Now jitter reduction is off } else { writeSabreReg(0x0A, 0xCE); // If jitter reduction is off, turn on lcd.setCursor(7,2); lcd.print("ON "); jitter=true; // Now jitter reduction is on } selectMillis=millis(); // Update being-in-select-mode timer break; case DPL: if (digitalRead(4) == digitalRead(2)) { // CCW dpllBW--; // select lower bandwidth setAndPrintDPLL(dpllBW%8); } else { dpllBW++; // select higher bandwidth setAndPrintDPLL(dpllBW%8); } selectMillis=millis(); // Update being-in-select-mode timer break; } // End of switch rotating=false; // Reset the flag } // End of while(rotating) // When the being-in-select mode timer expres, we revert to volume mode if(selectMode&&millis()-selectMillis > INTERVAL_SELECT*1000){ selectMode=false; clearSelectBar(); select=VOL; // Back to volume mode } } // End of loop()
Advertisement
Pingback: New code posted « H I F I D U I N O