01강. CubeMX가 만들어준 코드는 '완성'이 아니다.

 01강. CubeMX가 만들어준 코드는 '완성'이 아니다.

CubeMX 코드는 끝이 아니라 시작입니다. 자동 생성된 main.c에 비즈니스 로직을 채워 넣는 습관을 버리고, 유지보수 가능한 펌웨어 아키텍처의 첫걸음을 떼는 방법을 소개합니다..

입사 후 처음 맡은 프로젝트, 열심히 main.c/* USER CODE BEGIN */ 주석 사이에 코드를 짜넣었습니다. 그런데 갑자기 하드웨어 팀에서 "핀 맵이 조금 바뀌었어요."라며 회로도를 다시 줍니다. 떨리는 마음으로 CubeMX에서 Generate Code를 눌렀는데... 😱

혹시 며칠 밤새 작성한 소중한 코드가 엉키거나 사라져 버린 경험, 다들 한 번쯤 있으시죠? 오늘은 10년 차 선배로서 여러분이 이 '재성성(Regeneration)의 늪'에서 탈출할 수 있는 가장 확실한 설계 원칙을 알려드리려 합니다. 기능 구현도 중요하지만, 그보다 먼저 갖춰야 할 것은 '무너지지 않는 구조'입니다. 😊

1. CubeMX가 주는 코드는 '완성품'이 아닙니다 🤔

많은 주니어 개발자분들이 CubeMX나 CubeIDE가 생성해 준 프로젝트를 '개발의 베이스캠프'가 아닌 '최종 목적지'로 착각하곤 합니다. 하지만 냉정하게 말해서, 자동 생성된 코드는 단지 하드웨어를 깨우는(Init) 과정일 뿐입니다.

여러분이 구현해야 할 '비즈니스 로직(Business Logic)'은 하드웨어 초기화 코드와 철저히 분리되어야 합니다. 그렇지 않으면 칩을 변경하거나, 핀 설정이 바뀔 때마다 여러분의 코드는 위협받게 됩니다.

💡 핵심 원칙!
main.c는 펌웨어의 '진입점(Entry Point)' 역할만 해야 합니다. 실제 프로그램의 지능은 별도의 모듈에 존재해야 합니다.

 

2. 스파게티 main.c 탈출하기: 실전 코드 비교 📊

백문이 불여일견이죠. 주니어 개발자의 흔한 실수와 이를 개선한 아키텍처 코드를 비교해 보겠습니다.

❌ 흔한 초보자의 main.c (Before)

모든 로직이 while(1) 안에 섞여 있고, HAL 라이브러리가 직접 호출됩니다.

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART2_UART_Init();

  /* USER CODE BEGIN 2 */
  int sensorValue = 0; // 지역 변수 남발
  /* USER CODE END 2 */

  while (1)
  {
    /* USER CODE BEGIN 3 */
    // 하드웨어 의존적인 코드가 비즈니스 로직과 뒤섞임
    if(HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_SET) {
        sensorValue++;
        HAL_UART_Transmit(&huart2, (uint8_t*)"Button Pressed\r\n", 16, 100);
        HAL_Delay(100); 
    }
    // 코드가 길어질수록 main 함수는 수천 줄이 됩니다.
  }
  /* USER CODE END 3 */
}
        

⭕ 아키텍처가 적용된 main.c (After)

App_Init()App_Process() 단 두 줄로 모든 것을 위임합니다.

/* 별도의 헤더 파일 포함 */
#include "ap.h" 

int main(void)
{
  /* MCU 하드웨어 초기화 (CubeMX 자동 생성 영역) */
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART2_UART_Init();

  /* USER CODE BEGIN 2 */
  // 1. 애플리케이션 초기화 진입
  App_Init(); 
  /* USER CODE END 2 */

  while (1)
  {
    /* USER CODE BEGIN 3 */
    // 2. 애플리케이션 무한 반복 처리
    App_Process(); 
  }
  /* USER CODE END 3 */
}
        

보이시나요? 이제 main.c는 아무리 자동 생성을 다시 해도 안전합니다. 우리의 소중한 코드는 별도의 파일(ap.c 등)에 안전하게 보관되어 있으니까요.

 

3. App_Init과 App_Process 구현 방법 👩‍💻

그럼 이 두 함수는 어떻게 만들어야 할까요? 프로젝트에 src/ap와 같은 폴더를 만들고 ap.cap.h를 생성하세요. 이것이 바로 하드웨어와 소프트웨어를 분리하는 첫 단추입니다.

  • App_Init(): 하드웨어가 준비된 후, 소프트웨어 모듈(타이머, 통신 버퍼, FSM 등)을 초기화합니다.
  • App_Process(): while(1) 문 안에서 계속 호출되며, 폴링(Polling) 방식이나 상태 머신(FSM)을 수행합니다.
⚠️ 주의하세요!
App_Process() 내부에서 HAL_Delay()를 사용하여 흐름을 막으면(Blocking) 안 됩니다. 대신 millis()와 같은 비동기 타이머 방식을 사용해야 멀티태스킹과 유사한 동작을 구현할 수 있습니다. (이 부분은 다음 강의에서 다룹니다!)

 

📝

제1강 핵심 요약

✨ CubeMX의 역할: 단순한 하드웨어 초기화 도구일 뿐입니다.
❌ 피해야 할 것: USER CODE BEGIN 주석 사이에 비즈니스 로직 욱여넣기.
✅ 해야 할 것:
main.c = App_Init() + App_Process()
🚀 기대 효과: 코드 재사용성 증가 및 하드웨어 변경 시 멘탈 붕괴 방지.

자주 묻는 질문 ❓

Q: 함수를 분리하면 호출 오버헤드 때문에 느려지지 않나요?
A: 현대의 MCU(특히 STM32F4 급)에서 함수 호출 오버헤드는 무시할 수 있을 만큼 작습니다. 오히려 스파게티 코드로 인한 논리적 오류와 디버깅 시간 낭비가 훨씬 더 큰 비용입니다.
Q: C++로 개발할 때도 같은 방식을 쓰나요?
A: 네, 더 적극적으로 씁니다. main.c는 C언어 파일이므로, 보통 App_Init() 내부에서 C++ 객체를 생성하고 래퍼(Wrapper) 함수를 통해 연결하는 방식을 사용합니다.
Q: ap.c 파일은 어디에 만들어야 하나요?
A: Core/Src 폴더에 만들어도 되지만, 저는 별도의 폴더(예: src/common)를 만들어 관리하는 것을 추천합니다. 이렇게 하면 나중에 다른 프로젝트로 코드를 복사하기 쉽습니다.

오늘은 펌웨어 아키텍처의 가장 기초인 '진입점 분리'에 대해 알아봤습니다. 처음엔 파일이 늘어나서 귀찮을 수 있지만, 프로젝트가 커질수록 이 구조가 여러분을 구원해 줄 것입니다.

혹시 여러분만의 프로젝트 관리 팁이 있나요? 아니면 적용하면서 어려운 점이 있었나요? 다음 시간에는 '하드웨어 독립적인 타이머 만들기'로 찾아오겠습니다! 😊


목차로 이동

다음 이전