
PWM은 디지털 출력으로 아날로그 효과를 만들어내는 핵심 기술입니다. LED 밝기 조절, 모터 속도 제어, 서보 모터 제어, 오디오 출력 등 다양한 분야에서 사용됩니다.
PWM은 전압의 높낮이를 조절하는 것이 아닌, 전기를 켜는 시간(On)과 끄는 시간(Off)의 비율을 아주 빠르게 조절하는 방식으로, 한 주기에 스위치가 얼마나 오래 켜져있는지에 따라 출력의 세기가 결정되는 방법입니다. 이 때 관여하는 인자들은 다음과 같습니다.
주기(Period): 신호가 한 번 ON/OFF를 반복하는 전체 시간입니다. 펄스 폭(Pulse Width): 한 주기 내에서 신호가 'ON' 상태로 유지되는 시간입니다. 듀티 사이클(Duty Cycle): 주기 대비 펄스 폭의 비율을 퍼센트(%)로 나타낸 것입니다.
PWM의 기본 원리
HIGH (100% 밝기) ████████████████████████████
PWM 75% ██████████████████░░░░░░░░░░
PWM 50% ████████████░░░░░░░░░░░░░░░░
PWM 25% ██████░░░░░░░░░░░░░░░░░░░░░░
LOW (0% 밝기) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░
◀────── 1 Period ──────▶
PWM 파라미터
주기 (Period): 신호가 반복되는 시간
주파수 (Frequency): 1 / Period (Hz)
Duty Cycle: HIGH 상태 비율 (%)
펄스 폭 (Pulse Width): HIGH 상태 시간
Duty Cycle 계산
Duty Cycle (%) = (Pulse Width / Period) × 100
예: Period = 1ms, Pulse Width = 0.5ms
Duty Cycle = (0.5 / 1.0) × 100 = 50%
사람의 눈은 빠른 깜박임을 평균 밝기로 인식합니다. 이를 활용하여 디지털 신호로 아날로그 효과를 만듭니다.
시간 평균 전압
PWM 100%: 평균 전압 = 3.3V → 최대 밝기
PWM 75%: 평균 전압 = 2.48V → 75% 밝기
PWM 50%: 평균 전압 = 1.65V → 50% 밝기
PWM 25%: 평균 전압 = 0.83V → 25% 밝기
PWM 0%: 평균 전압 = 0V → 꺼짐
깜박임 없는 조건
주파수 > 100Hz (Period < 10ms)
- 100Hz: 사람 눈의 한계 (깜박임 감지 가능)
- 1kHz: 부드러운 밝기 조절 (권장)
- 10kHz 이상: 모터 제어에 적합
| 항목 | 하드웨어 PWM | 소프트웨어 PWM |
|---|---|---|
| 구현 | 타이머 하드웨어 | 인터럽트/루프 |
| 정확도 | 매우 높음 | 낮음 (지터 발생) |
| CPU 부하 | 거의 없음 | 높음 |
| 채널 수 | 제한적 (타이머 채널) | 무제한 (이론상) |
| 주파수 | 매우 높음 가능 | 낮음 (~1kHz) |
| 사용 예 | LED, 모터, 서보 | 간단한 LED |
STM32의 타이머를 이용해 앞서 배운 타임아웃/인터럽트 제어 뿐만 아니라 PWM 출력 기능도 제어할 수 있습니다. STM32에서 PWM을 만들 때는 CCR(Capture/Compare Register)이라는 레지스터를 사용하며 다음과 같은 로직으로 작동합니다.
ARR: 전체 주기 (예: 1000까지 세기) CCR: 켜져 있을 시간 (예: 500으로 설정) 동작: CNT가 0부터 올라가다가 **CCR(500)**보다 작으면 HIGH, 크면 LOW를 출력합니다. 이렇게 하면 정확히 50% Duty Cycle이 만들어집니다.
PWM 생성 과정
Timer Counter: 0 → 1 → 2 → ... → CCRx → ... → ARR
↑ ↑ ↑
시작(LOW) HIGH→LOW 다시 시작(ARR+1)
출력: ████████████████░░░░░░░░░░░░
0 CCRx ARR
Duty Cycle = CCRx / ARR × 100%
PWM Mode 1 vs Mode 2
PWM Mode 1 (일반적):
Counter < CCRx: 출력 = HIGH
Counter ≥ CCRx: 출력 = LOW
PWM Mode 2 (반대):
Counter < CCRx: 출력 = LOW
Counter ≥ CCRx: 출력 = HIGH
STM32F407 타이머 채널
TIM1: CH1-CH4 (고급 타이머)
TIM2: CH1-CH4 (32비트)
TIM3: CH1-CH4 (범용)
TIM4: CH1-CH4 (범용)
TIM5: CH1-CH4 (32비트)
...
GPIO 핀 매핑 (예: TIM4)
//TIM4의 멀티 채널을 이용해 각기 다른 LED 제어
TIM4_CH1: PD12 (LED4 - Green)
TIM4_CH2: PD13 (LED3 - Orange)
TIM4_CH3: PD14 (LED5 - Red)
TIM4_CH4: PD15 (LED6 - Blue)
주파수 vs 해상도 트레이드오프
우선순위별 PWM 설정 전략
| 우선순위 | 용도 (예시) | 특징 |
|---|---|---|
| 고주파 우선 | 모터 제어, 스위칭 파워(SMPS) | 가청 주파수 소음(20Hz~20kHz)을 피하기 위해 보통 20kHz 이상의 높은 주파수를 사용합니다. 주기가 짧아지는 만큼 카운팅할 수 있는 단계(해상도)는 상대적으로 줄어듭니다. |
| 고해상도 우선 | LED 밝기 조절 (Dimming), 오디오 DAC | 사람이 인지하기 힘든 수준의 아주 미세한 밝기 변화나 부드러운 아날로그 파형을 구현하는 것이 목적입니다. 주파수를 낮추는 대신 한 주기 내의 제어 단계(Resolution)를 최대한 늘려 정밀도를 확보합니다. |
Timer Clock = 84MHz
ARR = 999 → 주파수 = 84kHz, 해상도 = 1000 steps
ARR = 99 → 주파수 = 840kHz, 해상도 = 100 steps
ARR = 9 → 주파수 = 8.4MHz, 해상도 = 10 steps
일반적인 설정 (LED):
주파수 = 1kHz ~ 10kHz
해상도 = 256 ~ 1000 steps
계산 공식
PWM_Frequency = Timer_Clock / ((PSC + 1) × (ARR + 1))
Resolution = ARR + 1
예: 1kHz, 1000 steps
84MHz / ((0 + 1) × (83999 + 1)) = 1kHz
Resolution = 84000 steps
또는
84MHz / ((83 + 1) × (999 + 1)) = 1kHz
Resolution = 1000 steps (더 실용적)
STM32CubeMX에서 PWM을 설정하고 기본적인 밝기 제어를 구현합니다.
Step 1: GPIO 및 타이머 설정
Pinout & Configuration:
1. GPIO 확인
PD12: TIM4_CH1 (LED4)
PD13: TIM4_CH2 (LED3)
PD14: TIM4_CH3 (LED5)
PD15: TIM4_CH4 (LED6)
2. Timers → TIM4 선택
3. Channel 설정
Channel1: PWM Generation CH1
Channel2: PWM Generation CH2
Channel3: PWM Generation CH3
Channel4: PWM Generation CH4
Step 2: 타이머 파라미터 설정 (1kHz, 1000 steps)
Configuration → TIM4
Parameter Settings:
Prescaler (PSC): 83
Counter Mode: Up
Counter Period (ARR): 999
Internal Clock Division: No Division
Auto-reload preload: Enable
계산 확인:
Timer Clock = 84MHz
Counter Clock = 84MHz / (83 + 1) = 1MHz
PWM Frequency = 1MHz / (999 + 1) = 1kHz ✓
Resolution = 1000 steps ✓
Step 3: PWM 채널 설정
PWM Generation Channel 1-4:
Mode: PWM mode 1
Pulse (CCR): 0 (초기값)
Output compare preload: Enable
Fast Mode: Disable
CH Polarity: High
Step 4: 코드 생성
Project Manager → Generate Code
/* TIM4 init function */
void MX_TIM4_Init(void)
{
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
TIM_OC_InitTypeDef sConfigOC = {0};
htim4.Instance = TIM4;
htim4.Init.Prescaler = 83;
htim4.Init.CounterMode = TIM_COUNTERMODE_UP;
htim4.Init.Period = 999;
htim4.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim4.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
if (HAL_TIM_Base_Init(&htim4) != HAL_OK)
{
Error_Handler();
}
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
if (HAL_TIM_ConfigClockSource(&htim4, &sClockSourceConfig) != HAL_OK)
{
Error_Handler();
}
if (HAL_TIM_PWM_Init(&htim4) != HAL_OK)
{
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim4, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 0;
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
// Channel 1-4 설정
if (HAL_TIM_PWM_ConfigChannel(&htim4, &sConfigOC, TIM_CHANNEL_1) != HAL_OK)
{
Error_Handler();
}
if (HAL_TIM_PWM_ConfigChannel(&htim4, &sConfigOC, TIM_CHANNEL_2) != HAL_OK)
{
Error_Handler();
}
if (HAL_TIM_PWM_ConfigChannel(&htim4, &sConfigOC, TIM_CHANNEL_3) != HAL_OK)
{
Error_Handler();
}
if (HAL_TIM_PWM_ConfigChannel(&htim4, &sConfigOC, TIM_CHANNEL_4) != HAL_OK)
{
Error_Handler();
}
}
PWM 시작
/* USER CODE BEGIN 2 */
// 모든 채널 PWM 시작
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_2);
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_3);
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_4);
printf("PWM started on all channels\r\n");
/* USER CODE END 2 */
Duty Cycle 설정 방법
/* USER CODE BEGIN 0 */
// 방법 1: 직접 CCR 레지스터 설정 -> 성능 및 정밀도 위주
void set_pwm_duty(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t duty)
{
// duty: 0 ~ 999 (0% ~ 100%)
__HAL_TIM_SET_COMPARE(htim, channel, duty);
}
// 방법 2: 백분율로 설정 -> 사용자 편의 위주
void set_pwm_percent(TIM_HandleTypeDef *htim, uint32_t channel, uint8_t percent)
{
// percent: 0 ~ 100
if (percent > 100) percent = 100;
uint16_t duty = (uint16_t)((percent * (htim->Init.Period + 1)) / 100);
__HAL_TIM_SET_COMPARE(htim, channel, duty);
}
// 방법 3: 8비트 값으로 설정 (0-255) -> 프로토콜 위주
void set_pwm_8bit(TIM_HandleTypeDef *htim, uint32_t channel, uint8_t value)
{
// value: 0 ~ 255
uint16_t duty = (uint16_t)((value * (htim->Init.Period + 1)) / 256);
__HAL_TIM_SET_COMPARE(htim, channel, duty);
}
/* USER CODE END 0 */
기본 사용 예제
/* USER CODE BEGIN 3 */
while (1)
{
// LED1: 25% 밝기
set_pwm_percent(&htim4, TIM_CHANNEL_1, 25);
// LED2: 50% 밝기
set_pwm_percent(&htim4, TIM_CHANNEL_2, 50);
// LED3: 75% 밝기
set_pwm_percent(&htim4, TIM_CHANNEL_3, 75);
// LED4: 100% 밝기
set_pwm_percent(&htim4, TIM_CHANNEL_4, 100);
HAL_Delay(2000);
// 모두 끄기
set_pwm_percent(&htim4, TIM_CHANNEL_1, 0);
set_pwm_percent(&htim4, TIM_CHANNEL_2, 0);
set_pwm_percent(&htim4, TIM_CHANNEL_3, 0);
set_pwm_percent(&htim4, TIM_CHANNEL_4, 0);
HAL_Delay(1000);
}
/* USER CODE END 3 */
선형 페이드
/* USER CODE BEGIN 0 */
void fade_in(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t duration_ms)
{
uint16_t arr = htim->Init.Period; //ARR
uint16_t steps = arr + 1;
uint16_t delay_per_step = duration_ms / steps;
for (uint16_t i = 0; i <= arr; i++) //CCR
{
__HAL_TIM_SET_COMPARE(htim, channel, i);
HAL_Delay(delay_per_step);
}
}
void fade_out(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t duration_ms)
{
uint16_t arr = htim->Init.Period;
uint16_t steps = arr + 1;
uint16_t delay_per_step = duration_ms / steps;
for (int16_t i = arr; i >= 0; i--)
{
__HAL_TIM_SET_COMPARE(htim, channel, i);
HAL_Delay(delay_per_step);
}
}
/* USER CODE END 0 */
/* USER CODE BEGIN 3 */
while (1)
{
fade_in(&htim4, TIM_CHANNEL_1, 1000); // 1초 동안 켜짐
HAL_Delay(500);
fade_out(&htim4, TIM_CHANNEL_1, 1000); // 1초 동안 꺼짐
HAL_Delay(500);
}
/* USER CODE END 3 */
부드러운 페이드 (지수 곡선)
/* USER CODE BEGIN 0 */
#include <math.h>
void fade_in_smooth(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t duration_ms)
{
uint16_t arr = htim->Init.Period;
uint8_t steps = 100;
uint16_t delay_per_step = duration_ms / steps;
for (uint8_t i = 0; i <= steps; i++)
{
// 지수 곡선 (사람 눈의 비선형 특성 반영)
float normalized = (float)i / steps;
float exponential = normalized * normalized; // x^2
uint16_t duty = (uint16_t)(exponential * arr);
__HAL_TIM_SET_COMPARE(htim, channel, duty);
HAL_Delay(delay_per_step);
}
}
void fade_out_smooth(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t duration_ms)
{
uint16_t arr = htim->Init.Period;
uint8_t steps = 100;
uint16_t delay_per_step = duration_ms / steps;
for (int8_t i = steps; i >= 0; i--)
{
float normalized = (float)i / steps;
float exponential = normalized * normalized;
uint16_t duty = (uint16_t)(exponential * arr);
__HAL_TIM_SET_COMPARE(htim, channel, duty);
HAL_Delay(delay_per_step);
}
}
/* USER CODE END 0 */
사인파 호흡
/* USER CODE BEGIN 0 */
#include <math.h>
#define PI 3.14159265f
void breathing_effect(TIM_HandleTypeDef *htim, uint32_t channel,
uint16_t period_ms, uint8_t min_brightness, uint8_t max_brightness)
{
uint16_t arr = htim->Init.Period;
uint16_t steps = 100;
uint16_t delay_per_step = period_ms / steps;
for (uint16_t i = 0; i < steps; i++)
{
// 사인파: 0 → 1 → 0
float angle = (float)i * 2.0f * PI / steps;
float sine_value = (sinf(angle) + 1.0f) / 2.0f; // 0.0 ~ 1.0
// 최소/최대 밝기 범위 적용
uint8_t brightness = min_brightness +
(uint8_t)(sine_value * (max_brightness - min_brightness));
uint16_t duty = (uint16_t)((brightness * arr) / 100);
__HAL_TIM_SET_COMPARE(htim, channel, duty);
HAL_Delay(delay_per_step);
}
}
/* USER CODE END 0 */
/* USER CODE BEGIN 3 */
while (1)
{
// 2초 주기로 10% ~ 100% 사이에서 호흡
breathing_effect(&htim4, TIM_CHANNEL_1, 2000, 10, 100);
}
/* USER CODE END 3 */
다중 채널 호흡 (위상차)
/* USER CODE BEGIN 0 */
void multi_breathing(TIM_HandleTypeDef *htim, uint16_t period_ms)
{
uint16_t arr = htim->Init.Period;
uint16_t steps = 100;
uint16_t delay_per_step = period_ms / steps;
for (uint16_t i = 0; i < steps; i++)
{
// CH1: 0도
float angle1 = (float)i * 2.0f * PI / steps;
float brightness1 = (sinf(angle1) + 1.0f) / 2.0f;
// CH2: 90도 위상차
float angle2 = angle1 + PI / 2.0f;
float brightness2 = (sinf(angle2) + 1.0f) / 2.0f;
// CH3: 180도 위상차
float angle3 = angle1 + PI;
float brightness3 = (sinf(angle3) + 1.0f) / 2.0f;
// CH4: 270도 위상차
float angle4 = angle1 + 3.0f * PI / 2.0f;
float brightness4 = (sinf(angle4) + 1.0f) / 2.0f;
__HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_1, (uint16_t)(brightness1 * arr));
__HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_2, (uint16_t)(brightness2 * arr));
__HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_3, (uint16_t)(brightness3 * arr));
__HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_4, (uint16_t)(brightness4 * arr));
HAL_Delay(delay_per_step);
}
}
/* USER CODE END 0 */
빠른 플래시
/* USER CODE BEGIN 0 */
void pulse_effect(TIM_HandleTypeDef *htim, uint32_t channel,
uint8_t pulses, uint16_t pulse_duration_ms)
{
uint16_t arr = htim->Init.Period;
for (uint8_t i = 0; i < pulses; i++)
{
// 즉시 켜기
__HAL_TIM_SET_COMPARE(htim, channel, arr);
HAL_Delay(pulse_duration_ms);
// 즉시 끄기
__HAL_TIM_SET_COMPARE(htim, channel, 0);
HAL_Delay(pulse_duration_ms);
}
}
/* USER CODE END 0 */
/* USER CODE BEGIN 3 */
while (1)
{
pulse_effect(&htim4, TIM_CHANNEL_1, 3, 100); // 3번 깜박임
HAL_Delay(1000);
}
/* USER CODE END 3 */
하트비트 효과
/* USER CODE BEGIN 0 */
void heartbeat_effect(TIM_HandleTypeDef *htim, uint32_t channel)
{
uint16_t arr = htim->Init.Period;
// 첫 번째 비트
for (uint16_t i = 0; i <= arr; i += 50)
{
__HAL_TIM_SET_COMPARE(htim, channel, i);
HAL_Delay(5);
}
for (uint16_t i = arr; i > 0; i -= 50)
{
__HAL_TIM_SET_COMPARE(htim, channel, i);
HAL_Delay(5);
}
HAL_Delay(100);
// 두 번째 비트
for (uint16_t i = 0; i <= arr; i += 50)
{
__HAL_TIM_SET_COMPARE(htim, channel, i);
HAL_Delay(5);
}
for (uint16_t i = arr; i > 0; i -= 50)
{
__HAL_TIM_SET_COMPARE(htim, channel, i);
HAL_Delay(5);
}
HAL_Delay(800); // 긴 휴지기
}
/* USER CODE END 0 */
RGB LED 색상 전환
/* USER CODE BEGIN 0 */
typedef struct {
uint8_t r;
uint8_t g;
uint8_t b;
} RGB_t;
void set_rgb(TIM_HandleTypeDef *htim, RGB_t color)
{
uint16_t arr = htim->Init.Period;
// R: CH1, G: CH2, B: CH3
__HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_1, (color.r * arr) / 255);
__HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_2, (color.g * arr) / 255);
__HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_3, (color.b * arr) / 255);
}
void rainbow_effect(TIM_HandleTypeDef *htim, uint16_t duration_ms)
{
RGB_t colors[] = {
{255, 0, 0}, // 빨강
{255, 127, 0}, // 주황
{255, 255, 0}, // 노랑
{0, 255, 0}, // 초록
{0, 0, 255}, // 파랑
{75, 0, 130}, // 남색
{148, 0, 211} // 보라
};
uint8_t num_colors = sizeof(colors) / sizeof(RGB_t);
for (uint8_t i = 0; i < num_colors; i++)
{
set_rgb(htim, colors[i]);
HAL_Delay(duration_ms);
}
}
void smooth_rainbow(TIM_HandleTypeDef *htim, uint16_t cycle_duration_ms)
{
uint16_t steps = 360; // 색상환 360도
uint16_t delay_per_step = cycle_duration_ms / steps;
for (uint16_t i = 0; i < steps; i++)
{
RGB_t color = hsv_to_rgb(i, 100, 100); // HSV → RGB 변환
set_rgb(htim, color);
HAL_Delay(delay_per_step);
}
}
// HSV to RGB 변환 함수
RGB_t hsv_to_rgb(uint16_t h, uint8_t s, uint8_t v)
{
RGB_t rgb;
uint8_t region = h / 43;
uint8_t remainder = (h - (region * 43)) * 6;
uint8_t p = (v * (255 - s)) >> 8;
uint8_t q = (v * (255 - ((s * remainder) >> 8))) >> 8;
uint8_t t = (v * (255 - ((s * (255 - remainder)) >> 8))) >> 8;
switch (region)
{
case 0: rgb.r = v; rgb.g = t; rgb.b = p; break;
case 1: rgb.r = q; rgb.g = v; rgb.b = p; break;
case 2: rgb.r = p; rgb.g = v; rgb.b = t; break;
case 3: rgb.r = p; rgb.g = q; rgb.b = v; break;
case 4: rgb.r = t; rgb.g = p; rgb.b = v; break;
default: rgb.r = v; rgb.g = p; rgb.b = q; break;
}
return rgb;
}
/* USER CODE END 0 */
타이머 인터럽트를 사용하여 논블로킹 LED 효과를 구현합니다.
타이머 인터럽트 기반 페이드
/* USER CODE BEGIN 0 */
typedef struct {
uint16_t current_duty;
uint16_t target_duty;
int16_t step;
uint8_t active;
} PWM_Fade_t;
PWM_Fade_t fade_ch1 = {0};
void start_fade(PWM_Fade_t *fade, uint16_t from, uint16_t to, uint16_t duration_ms)
{
fade->current_duty = from;
fade->target_duty = to;
// 1ms마다 호출되므로 duration_ms 스텝
int32_t total_change = (int32_t)to - (int32_t)from;
fade->step = total_change / (int16_t)duration_ms;
fade->active = 1;
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM6) // 1ms 타이머
{
if (fade_ch1.active)
{
fade_ch1.current_duty += fade_ch1.step;
// 목표 도달 확인
if ((fade_ch1.step > 0 && fade_ch1.current_duty >= fade_ch1.target_duty) ||
(fade_ch1.step < 0 && fade_ch1.current_duty <= fade_ch1.target_duty))
{
fade_ch1.current_duty = fade_ch1.target_duty;
fade_ch1.active = 0;
}
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, fade_ch1.current_duty);
}
}
}
/* USER CODE END 0 */
/* USER CODE BEGIN 2 */
// TIM6: 1ms 인터럽트 타이머
HAL_TIM_Base_Start_IT(&htim6);
// TIM4: PWM 출력
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_1);
/* USER CODE END 2 */
/* USER CODE BEGIN 3 */
while (1)
{
// 페이드 인 시작 (1초 동안)
start_fade(&fade_ch1, 0, 999, 1000);
// 페이드 진행 중 다른 작업 가능
while (fade_ch1.active)
{
printf("Fading... Duty: %d\r\n", fade_ch1.current_duty);
HAL_Delay(100);
}
HAL_Delay(500);
// 페이드 아웃
start_fade(&fade_ch1, 999, 0, 1000);
while (fade_ch1.active)
{
HAL_Delay(100);
}
HAL_Delay(500);
}
/* USER CODE END 3 */
4개 채널 동시 페이드
/* USER CODE BEGIN 0 */
#define NUM_PWM_CHANNELS 4
PWM_Fade_t fades[NUM_PWM_CHANNELS] = {0};
uint32_t pwm_channels[NUM_PWM_CHANNELS] = {
TIM_CHANNEL_1, TIM_CHANNEL_2, TIM_CHANNEL_3, TIM_CHANNEL_4
};
void start_fade_multi(uint8_t channel, uint16_t from, uint16_t to, uint16_t duration_ms)
{
if (channel < NUM_PWM_CHANNELS)
{
start_fade(&fades[channel], from, to, duration_ms);
}
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM6)
{
for (uint8_t i = 0; i < NUM_PWM_CHANNELS; i++)
{
if (fades[i].active)
{
fades[i].current_duty += fades[i].step;
if ((fades[i].step > 0 && fades[i].current_duty >= fades[i].target_duty) ||
(fades[i].step < 0 && fades[i].current_duty <= fades[i].target_duty))
{
fades[i].current_duty = fades[i].target_duty;
fades[i].active = 0;
}
__HAL_TIM_SET_COMPARE(&htim4, pwm_channels[i], fades[i].current_duty);
}
}
}
}
/* USER CODE END 0 */
/* USER CODE BEGIN 3 */
while (1)
{
// 각 채널 순차적으로 페이드 인
start_fade_multi(0, 0, 999, 500);
HAL_Delay(250);
start_fade_multi(1, 0, 999, 500);
HAL_Delay(250);
start_fade_multi(2, 0, 999, 500);
HAL_Delay(250);
start_fade_multi(3, 0, 999, 500);
HAL_Delay(2000);
// 동시에 페이드 아웃
for (uint8_t i = 0; i < NUM_PWM_CHANNELS; i++)
{
start_fade_multi(i, 999, 0, 1000);
}
HAL_Delay(2000);
}
/* USER CODE END 3 */
/* USER CODE BEGIN 0 */
typedef enum {
LED_SEQ_IDLE,
LED_SEQ_FADE_IN,
LED_SEQ_HOLD_HIGH,
LED_SEQ_FADE_OUT,
LED_SEQ_HOLD_LOW
} LED_Sequence_State_t;
typedef struct {
LED_Sequence_State_t state;
uint16_t counter;
uint16_t current_brightness;
} LED_Sequence_t;
LED_Sequence_t led_seq = {LED_SEQ_IDLE, 0, 0};
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM6)
{
switch (led_seq.state)
{
case LED_SEQ_IDLE:
// 아무것도 안 함
break;
case LED_SEQ_FADE_IN:
led_seq.current_brightness++;
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, led_seq.current_brightness);
if (led_seq.current_brightness >= 999)
{
led_seq.state = LED_SEQ_HOLD_HIGH;
led_seq.counter = 0;
}
break;
case LED_SEQ_HOLD_HIGH:
led_seq.counter++;
if (led_seq.counter >= 1000) // 1초 유지
{
led_seq.state = LED_SEQ_FADE_OUT;
}
break;
case LED_SEQ_FADE_OUT:
led_seq.current_brightness--;
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, led_seq.current_brightness);
if (led_seq.current_brightness == 0)
{
led_seq.state = LED_SEQ_HOLD_LOW;
led_seq.counter = 0;
}
break;
case LED_SEQ_HOLD_LOW:
led_seq.counter++;
if (led_seq.counter >= 500) // 0.5초 유지
{
led_seq.state = LED_SEQ_FADE_IN; // 다시 시작
}
break;
}
}
}
void start_led_sequence(void)
{
led_seq.state = LED_SEQ_FADE_IN;
led_seq.counter = 0;
led_seq.current_brightness = 0;
}
void stop_led_sequence(void)
{
led_seq.state = LED_SEQ_IDLE;
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, 0);
}
/* USER CODE END 0 */
사람의 눈은 밝기를 비선형적으로 인식합니다. 감마 보정을 통해 선형 값을 시각적으로 균일한 밝기로 변환합니다.
/* USER CODE BEGIN 0 */
// 감마 보정 룩업 테이블 (8bit → 10bit)
const uint16_t gamma_table[256] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1,
2, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 8, 8, 9, 10, 11,
12, 13, 15, 16, 17, 18, 20, 21, 23, 24, 26, 27, 29, 31, 33, 35,
37, 39, 41, 43, 45, 48, 50, 52, 55, 57, 60, 62, 65, 68, 71, 74,
77, 80, 83, 86, 90, 93, 97, 100, 104, 108, 112, 116, 120, 124, 128, 132,
137, 141, 146, 151, 155, 160, 165, 170, 175, 181, 186, 191, 197, 202, 208, 214,
220, 225, 231, 237, 244, 250, 256, 263, 269, 276, 283, 289, 296, 303, 310, 318,
325, 332, 340, 347, 355, 363, 371, 379, 387, 395, 403, 411, 420, 428, 437, 446,
454, 463, 472, 481, 490, 499, 508, 518, 527, 537, 546, 556, 566, 576, 586, 596,
606, 616, 627, 637, 648, 658, 669, 680, 691, 702, 713, 724, 735, 747, 758, 770,
782, 793, 805, 817, 829, 842, 854, 866, 879, 891, 904, 917, 930, 943, 956, 969,
982, 996,1009,1023,1036,1050,1064,1078,1092,1106,1121,1135,1150,1164,1179,1194,
1209,1224,1239,1254,1270,1285,1301,1316,1332,1348,1364,1380,1396,1413,1429,1446,
1462,1479,1496,1513,1530,1547,1565,1582,1599,1617,1635,1652,1670,1688,1706,1724,
1743,1761,1780,1798,1817,1836,1855,1874,1893,1912,1932,1951,1971,1991,2010,2030,
2051,2071,2091,2112,2132,2153,2174,2195,2216,2237,2258,2280,2301,2323,2345,2366
};
void set_led_brightness_gamma(TIM_HandleTypeDef *htim, uint32_t channel, uint8_t brightness)
{
// brightness: 0 ~ 255
uint16_t corrected = gamma_table[brightness];
// ARR=999일 때 2366을 999로 스케일링
uint16_t duty = (corrected * 999) / 2366;
__HAL_TIM_SET_COMPARE(htim, channel, duty);
}
/* USER CODE END 0 */
/* USER CODE BEGIN 3 */
while (1)
{
// 감마 보정 없이
printf("Linear fade:\r\n");
for (uint16_t i = 0; i <= 255; i++)
{
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, (i * 999) / 255);
HAL_Delay(10);
}
HAL_Delay(1000);
// 감마 보정 적용
printf("Gamma corrected fade:\r\n");
for (uint16_t i = 0; i <= 255; i++)
{
set_led_brightness_gamma(&htim4, TIM_CHANNEL_2, i);
HAL_Delay(10);
}
HAL_Delay(1000);
}
/* USER CODE END 3 */
/* USER CODE BEGIN 0 */
void change_pwm_frequency(TIM_HandleTypeDef *htim, uint32_t frequency_hz)
{
// 타이머 중지
HAL_TIM_PWM_Stop(htim, TIM_CHANNEL_ALL);
// 새로운 주파수 계산
uint32_t timer_clock = 84000000; // 84MHz
uint32_t prescaler = 0;
uint32_t period;
// 적절한 prescaler 찾기
for (prescaler = 0; prescaler < 65536; prescaler++)
{
period = (timer_clock / ((prescaler + 1) * frequency_hz)) - 1;
if (period < 65536) // 16비트 범위 내
{
break;
}
}
// 파라미터 업데이트
__HAL_TIM_SET_PRESCALER(htim, prescaler);
__HAL_TIM_SET_AUTORELOAD(htim, period);
// Duty cycle 재계산 (50% 유지)
__HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_1, period / 2);
// 타이머 재시작
HAL_TIM_PWM_Start(htim, TIM_CHANNEL_1);
printf("PWM Frequency changed to %lu Hz\r\n", frequency_hz);
printf(" PSC: %lu, ARR: %lu\r\n", prescaler, period);
}
/* USER CODE END 0 */
부저로 음계 출력
/* USER CODE BEGIN 0 */
// 음계 주파수 (Hz)
#define NOTE_C4 262
#define NOTE_D4 294
#define NOTE_E4 330
#define NOTE_F4 349
#define NOTE_G4 392
#define NOTE_A4 440
#define NOTE_B4 494
#define NOTE_C5 523
typedef struct {
uint16_t frequency;
uint16_t duration_ms;
} Note_t;
void play_tone(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t frequency, uint16_t duration_ms)
{
if (frequency == 0)
{
// 쉼표
HAL_TIM_PWM_Stop(htim, channel);
}
else
{
// 주파수 설정
uint32_t timer_clock = 84000000;
uint32_t period = (timer_clock / frequency) - 1;
__HAL_TIM_SET_AUTORELOAD(htim, period);
__HAL_TIM_SET_COMPARE(htim, channel, period / 2); // 50% duty
HAL_TIM_PWM_Start(htim, channel);
}
HAL_Delay(duration_ms);
HAL_TIM_PWM_Stop(htim, channel);
}
void play_melody(TIM_HandleTypeDef *htim, uint32_t channel, Note_t *notes, uint8_t length)
{
for (uint8_t i = 0; i < length; i++)
{
play_tone(htim, channel, notes[i].frequency, notes[i].duration_ms);
HAL_Delay(50); // 음 사이 간격
}
}
/* USER CODE END 0 */
/* USER CODE BEGIN 3 */
// 도레미파솔라시도
Note_t scale[] = {
{NOTE_C4, 500},
{NOTE_D4, 500},
{NOTE_E4, 500},
{NOTE_F4, 500},
{NOTE_G4, 500},
{NOTE_A4, 500},
{NOTE_B4, 500},
{NOTE_C5, 500}
};
while (1)
{
play_melody(&htim4, TIM_CHANNEL_1, scale, 8);
HAL_Delay(2000);
}
/* USER CODE END 3 */
버튼을 누를 때마다 LED 밝기가 0% → 33% → 66% → 100% → 0% 순서로 변경되도록 구현하세요.
요구사항
힌트
typedef enum {
BRIGHTNESS_OFF = 0,
BRIGHTNESS_LOW = 33,
BRIGHTNESS_MID = 66,
BRIGHTNESS_HIGH = 100
} Brightness_Level_t;
Brightness_Level_t current_level = BRIGHTNESS_OFF;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == BUTTON_PIN)
{
// 다음 밝기 레벨로 전환
switch (current_level)
{
case BRIGHTNESS_OFF: current_level = BRIGHTNESS_LOW; break;
case BRIGHTNESS_LOW: current_level = BRIGHTNESS_MID; break;
case BRIGHTNESS_MID: current_level = BRIGHTNESS_HIGH; break;
case BRIGHTNESS_HIGH: current_level = BRIGHTNESS_OFF; break;
}
// 페이드 시작
start_fade(&fade_ch1, fade_ch1.current_duty,
(current_level * 999) / 100, 500);
}
}
3개의 PWM 채널(R, G, B)을 사용하여 다양한 색상을 만드세요.
요구사항
힌트
RGB_t colors[] = {
{255, 0, 0}, // 빨강
{0, 255, 0}, // 초록
{0, 0, 255}, // 파랑
{255, 255, 0}, // 노랑
{0, 255, 255}, // 청록
{255, 0, 255}, // 자홍
{255, 255, 255} // 흰색
};
void transition_color(RGB_t from, RGB_t to, uint16_t duration_ms)
{
uint16_t steps = 100;
uint16_t delay_per_step = duration_ms / steps;
for (uint16_t i = 0; i <= steps; i++)
{
RGB_t current;
current.r = from.r + ((to.r - from.r) * i) / steps;
current.g = from.g + ((to.g - from.g) * i) / steps;
current.b = from.b + ((to.b - from.b) * i) / steps;
set_rgb(&htim4, current);
HAL_Delay(delay_per_step);
}
}
4개의 LED를 사용하여 시간을 표시하세요.
요구사항
힌트
void update_clock_display(uint8_t hours, uint8_t minutes, uint8_t seconds)
{
// 시: 0-23을 0-100%로 매핑
uint8_t hour_brightness = (hours * 100) / 23;
set_pwm_percent(&htim4, TIM_CHANNEL_1, hour_brightness);
// 분: 0-59를 0-100%로 매핑
uint8_t minute_brightness = (minutes * 100) / 59;
set_pwm_percent(&htim4, TIM_CHANNEL_2, minute_brightness);
// 초: 0-59를 0-100%로 매핑
uint8_t second_brightness = (seconds * 100) / 59;
set_pwm_percent(&htim4, TIM_CHANNEL_3, second_brightness);
// 초 표시 LED는 1Hz로 깜박임
if (seconds % 2 == 0)
set_pwm_percent(&htim4, TIM_CHANNEL_4, 100);
else
set_pwm_percent(&htim4, TIM_CHANNEL_4, 0);
}
증상
LED가 전혀 켜지지 않음
원인 및 해결
// 1. PWM 시작 확인
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_1); // 호출했는지 확인
// 2. GPIO 설정 확인 (CubeMX)
// Pin Configuration:
// PD12: TIM4_CH1 (Alternate Function)
// 3. Duty Cycle 확인
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, 500); // 0이 아닌 값
// 4. 타이머 클럭 활성화
__HAL_RCC_TIM4_CLK_ENABLE();
// 5. GPIO 클럭 활성화
__HAL_RCC_GPIOD_CLK_ENABLE();
증상
부드럽게 켜지지 않고 깜박거림
원인
PWM 주파수가 너무 낮음
해결
// 주파수를 100Hz 이상으로 설정
// 현재 설정 확인
uint32_t freq = 84000000 / ((PSC + 1) * (ARR + 1));
printf("PWM Frequency: %lu Hz\r\n", freq);
// 1kHz로 변경 (권장)
// PSC = 83, ARR = 999
// Freq = 84MHz / (84 * 1000) = 1kHz
증상
50% duty cycle인데 눈으로 보면 80%처럼 밝음
원인
사람의 눈은 밝기를 비선형적으로 인식
해결
// 감마 보정 적용
set_led_brightness_gamma(&htim4, TIM_CHANNEL_1, 128); // 50%
// 또는 제곱 함수 사용
uint8_t linear = 50; // 0-100
uint16_t corrected = (linear * linear * 999) / 10000;
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, corrected);
증상
한 채널을 변경하면 다른 채널도 영향받음
원인
모든 채널이 같은 ARR 공유
해결
// 같은 타이머의 모든 채널은 같은 주파수
// 각 채널의 CCR만 독립적으로 조절 가능
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, 250); // 25%
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_2, 500); // 50%
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_3, 750); // 75%
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_4, 999); // 100%
// 다른 주파수가 필요하면 다른 타이머 사용
1. PWM 파라미터
Frequency = Timer_Clock / ((PSC + 1) × (ARR + 1))
Duty Cycle (%) = (CCR / ARR) × 100
Resolution = ARR + 1
예: LED 제어
Frequency = 1kHz (깜박임 없음)
Resolution = 1000 steps (0.1% 단위 조절)
PSC = 83, ARR = 999
2. PWM 설정 가이드
| 용도 | 주파수 | 해상도 | 설정 예 |
|---|---|---|---|
| LED 밝기 | 1kHz | 1000 | PSC=83, ARR=999 |
| 서보 모터 | 50Hz | 1000 | PSC=83, ARR=19999 |
| DC 모터 | 20kHz | 420 | PSC=0, ARR=4199 |
| 부저 (음계) | 가변 | - | 동적 계산 |
3. 밝기 제어 방법 비교
| 방법 | 장점 | 단점 |
|---|---|---|
| 직접 CCR 설정 | 빠름 | 코드 가독성 낮음 |
| 백분율 | 직관적 | 계산 필요 |
| 8비트 값 | 다른 시스템 호환 | 정밀도 제한 |
| 감마 보정 | 시각적으로 선형 | 룩업 테이블 필요 |
TIMx_CR1: 제어 레지스터
TIMx_PSC: Prescaler
TIMx_ARR: Auto-Reload Register (Period)
TIMx_CCRx: Capture/Compare Register (Duty Cycle)
TIMx_CCMR: Capture/Compare Mode Register
TIMx_CCER: Capture/Compare Enable Register
// PWM 시작/정지
HAL_TIM_PWM_Start(&htim, channel);
HAL_TIM_PWM_Stop(&htim, channel);
// Duty Cycle 설정
__HAL_TIM_SET_COMPARE(&htim, channel, value);
// 주파수 변경
__HAL_TIM_SET_PRESCALER(&htim, psc);
__HAL_TIM_SET_AUTORELOAD(&htim, arr);
// 현재 값 읽기
uint32_t current = __HAL_TIM_GET_COMPARE(&htim, channel);
// Duty를 퍼센트로 변환
#define DUTY_TO_PERCENT(duty, arr) ((duty * 100) / (arr))
// 퍼센트를 Duty로 변환
#define PERCENT_TO_DUTY(percent, arr) ((percent * (arr)) / 100)
// 8비트를 Duty로 변환
#define BYTE_TO_DUTY(value, arr) ((value * (arr)) / 255)