HAL 드라이버 없이, 레지스터 접근만으로 UART 통신 예제를 만들었다. 처음 시작할 때는 어디서부터 시작해야 할 지 몰라 막막했고, 하나하나 이해하면서 진행하느라 완성하는 데 약 2시간이 걸렸다. 전체 흐름을 이해하니 생각보다 별것 아니라는 것을 알게 되었다. 레지스터에 직접 접근하는 것도 익숙해졌고, 각 페리페럴에 공급될 클록 신호가 만들어지는 과정도 살펴볼 수 있었다.
프로젝트를 만들 때 HAL 드라이버를 포함하지 않았기 때문에 접근할 레지스터 주소를 데이터 시트를 확인해가며 직접 입력하여 만들었다. Reference Manual(RM0008)의 3.3 Memory Map을 참고하면 좋다. 이번 예제에 필요한 레지스터 주소는 아래와 같다. (사실 USART1은 사용하지 않는데 모르고 만들었다. 아깝기도 하고 다음에 재사용할 것이므로 지우지 않았다.)
#define RCC_BASE (0x40021000)
#define RCC_APB2ENR (*(volatile unsigned int*) (RCC_BASE + 0x18))
#define RCC_APB1ENR (*(volatile unsigned int*) (RCC_BASE + 0x1C))
#define GPIOA_BASE (0x40010800)
#define GPIOA_CRL (*(volatile unsigned int*) (GPIOA_BASE + 0x00))
#define GPIOA_CRH (*(volatile unsigned int*) (GPIOA_BASE + 0x04))
#define USART1_BASE (0x40013800)
#define USART1_SR (*(volatile unsigned int*) (USART1_BASE))
#define USART1_DR (*(volatile unsigned int*) (USART1_BASE + 0x04))
#define USART1_BRR (*(volatile unsigned int*) (USART1_BASE + 0x08))
#define USART1_CR1 (*(volatile unsigned int*) (USART1_BASE + 0x0C))
#define USART2_BASE (0x40004400)
#define USART2_SR (*(volatile unsigned int*) (USART2_BASE))
#define USART2_DR (*(volatile unsigned int*) (USART2_BASE + 0x04))
#define USART2_BRR (*(volatile unsigned int*) (USART2_BASE + 0x08))
#define USART2_CR1 (*(volatile unsigned int*) (USART2_BASE + 0x0C))
NUCLEO-F103RB 개발 보드에 있는 USB를 통해 시리얼 통신을 할 수 있는 포트는 UART2 이다. 그리고 UART2는 GPIOA의 2번 3번 핀을 사용한다. 따라서 가장 먼저 UART2와 GPIOA가 동작할 수 있도록 클록 신호를 공급해주는 설정을 해주었다. UART2는 APB1ENR, GPIOA는 APB2ENR 레지스터에서 설정할 수 있다.
RCC_APB2ENR |= 0x4; // IOPAEN set
RCC_APB1ENR |= 0x20000; // USART2EN set
그다음은 GPIO 설정이다. GPIOA 2번 3번 핀을 각각 TX, RX로 사용할 수 있도록 했다. PIN2는 대체 기능(Alternate function)을 사용하는 push-pull output으로 설정했다. PIN3 설정은 초깃값 그대로 사용했다. 초깃값은 floating input으로 설정되어 있다.
GPIOA_CRL |= (3 << 8) | (2 << 10);
GPIOA_CRL &= ~(1 << 10);
이제 UART를 설정할 차례이다. 보드 레이트는 9600으로 설정하려는데 여기서 고민할 게 생겼다. UART2로 공급되는 클록 주파수를 알아야 보드 레이트 설정값을 계산할 수 있기 때문이다. 시스템 클록에 관한 설정은 아무것도 건드리지 않았으므로 초깃값이 무엇인지 알면 해결될 것 같았다. 그렇다면 어느 레지스터를 확인하면 될까 생각하다가 다이어그램 하나가 보였다.
별표 표시한 곳에 UART2가 있으므로 그곳까지 거치는 레지스터 값을 확인하면 되겠다고 생각했다. 우선 확인해야 할 값은 SW
값이다. RCC 레지스터 맵을 찾아보니 RCC_CFGR이라는 레지스터에서 SW
(System clock switch) 값 설정을 위한 비트 필드가 있는 것을 확인했다. 이어서 AHB prescaler, APB1 prescaler 설정 초깃값을 찾아 확인해보니 UART2에는 칩 내부에서 발생시킨 8MHz 클록 신호가 그대로 전달되는 것을 알게 되었다.
보드 레이트가 9600이 되도록 하는 설정값을 구하고 UART2의 BRR 레지스터에 값을 설정했다. 그리고 UART2로 송수신 모두 사용할 것이므로 RX, TX를 활성화했다. 물론 UART2 자체도 활성화했다.
USART2_BRR = 0x341;
USART2_CR1 |= (0x200C);
이것으로 초기 설정을 끝냈다. 이제는 메인 루프에서 UART 수신을 감지하고 입력된 값을 그대로 송신하는 코드를 작성할 차례다. UART 수신 여부(RXNE, Read data register not empty)와 송신 완료 여부(TC, Transmission complete)는 SR 레지스터를 확인하면 된다. SR 레지스터는 UART의 상태 레지스터(Status register)이다. 수신된 데이터를 가져오기 위해서는 DR 레지스터값을 읽으면 된다. 반대로 송신하려면 DR 레지스터에 값을 쓰면 된다. 아주 간단하다.
while(1)
{
if (USART2_SR & (1 << 5))
{
char ch = USART2_DR;
USART2_DR = ch;
while(!(USART2_SR & (1 << 6)));
}
}
당연하게도 예제 코드를 작성하는 동안 한 번에 성공하지는 못했다. 레지스터 주소를 엉뚱하게 적거나, 설정 비트를 잘못 입력하는 등 자잘한 실수가 잦았다. 그럴 때마다 디버그 모드에서 코드를 한 스텝씩 실행해보고, 디스어셈블해서 보기도 하고 SFRs(Special Function Registers) 도구로 레지스터값을 들여다보며 무엇을 실수했는지 찾아가며 고쳐나갔다.
이 과정을 거치고 나니 막연했던 것들이 선명하게 보이기 시작했다. MCU 내부 구조를 파악하는 방법을 익힐 수 있었고 HAL이라는 편리한 도구 뒤에 가려진 실제 동작을 이해하는 데 큰 도움이 되었다. 물론 생산성 측면에서는 HAL을 쓰는 게 좋겠지만 레지스터 직접 접근을 통해 개발하는 것도 익숙해지면 나름 장점이 많을 것으로 생각한다.
참고 자료: 링크
'연구 노트 > Embedded' 카테고리의 다른 글
[ODROID] Vu8M 디스플레이 연결 설정하기 (0) | 2022.08.29 |
---|---|
GNU 어셈블러와 링커 스크립트 매뉴얼 (0) | 2021.04.22 |
ABI, EABI 그 의미를 이해하고 툴 체인 이름까지 해석하기 (0) | 2021.01.29 |
QEMU 사용시 쉘 종류에 따라서 옵션 값이 달라지는 문제 (0) | 2021.01.20 |
Windows 10에서 RTOS 개발환경 구축하기 3 - WSL 2에서 GUI 프로그램을 실행하기 위한 준비 (2) | 2021.01.17 |