Use two independent hardware timers configured in Output Compare or PWM mode so that:
Different microcontrollers use different names for the same concept:
Generic term: Timer Period Register or Maximum Count Value – the value at which the timer counter resets back to zero.
Operation:
Timer Frequency:
Timer_Freq = Timer_Clock / (Max_Count_Value + 1)
Output Frequency in Toggle Mode:
Output_Freq = Timer_Freq / 2
(Reason: Two toggles make one full waveform cycle)
Given:
Formula:
Max_Count_Value = (Timer_Clock / (2 × Desired_Freq)) − 1
For 307 kHz:
Max_Count_Value = (64,000,000 / (2 × 307,000)) − 1 ≈ 103
For 570 kHz:
Max_Count_Value = (64,000,000 / (2 × 570,000)) − 1 ≈ 55
Both timers run independently and continuously, generating precise square waves without affecting the main loop’s performance.
So, by considering the above points, we can implement the task.
Below are the solutions to the given task using different microcontrollers
We’re using an STM32 NUCLEO-F103RB board.
We have to generate two square waves with different frequencies(307 kHz and 570 kHz).
We will use two different hardware timers available to use in output compare toggle mode.
Circuit Diagram:
Project Setup in STM32CubeIDE
0
103
0
55
HAL_Init()
– Initializes the HAL library.SystemClock_Config()
– Configures the system clock.MX_USART2_UART_Init()
→ Sets up USART2.MX_GPIO_Init()
– Configures GPIO clocks.MX_TIM1_Init()
– Sets up TIM1.MX_TIM2_Init()
– Sets up TIM2.
TIM1 Initialization – Square Wave Generation
htim1.Instance = TIM1;
htim1.Init.Prescaler = 0; // No division, full 64 MHz input
htim1.Init.Period = 103; // Auto-reload for target frequency
sConfigOC.OCMode = TIM_OCMODE_TOGGLE; // Toggle output on match
sConfigOC.Pulse = 0; // Match at counter = 0
HAL_TIM_OC_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1);
Fout = fclk / (2×(PSC+1) × (ARR+1))
Fout ≈ 64 MHz / (2×104) ≈ 307.7 kHz
TIM2 Initialization – Square Wave Generation
htim2.Instance = TIM2;
htim2.Init.Prescaler = 0; // No division
htim2.Init.Period = 55; // Faster toggle than TIM1
sConfigOC.OCMode = TIM_OCMODE_TOGGLE; // Toggle output
sConfigOC.Pulse = 0;
HAL_TIM_OC_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
Fout ≈ 64 MHz / (2×56) ≈ 571.4 kHz
Main loop logic
int main(void) {
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART2_UART_Init();
MX_TIM1_Init();
MX_TIM2_Init();
// Start in Output Compare mode
HAL_TIM_OC_Start(&htim1, TIM_CHANNEL_1); //PA8(D7)
HAL_TIM_OC_Start(&htim2, TIM_CHANNEL_1); //PA0(A0)
while (1) {
}
}
The complete STM32CubeIDE project (including .ioc configuration, main.c, and HAL files) is available here:
📥 Download Project
The ESP32’s LEDC (LED Controller) hardware is used to generate signals. Normally, it’s for PWM, but with a 50% duty cycle, it produces a square wave.
Two square wave signals are created with an independent Timer for each:
#include "driver/ledc.h" // Include ESP32 LEDC (PWM) driver
// Hardware configuration for square wave signal 307 kHz
#define SQW1_GPIO 4 // GPIO pin for first square wave
#define SQW1_FREQ 307000 // Frequency for square wave 1 (~307 kHz)
#define SQW1_CHANNEL LEDC_CHANNEL_0 // Use LEDC channel 0
#define SQW1_TIMER LEDC_TIMER_0 // Use LEDC timer 0
#define SQW1_MODE LEDC_HIGH_SPEED_MODE// High-speed mode
#define SQW1_RES LEDC_TIMER_2_BIT // Resolution = 2 bits
// Hardware configuration for square wave signal 570 kHz
#define SQW2_GPIO 5 // GPIO pin for second square wave
#define SQW2_FREQ 570000 // Frequency for square wave 2 (~570 kHz)
#define SQW2_CHANNEL LEDC_CHANNEL_1 // Use LEDC channel 1
#define SQW2_TIMER LEDC_TIMER_1 // Use LEDC timer 1
#define SQW2_MODE LEDC_HIGH_SPEED_MODE// High-speed mode
#define SQW2_RES LEDC_TIMER_2_BIT // Resolution = 2 bits
void setup() {
// Configure Timer for Square Wave 1 (≈307 kHz)
ledc_timer_config_t timer1 = {
.speed_mode = SQW1_MODE,
.duty_resolution = SQW1_RES,
.timer_num = SQW1_TIMER,
.freq_hz = SQW1_FREQ,
.clk_cfg = LEDC_AUTO_CLK
};
ledc_timer_config(&timer1);
// Configure Channel for Square Wave 1
ledc_channel_config_t channel1 = {
.gpio_num = SQW1_GPIO,
.speed_mode = SQW1_MODE,
.channel = SQW1_CHANNEL,
.intr_type = LEDC_INTR_DISABLE,
.timer_sel = SQW1_TIMER,
.duty = 2, // 50% duty → square wave
.hpoint = 0
};
ledc_channel_config(&channel1);
// Configure Timer for Square Wave 2 (≈570 kHz)
ledc_timer_config_t timer2 = {
.speed_mode = SQW2_MODE,
.duty_resolution = SQW2_RES,
.timer_num = SQW2_TIMER,
.freq_hz = SQW2_FREQ,
.clk_cfg = LEDC_AUTO_CLK
};
ledc_timer_config(&timer2);
// Configure Channel for Square Wave 2
ledc_channel_config_t channel2 = {
.gpio_num = SQW2_GPIO,
.speed_mode = SQW2_MODE,
.channel = SQW2_CHANNEL,
.intr_type = LEDC_INTR_DISABLE,
.timer_sel = SQW2_TIMER,
.duty = 2, // 50% duty → square wave
.hpoint = 0
};
ledc_channel_config(&channel2);
// update frequency & duty for both channels
ledc_set_freq(SQW1_MODE, SQW1_TIMER, SQW1_FREQ);
ledc_set_duty(SQW1_MODE, SQW1_CHANNEL, 2);
ledc_update_duty(SQW1_MODE, SQW1_CHANNEL);
ledc_set_freq(SQW2_MODE, SQW2_TIMER, SQW2_FREQ);
ledc_set_duty(SQW2_MODE, SQW2_CHANNEL, 2);
ledc_update_duty(SQW2_MODE, SQW2_CHANNEL);
}
void loop() {
// Nothing required here — square waves run entirely in hardware
}
Let's understand the important function
ledc_timer_config(&timer_config)
freq_hz
)duty_resolution
)LEDC_HIGH_SPEED_MODE
)LEDC_TIMER_0
, LEDC_TIMER_1
, etc.)ledc_timer_config(&timer1);
→ Sets up Timer 0 to run at 307 kHz with 2-bit resolution.ledc_channel_config(&channel_config)
ledc_channel_config(&channel1);
→ Sends Timer 0’s square wave (307 kHz, 50%) out on GPIO 4.ledc_set_freq(speed_mode, timer, freq)
ledc_set_freq(SQW1_MODE, SQW1_TIMER, SQW1_FREQ);
ledc_set_duty(speed_mode, channel, duty)
ledc_set_duty(SQW1_MODE, SQW1_CHANNEL, 2);
ledc_update_duty(speed_mode, channel)
ledc_set_duty()
.ledc_update_duty(SQW1_MODE, SQW1_CHANNEL);
We are using the Arduino UNO development board and programming it using the Arduino IDE.
Using tone() function
We can use the tone() function for the generation of square waves. However, it has some limitations
So we will use Timer1 and Timer2 for the toggling of pins (i.e., generating the frequency).
void setup() {
cli(); // Disable interrupts during configuration
// Timer2 configuration for ~307 kHz on D11 (OC2A)
TCCR2A = (1 << WGM21) | (1 << COM2A0); // CTC mode, toggle OC2A on match
TCCR2B = (1 << CS20); // Prescaler = 1
OCR2A = 25; // Compare match value for ~307 kHz
// Timer1 configuration for ~570 kHz on D9 (OC1A)
TCCR1A = (1 << COM1A0); // Toggle OC1A on compare match
TCCR1B = (1 << WGM12) | (1 << CS10); // CTC mode, prescaler = 1
OCR1A = 13; // Compare match value for ~570 kHz
// Set pins as output
pinMode(9, OUTPUT);
pinMode(11, OUTPUT);
sei(); // Enable interrupts
}
void loop() {
while (true) {
//In this loop, the Microcontroller monitors and performs important tasks constantly.
}
}
WGM21
set): Timer2 resets when reached at OCR2A
value.OC2A
(COM2A0
set): The pin automatically toggles.CS20
set): Timer2 runs at full speed (16 MHz).OCR2A
= 25: Generates 307 kHz.WGM12
set): Timer1 resets when it reaches at the OCR1A
value.OC1A
(COM1A0
set): Pin toggles on compare match.CS10
set): Runs at full 16 MHz.OCR1A
= 13: Generates 570 kHz.cli()
disables interrupts to ensure configuration stability.sei()
enables interrupts after configuration is complete.loop()
function is needed as everything runs in hardware.Hardware setup
DSO output