입사 후 처음 맡은 프로젝트, 열심히 main.c의 /* USER CODE BEGIN */ 주석 사이에 코드를 짜넣었습니다. 그런데 갑자기 하드웨어 팀에서 "핀 맵이 조금 바뀌었어요."라며 회로도를 다시 줍니다. 떨리는 마음으로 CubeMX에서 Generate Code를 눌렀는데... 😱
혹시 며칠 밤새 작성한 소중한 코드가 엉키거나 사라져 버린 경험, 다들 한 번쯤 있으시죠? 오늘은 10년 차 선배로서 여러분이 이 '재성성(Regeneration)의 늪'에서 탈출할 수 있는 가장 확실한 설계 원칙을 알려드리려 합니다. 기능 구현도 중요하지만, 그보다 먼저 갖춰야 할 것은 '무너지지 않는 구조'입니다. 😊
1. CubeMX가 주는 코드는 '완성품'이 아닙니다 🤔
많은 주니어 개발자분들이 CubeMX나 CubeIDE가 생성해 준 프로젝트를 '개발의 베이스캠프'가 아닌 '최종 목적지'로 착각하곤 합니다. 하지만 냉정하게 말해서, 자동 생성된 코드는 단지 하드웨어를 깨우는(Init) 과정일 뿐입니다.
여러분이 구현해야 할 '비즈니스 로직(Business Logic)'은 하드웨어 초기화 코드와 철저히 분리되어야 합니다. 그렇지 않으면 칩을 변경하거나, 핀 설정이 바뀔 때마다 여러분의 코드는 위협받게 됩니다.
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.c와 ap.h를 생성하세요. 이것이 바로 하드웨어와 소프트웨어를 분리하는 첫 단추입니다.
- App_Init(): 하드웨어가 준비된 후, 소프트웨어 모듈(타이머, 통신 버퍼, FSM 등)을 초기화합니다.
- App_Process():
while(1)문 안에서 계속 호출되며, 폴링(Polling) 방식이나 상태 머신(FSM)을 수행합니다.
App_Process() 내부에서 HAL_Delay()를 사용하여 흐름을 막으면(Blocking) 안 됩니다. 대신 millis()와 같은 비동기 타이머 방식을 사용해야 멀티태스킹과 유사한 동작을 구현할 수 있습니다. (이 부분은 다음 강의에서 다룹니다!)
제1강 핵심 요약
자주 묻는 질문 ❓
오늘은 펌웨어 아키텍처의 가장 기초인 '진입점 분리'에 대해 알아봤습니다. 처음엔 파일이 늘어나서 귀찮을 수 있지만, 프로젝트가 커질수록 이 구조가 여러분을 구원해 줄 것입니다.
혹시 여러분만의 프로젝트 관리 팁이 있나요? 아니면 적용하면서 어려운 점이 있었나요? 다음 시간에는 '하드웨어 독립적인 타이머 만들기'로 찾아오겠습니다! 😊

