ESP32-S3 I2S signal generator with serial command interface
This commit is contained in:
commit
1d89359807
|
|
@ -0,0 +1,3 @@
|
||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||||
|
project(signal_generator)
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
idf_component_register(
|
||||||
|
SRCS "main.c" "i2s_out.c" "waveform.c" "cmd.c"
|
||||||
|
INCLUDE_DIRS "."
|
||||||
|
REQUIRES driver esp_driver_i2s
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
#include "cmd.h"
|
||||||
|
#include "waveform.h"
|
||||||
|
#include "debug.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#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 <hz> [-c <ch>] sine wave\n"
|
||||||
|
" square -f <hz> [-d <duty%%>] [-c <ch>] square wave\n"
|
||||||
|
" saw -f <hz> [-c <ch>] triangle wave\n"
|
||||||
|
" off [-c <ch>] silence\n"
|
||||||
|
"\n"
|
||||||
|
" -f <hz> frequency (float ok)\n"
|
||||||
|
" -d <0-100> duty cycle %% (square only, default 50)\n"
|
||||||
|
" -c <ch> 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 <hz> 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);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
void cmd_start_task(void);
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
#include "i2s_out.h"
|
||||||
|
#include "waveform.h"
|
||||||
|
#include "debug.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#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);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
void i2s_out_init(void);
|
||||||
|
void i2s_out_start_task(void);
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
#include "waveform.h"
|
||||||
|
#include "debug.h"
|
||||||
|
|
||||||
|
#include <math.h>
|
||||||
|
#include <string.h>
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#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);
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
CONFIG_IDF_TARGET="esp32s3"
|
||||||
|
CONFIG_ESPTOOLPY_FLASHSIZE_2MB=y
|
||||||
Loading…
Reference in New Issue