The ESP32 microcontroller has a hardware PCM module driven by an 80MHz clock. This was originally intended for providing PWM signals to control LED brightness and as a result is called LEDC, but it is just a rectangular wave generator and could be used to generate a square wave 77.5kHz signal with an accuracy of about 0.001kHz.
The number of LEDC channels depends on the variant of the ESP32 but all of them have at least three channels and could generate the three frequencies you need. And since this is a hardware module controlled by the processor it is not affected by interrupts or any of the other things the CPU might get up to.
ESP32 dev boards are only a few dollars and are easy to program so this seems a simple solution. Since I found myself with some spare time I put together a quick program to generate the three frequencies you need:
//----------------------------------------------------------------------
// Use LEDC to generate:
// - 40kHz square wave on GPIO 5
// - 60kHz square wave on GPIO 6
// - 77.5kHz square wave on GPIO 7
// This is tested on the C3, C6, S3 and original ESP32
//----------------------------------------------------------------------
#include "driver/ledc.h"
#include "freertos/FreeRTOS.h"
#include "esp_log.h"
const char* TAG = "LEDC";
// Just to be annoying the original ESP32 works slightly differently
// from the later ones.
#if CONFIG_IDF_TARGET_ESP32
#define XTAL_CLK_FREQ 80000000
#define SPEED_MODE LEDC_HIGH_SPEED_MODE
#define CLK_CFG LEDC_USE_APB_CLK
#else
#define SPEED_MODE LEDC_LOW_SPEED_MODE
#define CLK_CFG LEDC_USE_XTAL_CLK
#endif
// Set the LEDC frequency
bool set_freq(uint32_t Frequency, ledc_timer_t Timer, ledc_channel_t Channel, int Pin) {
// The duty cycle resolution is the number of bits needed for the clock divider, but it cannot exceed 20 bits.
int clk_divider = (XTAL_CLK_FREQ/Frequency) >> 2;
int duty_res_bits = 1;
while (clk_divider > 0 && duty_res_bits < 20) {
clk_divider = clk_divider >> 1;
duty_res_bits++;
}
// Configure the timer
esp_err_t e;
ledc_timer_config_t t = {0};
t.speed_mode = SPEED_MODE;
t.duty_resolution = duty_res_bits;
t.timer_num = Timer;
t.freq_hz = Frequency;
t.clk_cfg = CLK_CFG;
if ((e = ledc_timer_config(&t)) != ESP_OK) {
ESP_LOGE(TAG, "ledc_timer_config returned %s", esp_err_to_name(e));
return false;
}
// Start the LEDC signal
ledc_channel_config_t c = {0};
c.gpio_num = Pin;
c.speed_mode = SPEED_MODE;
c.channel = Channel;
c.duty = 1 << (duty_res_bits-1);
c.timer_sel = Timer;
if ((e = ledc_channel_config(&c)) != ESP_OK) {
ESP_LOGE(TAG, "ledc_channel_config returned %s", esp_err_to_name(e));
return false;
}
// Return indicating success
return true;
}
void app_main(void) {
ESP_LOGI(TAG, "Configuring LEDC outputs");
set_freq(40000, LEDC_TIMER_0, LEDC_CHANNEL_0, GPIO_NUM_5);
set_freq(60000, LEDC_TIMER_1, LEDC_CHANNEL_1, GPIO_NUM_6);
set_freq(77500, LEDC_TIMER_2, LEDC_CHANNEL_2, GPIO_NUM_7);
// Suspend the task and let LEDC get on with it
ESP_LOGI(TAG, "Suspending main task");
vTaskSuspend(0);
}