I've done it already.
I just fixed the look up table to go though one cycle instead of 1.5 cycles.
Code
/*
* High-Frequency PWM Waveform Generator with RC Filter
*
* This sketch generates one of three waveforms (square, sawtooth, sine)
* by updating the PWM duty cycle on pin 9 at a rate determined by the desired
* waveform frequency and the number of samples per period.
*
* The PWM output is filtered through an external RC low-pass filter
* (e.g., a 470 Ω resistor in series with a 10 nF capacitor to ground)
* to produce a smooth analog voltage.
*
* User inputs (via Serial Monitor):
* - Waveform type: 1 = square, 2 = sawtooth, 3 = sine.
* - Desired waveform frequency in Hz.
*
* NOTE on Serial Input:
* A custom function getInput() is used to prompt for and retrieve a complete,
* non-empty line from the Serial Monitor without inserting delays. This avoids
* the problem of leftover end-of-line characters (EOL's) being interpreted as
* empty input.
*
* For more information on the Serial API, see:
* - Serial.begin(): https://docs.arduino.cc/reference/en/language/functions/communication/serial/begin/
* - Serial.available(): https://docs.arduino.cc/reference/en/language/functions/communication/serial/available/
* - Serial.readStringUntil(): https://docs.arduino.cc/reference/en/language/functions/communication/serial/readstringuntil/
*
* ++u/ripred3 – Feb 3, 2025
*
*/
#include <Arduino.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#define NUM_SAMPLES 64 // Number of samples per waveform period
#define PWM_PIN 9 // PWM output pin (Timer1 output)
// ---------- Global Variables ----------
volatile uint8_t waveform_type = 0; // 1: square, 2: sawtooth, 3: sine
volatile uint16_t sample_index = 0; // Current index for waveform sample progression
volatile uint8_t saw_value = 0; // Sawtooth waveform current value
// ---------- Sine Wave Lookup Table (8-bit values: 0-255) ----------
const uint8_t sine_table[NUM_SAMPLES] PROGMEM = {
128, 140, 152, 164, 176, 187, 198, 208,
217, 226, 233, 240, 245, 249, 252, 254,
255, 254, 252, 249, 245, 240, 233, 226,
217, 208, 198, 187, 176, 164, 152, 140,
128, 115, 103, 91, 79, 68, 57, 47,
38, 29, 22, 15, 10, 6, 3, 1,
1, 1, 3, 6, 10, 15, 22, 29,
38, 47, 57, 68, 79, 91, 103, 115
};
// ---------- Timer2 Prescaler Options ----------
struct PrescalerOption {
uint16_t prescaler;
uint8_t cs_bits; // Clock select bits for Timer2 (CS22:0)
};
PrescalerOption options[] = {
{1, (1 << CS20)},
{8, (1 << CS21)},
{32, (1 << CS21) | (1 << CS20)},
{64, (1 << CS22)},
{128, (1 << CS22) | (1 << CS20)},
{256, (1 << CS22) | (1 << CS21)},
{1024, (1 << CS22) | (1 << CS21) | (1 << CS20)}
};
#define NUM_OPTIONS (sizeof(options) / sizeof(options[0]))
// ---------- Timer2 ISR: Updates PWM Duty Cycle ----------
ISR(TIMER2_COMPA_vect) {
uint8_t output_val = 0;
switch (waveform_type) {
case 1: // Square wave: output 255 for first half of samples, then 0.
output_val = (sample_index < (NUM_SAMPLES / 2)) ? 255 : 0;
break;
case 2: // Sawtooth wave: continuously increment value.
output_val = saw_value;
saw_value++; // 8-bit arithmetic wraps from 255 back to 0.
break;
case 3: // Sine wave: retrieve value from lookup table.
output_val = pgm_read_byte(&(sine_table[sample_index]));
break;
default:
output_val = 0;
break;
}
sample_index++;
if (sample_index >= NUM_SAMPLES) {
sample_index = 0;
}
// Update Timer1's PWM duty cycle by writing to OCR1A.
OCR1A = output_val;
}
// ---------- Function: getInput -----------------
// Prompts the user and waits (busy-waiting) for a non-empty line from the Serial Monitor.
// Uses Serial.available() and Serial.readStringUntil() without adding delay() calls.
// For Serial API details, see:
// - Serial.available(): https://docs.arduino.cc/reference/en/language/functions/communication/serial/available/
// - Serial.readStringUntil(): https://docs.arduino.cc/reference/en/language/functions/communication/serial/readstringuntil/
String getInput(const char* prompt) {
Serial.println(prompt);
String input = "";
// Busy-wait until a non-empty line is received.
while (input.length() == 0) {
if (Serial.available() > 0) {
input = Serial.readStringUntil('\n');
input.trim(); // Remove any whitespace or EOL characters.
}
}
return input;
}
// ---------- Setup Timer2 for Waveform Updates ----------
void setup_timer2(uint32_t sample_rate) {
uint8_t chosen_cs = 0;
uint16_t chosen_ocr = 0;
// Determine a prescaler option yielding OCR2A <= 255.
for (uint8_t i = 0; i < NUM_OPTIONS; i++) {
uint32_t ocr = (F_CPU / (options[i].prescaler * sample_rate)) - 1;
if (ocr <= 255) {
chosen_cs = options[i].cs_bits;
chosen_ocr = ocr;
break;
}
}
// If no valid prescaler was found, use the maximum prescaler.
if (chosen_cs == 0) {
chosen_cs = options[NUM_OPTIONS - 1].cs_bits;
chosen_ocr = 255;
}
cli(); // Disable interrupts during Timer2 configuration.
TCCR2A = 0;
TCCR2B = 0;
TCNT2 = 0;
TCCR2A |= (1 << WGM21); // Set Timer2 to CTC mode.
OCR2A = chosen_ocr;
TCCR2B |= chosen_cs;
TIMSK2 |= (1 << OCIE2A); // Enable Timer2 Compare Match interrupt.
sei(); // Re-enable interrupts.
}
// ---------- Setup Timer1 for PWM Output on Pin 9 ----------
void setup_timer1_pwm() {
pinMode(PWM_PIN, OUTPUT);
cli(); // Disable interrupts during Timer1 configuration.
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
// Configure Timer1 for 8-bit Fast PWM on channel A (pin 9) in non-inverting mode.
TCCR1A |= (1 << WGM10) | (1 << COM1A1);
TCCR1B |= (1 << CS10); // No prescaling: PWM frequency ≈ 16MHz/256 ≈ 62.5 kHz.
sei(); // Re-enable interrupts.
}
// ---------- Setup Function ----------
void setup() {
Serial.begin(115200); // Preferred baud rate.
while (!Serial) { } // Wait for the Serial Monitor connection.
Serial.println(F("High-Frequency PWM Waveform Generator"));
Serial.println(F("======================================"));
// --- Get Waveform Type ---
String typeString = getInput("Enter waveform type (1 = square, 2 = sawtooth, 3 = sine):");
waveform_type = typeString.toInt();
Serial.print(F("Waveform type: "));
Serial.println(waveform_type);
// --- Get Desired Waveform Frequency ---
String freqString = getInput("Enter desired waveform frequency in Hz (e.g., 100):");
uint32_t waveform_freq = freqString.toInt();
Serial.print(F("Waveform frequency: "));
Serial.print(waveform_freq);
Serial.println(F(" Hz"));
// Compute the sample rate as: waveform frequency * NUM_SAMPLES.
uint32_t sample_rate = waveform_freq * NUM_SAMPLES;
Serial.print(F("Computed sample rate: "));
Serial.print(sample_rate);
Serial.println(F(" Hz"));
// Initialize PWM on Timer1.
setup_timer1_pwm();
// Initialize Timer2 to update the PWM duty cycle.
setup_timer2(sample_rate);
Serial.println(F("Setup complete."));
Serial.println(F("Remember to apply the RC low-pass filter (e.g., 470 Ω resistor + 10 nF capacitor) to PWM output on pin 9."));
}
// ---------- Main Loop ----------
void loop() {
// No processing is needed here as waveform generation is handled in the Timer2 ISR.
// The loop remains empty to allow uninterrupted timer interrupts.
}