Genertaing 1 kHz to 50 KHz PWM signal using Zephyr APIs and HAL Constructs

Being inspired by this post, I have taken up this task to generate a variable frequency (1 kHz - 50 kHz) PWM signal from TIM3_CH3 using Zephyr APIs and HAL Constructs on DPin-3 of Header of UNO Q.

Cooperation from forum members would be highly appreciated.

.... in progress

Sounds like fun.

Quick notes:

I issued a PR to add that type functionality to ArduinoCore-zephyr.
core: analog: add analogWriteFrequency by KurtE · Pull Request #493 · arduino/ArduinoCore-zephyr

Not holding breath that it will be incorporated, but one can hope.

So I added the support to my ArduinoCore-zephyr toolbox library
Arduino_zephyr_toolbox/examples/zephyr_analogWrite_change_frequency/zephyr_analogWrite_change_frequency.ino at main · KurtE/Arduino_zephyr_toolbox

I just checked out your links and found the sketches which I’ll definitely test to see the output. If Arduino incorporates your proposals, it will make generating high-frequency PWM signals on the UNO Q much easier by just calling functions with the necessary parameters, which isn't currently possible on the UNO R3.

For my university students, I would love to explore both Zephyr and HAL-based programming to cover fast PWM signal generation for academic purposes. HAL-based code will likely be more appealing to them, as they are already exposed (Midterm course: ATmega328P; Finalterm course: QBR22210 MPU + STM32U585 MCU) to direct register manipulation on the ATmega328P for generating fast PWM signals.

HAL Constructs based Sketch: (developed with the assistance of AI)
It generates a 1 kHz to 50 kHz PWM signal with a 50% duty cycle on DPin-3 (Fig-1) of the UNO Q header. The sketch uses TIM3_CH3 of the STM32U585 MCU.


Figure-1:

#include <Arduino.h>
#include <stm32u5xx.h>

#define FREQ_MIN_HZ     1000U
#define FREQ_MAX_HZ     50000U
#define ADC_MAX_14BIT   16383U

/* System Clock frequency (assuming default 160 MHz SYSCLK for STM32U5) */
#define TIM_CLOCK_FREQ  160000000U 

uint32_t last_freq = 0;

void init_TIM3_CH3_PWM_Registers(void) {
    /* 1. Enable Clock for GPIOB (or GPIOA) and TIM3 */
    RCC->AHB2ENR1 |= RCC_AHB2ENR1_GPIOBEN;
    RCC->APB1ENR1 |= RCC_APB1ENR1_TIM3EN;

    /* 2. Configure PB0 as Alternate Function AF2 (TIM3_CH3 -> DPin-3) */
    GPIOB->MODER   &= ~(GPIO_MODER_MODE0_Msk);
    GPIOB->MODER   |= (2U << GPIO_MODER_MODE0_Pos);  /* Alternate Function Mode */
    GPIOB->AFR[0]  &= ~(GPIO_AFRL_AFSEL0_Msk);
    GPIOB->AFR[0]  |= (2U << GPIO_AFRL_AFSEL0_Pos);  /* AF2 = TIM3_CH3 */

    /* 3. Disable Timer during configuration */
    TIM3->CR1 &= ~TIM_CR1_CEN;
    TIM3->PSC  = 0;                                  /* Prescaler = 0 */
    TIM3->ARR  = (TIM_CLOCK_FREQ / 1000U) - 1;       /* Default 1 kHz period */
    TIM3->CCR3 = TIM3->ARR / 2;                      /* 50% Duty Cycle for Channel 3 */

    /* 4. Configure TIM3 Channel 3 in PWM Mode 1 with Preload Enabled */
    TIM3->CCMR2 &= ~(TIM_CCMR2_OC3M_Msk | TIM_CCMR2_CC3S_Msk);
    TIM3->CCMR2 |= (6U << TIM_CCMR2_OC3M_Pos);       /* PWM Mode 1 on Channel 3 */
    TIM3->CCMR2 |= TIM_CCMR2_OC3PE;                  /* Preload Enable for Channel 3 */

    /* 5. Enable Channel 3 Output and Auto-Reload Preload */
    TIM3->CCER  |= TIM_CCER_CC3E;                    /* Enable Channel 3 output */
    TIM3->CR1   |= TIM_CR1_ARPE;                     /* Enable Auto-Reload Preload */
    TIM3->EGR   |= TIM_EGR_UG;                       /* Force update event */

    /* 6. Start Timer */
    TIM3->CR1   |= TIM_CR1_CEN;
}

void update_PWM_Frequency_Registers(uint32_t freq_hz) {
    if (freq_hz < FREQ_MIN_HZ) freq_hz = FREQ_MIN_HZ;
    if (freq_hz > FREQ_MAX_HZ) freq_hz = FREQ_MAX_HZ;

    /* Calculate Auto-Reload Value for Target Frequency */
    uint32_t arr_val = (TIM_CLOCK_FREQ / freq_hz) - 1;
    
    /* Update ARR for frequency and CCR3 for 50% duty cycle */
    TIM3->ARR  = arr_val;
    TIM3->CCR3 = arr_val / 2;
}

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

    /* Setup A0 pin with 14-bit ADC resolution */
  //  pinMode(A0, INPUT); never do like it; the channel does not work
    analogReadResolution(14);   //supports: 14, 12, 10, 8 bit resolution

    /* Initialize free-running PWM on TIM3 Channel 3 */
    init_TIM3_CH3_PWM_Registers();

    Serial.println("TIM3 CH3 Free-Running PWM initialized at DPin-3.");
}

void loop() {
    /* Read 14-bit ADC value (0 to 16,383) */
    uint32_t adc_val = analogRead(A0);

    /* Map ADC reading linearly from 1 kHz to 50 kHz */
    uint32_t target_freq = FREQ_MIN_HZ + 
        ((uint64_t)adc_val * (FREQ_MAX_HZ - FREQ_MIN_HZ)) / ADC_MAX_14BIT;

    if (target_freq != last_freq) {
        update_PWM_Frequency_Registers(target_freq);
        last_freq = target_freq;

        Serial.print("ADC: ");
        Serial.print(adc_val);
        Serial.print(" | Freq: ");
        Serial.print(target_freq);
        Serial.print(" Hz | ARR: ");
        Serial.println(TIM3->ARR);
    }

    delay(3000);
}

Output:

ADC: 8362 | Freq: 26009 Hz | ARR: 6150  //A0-pin is at: 3.3/2 volt and 14-bt resolution
ADC: 8360 | Freq: 26003 Hz | ARR: 6152
ADC: 8363 | Freq: 26012 Hz | ARR: 6150
ADC: 8350 | Freq: 25974 Hz | ARR: 6159
ADC: 8355 | Freq: 25989 Hz | ARR: 6155

Interested readers may explore the sketch and learn the ways of manipulating the 32-bit registers of the STM32U585 MCU on the UNO Q board.