마이크로컨트롤러를 활용한 가변 듀티 사이클 PWM 파형 생성 가이드

기초 이론

1. PWM 동작 원리

듀티 사이클(Duty Cycle) = High 상태 시간 / 전체 주기 × 100%

PWM、周波数 = 1 / (T_high + T_low)
T_high: High 상태 지속 시간
T_low: Low 상태 지속 시간

2. 주요 마이크로컨트롤러 PWM 구현 비교

마이크로컨트롤러 PWM 모듈 최대 주파수 해상도 채널 수
STM32F103 고급/범용 타이머 72MHz 16-bit 최대 12채널
STM32F407 고급/범용 타이머 168MHz 16-bit 최대 12채널
ATmega328P 타이머 0/1/2 16MHz 8-bit/16-bit 6채널
ESP32 LEDC/MCPWM 80MHz 16-bit 최대 16채널
8051 파생형 소프트웨어 에뮬레이션 주파수에 따라 좌우 소프트웨어 조절 제한 없음

구현 예제

1. STM32 기반 구현 (HAL 라이브러리 활용)

메인 프로그램 (main.c)

/**
  * STM32 PWM 파형 생성 프로그램
  * MCU: STM32F103C8T6
  * 기능: 4채널 독립 PWM 출력, 듀티 사이클 조절 가능
  * 주파수 범위: 1Hz ~ 100KHz
  * 듀티 사이클 범위: 0-100%
  */

#include "main.h"
#include <stdio.h>
#include <string.h>

TIM_HandleTypeDef htim3;
TIM_HandleTypeDef htim4;
UART_HandleTypeDef huart2;

typedef struct {
    TIM_HandleTypeDef* timer_handle;
    uint32_t pwm_channel;
    uint32_t wave_freq;
    uint32_t duty_value;
    uint32_t period_count;
    uint32_t compare_count;
    uint8_t active_flag;
} PWM_Config;

PWM_Config pwm_output[4];
uint8_t selected_idx = 0;
char serial_out[120];

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_TIM3_Init(void);
static void MX_TIM4_Init(void);
static void MX_USART2_UART_Init(void);
void PWM_Configure(PWM_Config* cfg, TIM_HandleTypeDef* htim, 
                  uint32_t ch, uint32_t freq, uint32_t duty);
void PWM_ChangeFreq(PWM_Config* cfg, uint32_t freq);
void PWM_ChangeDuty(PWM_Config* cfg, uint32_t duty);
void PWM_Enable(PWM_Config* cfg);
void PWM_Disable(PWM_Config* cfg);
void PWM_Refresh(PWM_Config* cfg);
void Serial_Print(char* str);
void Show_Menu(void);
void Handle_Command(char* cmd);

int main(void) {
    HAL_Init();
    SystemClock_Config();
    
    MX_GPIO_Init();
    MX_TIM3_Init();
    MX_TIM4_Init();
    MX_USART2_UART_Init();
    
    // PWM 채널 초기화
    // 채널 1: PC6, TIM3_CH1, 1KHz, 50% 듀티
    PWM_Configure(&pwm_output[0], &htim3, TIM_CHANNEL_1, 1000, 5000);
    
    // 채널 2: PC7, TIM3_CH2, 2KHz, 30% 듀티
    PWM_Configure(&pwm_output[1], &htim3, TIM_CHANNEL_2, 2000, 3000);
    
    // 채널 3: PC8, TIM4_CH1, 5KHz, 70% 듀티
    PWM_Configure(&pwm_output[2], &htim4, TIM_CHANNEL_1, 5000, 7000);
    
    // 채널 4: PC9, TIM4_CH2, 10KHz, 20% 듀티
    PWM_Configure(&pwm_output[3], &htim4, TIM_CHANNEL_2, 10000, 2000);
    
    // 모든 PWM 채널 활성화
    for(int i = 0; i < 4; i++) {
        PWM_Enable(&pwm_output[i]);
    }
    
    Show_Menu();
    
    char recv_buf[50];
    uint8_t recv_ptr = 0;
    
    while (1) {
        if(HAL_UART_Receive(&huart2, (uint8_t*)&recv_buf[recv_ptr], 1, 10) == HAL_OK) {
            if(recv_buf[recv_ptr] == '\r' || recv_buf[recv_ptr] == '\n') {
                recv_buf[recv_ptr] = '\0';
                if(recv_ptr > 0) {
                    Handle_Command(recv_buf);
                }
                recv_ptr = 0;
            } else {
                recv_ptr = (recv_ptr + 1) % 50;
            }
        }
        
        HAL_Delay(10);
    }
}

void PWM_Configure(PWM_Config* cfg, TIM_HandleTypeDef* htim, 
                  uint32_t ch, uint32_t freq, uint32_t duty) {
    cfg->timer_handle = htim;
    cfg->pwm_channel = ch;
    cfg->wave_freq = freq;
    cfg->duty_value = duty;
    cfg->active_flag = 0;
    
    PWM_ChangeFreq(cfg, freq);
    PWM_ChangeDuty(cfg, duty);
}

void PWM_ChangeFreq(PWM_Config* cfg, uint32_t freq) {
    if(freq < 1) freq = 1;
    if(freq > 100000) freq = 100000;
    
    cfg->wave_freq = freq;
    
    uint32_t base_clock = 1000000;  // 1MHz
    cfg->period_count = (base_clock / freq) - 1;
    
    __HAL_TIM_SET_AUTORELOAD(cfg->timer_handle, cfg->period_count);
    
    PWM_ChangeDuty(cfg, cfg->duty_value);
    
    if(cfg->active_flag) {
        PWM_Refresh(cfg);
    }
}

void PWM_ChangeDuty(PWM_Config* cfg, uint32_t duty) {
    if(duty > 10000) duty = 10000;
    
    cfg->duty_value = duty;
    cfg->compare_count = (duty * cfg->period_count) / 10000;
    
    switch(cfg->pwm_channel) {
        case TIM_CHANNEL_1:
            __HAL_TIM_SET_COMPARE(cfg->timer_handle, TIM_CHANNEL_1, cfg->compare_count);
            break;
        case TIM_CHANNEL_2:
            __HAL_TIM_SET_COMPARE(cfg->timer_handle, TIM_CHANNEL_2, cfg->compare_count);
            break;
        case TIM_CHANNEL_3:
            __HAL_TIM_SET_COMPARE(cfg->timer_handle, TIM_CHANNEL_3, cfg->compare_count);
            break;
        case TIM_CHANNEL_4:
            __HAL_TIM_SET_COMPARE(cfg->timer_handle, TIM_CHANNEL_4, cfg->compare_count);
            break;
    }
    
    if(cfg->active_flag) {
        PWM_Refresh(cfg);
    }
}

void PWM_Enable(PWM_Config* cfg) {
    HAL_TIM_PWM_Start(cfg->timer_handle, cfg->pwm_channel);
    cfg->active_flag = 1;
}

void PWM_Disable(PWM_Config* cfg) {
    HAL_TIM_PWM_Stop(cfg->timer_handle, cfg->pwm_channel);
    cfg->active_flag = 0;
}

void PWM_Refresh(PWM_Config* cfg) {
    HAL_TIM_PWM_Stop(cfg->timer_handle, cfg->pwm_channel);
    
    __HAL_TIM_SET_AUTORELOAD(cfg->timer_handle, cfg->period_count);
    
    PWM_ChangeDuty(cfg, cfg->duty_value);
    
    HAL_TIM_PWM_Start(cfg->timer_handle, cfg->pwm_channel);
}

void Serial_Print(char* str) {
    HAL_UART_Transmit(&huart2, (uint8_t*)str, strlen(str), 1000);
    HAL_UART_Transmit(&huart2, (uint8_t*)"\r\n", 2, 1000);
}

void Show_Menu(void) {
    Serial_Print("\r\n=== PWM 파형 발생기 ===");
    Serial_Print("명령어 목록:");
    Serial_Print("  c [1-4] - 채널 선택");
    Serial_Print("  f [Hz]  - 주파수 설정");
    Serial_Print("  d [0-100] - 듀티 사이클 설정");
    Serial_Print("  s       - 시작/정지");
    Serial_Print("  i       - 정보 표시");
    Serial_Print("  m       - 메뉴 표시");
    Serial_Print("현재 채널: 1");
    
    for(int i = 0; i < 4; i++) {
        sprintf(serial_out, "채널%d: %dHz, %.1f%%", 
                i+1, 
                pwm_output[i].wave_freq,
                pwm_output[i].duty_value / 100.0);
        Serial_Print(serial_out);
    }
}

void Handle_Command(char* cmd) {
    char cmd_type = cmd[0];
    int param = 0;
    
    if(strlen(cmd) > 2) {
        sscanf(&cmd[2], "%d", &param);
    }
    
    switch(cmd_type) {
        case 'c':
            if(param >= 1 && param <= 4) {
                selected_idx = param - 1;
                sprintf(serial_out, "채널%d 선택됨", param);
                Serial_Print(serial_out);
            }
            break;
            
        case 'f':
            if(param > 0) {
                PWM_ChangeFreq(&pwm_output[selected_idx], param);
                sprintf(serial_out, "채널%d 주파수 %dHz로 설정", 
                        selected_idx+1, param);
                Serial_Print(serial_out);
            }
            break;
            
        case 'd':
            if(param >= 0 && param <= 100) {
                PWM_ChangeDuty(&pwm_output[selected_idx], param * 100);
                sprintf(serial_out, "채널%d 듀티 사이클 %d%%로 설정", 
                        selected_idx+1, param);
                Serial_Print(serial_out);
            }
            break;
            
        case 's':
            if(pwm_output[selected_idx].active_flag) {
                PWM_Disable(&pwm_output[selected_idx]);
                sprintf(serial_out, "채널%d 정지됨", selected_idx+1);
            } else {
                PWM_Enable(&pwm_output[selected_idx]);
                sprintf(serial_out, "채널%d 시작됨", selected_idx+1);
            }
            Serial_Print(serial_out);
            break;
            
        case 'i':
            sprintf(serial_out, "채널%d: %dHz, %.1f%%, %s", 
                    selected_idx+1,
                    pwm_output[selected_idx].wave_freq,
                    pwm_output[selected_idx].duty_value / 100.0,
                    pwm_output[selected_idx].active_flag ? "실행" : "대기");
            Serial_Print(serial_out);
            break;
            
        case 'm':
            Show_Menu();
            break;
            
        default:
            Serial_Print("알 수 없는 명령어, 'm'을 눌러 메뉴 확인");
            break;
    }
}

타이머 3 초기화 코드

/**
  * TIM3 초기화
  * 채널 1: PC6
  * 채널 2: PC7
  * 채널 3: PC8
  * 채널 4: PC9
  */
static void MX_TIM3_Init(void) {
    TIM_ClockConfigTypeDef clock_src = {0};
    TIM_MasterConfigTypeDef master_cfg = {0};
    TIM_OC_InitTypeDef pwm_cfg = {0};
    
    htim3.Instance = TIM3;
    htim3.Init.Prescaler = 72 - 1;
    htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim3.Init.Period = 1000 - 1;
    htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
    HAL_TIM_Base_Init(&htim3);
    
    clock_src.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
    HAL_TIM_ConfigClockSource(&htim3, &clock_src);
    HAL_TIM_PWM_Init(&htim3);
    
    master_cfg.MasterOutputTrigger = TIM_TRGO_RESET;
    master_cfg.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
    HAL_TIMEx_MasterConfigSynchronization(&htim3, &master_cfg);
    
    pwm_cfg.OCMode = TIM_OCMODE_PWM1;
    pwm_cfg.Pulse = 500;
    pwm_cfg.OCPolarity = TIM_OCPOLARITY_HIGH;
    pwm_cfg.OCFastMode = TIM_OCFAST_DISABLE;
    
    HAL_TIM_PWM_ConfigChannel(&htim3, &pwm_cfg, TIM_CHANNEL_1);
    HAL_TIM_PWM_ConfigChannel(&htim3, &pwm_cfg, TIM_CHANNEL_2);
    HAL_TIM_PWM_ConfigChannel(&htim3, &pwm_cfg, TIM_CHANNEL_3);
    HAL_TIM_PWM_ConfigChannel(&htim3, &pwm_cfg, TIM_CHANNEL_4);
}

타이머 4 초기화 코드

/**
  * TIM4 초기화
  * 채널 1: PB6
  * 채널 2: PB7
  * 채널 3: PB8
  * 채널 4: PB9
  */
static void MX_TIM4_Init(void) {
    TIM_ClockConfigTypeDef clock_src = {0};
    TIM_MasterConfigTypeDef master_cfg = {0};
    TIM_OC_InitTypeDef pwm_cfg = {0};
    
    htim4.Instance = TIM4;
    htim4.Init.Prescaler = 72 - 1;
    htim4.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim4.Init.Period = 1000 - 1;
    htim4.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    htim4.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
    HAL_TIM_Base_Init(&htim4);
    
    clock_src.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
    HAL_TIM_ConfigClockSource(&htim4, &clock_src);
    HAL_TIM_PWM_Init(&htim4);
    
    master_cfg.MasterOutputTrigger = TIM_TRGO_RESET;
    master_cfg.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
    HAL_TIMEx_MasterConfigSynchronization(&htim4, &master_cfg);
    
    pwm_cfg.OCMode = TIM_OCMODE_PWM1;
    pwm_cfg.Pulse = 500;
    pwm_cfg.OCPolarity = TIM_OCPOLARITY_HIGH;
    pwm_cfg.OCFastMode = TIM_OCFAST_DISABLE;
    
    HAL_TIM_PWM_ConfigChannel(&htim4, &pwm_cfg, TIM_CHANNEL_1);
    HAL_TIM_PWM_ConfigChannel(&htim4, &pwm_cfg, TIM_CHANNEL_2);
    HAL_TIM_PWM_ConfigChannel(&htim4, &pwm_cfg, TIM_CHANNEL_3);
    HAL_TIM_PWM_ConfigChannel(&htim4, &pwm_cfg, TIM_CHANNEL_4);
}

2. Arduino 기반 구현

/**
  * Arduino PWM 파형 생성 프로그램
  * 6채널 PWM 출력 지원
  * 주파수 범위: 1Hz ~ 31KHz (핀별 상이)
  * 듀티 사이클 범위: 0-100%
  */

#include <Arduino.h>

const int pwm_pin_array[] = {3, 5, 6, 9, 10, 11};
const int total_channels = 6;

struct PWMChan {
    int pin_num;
    float freq_hz;
    float duty_pct;
    uint32_t period_us;
    uint32_t on_time_us;
    bool run_flag;
};

PWMChan ch_data[total_channels];

struct TimerRegSet {
    uint8_t timer_id;
    uint8_t tccra_val;
    uint8_t tccrb_val;
    uint8_t wgm_mode;
    uint8_t com_mode;
    uint32_t max_freq;
};

const TimerRegSet timer_table[] = {
    {0, 0, 0, 0, 0, 62500},
    {0, 1, 3, 5, 1, 31250},
    {0, 1, 3, 2, 1, 7812},
    {0, 1, 3, 7, 1, 976},
    {1, 1, 3, 5, 1, 31250},
    {1, 1, 3, 2, 1, 3906},
    {1, 1, 3, 8, 1, 488},
    {2, 1, 3, 5, 1, 31250},
    {2, 1, 3, 2, 1, 3906},
    {2, 1, 3, 7, 1, 976}
};

void setup() {
    Serial.begin(115200);
    
    for(int i = 0; i < total_channels; i++) {
        ch_data[i].pin_num = pwm_pin_array[i];
        ch_data[i].freq_hz = 1000;
        ch_data[i].duty_pct = 50;
        ch_data[i].run_flag = true;
        
        pinMode(ch_data[i].pin_num, OUTPUT);
        
        applyPWMFreq(ch_data[i].pin_num, ch_data[i].freq_hz);
        analogWrite(ch_data[i].pin_num, ch_data[i].duty_pct * 2.55);
    }
    
    Serial.println("=== Arduino PWM 발생기 ===");
    Serial.println("초기화 완료, 6채널 PWM 출력 준비됨");
    printStatus();
}

void loop() {
    if(Serial.available() > 0) {
        char command = Serial.read();
        processCmd(command);
    }
    
    delay(10);
}

void applyPWMFreq(int pin, float freq) {
    uint8_t timer = digitalPinToTimer(pin);
    
    switch(timer) {
        case TIMER0A:
        case TIMER0B:
            setTimer0Freq(freq);
            break;
            
        case TIMER1A:
        case TIMER1B:
            setTimer1Freq(freq);
            break;
            
        case TIMER2A:
        case TIMER2B:
            setTimer2Freq(freq);
            break;
    }
}

void setTimer0Freq(float freq) {
    uint8_t duty5 = (OCR0A * 100) / 255;
    uint8_t duty6 = (OCR0B * 100) / 255;
    
    if(freq >= 31250) {
        TCCR0A = (1 << COM0A1) | (1 << COM0B1) | (1 << WGM01) | (1 << WGM00);
        TCCR0B = (1 << CS00);
        OCR0A = (duty5 * 255) / 100;
        OCR0B = (duty6 * 255) / 100;
    } else if(freq >= 7812) {
        TCCR0A = (1 << COM0A1) | (1 << COM0B1) | (1 << WGM01);
        TCCR0B = (1 << CS01);
        OCR0A = (duty5 * 255) / 100;
        OCR0B = (duty6 * 255) / 100;
    } else {
        TCCR0A = (1 << COM0A1) | (1 << COM0B1) | (1 << WGM01) | (1 << WGM00);
        TCCR0B = (1 << CS01) | (1 << CS00);
        OCR0A = (duty5 * 255) / 100;
        OCR0B = (duty6 * 255) / 100;
    }
}

void setTimer1Freq(float freq) {
    uint16_t duty9 = (OCR1A * 100) / 65535;
    uint16_t duty10 = (OCR1B * 100) / 65535;
    
    if(freq >= 31250) {
        TCCR1A = (1 << COM1A1) | (1 << COM1B1) | (1 << WGM10);
        TCCR1B = (1 << WGM12) | (1 << CS10);
        OCR1A = (duty9 * 65535) / 100;
        OCR1B = (duty10 * 65535) / 100;
    } else if(freq >= 3906) {
        TCCR1A = (1 << COM1A1) | (1 << COM1B1) | (1 << WGM11);
        TCCR1B = (1 << WGM13) | (1 << CS11);
        OCR1A = (duty9 * 65535) / 100;
        OCR1B = (duty10 * 65535) / 100;
    } else {
        TCCR1A = (1 << COM1A1) | (1 << COM1B1) | (1 << WGM11) | (1 << WGM10);
        TCCR1B = (1 << WGM13) | (1 << CS11) | (1 << CS10);
        OCR1A = (duty9 * 65535) / 100;
        OCR1B = (duty10 * 65535) / 100;
    }
}

void setTimer2Freq(float freq) {
    uint8_t duty3 = (OCR2B * 100) / 255;
    uint8_t duty11 = (OCR2A * 100) / 255;
    
    if(freq >= 31250) {
        TCCR2A = (1 << COM2A1) | (1 << COM2B1) | (1 << WGM20);
        TCCR2B = (1 << WGM22) | (1 << CS20);
        OCR2A = (duty11 * 255) / 100;
        OCR2B = (duty3 * 255) / 100;
    } else if(freq >= 3906) {
        TCCR2A = (1 << COM2A1) | (1 << COM2B1) | (1 << WGM21);
        TCCR2B = (1 << WGM22) | (1 << CS21);
        OCR2A = (duty11 * 255) / 100;
        OCR2B = (duty3 * 255) / 100;
    } else {
        TCCR2A = (1 << COM2A1) | (1 << COM2B1) | (1 << WGM21) | (1 << WGM20);
        TCCR2B = (1 << CS22) | (1 << CS21) | (1 << CS20);
        OCR2A = (duty11 * 255) / 100;
        OCR2B = (duty3 * 255) / 100;
    }
}

void setPWMDuty(int pin, float duty) {
    if(duty < 0) duty = 0;
    if(duty > 100) duty = 100;
    
    for(int i = 0; i < total_channels; i++) {
        if(ch_data[i].pin_num == pin) {
            ch_data[i].duty_pct = duty;
            break;
        }
    }
    
    analogWrite(pin, duty * 2.55);
}

class SoftPWM {
private:
    int io_pin;
    float frequency;
    float duty_cycle;
    uint32_t period_us;
    uint32_t on_time_us;
    bool is_running;
    uint32_t last_check;
    
public:
    SoftPWM(int pin) {
        io_pin = pin;
        frequency = 1000;
        duty_cycle = 50;
        is_running = false;
        pinMode(io_pin, OUTPUT);
    }
    
    void start(float freq, float duty) {
        frequency = freq;
        duty_cycle = duty;
        period_us = 1000000 / frequency;
        on_time_us = (period_us * duty_cycle) / 100;
        is_running = true;
        last_check = micros();
    }
    
    void update() {
        if(!is_running) return;
        
        uint32_t now = micros();
        uint32_t elapsed = now - last_check;
        
        if(elapsed >= period_us) {
            last_check = now;
            digitalWrite(io_pin, HIGH);
        } else if(elapsed >= on_time_us) {
            digitalWrite(io_pin, LOW);
        }
    }
    
    void setFreq(float freq) {
        frequency = freq;
        period_us = 1000000 / frequency;
        on_time_us = (period_us * duty_cycle) / 100;
    }
    
    void setDuty(float duty) {
        duty_cycle = duty;
        on_time_us = (period_us * duty_cycle) / 100;
    }
    
    void stop() {
        is_running = false;
        digitalWrite(io_pin, LOW);
    }
    
    void begin() {
        is_running = true;
        last_check = micros();
    }
};

SoftPWM sw_pwm_1(2);
SoftPWM sw_pwm_2(4);

void processCmd(char cmd) {
    switch(cmd) {
        case 's':
            printStatus();
            break;
            
        case '1':
            configureChan(0);
            break;
            
        case '2':
            configureChan(1);
            break;
            
        case 'h':
            showHelp();
            break;
    }
}

void printStatus() {
    Serial.println("\n=== PWM 채널 상태 ===");
    for(int i = 0; i < total_channels; i++) {
        Serial.print("채널");
        Serial.print(i+1);
        Serial.print("(핀");
        Serial.print(ch_data[i].pin_num);
        Serial.print("): ");
        Serial.print(ch_data[i].freq_hz);
        Serial.print("Hz, ");
        Serial.print(ch_data[i].duty_pct);
        Serial.print("%");
        Serial.println(ch_data[i].run_flag ? " [실행]" : " [정지]");
    }
}

void showHelp() {
    Serial.println("\n=== 명령어 목록 ===");
    Serial.println("s - 상태 표시");
    Serial.println("1-6 - 채널 선택");
    Serial.println("f [주파수] - 주파수 설정");
    Serial.println("d [듀티] - 듀티 사이클 설정");
    Serial.println("e - 활성화/비활성화");
    Serial.println("h - 도움말 표시");
}

void configureChan(int idx) {
    if(idx < 0 || idx >= total_channels) return;
    
    Serial.print("\n채널");
    Serial.println(idx+1);
    Serial.println("주파수 입력(Hz): ");
    
    while(!Serial.available());
    float freq = Serial.parseFloat();
    
    Serial.println("듀티 사이클 입력(%): ");
    while(!Serial.available());
    float duty = Serial.parseFloat();
    
    applyPWMFreq(ch_data[idx].pin_num, freq);
    setPWMDuty(ch_data[idx].pin_num, duty);
    
    ch_data[idx].freq_hz = freq;
    ch_data[idx].duty_pct = duty;
    
    Serial.print("채널");
    Serial.print(idx+1);
    Serial.print("설정 완료: ");
    Serial.print(freq);
    Serial.print("Hz, ");
    Serial.print(duty);
    Serial.println("%");
}

3. 8051 마이크로컨트롤러 구현 (소프트웨어 PWM)

/**
  * 8051 마이크로컨트롤러 소프트웨어 PWM 구현
  * MCU: AT89C51/STC89C52
  * 크리스탈: 11.0592MHz
  * 4채널 독립 PWM 구현 가능
  */

#include <reg52.h>
#include <intrins.h>

sbit PWM_CH1 = P1^0;
sbit PWM_CH2 = P1^1;
sbit PWM_CH3 = P1^2;
sbit PWM_CH4 = P1^3;

sbit BTN_INC = P3^2;
sbit BTN_DEC = P3^3;
sbit BTN_SEL = P3^4;
sbit BTN_FREQ = P3^5;

sbit LCD_RS = P2^0;
sbit LCD_RW = P2^1;
sbit LCD_EN = P2^2;
#define LCD_PORT P0

typedef struct {
    unsigned char duty_cycle;
    unsigned int high_cnt;
    unsigned int low_cnt;
    unsigned int counter;
    bit output_state;
    bit enable_flag;
} PWM_Device;

PWM_Device pwm_dev[4];
unsigned char active_ch = 0;
unsigned int pwm_cycle = 1000;
unsigned char freq_list[] = {100, 200, 500, 1000, 2000};
unsigned char freq_idx = 2;

void Timer0_Setup(void);
void PWM_Boot(void);
void PWM_Refresh(void);
void Button_Scan(void);
void LCD_Setup(void);
void LCD_Cmd(unsigned char cmd);
void LCD_Char(unsigned char dat);
void LCD_Text(unsigned char x, unsigned char y, unsigned char *str);
void LCD_Num(unsigned char x, unsigned char y, unsigned int val);
void Wait(unsigned int ms);

void main(void) {
    Timer0_Setup();
    LCD_Setup();
    PWM_Boot();
    
    LCD_Text(0, 0, "PWM Generator");
    LCD_Text(0, 1, "CH:1 500Hz 50%");
    
    while(1) {
        Button_Scan();
    }
}

void Timer0_Setup(void) {
    TMOD &= 0xF0;
    TMOD |= 0x01;
    
    TH0 = (65536 - 46) / 256;
    TL0 = (65536 - 46) % 256;
    
    ET0 = 1;
    TR0 = 1;
    EA = 1;
}

void PWM_Boot(void) {
    unsigned char i;
    
    for(i = 0; i < 4; i++) {
        pwm_dev[i].duty_cycle = 50;
        pwm_dev[i].enable_flag = 1;
        
        pwm_cycle = 1000000 / freq_list[freq_idx];
        pwm_dev[i].high_cnt = (pwm_cycle * pwm_dev[i].duty_cycle) / 100;
        pwm_dev[i].low_cnt = pwm_cycle - pwm_dev[i].high_cnt;
        pwm_dev[i].counter = 0;
        pwm_dev[i].output_state = 0;
    }
    
    PWM_CH1 = 0;
    PWM_CH2 = 0;
    PWM_CH3 = 0;
    PWM_CH4 = 0;
}

void Timer0_Interrupt(void) interrupt 1 {
    static unsigned int tick_cnt = 0;
    
    TH0 = (65536 - 46) / 256;
    TL0 = (65536 - 46) % 256;
    
    tick_cnt++;
    
    if(tick_cnt >= 20) {
        tick_cnt = 0;
        PWM_Refresh();
    }
}

void PWM_Refresh(void) {
    unsigned char i;
    
    for(i = 0; i < 4; i++) {
        if(!pwm_dev[i].enable_flag) {
            switch(i) {
                case 0: PWM_CH1 = 0; break;
                case 1: PWM_CH2 = 0; break;
                case 2: PWM_CH3 = 0; break;
                case 3: PWM_CH4 = 0; break;
            }
            continue;
        }
        
        pwm_dev[i].counter++;
        
        if(pwm_dev[i].output_state) {
            if(pwm_dev[i].counter >= pwm_dev[i].high_cnt) {
                pwm_dev[i].output_state = 0;
                pwm_dev[i].counter = 0;
                
                switch(i) {
                    case 0: PWM_CH1 = 0; break;
                    case 1: PWM_CH2 = 0; break;
                    case 2: PWM_CH3 = 0; break;
                    case 3: PWM_CH4 = 0; break;
                }
            }
        } else {
            if(pwm_dev[i].counter >= pwm_dev[i].low_cnt) {
                pwm_dev[i].output_state = 1;
                pwm_dev[i].counter = 0;
                
                switch(i) {
                    case 0: PWM_CH1 = 1; break;
                    case 1: PWM_CH2 = 1; break;
                    case 2: PWM_CH3 = 1; break;
                    case 3: PWM_CH4 = 1; break;
                }
            }
        }
    }
}

void Set_PWM_Freq(unsigned char freq_hz) {
    unsigned char i;
    
    pwm_cycle = 1000000 / freq_hz;
    
    for(i = 0; i < 4; i++) {
        pwm_dev[i].high_cnt = (pwm_cycle * pwm_dev[i].duty_cycle) / 100;
        pwm_dev[i].low_cnt = pwm_cycle - pwm_dev[i].high_cnt;
        pwm_dev[i].counter = 0;
    }
}

void Set_PWM_Duty(unsigned char ch, unsigned char duty) {
    if(ch >= 4) return;
    if(duty > 100) duty = 100;
    
    pwm_dev[ch].duty_cycle = duty;
    pwm_dev[ch].high_cnt = (pwm_cycle * duty) / 100;
    pwm_dev[ch].low_cnt = pwm_cycle - pwm_dev[ch].high_cnt;
    pwm_dev[ch].counter = 0;
    
    LCD_Text(0, 1, "CH:");
    LCD_Char(active_ch + 1 + '0');
    LCD_Text(4, 1, "Hz ");
    LCD_Num(7, 1, freq_list[freq_idx]);
    LCD_Text(11, 1, "  ");
    LCD_Num(11, 1, duty);
    LCD_Text(13, 1, "% ");
}

void Button_Scan(void) {
    static bit btn_inc_state = 0;
    static bit btn_dec_state = 0;
    static bit btn_sel_state = 0;
    static bit btn_freq_state = 0;
    
    if(!BTN_INC) {
        if(!btn_inc_state) {
            btn_inc_state = 1;
            Wait(10);
            if(!BTN_INC) {
                if(pwm_dev[active_ch].duty_cycle < 100) {
                    Set_PWM_Duty(active_ch, pwm_dev[active_ch].duty_cycle + 1);
                }
            }
        }
    } else {
        btn_inc_state = 0;
    }
    
    if(!BTN_DEC) {
        if(!btn_dec_state) {
            btn_dec_state = 1;
            Wait(10);
            if(!BTN_DEC) {
                if(pwm_dev[active_ch].duty_cycle > 0) {
                    Set_PWM_Duty(active_ch, pwm_dev[active_ch].duty_cycle - 1);
                }
            }
        }
    } else {
        btn_dec_state = 0;
    }
    
    if(!BTN_SEL) {
        if(!btn_sel_state) {
            btn_sel_state = 1;
            Wait(10);
            if(!BTN_SEL) {
                active_ch = (active_ch + 1) % 4;
                
                LCD_Text(0, 1, "CH:");
                LCD_Char(active_ch + 1 + '0');
                LCD_Text(4, 1, "Hz ");
                LCD_Num(7, 1, freq_list[freq_idx]);
                LCD_Text(11, 1, "  ");
                LCD_Num(11, 1, pwm_dev[active_ch].duty_cycle);
                LCD_Text(13, 1, "% ");
            }
        }
    } else {
        btn_sel_state = 0;
    }
    
    if(!BTN_FREQ) {
        if(!btn_freq_state) {
            btn_freq_state = 1;
            Wait(10);
            if(!BTN_FREQ) {
                freq_idx = (freq_idx + 1) % 5;
                Set_PWM_Freq(freq_list[freq_idx]);
                
                LCD_Text(0, 1, "CH:");
                LCD_Char(active_ch + 1 + '0');
                LCD_Text(4, 1, "Hz ");
                LCD_Num(7, 1, freq_list[freq_idx]);
                LCD_Text(11, 1, "  ");
                LCD_Num(11, 1, pwm_dev[active_ch].duty_cycle);
                LCD_Text(13, 1, "% ");
            }
        }
    } else {
        btn_freq_state = 0;
    }
}

void LCD_Setup(void) {
    LCD_Cmd(0x38);
    Wait(5);
    LCD_Cmd(0x0C);
    Wait(5);
    LCD_Cmd(0x06);
    Wait(5);
    LCD_Cmd(0x01);
    Wait(5);
}

void LCD_Cmd(unsigned char cmd) {
    LCD_RS = 0;
    LCD_RW = 0;
    LCD_EN = 0;
    LCD_PORT = cmd;
    Wait(1);
    LCD_EN = 1;
    Wait(1);
    LCD_EN = 0;
}

void LCD_Char(unsigned char dat) {
    LCD_RS = 1;
    LCD_RW = 0;
    LCD_EN = 0;
    LCD_PORT = dat;
    Wait(1);
    LCD_EN = 1;
    Wait(1);
    LCD_EN = 0;
}

void LCD_Text(unsigned char x, unsigned char y, unsigned char *str) {
    unsigned char i = 0;
    
    if(y == 0) {
        LCD_Cmd(0x80 + x);
    } else {
        LCD_Cmd(0xC0 + x);
    }
    
    while(str[i] != '\0') {
        LCD_Char(str[i]);
        i++;
    }
}

void LCD_Num(unsigned char x, unsigned char y, unsigned int val) {
    unsigned char buf[6];
    unsigned char i = 0;
    
    if(val == 0) {
        buf[0] = '0';
        buf[1] = '\0';
    } else {
        while(val > 0) {
            buf[i++] = val % 10 + '0';
            val /= 10;
        }
        buf[i] = '\0';
        
        for(int j = 0; j < i/2; j++) {
            unsigned char tmp = buf[j];
            buf[j] = buf[i-1-j];
            buf[i-1-j] = tmp;
        }
    }
    
    LCD_Text(x, y, buf);
}

void Wait(unsigned int ms) {
    unsigned int i, j;
    for(i = 0; i < ms; i++) {
        for(j = 0; j < 114; j++);
    }
}

성능 비교 분석

구현 방식 주파수 범위 듀티 정밀도 채널 수 CPU 부하 적용 분야
STM32 하드웨어 PWM 1Hz-72MHz 16-bit(0.0015%) 최대 12채널 매우 낮음 모터 제어, LED 디밍
Arduino 하드웨어 PWM 30Hz-62KHz 8-bit(0.4%) 6채널 낮음 단순 제어, 서보 모터
8051 소프트웨어 PWM 1Hz-2KHz 8-bit(0.4%) 무제한 높음 단순 응용, 학습용
ESP32 하드웨어 PWM 1Hz-40MHz 16-bit(0.0015%) 16채널 매우 낮음 IoT 기기, 복잡한 제어

실용 회로 설계

1. PWM 신호 처리 회로

PWM 출력 → 전압 팔로워 → 저대역 통과 필터 → 전원 증폭 → 부하

2. 일반적인 적용 회로

; LED 디밍 회로
VCC ──┬── LED ──┬── 제한 저항 ── PWM 핀
      │         │
     캐패시터  NPN 트랜지스터
      │         │
     GND       GND

; 모터 구동 회로
PWM ── 74HC14(슈미트 트리거) ── IR2104(하프 브릿지 드라이버) ── MOSFET ── 모터

응용 예제

1. LED 브레스 램프

// STM32 브레스 LED 구현
void Breathe_LED(TIM_HandleTypeDef* htim, uint32_t channel) {
    static uint16_t pwm_level = 0;
    static int8_t direction = 1;
    
    pwm_level += direction;
    
    if(pwm_level >= 10000) direction = -1;
    if(pwm_level <= 0) direction = 1;
    
    __HAL_TIM_SET_COMPARE(htim, channel, pwm_level);
    HAL_Delay(1);
}

2. 서보 모터 제어

// 서보 모터 각도 제어 (0-180도 → 0.5ms-2.5ms 펄스)
void Servo_Angle(TIM_HandleTypeDef* htim, uint32_t channel, uint8_t angle) {
    uint32_t pulse_us = 500 + (angle * 2000 / 180);
    
    __HAL_TIM_SET_COMPARE(htim, channel, pulse_us);
}

3. DC 모터 속도 제어

// 모터 정회전/역회전 및 속도 제어
typedef enum {
    MOTOR_HALT = 0,
    MOTOR_CW,
    MOTOR_CCW
} MotorDir;

void Motor_Drive(TIM_HandleTypeDef* pwm_timer, uint32_t pwm_ch,
                 GPIO_TypeDef* dir_port, uint16_t pin_fwd, uint16_t pin_rev,
                 MotorDir direction, uint16_t speed_val) {
    if(direction == MOTOR_CW) {
        HAL_GPIO_WritePin(dir_port, pin_fwd, GPIO_PIN_SET);
        HAL_GPIO_WritePin(dir_port, pin_rev, GPIO_PIN_RESET);
    } else if(direction == MOTOR_CCW) {
        HAL_GPIO_WritePin(dir_port, pin_fwd, GPIO_PIN_RESET);
        HAL_GPIO_WritePin(dir_port, pin_rev, GPIO_PIN_SET);
    } else {
        HAL_GPIO_WritePin(dir_port, pin_fwd, GPIO_PIN_RESET);
        HAL_GPIO_WritePin(dir_port, pin_rev, GPIO_PIN_RESET);
    }
    
    __HAL_TIM_SET_COMPARE(pwm_timer, pwm_ch, speed_val);
}

디버깅 기법

  1. 오실로스코프 활용: PWM 파형 관찰, 주파수 및 듀티 사이클 측정
  2. 멀티미터 활용: 평균 전압 측정으로 듀티 사이클 검증
  3. 소프트웨어 디버깅: 직렬 통신을 통한 현재 PWM 파라미터 출력
  4. 점진적 조정: 저주파수부터 시작하여 점차 주파수 상승하며 테스트

태그: STM32 Arduino 8051 PWM 마이크로컨트롤러

6월 12일 18:06에 게시됨