This is an old revision of the document!
This project is an electric guitar effects pedal built around the ATmega328P Xplained Mini development board.
The pedal receives the signal from an electric guitar, processes it using the microcontroller, and sends the modified signal further to an amplifier or audio system. The main effects chosen for this project are distortion and tremolo, together with a simple clean/bypass mode.
The project started from the idea that guitar pedals are both useful and interesting from a technical point of view. They are good examples of systems that combine analog electronics with embedded software. Instead of making a very complex multi-effect unit, the goal was to create something simpler, realistic, and possible to build by hand with common components.
This project is useful for us because it helps us apply concepts learned in the laboratories, such as ADC, PWM, timers, interrupts, GPIO, I2C, and UART, in a practical product. It can also be useful for other students or hobbyists who want to understand the basics of digital audio effects on a microcontroller.
The project can be split into a few simple functional blocks:
Suggested block diagram:
Signal flow:
Guitar Input → Analog Conditioning → ADC → Effect Processing → PWM Output → Output Filter → Guitar Amplifier
Control flow:
The interaction is simple: the input signal is first prepared so it can be safely read by the microcontroller. Then the ADC samples it, the firmware applies either distortion or tremolo, and the result is sent to the PWM output. After filtering, the output becomes an analog signal again and goes to the amplifier.
The electric guitar produces an AC signal, but the ADC of the microcontroller works with positive voltages only. Because of this, the signal has to be adapted before sampling.
The analog conditioning stage contains:
This stage is important because it makes the signal compatible with the ADC and improves stability during processing.
The main processing unit is the ATmega328P from the Xplained Mini board. It handles:
The output signal is generated as PWM. Since PWM is not directly suitable as an audio signal, it is passed through an RC low-pass filter. After filtering, the signal is sent to the output jack.
An op-amp buffer can also be used here to improve output stability.
The user controls are:
The firmware will be written in C/C++ using an AVR-compatible development environment:
The project will try to keep the code as simple as possible. Only a few external libraries may be used:
Most of the main logic will be implemented manually.
This project includes functionality from multiple laboratories:
The firmware can be organized into the following modules:
The general execution flow is:
This mode sends the input further without applying a strong audio effect. In the final hardware version, the footswitch may also provide real hardware bypass.
Distortion is implemented by increasing the amplitude of the signal and clipping it when it goes above a threshold.
Example logic:
The distortion intensity is controlled by a potentiometer.
Tremolo changes the signal amplitude periodically. It is implemented by multiplying the input signal with a slowly changing factor generated in software.
Parameters:
The modulation can be generated using a counter or a lookup table.
At this stage, the project has a complete design plan and a clear implementation direction.
The expected final result is a working prototype that:
The project is a realistic embedded application that combines hardware and software in a clear and useful way.
Its main advantage is that it remains simple enough to be built by hand while still including important concepts from the laboratory. Instead of trying to implement many difficult effects, the project focuses on two effects that are easier to understand and demonstrate: distortion and tremolo.
Example files:
#define F_CPU 16000000UL
#include <avr/io.h> #include <avr/interrupt.h> #include <avr/pgmspace.h> #include <util/atomic.h> #include <util/delay.h> #include <stdint.h>
===================================================== PIN DEFINITIONS ===================================================== #define STATUS_LED PB5 #define BUTTON_EFFECT PD2 INT0 #define BUTTON_MODE PD3 INT1 #define PWM_OUT PD5 OC0B
#define ADC_AUDIO 0 PC0 / ADC0 #define ADC_GAIN 1 PC1 / ADC1 #define ADC_TREMOLO 2 PC2 / ADC2 OLED pins pe PORTB #define OLED_SCL PB0 #define OLED_SDA PB1 #define OLED_RST PB2 #define OLED_DC PB4
#define OLED_PORT PORTB #define OLED_DDR DDRB
#define OLED_WIDTH 128 #define OLED_PAGES 8 8 pentru 128×64; daca e 128×32 pune 4
AUDIO TUNING
Gain-urile sunt in Q4: 16 = 1x 32 = 2x 64 = 4x 80 = 5x 128 = 8x Pentru ca ai deja zgomot mare, distortion-ul este intentionat subtil.
#define CLEAN_GAIN_Q4 64 clean ramane 4x #define TREM_GAIN_Q4 80 tremolo putin mai tare
#define DIST_GAIN_MIN_Q4 96 6x #define DIST_GAIN_MAX_Q4 180 11.25x, distortion mai clar
#define DIST_CLIP_MAX_8BIT 46 clipping mai vizibil #define DIST_CLIP_MIN_8BIT 18 clipping mai agresiv la pot mare
#define NOISE_GATE_10BIT 4 #define AUDIO_SMOOTH_SHIFT 0 mai direct, mai puternic, dar si mai zgomotos
EFFECT STATE
#define EFFECT_DIST 0 #define EFFECT_TREM 1
volatile uint8_t effect_enabled = 0; volatile uint8_t selected_effect = EFFECT_DIST;
volatile uint8_t effect_button_event = 0; volatile uint8_t mode_button_event = 0;
volatile uint32_t g_millis = 0;
volatile uint32_t last_effect_interrupt_ms = 0; volatile uint32_t last_mode_interrupt_ms = 0;
#define DEBOUNCE_MS 50
valori citite din potentiometre, 0..255 volatile uint8_t pot_gain_raw = 128; volatile uint8_t pot_tremolo_raw = 128; parametri audio derivati din potentiometre volatile uint8_t g_dist_gain_q4 = DIST_GAIN_MIN_Q4; volatile uint8_t g_dist_clip = DIST_CLIP_MAX_8BIT; volatile uint8_t g_trem_step = 8;
pentru ADC multiplexing volatile uint8_t current_adc_channel = ADC_AUDIO; volatile uint8_t adc_skip_sample = 0; volatile uint16_t audio_sample_counter = 0;
SMALL UTILS
static inline uint16_t abs16(int16_t x) {
return (x < 0) ? (uint16_t)(-x) : (uint16_t)x;
}
static inline int16_t clamp_i16(int16_t x, int16_t min_value, int16_t max_value) {
if (x < min_value)
{
return min_value;
}
if (x > max_value)
{
return max_value;
}
return x;
}
===================================================== TIMER1: 1 ms system clock ===================================================== void timer1_init_1ms(void) { TCCR1A = 0; TCCR1B = 0; 16 MHz / 64 = 250 kHz
// 250 ticks = 1 ms OCR1A = 249;
TCCR1B |= (1 << WGM12); // CTC mode TCCR1B |= (1 << CS11) | (1 << CS10); // prescaler 64
TIMSK1 |= (1 << OCIE1A);
}
ISR(TIMER1_COMPA_vect) {
g_millis++;
}
uint32_t millis_get(void) {
uint32_t value;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
value = g_millis;
}
return value;
}
===================================================== BUTTONS: INT0 / INT1 ===================================================== void buttons_init(void) { PD2, PD3 input
DDRD &= ~(1 << BUTTON_EFFECT); DDRD &= ~(1 << BUTTON_MODE);
// internal pull-up PORTD |= (1 << BUTTON_EFFECT); PORTD |= (1 << BUTTON_MODE);
// INT0 falling edge EICRA |= (1 << ISC01); EICRA &= ~(1 << ISC00);
// INT1 falling edge EICRA |= (1 << ISC11); EICRA &= ~(1 << ISC10);
// clear old flags EIFR |= (1 << INTF0) | (1 << INTF1);
// enable INT0, INT1 EIMSK |= (1 << INT0) | (1 << INT1);
}
ISR(INT0_vect) {
uint32_t now = g_millis;
if ((uint32_t)(now - last_effect_interrupt_ms) >= DEBOUNCE_MS)
{
effect_button_event = 1;
last_effect_interrupt_ms = now;
}
}
ISR(INT1_vect) {
uint32_t now = g_millis;
if ((uint32_t)(now - last_mode_interrupt_ms) >= DEBOUNCE_MS)
{
mode_button_event = 1;
last_mode_interrupt_ms = now;
}
}
===================================================== PWM OUTPUT: PD5 / OC0B ===================================================== void pwm_init_pd5(void) { DDRD |= (1 « PWM_OUT); Timer0 Fast PWM, 8-bit
// OC0B non-inverting pe PD5 // PWM freq = 16 MHz / 256 = 62.5 kHz TCCR0A = (1 << COM0B1) | (1 << WGM01) | (1 << WGM00); TCCR0B = (1 << CS00);
OCR0B = 128;
}
===================================================== ADC: audio + potentiometers ===================================================== void adc_select_channel(uint8_t channel) { ADMUX = (1 « REFS0) | (channel & 0x0F); } void adc_init(void) { current_adc_channel = ADC_AUDIO; AVcc reference, ADC0 first
adc_select_channel(ADC_AUDIO);
// ADC enable, ADC interrupt, auto trigger, prescaler 64
// ADC clock = 16 MHz / 64 = 250 kHz
ADCSRA = (1 << ADEN) |
(1 << ADIE) |
(1 << ADATE) |
(1 << ADPS2) |
(1 << ADPS1);
// free running mode ADCSRB = 0;
// disable digital input buffers on ADC0, ADC1, ADC2 DIDR0 = (1 << ADC0D) | (1 << ADC1D) | (1 << ADC2D);
// start conversions ADCSRA |= (1 << ADSC);
}
===================================================== AUDIO PROCESSING ===================================================== void audio_process_sample(uint16_t raw10) { static int32_t dc_q8 = 1); } void oled_clear(void) { for (uint8_t page = 0; page < OLED_PAGES; page++) { oled_set_cursor(page, 0); for (uint8_t col = 0; col < OLED_WIDTH; col++) { oled_data(0x00); } } } void oled_init(void) { OLED_DDR |= (1 « OLED_SCL) | (1 « OLED_SDA) | (1 « OLED_RST) | (1 « OLED_DC); oled_pin_low(OLED_SCL); oled_pin_low(OLED_SDA); oled_reset(); oled_command(0xAE); display OFF
oled_command(0xD5); oled_command(0x80);
oled_command(0xA8); oled_command(0x3F); // 128x64
oled_command(0xD3); oled_command(0x00);
oled_command(0x40);
oled_command(0x8D); oled_command(0x14);
oled_command(0x20); oled_command(0x00);
oled_command(0xA1); oled_command(0xC8);
oled_command(0xDA); oled_command(0x12);
oled_command(0x81); oled_command(0xCF);
oled_command(0xD9); oled_command(0xF1);
oled_command(0xDB); oled_command(0x40);
oled_command(0xA4); oled_command(0xA6); oled_command(0xAF);
oled_clear();
} ===================================================== OLED FONT MINIMAL 5×7 ===================================================== const uint8_t FONT_SPACE[5] PROGMEM = {0x00,0x00,0x00,0x00,0x00}; const uint8_t FONT_COLON[5] PROGMEM = {0x00,0x36,0x36,0x00,0x00}; const uint8_t FONT_A[5] PROGMEM = {0x7E,0x11,0x11,0x11,0x7E}; const uint8_t FONT_C[5] PROGMEM = {0x3E,0x41,0x41,0x41,0x22}; const uint8_t FONT_D[5] PROGMEM = {0x7F,0x41,0x41,0x22,0x1C}; const uint8_t FONT_E[5] PROGMEM = {0x7F,0x49,0x49,0x49,0x41}; const uint8_t FONT_F[5] PROGMEM = {0x7F,0x09,0x09,0x09,0x01}; const uint8_t FONT_I[5] PROGMEM = {0x00,0x41,0x7F,0x41,0x00}; const uint8_t FONT_L[5] PROGMEM = {0x7F,0x40,0x40,0x40,0x40}; const uint8_t FONT_M[5] PROGMEM = {0x7F,0x02,0x0C,0x02,0x7F}; const uint8_t FONT_N[5] PROGMEM = {0x7F,0x04,0x08,0x10,0x7F}; const uint8_t FONT_O[5] PROGMEM = {0x3E,0x41,0x41,0x41,0x3E}; const uint8_t FONT_P[5] PROGMEM = {0x7F,0x09,0x09,0x09,0x06}; const uint8_t FONT_R[5] PROGMEM = {0x7F,0x09,0x19,0x29,0x46}; const uint8_t FONT_S[5] PROGMEM = {0x46,0x49,0x49,0x49,0x31}; const uint8_t FONT_T[5] PROGMEM = {0x01,0x01,0x7F,0x01,0x01}; const uint8_t FONT_X[5] PROGMEM = {0x63,0x14,0x08,0x14,0x63}; const uint8_t* get_char_bitmap(char c) { switch © { case 'A': return FONT_A; case 'C': return FONT_C; case 'D': return FONT_D; case 'E': return FONT_E; case 'F': return FONT_F; case 'I': return FONT_I; case 'L': return FONT_L; case 'M': return FONT_M; case 'N': return FONT_N; case 'O': return FONT_O; case 'P': return FONT_P; case 'R': return FONT_R; case 'S': return FONT_S; case 'T': return FONT_T; case 'X': return FONT_X; case ':': return FONT_COLON; case ' ': return FONT_SPACE; default: return FONT_SPACE; } } void oled_print_char(char c) { const uint8_t* bitmap = get_char_bitmap©; for (uint8_t i = 0; i < 5; i++) { oled_data(pgm_read_byte(&bitmap[i])); } oled_data(0x00); } void oled_print(const char* text) { while (*text) { oled_print_char(*text); text++; } } void oled_print_at(uint8_t page, uint8_t column, const char* text) { oled_set_cursor(page, column); oled_print(text); } void oled_show_status(uint8_t pedal_on, uint8_t effect) { oled_clear(); oled_print_at(1, 0, “PEDAL:”); if (pedal_on) { oled_print(“ON”); } else { oled_print(“OFF”); } oled_print_at(3, 0, “FX:”); if (effect == EFFECT_DIST) { oled_print(“DIST”); } else { oled_print(“TREM”); } } ===================================================== CONTROL PARAMETER UPDATE ===================================================== void update_audio_parameters_from_pots(void) {
uint8_t gain = pot_gain_raw; uint8_t trem = pot_tremolo_raw;
// distortion gain subtil
uint8_t dist_gain = DIST_GAIN_MIN_Q4 +
(uint8_t)(((uint16_t)gain * (DIST_GAIN_MAX_Q4 - DIST_GAIN_MIN_Q4)) / 255);
// la gain mare, clip-ul scade putin
uint8_t dist_clip = DIST_CLIP_MAX_8BIT -
(uint8_t)(((uint16_t)gain * (DIST_CLIP_MAX_8BIT - DIST_CLIP_MIN_8BIT)) / 255);
// tremolo speed: aproximativ lent -> rapid uint8_t trem_step = 2 + (uint8_t)(((uint16_t)trem * 60) / 255);
g_dist_gain_q4 = dist_gain; g_dist_clip = dist_clip; g_trem_step = trem_step;
} ===================================================== STATUS LED ===================================================== void status_led_update(void) { static uint32_t last_toggle_ms = 0; static uint8_t led_state = 0; uint32_t now = millis_get(); if (!effect_enabled) { PORTB &= ~(1 « STATUS_LED); led_state = 0; return; } if (selected_effect == EFFECT_DIST) { PORTB |= (1 « STATUS_LED); return; } Tremolo mode: blink status LED
if ((uint32_t)(now - last_toggle_ms) >= 250)
{
last_toggle_ms = now;
led_state = !led_state;
if (led_state)
{
PORTB |= (1 << STATUS_LED);
}
else
{
PORTB &= ~(1 << STATUS_LED);
}
}
} ===================================================== MAIN ===================================================== int main(void) { status LED
DDRB |= (1 << STATUS_LED);
// Init output and controls pwm_init_pd5();
// OLED init inainte de audio interrupts oled_init(); oled_show_status(effect_enabled, selected_effect);
timer1_init_1ms(); buttons_init(); adc_init();
sei();
uint32_t last_params_update_ms = 0; uint8_t screen_update_needed = 1;
while (1)
{
uint8_t local_effect_event = 0;
uint8_t local_mode_event = 0;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
if (effect_button_event)
{
local_effect_event = 1;
effect_button_event = 0;
}
if (mode_button_event)
{
local_mode_event = 1;
mode_button_event = 0;
}
}
if (local_effect_event)
{
effect_enabled = !effect_enabled;
screen_update_needed = 1;
}
if (local_mode_event)
{
if (selected_effect == EFFECT_DIST)
{
selected_effect = EFFECT_TREM;
}
else
{
selected_effect = EFFECT_DIST;
}
screen_update_needed = 1;
}
uint32_t now = millis_get();
if ((uint32_t)(now - last_params_update_ms) >= 20)
{
last_params_update_ms = now;
update_audio_parameters_from_pots();
}
if (screen_update_needed)
{
screen_update_needed = 0;
oled_show_status(effect_enabled, selected_effect);
}
status_led_update(); }
} ===== Bibliography/Resources ====== Export to PDF
static int16_t smoothed_pwm = 128; static uint16_t trem_phase = 0;
// DC removal foarte lent. // Semnalul este centrat in jur de 512, dar bias-ul real poate varia. dc_q8 += ((((int32_t)raw10 << 8) - dc_q8) >> 10);
int16_t centered10 = (int16_t)((((int32_t)raw10 << 8) - dc_q8) >> 8);
// noise gate foarte bland uint16_t ax = abs16(centered10);
if (ax < NOISE_GATE_10BIT)
{
centered10 = 0;
}
else if (ax < (NOISE_GATE_10BIT * 2))
{
centered10 /= 2;
}
uint8_t local_enabled = effect_enabled; uint8_t local_effect = selected_effect;
int16_t y = 0;
if (!local_enabled)
{
// Software bypass / clean passthrough
y = ((int32_t)centered10 * CLEAN_GAIN_Q4) >> 6;
}
else if (local_effect == EFFECT_DIST)
{
// Distortion subtil:
// 1. boost moderat
// 2. soft clipping simetric
uint8_t gain_q4 = g_dist_gain_q4;
uint8_t clip_value = g_dist_clip;
y = ((int32_t)centered10 * gain_q4) >> 6;
if (y > clip_value)
{
y = clip_value;
}
else if (y < -(int16_t)clip_value)
{
y = -(int16_t)clip_value;
}
// limit final mai larg, dar cu clipping clar
y = clamp_i16(y, -125, 125);
}
else
{
// Tremolo:
// clean signal * LFO triunghiular
y = ((int32_t)centered10 * TREM_GAIN_Q4) >> 6;
trem_phase += g_trem_step;
uint8_t tri;
if (trem_phase & 0x8000)
{
tri = (uint8_t)((65535 - trem_phase) >> 8); // 127..0
}
else
{
tri = (uint8_t)(trem_phase >> 8); // 0..127
}
// tremolo mai pronuntat: volum intre ~15% si ~100%
uint8_t modulation = 40 + ((uint16_t)tri * 215) / 127;
y = ((int32_t)y * modulation) >> 8; }
y = clamp_i16(y, -120, 120);
int16_t target_pwm = 128 + y;
if (target_pwm < 0)
{
target_pwm = 0;
}
else if (target_pwm > 255)
{
target_pwm = 255;
}
#if AUDIO_SMOOTH_SHIFT > 0smoothed_pwm += ((target_pwm - smoothed_pwm) >> AUDIO_SMOOTH_SHIFT); OCR0B = (uint8_t)smoothed_pwm;#else
OCR0B = (uint8_t)target_pwm;#endif } ISR(ADC_vect) {
uint16_t value = ADC;
// Dupa schimbarea canalului ADC, aruncam prima mostra.
if (adc_skip_sample)
{
adc_skip_sample = 0;
return;
}
if (current_adc_channel == ADC_AUDIO)
{
audio_process_sample(value);
audio_sample_counter++;
// Din cand in cand citim potentiometrele.
// Nu le citim continuu, ca sa pastram audio-ul cat mai stabil.
if (audio_sample_counter >= 256)
{
audio_sample_counter = 0;
current_adc_channel = ADC_GAIN;
adc_select_channel(ADC_GAIN);
adc_skip_sample = 1;
}
}
else if (current_adc_channel == ADC_GAIN)
{
pot_gain_raw = (uint8_t)(value >> 2);
current_adc_channel = ADC_TREMOLO;
adc_select_channel(ADC_TREMOLO);
adc_skip_sample = 1;
}
else
{
pot_tremolo_raw = (uint8_t)(value >> 2);
current_adc_channel = ADC_AUDIO;
adc_select_channel(ADC_AUDIO);
adc_skip_sample = 1;
}
}
=====================================================
OLED LOW LEVEL
=====================================================
static void oled_pin_high(uint8_t pin)
{
OLED_PORT |= (1 « pin);
}
static void oled_pin_low(uint8_t pin)
{
OLED_PORT &= ~(1 « pin);
}
void oled_spi_write(uint8_t data)
{
for (uint8_t i = 0; i < 8; i++)
{
if (data & 0x80)
{
oled_pin_high(OLED_SDA);
}
else
{
oled_pin_low(OLED_SDA);
}
oled_pin_high(OLED_SCL);
oled_pin_low(OLED_SCL);
data «= 1;
}
}
void oled_command(uint8_t cmd)
{
oled_pin_low(OLED_DC);
oled_spi_write(cmd);
}
void oled_data(uint8_t data)
{
oled_pin_high(OLED_DC);
oled_spi_write(data);
}
void oled_reset(void)
{
oled_pin_low(OLED_RST);
_delay_ms(50);
oled_pin_high(OLED_RST);
_delay_ms(50);
}
void oled_set_cursor(uint8_t page, uint8_t column)
{
oled_command(0xB0 + page);
oled_command(0x00 + (column & 0x0F));
oled_command(0x10 + ((column » 4) & 0x0F