Skip to content

Instantly share code, notes, and snippets.

Created October 27, 2017 02:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save anonymous/b197c8b70bb9482c9bc5ff8511f9bf89 to your computer and use it in GitHub Desktop.
Save anonymous/b197c8b70bb9482c9bc5ff8511f9bf89 to your computer and use it in GitHub Desktop.
Arduino UNO R3 code for AK4490 DAC
#include <LiquidCrystal.h>
#include <rotary.h>
#include <Wire.h>
#include <EEPROM.h>
#include <LcdBarGraph.h>
/*
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
Also it uses LcdBarGraph by Balazs Kelemen available at this time from:
http://www.arduinolibraries.info/libraries/lcd-bar-graph
The rotary shaft encoder similar to the one I used is described at:
https://wemakethings.net/2014/05/26/rotary-encoder-teardown/
If you search aliexpress.com for "optical rotary encoder 600" you should be able
to find a source for one.
I opened up the encoder and bypassed the 5V regulator so that I could operat it
directly from a 5V supply.
I used a level shifter between the AKM4490 board and the Arduino Uno as
described here:
http://joe-ideas.blogspot.com/2013/09/bidirectional-33v-5v-evel-shifter.html
*/
/*
LCD connections:
* LCD RS pin to digital pin 12
* LCD Enable pin to digital pin 11
* LCD D4 pin to digital pin 5
* LCD D5 pin to digital pin 4
* LCD D6 pin to digital pin 3
* LCD D7 pin to digital pin 2
* LCD R/W pin to ground
* 10K resistor:
* ends to +5V and ground
* wiper to LCD VO pin (pin 3)
*/
/* 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 will never be 0 by design, we will erase the entire EEPROM to all 0 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 0. 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.
I calculate the EEPROM will wear out in approximately 100 years with 1 minute updates, but my calcs are
probably wrong.
*/
//#define DEBUG true
#define LCD_ROWS 4
#define LCD_COLS 20
#define VOL_MIN 1 // 0 is used as the EEPROM escape character; 0 value mutes the AK4490, and there are other ways to do that
#define VOL_MAX 0xff
#define RotaryDecimationFactor 10 // pick a value that gives you the attenuation change rate you like when you turn the rotary encoder
#define AK4490_0_CHIP_ADDRESS 0x12 // CAD0 and CAD1 == 'L'
#define CONTROL1_REGISTER 0
#define LEFT_VOL_REGISTER 3
#define RIGHT_VOL_REGISTER 4
#define EEPROM_SIZE 1024 /* the Arduino Uno has 1KB of EEPROM */
#define EEPROM_ERASE_BYTE 0 // use this value as an escape character in the EEPROM
#define WRITE_VOL_DELAY 60000 /* delay 1 minute (60000 ms) after volume change before sending value to EEPROM */
// initialize the library with the numbers of the interface pins
LiquidCrystal lcd(12, 11, 10, 9, 8, 7);
LcdBarGraph *lbg;
Rotary RotaryEncoder = Rotary(2, 3); /* The pins the rotary encoder uses must be interrupt pins. */
volatile byte CurrentVolume = VOL_MIN; // declared volatile because ISR changes value
byte PreviousVolume = VOL_MAX;
volatile byte RotaryCount = 0; // declared volatile because ISR changes value
int CurrentBufferPosition = 0;
boolean TriggerVolumePersist = false;
boolean EEPROMMalfunction = false;
unsigned long TriggerStartMS = 0L; /* persistence delay start time in milliseconds */
ISR(PCINT2_vect)
{
unsigned char result = RotaryEncoder.process();
if (result)
{
if(RotaryCount < RotaryDecimationFactor)
{
++RotaryCount;
if(RotaryCount >= RotaryDecimationFactor)
{
if(result == DIR_CW)
{
if(CurrentVolume < VOL_MAX)
{
CurrentVolume++;
}
}
else
{
if(CurrentVolume > VOL_MIN)
{
CurrentVolume--;
}
}
RotaryCount = 0;
}
}
}
}
byte haveAK4490()
{
Wire.beginTransmission(AK4490_0_CHIP_ADDRESS);
byte Error = Wire.endTransmission();
if (Error == 0)
{
return true;
}
return false;
}
uint8_t readI2C(uint8_t devAddr, uint8_t regAddr)
{
Wire.beginTransmission(devAddr);
Wire.write(regAddr); // Queues the address of the register
Wire.endTransmission(); // Sends the address of the register
Wire.requestFrom(devAddr, (uint8_t)1); // request one byte from address
if(Wire.available()) // Wire.available indicates if data is available
{
return Wire.read(); // Wire.read() reads the data on the wire
}
return 0; // If no data in the wire, then return 0 to indicate error
}
void writeI2C(byte devAddr, byte regAddr, byte regVal)
{
Wire.beginTransmission(devAddr);
Wire.write(regAddr); // Specifying the address of register
Wire.write(regVal); // Writing the value into the register
Wire.endTransmission();
}
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(byte Volume)
{
writeI2C(AK4490_0_CHIP_ADDRESS, LEFT_VOL_REGISTER, Volume);
writeI2C(AK4490_0_CHIP_ADDRESS, RIGHT_VOL_REGISTER, Volume);
}
boolean persistVolume()
{
#ifdef DEBUG
Serial.print("Persisting attenuation: ");
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 attenuation: ");
Serial.print(CurrentVolume, HEX);
Serial.print(" at: ");
Serial.println(CurrentBufferPosition, HEX);
#endif
return true;
}
void setupAK4490()
{
writeI2C(AK4490_0_CHIP_ADDRESS, CONTROL1_REGISTER, 0x87); // auto clock sensing, 24 bit I2S, RSTN normal
}
void setup() {
int i = 0;
Wire.begin(); // Join the I2C bus as a master
#ifdef DEBUG
Serial.begin(9600); /* set up Serial library at 9600 bps */
#endif
// set up the LCD's number of columns and rows
lcd.begin(LCD_COLS, LCD_ROWS);
lbg = new LcdBarGraph(&lcd, LCD_COLS); // couldn't make this work as a global; got random garbage in partial characters
lcd.clear(); // put LCD back into DDRAM mode; see https://forum.arduino.cc/index.php?topic=459931.0
// connect pullups to rotary encoder pins
digitalWrite(2, HIGH);
digitalWrite(3, HIGH);
// setup rotary encoder interrupt
PCICR |= (1 << PCIE2);
PCMSK2 |= (1 << PCINT18) | (1 << PCINT19);
sei();
CurrentBufferPosition = findCurrentBufferPosition(); /* position of last write in circular buffer */
/* init attenuation */
CurrentVolume = getPersistedVolume();
#ifdef DEBUG
Serial.print("Startup persisted attenuation: ");
Serial.println(CurrentVolume, HEX);
#endif
PreviousVolume = EEPROM_ERASE_BYTE; // kick off display of volume during 1st pass through loop
if(CurrentVolume > VOL_MAX || CurrentVolume < VOL_MIN)
{
CurrentVolume = VOL_MIN;
}
// wait for AK4490 I2C
for(i = 0; i < 1000; i++)
{
if(haveAK4490())
{
break;
}
delay(1);
}
if(i == 1000)
{
lcd.setCursor(0, 2);
lcd.print("AK4490 Not Found");
}
else
{
setupAK4490();
setVolume(CurrentVolume);
}
}
void loop()
{
if(!EEPROMMalfunction)
{
if(PreviousVolume != CurrentVolume)
{
PreviousVolume = CurrentVolume;
setVolume(CurrentVolume);
lbg->drawValue(CurrentVolume, VOL_MAX); // update bar graph
lcd.setCursor(0, 1);
lcd.print(" ");
String StrAttn("-");
StrAttn += ((byte)~CurrentVolume)/2;
if(!(CurrentVolume % 2))
{
StrAttn += ".5";
}
StrAttn += (" dB");
lcd.setCursor(9 - StrAttn.length(), 1);
lcd.print(StrAttn);
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);
lcd.setCursor(0, 1);
lcd.print("EEPROM Error");
delay(1000);
}
}
/*
* EEPROM Erase
*
* Sets all of the bytes of the EEPROM to 0.
* This example code is in the public domain.
*/
#include <EEPROM.h>
void setup()
{
// write a 0xff to all 1024 bytes of the UNO R3 EEPROM
for (int i = 0; i < 1024; i++)
{
if(0xff != EEPROM.read(i))
{
EEPROM.write(i, 0);
}
}
// turn the LED on when we're done
digitalWrite(13, HIGH);
}
void loop()
{
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment