การปรับ duty cycle ของ pwm ในแต่คาบทำยังไงคะ?

  • 3 Replies
  • 599 Views
จุดประสงค์หลักคือต้องการสร้าง sine wave ความถี่สูงค่ะ ประมาณ 100-500kHz แรงดัน 15-20 V
เลยจะสร้าง pwm ที่มี cuty cycle ที่ต่างกันในแต่ละคาบ



ตอนนี้ใช้บอร์ด STM32F407-DISC1 กับโปรแกรม CooCox CoIDE ค่ะ สามารถสร้าง pwm ได้แล้ว แต่ยังปรับ duty cycle ในแต่ละคาบไม่ได้ ทุกคาบจะมี duty cycle เท่ากันหมด
ใช้ code จากเว็บนี้ค่ะ https://stm32f4-discovery.net/2014/05/stm32f4-stm32f429-discovery-pwm-tutorial/

ก่อนหน้านี้ใช้ arduino mega2560 สร้าง pwm ที่ปรับ duty cycle ในแต่ละคาบได้แล้ว ลองต่อกับ low pass filter แล้วได้ sine wave สูงสุด 10 kHz ที่ 8 samples จากเว็บนี้ค่ะ http://www.eprojectszone.com/how-to-generate-a-sine-wave-from-arduino-or-atmega-328/

ลองใช้ด้วยวิธีเดียวกันกับ STM32F407-DISC1 คือกำหนดความถี่คาบ แล้วก็ interrupt ไปอีก timer หนึ่งเพื่อกำหนด duty cycle แต่ใช้ไม่ได้
ต้องทำยังไงคะถึงจะสามารถปรับ duty cycle ในแต่ละคาบได้

ต้องการความถี่ Sine Wave 500kHz หมายความว่าความถี่ PWM ต้องสูงกว่าหลายเท่าเลยครับ
ถ้าต้องการ 8 Sample ต่อคลื่น ต้องใช้ PWM สูงถึง 500k x 8 = 4MHz
ใช้เป็น DAC น่าจะตอบโจทย์ได้มากกว่า

ส่วนการใช้ PWM กับ STM32F4 ผมทำเป็นตัวอย่างให้ โหลดได้ที่นี้ (ใช้กับ SW4STM32)
https://drive.google.com/drive/folders/1YfzT062ain57cf_3quq42B02wti4jhkx?usp=sharing

ใช้ TIM4 สร้าง PWM (ความถี่ 3.6MHz) ใช้ DMA โหลดค่า Duty Cycle (36 Sample) จะได้ Sine wave ประมาณ 100kHz

*

Offline dec

  • **
  • 71
    • View Profile
การสร้าง sine wave ความถี่สูง ประมาณ 100-500kHz ด้วย PWM
ค่อนข้างจะเกินความสามารถของ Microcontroller

-------------------------------------------------------------------------------------------------------------

อธิบายพื้นฐานของ Timer กับ PWM ของ STM32 คร่าว ๆ ก่อน ถ้าเข้าใจอยู่แล้วข้ามไปได้เลย

Timer แต่ละตัวของ STM32 จะมีความถี่สัญญาณ Timer Clock ไม่เท่ากัน
ขึ้นอยู่กับว่า Timer ตัวนั้นเกาะอยู่บน Bus APB1 หรือ APB2

อ้างอิงตาม STM32F407

Timer ทุกตัวจะมี Phase Lock Loop เพิ่มความถี่สัญญาณ Timer Clock เป็น 2 เท่า
ของสัญญาณ Clock จาก Bus APBx

ซึ่ง STM32F407 มีความถี่ Clock APB1 อยู่ที่ 42MHz และ APB2 จะอยู่ที่ 84MHz
ดังนั้นความถี่ Timer Clock ที่อยู่บน APB1 จะเป็น 84MHz และบน APB2 จะเป็น 164MHz





การทำงานก็ง่ายๆ วงจร Timer ก็จะป้อน Timer Clock ให้กับ Counter Register เพื่อ Trigger
ให้นับเลข ถ้าความถี่ Timer Clock สูงเกินไป STM32 ยังมีขั้นตอนให้สามารถ Scale
ลดความถี่ Timer Clock ได้ โดยกำหนดค่า Prescaler ซึ่งมีสูตรคำนวณเป็น

Timer Clock = Max Timer Clock / ( Prescaler + 1 )

Timer ส่วนใหญ่จะมี Counter Register ขนาด 16Bit บางตัวก็มี 32Bit
Timer จะนับเลขไปเรื่อยๆ จนเกิดการ Overflow แล้วก็จะกลับมาที่ 0
ณ เวลาที่ Counter Register เกิดการ Overflow จะเรียก Event นี้ว่า "Update"
ซึ่งเป็น 1 เหตุการณ์ที่สามารถโปรแกรมให้เกิดการ Interrupt ได้

ต่อไป การรับจนเกิด Overflow นั้น Counter Register ขนาด 16Bit ต้องนับเลขถึง 65535
ถึงจะเกิดการ Overflow มันอาจจะนานเกินไปที่จะให้นับถึง 65535 ซึ่งเราสามารถกำหนดค่า
ที่จะเกิดการ Overflow ได้โดยกำหนดค่า Period เช่นถ้าเรากำหนด Period = 1000
Timer จะนับมาถึง 1000 แล้วต่อไปก็จะกลายเป็น 0 เลย


ทีนี้จะทำ PWM แบบ Hardware หลักการทำงานก็ Timer บางตัวของ STM32
จะมี Output Compare Channel ซึ่งจะผูกกับขาของ STM32 เลย (ต้องเปิด Datasheet เพื่อดูว่า
Timer แต่ละตัวมี Output Compare Channel ออกที่ขาไหนบ้าง มันจะเขียนประมาณว่า TIM1_CH1, TIM4_CH2)
โดยตัว Output Channel นี่จะมีการให้เซ็ตค่า Register ค่าหนึ่ง เรียกว่าค่า Compare
ถ้า Counter Register นับจนเกินค่า Compare สัญญาณที่ขาของ STM32 ที่ใช้เป็น Output Compare Channel
จะเกิดการ Toggle แล้วเมื่อ Counter เกิดการ Overflow ก็จะ Toggle กลับไปยัง State ปกติ

ยกตัวอย่าง ตั้งค่า Period ไว้ที่ 100 เพื่อต้องการให้ปรับ Duty cycle ได้ 101 ค่า (0 - 100)
ตั้งค่า Compare ไว้ที่ 50 ระหว่างที่ Counter Register นับ 0 - 50 สัญญาณที่ขาจะเป็น High
พอนับเลยไป 51 สัญญาณที่ขาจะสลับเป็น Low ทันที แล้วเมื่อ Counter Register เกิด Overflow
สัญญาณที่ขาจะกลับเป็น High

-------------------------------------------------------------------------------------------------------------

ทีนี้มาลองคำนวณง่ายๆ

Sine Wave 500kHz มีความถี่ Nyquist อยู่ที่ 1MHz
การแปลง Sine Wave จาก Digital มาเป็น Analog
เราต้องการ Sampling Rate อย่างต่ำที่ 2 เท่าของความถี่ Nyquist (4 เท่าของ Sine Wave)
ก็คือ 2MHz

แปลว่าเราต้องสร้างสัญญาณ PWM ที่มีความถี่อย่างน้อย 2MHz

เราจะได้สัญญาณ PWM ที่ใช้นิยามสัญญาณ Sine Wave 500kHz หน้าตาประมาณนี้ (กราฟสีเขียว)



แล้วถ้าห่างต้องการให้ปรับ Duty cycle ได้ 101 ระดับ (0 - 100) เราต้องกำหนดค่า Period = 100

เมื่อได้เงื่อนไขแล้ว ลองใช้ Timer 1 เป็นตัวอ้างอิงในการคำนวณดู

Timer 1 มี Max Timer Clock อยู่ที่ 168MHz
กำหนด Prescaler เป็น 0 เลย

Timer Clock = 168MHz / ( 0 + 1 ) = 168MHz

กำหนด Period = 100 Timer ต้องนับ 100 + 1 ครั้ง เพื่อให้เกิด Overflow

ความถี่ PWM = 168MHz / (100 + 1) = 1.6634MHz

จะเห็นว่าไม่สามารถทำสัญญาณ PWM ที่มีความถี่อย่างน้อย 2MHz ได้
อาจต้อง Upgrade  ไปใช้ STM32 ที่แรงขึ้นกว่านี้อย่าง STM32F7 หรือ STM32H7
ที่มีความเร็วสูงถึง 216MHz และ 400MHz ตามลำดับ ซึ่งมีราคาแพงกว่า

ถ้าสามารถ Upgrade ได้ ก็ต้องลองคิดดูว่าสัญญาณ PWM หน้าตาแบบนั้น
เพียงพอที่จะใช้งานรึเปล่า ต่อให้ใช้ STM32H7 เองก็อาจเพิ่ม Sampling Rate ได้อีกแค่ 1 เท่า
ปกติถ้าจะให้กราฟออกมาสวยๆ อาจต้องใช้ Sampling Rate ซัก 50 เท่าของ Sine Wave
หรือก็คือ 25MHz ทีนี้จะหา Microcontroller ตัวไหนที่มันจะสร้าง PWM 25MHz ได้กันล่ะ

-------------------------------------------------------------------------------------------------------------

ต่อมาเรื่อง Code

ในจุดนี้ผมคิดว่าคุณน่าจะรู้แล้วว่าจะเปลี่ยนค่า Duty Cycle ตอนไหน ก็ทุกๆ ครั้งที่เกิด Event Update หรือ
Timer Overflow นั่นเอง ผมจะอ้างอิงจาก Code ที่คุณยกมาเลยนะ ทำต่อจากนั้นเลย
ก่อนอื่นต้องเขียน Code เพิ่มเพื่อเปิดใช้งาน Timer Interrupt

ย่างแรกใส่ #include "misc.h" เพิ่ม (Library บางตัวเขียนเต็มๆ ว่า "stm32f4xx_misc.h")

ใน Function int main(void)
เพิ่ม NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); ไว้ก่อน TM_LEDS_Init();

ใน Function void TM_TIMER_Init(void)
ประกาศตัวแปร NVIC_InitTypeDef NVIC_InitStruct; เพิ่ม
แล้วลงมาก่อนบรรทัด TIM_Cmd(TIM4, ENABLE); เพิ่ม Code ดังนี้

Quote
    TIM_ITConfig(TIM4, TIM_IT_Update, ENABLE);
   
    NVIC_InitStruct.NVIC_IRQChannel = TIM4_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
    NVIC_Init(&NVIC_InitStruct);

Code ส่วนที่เพิ่มมาคือ เปิดให้ Timer 4 ส่งสัญญาณ Event Update ไปให้ CPU
และทำการเปิดใช้งาน Interrupt Request สำหรับ Timer 4 (เปิดใช้งานให้ CPU กระโดดไปทำงาน
ที่ Function Interrupt ได้นั่นแหละ)

เพิ่ม Function TIM4_IRQHandler เพื่อให้ CPU กระโดดมาทำงาน
ตามที่ถามว่าจะเปลี่ยน Duty Cycle ยังไง จริงๆ เปลี่ยนได้ง่ายๆ
โดยใช้ Function TIM_SetCompareX โดย X คือเลข Channel

Code: [Select]
void TIM4_IRQHandler(void)
{
  uint32_t compare;
  static uint32_t up_down_flags = 0;
 
  if(TIM_GetFlagStatus(TIM4, TIM_FLAG_Update) == SET)
  {
    TIM_ClearITPendingBit(TIM4, TIM_FLAG_Update);
   
    compare = TIM_GetCapture1(TIM4);
   
    if(up_down_flags == 0)
    {
      if(compare == 8399)
      {
        up_down_flags = 1;
        compare--;
      }
      else
        compare++;
    }
    else
    {
      if(compare == 0)
      {
        up_down_flags = 0;
        compare++;
      }
      else
        compare--;
    }
   
    TIM_SetCompare1(TIM4, compare);
  }
}

ใน Function interrupt นี้ผมเขียนให้มันเปลี่ยน Duty Cycle ขึ้นลงเฉยๆ คุณจะเห็นไฟสีเขียวหรี่แล้วสว่างขึ้น

อันนี้ Code เต็มๆ
Code: [Select]
/**
 *    PWM example for STM32F4 Discovery
 *    It should work on STM32F429 Discovery too and all other STM32F4xx devices
 *
 *    @author     Tilen Majerle
 *    @email        tilen@majerle.eu
 *    @website    http://stm32f4-discovery.net
 *    @ide        Keil uVision 5
 */
#include "stm32f4xx.h"
#include "misc.h"
#include "stm32f4xx_rcc.h"
#include "stm32f4xx_gpio.h"
#include "stm32f4xx_tim.h"
 
void TM_LEDS_Init(void) {
    GPIO_InitTypeDef GPIO_InitStruct;
   
    /* Clock for GPIOD */
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE);
 
    /* Alternating functions for pins */
    GPIO_PinAFConfig(GPIOD, GPIO_PinSource12, GPIO_AF_TIM4);
    GPIO_PinAFConfig(GPIOD, GPIO_PinSource13, GPIO_AF_TIM4);
    GPIO_PinAFConfig(GPIOD, GPIO_PinSource14, GPIO_AF_TIM4);
    GPIO_PinAFConfig(GPIOD, GPIO_PinSource15, GPIO_AF_TIM4);
   
    /* Set pins */
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
    GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_NOPULL;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_Init(GPIOD, &GPIO_InitStruct);
}
 
void TM_TIMER_Init(void) {
    NVIC_InitTypeDef NVIC_InitStruct;
    TIM_TimeBaseInitTypeDef TIM_BaseStruct;
   
    /* Enable clock for TIM4 */
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);
/*   
    TIM4 is connected to APB1 bus, which has on F407 device 42MHz clock                 
    But, timer has internal PLL, which double this frequency for timer, up to 84MHz     
    Remember: Not each timer is connected to APB1, there are also timers connected     
    on APB2, which works at 84MHz by default, and internal PLL increase                 
    this to up to 168MHz                                                             
   
    Set timer prescaller
    Timer count frequency is set with
   
    timer_tick_frequency = Timer_default_frequency / (prescaller_set + 1)       
   
    In our case, we want a max frequency for timer, so we set prescaller to 0         
    And our timer will have tick frequency       
   
    timer_tick_frequency = 84000000 / (0 + 1) = 84000000
*/   
    TIM_BaseStruct.TIM_Prescaler = 0;
    /* Count up */
    TIM_BaseStruct.TIM_CounterMode = TIM_CounterMode_Up;
/*
    Set timer period when it have reset
    First you have to know max value for timer
    In our case it is 16bit = 65535
    To get your frequency for PWM, equation is simple
   
    PWM_frequency = timer_tick_frequency / (TIM_Period + 1)
   
    If you know your PWM frequency you want to have timer period set correct
   
    TIM_Period = timer_tick_frequency / PWM_frequency - 1
   
    In our case, for 10Khz PWM_frequency, set Period to
   
    TIM_Period = 84000000 / 10000 - 1 = 8399
   
    If you get TIM_Period larger than max timer value (in our case 65535),
    you have to choose larger prescaler and slow down timer tick frequency
*/
    TIM_BaseStruct.TIM_Period = 8399; /* 10kHz PWM */
    TIM_BaseStruct.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_BaseStruct.TIM_RepetitionCounter = 0;
    /* Initialize TIM4 */
    TIM_TimeBaseInit(TIM4, &TIM_BaseStruct);
   
    TIM_ITConfig(TIM4, TIM_IT_Update, ENABLE);
   
    NVIC_InitStruct.NVIC_IRQChannel = TIM4_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
    NVIC_Init(&NVIC_InitStruct);
   
    /* Start count on TIM4 */
    TIM_Cmd(TIM4, ENABLE);
}

void TM_PWM_Init(void) {
    TIM_OCInitTypeDef TIM_OCStruct;
   
    /* Common settings */
   
    /* PWM mode 2 = Clear on compare match */
    /* PWM mode 1 = Set on compare match */
    TIM_OCStruct.TIM_OCMode = TIM_OCMode_PWM2;
    TIM_OCStruct.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCStruct.TIM_OCPolarity = TIM_OCPolarity_Low;
   
/*
    To get proper duty cycle, you have simple equation
   
    pulse_length = ((TIM_Period + 1) * DutyCycle) / 100 - 1
   
    where DutyCycle is in percent, between 0 and 100%
   
    25% duty cycle:     pulse_length = ((8399 + 1) * 25) / 100 - 1 = 2099
    50% duty cycle:     pulse_length = ((8399 + 1) * 50) / 100 - 1 = 4199
    75% duty cycle:     pulse_length = ((8399 + 1) * 75) / 100 - 1 = 6299
    100% duty cycle:    pulse_length = ((8399 + 1) * 100) / 100 - 1 = 8399
   
    Remember: if pulse_length is larger than TIM_Period, you will have output HIGH all the time
*/
    TIM_OCStruct.TIM_Pulse = 2099; /* 25% duty cycle */
    TIM_OC1Init(TIM4, &TIM_OCStruct);
    TIM_OC1PreloadConfig(TIM4, TIM_OCPreload_Enable);
   
    TIM_OCStruct.TIM_Pulse = 4199; /* 50% duty cycle */
    TIM_OC2Init(TIM4, &TIM_OCStruct);
    TIM_OC2PreloadConfig(TIM4, TIM_OCPreload_Enable);
   
    TIM_OCStruct.TIM_Pulse = 6299; /* 75% duty cycle */
    TIM_OC3Init(TIM4, &TIM_OCStruct);
    TIM_OC3PreloadConfig(TIM4, TIM_OCPreload_Enable);
   
    TIM_OCStruct.TIM_Pulse = 8399; /* 100% duty cycle */
    TIM_OC4Init(TIM4, &TIM_OCStruct);
    TIM_OC4PreloadConfig(TIM4, TIM_OCPreload_Enable);
}
 
int main(void) {
    /* Initialize system */
    SystemInit();
   
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
 
    /* Init leds */
    TM_LEDS_Init();
    /* Init timer */
    TM_TIMER_Init();
    /* Init PWM */
    TM_PWM_Init();
   
    while (1) {
    }
}

void TIM4_IRQHandler(void)
{
  uint32_t compare;
  static uint32_t up_down_flags = 0;
 
  if(TIM_GetFlagStatus(TIM4, TIM_FLAG_Update) == SET)
  {
    TIM_ClearITPendingBit(TIM4, TIM_FLAG_Update);
   
    compare = TIM_GetCapture1(TIM4);
   
    if(up_down_flags == 0)
    {
      if(compare == 8399)
      {
        up_down_flags = 1;
        compare--;
      }
      else
        compare++;
    }
    else
    {
      if(compare == 0)
      {
        up_down_flags = 0;
        compare++;
      }
      else
        compare--;
    }
   
    TIM_SetCompare1(TIM4, compare);
  }
}

-------------------------------------------------------------------------------------------------------------

สุดท้าย มีเรื่องที่อยากจะบอกคือ CooCox CoIDE หยุดพัฒนาแล้ว เป็นไปได้ก็อยากให้เปลี่ยนครับ
มีตัวที่คล้ายๆ กันอยู่เช่น System Workbench for STM32 และ TrueSTUDIO ทั้ง 2 ตัวนี้เป็น
Eclipse based เหมือนกับ CoIDE โดยเฉพาะ TrueSTUDIO ตอนนี้ ST เป็นเจ้าของเอง

และ Library ที่คุณใช้อยู่เรียกว่า Standard Peripheral Library (SPL) เป็น Library เก่าที่ ST
ไม่พัฒนาต่อแล้ว ST ผลักดันให้ใช้ STM32Cube แทน

ทั้งนี้ทั้งนั้นแล้ว ถ้ามันยังใช้พัฒนาได้ก็ขึ้นอยู่กับการตัดสินใจของคุณครับ ส่วนตัวผมก็ชอบ SPL
แต่ก็ต้องยอมไปใช้ STM32Cube เพราะชิปตัวใหม่ๆ ไม่มี SPL ให้ใช้แล้ว หรือถ้าสนใจจะเปลี่ยนไปใช้
Cube หรือเปลี่ยน IDE ก็ไม่ต้องรีบครับ ทำงานนี้ให้เสร็จก่อน แล้วค่อยไปหัดใช้ทีหลัง

-------------------------------------------------------------------------------------------------------------

สุดท้ายอีกที STM32 น่าจะอยู่หมวด ARM นะครับ

อธิบายละเอียดดีมากเลย ขอบคุณมากค่ะ