STM32 레지스터, 이 1비트 때문에 3시간이 걸렸다
- 03 Dec, 2025
STM32 레지스터, 이 1비트 때문에 3시간이 걸렸다
오전 10시, 시작은 평범했다
출근했다. 커피 마시고 보드 켰다. 어제 작성한 UART 코드 확인할 차례.
STM32F4. 익숙한 칩이다. 이번 프로젝트에서 벌써 세 번째 보드. UART2로 센서 데이터 받아서 USB로 PC에 전송하는 펌웨어. 간단해 보였다.
코드 작성은 30분. HAL 라이브러리 쓰면 금방이다. 빌드, 업로드. 정상.
그런데 데이터가 안 온다.

터미널 열었다. 아무것도 없다. 센서는 분명 데이터 보내고 있다. 오실로스코ープ로 확인했으니까.
‘설정 문제겠지.’
보레이트 확인. 115200. 맞다. 패리티 없음. 스톱비트 1. 다 맞다.
HAL_UART_Receive() 호출. 리턴값은 HAL_OK. 근데 버퍼는 비어있다.
이상하다.
오전 11시, 기본부터 다시
문제를 단순화했다. 센서 연결 끊고 UART 루프백 테스트.
TX와 RX 핀 쇼트. 자기가 보낸 거 자기가 받는 거다. 이것도 안 되면 설정 문제 확실.
코드 수정했다.
uint8_t tx_data = 0x55;
uint8_t rx_data = 0x00;
HAL_UART_Transmit(&huart2, &tx_data, 1, 100);
HAL_UART_Receive(&huart2, &rx_data, 1, 100);
실행. rx_data는 여전히 0x00.
‘HAL 문제인가?’
레지스터 직접 봤다. USART2->SR 레지스터. Status Register.
TXE(Transmit Data Register Empty) 비트는 1. 전송 완료됐다는 뜻.
RXNE(Read Data Register Not Empty) 비트는 0. 데이터 안 받았다는 뜻.
분명 TX에서 나간 신호가 RX로 들어와야 하는데.
오실로스코프 다시 꺼냈다.

파형 확인. TX 핀에서 정상적으로 신호 나온다. 0x55. 10101010. 깔끔하다.
RX 핀도 똑같다. 물리적으로는 신호 들어온다.
그럼 문제는 소프트웨어다.
정오, 스펙 문서 지옥
Reference Manual 펼쳤다. RM0090. 1700페이지.
USART 챕터. 30장. 60페이지 분량.
레지스터 맵 표 봤다. USART_CR1, CR2, CR3. Control Register.
하나씩 확인.
CR1:
- UE(USART Enable): 1. 맞다.
- TE(Transmitter Enable): 1. 맞다.
- RE(Receiver Enable): 1. 맞다.
CR2:
- STOP bits: 00. 1 스톱비트. 맞다.
CR3:
- 다 0. 기본값.
‘설정은 다 맞는데?’
HAL 코드 뜯어봤다. HAL_UART_Init() 함수. 뭐 하는지 하나씩.
클럭 활성화. GPIO 설정. USART 레지스터 초기화. 순서대로.
이상 없어 보인다.
점심시간. 편의점 김밥 사 왔다. 먹으면서도 생각했다.
‘뭘 놓쳤지?‘
오후 2시, 다른 접근
HAL 버리고 레지스터 직접 제어하기로 했다.
새 프로젝트 만들었다. HAL 없이. 레지스터만.
// 클럭 활성화
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
// GPIO 설정 (PA2: TX, PA3: RX)
GPIOA->MODER |= (2 << 4) | (2 << 6); // Alternate function
GPIOA->AFR[0] |= (7 << 8) | (7 << 12); // AF7 (USART2)
// USART 설정
USART2->BRR = 0x16D; // 115200 baud (16MHz / 115200)
USART2->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;
빌드. 업로드. 똑같다. 전송은 되는데 수신은 안 된다.

‘GPIO 문제인가?’
PA3 핀 설정 다시 봤다. Alternate Function 맞다. AF7 맞다.
‘풀업? 풀다운?’
GPIOA->PUPDR &= ~(3 << 6); // No pull
원래 설정 안 했었다. 기본값이 No pull이니까.
혹시나 해서 풀업으로 바꿔봤다.
GPIOA->PUPDR |= (1 << 6); // Pull-up
실행. 여전히 안 된다.
오후 3시, 커뮤니티 검색
구글 켰다. “STM32 UART not receiving”.
Stack Overflow. 비슷한 질문 수십 개.
- 보레이트 안 맞음 → 내 경우는 아님
- 클럭 설정 틀림 → 확인했음
- GPIO AF 잘못 설정 → 맞음
- 인터럽트 충돌 → 폴링으로 하는데
도움 안 됐다.
STM32 공식 포럼. 2018년 글 하나.
“UART RX not working on PA3, but works on PA10.”
‘핀을 바꿔?’
우리 보드는 PA3만 라우팅되어 있다. PA10은 다른 용도.
댓글 읽었다. “Check JTAG pin conflict.”
JTAG?
데이터시트 봤다. PA3는… 그냥 GPIO다. JTAG 아니다.
막혔다.
오후 4시, 실험 시작
논리적으로 안 풀리면 실험이다.
모든 레지스터 값 프린트했다.
printf("CR1: 0x%08X\n", USART2->CR1);
printf("CR2: 0x%08X\n", USART2->CR2);
printf("CR3: 0x%08X\n", USART2->CR3);
printf("SR: 0x%08X\n", USART2->SR);
printf("BRR: 0x%08X\n", USART2->BRR);
출력:
CR1: 0x0000200C
CR2: 0x00000000
CR3: 0x00000000
SR: 0x000000C0
BRR: 0x0000016D
Reference Manual과 비교.
CR1: 0x0000200C
- Bit 13 (UE): 1
- Bit 3 (TE): 1
- Bit 2 (RE): 1
맞다.
SR: 0x000000C0
- Bit 7 (TXE): 1
- Bit 6 (TC): 1
전송은 완료됐다. 근데 RXNE는 0.
‘레지스터 값은 정상인데 왜 안 되는 거야?’
그때 생각났다. CR1 말고 CR3.
CR3: 0x00000000.
스펙 읽었다. CR3 비트들.
- Bit 10 (CTSIE): CTS interrupt
- Bit 9 (CTSE): CTS enable
- Bit 8 (RTSE): RTS enable
- …
다 0. 우리는 하드웨어 플로우 컨트롤 안 쓴다.
근데 한 줄이 눈에 들어왔다.
“Bit 0 (EIE): Error interrupt enable”
‘에러 인터럽트?’
관련 없어 보였다. 우리는 폴링 방식이니까.
그래도 혹시나 싶어서 켜봤다.
USART2->CR3 |= USART_CR3_EIE;
빌드. 업로드.
똑같다.
오후 5시, 1비트의 비밀
CR1 다시 봤다. 한 비트씩.
Bit 15~14: OVER8. 오버샘플링.
Reference Manual: “0: oversampling by 16 (default)” “1: oversampling by 8”
우리는 0. 기본값.
Bit 13: UE. USART Enable. 1.
Bit 12: M. Word length. “0: 1 Start bit, 8 Data bits”
- 맞다.
Bit 11: WAKE. 웨이크업 방식. 관련 없다.
Bit 10: PCE. Parity Control Enable. 0. 패리티 없음.
Bit 9: PS. Parity Selection. 관련 없다.
Bit 8: PEIE. PE interrupt enable.
‘잠깐.’
PEIE. Parity Error Interrupt Enable.
“This bit is set and cleared by software.”
설정은 소프트웨어가 한다. 맞다.
“Default value: 0”
기본값 0.
근데 주석 하나 더 있었다.
“Note: This bit should be cleared before USART is enabled (UE=1).”
‘UE 켜기 전에 클리어?’
기본값이 0인데 왜 클리어?
그 순간 깨달았다.
‘기본값이 0이 아닐 수도 있다.’
리셋 후 레지스터 초기값. 스펙에는 0이라고 나와 있다.
근데 실제로는?
코드 수정했다. UE 켜기 전에 명시적으로 클리어.
USART2->CR1 &= ~USART_CR1_PEIE; // 명시적 클리어
USART2->CR1 |= USART_CR1_UE; // 그 다음 Enable
빌드. 업로드.
터미널 열었다.
데이터 들어온다.
”…”
3시간.
오후 5시 30분, 원인 분석
왜 PEIE 비트가 문제였을까?
추측: 전에 다른 펌웨어 테스트하다가 비트가 1로 설정됐다. 리셋했지만 일부 레지스터는 유지됐다.
아니면: 보드 전원 문제. 완전히 방전되지 않으면 일부 값이 남아있다.
확인 방법: 보드 전원 완전히 끄고 30초 기다린 후 다시 켜기.
해봤다. 그 상태에서는 원래 코드도 작동했다.
‘역시.’
스펙 문서는 “리셋 후 기본값”을 명시한다. 근데 “전원 차단 후”는 보장 안 한다.
개발 중에는 전원 계속 켜놓고 리셋만 누른다. 그 상태에서 레지스터 일부가 유지될 수 있다.
교훈: 레지스터 설정할 때 “기본값이니까 안 써도 되겠지” 하지 말 것.
명시적으로 써라. 모든 비트를.
// 나쁜 예
USART2->CR1 = USART_CR1_UE; // 다른 비트는 기본값이겠지?
// 좋은 예
USART2->CR1 = 0; // 일단 초기화
USART2->CR1 |= USART_CR1_TE | USART_CR1_RE | USART_CR1_UE; // 필요한 것만
오후 6시, 코드 정리
전체 코드 리팩토링했다.
모든 레지스터 초기화 코드에 명시적 클리어 추가.
void UART_Init(void) {
// 클럭
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
// GPIO 완전 초기화
GPIOA->MODER &= ~((3 << 4) | (3 << 6));
GPIOA->MODER |= (2 << 4) | (2 << 6);
GPIOA->AFR[0] &= ~((0xF << 8) | (0xF << 12));
GPIOA->AFR[0] |= (7 << 8) | (7 << 12);
// USART 완전 초기화
USART2->CR1 = 0; // 모든 비트 클리어
USART2->CR2 = 0;
USART2->CR3 = 0;
USART2->BRR = 0x16D;
USART2->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;
}
테스트. 전원 켰다 껐다 반복. 10번.
매번 정상 작동.
이제 됐다.
오후 7시, 회고
3시간이 걸렸다.
1비트 때문에.
스펙 문서에는 “기본값: 0”이라고 나와 있었다.
근데 실제로는 아니었다.
임베디드는 이런 거다. 가정하면 안 된다. 모든 걸 명시해야 한다.
“당연히 0이겠지” → 3시간 날림
“명시적으로 0을 쓰자” → 10초 추가, 3시간 절약
다음부터는:
- 레지스터 쓸 때 전체를 초기화
- 필요한 비트만 OR로 설정
- 기본값 믿지 말기
HAL 라이브러리도 완벽하지 않다. 내부 구현 확인해야 한다.
저수준은 이런 맛이다.
웹 개발자들은 모를 거다. 변수 초기화 안 해도 undefined일 뿐. 프로그램은 돌아간다.
우리는 다르다. 1비트가 전체를 멈춘다.
그래도 찾아냈을 때의 쾌감. 이것 때문에 한다.
내일은 SPI다. 또 무슨 일이 있을까.
