Skip to content

Instantly share code, notes, and snippets.

Created November 30, 2014 04:08
Show Gist options
  • Save anonymous/75fbf71175910d971529 to your computer and use it in GitHub Desktop.
Save anonymous/75fbf71175910d971529 to your computer and use it in GitHub Desktop.
Arduino CS3318 30/11/2014
/* 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