STM32 #10

홍태준·2026년 1월 31일

STM32

목록 보기
10/15
post-thumbnail

Week 2 Day 5: 프로젝트: PWM 기반 부저 멜로디 재생

학습 목표

  • PWM을 이용한 부저 제어 원리를 이해한다
  • 음계와 주파수의 관계를 파악하고 구현한다
  • 타이머를 활용하여 정확한 음 길이를 제어한다
  • 멜로디 데이터 구조를 설계하고 재생 엔진을 구현한다
  • 음량 조절, 템포 변경 등 고급 기능을 추가한다

1. 부저와 소리의 이해

1.1 부저의 종류와 동작 원리

수동 부저 vs 능동 부저

능동 부저 (Active Buzzer)
- 내부에 발진 회로 포함
- 전원만 인가하면 고정된 주파수 출력
- 제어 불가능
- 용도: 알람, 경고음

수동 부저 (Passive Buzzer)
- 외부에서 주파수 신호 입력 필요
- PWM으로 주파수 제어
- 다양한 음 출력 가능
- 용도: 멜로디 재생, 효과음

수동 부저 동작 원리

PWM 신호 입력 → 압전 소자 진동 → 소리 발생

주파수가 높을수록 → 고음
주파수가 낮을수록 → 저음
듀티 사이클 → 음량 제어 (50% 권장)

예시:
440Hz PWM → 라(A4) 음
880Hz PWM → 라(A5) 음 (1옥타브 위)

연결 방법

STM32 PWM 출력 핀 (예: PA8, TIM1_CH1)
    ↓
[저항 100Ω] (선택사항, 전류 제한)
    ↓
부저 (+)
    ↓
부저 (-)
    ↓
GND

또는 트랜지스터 드라이버 사용:
PWM → [저항 1kΩ] → 트랜지스터 베이스
                     콜렉터 → 부저 (+)
                     이미터 → GND

1.2 음계와 주파수

12평균율 음계

서양 음악의 기본: 1옥타브 = 12개 반음

C   C#  D   D#  E   F   F#  G   G#  A   A#  B
도  도# 레  레# 미  파  파# 솔  솔# 라  라# 시

주파수 계산 공식

기준음: A4 (라) = 440Hz

반음 위 = 현재 주파수 × 2^(1/12)
반음 아래 = 현재 주파수 / 2^(1/12)

1옥타브 위 = 현재 주파수 × 2
1옥타브 아래 = 현재 주파수 / 2

예시:
A4 = 440Hz
A5 = 440Hz × 2 = 880Hz
A3 = 440Hz / 2 = 220Hz
A#4 = 440Hz × 2^(1/12) = 466.16Hz

음계 주파수 테이블 (4옥타브 기준)

// C4(도) ~ B4(시)
const uint16_t notes_freq[] = {
  262,  // C4  (도)
  277,  // C#4 (도#)
  294,  // D4  (레)
  311,  // D#4 (레#)
  330,  // E4  (미)
  349,  // F4  (파)
  370,  // F#4 (파#)
  392,  // G4  (솔)
  415,  // G#4 (솔#)
  440,  // A4  (라)
  466,  // A#4 (라#)
  494   // B4  (시)
};

1.3 음 길이와 템포

음표 길이

온음표 (Whole Note)      = 4박자
2분음표 (Half Note)      = 2박자
4분음표 (Quarter Note)   = 1박자
8분음표 (Eighth Note)    = 0.5박자
16분음표 (Sixteenth Note) = 0.25박자

점음표 (Dotted Note) = 원래 길이 × 1.5
예: 점4분음표 = 1박자 × 1.5 = 1.5박자

템포 (BPM)

BPM = Beats Per Minute (1분당 박자 수)

1박자 시간(ms) = 60000 / BPM

예시:
BPM = 120
1박자 = 60000 / 120 = 500ms
4분음표 = 500ms
8분음표 = 250ms
2분음표 = 1000ms

2. PWM 부저 제어 기본

2.1 CubeMX 설정

Timer 설정 (TIM1 예시)

1. Pinout & Configuration
   - Timers → TIM1 선택
   - Clock Source: Internal Clock
   - Channel1: PWM Generation CH1
   
2. Parameter Settings
   - Prescaler: 계산 필요 (아래 참조)
   - Counter Period (ARR): 계산 필요
   - Pulse (CCR): ARR / 2 (50% 듀티)
   
3. GPIO Settings
   - PA8: TIM1_CH1 (부저 연결)

주파수 설정 계산

Timer 주파수 = APB 클럭 / (Prescaler + 1)
출력 주파수 = Timer 주파수 / (ARR + 1)

예시: 440Hz (라) 출력
APB2 클럭 = 168MHz (TIM1은 APB2)
목표 주파수 = 440Hz

방법 1: 높은 분해능
Prescaler = 0
Timer 주파수 = 168MHz
ARR = 168,000,000 / 440 - 1 = 381,817
CCR = ARR / 2 = 190,909

방법 2: 간단한 계산
Prescaler = 168 - 1
Timer 주파수 = 1MHz
ARR = 1,000,000 / 440 - 1 = 2,272
CCR = ARR / 2 = 1,136

2.2 기본 부저 제어 함수

초기화 코드

#include "main.h"

extern TIM_HandleTypeDef htim1;

void buzzer_init(void)
{
  // PWM 시작 (초기에는 무음)
  HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
  __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 0);  // 무음
}

void buzzer_off(void)
{
  __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 0);
}

주파수 설정 함수

void buzzer_set_frequency(uint16_t frequency)
{
  if (frequency == 0)
  {
    buzzer_off();
    return;
  }
  
  // Prescaler = 168 - 1 (1MHz Timer)
  uint32_t arr = 1000000 / frequency - 1;
  uint32_t ccr = arr / 2;  // 50% 듀티
  
  __HAL_TIM_SET_AUTORELOAD(&htim1, arr);
  __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, ccr);
}

음 재생 함수

void buzzer_play_tone(uint16_t frequency, uint16_t duration_ms)
{
  buzzer_set_frequency(frequency);
  HAL_Delay(duration_ms);
  buzzer_off();
}

2.3 기본 테스트

음계 테스트

void test_scale(void)
{
  const uint16_t scale[] = {262, 294, 330, 349, 392, 440, 494, 523};
  const char* names[] = {"C", "D", "E", "F", "G", "A", "B", "C"};
  
  printf("\r\n=== Scale Test ===\r\n");
  
  for (uint8_t i = 0; i < 8; i++)
  {
    printf("%s: %d Hz\r\n", names[i], scale[i]);
    buzzer_play_tone(scale[i], 500);
    HAL_Delay(100);  // 음 사이 간격
  }
  
  printf("Complete!\r\n");
}

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  MX_TIM1_Init();
  
  buzzer_init();
  
  printf("\r\n=== Buzzer Test ===\r\n");
  
  test_scale();
  
  while (1)
  {
    HAL_Delay(1000);
  }
}

3. 음계 데이터베이스 구축

3.1 음계 정의

매크로 정의 방식

// 음표 정의
#define NOTE_C4   262
#define NOTE_CS4  277
#define NOTE_D4   294
#define NOTE_DS4  311
#define NOTE_E4   330
#define NOTE_F4   349
#define NOTE_FS4  370
#define NOTE_G4   392
#define NOTE_GS4  415
#define NOTE_A4   440
#define NOTE_AS4  466
#define NOTE_B4   494

#define NOTE_C5   523
#define NOTE_CS5  554
#define NOTE_D5   587
#define NOTE_DS5  622
#define NOTE_E5   659
#define NOTE_F5   698
#define NOTE_FS5  740
#define NOTE_G5   784
#define NOTE_GS5  831
#define NOTE_A5   880
#define NOTE_AS5  932
#define NOTE_B5   988

#define NOTE_REST 0  // 쉼표

열거형 정의 방식

typedef enum {
  NOTE_C,
  NOTE_CS,
  NOTE_D,
  NOTE_DS,
  NOTE_E,
  NOTE_F,
  NOTE_FS,
  NOTE_G,
  NOTE_GS,
  NOTE_A,
  NOTE_AS,
  NOTE_B,
  NOTE_MAX
} Note_t;

typedef enum {
  OCTAVE_3 = 0,
  OCTAVE_4 = 1,
  OCTAVE_5 = 2,
  OCTAVE_6 = 3,
  OCTAVE_MAX
} Octave_t;

// 주파수 테이블 (옥타브별)
const uint16_t note_freq_table[OCTAVE_MAX][NOTE_MAX] = {
  // Octave 3
  {131, 139, 147, 156, 165, 175, 185, 196, 208, 220, 233, 247},
  // Octave 4
  {262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494},
  // Octave 5
  {523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988},
  // Octave 6
  {1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976}
};

uint16_t get_note_frequency(Note_t note, Octave_t octave)
{
  if (note >= NOTE_MAX || octave >= OCTAVE_MAX)
    return 0;
  
  return note_freq_table[octave][note];
}

3.2 음표 길이 정의

음표 타입

typedef enum {
  DURATION_WHOLE = 4,      // 온음표
  DURATION_HALF = 2,       // 2분음표
  DURATION_QUARTER = 1,    // 4분음표
  DURATION_EIGHTH = 0,     // 8분음표 (특수 처리)
  DURATION_SIXTEENTH = 0   // 16분음표 (특수 처리)
} NoteDuration_t;

// 실제 시간 계산
uint16_t calculate_duration_ms(NoteDuration_t duration, uint16_t bpm)
{
  uint16_t quarter_note_ms = 60000 / bpm;
  
  switch (duration)
  {
    case DURATION_WHOLE:
      return quarter_note_ms * 4;
    
    case DURATION_HALF:
      return quarter_note_ms * 2;
    
    case DURATION_QUARTER:
      return quarter_note_ms;
    
    case DURATION_EIGHTH:
      return quarter_note_ms / 2;
    
    case DURATION_SIXTEENTH:
      return quarter_note_ms / 4;
    
    default:
      return quarter_note_ms;
  }
}

확장된 음표 길이

typedef struct {
  float beats;       // 박자 수 (0.25, 0.5, 1, 1.5, 2, 3, 4 등)
  uint8_t is_dotted; // 점음표 여부
} NoteLength_t;

uint16_t get_note_duration_ms(NoteLength_t length, uint16_t bpm)
{
  uint16_t quarter_ms = 60000 / bpm;
  uint16_t duration = (uint16_t)(quarter_ms * length.beats);
  
  if (length.is_dotted)
  {
    duration = (uint16_t)(duration * 1.5f);
  }
  
  return duration;
}

// 사용 예시
NoteLength_t quarter = {1.0f, 0};         // 4분음표
NoteLength_t dotted_quarter = {1.0f, 1};  // 점4분음표
NoteLength_t eighth = {0.5f, 0};          // 8분음표
NoteLength_t half = {2.0f, 0};            // 2분음표

4. 멜로디 데이터 구조

4.1 단순 구조

음표 구조체

typedef struct {
  uint16_t frequency;  // Hz
  uint16_t duration;   // ms
} MusicNote_t;

// 학교종 멜로디
const MusicNote_t school_bell[] = {
  {NOTE_G4, 500}, {NOTE_G4, 500}, {NOTE_A4, 500}, {NOTE_A4, 500},
  {NOTE_G4, 500}, {NOTE_G4, 500}, {NOTE_E4, 1000},
  {NOTE_G4, 500}, {NOTE_G4, 500}, {NOTE_E4, 500}, {NOTE_E4, 500},
  {NOTE_D4, 1500},
  {NOTE_G4, 500}, {NOTE_G4, 500}, {NOTE_A4, 500}, {NOTE_A4, 500},
  {NOTE_G4, 500}, {NOTE_G4, 500}, {NOTE_E4, 1000},
  {NOTE_G4, 500}, {NOTE_E4, 500}, {NOTE_D4, 500}, {NOTE_E4, 500},
  {NOTE_C4, 1500},
  {0, 0}  // 종료 마커
};

4.2 고급 구조

완전한 음표 구조

typedef struct {
  Note_t note;           // 음계 (C, D, E, ...)
  Octave_t octave;       // 옥타브 (3, 4, 5, ...)
  float beats;           // 박자 수
  uint8_t is_dotted;     // 점음표
  uint8_t is_rest;       // 쉼표
} AdvancedNote_t;

// 학교종 (BPM 120)
const AdvancedNote_t school_bell_advanced[] = {
  {NOTE_G, OCTAVE_4, 1.0f, 0, 0},  // 솔
  {NOTE_G, OCTAVE_4, 1.0f, 0, 0},  // 솔
  {NOTE_A, OCTAVE_4, 1.0f, 0, 0},  // 라
  {NOTE_A, OCTAVE_4, 1.0f, 0, 0},  // 라
  {NOTE_G, OCTAVE_4, 1.0f, 0, 0},  // 솔
  {NOTE_G, OCTAVE_4, 1.0f, 0, 0},  // 솔
  {NOTE_E, OCTAVE_4, 2.0f, 0, 0},  // 미 (2박)
  
  {NOTE_G, OCTAVE_4, 1.0f, 0, 0},  // 솔
  {NOTE_G, OCTAVE_4, 1.0f, 0, 0},  // 솔
  {NOTE_E, OCTAVE_4, 1.0f, 0, 0},  // 미
  {NOTE_E, OCTAVE_4, 1.0f, 0, 0},  // 미
  {NOTE_D, OCTAVE_4, 3.0f, 0, 0},  // 레 (3박)
  
  {NOTE_G, OCTAVE_4, 1.0f, 0, 0},  // 솔
  {NOTE_G, OCTAVE_4, 1.0f, 0, 0},  // 솔
  {NOTE_A, OCTAVE_4, 1.0f, 0, 0},  // 라
  {NOTE_A, OCTAVE_4, 1.0f, 0, 0},  // 라
  {NOTE_G, OCTAVE_4, 1.0f, 0, 0},  // 솔
  {NOTE_G, OCTAVE_4, 1.0f, 0, 0},  // 솔
  {NOTE_E, OCTAVE_4, 2.0f, 0, 0},  // 미 (2박)
  
  {NOTE_G, OCTAVE_4, 1.0f, 0, 0},  // 솔
  {NOTE_E, OCTAVE_4, 1.0f, 0, 0},  // 미
  {NOTE_D, OCTAVE_4, 1.0f, 0, 0},  // 레
  {NOTE_E, OCTAVE_4, 1.0f, 0, 0},  // 미
  {NOTE_C, OCTAVE_4, 3.0f, 0, 0},  // 도 (3박)
  
  {NOTE_C, OCTAVE_4, 0.0f, 0, 1}   // 종료
};

4.3 곡 정보 구조

곡 메타데이터

typedef struct {
  const char* title;           // 곡 제목
  const char* composer;        // 작곡가
  uint16_t bpm;                // 템포
  const AdvancedNote_t* notes; // 음표 데이터
  uint16_t note_count;         // 음표 개수
} Song_t;

// 곡 정의
const Song_t songs[] = {
  {
    .title = "School Bell",
    .composer = "Traditional",
    .bpm = 120,
    .notes = school_bell_advanced,
    .note_count = sizeof(school_bell_advanced) / sizeof(AdvancedNote_t)
  },
  // 다른 곡들...
};

5. 멜로디 재생 엔진

5.1 기본 재생 함수

단순 재생

void play_melody_simple(const MusicNote_t* melody)
{
  uint16_t i = 0;
  
  while (melody[i].frequency != 0 || melody[i].duration != 0)
  {
    if (melody[i].frequency > 0)
    {
      buzzer_set_frequency(melody[i].frequency);
    }
    else
    {
      buzzer_off();  // 쉼표
    }
    
    HAL_Delay(melody[i].duration);
    buzzer_off();
    HAL_Delay(50);  // 음 사이 간격
    
    i++;
  }
}

// 사용 예시
int main(void)
{
  // ...
  buzzer_init();
  
  printf("Playing: School Bell\r\n");
  play_melody_simple(school_bell);
  
  printf("Complete!\r\n");
  
  while (1)
  {
    HAL_Delay(1000);
  }
}

5.2 고급 재생 함수

템포 기반 재생

void play_song(const Song_t* song)
{
  printf("\r\n=== Now Playing ===\r\n");
  printf("Title: %s\r\n", song->title);
  printf("Composer: %s\r\n", song->composer);
  printf("BPM: %d\r\n\r\n", song->bpm);
  
  uint16_t quarter_ms = 60000 / song->bpm;
  
  for (uint16_t i = 0; i < song->note_count; i++)
  {
    const AdvancedNote_t* note = &song->notes[i];
    
    // 종료 체크
    if (note->is_rest && note->beats == 0)
      break;
    
    // 음 길이 계산
    uint16_t duration = (uint16_t)(quarter_ms * note->beats);
    if (note->is_dotted)
    {
      duration = (uint16_t)(duration * 1.5f);
    }
    
    // 재생
    if (!note->is_rest)
    {
      uint16_t freq = get_note_frequency(note->note, note->octave);
      buzzer_set_frequency(freq);
    }
    else
    {
      buzzer_off();
    }
    
    // 음 길이의 90%만 소리 (레가토 효과)
    HAL_Delay((uint16_t)(duration * 0.9f));
    buzzer_off();
    HAL_Delay((uint16_t)(duration * 0.1f));
  }
  
  buzzer_off();
  printf("\r\nPlayback complete!\r\n");
}

5.3 비블로킹 재생

타이머 인터럽트 기반

typedef struct {
  const Song_t* song;
  uint16_t current_note;
  uint32_t note_start_time;
  uint16_t note_duration;
  uint8_t is_playing;
  uint8_t is_paused;
} MusicPlayer_t;

volatile MusicPlayer_t player = {0};

void music_player_init(void)
{
  player.song = NULL;
  player.current_note = 0;
  player.is_playing = 0;
  player.is_paused = 0;
}

void music_player_start(const Song_t* song)
{
  player.song = song;
  player.current_note = 0;
  player.is_playing = 1;
  player.is_paused = 0;
  player.note_start_time = HAL_GetTick();
  
  // 첫 음 재생
  play_next_note();
}

void play_next_note(void)
{
  if (player.song == NULL || !player.is_playing)
    return;
  
  if (player.current_note >= player.song->note_count)
  {
    // 재생 완료
    player.is_playing = 0;
    buzzer_off();
    return;
  }
  
  const AdvancedNote_t* note = &player.song->notes[player.current_note];
  
  // 종료 체크
  if (note->is_rest && note->beats == 0)
  {
    player.is_playing = 0;
    buzzer_off();
    return;
  }
  
  // 음 길이 계산
  uint16_t quarter_ms = 60000 / player.song->bpm;
  player.note_duration = (uint16_t)(quarter_ms * note->beats);
  
  if (note->is_dotted)
  {
    player.note_duration = (uint16_t)(player.note_duration * 1.5f);
  }
  
  // 재생
  if (!note->is_rest)
  {
    uint16_t freq = get_note_frequency(note->note, note->octave);
    buzzer_set_frequency(freq);
  }
  else
  {
    buzzer_off();
  }
  
  player.note_start_time = HAL_GetTick();
}

void music_player_update(void)
{
  if (!player.is_playing || player.is_paused)
    return;
  
  uint32_t elapsed = HAL_GetTick() - player.note_start_time;
  
  // 음의 90% 지점에서 소리 끄기
  if (elapsed >= (uint32_t)(player.note_duration * 0.9f))
  {
    buzzer_off();
  }
  
  // 음 종료
  if (elapsed >= player.note_duration)
  {
    player.current_note++;
    play_next_note();
  }
}

void music_player_pause(void)
{
  player.is_paused = 1;
  buzzer_off();
}

void music_player_resume(void)
{
  if (player.is_paused)
  {
    player.is_paused = 0;
    player.note_start_time = HAL_GetTick();
  }
}

void music_player_stop(void)
{
  player.is_playing = 0;
  player.is_paused = 0;
  buzzer_off();
}

// 메인 루프
int main(void)
{
  // ...
  buzzer_init();
  music_player_init();
  
  music_player_start(&songs[0]);
  
  while (1)
  {
    music_player_update();
    
    // 다른 작업 가능
    HAL_Delay(1);
  }
}

6. 고급 기능 구현

6.1 음량 조절

PWM 듀티 사이클 제어

typedef enum {
  VOLUME_MUTE = 0,
  VOLUME_LOW = 25,
  VOLUME_MEDIUM = 50,
  VOLUME_HIGH = 75,
  VOLUME_MAX = 100
} Volume_t;

volatile Volume_t current_volume = VOLUME_MEDIUM;

void buzzer_set_frequency_with_volume(uint16_t frequency, Volume_t volume)
{
  if (frequency == 0 || volume == VOLUME_MUTE)
  {
    buzzer_off();
    return;
  }
  
  uint32_t arr = 1000000 / frequency - 1;
  uint32_t ccr = (arr * volume) / 100;
  
  __HAL_TIM_SET_AUTORELOAD(&htim1, arr);
  __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, ccr);
}

void set_volume(Volume_t volume)
{
  current_volume = volume;
}

Volume_t get_volume(void)
{
  return current_volume;
}

// 재생 함수에 적용
void play_next_note_with_volume(void)
{
  // ... (이전 코드)
  
  if (!note->is_rest)
  {
    uint16_t freq = get_note_frequency(note->note, note->octave);
    buzzer_set_frequency_with_volume(freq, current_volume);
  }
  
  // ...
}

6.2 템포 조절

배속 재생

typedef enum {
  TEMPO_HALF = 50,      // 0.5배속
  TEMPO_NORMAL = 100,   // 1배속
  TEMPO_FAST = 150,     // 1.5배속
  TEMPO_DOUBLE = 200    // 2배속
} TempoMultiplier_t;

volatile TempoMultiplier_t tempo_multiplier = TEMPO_NORMAL;

uint16_t get_adjusted_bpm(uint16_t original_bpm)
{
  return (original_bpm * tempo_multiplier) / 100;
}

void set_tempo(TempoMultiplier_t tempo)
{
  tempo_multiplier = tempo;
}

// 재생 시 적용
void play_next_note_with_tempo(void)
{
  // ...
  
  uint16_t adjusted_bpm = get_adjusted_bpm(player.song->bpm);
  uint16_t quarter_ms = 60000 / adjusted_bpm;
  player.note_duration = (uint16_t)(quarter_ms * note->beats);
  
  // ...
}

6.3 반복 재생

루프 및 구간 반복

typedef struct {
  uint8_t enabled;
  uint16_t start_note;
  uint16_t end_note;
  uint16_t repeat_count;
  uint16_t current_repeat;
} LoopInfo_t;

volatile LoopInfo_t loop_info = {0};

void set_loop(uint16_t start, uint16_t end, uint16_t count)
{
  loop_info.enabled = 1;
  loop_info.start_note = start;
  loop_info.end_note = end;
  loop_info.repeat_count = count;
  loop_info.current_repeat = 0;
}

void disable_loop(void)
{
  loop_info.enabled = 0;
}

void play_next_note_with_loop(void)
{
  // ... (음 재생 로직)
  
  // 루프 체크
  if (loop_info.enabled && 
      player.current_note >= loop_info.end_note)
  {
    loop_info.current_repeat++;
    
    if (loop_info.current_repeat < loop_info.repeat_count)
    {
      player.current_note = loop_info.start_note;
      return;
    }
    else
    {
      loop_info.enabled = 0;
    }
  }
  
  // ...
}

// 전체 곡 반복
void set_infinite_loop(void)
{
  set_loop(0, player.song->note_count - 1, 0xFFFF);
}

6.4 음색 효과

비브라토 (Vibrato)

typedef struct {
  uint8_t enabled;
  uint16_t rate;       // Hz (주파수 변동 속도)
  uint16_t depth;      // Hz (주파수 변동 폭)
} Vibrato_t;

volatile Vibrato_t vibrato = {
  .enabled = 0,
  .rate = 5,    // 5Hz 변동
  .depth = 10   // ±10Hz
};

void apply_vibrato(uint16_t base_freq)
{
  if (!vibrato.enabled)
  {
    buzzer_set_frequency_with_volume(base_freq, current_volume);
    return;
  }
  
  static uint32_t last_update = 0;
  static int16_t offset = 0;
  static int8_t direction = 1;
  
  uint32_t now = HAL_GetTick();
  
  if (now - last_update >= (1000 / vibrato.rate / 2))
  {
    offset += direction * (vibrato.depth / 10);
    
    if (abs(offset) >= vibrato.depth)
    {
      direction = -direction;
    }
    
    last_update = now;
  }
  
  uint16_t modulated_freq = base_freq + offset;
  buzzer_set_frequency_with_volume(modulated_freq, current_volume);
}

트레몰로 (Tremolo)

typedef struct {
  uint8_t enabled;
  uint16_t rate;       // Hz (음량 변동 속도)
  uint8_t depth;       // % (음량 변동 폭)
} Tremolo_t;

volatile Tremolo_t tremolo = {
  .enabled = 0,
  .rate = 5,     // 5Hz
  .depth = 30    // 30% 변동
};

void apply_tremolo(uint16_t frequency)
{
  if (!tremolo.enabled)
  {
    buzzer_set_frequency_with_volume(frequency, current_volume);
    return;
  }
  
  static uint32_t last_update = 0;
  static int8_t volume_offset = 0;
  static int8_t direction = 1;
  
  uint32_t now = HAL_GetTick();
  
  if (now - last_update >= (1000 / tremolo.rate / 2))
  {
    volume_offset += direction * (tremolo.depth / 10);
    
    if (abs(volume_offset) >= tremolo.depth)
    {
      direction = -direction;
    }
    
    last_update = now;
  }
  
  int16_t modulated_volume = current_volume + volume_offset;
  
  if (modulated_volume < 0)
    modulated_volume = 0;
  if (modulated_volume > 100)
    modulated_volume = 100;
  
  buzzer_set_frequency_with_volume(frequency, (Volume_t)modulated_volume);
}

7. 사용자 인터페이스

7.1 버튼 제어

재생 컨트롤

#define BTN_PLAY_PAUSE  GPIOA, GPIO_PIN_0
#define BTN_STOP        GPIOA, GPIO_PIN_1
#define BTN_NEXT        GPIOA, GPIO_PIN_2
#define BTN_PREV        GPIOA, GPIO_PIN_3
#define BTN_VOL_UP      GPIOA, GPIO_PIN_4
#define BTN_VOL_DOWN    GPIOA, GPIO_PIN_5

void handle_button_press(void)
{
  static uint32_t last_press[6] = {0};
  uint32_t now = HAL_GetTick();
  uint32_t debounce = 200;  // 200ms 디바운스
  
  // Play/Pause
  if (HAL_GPIO_ReadPin(BTN_PLAY_PAUSE) == GPIO_PIN_RESET &&
      (now - last_press[0] > debounce))
  {
    if (player.is_playing && !player.is_paused)
    {
      music_player_pause();
      printf("Paused\r\n");
    }
    else if (player.is_paused)
    {
      music_player_resume();
      printf("Resumed\r\n");
    }
    else
    {
      music_player_start(&songs[0]);
      printf("Playing\r\n");
    }
    
    last_press[0] = now;
  }
  
  // Stop
  if (HAL_GPIO_ReadPin(BTN_STOP) == GPIO_PIN_RESET &&
      (now - last_press[1] > debounce))
  {
    music_player_stop();
    printf("Stopped\r\n");
    last_press[1] = now;
  }
  
  // Volume Up
  if (HAL_GPIO_ReadPin(BTN_VOL_UP) == GPIO_PIN_RESET &&
      (now - last_press[4] > debounce))
  {
    if (current_volume < VOLUME_MAX)
    {
      current_volume = (Volume_t)(current_volume + 25);
      printf("Volume: %d\r\n", current_volume);
    }
    last_press[4] = now;
  }
  
  // Volume Down
  if (HAL_GPIO_ReadPin(BTN_VOL_DOWN) == GPIO_PIN_RESET &&
      (now - last_press[5] > debounce))
  {
    if (current_volume > VOLUME_MUTE)
    {
      current_volume = (Volume_t)(current_volume - 25);
      printf("Volume: %d\r\n", current_volume);
    }
    last_press[5] = now;
  }
}

int main(void)
{
  // ...
  
  while (1)
  {
    music_player_update();
    handle_button_press();
    
    HAL_Delay(1);
  }
}

7.2 LCD 디스플레이

재생 정보 표시

void display_player_status(void)
{
  static uint32_t last_update = 0;
  uint32_t now = HAL_GetTick();
  
  if (now - last_update < 100)  // 100ms마다 업데이트
    return;
  
  lcd_clear();
  
  if (player.is_playing)
  {
    // 첫 번째 줄: 곡 제목
    lcd_set_cursor(0, 0);
    lcd_print(player.song->title);
    
    // 두 번째 줄: 재생 상태
    lcd_set_cursor(1, 0);
    
    if (player.is_paused)
    {
      lcd_print("PAUSED");
    }
    else
    {
      char status[16];
      uint16_t progress = (player.current_note * 100) / player.song->note_count;
      sprintf(status, "Playing %d%%", progress);
      lcd_print(status);
    }
    
    // 세 번째 줄: 음량
    lcd_set_cursor(2, 0);
    char vol_str[16];
    sprintf(vol_str, "Vol: %d%%", current_volume);
    lcd_print(vol_str);
    
    // 네 번째 줄: BPM
    lcd_set_cursor(3, 0);
    char bpm_str[16];
    uint16_t current_bpm = get_adjusted_bpm(player.song->bpm);
    sprintf(bpm_str, "BPM: %d", current_bpm);
    lcd_print(bpm_str);
  }
  else
  {
    lcd_set_cursor(0, 0);
    lcd_print("Music Player");
    lcd_set_cursor(1, 0);
    lcd_print("Press PLAY");
  }
  
  last_update = now;
}

8. 실전 프로젝트: 멀티송 플레이어

8.1 전체 시스템 구조

헤더 파일

// music_player.h
#ifndef MUSIC_PLAYER_H
#define MUSIC_PLAYER_H

#include "main.h"

// 음계 정의
typedef enum {
  NOTE_C, NOTE_CS, NOTE_D, NOTE_DS, NOTE_E, NOTE_F,
  NOTE_FS, NOTE_G, NOTE_GS, NOTE_A, NOTE_AS, NOTE_B,
  NOTE_MAX
} Note_t;

typedef enum {
  OCTAVE_3 = 0, OCTAVE_4, OCTAVE_5, OCTAVE_6, OCTAVE_MAX
} Octave_t;

typedef struct {
  Note_t note;
  Octave_t octave;
  float beats;
  uint8_t is_dotted;
  uint8_t is_rest;
} AdvancedNote_t;

typedef struct {
  const char* title;
  const char* composer;
  uint16_t bpm;
  const AdvancedNote_t* notes;
  uint16_t note_count;
} Song_t;

typedef enum {
  VOLUME_MUTE = 0, VOLUME_LOW = 25, VOLUME_MEDIUM = 50,
  VOLUME_HIGH = 75, VOLUME_MAX = 100
} Volume_t;

// 함수 선언
void music_player_init(void);
void music_player_start(const Song_t* song);
void music_player_pause(void);
void music_player_resume(void);
void music_player_stop(void);
void music_player_update(void);
void set_volume(Volume_t volume);
uint8_t is_playing(void);

#endif

8.2 곡 라이브러리

여러 곡 정의

// songs.c
#include "music_player.h"

// 학교종
const AdvancedNote_t school_bell_notes[] = {
  {NOTE_G, OCTAVE_4, 1.0f, 0, 0}, {NOTE_G, OCTAVE_4, 1.0f, 0, 0},
  {NOTE_A, OCTAVE_4, 1.0f, 0, 0}, {NOTE_A, OCTAVE_4, 1.0f, 0, 0},
  {NOTE_G, OCTAVE_4, 1.0f, 0, 0}, {NOTE_G, OCTAVE_4, 1.0f, 0, 0},
  {NOTE_E, OCTAVE_4, 2.0f, 0, 0},
  {NOTE_G, OCTAVE_4, 1.0f, 0, 0}, {NOTE_G, OCTAVE_4, 1.0f, 0, 0},
  {NOTE_E, OCTAVE_4, 1.0f, 0, 0}, {NOTE_E, OCTAVE_4, 1.0f, 0, 0},
  {NOTE_D, OCTAVE_4, 3.0f, 0, 0},
  {NOTE_C, OCTAVE_4, 0.0f, 0, 1}  // 종료
};

// 작은 별
const AdvancedNote_t twinkle_star_notes[] = {
  {NOTE_C, OCTAVE_4, 1.0f, 0, 0}, {NOTE_C, OCTAVE_4, 1.0f, 0, 0},
  {NOTE_G, OCTAVE_4, 1.0f, 0, 0}, {NOTE_G, OCTAVE_4, 1.0f, 0, 0},
  {NOTE_A, OCTAVE_4, 1.0f, 0, 0}, {NOTE_A, OCTAVE_4, 1.0f, 0, 0},
  {NOTE_G, OCTAVE_4, 2.0f, 0, 0},
  {NOTE_F, OCTAVE_4, 1.0f, 0, 0}, {NOTE_F, OCTAVE_4, 1.0f, 0, 0},
  {NOTE_E, OCTAVE_4, 1.0f, 0, 0}, {NOTE_E, OCTAVE_4, 1.0f, 0, 0},
  {NOTE_D, OCTAVE_4, 1.0f, 0, 0}, {NOTE_D, OCTAVE_4, 1.0f, 0, 0},
  {NOTE_C, OCTAVE_4, 2.0f, 0, 0},
  {NOTE_C, OCTAVE_4, 0.0f, 0, 1}  // 종료
};

// 생일 축하 노래
const AdvancedNote_t happy_birthday_notes[] = {
  {NOTE_G, OCTAVE_4, 0.75f, 0, 0}, {NOTE_G, OCTAVE_4, 0.25f, 0, 0},
  {NOTE_A, OCTAVE_4, 1.0f, 0, 0}, {NOTE_G, OCTAVE_4, 1.0f, 0, 0},
  {NOTE_C, OCTAVE_5, 1.0f, 0, 0}, {NOTE_B, OCTAVE_4, 2.0f, 0, 0},
  
  {NOTE_G, OCTAVE_4, 0.75f, 0, 0}, {NOTE_G, OCTAVE_4, 0.25f, 0, 0},
  {NOTE_A, OCTAVE_4, 1.0f, 0, 0}, {NOTE_G, OCTAVE_4, 1.0f, 0, 0},
  {NOTE_D, OCTAVE_5, 1.0f, 0, 0}, {NOTE_C, OCTAVE_5, 2.0f, 0, 0},
  
  {NOTE_G, OCTAVE_4, 0.75f, 0, 0}, {NOTE_G, OCTAVE_4, 0.25f, 0, 0},
  {NOTE_G, OCTAVE_5, 1.0f, 0, 0}, {NOTE_E, OCTAVE_5, 1.0f, 0, 0},
  {NOTE_C, OCTAVE_5, 1.0f, 0, 0}, {NOTE_B, OCTAVE_4, 1.0f, 0, 0},
  {NOTE_A, OCTAVE_4, 2.0f, 0, 0},
  
  {NOTE_F, OCTAVE_5, 0.75f, 0, 0}, {NOTE_F, OCTAVE_5, 0.25f, 0, 0},
  {NOTE_E, OCTAVE_5, 1.0f, 0, 0}, {NOTE_C, OCTAVE_5, 1.0f, 0, 0},
  {NOTE_D, OCTAVE_5, 1.0f, 0, 0}, {NOTE_C, OCTAVE_5, 2.0f, 0, 0},
  {NOTE_C, OCTAVE_4, 0.0f, 0, 1}  // 종료
};

// 곡 목록
const Song_t song_list[] = {
  {
    .title = "School Bell",
    .composer = "Traditional",
    .bpm = 120,
    .notes = school_bell_notes,
    .note_count = sizeof(school_bell_notes) / sizeof(AdvancedNote_t)
  },
  {
    .title = "Twinkle Star",
    .composer = "Traditional",
    .bpm = 120,
    .notes = twinkle_star_notes,
    .note_count = sizeof(twinkle_star_notes) / sizeof(AdvancedNote_t)
  },
  {
    .title = "Happy Birthday",
    .composer = "Traditional",
    .bpm = 120,
    .notes = happy_birthday_notes,
    .note_count = sizeof(happy_birthday_notes) / sizeof(AdvancedNote_t)
  }
};

const uint8_t song_count = sizeof(song_list) / sizeof(Song_t);

extern const Song_t song_list[];
extern const uint8_t song_count;

8.3 메인 애플리케이션

플레이리스트 기능

// main.c
#include "main.h"
#include "music_player.h"
#include "songs.h"
#include "stdio.h"

uint8_t current_song_index = 0;

void play_next_song(void)
{
  current_song_index++;
  
  if (current_song_index >= song_count)
  {
    current_song_index = 0;
  }
  
  music_player_start(&song_list[current_song_index]);
  
  printf("\r\n=== Now Playing ===\r\n");
  printf("Song %d/%d: %s\r\n", 
         current_song_index + 1, 
         song_count,
         song_list[current_song_index].title);
}

void play_previous_song(void)
{
  if (current_song_index == 0)
  {
    current_song_index = song_count - 1;
  }
  else
  {
    current_song_index--;
  }
  
  music_player_start(&song_list[current_song_index]);
  
  printf("\r\n=== Now Playing ===\r\n");
  printf("Song %d/%d: %s\r\n", 
         current_song_index + 1, 
         song_count,
         song_list[current_song_index].title);
}

void show_playlist(void)
{
  printf("\r\n=== Playlist ===\r\n");
  
  for (uint8_t i = 0; i < song_count; i++)
  {
    if (i == current_song_index)
    {
      printf("> ");
    }
    else
    {
      printf("  ");
    }
    
    printf("%d. %s (%s) - %d BPM\r\n",
           i + 1,
           song_list[i].title,
           song_list[i].composer,
           song_list[i].bpm);
  }
  
  printf("\r\n");
}

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  MX_TIM1_Init();
  
  printf("\r\n=== Music Player System ===\r\n");
  printf("Total songs: %d\r\n", song_count);
  
  music_player_init();
  show_playlist();
  
  printf("\r\nCommands:\r\n");
  printf("1: Play/Pause\r\n");
  printf("2: Stop\r\n");
  printf("3: Next song\r\n");
  printf("4: Previous song\r\n");
  printf("5: Volume up\r\n");
  printf("6: Volume down\r\n");
  printf("7: Show playlist\r\n\r\n");
  
  while (1)
  {
    music_player_update();
    handle_button_press();
    
    // 곡 종료 시 다음 곡 자동 재생
    static uint8_t was_playing = 0;
    
    if (was_playing && !is_playing())
    {
      HAL_Delay(500);  // 0.5초 대기
      play_next_song();
    }
    
    was_playing = is_playing();
    
    HAL_Delay(1);
  }
}

9. 최적화 및 디버깅

9.1 메모리 최적화

PROGMEM 사용 (플래시 메모리 저장)

// 곡 데이터를 플래시에 저장하여 RAM 절약
const AdvancedNote_t school_bell_notes[] __attribute__((section(".rodata"))) = {
  // ...
};

// 읽기
AdvancedNote_t note;
memcpy(&note, &school_bell_notes[i], sizeof(AdvancedNote_t));

9.2 타이밍 정확도

타이머 DMA 사용

// CCR 값을 DMA로 자동 업데이트하여 정확한 타이밍 확보
uint16_t frequency_buffer[100];

void setup_pwm_dma(void)
{
  // DMA 설정으로 주파수 변경 시 지터 최소화
  HAL_TIM_PWM_Start_DMA(&htim1, TIM_CHANNEL_1, 
                         (uint32_t*)frequency_buffer, 100);
}

9.3 디버그 출력

재생 상태 모니터링

void debug_print_player_status(void)
{
  if (!player.is_playing)
    return;
  
  printf("Note: %d/%d | Time: %lu/%lu ms | Vol: %d%%\r\n",
         player.current_note,
         player.song->note_count,
         HAL_GetTick() - player.note_start_time,
         player.note_duration,
         current_volume);
}

10. 실습 과제

과제 1: 멜로디 작곡기

사용자가 버튼으로 음을 입력하여 멜로디를 작곡하고 재생하는 시스템을 구현하세요.

요구사항

  • 12개 버튼으로 음계 입력
  • 최대 100개 음표 저장
  • 녹음/재생/삭제 기능
  • EEPROM에 저장

힌트

typedef struct {
  AdvancedNote_t notes[100];
  uint16_t note_count;
} UserSong_t;

void record_note(Note_t note, Octave_t octave);
void save_to_eeprom(void);
void load_from_eeprom(void);

과제 2: RTTTL 파서

RTTTL(Ring Tone Text Transfer Language) 형식의 멜로디를 파싱하여 재생하세요.

요구사항

  • RTTTL 문자열 파싱
  • 동적 곡 생성
  • UART로 RTTTL 수신
  • 재생 및 저장

RTTTL 예시

Super Mario:d=4,o=5,b=125:16e6,16e6,32p,8e6,16c6,8e6,8g6,8p,8g

힌트

void parse_rtttl(const char* rtttl, Song_t* song);
uint16_t parse_note(const char* note_str);
uint8_t parse_duration(const char* dur_str);

과제 3: 음악 시각화

LCD에 재생 중인 음표를 실시간으로 시각화하세요.

요구사항

  • 악보 형태 표시
  • 현재 재생 위치 표시
  • 스펙트럼 분석기 효과
  • 박자 표시

힌트

void draw_note_on_lcd(Note_t note, Octave_t octave, uint8_t x);
void draw_spectrum(uint16_t frequency);
void animate_beat(void);

11. 트러블슈팅

문제 1: 소리가 나지 않음

증상

부저에서 아무 소리도 나지 않음

진단 및 해결

// 1. PWM 시작 확인
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);

// 2. CCR 값 확인
uint32_t ccr = __HAL_TIM_GET_COMPARE(&htim1, TIM_CHANNEL_1);
printf("CCR: %lu\r\n", ccr);  // 0이면 문제

// 3. 핀 설정 확인
// PA8 = TIM1_CH1 (AF1)

// 4. 부저 극성 확인
// (+)와 (-) 반대로 연결했을 수 있음

// 5. 주파수 범위 확인
// 인간 가청 주파수: 20Hz ~ 20kHz
// 부저 권장: 200Hz ~ 5kHz

문제 2: 음이 정확하지 않음

증상

음계가 맞지 않음, 음이 높거나 낮음

해결

// 원인: Prescaler 설정 오류
// APB2 클럭 확인 (TIM1은 APB2)
// SystemCoreClock, APB2 클럭 확인

// 계산 재확인
// Timer 주파수 = APB2 클럭 / (PSC + 1)
// 출력 주파수 = Timer 주파수 / (ARR + 1)

// 예시: 440Hz 목표
// APB2 = 168MHz, PSC = 167
// Timer = 168MHz / 168 = 1MHz
// ARR = 1MHz / 440Hz - 1 = 2272

void verify_frequency(void)
{
  uint32_t psc = __HAL_TIM_GET_PRESCALER(&htim1);
  uint32_t arr = __HAL_TIM_GET_AUTORELOAD(&htim1);
  
  uint32_t timer_freq = 168000000 / (psc + 1);
  uint32_t output_freq = timer_freq / (arr + 1);
  
  printf("PSC: %lu, ARR: %lu\r\n", psc, arr);
  printf("Output: %lu Hz\r\n", output_freq);
}

문제 3: 음 사이에 딸각 소리

증상

음이 바뀔 때 클릭 소리 발생

해결

// 원인: PWM 값이 급격히 변함

// 해결: 페이드 효과
void buzzer_fade_to_frequency(uint16_t new_freq, uint16_t fade_ms)
{
  uint32_t old_arr = __HAL_TIM_GET_AUTORELOAD(&htim1);
  uint32_t new_arr = 1000000 / new_freq - 1;
  
  int32_t step = (int32_t)(new_arr - old_arr) / 10;
  
  for (int i = 0; i < 10; i++)
  {
    old_arr += step;
    __HAL_TIM_SET_AUTORELOAD(&htim1, old_arr);
    __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, old_arr / 2);
    HAL_Delay(fade_ms / 10);
  }
}

// 또는 음 사이 짧은 무음 삽입
buzzer_off();
HAL_Delay(10);
buzzer_set_frequency(next_freq);

문제 4: 타이밍이 맞지 않음

증상

BPM 120인데 실제로는 더 빠르거나 느림

해결

// 원인: HAL_Delay() 오차 누적

// 해결: 절대 시간 기준 사용
void play_with_accurate_timing(void)
{
  uint32_t start_time = HAL_GetTick();
  uint16_t quarter_ms = 60000 / player.song->bpm;
  
  for (uint16_t i = 0; i < player.song->note_count; i++)
  {
    const AdvancedNote_t* note = &player.song->notes[i];
    
    // 이 음이 시작해야 할 절대 시간
    uint32_t target_time = start_time + (i * quarter_ms * note->beats);
    
    // 현재 시간
    uint32_t now = HAL_GetTick();
    
    // 대기
    if (target_time > now)
    {
      HAL_Delay(target_time - now);
    }
    
    // 재생
    if (!note->is_rest)
    {
      uint16_t freq = get_note_frequency(note->note, note->octave);
      buzzer_set_frequency(freq);
    }
  }
}

12. 학습 정리

오늘 배운 내용

부저 제어 기본

  • 수동 부저는 PWM으로 주파수 제어
  • 주파수 = 음 높이, 듀티 사이클 = 음량
  • 음계와 주파수의 관계 이해

멜로디 재생

  • 음표 구조체 설계
  • BPM 기반 타이밍 계산
  • 블로킹/비블로킹 재생

고급 기능

  • 음량 조절 (PWM 듀티)
  • 템포 조절 (BPM 배속)
  • 반복 재생
  • 음색 효과 (비브라토, 트레몰로)

시스템 구현

  • 멀티송 플레이어
  • 버튼 컨트롤
  • LCD 디스플레이

핵심 개념

1. 주파수 설정

ARR = Timer_Freq / Output_Freq - 1
CCR = ARR / 2  // 50% 듀티

2. 음 길이 계산

duration_ms = (60000 / BPM) * beats

3. 비블로킹 재생

// HAL_GetTick() 기반 시간 관리
if (elapsed >= duration) play_next_note();

음계 주파수 요약표

음계4옥타브5옥타브6옥타브
C (도)262 Hz523 Hz1047 Hz
D (레)294 Hz587 Hz1175 Hz
E (미)330 Hz659 Hz1319 Hz
F (파)349 Hz698 Hz1397 Hz
G (솔)392 Hz784 Hz1568 Hz
A (라)440 Hz880 Hz1760 Hz
B (시)494 Hz988 Hz1976 Hz
profile
당신의 코딩 메이트

0개의 댓글