Este projeto está relacionado com Gerar Senóide.
Versão usando ESP-IDF Puro…
A idéia é criar uma pequena rotina que:
Gera senóide na frequência de: 440 Hz
Usa frequência/faxa de amostragem:
Abordagem/versão usando “Angle Accumulator”:
Geração direta de uma senoide usando acumulador de ângulo.
O ãngulo é incrementado a cada nova amostragem (interrupção de um timer/ISR):
E o valor instantâneo da senoide é calculado via:
Usa DAC1: GPIO25 - 8 bits.
O botão in-board BOOT é usado para ligar/desligar geração da senóide
Acrescentando pino I/O de DEBUG, onde deve aparecer uma onda quadrada oscilando na taxa de amostragem (10 KHz), mas cujo duty-cycle corresponde ao tempo de processamento usado para processar o seno.
Usa um LED externo para indicar se a senóide está sendo gerada ou não.
Acrescenta o contador TickCount cujo conteúdo é incromentado pela ISR e mostrado/zerado a cada segundo. Objetivo: moonitorar via comunicação serial, execução da ISR.
Arqutetura usada:
xxxxxxxxxxGPTimer ISR @ 10 kHz|+--> gera senoide no DAC|+--> incrementa TickCountbuttonTask|+--> lê botão + debouncestatusTask|+--> mostra TickCount a cada 1 s
A arquitetura preve po bloco principal: GPTimer + ISR a 10 kHz para gerar a senoide, e adicionalmente o contador TickCount dentro da ISR que permite confirmar o bom andamento da ISR via comunicação serial.
Comparando programação usando Arduino-IDE vs ESP-IDF Puro:
| IDE-Arduino | ESP-IDF Puro |
|---|---|
setup() | app_main() |
loop() | duas tasks FreeRTOS:buttonTask() para leitura/debounce do botão usado para ligar/desligar geração da senóidestatusTask() para mostrar o conteúdo de TickCount a cada segundo, via comunicação seral; |
digitalWrite() | gpio_set_level() |
digitalRead() | gpio_get_level() |
dacWrite() | dac_output_voltage(). |
Serial.print() | printf() |
delay() | vTaskDelay() |
millis() | Não usado. |
IRAM_ATTR e acionada pelo GPTimer a cada 100 µs.Há ainda um ponto importante: no código Arduino, TickCount é zerado pela loop() enquanto a ISR pode incrementá-lo simultaneamente. No ESP-IDF continua com esse comportamento funcional, mas faz a leitura/zeragem de forma adequada para uma variável compartilhada com a ISR.
xxxxxxxxxxapp_main()│├── calcula angle_increment│├── setupDAC()│├── setupButton()│├── cria buttonTask()│└── setupTimer()│├── cria GPTimer│├── registra onTimer()│├── configura alarme = 100 us│├── auto_reload = true│├── enable│└── start│▼┌─────────────┐│ GPTimer │└──────┬──────┘│a cada 100 µs│▼onTimer()│┌──────┴──────┐│ │generateSine? ││ │SIM │ │ NÃO▼ ▼calcula seno DAC = 128│▼DAC = 0..255
Segue cópia de:
~/Documents/ESP32/Projetos/Senoide2/main/main.c:
x
/******** Gera Senóide - versão: angle Accumulator* Para ESP-IDF "Puro"** Geração direta de uma senoide usando acumulador de fase.** Frequência da senoide: 440 Hz* Frequência de amostragem: 10 kHz* DAC1: GPIO25 - 8 bits** A fase é incrementada a cada interrupção do timer:** angle = angle + angle_increment** e o valor instantâneo da senoide é calculado por:** sin(angle)** Versão para ESP-IDF 6.1*******/
/* ============================================================* Constantes globais / definições* ============================================================ */
// DAC1 do ESP32 clássico// Botão BOOT da DevKitC_v4// GPIO para medir duty-cycle da ISR// LED externo / indicador
// Frequência de amostragem// Frequência desejada da senoide// Intervalo entre amostras: // 1 / 10000 = 100 us
// periodo de deboucing para botão
/* ============================================================* Variáveis globais* ============================================================ */
// Variáveis utilizadas pela ISR.// angle é alterado somente pela ISR.// angle_increment é calculado antes da inicialização do timer.static volatile float angle = 0.0f; // Angulo atual da senoidestatic float angle_increment = 0.0f; // Será calculado em app_main().
// generateSine é controlado pela buttonTask e lido pela ISR.// volatile garante que o compilador não faça otimizações// inadequadas sobre o acesso à variável.static volatile bool generateSine = false; // Indica se a senoide deve ser gerada.
static dac_oneshot_handle_t dac_handler; // Handler do DAC.static gptimer_handle_t timer_handler; // Handler do GPTimer.
// Contador incrementado pela ISR.// A ISR é executada 10.000 vezes por segundo.static volatile uint32_t TickCount = 0;
/* ============================================================* Interrupção do GPTimer* Esta função é executada a cada 100 us = 10 kHz.* ============================================================ */
static bool IRAM_ATTR onTimer( gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx) { (void)timer; (void)edata; (void)user_ctx;
// marca início da execução da ISR gpio_set_level(DEBUG_PIN, 1); // Levanta nível lógico pino de DEBUG // gpio_set_level() é utilizado em lugar de digitalWrite(). // incrementa TickCount // Como a ISR ocorre 10.000 vezes/s, esperamos // aproximadamente 10.000 incrementos por segundo. TickCount++;
if (generateSine) { // Calcula o valor instantâneo da senoide. // sinf() produz: -1 <= seno <= +1 float sine_value = sinf(angle);
// Converte de [-1, +1] // para: [0, 255] <-- deve ser enviado para DAC // O fator 255/2=127.5 produz amplitude de pico uint8_t dac_value = (uint8_t)((sine_value + 1.0f) * 127.5f);
// Envia o valor calculado para o DAC. // dac_oneshot_output_voltage(dac_handler,dac_value); dac_output_voltage(DAC_CHANNEL_1, dac_value);
// Avança a fase para a próxima amostra. angle += angle_increment;
// Mantém a fase dentro do intervalo [0, 2*pi). if (angle >= 2.0f * M_PI) { angle -= 2.0f * M_PI; } } else { // Quando a senoide está desligada, // mantém o DAC no ponto médio. // dac_oneshot_output_voltage(dac_handler, 128); dac_output_voltage(DAC_CHANNEL_1, 128); }
// marca final da ISR gpio_set_level(DEBUG_PIN, 0); // Baixa nível lógico do pino de DEBUG // Não solicita troca de contexto. return false; }
/* ============================================================* Configuração do DAC* ============================================================ */static void setupDAC(void){ /* dac_oneshot_config_t dac_config = { // DAC_CHAN_0 corresponde ao DAC1, // que está ligado ao GPIO25 no ESP32 clássico. .chan_id = DAC_CHAN_0 };
ESP_ERROR_CHECK( dac_oneshot_new_channel( &dac_config, &dac_handler ) );
// Inicialmente coloca o DAC no ponto médio aproximado ESP_ERROR_CHECK( dac_oneshot_output_voltage( dac_handler, 128 ) ); */ // DAC1 = GPIO 25 ESP_ERROR_CHECK( dac_output_enable(DAC_CHANNEL_1) );
// Inicialmente coloca a saída no ponto médio. ESP_ERROR_CHECK( dac_output_voltage(DAC_CHANNEL_1, 128) );
printf("> DAC1 configurado para ponto central (128)\n"); }
/* ============================================================* Configuração dos pinos usados de GPIO* Pino DEBUG: Verificar tempo de execuçao da tarefa periodica onTimer()* ============================================================ */static void setupGPIO(void){ // DEBUG_PIN gpio_config_t debug_config = { .pin_bit_mask = (1ULL << DEBUG_PIN), .mode = GPIO_MODE_OUTPUT, .pull_up_en = GPIO_PULLUP_DISABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, .intr_type = GPIO_INTR_DISABLE };
ESP_ERROR_CHECK( gpio_config(&debug_config) );
gpio_set_level(DEBUG_PIN, 0);
// LED_PIN gpio_config_t led_config = { .pin_bit_mask = (1ULL << LED_PIN), .mode = GPIO_MODE_OUTPUT, .pull_up_en = GPIO_PULLUP_DISABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, .intr_type = GPIO_INTR_DISABLE };
ESP_ERROR_CHECK( gpio_config(&led_config) );
gpio_set_level(LED_PIN, 0);
// BUTTON_PIN // Botão BOOT é ativo em LOW. // não pressionado = HIGH // pressionado = LOW gpio_config_t button_config = { .pin_bit_mask = (1ULL << BUTTON_PIN), .mode = GPIO_MODE_INPUT, .pull_up_en = GPIO_PULLUP_ENABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, .intr_type = GPIO_INTR_DISABLE };
ESP_ERROR_CHECK( gpio_config(&button_config) );}
/* ============================================================ * UpdateStatus() * * Mostra no LED e no terminal serial o estado da senoide. * ============================================================ */
static void UpdateStatus(void){ gpio_set_level(LED_PIN, generateSine ? 1 : 0); printf("Senoide: %s\n", generateSine ? "ON" : "Off");}
/* ============================================================* Tarefa do botão* Faz leitura e deboucing do botão* ============================================================ */
static void buttonTask(void *arg){ (void)arg; int estadoBotaoAnterior = 1; // HIGH int estadoBotaoAtual = 1; // HIGH
TickType_t ultimoTempoDebounce = 0;
while (1){ // Lê o botão. // gpio_get_level(): // 0 = LOW = pressionado // 1 = HIGH = solto int leitura = gpio_get_level(BUTTON_PIN);
TickType_t agora = xTaskGetTickCount();
// Verifica se houve mudança. if (leitura != estadoBotaoAnterior) { ultimoTempoDebounce = agora; }
// Verifica se a leitura permaneceu estável durante // o período de debounce. if ((agora - ultimoTempoDebounce) >= pdMS_TO_TICKS(DEBOUNCE_MS)) { // O estado efetivamente mudou. if (leitura != estadoBotaoAtual) { estadoBotaoAtual = leitura; // Só reage à transição para LOW. // Ou seja, somente quando o botão é pressionado. if (estadoBotaoAtual == 0) { generateSine = !generateSine; UpdateStatus(); } } } estadoBotaoAnterior = leitura;
// Não precisamos executar esta task continuamente. // 5 ms fornece uma frequência de amostragem de 200 Hz, // mais que suficiente para o debounce do botão. vTaskDelay(pdMS_TO_TICKS(5)); }}
/* ============================================================* Configuração do GPTimer* ============================================================ */
static void setupTimer(void){ // Resolução do timer: 1 MHz // 1 tick = 1 us gptimer_config_t timer_config = { .clk_src = GPTIMER_CLK_SRC_DEFAULT, .direction = GPTIMER_COUNT_UP, .resolution_hz = 1 * 1000 * 1000 };
// Create a timer instance ESP_ERROR_CHECK( gptimer_new_timer(&timer_config, &timer_handler) );
// Registra a callback da ISR // Call the user callback function when the timer overflow event occurs gptimer_event_callbacks_t callbacks = { .on_alarm = onTimer };
ESP_ERROR_CHECK( gptimer_register_event_callbacks( timer_handler, &callbacks, NULL) );
// Configura o timer overflow para 100 us. // 100 us = 10 kHz. /* gptimer_alarm_config_t alarm_config = { .reload_count = 0, .alarm_count = SAMPLE_PERIOD_US, .flags.auto_reload_on_alarm = true }; // Versão abaixo evita eventuais problemas com designação de: // .flags.auto_reload_on_alarm */ gptimer_alarm_config_t alarm_config = { .alarm_count = SAMPLE_PERIOD_US, .reload_count = 0 }; alarm_config.flags.auto_reload_on_alarm = true;
ESP_ERROR_CHECK( gptimer_set_alarm_action(timer_handler, &alarm_config) );
// Habilita o timer. ESP_ERROR_CHECK( gptimer_enable(timer_handler) ); // Inicia o timer. ESP_ERROR_CHECK( gptimer_start(timer_handler) );}
/* ============================================================ * Task de status * * Mostra TickCount aproximadamente a cada segundo. * ============================================================ */
static void statusTask(void *arg){ (void)arg; while (1){ // Aguarda aproximadamente 1 segundo. vTaskDelay(pdMS_TO_TICKS(1000)); // Copia o contador e zera. // Como TickCount é modificado pela ISR, fazemos // uma pequena seção crítica para evitar que a ISR // altere o valor entre a leitura e o zero. // // IMPORTANTE: // Esta parte é apenas para o contador. // A geração da senoide continua totalmente na ISR. uint32_t ticks;
portENTER_CRITICAL(NULL); ticks = TickCount; TickCount = 0; portEXIT_CRITICAL(NULL);
printf("TickCount = %lu\n", (unsigned long)ticks); }}
/* ============================================================* Programa principal* ============================================================ */
void app_main(void){ // Calcula o incremento de fase. // Δφ = 2π f / fs // Para 440 Hz e 10 kHz: // Δφ ≈ 0.27646 rad angle_increment = 2.0f * M_PI * FREQUENCY_HZ / SAMPLE_RATE_HZ; angle = 0.0f; // Começa com fase zero.
printf("\n"); printf("..... Gerador Senoidal .....\n"); printf("> angle_increment = %f\n", angle_increment); setupGPIO(); // Configira pinos da GPIO setupDAC(); // Configura o DAC.
// Estado inicial generateSine = false; UpdateStatus(); // Dá algum tempo para as mensagens iniciais aparecerem // antes de iniciar a interrupção de 10 kHz. vTaskDelay(pdMS_TO_TICKS(100));
// Cria as tasks: // buttonTask: // leitura/debounce do botão // statusTask: // monitoração do TickCount. xTaskCreate( buttonTask, "buttonTask", 2048, NULL, 5, NULL );
xTaskCreate( statusTask, "statusTask", 2048, NULL, 4, NULL ); // Finalmente inicia o GPTimer // A partir deste ponto a ISR onTimer() // será executada a cada 100 us. setupTimer();
// app_main() não precisa terminar. // As tasks e a ISR continuam executando. while (1) { vTaskDelay(pdMS_TO_TICKS(1000)); }}Se você quiser ver um exemplo de compilação do programa ver Exemplo de Compilação de Projeto.
Note que estamos chamando dac_oneshot_output_voltage() a cada 100
A documentação atual do DAC Digital To Analog Converter (DAC) diz explicitamente que dac_oneshot_output_voltage() pode ser chamado em ISR — isso é conveniente. Ela também informa que a chamada normalmente leva 7–11 µs no ESP32.
A chamada do DAC não é, por si só, o problema mas o processamento sinf() dentro de uma ISR de 10 kHz é preocupante.
Para uma senoide de 440 Hz, temos somente:
xxxxxxxxxx10.000 amostras/s↓440 ciclos/s≈ 22,7 amostras por ciclo
e isso já sugere uma solução muito melhor: usar uma tabela de seno (lookup table) ou outra técnica de geração de seno, em vez de calcular sinf() dentro da interrupção. Isso também deixaria a ISR muito mais curta e previsível.
Por isto foi configurado um pino na GPIO para DEBUG (pino livre 26). Se for conectado um osciloscópio ao mesmo, deve ser detectada uma onda quadrada de 10 KHz (mesma frequência usada para amostrar a senóide), mas o período de tempo em que este pino passa em nível lógico algo (seu “duty-cycle”) permite ter uma idéia real do tempo de processamento gasto para executar a tarefa onTime() (ou a ISR via chamada IRAM_ATTR). No osciloscópio deve ser observado algo como:
xxxxxxxxxx<--------- 100 µs --------->┌───────┐ ┌───────┐│ │ │ │────────┘ └─────────────────┘ └────<-->↑│tempo da ISR
O resultado deve ser algo como:
Deve ser aproximadamente:
Se, por exemplo, o osciloscópio mostrar:
xxxxxxxxxxHIGH = 12 µsPERÍODO = 100 µs
então:
Ou seja, aproximadamente 12% de cada período de amostragem está sendo consumido pela ISR.
Note: A função IRAM_ATTR, é apropriada para uma callback que roda em contexto de interrupção quando se pretende comportamento “IRAM-safe”. A documentação da Espressif também chama atenção para o fato de que as funções chamadas pela callback precisam ser adequadas a esse contexto (execução de rápidas tarefas periódicas).
Para saber mais sobre IRAM & IRAM Safe, clique aqui.
Você tmabém pode se aprofundar mais da documentaçao da Espressif sobre: Digital To Analog Converter (DAC) e sobre IRAM Safe.
A gpio_set_level() é uma função que pode ser usada nesse contexto; a documentação do driver GPIO prevê inclusive a possibilidade de colocá-la em IRAM para execução durante cache desabilitado com CONFIG_GPIO_CTRL_FUNC_IN_IRAM.
<< Voltar para tópico de origem: Gerar Senóide | Ver Versão usando Arduíno-IDE.
Fernando Passold, em 14/09/2026.