기초 이론
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", ¶m);
}
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);
}
디버깅 기법
- 오실로스코프 활용: PWM 파형 관찰, 주파수 및 듀티 사이클 측정
- 멀티미터 활용: 평균 전압 측정으로 듀티 사이클 검증
- 소프트웨어 디버깅: 직렬 통신을 통한 현재 PWM 파라미터 출력
- 점진적 조정: 저주파수부터 시작하여 점차 주파수 상승하며 테스트