UART 가 무엇이냐
UART(Universal Asynchronous Receiver / Transmitter) 는 병렬 데이터를 비동기 직렬 방식으로 데이터를 전송하는 컴퓨터 HW(집적 회로)이다.
- 병렬 데이터 : 여러 개의 독립된 데이터 (H → 0100 1000 8개의 독립적인 bit)
- 직렬 방식 전송 : 데이터를 하나의 선을 통해 순서대로 하나씩 전송
- ↔ 병렬 방식 전송 : 데이터를 여러 선을 통해 한 번에 여러개씩 전송
- 비동기 : 클럭 없이 데이터만 주고받는다. 양 쪽에서 같은 프로토콜로 데이터를 주고받기로 미리 약속
ㅤ
잠깐, 동기 통신과 비동기 통신
동기 통신 : Synchronous Communication
- 보내는 쪽에서 CLK 정보를 함께 전달
- 받는 쪽에서는 CLK의 Edge 시점에 데이터를 읽는다.
- 추가적인 CLK 선이 필요하다고 하는데, 맨체스터 인코딩이나 클럭 임베딩 등의 방식으로 신호 자체에 CLK 정보를 포함(스크램블)시키는 방식들이 있음. → 이러한 경우에 PLL, OverSampling 드으이 방식으로 클럭을 복원해 신호로부터 데이터를 추출하기 위한 추가 cost가 들어간다.
- 고속 통신은 동기 통신을 사용함. (비동기 통신 시 양쪽 장치에서 만들어내는 CLK의 오차로 인해 값 통신에 오류가 있을 수 있다 — 백만 CLK 당 20~50CLK 오차 발생 가능함)
ㅤ

위 이미지는 I2C 통신하는 과정에서 CLK과 데이터 신호가 함께 들어오는 순간을 애널라이저로 직접 확인해본 데이터이다.
ㅤ
비동기 통신 : Asynchronous Communication
- 별도의 CLK 라인이 없음
- 보내는 쪽과 받는 쪽이 서로 약속된 속도로 독립적인 클럭을 이용해 신호를 주고받음.
- 각 데이터 프레임의 시작과 끝을 Start bit - Stop bit 으로 표시
- 추가적인 HW나 절차가 없어서 구현이 단순하다.
ㅤ

뒤에서 기술할 내용이지만, 위 이미지는 UART 통신으로 전달하는 신호를 애널라이저로 직접 확인해본 데이터이다. 비동기 통신을 이용하면 이렇게 CLK 정보 없이 약속된 내용만으로 신호에서 데이터를 추출해낼 수 있다.
ㅤ
어디에 쓰냐면?
- 장거리 통신
- 배선이 간단하고 멀리까지 신호를 전송할 수 있어서, 느려도 원거리 통신이 필요하다면 UART를 사용할 수 있다.
- 블루투스, GPS
- UART가 오래된 방식이라 호환되는 기술(프로토콜)이 많다.
- 위 2개가 약 20~30% 정도 되는 듯. (← 팩트체크 안됨)
- 디버깅
- PC와 MCU를 연결해 디버깅 용으로 많이 활용한다. (← Claude 발 정보로는 이게 90%+ 를 차지한다고 한다. 그래서 강사님께서 UART Printf 를 만들어두고 자주 사용한다고 하셨던건가 싶다 ㅋㅋㅋ)
ㅤ
비트레이트 - 보 레이트(Baud Rate)
어떻게 그러면 UART 같은 비동기 통신 방식에서는 CLK 정보 없이도 신호로부터 데이터를 뽑아낼 수 있는것이냐? 이를 알기 위해서는 비트레이트에 대해 파악해야한다.
ㅤ
헷갈리기 쉬운 용어
- Baud Rate (보: 레이트 라고 읽는 듯)
- 초당 신호 변화 횟수, 물리적인 신호의 전환 속도를 의미
- Bit Rate
- 초당 전송되는 비트 수, 논리적인 데이터 전송 속도를 의미
ㅤ
우리가 다루고 있는 STM32의 UART에서는 신호의 전압은 High / Low 2가지 선택지가 있다. 그러나 4-PAM 같은 통신에서는 전압의 단계가 4단계(0V / 1.5V / 3.5V / 5V)가 있다. 요런 경우에는 하나의 전압 신호가 2개의 Bit(00 / 01 / 10 / 11)을 의미할 수 있다. 즉, 신호가 한 번 변할 때 마다 2개의 Bit를 전송할 수 있다는 뜻. 이런 통신에서는 Bit Rate = Baud Rate * 2 가 될 수 있다. (초당 신호 수 * 신호 당 bit 수 = 초당 bit 수)
ㅤ
그러나 우리가 지금 보고 있는 STM32 UART 에서는 전압이 High / Low 2가지 뿐이므로 Baud Rate == Bit Rate 라고 생각해도 괜찮다. 다만 두 용어가 지칭하는 의미가 다르다는 것은 파악해두자!
ㅤ
원래 Baud Rate는 그래서 단위도 Bit Rate와 다르다. Baud Rate의 단위는 Baud를 사용하지만, 우리의 시스템에서는 이 두 값이 항상 동일하기 때문에, 아래 이미지에서처럼 Baud Rate의 단위를 Bit Rate의 단위인 Bits/s 를 사용하고 있는 것을 확인할 수 있다.

ㅤㅤ
TeraTerm 같은 프로그램에서 보면, Baud Rate가 이미 정해져있는 여러 값들 중에 하나를 선택해서 사용하고 있음을 확인할 수 있었다. 이게 일반적으로 많이 사용하는 표준 Baud Rate이다. ‘관습적으로’ 300의 배수를 사용한다.

ㅤ

ㅤ
Baud Rate는 다양한 환경적 영향을 받는다.
- 케이블의 길이 : 짧을수록 높은 Baud Rate를 사용할 수 있다. (지연, 감쇄 등의 영향을 덜 받기 때문인듯) 우리가 사용해보고 있는 일반적인 115,200 Bit/s 에서는 15m 정도의 케이블 길이라고 한다.
- 노이즈 환경 : 노이즈가 많은 환경일수록 낮은 속도가 권장된다. 아래에서 보겠지만, 샘플링을 통해 값들을 정확히 읽어내기 위해서는 너무 빠르면 노이즈 영향을 많이 받아서 힘들 것이라 추측된다.
- 클럭 소스 : 클럭의 소스가 누구냐(클럭이 얼마나 정확한지)에 따라서도 영향을 받는다. CLK도 노이즈라고 생각이 되는데, 크리스탈에서 만들어지는 CLK은 오차가 거의 없지만 RC를 이용해 전기적으로 만든 CLK은 오차가 커서 불안정하다.
ㅤ
그러면 CLK 정보 없이 어떻게 UART는 데이터를 읽을까?
앞서, UART IOC 설정을 보면 아래에 ‘OverSampling’ 이라는 항목이 있다. 이는 각 신호에 대해서 얼마나 잘게 쪼개어 값을 확인할 것인가? 를 말한다. 일반적으로 over sampling은 16(또는 8)Sample을 이용하며, 데이터를 보내기로 약속한 주파수보다 16배 더 높은 주파수로 신호를 쪼개고, 이 중에서 일부 값을 확인해 각 타이밍의 전압이 어떤 신호일지 추측한다.
ㅤ
아래 이미지처럼 각 타이밍의 신호에 대해서 16등분하여 샘플링을 하고, 그 중에서 보통 한가운데 있는 값들을 샘플링한다. 목적은 노이즈를 제거하는 것. 혹시나 신호가 0인데 하필 한가운데에 있는 bit를 가져올 때 노이즈가 껴서 신호가 1이 되어있으면 잘못된 값을 읽게 되는 것이니, 이를 막고자 여러 샘플링 값들을 확인한다.
ㅤ
레퍼런스 메뉴얼에서는 가운데 3개의 샘플링을 가져와서 값을 확인한다고 한다. 이때 연속 3개의 샘플링 값이 동일하다면 해당 값을 가져오지만, 혹시나 값이 다른 bit가 섞여있는 경우에는 신호에 현재 노이즈가 있다고 판단해 NE(Noise Error Flag) Status를 SET 하고 대략적으로 값을 해석해 넣는다. (이때 NE 노이즈에 대한 interrupt enable을 해뒀다면 interrupt handler가 호출됨! → 노이즈가 많은 환경인지 확인 가능!)


ㅤ
만약 oversampling을 16으로 하는 경우에, 통신에 사용할 수 있는 최대 주파수는 $f_{PCLK} / 16$ 라고 한다. 아무래도 통신에 사용하는 주파수보다 16배나 더 자주 비트를 쪼개어 확인해야 하기 때문에 최대 주파수를 샘플링 주파수에 맞춰주는 것으로 생각된다.

ㅤ
만약 샘플링 개수를 8개로 선택한다면 CLK 편차로 인해 발생할 수 있는 노이즈 Tolerance(내구성… 이라는 단어만 따오르네, 뭔가 찰떡같은 말이 있을텐데)가 낮아질 수 있는 대신 성능이 조금 더 빨라질 수 있다. (당연한건가)
ㅤ
여기에서 언급되는 PCLK 주파수는 Clock Configuration 세팅에서 값을 확인해줄 수 있었다. APB1 Peripheral CLK으로 연결되는 클럭이 PCLK에 해당한다. (Peripheral CLK일까..?)

ㅤ
UART의 프레임
그렇다면 UART에서는 어떤 형태로 데이터를 전송할까?
ㅤ
UART에서 한 번에 보내는 하나의 데이터 단위를 ‘프레임’ 이라고 한다.

ㅤ
Frame의 구성요소
- IDLE : 데이터를 주고받지 않는 평상시. High 상태. (데이터를 주고받지 않는 상태와 연결이 끊어진 것과 구분하기 위해서 HIGH를 유지한다고 생각됨)
- Start Bit : 항상 0 (IDLE → Falling Edge 가 발생하면 신호가 들어오는지 체크) 이때 단순히 Falling Edge라고 신호의 시작이라 판단하지는 않는다 (노이즈 일 수 있으니깐). Sampling을 보고 0이 적당히 유지된다고 판단하면 신호의 입력으로 판단. 아닌 경우에는 무시한다.ㅅ
- Data Bit : 실제로 보내는 데이터. 5~9Bit 로 구성. Data Bit에는 LSB 부터 데이터가 차례대로 담긴다. 단순히 숫자를 읽듯이 우→좌 로 읽으면 값이 달라질 수 있으니 유의할 것.
- Parity Bit : 에러 검출용 Bit (옵셔널). 일반적인 환경에서는 전송 속도를 위해 사용하지 않으나, 노이즈가 많은 환경이나 데이터의 무결성이 중요한 경우에는 사용하는 듯.
- Stop Bit : 항상 1 / 1~2 Bit 사용. 기본으로는 1Bit를 사용하는데, 느린 장치와 통신하는 경우 2 Bit으로 시간 여유를 줄 수 있다. (그렇다면 2Bit는 레거시인가?) 특정 모듈과 통신하기 위해서 0.5, 1.5 Bit을 사용하기도 하는 듯. 옵션에 있다.
ㅤ
Data Bit가 기본으로 8 bit 단위를 사용하는건 아마도 1 Byte를 기본 단위로 사용하기 때문일거라 생각이 든다.
물론 ASCII를 이용하기 때문에 7 bit + 노이즈가 많은 환경이여서 parity 1 bit 를 사용해서 8 bit 라는 해석도 가능하지 않을까? 싶다. 그래도 이게 살아남은건 1 Byte 단위가 깔끔해서라고 생각된다 ^,^
ㅤ
만약 앞서 송수신측이 약속한 내용과 다르게, Stop Bit이 나와야 하는 위치에 Stop Bit이 없다면 (Low 라면) Framing Error 로 판단한다.
- 노이즈로 인해 Stop Bit을 0으로 읽은 경우
- 양쪽 시스템의 Baud Rate(CLK)의 차이로 인해 신호를 읽는 타이밍이 일그러진 경우
ㅤ
ㅤ
만약 뭔가 중요한 신호를 보내고 싶을 때에는 10프레임 가량 LOW 신호를 길게 보내는 Break Frame을 사용하기도 한다. (← 이게 Break Frame의 역할으로 보인다) 문서를 읽어보니 USART에서는 Break Frame이 들어오면 Framing Error 로 처리한다고 하는데, Interrupt를 통한 장치의 Reset 명령어 정도로 사용할 수 있지 않을까? 싶다. 바로 위 그림에서처럼, Stop Bit 자리에 Break (Low)가 유지되고 있어 자연스럽게 Framing Error로 넘어가나보다.
ㅤ
ㅤ
약간 의아했던건, UART는 한 번 Start를 하면 한 번에 길게 데이터를 주지 않고 딱 한 프레임 (여러 검증용 Bit 포함해서 10~11 Bit 정도) 단위로만 Stop 시키고 다시 Start Bit을 붙여줘야한다. 굳이..? 너무 보일러플레이트 Bit가 많은거 아닌가…? 라고 생각을 했는데, 이것도 어느정도 의미가 있는 행동이였다.
ㅤ
어쩌다보면 송신쪽의 Baud Rate와 수신측의 Baud Rate가 딱 정확하게 일치하지 않을 수 있다. 만약 보내는 쪽보다 받는 쪽 Baud Rate가 조금 더 빠르다고 해보자. 그러면 데이터를 받을수록 점점 샘플링 구간이 앞쪽으로 밀려서, 값을 읽기위해 샘플링하는 구간이 각 Bit의 가장자리로 밀릴 수도 있다. 이렇게 약간의 오차가 누적되면 Bit 해석에서 오류가 발생할 수 있다!
ㅤ
10Bit 마다 Start Bit으로 첫 구간을 다시 정함으로써, 매 프레임을 수신할 때마다 샘플링 기준점을 다시 잡는다면 약간의 Baud Rate 오차가 있더라도 길게 누적되지 않도록 하여 비교적 안전하게 신호를 주고받을 수 있다.
ㅤ
물론 Baud Rate의 오차율이 적다면 프레임을 더 길게 유지할 수도 있었겠지만, 이 한 프레임이 뭔가 역사적으로 정해진 수치가 아닐까? 사실 나는 이게 char 단위라서 8bit 정도를 한 프레임에 담는다고 생각했었는데,
ㅤ
ㅤ
STM32에서 UART의 HW를 확인해보자
UART 는 데이터의 송수신을 위해 기본적으로 3개의 선을 연결한다. (만약 단방향 전송이라면 2개만 사용해도 될 듯)

https://vanhunteradams.com/Protocols/UART/UART.html
- Device1의 송신 포트 TX → Device2의 수신 포트 RX
- Device1의 수신 포트 RX → Device2의 송신 포트 TX
- 두 장치의 기준 전압을 맞추기 위한 GND
ㅤ
가장 대표적으로 UART 통신을 만날 수 있는 방식은 아래 RS232C 통신 규격이다. (VGA 선이랑 되게 비슷하게 생겼는데, 둘이 하는 역할이 다르다. RS232는 직렬통신 / VGA는 아날로그 비디오 전송이 목적이다)

위 이미지처럼 UART에서는 GND, RX, TX 3개의 선을 기본으로 사용한다. 이외에도 RTS, CTS (Request To Send, Clear To Send), DTR, DSR (Data Transfer Ready, Data Set Ready), CD (Carrier Detect), RI (Ring Indicator) 등 다양한 신호선을 보조적으로 활용해 안정적인 통신을 위해 사용한다.
ㅤ

http://www.assistlab.co.kr/content/microcontroller/pic/communication/rs232.htm
ㅤ
이 중에서 RTS와 CTS는 STM32 에서도 설정을 통해 사용할 수 있다. 옵션에 보면 RS232 프로토콜(?)을 사용할 지에 대해서 선택할 수 있다. DTE-DCE 간에 빠른 속도로 통신할 때 (처리속도 느림 / 버퍼 작음 등의 이슈로 인한) 데이터 손실 방지를 위해 이 선을 추가로 사용한다.
- DTE - Data Terminal Equipment : 데이터 끝단에서 실제로 데이터를 사용하는 장치 (PC / MCU / 사람 등)
- DCE - Data Communication Equipment : 데이터를 송수신하기 위해 사용하는 중간 장치 (모뎀 등)
- Request To Send : DTE가 DCE에게 데이터를 줘도 될지에 대해 물어봄
- Clear To Send : DCE가 DTE에게 데이터 줘도 된다고 알려줌

ㅤ
PC와 MCU
의 디버깅 연결처럼 양쪽 DTE끼리 DCE 없이 바로 연결하는 경우에는 이를 “널 모뎀” 방식이라고 부른다. 이 경우에는 위 그림처럼 서로간의 TX-RX를 연결해줘야한다.
ㅤ
PC와 MCU의 연결은 널 모뎀 방식이 아니다!
MCU(DTE) → UART → UART to USB(DCE) → USB → USB to UART(DCE) → UART → PC 로 넘어가기 때문에, 여기에서는MCU와 USB 사이의 통신 전달을 위해서 UART를 사용한다고 봐야한다. 에 대해서 질문을 했는데, USB는 단순히 데이터 버스의 역할이기 때문에 USB가 DCE의 역할을 한다…? 고 보기는 조금 애매한 감이 있음. 그러나 메인 MCU와 ST-Link 쪽의 작은 MCU 둘은 회로도를 살펴보면 둘이 직접 TX와 RX가 꼬인 채로 연결이 되어있어서 이 친구들은 널 모뎀이라고 볼 수 있다.
이것은 원래 나의 생각 : MCU 상단에 디버깅(ST_LINK)를 위해 붙어있는 작은 MCU에 연결을 살펴보면, STLK_TX, RX 신호가 들어와서 MCU를 거쳐 USB_DP, DM으로 바뀌어 USB를 타고 PC로 이동한다. 즉 MCU(DTE) → 작은MCU(DCE)→PC 로 가기 때문에, 이걸 널모뎀이 아니라고 생각한다.
ㅤ

ㅤ
STM32 보드에서는 UART와 함께 동기 신호를 다루는 USRT(Universial Synchronous Reicever / Transmitter) 를 합쳐서 USART 라는 이름으로도 이 로직을 사용할 수 있다. 참고하자!
ㅤ
STM32F429 에는 UART + USART가 총 8개 있다.

ㅤ
얘네들이 연결된 BUS가 다른데, 이렇게 연결된 버스를 나눠놓은걸 보니 USART1과 USART6 이 조금 더 고속의 통신이 필요한 장치들과 통신하기 위해서 사용하는 포트라고 생각하면 될 것 같다.
ㅤ
그런데 개인적인 생각으로는 45MHz와 90MHz가 그렇게 큰 차이가 있을까 싶다. 사실 단순히 2배 차이라면 bit당 샘플링을 16개 → 8개로 바꾼거랑 동일하지 않나…? 흠 그냥 단순히 CLK이 목적이 아니라 다른 장치와의 연결이 목적일지도?
ㅤ
USART Block Diagram

ㅤ
우선 하단의 Baud Rate Generator
- APB CLK ($f_{PCLK1}, f_{PCLK2}$) 로부터 CLK을 만들어낸다.
- USART_BRR (고정소수점) 으로부터 만들어낸 USART Divider 값, OverSampling 빈도 (16인지 8인지) 를 APB CLK에 곱해 원하는 주파수를 만든다.
- 이걸 Transmit Control / Receive Control 에게 전달해 통신 과정에서 활용한다.
ㅤ
송신 흐름
- CPU 또는 DMA가 Transmit Data Regsiter에 보내고 싶은 데이터를 작성한다.
- 데이터가 Transmit Shift Register로 이동한다.
- TSR의 값에 Parity, Start, Stop bit을 붙인다.
- 한 비트씩 TX 핀으로 출력한다 (병렬데이터 → 직렬데이터화)
ㅤ
송신에 대한 제어는 Transmit Control이 담당한다.
CR1레지스터의 Bit 들이 송신 제어 신호를 가지고 있다- TE (Transmit Enable) : 송신을 활성화
- TCIE (전송완료), TXEIE (전송데이터 없음) : 송신에 대한 Interrupt의 활성화를 제어
ㅤ
수신 흐름
- RX 핀에서 신호를 감지한다.
- Start Bit 이라면 동기화!
- RX 핀으로 들어오는 직렬 데이터를 16배 샘플링을 통해 한 비트씩 받아서 Receive Shift Register에 담는다. (직렬데이터 → 병렬데이터화)
- RSR에서 조립한 1 Byte (완전한 데이터) 를 Receive Data Register에 담는다.
- CPU나 DMA가 BUS를 통해 이 값들을 읽어간다.
ㅤ
수신에 대한 제어는 Receive Control이 담당한다.
- Transmit과 마찬가지로,
CR1레지스터의 Bit 들이 수신 제어 신호를 가지고 있다 - RE (Receive Enable) : 수신을 활성화
- RXNEIE (데이터 읽을 준비 완료), IDLEIE (노는 Line 발견) : 수신에 대한 Interrupt 활성화를 제어
ㅤ
Wakeup Unit
- 특정한 데이터 패턴이 들어왔을 때 Recive Control 에게 알려 시스템이 Sleep 모드에서 깨어날 수 있도록 지원한다. (오호 이게 HW적으로 있는건가)
ㅤ
여기에서 TDR이 비었는지를 굳이 SR을 통해 확인하는 이유는? (TDR)
ㅤ
UART 의 동작 제어
우선 UART를 제어, 사용하기 위한 레지스터는 기본적으로 다음과 같이 구성되어있다. (목차를 통해 확인함!)
레지스터에 대한 각각의 설명을 읽어보고 요약해보면 다음과 같다.
USART_SR: Status Register- 현재 USART 내 각종 상태 플래그의 확인
- USART를 코드로 조작할 때 사용할 수 있는 플래그들이 여기에 많이 있다. (전송 완료 / 데이터 담을 수 있는지 / 읽을 값이 있는지 등)
USART_DR: Data Register- 주소는 하나이지만, DR은 2개의 레지스터로 구성되어있음. (앞서 다이어그램에서 확인한 TDR, RDR로 보임)
- 32bit 레지스터에서 하위 9개의 Bit [8:0]를 사용한다.
- Parity Bit을 활성화했다면 송신 과정에서는 MSB에 Parity Bit이 자동으로 작성되며, 수신할 때에는 MSB에 Parity Bit이 들어있다.
USART_CR1: Control Register 1- 대부분의 USART 동작과 관련된 설정은 Control Register에 들어있다.
USART_CR2: Control Register 2USART_CR3: Control Register 3USART_BRR: Baud rate를 결정하는 분주비 설정

그치만 백견이 불여일코드라고 하였다. 코드로 작성해보면서 통신에 꼭 필요한 레지스터 먼저 확인해보자.
20251031 추가 +) BRR Register와 USARTDIV로 CLK 만들어내기
라고 할 뻔. 이론적으로 알아야하는 내용이 있어 우선 짚고 넘어가자.
BRR 레지스터에 대한 설명을 읽어보면, 내가 처음에 이해했던 바와 약간 다르게 작성되어있었다.

- Mantissa는 정수부, Fraction은 실수부가 맞음.
- 그런데 Oversampling을 얼마로 했냐에 따라서 해석하는 방식이 달라진다. (대충 띠용스 싸운드) OVER8의 값에 따라서 LSB가 아닌 BRR[3]을 비운다고 되어있다! (나는 LSB를 비우는 줄 알았었다.)
BRR = 0x00000036 인 상황에서
1) 만약 Oversampling 이 16배 였다면
0000 0011 0110
-> 정수 부분은 3 (1 + 2)
-> 소수 부분은 0.375 (1/4 + 1/8)
=> USARTDIV의 값은 3.375 라고 해석할 수 있다.
2) 만약 Oversampling 이 8배 였다면
0000 0011 0110
-> 정수 부분은 3 (1 + 2)
-> 소수 부분은 0.75 (1/2 + 1/4)
=> USARTDIV의 값은 3.75 라고 해석할 수 있다.
ㅤ
그래서 BRR이 어떻게 동작하냐면

ㅤ
ㅤ
- BRR → USARTDIV 계산
- BRR 레지스터 값을 통해 만들어낸 USARTDIV는 $\frac{f_{\text{PCLK }}\text{Hz}}{\text{Baud Rate} \times \text{Oversampling}}$ 의 수치를 가진다. 사실 이 값을 계산해서 BRR에 담아주는 거임.
- 그러면 USART와 연결된 $f_{\text{PCLK}}$에 대해서 USARDIV의 역수를 곱해준다.
- $\frac{f_{\text{PCLK}}}{\text{USARTDIV}} = {\text{Baud Rate} \times \text{Oversampling}}$ 이라는걸 알 수 있다.
- 근데 엄밀히 말하자면, CLK으로 들어오는 신호는 주파수를 표현하는 값이 아니라 진짜 똑딱똑딱 거리는 CLK이고, 이걸 사용해서 Baud Rate X 오버샘플링에 해당하는 주파수를 만들어낸다고 보면 된다.
- CLK 사용하기
- RX는 Baud Rate X Oversampling 의 주파수 CLK을 그대로 사용해 샘플링을 처리
- TX는 여기에 Oversampling을 나눈 주파수로 분주해, Baud Rate에 해당하는 CLK을 구한 뒤 송신을 수행.
BRR이 단순히 마법같은 일을 해주지는 않는다. 지금 보니, PCLK으로 들어오는 주파수가 바뀌면 BRR의 값도 변경이 필요하기 때문에 단순히 BRR에 상수를 계산해서 때려넣을 수는 없을 것 같고, PCLK의 값을 변수로 받아와서 필요한 연산을 처리한 뒤에 값을 넣어주는게 맞을 것 같다.
ㅤ
혹시 BRR 값 계산 방법이 궁금하다면 레퍼런스 메뉴얼 문서를 찾아보자! Oversampling 8 / 16 상황에 대해서 BRR 계산 방법에 대해서 예시가 친절하게 작성되어있다. (거기에 오타도 있음 ㅋㅋ)
ㅤ
아무래도 BRR의 처리 과정에서 소수점이 절삭되다보니, (소수점 부분이 4bit 밖에 안됨 → 소수점 값에 대한 정확도가 매우 낮다) 오차가 발생할 수 밖에 없다.

ㅤ
오버샘플링을 16번 수행하여 검사를 한다고 하더라도 실제 CLK을 이용해 Baud Rate에 해당하는 CLK을 만들면 오차가 상당히 발생할 수 밖에 없는 듯 하다. 물론 이게 컴퓨터끼리니까 동일한 아키텍처(?)를 사용하는 경우에는 이렇게 만들어내는 CLK이 일치하게 되니깐 에러가 꽤나 줄어들 수 있을 것 같다.(오히려 좋아 ㅋㅋ) 이렇게 Bit 가 길어질수록 실제와 이론적인 Baud Rate의 차이가 발생하니깐 어쩔 수 없이 Frame의 길이가 10 Bit 정도로 제한되는것이 아닐까 궁예해본다!
UART 코드 작성해보기
UART로 문자열 보내기
우선 첫 번째로, USB로 연결된 PC에게 UART로 문자열을 한 번 보내보자.
USB로 연결이 되어있기 때문에 ST_Link와 연결된 USART3를 GPIO Pin PD8, PD9를 사용하도록 지정해줘야한다.
ㅤ
지금은 Init 부분 등은 HAL을 이용하고, 주요 로직만 체크해보자.
데이터를 쓰거나 읽기 위해서는 USART_DR 레지스터를 이용하면 된다. DR 레지스터에 값을 쓰려 할때는 Address Decoder에 의해 TDR로, 읽으려고 할 때는 RDR로 HW 적으로 레지스터를 지정해준다. 그리고 이 값에 Bit를 작성해넣으면 앞서 봤던 전송 로직이 시작된다.
ㅤ

ㅤ
다만, 여기에서 단순히 8bit 단위로 (uint8_t 단위) 데이터를 전달하기 때문에 한 번에 하나의 Char 만 담을 수 있다. 그 다음 글자를 담을 수 있는 타이밍은 TDR에 있는 데이터가 TSR로 내려간 다음이다. TDR이 현재 비어있는지 확인하기 위해서는 Status Register (SR)를 확인해주면 된다. 여기에서 7번째 bit에 해당하는 TXE Flag 가 1이라면 지금 값을 담을 수 있는 상태임을 알고 데이터를 담아줄 수 있다.

ㅤ
이걸 이용해서 코드로 짜보면! 이렇게 하나의 String을 보내는 UART 코드를 쑉 만들어줄 수 있다.
main()
{
...
MX_GPIO_Init();
MX_USART3_UART_Init();
uint8_t* message = "Hello, world!\n\r";
uint8_t* message_ptr = message;
while (1)
{
while(*message_ptr) {
// SR 레지스터의 TXE bit 를 확인
if(USART3->SR >> 0x7 & 0x1) {
USART3->DR = *message_ptr++;
}
}
}
ㅤ
위 코드를 통해서 데이터는 잘 보내진다!

ㅤ
여기에서 데이터를 보내는 부분을 애널라이저로 직접 확인해보면 아래처럼 Bit 들을 확인할 수 있다.

ㅤ
여기에서 첫 번재 글자 H 와 두 번째 글자 e 를 살펴보면 UART 통신의 Frame 에 대해서도 확인할 수 있다!
위 신호 파형을 살펴보면, 파형이 IDLE (1) → Start Bit (0) → Data Bit (00010010) → Stop Bit(1) → Start Bit (0) → Data Bit(10100110) →Stop Bit (1) → … 의 형식으로 구성되어 있다는 것을 알 수 있다.
ㅤ
여기에서 Data Bit은 LSB 부터 담기때문에 거꾸로 읽어야 한다. 즉, 0b01001000(0x48), 0b01100101(0x65) 이라는 값이 UART를 통해 나간 것이고, 이 값은 ASCII에서 확인해보면 H, e 이다. 데이터가 잘 나간 것을 알 수 있다 :)

https://sheepone.tistory.com/47
ㅤ
UART로 데이터 읽기
데이터를 읽을 때에도 동일하게 UART의 DR 레지스터를 사용한다. 다만, “읽을 값이 있을 때에만 유효”한 동작이므로 SR 레지스터의 RXNE 플래그 비트를 확인해 읽을 값이 있다면 값을 읽어주도록 한다. 놀라운 점은 DR에 읽기 동작을 수행해서 값을 읽으면 RXNE 플래그가 자동으로 0으로 내려오기 때문에 따로 읽었음! 을 관리할 필요는 없다. (카톡 채팅 읽으면 없어지는 숫자st) 다만 RDR을 읽는다고 해서 RDR 레지스터 자체를 비워주는건 아니라서, RXNE가 0일때 DR 읽기를 시도하면 마지막으로 읽어온 글자가 남아있다. (알고싶지 않았다)

ㅤ
아래는 PC 터미널에 글자를 쓰면 → MCU에서 이걸 읽어서 PC에게로 다시 글자를 보내는 Echo Back에 대한 구현이다.
while (1)
{
// 만약 읽을게 있으면
if(USART3->SR >> 5 & 0x1) {
char_to_send = USART3->DR;
}
// 보낼게 있음 + DR에 담을 수 있으면
if(char_to_send && (USART3->SR >> 7) & 0x1) {
USART3->DR = char_to_send;
char_to_send = 0;
}

ㅤ
글자 하나를 보낼 때 8bit + Start Stop 2bit ⇒ 10 bit를 전송하게 될 것이다.
여기에서 Baud Rate를 115,200 bits/s 으로 지정해줬는데, 이걸 대충 100K 라고 생각한다면
→ $\frac{10}{100K} = \frac{1}{10K}$ 초가 걸리고, CPU가 1MHz(=1,000KHz) 라고 생각하면 대략 글자 하나를 보내는데 CPU 입장에서는 약 100,000CLK 정도가 소요되는 매우 느린 동작이다.
그래서 일반적으로는 인터럽트를 이용해서 글자를 입력할 수 있을 때 잠깐 와서 다음 글자를 넣어주고 다시 원래 하던 일을 처리하러 가거나 / 풀링으로 전송을 기다리다가 더 급한 프로세스(테스크)의 인터럽트가 발생하면 급한 일을 먼저 처리하고 돌아와서 글자를 다시 전송하는 방식으로 구현한다(→ 이게 지금 코드 상황).
ㅤ
디버깅 용 Printf 구현해보기
UART Printf를 구현하면 이제부터 시스템에서 이상이 있는 것 같을 때 (의심스러울 때) 값을 터미널을 통해서 찍어보면서 확인할 수 있게 된다. 보드를 처음에 파악하고 설정하려고 할 때, LED 다음으로 살리면 매우 좋음.
ㅤ
uint32_t uart_printf (const char * format, ...) {
if(uart_address == 0) {
return 0;
}
va_list ap;
uint8_t string[256];
uint32_t ch_count = 0;
// 가변인자 사용 공부하기!!!
// 아래는 stdarg와 stdio 의 함수들임
va_start(ap, format);
vsprintf(string, format, ap);
va_end(ap);
// 받은 메시지를 한 글자씩 출력하기
uint8_t* string_ptr = string;
while(*string_ptr) {
if('.' && (USART3->SR >> 7) & 0x1) {
uart_address->DR = *string_ptr++;
ch_count++;
}
}
return ch_count;
}
ㅤ
이 함수를 사용해서 적용해보면 아래처럼 %d 같은 포맷을 적용해서 보내지는 것을 확인할 수 있다.
uart_printf("Nice To see ya %d %d", 1, 21);

ㅤ
UART_Init
UART의 Init 과정은 GPIO랑 거의 비슷하다. 다만 차이가 있다면 UART와 연결된 In/Out GPIO port에도 활성화를 해주어야 한다! 그리고 Baud Rate는 115,200 의 분주비를 넣어주는게 아니라, CLK을 계산해서 BRR 레지스터에 넣어줘야한다.
// 다른 동작 다 떼고, 단순히 uart_printf 함수를 동작할 수 있도록 하는 Init은 아래와 같다.
MX_USART3_UART_Init();
// MX_USART3_UART_Init 함수를 까고보면 결국 아래 동작들을 수행한다.
// (이미 레지스터가 모두 RESET Value로 지정되어있다고 가정)
RCC->AHB1ENR |= 0x1 << 3;
RCC->APB1ENR |= 0x1 << 18;
GPIOD->MODER |= 0b10 << 18;
GPIOD->MODER |= 0b10 << 16;
GPIOD->OSPEEDR |= 0b11 << 16;
GPIOD->OSPEEDR |= 0b11 << 18;
GPIOD->AFR[1] |= 0b0111 << 0;
GPIOD->AFR[1] |= 0b0111 << 4;
USART3->CR1 |= 0x3 << 2;
USART3->BRR = 139;
USART3->CR1 |= 0x1 << 13;
ㅤ
아자스!
'Embedded System > MCU' 카테고리의 다른 글
| [MCU] 풀업, 풀다운 저항은 MOSFET인가? (0) | 2025.11.02 |
|---|---|
| [MCU] V=IR과 기본 임피던스부터 풀업과 풀다운, 푸시풀과 오픈드레인까지 (0) | 2025.11.02 |
| [MCU] GPIO 실습 : USER Button과 LED 켜기 (0) | 2025.10.28 |
| [MCU] 빌드 프로세스와 컴파일 환경 (1) | 2025.10.27 |
| [MCU] GPIO의 하드웨어 구조와 데이터시트, 침침한 눈을 곁들인 (1) | 2025.10.24 |