본문 바로가기
  • 빠르게 배우고, 선명하게 기록하다: Swift's Tech Note
드론·로봇 개발/임베디드·펌웨어

STM32 Oneshot125 PWM: 2kHz 타이머로 BLDC ESC 구동하기

by 스위프트31 2023. 2. 25.
728x90
반응형

STM32 Oneshot125 PWM을 타이머 두 개(TIM3, TIM5)로 만들어 드론의 BLDC 모터 ESC를 구동한 과정을 정리했습니다. 84MHz 타이머 클럭에서 주기 2kHz, 펄스 폭 125~250µs를 만드는 설정과 SBUS 수신기 값을 모터 출력으로 바꾸는 코드, 그리고 코드를 다시 읽다가 찾은 계산 오류까지 적었습니다.

STM32 자작드론 시리즈 · GPS 수신(UART DMA) · SBUS 수신기 · Oneshot125 PWM 모터 출력 · SPI 플래시(W25Q128) · ADC 배터리 전압 DMA

한눈에 보기

  • MCU: STM32F405RGT6 (Revolt FC 핀맵 사용). 모터 4개는 TIM3(CH3, CH4)과 TIM5(CH3, CH4)로 출력합니다.
  • 타이머 설정: Prescaler 0, ARR 41999, 84MHz 클럭 기준 주기 500µs(2kHz)입니다.
  • Oneshot125는 펄스 폭이 125~250µs이고, 이 값은 CCR 10500~21000에 해당합니다.
  • SBUS 값을 모터 출력으로 옮기는 식은 정수 나눗셈 때문에 최대 출력이 20100(약 239µs)에서 멈춥니다. 곱셈을 먼저 하도록 고치면 됩니다.

2kHz 주기에서 최소 125µs와 최대 250µs 펄스 폭을 나타낸 Oneshot125 PWM 타이밍 도식
Oneshot125 PWM 타이밍. 주기는 500µs로 고정하고 펄스 폭만 125~250µs로 바꿉니다 (직접 제작)

Oneshot125는 어떤 신호인가요?

ESC에 모터 속도를 알려 주는 아날로그 펄스 신호의 한 종류입니다. 드론 모터(브러시리스)는 3상 신호로 돌고, 이 3상 신호는 ESC가 만듭니다. FC는 ESC에 "얼마나 빠르게"를 PWM 펄스 폭으로 전달하고, 이 전달 방식이 ESC 프로토콜입니다. 아래 표는 프로토콜별 펄스 폭과 갱신 주파수입니다.

프로토콜 펄스 폭 갱신 주파수 방식
Standard PWM 1000~2000µs 0.5kHz 아날로그
OneShot125 125~250µs 4kHz 아날로그
OneShot42 42~84µs 11.9kHz 아날로그
MultiShot 5~25µs 40kHz 아날로그
DShot300 53.3µs(프레임) 18.8kHz 디지털

출처: Oscar Liang, ESC 프로토콜 정리, 확인일 2026-10-08. 갱신 주파수는 해당 프로토콜이 낼 수 있는 값입니다.

예전에 쓴 이 글에서는 Oneshot125를 "125µs 주기(8kHz)"라고 적었습니다. 정확히는 125µs가 주기가 아니라 최소 펄스 폭입니다. 프로토콜이 새로 나올수록 펄스가 짧아지고 갱신 주파수가 높아져서 모터를 더 빠르게 제어할 수 있고, 레이싱 드론처럼 제어 알고리즘이 빠를수록 이런 빠른 프로토콜이 쓰입니다. 이 시리즈의 FC는 2kHz로 갱신하며, 이는 Oneshot125가 낼 수 있는 4kHz 이내입니다.

모터 핀과 타이머 정하기

Revolt FC에서 모터 신호가 나가는 핀은 Betaflight 저장소의 REVOLT config.h에 정의되어 있습니다.

#define MOTOR1_PIN PB0
#define MOTOR2_PIN PB1
#define MOTOR3_PIN PA3
#define MOTOR4_PIN PA2

CubeMX 핀 배정(그리고 생성된 코드의 GPIO 주석)에서 PB0, PB1은 TIM3 CH3, CH4에, PA2, PA3은 TIM5 CH3, CH4에 연결되어 있습니다. 그래서 타이머를 두 개 설정해야 합니다.

모터 핀 타이머와 채널
M1 PB0 TIM3 CH3
M2 PB1 TIM3 CH4
M3 PA3 TIM5 CH4
M4 PA2 TIM5 CH3

TIM3과 TIM5는 둘 다 APB1 버스에 연결된 타이머이고, 내부 클럭을 쓰면 84MHz로 동작합니다(제 구현 기록 기준). 이 값이 아래 모든 계산의 기준이니, 클럭 설정이 다르면 ARR과 CCR 값도 다시 계산해야 합니다.

CubeMX 설정

TIM3과 TIM5를 같은 값으로 설정합니다. 채널은 PWM Generation으로 켭니다.

항목 값 이유
Clock Source Internal Clock 내부 클럭 84MHz
Prescaler 0 분주 없이 84MHz 그대로 사용
Counter Mode Up 0에서 ARR까지 증가
Counter Period (ARR) 41999 42000카운트 ÷ 84MHz = 500µs, 즉 2kHz
PWM Mode PWM mode 1, 극성 High CCR 값까지 High 출력
auto-reload preload Enable ARR 변경이 주기 끝에 적용
Output compare preload, Fast Mode Enable preload는 CCR 변경을 주기 경계에서 반영

STM32CubeMX에서 TIM5 채널 3, 4를 PWM Generation으로 설정하고 Prescaler 0, Counter Period 41999를 입력한 화면
CubeMX의 TIM5 설정 화면 (STM32F405RGTx)

펄스 폭은 CCR(Capture/Compare Register) 값으로 정합니다. 84MHz에서 1µs는 84카운트이므로 아래처럼 계산됩니다.

펄스 폭 CCR 값
125µs (최소, 모터 정지) 10500
250µs (최대, 풀 스로틀) 21000
범위 10500

처음 테스트에서는 오실로스코프로 파형을 확인하려고 모터마다 다른 펄스 폭(M1 150µs, M2 200µs, M3 100µs, M4 50µs)을 넣어 보았습니다. 채널마다 폭이 다르게 나오면 핀과 타이머 매핑이 맞는 것입니다.

코드 1: 타이머 초기화 (LL 드라이버)

CubeMX가 만든 tim.c의 핵심은 아래입니다. HAL이 아니라 LL(Low-Layer) 드라이버를 썼습니다. TIM3과 TIM5가 같은 구조라서 TIM5만 적습니다.

void MX_TIM5_Init(void)
{
  LL_TIM_InitTypeDef TIM_InitStruct = {0};
  LL_TIM_OC_InitTypeDef TIM_OC_InitStruct = {0};

  LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM5);

  TIM_InitStruct.Prescaler = 0;
  TIM_InitStruct.CounterMode = LL_TIM_COUNTERMODE_UP;
  TIM_InitStruct.Autoreload = 41999;               // 주기 500us
  TIM_InitStruct.ClockDivision = LL_TIM_CLOCKDIVISION_DIV1;
  LL_TIM_Init(TIM5, &TIM_InitStruct);
  LL_TIM_EnableARRPreload(TIM5);
  LL_TIM_SetClockSource(TIM5, LL_TIM_CLOCKSOURCE_INTERNAL);

  TIM_OC_InitStruct.OCMode = LL_TIM_OCMODE_PWM1;
  TIM_OC_InitStruct.OCPolarity = LL_TIM_OCPOLARITY_HIGH;
  TIM_OC_InitStruct.CompareValue = 0;

  LL_TIM_OC_EnablePreload(TIM5, LL_TIM_CHANNEL_CH3);
  LL_TIM_OC_Init(TIM5, LL_TIM_CHANNEL_CH3, &TIM_OC_InitStruct);
  LL_TIM_OC_EnableFast(TIM5, LL_TIM_CHANNEL_CH3);
  LL_TIM_OC_EnablePreload(TIM5, LL_TIM_CHANNEL_CH4);
  LL_TIM_OC_Init(TIM5, LL_TIM_CHANNEL_CH4, &TIM_OC_InitStruct);
  LL_TIM_OC_EnableFast(TIM5, LL_TIM_CHANNEL_CH4);

  // GPIO: PA2(CH3), PA3(CH4)를 AF2(TIM3..5)로 설정
}

코드 2: ESC 캘리브레이션과 SBUS 연동

설정값은 Configure.h에 모아 두었습니다. 타이머 쪽 범위와 SBUS 수신기 채널 값 범위입니다.

#define MAX_TIM_PWM        21000   // 250us
#define MIN_TIM_PWM        10500   // 125us
#define RANGE_TIM_PWM      10500

#define MAX_RADIO_CH1_PWM  1792    // SBUS 채널 최댓값
#define MIN_RADIO_CH1_PWM  192
#define RANGE_RADIO_CH1_PWM 1600

main.c에서는 먼저 타이머와 네 채널을 켜고, 부팅 직후 ESC 캘리브레이션을 합니다. 최대 펄스를 7초 동안 보낸 뒤 최소 펄스로 내려 8초 동안 유지하는 순서입니다. 코드 주석에는 두 번째도 7초라고 적혀 있지만 실제 지연은 8초입니다.

// ESC PWM 켜기
LL_TIM_EnableCounter(TIM3);
LL_TIM_CC_EnableChannel(TIM3, LL_TIM_CHANNEL_CH3);   // M1
LL_TIM_CC_EnableChannel(TIM3, LL_TIM_CHANNEL_CH4);   // M2
LL_TIM_EnableCounter(TIM5);
LL_TIM_CC_EnableChannel(TIM5, LL_TIM_CHANNEL_CH4);   // M3
LL_TIM_CC_EnableChannel(TIM5, LL_TIM_CHANNEL_CH3);   // M4

// ESC 캘리브레이션: 최대 7초 -> 최소 8초
TIM3->CCR3 = TIM3->CCR4 = TIM5->CCR4 = TIM5->CCR3 = MAX_TIM_PWM;
usDelay(7000000);
TIM3->CCR3 = TIM3->CCR4 = TIM5->CCR4 = TIM5->CCR3 = MIN_TIM_PWM;
usDelay(8000000);

메인 루프에서는 SBUS 수신기가 failsafe 비트를 세우면 모터 출력을 최소로 내리고, 정상이면 조종기 1번 채널 값을 모터 출력으로 옮깁니다. 이 단계에서는 네 모터에 같은 값을 넣는 시험용 구조입니다.

if (msg_sbus.failsafe == MSG_SBUS_FS_BIT_MASK) {          // 수신기 failsafe -> 최소 출력
    TIM3->CCR3 = TIM3->CCR4 = TIM5->CCR4 = TIM5->CCR3 = MIN_TIM_PWM;
} else {
    uint32_t ccr = MIN_TIM_PWM
        + RANGE_TIM_PWM / RANGE_RADIO_CH1_PWM * (msg_sbus.rx_channel[0] - MIN_RADIO_CH1_PWM);
    TIM3->CCR3 = TIM3->CCR4 = TIM5->CCR4 = TIM5->CCR3 = ccr;
}

다시 읽다가 찾은 계산 오류

RANGE_TIM_PWM / RANGE_RADIO_CH1_PWM은 10500 ÷ 1600이고, 매크로가 모두 정수라서 C에서는 정수 나눗셈으로 6이 됩니다(실제 값은 6.5625). 그러면 조종기 스틱이 최대일 때 CCR은 아래와 같습니다.

10500 + 6 × (1792 - 192) = 10500 + 9600 = 20100   → 약 239µs (의도한 최대는 21000, 250µs)

풀 스로틀이 250µs까지 가지 못하고 약 239µs에서 멈춥니다. 곱셈을 먼저 하면 정수 연산으로도 정확하게 계산됩니다. 아래처럼 고치면 됩니다.

uint32_t ccr = MIN_TIM_PWM
    + (uint32_t)(msg_sbus.rx_channel[0] - MIN_RADIO_CH1_PWM) * RANGE_TIM_PWM / RANGE_RADIO_CH1_PWM;

이 오류는 이 글을 쓰려고 코드를 다시 읽다가 찾았습니다. 당시 저장소의 커밋(912df0c)에는 반영되어 있지 않으니, 코드를 가져다 쓰시는 분은 위처럼 바꿔서 쓰세요. 또 하나, SBUS 채널 값이 최솟값(192)보다 작게 들어오는 경우를 막는 범위 검사도 아직 없습니다.

정리와 다음 단계

  • 주기는 ARR로, 펄스 폭은 CCR로 정합니다. 84MHz에서는 1µs가 84카운트입니다.
  • ESC는 부팅 직후 최대 펄스 → 최소 펄스 순서로 캘리브레이션했습니다.
  • 수신기 failsafe가 켜지면 모터 출력을 최소로 내립니다.
  • 값을 옮기는 식은 곱셈을 먼저 하세요.

전체 소스는 GitHub의 STM32F4_FC_DEV에 있고, 이 글의 시점은 "done pwm generation using TIM sources with 2kHz PWM for brushless motors" 커밋입니다. 다음 글에서는 SPI로 W25Q128 플래시를 읽고 쓰는 드라이버를 다룹니다. 여러분은 Oneshot125 대신 DShot을 쓰고 계신가요? 사용 중인 ESC와 FC를 댓글로 알려 주세요.

2026년 10월 기준입니다. 코드와 설정은 2023년 2월 직접 구현한 내용이고, ESC 프로토콜 수치는 2026년 10월에 확인했습니다. 실제 기체에서는 프롭을 떼고 시험하세요.

728x90
반응형

댓글