Generating 3 Square Wave Signals with Phase Shifts and Adjustable Frequency (5Hz to 1000Hz) using Arduino Zero (SAMD21)

The goal of this project is to generate three square wave signals with frequencies adjustable from 5 Hz to 1000 Hz, controlled by a potentiometer, on an Arduino Zero (SAMD21). The three signals will have phase shifts of 0°, 120°, and 240°, and their duty cycles will be modulated using a D flip-flop.

#include <Arduino.h>

// Pins for square wave outputs
#define WAVE_PIN1 9   // First wave
#define WAVE_PIN2 10  // Second wave
#define WAVE_PIN3 11  // Third wave
#define POT_PIN A0    // Potentiometer input

// Timer and frequency definitions
#define MIN_FREQ 5       // Minimum frequency in Hz
#define MAX_FREQ 1000    // Maximum frequency in Hz
#define TIMER_CLOCK 48000000UL // SAMD21 clock frequency

// Filtering settings
#define FILTER_DEPTH 10
int filterBuffer[FILTER_DEPTH];
int filterIndex = 0;
int filterSum = 0;

volatile uint32_t wavePhase[3] = {0, 0, 0}; // Phase values for each wave
volatile uint32_t periodTicks = 0;          // Timer period in ticks

void setup() {
  // Configure wave output pins
  pinMode(WAVE_PIN1, OUTPUT);
  pinMode(WAVE_PIN2, OUTPUT);
  pinMode(WAVE_PIN3, OUTPUT);

  // Configure analog input for potentiometer
  analogReadResolution(12); // 12-bit resolution
  analogReference(AR_DEFAULT);

  // Set up TC4/TC5 for 32-bit timer
  GCLK->GENDIV.reg = GCLK_GENDIV_ID(4) | GCLK_GENDIV_DIV(1); // Divide by 1
  GCLK->GENCTRL.reg = GCLK_GENCTRL_ID(4) | GCLK_GENCTRL_SRC_DFLL48M | GCLK_GENCTRL_GENEN;
  while (GCLK->STATUS.bit.SYNCBUSY) {}

  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_ID_TC4_TC5 | GCLK_CLKCTRL_GEN_GCLK4 | GCLK_CLKCTRL_CLKEN;
  while (GCLK->STATUS.bit.SYNCBUSY) {}

  TC4->COUNT32.CTRLA.reg = TC_CTRLA_MODE_COUNT32 | TC_CTRLA_PRESCALER_DIV1;
  while (TC4->COUNT32.STATUS.bit.SYNCBUSY) {}

  TC4->COUNT32.CTRLBSET.reg = TC_CTRLBSET_ONESHOT; // One-shot mode for manual triggering
  while (TC4->COUNT32.STATUS.bit.SYNCBUSY) {}

  TC4->COUNT32.INTENSET.reg = TC_INTENSET_MC0;  // Enable match interrupt
  NVIC_EnableIRQ(TC4_IRQn);

  TC4->COUNT32.CTRLA.reg |= TC_CTRLA_ENABLE; // Enable the timer
  while (TC4->COUNT32.STATUS.bit.SYNCBUSY) {}
}

void loop() {
  // Read and filter potentiometer value
  int potValue = analogRead(POT_PIN);
  filterSum -= filterBuffer[filterIndex];
  filterBuffer[filterIndex] = potValue;
  filterSum += potValue;
  filterIndex = (filterIndex + 1) % FILTER_DEPTH;

  int filteredValue = filterSum / FILTER_DEPTH;

  // Map potentiometer value to frequency range
  int frequency = map(filteredValue, 0, 4095, MIN_FREQ, MAX_FREQ);

  // Calculate the timer period in ticks
  noInterrupts();
  periodTicks = TIMER_CLOCK / frequency;
  interrupts();

  // Update phases based on frequency
  wavePhase[0] = 0;                     // First wave (0° phase shift)
  wavePhase[1] = periodTicks / 3;       // Second wave (120° phase shift)
  wavePhase[2] = 2 * periodTicks / 3;   // Third wave (240° phase shift)

  delay(10); // Small delay for stabilization
}

void TC4_Handler() {
  static uint32_t ticks = 0;

  // Clear interrupt flag
  TC4->COUNT32.INTFLAG.reg = TC_INTFLAG_MC0;

  // Check phases and toggle pins
  if (ticks == wavePhase[0]) {
    digitalWrite(WAVE_PIN1, !digitalRead(WAVE_PIN1));
  }
  if (ticks == wavePhase[1]) {
    digitalWrite(WAVE_PIN2, !digitalRead(WAVE_PIN2));
  }
  if (ticks == wavePhase[2]) {
    digitalWrite(WAVE_PIN3, !digitalRead(WAVE_PIN3));
  }

  // Increment ticks and reset if necessary
  ticks++;
  if (ticks >= periodTicks) {
    ticks = 0;
    TC4->COUNT32.COUNT.reg = 0; // Reset the counter
  }
}

I would like to know if the code I provided can actually run on a board, as I am currently unable to run it myself. Additionally, if anyone could help me with this project, it would be greatly appreciated.

I have a SAMD21 based Arduino, although I haven't used it for a while. I may be able to test it tomorrow.

But it doesn't sound that taxing of a challenge for any Arduino, just by using micros() and digitalWrite(). The signal edges would be 166us apart at 50% duty cycle.

I didn't understand this part.

I think I did that what you want a little while ago for a Attiny13. Now that has only one 8-bit timer and 1Kbyte of memory, but it worked quite well.
What helped me most is realizing that 3 phase shifted square waves can be seen as a 6-case statement driven by a timer interrupt.

Code is here

Could you let me know if the code works on your Arduino Zero tomorrow? I plan to use a D flip-flop to generate three signals with a 50% duty cycle. However, I’m struggling to achieve a precise 50% duty cycle. I suspect that using an Arduino with digitalWrite and micros() might introduce delays, which could prevent accurate phase shifts of 0°, 120°, and 240°.

...and now I remember why... frustrating experience, constantly having to put it into bootloader mode, ports appearing & disappearing... ugh!

setup() doesn't complete:

11:46:38.705 -> Starting setup()
11:46:38.705 -> Set up TC4/TC5 for 32-bit timer
11:46:38.705 -> while #1
11:46:38.705 -> while #2
11:46:38.705 -> while #3
11:46:38.705 -> while #4
11:46:38.705 -> Enable match interrupt

Updated code for above:

#include <Arduino.h>

// Pins for square wave outputs
#define WAVE_PIN1 9   // First wave
#define WAVE_PIN2 10  // Second wave
#define WAVE_PIN3 11  // Third wave
#define POT_PIN A0    // Potentiometer input

// Timer and frequency definitions
#define MIN_FREQ 5       // Minimum frequency in Hz
#define MAX_FREQ 1000    // Maximum frequency in Hz
#define TIMER_CLOCK 48000000UL // SAMD21 clock frequency

// Filtering settings
#define FILTER_DEPTH 10
int filterBuffer[FILTER_DEPTH];
int filterIndex = 0;
int filterSum = 0;

volatile uint32_t wavePhase[3] = {0, 0, 0}; // Phase values for each wave
volatile uint32_t periodTicks = 0;          // Timer period in ticks

void setup() {
  SerialUSB.begin(115200);
  while (!SerialUSB);
  SerialUSB.println("Starting setup()");
  
  // Configure wave output pins
  pinMode(WAVE_PIN1, OUTPUT);
  pinMode(WAVE_PIN2, OUTPUT);
  pinMode(WAVE_PIN3, OUTPUT);

  // Configure analog input for potentiometer
  analogReadResolution(12); // 12-bit resolution
  analogReference(AR_DEFAULT);

  SerialUSB.println("Set up TC4/TC5 for 32-bit timer");

  // Set up TC4/TC5 for 32-bit timer
  GCLK->GENDIV.reg = GCLK_GENDIV_ID(4) | GCLK_GENDIV_DIV(1); // Divide by 1
  GCLK->GENCTRL.reg = GCLK_GENCTRL_ID(4) | GCLK_GENCTRL_SRC_DFLL48M | GCLK_GENCTRL_GENEN;
  SerialUSB.println("while #1");
  while (GCLK->STATUS.bit.SYNCBUSY) {}

  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_ID_TC4_TC5 | GCLK_CLKCTRL_GEN_GCLK4 | GCLK_CLKCTRL_CLKEN;
  SerialUSB.println("while #2");
  while (GCLK->STATUS.bit.SYNCBUSY) {}

  TC4->COUNT32.CTRLA.reg = TC_CTRLA_MODE_COUNT32 | TC_CTRLA_PRESCALER_DIV1;
  SerialUSB.println("while #3");
  while (TC4->COUNT32.STATUS.bit.SYNCBUSY) {}

  TC4->COUNT32.CTRLBSET.reg = TC_CTRLBSET_ONESHOT; // One-shot mode for manual triggering
  SerialUSB.println("while #4");;
  while (TC4->COUNT32.STATUS.bit.SYNCBUSY) {}

  SerialUSB.println("Enable match interrupt");;
  TC4->COUNT32.INTENSET.reg = TC_INTENSET_MC0;  // Enable match interrupt
  NVIC_EnableIRQ(TC4_IRQn);

  TC4->COUNT32.CTRLA.reg |= TC_CTRLA_ENABLE; // Enable the timer
  SerialUSB.println("while #5");
  while (TC4->COUNT32.STATUS.bit.SYNCBUSY) {}
  
  SerialUSB.println("setup() complete");
 }

void loop() {
  // Read and filter potentiometer value
  int potValue = analogRead(POT_PIN);
  SerialUSB.println(potValue);
  filterSum -= filterBuffer[filterIndex];
  filterBuffer[filterIndex] = potValue;
  filterSum += potValue;
  filterIndex = (filterIndex + 1) % FILTER_DEPTH;

  int filteredValue = filterSum / FILTER_DEPTH;

  // Map potentiometer value to frequency range
  int frequency = map(filteredValue, 0, 4095, MIN_FREQ, MAX_FREQ);

  // Calculate the timer period in ticks
  noInterrupts();
  periodTicks = TIMER_CLOCK / frequency;
  interrupts();

  // Update phases based on frequency
  wavePhase[0] = 0;                     // First wave (0° phase shift)
  wavePhase[1] = periodTicks / 3;       // Second wave (120° phase shift)
  wavePhase[2] = 2 * periodTicks / 3;   // Third wave (240° phase shift)

  delay(10); // Small delay for stabilization
}

void TC4_Handler() {
  static uint32_t ticks = 0;

  // Clear interrupt flag
  TC4->COUNT32.INTFLAG.reg = TC_INTFLAG_MC0;

  // Check phases and toggle pins
  if (ticks == wavePhase[0]) {
    digitalWrite(WAVE_PIN1, !digitalRead(WAVE_PIN1));
  }
  if (ticks == wavePhase[1]) {
    digitalWrite(WAVE_PIN2, !digitalRead(WAVE_PIN2));
  }
  if (ticks == wavePhase[2]) {
    digitalWrite(WAVE_PIN3, !digitalRead(WAVE_PIN3));
  }

  // Increment ticks and reset if necessary
  ticks++;
  if (ticks >= periodTicks) {
    ticks = 0;
    TC4->COUNT32.COUNT.reg = 0; // Reset the counter
  }
}

I still don't get this. The Arduino is generating the 3 signals. Where does a flip-flop come in?

I would like to clarify that the flip-flop is used purely to ensure a 50% duty cycle for the generated signals. The Arduino board generates the three square wave signals with the desired frequencies and phase shifts. These signals are then passed through hardware flip-flops to achieve a 50% duty cycle.

Currently, I cannot test the code on hardware, but I will be able to start testing it from next Monday.

Thank you for the updated code. I would like to know if the code correctly generates the three signals with the correct phase shift and if the frequency variation works properly. Could you let me know?

I think it's pretty clear that it does not. As I said before, the code is getting stuck in setup(). loop() never executes.

But isn't the flip-flop simply halving the frequency? That could easily be done in the code, removing the need for the flip-flop(s), unless I'm missing something. A schematic of the proposed circuit would help.

look this over.
may be good enough for 1kHz

const byte PinOut [] = { 12, 11, 10 };
const int  Nout      = 3;

enum { Off = HIGH, On = LOW };

unsigned long usecPeriod = 5000000L / 6;
         long usec0;

byte seq;

char s [90];

// -----------------------------------------------------------------------------
void
loop (void)
{
    unsigned long usec = micros ();

    if (usec - usec0 >= usecPeriod)  {
        usec0 += usecPeriod;

        byte idx = seq / 2;
        if (! (seq % 2))                            // 0, 2, 4
            digitalWrite (PinOut [idx], On);
        else  {                                     // 1, 3, 5  
            int i = (idx+Nout-1) % Nout;
            digitalWrite (PinOut [i], Off);
        }

        if ((2 * Nout) <= ++seq)
            seq = 0;
    }

    if (Serial.available ())  {
        char buf [90];
        int  n = Serial.readBytesUntil ('\n', buf, sizeof(buf)-1);
        buf [n] = '\0';
        usecPeriod = atol (buf);
        Serial.println (usecPeriod);
    }
}

void
setup (void)
{
    Serial.begin (9600);

    for (int n = 0; n < Nout; n++)  {
        pinMode (PinOut [n], OUTPUT);
        digitalWrite (PinOut [n], Off);
    }
    digitalWrite (PinOut [Nout-1], On);
}
1 Like

are the three signals the same frequency and synchronized?

implemented a similar project on an ESP32-S3 using RMT interface - see esp32s3-rmt-no-free-tx-channels-opening-third-channel

also using timer interrupts - see 2560-three-square-pulses-120-degree-phase-shift-varying-frequency

The project is to simulate a motor with three phases, each phase being 120° and 240° out of phase. The frequency should be adjustable using a potentiometer while maintaining the phase shift of each PWM signal. The frequencies range from 5Hz to a maximum of 1kHz, which corresponds to the motor's rotation in the simulation. The D flip-flop is not necessarily required for this project if my code works correctly with a 50% phase shift.
The three signals must have the same frequency and be synchronized according to the requested phase shift.

Could you try this code, but I’m afraid that using digitalWrite might cause delays in the phase shift. Thanks in advance for everything and everyone.

#include "Arduino.h"

const int pinPhase1 = 5;  // Pin for phase 1
const int pinPhase2 = 6;  // Pin for phase 2
const int pinPhase3 = 7;  // Pin for phase 3
const int pinFreqControl = A0;  // Pin to control the frequency

volatile uint32_t period = 1000;  // Period in microseconds (initially 1 ms for 1 kHz)
volatile uint32_t phaseShift120 = 333;  // Phase shift for 120° (in microseconds)
volatile uint32_t phaseShift240 = 666;  // Phase shift for 240° (in microseconds)

void setup() {
  // Initialize the pins as output
  pinMode(pinPhase1, OUTPUT);
  pinMode(pinPhase2, OUTPUT);
  pinMode(pinPhase3, OUTPUT);

  // Set initial states to LOW
  digitalWrite(pinPhase1, LOW);
  digitalWrite(pinPhase2, LOW);
  digitalWrite(pinPhase3, LOW);

  // Configure the TC3 timer to manage the signal generation
  configureTimer();
}

void loop() {
  // Read the potentiometer value from A0 to adjust the frequency
  int potValue = analogRead(pinFreqControl);
  float freq = map(potValue, 0, 1023, 5, 1000);  // Map the value to a frequency range (5 Hz to 1 kHz)

  // Calculate the new period based on the frequency
  noInterrupts();  // Disable interrupts while updating the values
  period = 1000000 / freq;  // Period in microseconds
  phaseShift120 = period / 3;  // Calculate 120° phase shift
  phaseShift240 = (2 * period) / 3;  // Calculate 240° phase shift
  interrupts();  // Re-enable interrupts
}

// Function to configure the TC3 timer
void configureTimer() {
  // Enable TC3 clock
  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 | GCLK_CLKCTRL_ID_TCC2_TC3;
  while (GCLK->STATUS.bit.SYNCBUSY);

  // Disable the counter while configuring
  TC3->COUNT16.CTRLA.reg &= ~TC_CTRLA_ENABLE;
  while (TC3->COUNT16.STATUS.bit.SYNCBUSY);

  // Configure the 16-bit mode and the prescaler
  TC3->COUNT16.CTRLA.reg = TC_CTRLA_MODE_COUNT16 | TC_CTRLA_PRESCALER_DIV16;
  while (TC3->COUNT16.STATUS.bit.SYNCBUSY);

  // Set the period for 1 kHz (initial value, will be updated in real-time)
  TC3->COUNT16.CC[0].reg = (48000000 / 16) / 1000;  // F_CPU / prescaler / frequency
  while (TC3->COUNT16.STATUS.bit.SYNCBUSY);

  // Enable the overflow interrupt
  TC3->COUNT16.INTENSET.reg = TC_INTENSET_OVF;
  NVIC_EnableIRQ(TC3_IRQn);

  // Enable the timer
  TC3->COUNT16.CTRLA.reg |= TC_CTRLA_ENABLE;
  while (TC3->COUNT16.STATUS.bit.SYNCBUSY);
}

// Interrupt handler for TC3
void TC3_Handler() {
  static uint32_t t = 0;

  // Clear the interrupt flag
  TC3->COUNT16.INTFLAG.reg = TC_INTFLAG_OVF;

  // Generate the three-phase signals
  if (t == 0) {
    digitalWrite(pinPhase1, !digitalRead(pinPhase1));  // Toggle phase 1
  } else if (t == phaseShift120) {
    digitalWrite(pinPhase2, !digitalRead(pinPhase2));  // Toggle phase 2 at 120° shift
  } else if (t == phaseShift240) {
    digitalWrite(pinPhase3, !digitalRead(pinPhase3));  // Toggle phase 3 at 240° shift
  }

  // Increment or reset the counter
  t += period / 2;
  if (t >= period) {
    t = 0;
  }

  // Adjust the timer period for the next interrupt
  TC3->COUNT16.CC[0].reg = (48000000 / 16) / (1000000 / period);
  while (TC3->COUNT16.STATUS.bit.SYNCBUSY);
}

Or maybe these can work with a hardware timer.

#include "Arduino.h"

// Pin assignments for three phases
const int pinPhase1 = 5;
const int pinPhase2 = 6;
const int pinPhase3 = 7;

// Pin for controlling frequency via analog input
const int pinFreqControl = A0;

// Variables for controlling the signal frequency and phase shift
volatile uint32_t period = 1000;  // Period in microseconds (initially set to 1 ms for 1 kHz)
volatile uint32_t phaseShift120 = 333;  // Phase shift for 120° (in microseconds)
volatile uint32_t phaseShift240 = 666;  // Phase shift for 240° (in microseconds)

void setup() {
  // Initialize pins as output
  pinMode(pinPhase1, OUTPUT);
  pinMode(pinPhase2, OUTPUT);
  pinMode(pinPhase3, OUTPUT);

  // Set initial states to LOW
  digitalWrite(pinPhase1, LOW);
  digitalWrite(pinPhase2, LOW);
  digitalWrite(pinPhase3, LOW);

  // Configure the timer for signal generation
  configureTimer();
}

void loop() {
  // Read potentiometer value from A0 to adjust frequency
  int potValue = analogRead(pinFreqControl);
  float freq = map(potValue, 0, 1023, 5, 1000);  // Map value to frequency range (5 Hz to 1 kHz)

  // Update period based on frequency
  noInterrupts();  // Disable interrupts while updating values
  period = 1000000 / freq;  // Period in microseconds
  phaseShift120 = period / 3;  // Calculate 120° phase shift
  phaseShift240 = (2 * period) / 3;  // Calculate 240° phase shift
  interrupts();  // Re-enable interrupts
}

// Function to configure the TC3 timer
void configureTimer() {
  // Enable TC3 clock
  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 | GCLK_CLKCTRL_ID_TCC2_TC3;
  while (GCLK->STATUS.bit.SYNCBUSY);

  // Disable the counter during configuration
  TC3->COUNT16.CTRLA.reg &= ~TC_CTRLA_ENABLE;
  while (TC3->COUNT16.STATUS.bit.SYNCBUSY);

  // Configure 16-bit mode and prescaler
  TC3->COUNT16.CTRLA.reg = TC_CTRLA_MODE_COUNT16 | TC_CTRLA_PRESCALER_DIV16;
  while (TC3->COUNT16.STATUS.bit.SYNCBUSY);

  // Set initial period for 1 kHz frequency (adjusted later in loop)
  TC3->COUNT16.CC[0].reg = (48000000 / 16) / 1000;  // F_CPU / prescaler / frequency
  while (TC3->COUNT16.STATUS.bit.SYNCBUSY);

  // Enable overflow interrupt for TC3
  TC3->COUNT16.INTENSET.reg = TC_INTENSET_OVF;
  NVIC_EnableIRQ(TC3_IRQn);

  // Enable the timer
  TC3->COUNT16.CTRLA.reg |= TC_CTRLA_ENABLE;
  while (TC3->COUNT16.STATUS.bit.SYNCBUSY);
}

// Interrupt handler for TC3
void TC3_Handler() {
  static uint32_t t = 0;

  // Clear the overflow interrupt flag
  TC3->COUNT16.INTFLAG.reg = TC_INTFLAG_OVF;

  // Directly toggle the phases based on timing
  if (t == 0) {
    // Set phase 1 HIGH (direct manipulation of output)
    PORT->Group[g_APinDescription[pinPhase1].ulPort].OUTSET.reg = (1 << g_APinDescription[pinPhase1].ulPin);
  } else if (t == phaseShift120) {
    // Set phase 2 HIGH (direct manipulation of output)
    PORT->Group[g_APinDescription[pinPhase2].ulPort].OUTSET.reg = (1 << g_APinDescription[pinPhase2].ulPin);
  } else if (t == phaseShift240) {
    // Set phase 3 HIGH (direct manipulation of output)
    PORT->Group[g_APinDescription[pinPhase3].ulPort].OUTSET.reg = (1 << g_APinDescription[pinPhase3].ulPin);
  }

  // Increment or reset the counter
  t += period / 2;
  if (t >= period) {
    t = 0;
  }

  // Reset phases to LOW at the correct times
  if (t == (period / 2)) {
    // Set all phases LOW at the halfway point
    PORT->Group[g_APinDescription[pinPhase1].ulPort].OUTCLR.reg = (1 << g_APinDescription[pinPhase1].ulPin);
    PORT->Group[g_APinDescription[pinPhase2].ulPort].OUTCLR.reg = (1 << g_APinDescription[pinPhase2].ulPin);
    PORT->Group[g_APinDescription[pinPhase3].ulPort].OUTCLR.reg = (1 << g_APinDescription[pinPhase3].ulPin);
  }

  // Update the timer period based on the new frequency
  TC3->COUNT16.CC[0].reg = (48000000 / 16) / (1000000 / period);
  while (TC3->COUNT16.STATUS.bit.SYNCBUSY);
}

Ok I'll try it later.

Even your original code used digitalWrite().

This code does not get stuck in setup().

I was able to verify that it calculates a frequency between 5Hz and 1000Hz as I adjust the pot.

However, on pin 5, I see a waveform with a frequency of only around 10Hz to 12Hz, whatever position of the pot. On pins 6, 7, no waveform at all.

#include "Arduino.h"

const int pinPhase1 = 5;  // Pin for phase 1
const int pinPhase2 = 6;  // Pin for phase 2
const int pinPhase3 = 7;  // Pin for phase 3
const int pinFreqControl = A0;  // Pin to control the frequency

volatile uint32_t period = 1000;  // Period in microseconds (initially 1 ms for 1 kHz)
volatile uint32_t phaseShift120 = 333;  // Phase shift for 120° (in microseconds)
volatile uint32_t phaseShift240 = 666;  // Phase shift for 240° (in microseconds)

void setup() {
  SerialUSB.begin(115200);
  while (!SerialUSB);
  SerialUSB.println("Starting setup()");
  
  // Initialize the pins as output
  pinMode(pinPhase1, OUTPUT);
  pinMode(pinPhase2, OUTPUT);
  pinMode(pinPhase3, OUTPUT);

  // Set initial states to LOW
  digitalWrite(pinPhase1, LOW);
  digitalWrite(pinPhase2, LOW);
  digitalWrite(pinPhase3, LOW);

  // Configure the TC3 timer to manage the signal generation
  configureTimer();

  SerialUSB.println("setup() complete");
}

void loop() {
  // Read the potentiometer value from A0 to adjust the frequency
  int potValue = analogRead(pinFreqControl);
  float freq = map(potValue, 0, 1023, 5, 1000);  // Map the value to a frequency range (5 Hz to 1 kHz)
  //SerialUSB.println(freq);
  // Calculate the new period based on the frequency
  noInterrupts();  // Disable interrupts while updating the values
  period = 1000000 / freq;  // Period in microseconds
  phaseShift120 = period / 3;  // Calculate 120° phase shift
  phaseShift240 = (2 * period) / 3;  // Calculate 240° phase shift
  interrupts();  // Re-enable interrupts
}

// Function to configure the TC3 timer
void configureTimer() {
  // Enable TC3 clock
  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 | GCLK_CLKCTRL_ID_TCC2_TC3;
  while (GCLK->STATUS.bit.SYNCBUSY);

  // Disable the counter while configuring
  TC3->COUNT16.CTRLA.reg &= ~TC_CTRLA_ENABLE;
  while (TC3->COUNT16.STATUS.bit.SYNCBUSY);

  // Configure the 16-bit mode and the prescaler
  TC3->COUNT16.CTRLA.reg = TC_CTRLA_MODE_COUNT16 | TC_CTRLA_PRESCALER_DIV16;
  while (TC3->COUNT16.STATUS.bit.SYNCBUSY);

  // Set the period for 1 kHz (initial value, will be updated in real-time)
  TC3->COUNT16.CC[0].reg = (48000000 / 16) / 1000;  // F_CPU / prescaler / frequency
  while (TC3->COUNT16.STATUS.bit.SYNCBUSY);

  // Enable the overflow interrupt
  TC3->COUNT16.INTENSET.reg = TC_INTENSET_OVF;
  NVIC_EnableIRQ(TC3_IRQn);

  // Enable the timer
  TC3->COUNT16.CTRLA.reg |= TC_CTRLA_ENABLE;
  while (TC3->COUNT16.STATUS.bit.SYNCBUSY);
}

// Interrupt handler for TC3
void TC3_Handler() {
  static uint32_t t = 0;

  // Clear the interrupt flag
  TC3->COUNT16.INTFLAG.reg = TC_INTFLAG_OVF;

  // Generate the three-phase signals
  if (t == 0) {
    digitalWrite(pinPhase1, !digitalRead(pinPhase1));  // Toggle phase 1
  } else if (t == phaseShift120) {
    digitalWrite(pinPhase2, !digitalRead(pinPhase2));  // Toggle phase 2 at 120° shift
  } else if (t == phaseShift240) {
    digitalWrite(pinPhase3, !digitalRead(pinPhase3));  // Toggle phase 3 at 240° shift
  }

  // Increment or reset the counter
  t += period / 2;
  if (t >= period) {
    t = 0;
  }

  // Adjust the timer period for the next interrupt
  TC3->COUNT16.CC[0].reg = (48000000 / 16) / (1000000 / period);
  while (TC3->COUNT16.STATUS.bit.SYNCBUSY);
}

This code also completes setup() and calculates a frequency between 5 and 1000 from the pot. However, I see no waveform on any pins.

Fixed frequency but maybe this could help:

using four ESP32 timers using interrupts
one timer to generate the three signals of required period and three others to generate the pulses of duty cycle width

// ESP32 three phase square wave variable duty cycle using timers

#define phase1 16  // signal output pins
#define phase2 18
#define phase3 17

#define PERIOD 10000                      // period 1KHz is 10000
#define DUTY_CYCLE 50                     // percentage
#define PULSE_WIDTH PERIOD*DUTY_CYCLE/100 // resultant pulse width

hw_timer_t *timerPeriod = NULL;  // hardware timer
hw_timer_t *timerPhase1 = NULL;  // hardware timer
hw_timer_t *timerPhase2 = NULL;  // hardware timer
hw_timer_t *timerPhase3 = NULL;  // hardware timer

// interrupt service routine to generate HIGH levels
volatile int counter = 0;   // interrupt counter
void ARDUINO_ISR_ATTR onTimer() {
  static byte state = 0;    // determines which phase to generate
  switch (state) {
    case 0:
      digitalWrite(phase1, HIGH);         // set phase output HIGH
      timerWrite(timerPhase1, 0);         // clear timer
      timerAlarm(timerPhase1, PULSE_WIDTH, false, 0);  // generate one shot
      break;
    case 1:
      digitalWrite(phase2, HIGH);
      timerWrite(timerPhase2, 0);
      timerAlarm(timerPhase2, PULSE_WIDTH, false, 0);  // generate one shot
      break;                                        
    case 2:
      digitalWrite(phase3, HIGH);
      timerWrite(timerPhase3, 0);
      timerAlarm(timerPhase3, PULSE_WIDTH, false, 0);  // generate one shot
      break;                                  
  }
  if (++state >= 3) state = 0;  // reset state ?
  counter++;
}

// interrupt service routines to generate LOW levels
void ARDUINO_ISR_ATTR onTimerPhase1() {
  digitalWrite(phase1, LOW);          // set phase output LOW
}

void ARDUINO_ISR_ATTR onTimerPhase2() {
  digitalWrite(phase2, LOW);
}

void ARDUINO_ISR_ATTR onTimerPhase3() {
  digitalWrite(phase3, LOW);
}

void setup() {
  Serial.begin(115200);
  delay(2000);
  Serial.println("\n\nESP32 timer three phase square wave with duty Cycle");
  pinMode(phase1, OUTPUT);    // enable phase outputs
  pinMode(phase2, OUTPUT);
  pinMode(phase3, OUTPUT);
  digitalWrite(phase3, 1);
  // setup timer interrupts for 1KHz three phase
  if ((timerPeriod = timerBegin(10000000)) == NULL)  // Set timer frequency to 10Mhz
    Serial.println("ERROR! timer period initialisation failed");
  else Serial.println("Timer period initialization OK");
  if ((timerPhase1 = timerBegin(10000000)) == NULL)  // Set timer frequency to 10Mhz
    Serial.println("ERROR! timer Phase 1 initialisation failed");
  else Serial.println("Timer Phase 1 initialization OK");
  if ((timerPhase2 = timerBegin(10000000)) == NULL)  // Set timer frequency to 10Mhz
    Serial.println("ERROR! timer Phase 2 initialisation failed");
  else Serial.println("Timer Phase 2 initialization OK");
  if ((timerPhase3 = timerBegin(10000000)) == NULL)  // Set timer frequency to 10Mhz
    Serial.println("ERROR! timer Phase 3 initialisation failed");
  else Serial.println("Timer Phase 3 initialization OK");
  timerAttachInterrupt(timerPeriod, &onTimer);        // Attach timer ISR for period timing
  timerAttachInterrupt(timerPhase1, &onTimerPhase1);  // attch timers ISR for pulse timing
  timerAttachInterrupt(timerPhase2, &onTimerPhase2);  
  timerAttachInterrupt(timerPhase3, &onTimerPhase3); 
  // Set alarm to call onTimer function every second (value in 10 microseconds).
  // Repeat the alarm (third parameter) with unlimited count = 0 (fourth parameter).
  timerAlarm(timerPeriod, PERIOD / 3, true, 0);
}

// display interrupt coun ter every seconds
void loop() {
  static unsigned long timert = millis();
  if (millis() - timert >= 1000) {
    Serial.println(counter);
    counter = 0;
    timert = millis();
  }
}

50% duty cycle

20% duty cycle

80% duty cycle

the STM32 devices have multiple hardware timers so should be able to get similar results

EDIT: should be fairly easy to use same algorithm using micros() to generate similar results

I want to be able to do this with an Arduino Zero: generate three square waves with phase shifts of 0°, 120°, and 240°, like a real motor, using a 50% duty cycle and variable frequency. Currently, I can achieve this using the code with micros(). However, when I run it on my board, the signals are not synchronized, and the frequency cannot reach 1 kHz.

const int potPin = A0;  // Analog pin where the potentiometer is connected
float frequency = 1;    // Calculated frequency in Hz (initial minimum value)

const int pwmPin1 = 3;  // PWM pin 1
const int pwmPin2 = 5;  // PWM pin 2
const int pwmPin3 = 6;  // PWM pin 3
const int clearPin = 8;  // Reset pin
const int clearPin2 = 9; // Reset pin
const int clearPin3 = 10; // Reset pin

bool clearPinHandled = false;  // Flag to execute handleClearPin() only once

unsigned long period;       // Period in microseconds
unsigned long delayPhase;   // Phase shift in microseconds
unsigned long lastUpdate1;  // Update time for PWM 1
unsigned long lastUpdate2;  // Update time for PWM 2
unsigned long lastUpdate3;  // Update time for PWM 3

// Variables for filtering
const int smoothingFactor = 10;  // Smoothing factor (number of samples)
float smoothedValue = 0;         // Smoothed potentiometer value
const int tolerance = 5;         // Tolerance to ignore small variations

void setup() {
  pinMode(pwmPin1, OUTPUT);
  pinMode(pwmPin2, OUTPUT);
  pinMode(pwmPin3, OUTPUT);
  pinMode(clearPin, OUTPUT);
  pinMode(clearPin2, OUTPUT);
  pinMode(clearPin3, OUTPUT);

  // Initialize serial communication for debugging
  Serial.begin(9600);
}

void loop() {
  // Read the raw value from the potentiometer (0 to 1023)
  int potValue = analogRead(potPin);

  // Apply a moving average filter
  smoothedValue += (potValue - smoothedValue) / smoothingFactor;

  // Map the smoothed value to obtain a frequency between 5 Hz and 1000 Hz
  int mappedFrequency = map(smoothedValue, 0, 1023, 5, 1000);

  // Update the frequency if the variation exceeds the tolerance
  if (abs(mappedFrequency - frequency) > tolerance) {
    frequency = mappedFrequency;
  }

  // Calculate the period and phase shift
  period = (1.0 / frequency) * 1000000;  // Period in microseconds
  delayPhase = period / 3;               // 120° phase shift in microseconds

  static unsigned long lastPrintTime = 0;
  if (millis() - lastPrintTime > 1000) {  // Display every 1 second
    lastPrintTime = millis();
    Serial.print("Frequency: ");
    Serial.print(frequency);
    Serial.print(" Hz | ");
    Serial.print("Period: ");
    Serial.print(period);
    Serial.print(" us | Phase shift: ");
    Serial.print(delayPhase);
    Serial.println(" us");
  }

  // Get the current time
  unsigned long currentTime = micros();

  // PWM 1: Based on the period
  if (currentTime - lastUpdate1 >= period) {
    lastUpdate1 += period; // Prepare for the next pulse
    digitalWrite(pwmPin1, HIGH);
    delayMicroseconds(period / 2);  // Pulse width 50%
    digitalWrite(pwmPin1, LOW);
  }

  // PWM 2: Shifted by 120° (1/3 of the period)
  if (currentTime - lastUpdate2 >= period) {
    lastUpdate2 += period; // Prepare for the next pulse
    unsigned long phaseDelay = delayPhase;
    digitalWrite(pwmPin2, HIGH);
    delayMicroseconds(period / 2);  // Pulse width 50%
    digitalWrite(pwmPin2, LOW);
    digitalWrite(clearPin2, HIGH);
  }

  // PWM 3: Shifted by 240° (2/3 of the period)
  if (currentTime - lastUpdate3 >= period) {
    lastUpdate3 += period; // Prepare for the next pulse
    unsigned long phaseDelay = delayPhase * 2;
    digitalWrite(pwmPin3, HIGH);
    delayMicroseconds(period / 2);  // Pulse width 50%
    digitalWrite(pwmPin3, LOW);
  }
}

If someone could help me write code to achieve this using hardware timers, I would greatly appreciate it.