Sentry Turret System -1

최진철·2026년 2월 8일

Sentry Turret?

목록 보기
1/2

오늘은 cortex-M 보드를 이용한 간단한 서보 모터 제어 구현을 기록하겠습니다.!

1. 구현 내용


프로젝트 개요

  • MCU: STM32F401RE (Nucleo-64 보드)
  • 입력: HW-504 조이스틱 (X축: VRx, Y축: VRy)
    • VCC : 3.3v (STM32F401RE)
  • 출력: SG90 서보 모터 2개 (X축 제어: PA0, Y축 제어: PA8)
    • VCC : 5v (STM32F401RE)
  • 제어 방식: 조이스틱 아날로그 값 → PWM 펄스 폭 매핑 → 서보 각도 제어

1. 구현 내용


프로젝트 개요

  • MCU: STM32F401RE (Nucleo-64 보드)
  • 입력: HW-504 조이스틱 (X축: VRx, Y축: VRy)
    • VCC : 3.3v (STM32F401RE)
  • 출력: SG90 서보 모터 2개 (X축 제어: PA0, Y축 제어: PA8)
    • VCC : 5v (STM32F401RE)
  • 제어 방식: 조이스틱 아날로그 값 → PWM 펄스 폭 매핑 → 서보 각도 제어

Pin 설정 근거

STM32F401RE의 Datasheet와 Reference Manual의 Pin descriptions를 기반으로 기능 구현을 위한 임의의 Pin을 선택했습니다.

PinCtrl

기능사용 채널/타이머근거 문서 및 페이지
PA0TIM2_CH1 (PWM 출력)servo1 _sigDatasheet DS10086 Rev 4 : Table 9 (Pin definitions)
p.47: PA0 = TIM2_CH1 (AF1)
RM0368: Table 9 (Alternate function mapping)
p.68: TIM2_CH1 가능
PA8TIM1_CH1 (PWM 출력)servo2 _sigDatasheet: Table 9
p.48: PA8 = TIM1_CH1 (AF1)
RM0368 : Table 9
p.69: TIM1_CH1 가능
PB0ADC1_IN8 (아날로그 입력)조이스틱 VRx (X축)Datasheet : Table 9
p.49: PB0 = ADC1_IN8
RM0368, Ch.11 ADC, Table 48 (ADC pin assignments)
p.288: PB0 = ADC1_IN8
PB1ADC1_IN9 (아날로그 입력)조이스틱 VRy (Y축)Datasheet, Table 9
p.49: PB1 = ADC1_IN9
RM0368, Table 48
p.288: PB1 = ADC1_IN9
PB2GPIO Input (Pull-up)조이스틱 SW (버튼)Datasheet, Table 9,p.49: PB2 = GPIO (일반 입력 가능)
RM0368, Ch.7 GPIO, p.155~157: 입력 모드 및 내부 풀업 지원

STM32CubeMX 설정

Project Configuration 
==========================================
1. Clock Configuration
		HCLK = 84 MHz (기본 설정 그대로 사용 )

2. Timers → TIM2 
		Clock Source → Internal Clock
		Channel1 → PWM Generation CH1(PA0)
	Configuration
			Prescaler : 83
			Counter Period (ARR) : 19999
			Pulse : 1500 (초기 90)
			Mode : PWM mode 1
			Polarity : High
			
	-----------------------------
	Timers → TIM1
	Clock Source: Internal
	Channel1: PWM Generation CH1 (PA8)
	Configuration
			Prescaler : 83
			Counter Period (ARR) : 19999
			Pulse : 1500 (초기 90)
			Mode : PWM mode 1
			Polarity : High

3. GPIO
		PA0 → TIM2_CH1 (자동 설정됨)
		PA8 → TIM1_CH1 (자동 설정됨)
		PB2 → GPIO_Input + Pull-up  (Joystick SW Btn)
		~~#PA5 → Output (LED용, 선택)~~
		
4. ADC1
		Parameter Settings → Continuous Conversion Mode 체크
		Number of Conversion: 2
		Rank1: IN8 (PB0)
		Rank2: IN9 (PB1)

4. USART2 (선택, 디버그용)
		Mode → Asynchronous
		Baud Rate → 115200
		PA2 → USART2_TX

ADC setting

조이스틱 동작 원리 및 구현

동작 원리

HW-504 조이스틱은 2개의 potentiometerVRx, VRy)와 Push 버튼(SW)으로 구성.

  • potentiometer: X/Y축 움직임에 따라 저항 변화 → 아날로그 전압 출력 0~3.3V, 중앙 ≈1.65V ⇒ ADC 12bit 중앙값 : 2112^{11} = 2048 ADC 값)
  • SW: 누를 때 LOW 신호 (디지털 입력)

⇒ STM32 ADC로 아날로그 값을 디지털화하여 읽음. Continuous Mode + Polling 방식으로 실시간 value read

  • 근거 RM0368 Ch.11 (ADC) - 11.1: "12-bit ADC is a successive approximation analog-to-digital converter. It has up to 16 channels." Continuous Mode (11.3.5): "ADC does one conversion after another even if the EOC is not read." → 1회 변환이 아닌 자동으로 계속해서 ADC
    • CONT 비트를 1로 설정하면 연속 모드 ON

    • 시작 방법 2가지

      1. 외부 신호(트리거)로 시작
      2. 소프트웨어로 SWSTRT 비트 1로 설정 (ADC_CR2 레지스터)
        → 보통 코드에서 HAL_ADC_Start() 호출하면 이게 내부적으로 일어남
    • ADC Result
      - 마지막 변환 결과가 ADC_DR 레지스터 (16비트)에 저장됨
      - EOC 플래그 (End Of Conversion)가 1이 됨 → "변환 끝났어요!" 신호
      - 만약 EOCIE 비트가 1이면 → 인터럽트 발생 (CPU가 "오! 데이터 왔네" 하고 깨어남)

      시간축 --------------------------------------------------------------> 
      
      [START 명령]  ────────────────► (HAL_ADC_Start() 또는 SWSTRT=1)
      
                    ┌───────────────┐     ┌───────────────┐     ┌───────────────┐
      Conversion 1  │  변환 중...   │     │  변환 중...   │     │  변환 중...   │
                    └───────┬───────┘     └───────┬───────┘     └───────┬───────┘
                            │                       │                       │
                         EOC 플래그 1             EOC 플래그 1           EOC 플래그 1
                         ADC_DR 업데이트         ADC_DR 업데이트       ADC_DR 업데이트
                            ▼                       ▼                       ▼
                            └───────► 자동으로 다음 변환 시작 (Continuous 모드의 핵심!)
                                      (CONT=1이면 멈추지 않고 무한 반복)
      
      인터럽트나 DMA가 켜져 있으면:
    • EOC마다 인터럽트 발생 (EOCIE=1 시)

    • 또는 DMA가 자동으로 ADC_DR 값을 메모리(adc_values)에 복사

      
      **DMA (11.8.1):** "Converted regular channel values are stored in memory by DMA without CPU intervention."  ⇒ 추가적으로 구현 시 내용 업데이트 예정 
      

구현 함수 및 설정

  • CubeMX 설정
    • ADC1 - Continuous Conversion Mode ENABLE,
    • DMAContinuousRequests ENABLE,
    • Number of Conversion=2 (Rank1: IN8/PB0, Rank2: IN9/PB1).
    • SW 버튼: PB2 GPIO Input Pull-up, EXTI로 인터럽트 처리 (HAL_GPIO_EXTI_Callback).
  • 코드 구현 (main.c):

    1) MX_ADC1_Init() - ADC 하드웨어 초기 설정

    hadc1.Init.ScanConvMode = ENABLE;              // 여러 채널 순차 변환 허용 (2채널 스캔)
    hadc1.Init.ContinuousConvMode = ENABLE;        // 한 번 시작하면 자동 연속 변환 (멈추지 않음)
    hadc1.Init.NbrOfConversion = 2;                // 변환할 채널 수: 2개 (Rank1 + Rank2)
    hadc1.Init.DMAContinuousRequests = ENABLE;     // DMA가 계속 요청 가능 (하지만 지금은 폴링 쓰는 중)
    
    sConfig.Channel = ADC_CHANNEL_8;               // Rank 1: PB0 (VRx)
    sConfig.Rank = 1;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);
    
    sConfig.Channel = ADC_CHANNEL_9;               // Rank 2: PB1 (VRy)
    sConfig.Rank = 2;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);

    2) main() 시작 부분 - 초기값 테스트

    HAL_ADC_Start(&hadc1);                        // ADC 변환 시작
    
     // 첫 번째 ADC 값 읽기 테스트
      if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK)
      {
        adc_values[0] = HAL_ADC_GetValue(&hadc1);  // 첫 번째 채널 (Rank 1)
      }
      if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK)
      {
        adc_values[1] = HAL_ADC_GetValue(&hadc1);  // 두 번째 채널 (Rank 2)
      }
    • HAL_ADC_GetValue() → ADC_DR 레지스터에서 최신 변환 값을 가져온다

      (3) while(1) 루프 - 실시간 값 읽기 (폴링 방식)

      HAL_ADC_Stop(&hadc1);                         // 이전 변환 강제 종료
      HAL_Delay(1);                                 // 안정화 대기 (짧게)
      
      HAL_ADC_Start(&hadc1);                        // 새 변환 시작
      
      HAL_ADC_PollForConversion(&hadc1, 50);        // Rank1 변환 완료 기다림
      adc_values[0] = HAL_ADC_GetValue(&hadc1);     // X값 저장
      
      HAL_ADC_PollForConversion(&hadc1, 50);        // Rank2 변환 완료 기다림
      adc_values[1] = HAL_ADC_GetValue(&hadc1);     // Y값 저장

      HAL_ADC_PollForConversion(&hadc1, 50); ⇒ EOC 값이 1 return까지 wait

      이 때 50ms 이상 대기시 그냥 넘어간다 timeout!!

      ⇒ adc_values 에 X, Y 값이 들어간다 .

      (4) 초기 값 보정(캘리브레이션) 원리

      const int center_x = 2048;    // X축 중앙값
      const int center_y = 2048;    // Y축 중앙값
      const int deadzone = 100;     // 흔들림 방지
    • 조이스틱 중앙에 있을 때 이론적:v ADC 2048 , 하지만 오차로 1980~2100 사이 나올 수 있다.

    • 시리얼 모니터로 X/Y 값 확인 → 그 값을 center_x/y로 바꿈 시리얼에 "X=2010" 계속 나오면 → center_x = 2010;으로 수정

    • deadzone: 중앙 ±100 범위는 움직이지 않게 해서 미세 떨림 방지

      보정 적용

      if (x > center_x + deadzone)                  // 오른쪽으로 많이 기울면
        pulse1 = 1500 + (x - center_x - deadzone) * 1000 / (4095 - center_x - deadzone);
      else if (x < center_x - deadzone)             // 왼쪽으로 많이 기울면
        pulse1 = 1500 - (center_x - deadzone - x) * 1000 / (center_x - deadzone);

      → 중앙에서 멀어질수록 펄스 폭이 1500us 기준으로 ±1000us 변함 (500~2500us 범위)


서보모터 동작 원리 및 구현

동작 원리

SG90 서보는 PWM 신호로 제어.

PWM 주기 20ms (50Hz), 펄스 폭(Duty Cycle)으로 각도 결정

Duty_cycle : PWM 주기 안에서 High(ON) 시간의 비율(%)

  • 주기(Period): 전체 반복 시간 (서보는 20ms = 50Hz)
  • 펄스 폭(Pulse Width): High인 시간 (서보 제어 핵심: 1.0~2.0ms)
    • 1.5ms → 0°
    • 2.0ms → 90°
    • 1.0ms → -90°

Datasheet 근거

  • PWM 모드는 TIMx_ARR로 주파수(예: 50Hz)를 정하고, TIMx_CCRx로 듀티 사이클(펄스 폭)을 정해서 원하는 신호를 만들 수 있다 .
  • 각 채널마다 독립적으로 PWM 모드를 설정할 수 있고, 제대로 시작하려면 프리로드와 초기화 순서가 중요 →RM0368 Ch.13 (General-purpose timers) - 13.3.9
  • PWM 신호의 극성(High/Low 반전) CCxP bit에 설정

→ HAL_TIM_PWM_Start () 함수 정의를 확인

구현 함수 및 듀티 사이클 전압 관계

  • CubeMX 설정
    • TIM2/TIM1 - PWM Generation CH1, Prescaler=83, Period=19999 (50Hz).
  • 코드 구현 (main.c, 듀티 사이클 관계 반영)

    1단계: 타이머 하드웨어 초기화 (MX_TIMx_Init ())

    // MX_TIM2_Init() - 서보1 (PA0, TIM2_CH1)
    htim2.Init.Prescaler = 83;           // 클럭 분주 (84MHz / 84 = 1MHz)
    htim2.Init.Period = 19999;           // 주기 = 20ms (50Hz) → 서보 표준
    sConfigOC.OCMode = TIM_OCMODE_PWM1;  // PWM mode 1 (정상 방향)
    sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;  // High active
    
    // MX_TIM1_Init() - 서보2 (PA8, TIM1_CH1)
    htim1.Init.Prescaler = 83;
    htim1.Init.Period = 19999;           // 동일하게 20ms
    sConfigOC.OCMode = TIM_OCMODE_PWM1;

    2단계: PWM 출력 시작 - main()

    HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);  // PA0 - 서보1 시작
    HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);  // PA8 - 서보2 시작

    3단계: 실시간 펄스 폭 계산 (while 루프 안) + 안전 범위 제한 (서보 보호)

    uint32_t pulse1 = 1500;  // 기본값 = 1.5ms (중앙 90°)
    if (x > center_x + deadzone) {  // 조이스틱 오른쪽으로 기울면
      pulse1 = 1500 + (x - center_x - deadzone) * 1000 / (4095 - center_x - deadzone);
    } else if (x < center_x - deadzone) {  // 왼쪽으로 기울면
      pulse1 = 1500 - (center_x - deadzone - x) * 1000 / (center_x - deadzone);
    }
    
    if (pulse1 < 500) pulse1 = 500;    // 최소 0.5ms (0°)
    if (pulse1 > 2500) pulse1 = 2500;  // 최대 2.5ms (180°)
    조이스틱 움직임 → 펄스 폭 변화 (0.5ms ~ 2.5ms 범위)
    • SG90은 물리적으로 0.5~2.5ms 펄스만 허용

    • 이 범위를 넘으면 기어 손상 위험 → 코드에서 강제 제한

      4단계: 실제 PWM 신호 적용 (핀에 출력)

      __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, pulse1);  // 서보1 (PA0)
      __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, pulse2);  // 서보2 (PA8)
    • TIMx_CCRx 레지스터에 pulse 값을 쓰면 타이머가 자동으로 CCR 값만큼 High 유지

      듀티 사이클 변경

    • 서보 내부 회로가 이 펄스 폭을 읽어서 각도로 변환 → 회전

HAL_Delay(20)) 로 20ms 지연 → 펄스 주기에 맞게 delay 사용


느낀 점 및 보완사항

  • HAL 라이브러리 활용 ====> Low level 구현을 한번 진행해보자!
  • Polling 방식 구현 → DMA를 활용해 직접 레지스터 접근 구현 예정
    • 지금은 continuos mode의 이점을 활용하지 못하고 있다.

이어서 구현 과정에서 발생한 어려움과 해결과정을 위주로 정리해보도록 하겠습니다!!

profile
세상의 어려운 문제를 해결하자

0개의 댓글