#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// === OLED Setup ===
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); // -1 = no reset
// === CTCSS Frequencies (standard EIA/TIA-603) ===
const float ctcssFreq[] = {
67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5, 85.4, 88.5, 91.5,
94.8, 97.4, 100.0, 103.5, 107.2, 110.9, 114.8, 118.8, 123.0,
127.3, 131.8, 136.5, 141.3, 146.2, 151.4, 156.7, 159.8, 162.2,
165.5, 167.9, 171.3, 173.8, 177.3, 179.9, 183.5, 186.2, 189.9,
192.8, 196.6, 199.5, 203.5, 206.5, 210.7, 218.1, 225.7, 229.1,
233.6, 241.8, 250.3, 254.1
};
const int numTones = sizeof(ctcssFreq) / sizeof(ctcssFreq[0]);
int currentTone = 0;
// === Hardware ===
const int dacPin = 25; // DAC integrato ESP32
const int btnUp = 18;
const int btnDown = 19;
// === Sine Table (256 samples, 8-bit) ===
const int TABLE_SIZE = 256;
uint8_t sineTable[TABLE_SIZE];
// === Timing ===
const unsigned long SAMPLE_RATE = 10000; // 10 kHz
const unsigned long SAMPLE_INTERVAL_US = 1000000UL / SAMPLE_RATE; // 100 µs
unsigned long lastSampleTime = 0;
// === Phase control ===
volatile int tableIndex = 0;
volatile unsigned int phaseIncrement = 0; // samples per step
// === Debounce ===
unsigned long lastDebounceUp = 0;
unsigned long lastDebounceDown = 0;
const unsigned long debounceDelay = 200; // ms
// === Setup ===
void setup() {
// Inizializza tabella seno (0–255)
for (int i = 0; i < TABLE_SIZE; i++) {
sineTable[i] = (uint8_t)(127.5 + 127.5 * sin(2 * PI * i / TABLE_SIZE));
}
// Pulsanti
pinMode(btnUp, INPUT_PULLUP);
pinMode(btnDown, INPUT_PULLUP);
// OLED
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.begin(115200);
Serial.println("Errore OLED!");
for (;;);
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("CTCSS Generator");
display.println("ESP32 + DAC");
display.display();
delay(1500);
// Inizializza primo tono
updatePhaseIncrement();
updateDisplay();
}
void updatePhaseIncrement() {
// Campioni per ciclo: TABLE_SIZE
// Campioni al secondo: SAMPLE_RATE
// Campioni per passo = (freq * TABLE_SIZE) / SAMPLE_RATE
phaseIncrement = (unsigned int)((ctcssFreq[currentTone] * TABLE_SIZE) / SAMPLE_RATE * 65536.0);
}
void updateDisplay() {
display.clearDisplay();
display.setCursor(0, 0);
display.setTextSize(1);
display.println("CTCSS Generator");
display.drawLine(0, 10, 127, 10, SSD1306_WHITE);
display.setTextSize(2);
display.setCursor(0, 15);
display.print(ctcssFreq[currentTone], 1);
display.println(" Hz");
display.setTextSize(1);
display.setCursor(0, 40);
display.print("Tono ");
display.print(currentTone + 1);
display.print("/");
display.print(numTones);
display.setCursor(0, 52);
display.println("GPIO18/19: +/-");
display.display();
}
void loop() {
// === Generazione segnale (time-critical) ===
if (micros() - lastSampleTime >= SAMPLE_INTERVAL_US) {
dacWrite(dacPin, sineTable[tableIndex >> 8]); // usa byte alto di un accumulatore a 16 bit
tableIndex = (tableIndex + phaseIncrement) & 0xFFFF; // wrap automatico
lastSampleTime = micros();
}
// === Lettura pulsanti con debounce ===
if (digitalRead(btnUp) == LOW) {
if (millis() - lastDebounceUp > debounceDelay) {
currentTone = (currentTone + 1) % numTones;
updatePhaseIncrement();
updateDisplay();
lastDebounceUp = millis();
}
}
if (digitalRead(btnDown) == LOW) {
if (millis() - lastDebounceDown > debounceDelay) {
currentTone = (currentTone - 1 + numTones) % numTones;
updatePhaseIncrement();
updateDisplay();
lastDebounceDown = millis();
}
}
// Piccolo yield per Wi-Fi/Bluetooth (non necessario, ma buona pratica)
delay(1);
}