commit 1d89359807a04472dfe55793bbf676c48409d2e6 Author: jess Date: Fri Apr 3 09:30:08 2026 -0700 ESP32-S3 I2S signal generator with serial command interface diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..2bc0595 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,3 @@ +cmake_minimum_required(VERSION 3.16) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(signal_generator) diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt new file mode 100644 index 0000000..e8a49da --- /dev/null +++ b/main/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRCS "main.c" "i2s_out.c" "waveform.c" "cmd.c" + INCLUDE_DIRS "." + REQUIRES driver esp_driver_i2s +) diff --git a/main/cmd.c b/main/cmd.c new file mode 100644 index 0000000..d23c99d --- /dev/null +++ b/main/cmd.c @@ -0,0 +1,181 @@ +#include "cmd.h" +#include "waveform.h" +#include "debug.h" + +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" + +#define TAG "cmd" + +static const char *wave_name(wave_type_t t) +{ + switch (t) { + case WAVE_SINE: return "sine"; + case WAVE_SQUARE: return "square"; + case WAVE_SAW: return "saw"; + default: return "off"; + } +} + +static void print_help(void) +{ + printf("\n" + "Signal Generator Commands:\n" + " sine -f [-c ] sine wave\n" + " square -f [-d ] [-c ] square wave\n" + " saw -f [-c ] triangle wave\n" + " off [-c ] silence\n" + "\n" + " -f frequency (float ok)\n" + " -d <0-100> duty cycle %% (square only, default 50)\n" + " -c 0/l = left, 1/r = right, omit = both\n" + "\n"); +} + +typedef struct { + wave_type_t type; + float freq; + float duty; + int ch; /* 0=left, 1=right, -1=both */ + bool valid; +} parsed_cmd_t; + +static void str_lower(char *s) +{ + for (; *s; s++) *s = tolower((unsigned char)*s); +} + +static parsed_cmd_t parse_line(char *line) +{ + parsed_cmd_t cmd = { + .type = WAVE_NONE, + .freq = 440.0f, + .duty = 0.5f, + .ch = -1, + .valid = false, + }; + + /* strip newline */ + char *nl = strchr(line, '\n'); + if (nl) *nl = '\0'; + nl = strchr(line, '\r'); + if (nl) *nl = '\0'; + + if (line[0] == '\0') return cmd; + + str_lower(line); + + char *tok = strtok(line, " \t"); + if (!tok) return cmd; + + if (strcmp(tok, "sine") == 0) cmd.type = WAVE_SINE; + else if (strcmp(tok, "square") == 0) cmd.type = WAVE_SQUARE; + else if (strcmp(tok, "saw") == 0) cmd.type = WAVE_SAW; + else if (strcmp(tok, "off") == 0) cmd.type = WAVE_NONE; + else return cmd; + + bool need_freq = (cmd.type != WAVE_NONE); + bool got_freq = false; + + while ((tok = strtok(NULL, " \t")) != NULL) { + if (strcmp(tok, "-f") == 0) { + tok = strtok(NULL, " \t"); + if (!tok) return cmd; + cmd.freq = strtof(tok, NULL); + if (cmd.freq <= 0.0f || cmd.freq > 24000.0f) { + printf("frequency out of range (0 < f <= 24000)\n"); + return cmd; + } + got_freq = true; + } else if (strcmp(tok, "-d") == 0) { + tok = strtok(NULL, " \t"); + if (!tok) return cmd; + float d = strtof(tok, NULL); + if (d < 0.0f || d > 100.0f) { + printf("duty must be 0-100\n"); + return cmd; + } + cmd.duty = d / 100.0f; + } else if (strcmp(tok, "-c") == 0) { + tok = strtok(NULL, " \t"); + if (!tok) return cmd; + if (strcmp(tok, "0") == 0 || strcmp(tok, "l") == 0) + cmd.ch = 0; + else if (strcmp(tok, "1") == 0 || strcmp(tok, "r") == 0) + cmd.ch = 1; + else { + printf("channel: 0/l (left) or 1/r (right)\n"); + return cmd; + } + } + } + + if (need_freq && !got_freq) { + printf("-f required\n"); + return cmd; + } + + cmd.valid = true; + return cmd; +} + +static void apply_cmd(const parsed_cmd_t *cmd) +{ + const char *ch_str; + if (cmd->ch == 0) ch_str = "L"; + else if (cmd->ch == 1) ch_str = "R"; + else ch_str = "L+R"; + + if (cmd->ch < 0) { + waveform_set(0, cmd->type, cmd->freq, cmd->duty); + waveform_set(1, cmd->type, cmd->freq, cmd->duty); + } else { + waveform_set(cmd->ch, cmd->type, cmd->freq, cmd->duty); + } + + if (cmd->type == WAVE_NONE) { + ESP_LOGI(TAG, "%s: off", ch_str); + } else if (cmd->type == WAVE_SQUARE) { + ESP_LOGI(TAG, "%s: %s %.1f Hz duty %.0f%%", + ch_str, wave_name(cmd->type), cmd->freq, cmd->duty * 100.0f); + } else { + ESP_LOGI(TAG, "%s: %s %.1f Hz", ch_str, wave_name(cmd->type), cmd->freq); + } +} + +static void cmd_task(void *arg) +{ + char line[128]; + + DBG("cmd_task started"); + print_help(); + printf("> "); + fflush(stdout); + + for (;;) { + if (fgets(line, sizeof(line), stdin) == NULL) { + vTaskDelay(pdMS_TO_TICKS(100)); + continue; + } + + parsed_cmd_t cmd = parse_line(line); + if (cmd.valid) { + apply_cmd(&cmd); + } else if (line[0] != '\0' && line[0] != '\n' && line[0] != '\r') { + print_help(); + } + + printf("> "); + fflush(stdout); + } +} + +void cmd_start_task(void) +{ + xTaskCreatePinnedToCore(cmd_task, "cmd", 4096, NULL, 5, NULL, 0); +} diff --git a/main/cmd.h b/main/cmd.h new file mode 100644 index 0000000..20f51ce --- /dev/null +++ b/main/cmd.h @@ -0,0 +1,3 @@ +#pragma once + +void cmd_start_task(void); diff --git a/main/debug.h b/main/debug.h new file mode 100644 index 0000000..4190387 --- /dev/null +++ b/main/debug.h @@ -0,0 +1,9 @@ +#pragma once + +#include "esp_log.h" + +#ifdef DEBUG_MODE + #define DBG(fmt, ...) ESP_LOGD("siggen", fmt, ##__VA_ARGS__) +#else + #define DBG(fmt, ...) do {} while(0) +#endif diff --git a/main/i2s_out.c b/main/i2s_out.c new file mode 100644 index 0000000..b122d43 --- /dev/null +++ b/main/i2s_out.c @@ -0,0 +1,69 @@ +#include "i2s_out.h" +#include "waveform.h" +#include "debug.h" + +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "driver/i2s_std.h" +#include "driver/gpio.h" +#include "esp_log.h" + +#define TAG "i2s_out" +#define BUF_FRAMES 480 + +#define PIN_BCLK GPIO_NUM_16 +#define PIN_WS GPIO_NUM_15 +#define PIN_DOUT GPIO_NUM_17 + +static i2s_chan_handle_t tx_handle; + +void i2s_out_init(void) +{ + i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER); + chan_cfg.dma_frame_num = BUF_FRAMES; + ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle, NULL)); + + i2s_std_config_t std_cfg = { + .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SAMPLE_RATE), + .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO), + .gpio_cfg = { + .mclk = I2S_GPIO_UNUSED, + .bclk = PIN_BCLK, + .ws = PIN_WS, + .dout = PIN_DOUT, + .din = I2S_GPIO_UNUSED, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false, + }, + }, + }; + ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle, &std_cfg)); + ESP_ERROR_CHECK(i2s_channel_enable(tx_handle)); + + ESP_LOGI(TAG, "I2S TX initialized: 48kHz 16-bit stereo, BCLK=%d WS=%d DOUT=%d", + PIN_BCLK, PIN_WS, PIN_DOUT); +} + +static void i2s_out_task(void *arg) +{ + int16_t buf[BUF_FRAMES * 2]; + size_t bytes_written; + + DBG("i2s_out_task started"); + + for (;;) { + for (int i = 0; i < BUF_FRAMES; i++) { + buf[i * 2] = waveform_next_sample(0); + buf[i * 2 + 1] = waveform_next_sample(1); + } + i2s_channel_write(tx_handle, buf, sizeof(buf), &bytes_written, portMAX_DELAY); + } +} + +void i2s_out_start_task(void) +{ + xTaskCreatePinnedToCore(i2s_out_task, "i2s_out", 4096, NULL, 10, NULL, 1); +} diff --git a/main/i2s_out.h b/main/i2s_out.h new file mode 100644 index 0000000..319f0e5 --- /dev/null +++ b/main/i2s_out.h @@ -0,0 +1,4 @@ +#pragma once + +void i2s_out_init(void); +void i2s_out_start_task(void); diff --git a/main/main.c b/main/main.c new file mode 100644 index 0000000..119baaf --- /dev/null +++ b/main/main.c @@ -0,0 +1,21 @@ +#include "waveform.h" +#include "i2s_out.h" +#include "cmd.h" +#include "debug.h" + +#include "esp_log.h" + +#define TAG "siggen" + +void app_main(void) +{ + ESP_LOGI(TAG, "Signal Generator starting"); + DBG("app_main entry"); + + waveform_init(); + i2s_out_init(); + i2s_out_start_task(); + cmd_start_task(); + + ESP_LOGI(TAG, "all tasks running"); +} diff --git a/main/waveform.c b/main/waveform.c new file mode 100644 index 0000000..c9c41d6 --- /dev/null +++ b/main/waveform.c @@ -0,0 +1,73 @@ +#include "waveform.h" +#include "debug.h" + +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +#ifndef M_PI +#define M_PI 3.14159265358979323846f +#endif + +static channel_state_t channels[2]; +static SemaphoreHandle_t mutex; + +void waveform_init(void) +{ + mutex = xSemaphoreCreateMutex(); + memset(channels, 0, sizeof(channels)); + channels[0].duty = 0.5f; + channels[1].duty = 0.5f; + DBG("waveform_init complete"); +} + +void waveform_set(int ch, wave_type_t type, float freq, float duty) +{ + if (ch < 0 || ch > 1) return; + xSemaphoreTake(mutex, portMAX_DELAY); + channels[ch].type = type; + channels[ch].freq = freq; + channels[ch].duty = duty; + channels[ch].phase = 0.0f; + xSemaphoreGive(mutex); + DBG("waveform_set ch=%d type=%d freq=%.1f duty=%.2f", ch, type, freq, duty); +} + +static inline int16_t generate(channel_state_t *s) +{ + float p = s->phase; + float v; + + switch (s->type) { + case WAVE_SINE: + v = sinf(2.0f * M_PI * p); + break; + case WAVE_SQUARE: + v = (p < s->duty) ? 1.0f : -1.0f; + break; + case WAVE_SAW: + if (p < 0.5f) + v = p * 4.0f - 1.0f; + else + v = 3.0f - p * 4.0f; + break; + default: + return 0; + } + + s->phase += s->freq / (float)SAMPLE_RATE; + if (s->phase >= 1.0f) + s->phase -= 1.0f; + + return (int16_t)(AMPLITUDE * v); +} + +int16_t waveform_next_sample(int ch) +{ + if (ch < 0 || ch > 1) return 0; + xSemaphoreTake(mutex, portMAX_DELAY); + int16_t s = generate(&channels[ch]); + xSemaphoreGive(mutex); + return s; +} diff --git a/main/waveform.h b/main/waveform.h new file mode 100644 index 0000000..7bd473e --- /dev/null +++ b/main/waveform.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +#define SAMPLE_RATE 48000 +#define AMPLITUDE 16384 + +typedef enum { + WAVE_NONE = 0, + WAVE_SINE, + WAVE_SQUARE, + WAVE_SAW, +} wave_type_t; + +typedef struct { + wave_type_t type; + float freq; + float duty; + float phase; +} channel_state_t; + +void waveform_init(void); +void waveform_set(int ch, wave_type_t type, float freq, float duty); +int16_t waveform_next_sample(int ch); diff --git a/sdkconfig.defaults b/sdkconfig.defaults new file mode 100644 index 0000000..9f5bcf5 --- /dev/null +++ b/sdkconfig.defaults @@ -0,0 +1,2 @@ +CONFIG_IDF_TARGET="esp32s3" +CONFIG_ESPTOOLPY_FLASHSIZE_2MB=y