/****************************************************************
Project: Audio Signal Generator
File: Main
Version: 1.0 May 2024
Written by: Alexander Butovsky
*****************************************************************
Hardware:
- Arduino NANO
- Generator module vers.2.1 with 12-bit DAC IC MCP4725
- Attenuator module vers.3.0 with 80 dB attenuator IC PT2257
- Mode pushbutton connected to Nano input pin 5
- Start/Stop pushbutton connected to Nano input pin 6
- Up pushbutton connected to Nano input pin 7
- Down pushbutton connected to Nano input pin 8
- Generator Module Range input connected to Nano output pin 11
- Generator Module, Attenuator module and LCD 1602 display on
I2C bus (A4 - SDA, A5 - SCL)
- Rotary encoder connection: CLK - pin 2, DT - pin 3,
SW - pin 4 (toggles coarse or fine tuning mode)
******************************************************************
Version history
1.0 May 2024 Initial version
******************************************************************/
// comment following line to turn debugging mode off
#define __DEBUG__
#ifdef __DEBUG__
uint8_t errcode = 0;
#endif
#include <EEPROM.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
//#include <Adafruit_MCP4725.h>
#define DACaddr 0x61 // Generator module I2C address
#define AttAddr 0x44 // Attenuator module I2C address
#define DispAddr 0x3F // Display module I2C address
// Pins for encoder:
#define encA 2 //Encoder CLK output
#define encB 3 //Encoder DT output
#define encSW 4 //Encoder switch
// Pins for pushbuttons:
#define modeButtonPin 5
#define startButtonPin 6
#define upButtonPin 7
#define downButtonPin 8
#define rangeOut 11 //Connect this pin with Generator
//module "Range" input
/*******************************************************
* Standard one-third octave frequencies(in Hz) *
********************************************************
* Range=0 --> 12, 16, 20, 25, 32, 40, 50, 63, 80, 100, *
* 125, 160, 200, 250, 315, 400, 500 *
* Range=1 --> 630, 800, 1000, 1250, 1600, 2000, 2500, *
* 3150, 4000, 5000, 6300, 8000, 10000, *
* 12500, 16000, 20000, 25000 *
*******************************************************/
uint16_t oneThirdOctave[35] = {
12, 16, 20, 25, 32, 40, 50, 63, 80, 100,
125, 160, 200, 250, 315, 400, 500, 630,
800, 1000, 1250, 1600, 2000, 2500, 3150,
4000, 5000, 6300, 8000, 10000, 12500,
16000, 20000, 25000
};
enum modes {
normal_freq, //single tone generation, normal screen, focus on frequency field (default)
normal_voltage, //single tone generation, normal screen, focus on dBu field
sweep_setup_min, //no generation, setup screen, focus on min frequency field
sweep_setup_max, //no generation, setup screen, focus on max frequency field
sweep_setup_num, //no generation, setup screen, focus on number of points field
sweep_setup_type, //no generation, setup screen, focus on LIN/LOG field
sweep_setup_time, //no generation, setup screen, focus on duration field
sweep //sweeping tone generation, normal screen, no focus, encoder disabled
};
modes mode = normal_freq; //Current mode, by default - normal_freq mode
uint8_t stepMode = 0; //"0" - 1/3 octave (Coarse)
//"1" - 1% steps (Fine)
uint8_t onState = 0; //"0" - Generation is stopped
//"1" - Generation is running
/* Program stores following 7 variables in EEPROM on */
/* any button click and restore them on program start. */
uint16_t freq = 1000; //Current frequency
int16_t dBu = 0; //Current voltage in dBu
uint16_t freq1 = 100; //Min frequency in sweep mode
uint16_t freq2 = 10000; //max frequency in sweep mode
uint16_t points = 15; //Number of points in sweep mode
uint16_t sweepType = 0; //"0" - linear sweep, "1" - logarithmic sweep
uint16_t duration = 5; //Cycle duration in sweep mode
/* End of variables to be stored in EEPROM */
uint8_t currentStateA; //Encoder CLK output value now
uint8_t lastStateA; //Encoder CLK output value before
LiquidCrystal_I2C lcd(DispAddr,16,2); //Display object: 16 symbols in a row, 2 rows
/***************************************
** **
** VARIOUS SUPPLEMENTARY FUNCTIONS: **
** **
***************************************/
//-----------------------------------//
// Store program settings to EEPROM. //
// Function have to be called on any //
// button click. //
//-----------------------------------//
void storeSettings() {
EEPROM.put(0, freq);
EEPROM.put(2, dBu);
EEPROM.put(4, freq1);
EEPROM.put(6, freq2);
EEPROM.put(8, points);
EEPROM.put(10, sweepType);
EEPROM.put(12, duration);
EEPROM.update(14, 0x55); // Write the magic key to indicate
// that data has been stored.
}
//---------------------------------//
// Get program settings previously //
// stored in EEPROM. Function have //
// to be called on program start. //
//---------------------------------//
void retrieveSettings() {
if (EEPROM.read(14) == 0x55) { // Magic key is found - it's
EEPROM.get(0, freq); // ok to read stored settings,
EEPROM.get(2, dBu); // otherwise function doesn't
EEPROM.get(4, freq1); // read them and keeps default
EEPROM.get(6, freq2); // values intact.
EEPROM.get(8, points);
EEPROM.get(10, sweepType);
EEPROM.get(12, duration);
}
}
//---------------------------//
// Suspend further operation //
// while button is pressed //
//---------------------------//
void waitButtonRelease(int buttonPin) {
do {
delay(5);
} while (digitalRead(buttonPin) == LOW);
}
//-------------------------------//
// Get code to be sent to DAC by //
// means of linear interpolation //
//-------------------------------//
#define range0min 60 //corresponds to oneThirdOctave[0] = 12 Hz
#define range0max 2647 //corresponds to oneThirdOctave[16] = 500 Hz
#define range1min 86 //corresponds to oneThirdOctave[17] = 630 Hz
#define range1max 3505 //corresponds to oneThirdOctave[33] = 25000 Hz
// Values between points above are being calculated by map() function
int fCode(uint16_t Hz) {
if (Hz < 630) {
return (int)(map(Hz, oneThirdOctave[0], oneThirdOctave[16], range0min, range0max) + 0.5);
}
else {
return (int)(map(Hz, oneThirdOctave[17], oneThirdOctave[33], range1min, range1max) + 0.5);
}
}
//----------------------------------------------------//
// Length (in symbols) of numeric value to be printed //
//----------------------------------------------------//
uint8_t printLength(int16_t Param) {
uint8_t neg = (Param < 0) ? 1 : 0; // if Param is negative - add one extra position for "-"
if (abs(Param) > 9999) return (neg + 5);
else if (abs(Param) > 999) return (neg + 4);
else if (abs(Param) > 99) return (neg + 3);
else if (abs(Param) > 9) return (neg + 2);
else return (neg + 1);
}
//-----------------------------------------//
// Convert dBu to millivolts value. //
// Input argument is the decibel value //
// relative to 0 dBu, where 0 dBu = 775 mV //
//-----------------------------------------//
double dBu_to_voltage(int16_t dBu) {
return pow(10.0, float(dBu) / 20) * 775;
}
/*************************************
** **
** HANDLING SWEEP GENERATOR MODE: **
** **
*************************************/
void handleSweepMode() {
}
/*****************************************
** **
** HANDLING ROTARY ENCODER ROTATIONS: **
** **
*****************************************/
//----------------------------------------//
// Handle frequency change in coarse mode //
//----------------------------------------//
uint16_t handleCoarseModeStep(bool dir, uint16_t CurrentFreq) {
uint8_t index = 0;
do {
index++;
} while (oneThirdOctave[index] <= CurrentFreq);
if (dir) { //clockwise
return (index < 34) ? oneThirdOctave[index] : oneThirdOctave[33] ;
}
else { //counterclockwise
return (index > 1) ? oneThirdOctave[index-2] : oneThirdOctave[0] ;
}
}
//--------------------------------------//
// Handle frequency change in fine mode //
//--------------------------------------//
// Step for low frequences (<150 Hz) is 1 Hz,
// for higher frequencies (>150 Hz) step is 1%
#define fineModeStep 1.01
uint16_t handleFineModeStep(bool dir, uint16_t CurrentFreq) {
uint16_t NewFreq = CurrentFreq;
if (dir) { //clockwise
if (CurrentFreq < 150) NewFreq = CurrentFreq + 1;
else if (CurrentFreq < 25000) NewFreq = int(CurrentFreq * fineModeStep + 0.5);
}
else { //counterclockwise
if ((CurrentFreq < 150) && (CurrentFreq > 12)) NewFreq = CurrentFreq - 1;
else NewFreq = int(CurrentFreq / fineModeStep + 0.5);
}
return NewFreq;
}
//------------------------------------------//
// Handle encoder rotation in various modes //
//------------------------------------------//
// 1. normal_freq mode - encoder changes frequency
void handleFreqChange(bool dir) {
freq = (stepMode == 0) ? handleCoarseModeStep(dir, freq) : handleFineModeStep(dir, freq) ;
}
// 2. normal_voltage mode - encoder changes voltage
void handleVoltageChange(bool dir) {
if (dir && (dBu < 10)) dBu++;
if (!dir && (dBu > -60)) dBu--;
attSetAttenuation(abs(dBu - 10), 1); // (attenuation, channel)
}
// 3. sweep_setup_min mode - encoder changes min frequency
void handleMinFreqChange(bool dir) {
freq1 = (stepMode == 0) ? handleCoarseModeStep(dir, freq1) : handleFineModeStep(dir, freq1) ;
}
// 4. sweep_setup_max mode - encoder changes max frequency
void handleMaxFreqChange(bool dir) {
freq2 = (stepMode == 0) ? handleCoarseModeStep(dir, freq2) : handleFineModeStep(dir, freq2) ;
}
// 5. sweep_setup_num mode - encoder changes number of points
void handleNum(bool dir) {
if (dir && (points < 50)) points++;
if (!dir && (points > 2)) points--;
}
// 6. sweep_setup_type mode - encoder toggles LIN/LOG sweep type
void handleSweepType() {
sweepType = sweepType ^ 0x1;
}
// 7. sweep_setup_time mode - encoder changes cycle time in seconds
void handleTime(bool dir) {
if (dir && (duration < 50)) duration++;
if (!dir && (duration > 2)) duration--;
}
/******************************************
** **
** PROGRAM EXECUTION STARTS FROM HERE: **
** **
******************************************/
void setup() {
// Initialize pins
pinMode(modeButtonPin, INPUT_PULLUP);
pinMode(startButtonPin, INPUT_PULLUP);
pinMode(upButtonPin, INPUT_PULLUP);
pinMode(downButtonPin, INPUT_PULLUP);
pinMode(rangeOut, OUTPUT);
pinMode(encSW, INPUT); //Use INPUT_PULLUP if encoder doesn't have pullup resistor
pinMode(encA, INPUT);
pinMode(encB, INPUT);
#ifdef __DEBUG__
Serial.begin(9600);
Serial.println("---- Signal Generator v.1.0 ----");
#endif
//--- Get previously stored settings from EEPROM
retrieveSettings();
// --- Initialize LCD module
lcd.begin(16,2,0);
lcd.backlight();
// --- Display greeting message
display_greeting();
// --- Display initial screen
display_redraw();
}
void loop() {
// ====================================
// Read mode button and toggle mode
// ====================================
if (digitalRead(modeButtonPin) == LOW) {
waitButtonRelease(modeButtonPin);
if ((mode == normal_freq) || (mode == normal_voltage)) {
mode = sweep_setup_min;
attMute();
}
else {
mode = normal_freq;
attUnmute();
}
display_redraw();
storeSettings();
}
// =================================================
// Read Start/Stop button and turn generator on/off
// =================================================
if (digitalRead(startButtonPin) == LOW) {
waitButtonRelease(startButtonPin);
if ((mode == normal_freq) || (mode == normal_voltage)) {
if (onState == 0) {
onState = 1;
attUnmute();
}
else {
onState = 0;
attMute();
}
}
else {
handleSweepMode();
}
display_redraw();
storeSettings();
}
// ==================================
// Read Up button and move focus up
// ==================================
if (digitalRead(upButtonPin) == LOW) {
waitButtonRelease(upButtonPin);
switch (mode) {
case normal_voltage:
mode = normal_freq;
break;
case sweep_setup_min:
mode = sweep_setup_max;
break;
case sweep_setup_max:
mode = sweep_setup_min;
break;
case sweep_setup_num:
mode = sweep_setup_min;
break;
case sweep_setup_type:
mode = sweep_setup_min;
break;
case sweep_setup_time:
mode = sweep_setup_max;
break;
}
display_redraw();
storeSettings();
}
// ======================================
// Read Down button and move focus down
// ======================================
if (digitalRead(downButtonPin) == LOW) {
waitButtonRelease(downButtonPin);
switch (mode) {
case normal_freq:
mode = normal_voltage;
break;
case sweep_setup_min:
mode = sweep_setup_num;
break;
case sweep_setup_max:
mode = sweep_setup_time;
break;
case sweep_setup_num:
mode = sweep_setup_type;
break;
case sweep_setup_type:
mode = sweep_setup_time;
break;
case sweep_setup_time:
mode = sweep_setup_num;
break;
}
display_redraw();
storeSettings();
}
// ====================================
// Read encoder button and toggle step
// ====================================
if (digitalRead(encSW) == LOW) {
waitButtonRelease(encSW);
if ((mode == normal_freq) || (mode == sweep_setup_min) || (mode == sweep_setup_max)) {
stepMode = stepMode ^ 0x1; //Invert stepMode value
}
}
// ================================
// Read encoder and set parameter
// ================================
currentStateA = digitalRead(encA);
if ((currentStateA != lastStateA) && (currentStateA == LOW)) {
bool dir = (digitalRead(encB) == HIGH);
switch (mode) {
case normal_freq:
handleFreqChange(dir);
break;
case normal_voltage:
handleVoltageChange(dir);
break;
case sweep_setup_min:
handleMinFreqChange(dir);
break;
case sweep_setup_max:
handleMaxFreqChange(dir);
break;
case sweep_setup_num:
handleNum(dir);
break;
case sweep_setup_type:
handleSweepType();
break;
case sweep_setup_time:
handleTime(dir);
break;
default:
// statements
break;
}
display_redraw();
}
lastStateA = currentStateA;
}