Created
November 30, 2014 04:08
-
-
Save anonymous/75fbf71175910d971529 to your computer and use it in GitHub Desktop.
Arduino CS3318 30/11/2014
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Control code for Dr_EM's CS3318 volume control board by dhalbakken 30/11/2014 */ | |
#include <SPI.h> | |
#include <EEPROM.h> | |
#include <Rotary.h> | |
//#define DEBUG true | |
#define USE_SPI_LIB true | |
#define RESET_VOL 9 | |
/* The following are defined in <standard/pins_arduino.h>. We will use pin 10 to select the CS3318. | |
static const uint8_t SS = 10; // Slave Select | |
static const uint8_t MOSI = 11; // Master Out Slave In | |
static const uint8_t MISO = 12; // Master In Slave Out | |
static const uint8_t SCK = 13; // Serial Clock; this also happens to be connected to the yellow LED on the Arduino UNO R3 board | |
*/ | |
#define CS3318_ADDR 0x80 /* CS3318 chip address and R/W bit */ | |
#define VOL_MIN 0x12 /* -96dB is the maximum attenuation available in the CS3318 */ | |
#define VOL_MAX 0xd4 /* I want +1dB to be the maximum gain for my application */ | |
#define MASTER_VOL_REG 0x11 /* CS3318 master volume control register */ | |
#define MUTE_CONTROL_REG 0x0b /* register for setting up control of CS3318 mute pin */ | |
#define ZERO_X_CONTROL_REG 0x0c /* register for controlling zero-crossing detector */ | |
#define PDN_ALL_REG 0x0e /* register for master power down bit */ | |
#define PDN_ALL_CLEAR 0 /* clear all bits (but bit 0 is the only one of interest) */ | |
#define MUTE_PIN_DISABLE 0 /* clear all bits (but bit 5 is the only one of interest) */ | |
#define N_CHANNELS 8 /* number of channels in CS3811 */ | |
#define EEPROM_SIZE 1024 /* the Arduino Uno has 1KB of EEPROM */ | |
#define EEPROM_ERASE_BYTE 0xff | |
#define WRITE_VOL_DELAY 60000 /* delay 1 minute (60000 ms) after volume change before sending value to EEPROM */ | |
volatile byte CurrentVolume = VOL_MIN; | |
byte PreviousVolume = VOL_MIN; | |
int CurrentBufferPosition = 0; | |
boolean TriggerVolumePersist = false; | |
boolean EEPROMMalfunction = false; | |
unsigned long TriggerStartMS = 0L; /* persistence delay start time in milliseconds */ | |
/* | |
This sketch uses the rotary encoder handler written by Ben Buxton. | |
At this time, the code can be downloaded from https://github.com/brianlow/Rotary | |
*/ | |
Rotary RotaryEncoder = Rotary(2, 3); /* The pins the rotary encoder uses must be interrupt pins. */ | |
/* from CS3318 Data Sheet (Copyright (c) Cirrus Logic, Inc. 2006): | |
The CS3318 will remain in a completely powered-down state with the control port inaccessible until the RESET | |
pin is brought high. Once RESET is high, the control port will be accessible, but the internal amplifiers | |
will remain powered-down until the PDN_ALL bit is cleared. | |
To bring a channel out of power-down, both the PDN_ALL and the channel's PDNx bit must be cleared. By | |
default, all channels' PDNx bits are cleared, and the PDN_ALL bit is set. To minimize audible artifacts during | |
power-up process, the CS3318 automatically holds each channel's volume at mute until its amplifier has | |
completed its power-up sequence. Once the power-up process is complete, each channel's volume will automatically | |
be set to the correct level according to the CS3318's control port settings. | |
To place a channel in power-down, either the channel's PDNx bit or the PDN_ALL bit must be set. To minimize | |
audible artifacts during the power-down process, the CS3318 automatically places each channel in | |
mute before the amplifier begins its power-down sequence. | |
The power-up and power-down muting/volume changes are implemented as dictated by the zero-crossing | |
detection settings (see Zero-Crossing Detection on page 22). If an immediate power-up or power-down is | |
required, the zero-crossing mode should be set to immediate before changing the power-down state of the | |
device or channel. | |
Recommended Power-Up Sequence | |
1. Hold RESET low until the power supplies are stable. In this state, the control port is reset to its default | |
settings. | |
2. Bring RESET high. The device will remain in a low power state with the PDN_ALL bit set by default. | |
The control port will be accessible. | |
3. The desired register settings can be loaded while the PDN_ALL bit remains set. | |
4. Clear the PDN_ALL bit to initiate the power-up sequence. | |
Recommended Power-Down Sequence | |
1. Set the PDN_ALL bit to mute all channels and power-down all internal amplifiers. | |
2. If desired, hold RESET low to bring the CS3318's power consumption to an absolute minimum. | |
*/ | |
/* notes to self: | |
1. Implement mute if impending power down is detected. | |
2. The formula for volume: Register Value = (2 × Desired Volume Setting in dB) + 210 | |
*/ | |
/* persistence: | |
The EEPROM in the Arduino's microcontroller will be used to store the current volume. Since a memory address | |
in the EEPROM is guaranteed for a maximum of 100,000 erase/write cycles, we will use the entire kilobyte (Arduino | |
UNO) as a circular buffer to minimize the number of cycles at any one location. And, since the value of the | |
volume can never be 0xff, we will erase the entire EEPROM to all 0xff values one time using a separate sketch | |
before flashing this program into the Arduino's memory. | |
On startup of this program, we will look for the first byte in the EEPROM that differs from 0xff. That is the | |
current value of the volume, and the location of that byte is the current read/write location in the circular | |
buffer. When the time comes to write a new volume value, the byte at the current location will be erased to | |
0xff, the location will be incremented (or wrapped around to 0 again) and the new value will be written to the | |
new location. | |
If the EEPROM ever wears out, I will just install a new microcontroller chip or buy a new Arduino board to | |
replace it. We need to protect ourselves from the worn-out case, though. One way to do that might be to read | |
back a byte that was written and compare it to the value that was intended to be written. If they're different, | |
the EEPROM has failed. After a failure such as this, I think the best thing might be to set the volume to the | |
last known good value and start flashing the Arduino's built-in LED to signal failure. As the user, I should | |
notice that volume adjustments are no longer possible, and I should look at the Arduino's LED to see if it is | |
flashing. Or we could get too smart for our britches and do a voice announcement. | |
I calculate the EEPROM will wear out in approximately 100 years with 1 minute updates, but my calcs are | |
probably wrong. | |
*/ | |
#ifdef USE_SPI_LIB | |
void sendByteToVol(byte registr, byte data) | |
{ | |
digitalWrite(SS, LOW); /* select the CS3318 */ | |
// delayMicroseconds(1); /* CS3312 requires at least 20 ns (.02 microsecond) from chip select to clock edge */ | |
SPI.transfer(CS3318_ADDR); | |
// delayMicroseconds(8); /* 1 clock cycle with 125 kHz clock */ | |
SPI.transfer(registr); | |
// delayMicroseconds(8); /* 1 clock cycle with 125 kHz clock */ | |
SPI.transfer(data); | |
// delayMicroseconds(8); /* 1 clock cycle with 125 kHz clock */ | |
digitalWrite(SS, HIGH); /* deselect the CS3318 */ | |
delayMicroseconds(2); /* CS3312 requires at least 1 microsecond between transmissions */ | |
#ifdef DEBUG | |
Serial.print("Sent byte to CS3318 at "); | |
Serial.print(CS3318_ADDR, HEX); | |
Serial.print(", register "); | |
Serial.print(registr, HEX); | |
Serial.print(", data "); | |
Serial.println(data, HEX); | |
#endif | |
} | |
#else | |
/* try some code from linux_man */ | |
// data sent on RISING edge of clock | |
void spi_send_byte (unsigned char data) | |
{ | |
int i; | |
for (i=0; i<8; i++) { | |
if (data & 0x80) { | |
digitalWrite(MOSI, HIGH); | |
} | |
else { | |
digitalWrite(MOSI, LOW); | |
} | |
delay(1); | |
// strobe clock | |
digitalWrite(SCK, HIGH); | |
delay(1); | |
digitalWrite(SCK, LOW); | |
delay(1); | |
data = (data << 1); // left shift next bit over | |
} // for | |
} | |
//void spi_write_register (unsigned char map_byte, unsigned char data_byte) | |
void sendByteToVol(unsigned char map_byte, unsigned char data_byte) | |
{ | |
digitalWrite(SS, LOW); // start an SPI transaction | |
delay(1); | |
spi_send_byte(B10000000); // chip_addr and r/w bit | |
delay(2); | |
spi_send_byte(map_byte); // memory 'addr' (register addr) | |
delay(2); | |
spi_send_byte(data_byte); // the 8bits of user data (write that to the register) | |
// to be a little cleaner, we also turn data off when done | |
digitalWrite(MOSI, LOW); | |
delay(1); | |
digitalWrite(SS, HIGH); // finish an SPI transaction | |
delay(1); | |
} | |
#endif | |
int findCurrentBufferPosition() | |
{ | |
for(int i = 0; i < EEPROM_SIZE; i++) | |
{ | |
byte v = EEPROM.read(i); | |
if(v != EEPROM_ERASE_BYTE) | |
{ | |
return i; | |
} | |
} | |
return 0; | |
} | |
byte getPersistedVolume() | |
{ | |
return EEPROM.read(CurrentBufferPosition); | |
} | |
void setVolume() | |
{ | |
sendByteToVol(MASTER_VOL_REG, CurrentVolume); | |
#ifdef DEBUG | |
Serial.print("New volume: "); | |
Serial.println(CurrentVolume, HEX); | |
#endif | |
} | |
boolean persistVolume() | |
{ | |
#ifdef DEBUG | |
Serial.print("Persisting volume: "); | |
Serial.println(CurrentVolume, HEX); | |
#endif | |
EEPROM.write(CurrentBufferPosition, EEPROM_ERASE_BYTE); | |
if(EEPROM.read(CurrentBufferPosition) != EEPROM_ERASE_BYTE) | |
{ | |
#ifdef DEBUG | |
Serial.print("Cannot erase EEPROM at "); | |
Serial.println(CurrentBufferPosition, HEX); | |
#endif | |
return false; | |
} | |
if(++CurrentBufferPosition >= EEPROM_SIZE) | |
{ | |
CurrentBufferPosition = 0; | |
} | |
EEPROM.write(CurrentBufferPosition, CurrentVolume); | |
if(EEPROM.read(CurrentBufferPosition) != CurrentVolume) | |
{ | |
#ifdef DEBUG | |
Serial.print("Cannot write to EEPROM at "); | |
Serial.println(CurrentBufferPosition, HEX); | |
#endif | |
return false; | |
} | |
#ifdef DEBUG | |
Serial.print("Persisted volume: "); | |
Serial.print(CurrentVolume, HEX); | |
Serial.print(" at: "); | |
Serial.println(CurrentBufferPosition, HEX); | |
#endif | |
return true; | |
} | |
void setup() | |
{ | |
digitalWrite(RESET_VOL, LOW); /* # 1 in CS3318 Recommended Power-Up sequence: set low until everything is stabilized */ | |
pinMode(RESET_VOL, OUTPUT); /* CS3318 active low reset */ | |
#ifdef DEBUG | |
Serial.begin(9600); /* set up Serial library at 9600 bps */ | |
#endif | |
#ifdef USE_SPI_LIB | |
/* from Atmel 328P datasheet page 161: | |
When configured as a Master, the SPI interface has no automatic control of the SS line. This must be handled | |
by user software before communication can start. When this is done, writing a byte to the SPI Data Register | |
starts the SPI clock generator, and the hardware shifts the eight bits into the Slave. After shifting one byte, the | |
SPI clock generator stops, setting the end of Transmission Flag (SPIF). If the SPI Interrupt Enable bit (SPIE) in | |
the SPCR Register is set, an interrupt is requested. The Master may continue to shift the next byte by writing it | |
into SPDR, or signal the end of packet by pulling high the Slave Select, SS line. The last incoming byte will be | |
kept in the Buffer Register for later use. | |
*/ | |
digitalWrite(SCK, LOW); /* clock is idle when low, data is clocked into the CS3318 on the rising edge; the yellow LED will light up when this is HIGH */ | |
/* will be done by SPI.begin() | |
pinMode(SCK, OUTPUT); /* SPI clock line */ | |
digitalWrite(MOSI, LOW); | |
/* will be done by SPI.begin() | |
pinMode(MOSI, OUTPUT); /* SPI data line */ | |
/* will be done by SPI.begin() | |
digitalWrite(SS, HIGH); /* set high so it is deselected for now */ | |
/* will be done by SPI.begin() | |
pinMode(SS, OUTPUT); /* SPI active low chip select */ | |
/* slow down SPI clock??? */ | |
/* note: CS3318 datasheet says chip can handle 6 MHz clock. Arduino default is 4 MHz */ | |
// SPI.setClockDivider(SPI_CLOCK_DIV16); /* 1 MHz clock */ | |
// SPI.setClockDivider(SPI_CLOCK_DIV128); /* 125 kHz clock */ | |
SPI.setDataMode(SPI_MODE0); | |
SPI.setBitOrder(MSBFIRST); | |
SPI.begin(); | |
/* Atmel AVR151 app note (doc2585.pdf) page 11 says, "Clear SPI Interrupt Flag by reading SPSR and SPDR" during initialization. */ | |
byte x = SPSR; | |
x = SPDR; | |
#else | |
digitalWrite(SCK, LOW); /* clock is idle when low, data is clocked into the CS3318 on the rising edge; the yellow LED will light up when this is HIGH */ | |
pinMode(SCK, OUTPUT); /* SPI clock line */ | |
digitalWrite(MOSI, LOW); | |
pinMode(MOSI, OUTPUT); /* SPI data line */ | |
digitalWrite(SS, HIGH); /* set high so it is deselected for now */ | |
pinMode(SS, OUTPUT); /* SPI active low chip select */ | |
#endif | |
/* setup rotary encoder interrupt */ | |
PCICR |= (1 << PCIE2); | |
PCMSK2 |= (1 << PCINT18) | (1 << PCINT19); | |
sei(); | |
delay(2000); /* wait for other hardware to stabilize */ | |
CurrentBufferPosition = findCurrentBufferPosition(); /* position of last write in circular buffer */ | |
digitalWrite(RESET_VOL, HIGH); /* #2 in CS3318 Recommended Power-Up Sequence: take CS3318 out of reset mode */ | |
delay(20); /* wait for CS3318 come out of reset mode */ | |
/* the CS3318 datasheet says, "SPI Mode is selected if there is a high-to-low transition on the CS pin after the RESET pin has been brought high." */ | |
/* so here is the high-to-low transition noted above, selecting SPI serial control */ | |
digitalWrite(SS, LOW); | |
delay(1); | |
digitalWrite(SS, HIGH); | |
/* Dr_EM found that a minimum of 250 ms delay is necessary here. */ | |
/* see http://www.diyaudio.com/forums/analog-line-level/239950-cs3318-pcb-layout-15.html#post4116958 */ | |
delay(250); | |
/* write strange errata sequence to CS3318 per Cirrus_PCN_CS3308-18_B0_C0.pdf */ | |
sendByteToVol(0x00, 0x99); | |
sendByteToVol(0x1d, 0x86); | |
sendByteToVol(0x1f, 0x02); | |
sendByteToVol(0x00, 0x00); | |
/* disable hardware mute pin of the CS3318 */ | |
sendByteToVol(MUTE_CONTROL_REG, MUTE_PIN_DISABLE); | |
/* Default zero crossing detection timeout 18ms, which is about 1/2 cycle for a 31 Hz signal */ | |
// sendByteToVol(ZERO_X_CONTROL_REG, ); /* keep defaults???? */ | |
/* init master volume */ | |
CurrentVolume = PreviousVolume = getPersistedVolume(); | |
#ifdef DEBUG | |
Serial.print("Startup persisted volume: "); | |
Serial.println(CurrentVolume, HEX); | |
#endif | |
if(CurrentVolume > VOL_MAX || CurrentVolume < VOL_MIN) | |
{ | |
CurrentVolume = PreviousVolume = VOL_MIN; | |
} | |
sendByteToVol(MASTER_VOL_REG, CurrentVolume); | |
/* # 4 in CS3318 Recommended Power-Up Sequence: clear PDN_ALL bit */ | |
sendByteToVol(PDN_ALL_REG, PDN_ALL_CLEAR); | |
} | |
ISR(PCINT2_vect) | |
{ | |
unsigned char result = RotaryEncoder.process(); | |
if (result) | |
{ | |
if(result == DIR_CW) | |
{ | |
if(CurrentVolume < VOL_MAX) | |
{ | |
CurrentVolume++; | |
} | |
} | |
else | |
{ | |
if(CurrentVolume > VOL_MIN) | |
{ | |
CurrentVolume--; | |
} | |
} | |
} | |
} | |
void loop() | |
{ | |
if(!EEPROMMalfunction) | |
{ | |
if(PreviousVolume != CurrentVolume) | |
{ | |
setVolume(); | |
PreviousVolume = CurrentVolume; | |
if(EEPROM.read(CurrentBufferPosition) != CurrentVolume) | |
{ | |
TriggerStartMS = millis(); | |
TriggerVolumePersist = true; | |
} | |
} | |
if(TriggerVolumePersist) | |
{ | |
if(millis() - TriggerStartMS >= WRITE_VOL_DELAY) /* this will accomodate wrap-around of millis() return value */ | |
{ | |
if(!persistVolume()) /* write to EEPROM and check for malfunction */ | |
{ | |
/* EEPROM not functioning correctly */ | |
EEPROMMalfunction = true; | |
} | |
TriggerVolumePersist = false; | |
} | |
} | |
} | |
else | |
{ | |
/* blink the Arduino UNO's internal yellow LED, which also happens to be connected internally to the SCK pin on the UNO */ | |
digitalWrite(SCK, HIGH); | |
delay(1000); | |
digitalWrite(SCK, LOW); | |
delay(1000); | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment