10. 조이스틱으로 DC 모터 제어하기 + 비상 멈춤버튼 구현 (feat. freeRTOS)

owljun·2025년 8월 4일

학습 목표

  • 조이스틱 Analog 신호를 ADC 핀으로 입력받아 GPIO PWM 출력을 통해 DC모터 제어
  • freeRTOS 활용 우선순위 Task 실행
  • Micom 드라이버 활용 (HAL)

시연 영상

조이스틱을 통한 DC 모터 제어 도중 비상 버튼 인터럽트 발생시 즉시 동작중단.
Toggle 버튼을 활용하여, 다시 시작 가능하다.

H/W 구성


TASK 흐름도

구현 코드

  • freeRTOS
  osThreadId_t joystickTaskHandle;
  const osThreadAttr_t joystickTaskAttr = {
    .name = "joystickTask",
    .stack_size = 128 * 4,
    .priority = (osPriority_t) osPriorityNormal,
  };
  osKernelInitialize();
  joystickTaskHandle = osThreadNew(JoystickTask, NULL, &joystickTaskAttr);
  osKernelStart();
  • 모터 제어 함수정의
void Motor_Init()
{
	HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_RESET);
	HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET);
}

void Motor_Forward(uint16_t speed)
{
	HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_RESET);
	HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET);

	__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, speed);
}

void Motor_Backward(uint16_t speed)
{
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_SET);   // IN2 = 1
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET); // IN1 = 0

    __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, speed);
}

void Motor_Stop()
{
    __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, 0);       // PWM = 0

    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_RESET); // IN2 = 0
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET); // IN1 = 0
}
  • 조이스틱 값 읽기 및 Task 정의
uint16_t Read_Joystick()
{
    HAL_ADC_Start(&hadc1);
    HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);
    return HAL_ADC_GetValue(&hadc1);
}
void JoystickTask(void *argument)
{
    uint16_t x_val;

    for(;;)
    {
        if (!stopFlag)
        {
            x_val = Read_Joystick();

            if (x_val > 2300)
                Motor_Forward((x_val - 2048) * 2);
            else if (x_val < 1800)
                Motor_Backward((2048 - x_val) * 2);
            else
                Motor_Stop();
        }
        else
        {
            Motor_Stop(); // 멈춘 후 반복 중지
        }

        osDelay(10);
    }
}
  • 인터럽트 콜백 정의
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == GPIO_PIN_13) // 블루버튼 기준 (PA13 또는 PC13)
    {
        stopFlag ^= 1; // toggle
        Motor_Stop();  // 즉시 멈춤
    }
}

회고

이번 프로젝트의 목적은 단순한 GPIO 제어를 넘어서, FreeRTOS의 Task 관리 흐름을 직접 구성하며 시스템 제어를 구조화하는 것이었다.

1. RTOS 도입 의도와 변경

초기 설계에서는 EXTI 인터럽트 → RTOS Task 통신 구조를 염두에 두고,
인터럽트에서 osThreadFlagsSet()이나 vTaskNotifyGiveFromISR() 등을 활용해 RTOS 스케줄러로 작업을 넘길 계획이었다.

그러나 구현 과정에서 버튼 인터럽트에 의한 로직이 너무 단순하고 짧다는 사실을 확인했고,
이러한 작업까지 굳이 Task로 분리하게 되면 오히려 컨텍스트 스위칭 비용, Task wake-up latency 등으로 인해 실시간성이 손해를 본다는 점을 체감했다.

결국, 인터럽트 콜백 내에서 직접 Motor_Stop()을 호출하고,
stopFlag 제어도 Task 외부에서 수행하는 방식으로 설계를 간소화했다.


2. RTOS 도입의 타당성 재검토

이번 구현은 Task가 단 하나뿐이고, 인터럽트 핸들링도 매우 가볍다.
이런 상황에서는 FreeRTOS를 굳이 사용할 필요가 없으며,
Bare-metal 방식이 더 경량이며 실시간성도 보장되는 구조다.

하지만 RTOS의 구조적 장점은 향후 시스템 확장 시 명확히 드러난다.
예를 들어:

  • 센서 Task, 디스플레이 Task, 통신 Task 등이 독립적으로 동작하는 경우
  • 긴 연산 또는 블로킹 IO를 Task로 분리해 우선순위 기반 처리해야 하는 경우
  • 자원 공유를 세마포어, 큐 등을 통해 통제해야 하는 경우

지금은 RTOS가 과한 설계였지만, "왜 과했는가"를 직접 구현하며 체득한 경험 자체가 중요했고,
앞으로 시스템 규모에 따라 적절한 구조와 기술스택을 선택하는 안목을 키우는 데 큰 도움이 되었다.


3. 핵심 교훈

  • 인터럽트 핸들링은 무조건 RTOS Task로 넘긴다고 좋은 게 아니다.
    실시간성 보장, 실행 시간, 컨텍스트 전환 비용을 반드시 고려해야 한다.

  • Task는 "병렬성"이 아니라 "역할 분리"가 있을 때만 도입해야 의미가 있다.

  • 기술을 쓰는 것보다, "왜 안 쓰는지 설명할 수 있을 때" 더 잘 이해한 것이다.

profile
Embedded S/W Developer :)

0개의 댓글