Skip to content

Chapter 10

Hongjayeong-02 edited this page Oct 5, 2026 · 1 revision

10. UART — USART3

Goal

NUCLEO-F439ZI의 USART3를 register level에서 설정하고, ST-LINK Virtual COM Port를 통해 PC terminal로 문자열을 전송한다.

이번 실습:

STM32F439ZI
    │
    │ USART3
    │
    └── PD8 (TX)
          ↓
       ST-LINK
          ↓
       USB VCP
          ↓
         PC
          ↓
"Hello from NUCLEO-F439ZI!"

설정:

USART  : USART3
TX     : PD8
RX     : PD9
AF     : AF7
Baud   : 115200 bps
Clock  : 16 MHz
Format : 115200-8-N-1

1. Board Mapping

원서의 NUCLEO-F411과 현재 사용하는 NUCLEO-F439ZI는 Virtual COM Port 연결이 다르다.

  | NUCLEO-F411 | NUCLEO-F439ZI -- | -- | -- USART | USART2 | USART3 TX | PA2 | PD8 RX | PA3 | PD9 AF | AF7 | AF7 APB1 Enable | USART2EN | USART3EN AFR | AFR[0] | AFR[1]

따라서 원서 코드를 그대로 사용하면 compile은 되더라도 VCP에 출력이 나타나지 않을 수 있다.

이번 board에서는:

USART3 TX → PD8
USART3 RX → PD9

를 사용한다.


2. UART Registers

이번 코드에서 직접 사용하는 register:

RCC->AHB1ENR
RCC->APB1ENR

GPIOD->MODER GPIOD->AFR[1]

USART3->BRR USART3->CR1 USART3->SR USART3->DR

USART 주요 bit:

CR1 bit3   TE
→ Transmitter Enable

CR1 bit13 UE → USART Enable

SR bit7 TXE → Transmit Data Register Empty


3. Baud Rate

Peripheral clock:

APB1 Clock = 16 MHz

Target:

Baud Rate = 115200

기존 실습 코드:

BRR = (periph_clk + baudrate / 2) / baudrate;

따라서:

16,000,000 + 57,600
───────────────────
115,200

≈ 139

즉:

USART3->BRR ≈ 139

를 사용한다.

baudrate / 2를 먼저 더하는 것은 정수 나눗셈에서 반올림 효과를 주기 위한 것이다.


4. Practice Code

Project:

Inc/
├── gpio.h
├── systick.h
└── uart.h

Src/ ├── gpio.c ├── systick.c ├── uart.c └── main.c

gpio.c와 systick.c는 앞 chapter에서 만든 driver를 재사용한다.


5. uart.h

#ifndef UART_H
#define UART_H

#include "stm32f4xx.h"

/*

  • Initialize USART3.
  • Board:
  • NUCLEO-F439ZI
    
  • Pin:
  • PD8 → USART3_TX
    
  • PD9 → USART3_RX
    
  • Alternate Function:
  • AF7
    
  • Baud:
  • 115200 bps
    

*/ void uart_init(void);

/*

  • Send a null-terminated string through USART3.
  • Example:
  • uart_write_string("Hello\r\n"); */ void uart_write_string(const char *s);

/*

  • printf redirection hook.
  • This function is useful when the project is built
  • in an environment where newlib/newlib-nano is linked.
  • The standalone bare-metal Make build does not depend
  • on printf, so uart_write_string() is used directly. */ int __io_putchar(int ch);

#endif


6. uart.c

#include <stdint.h>
#include "uart.h"

/*

  • ============================================================
  • RCC Clock Enable Bits
  • ============================================================ */

/*

  • RCC AHB1ENR bit3:
  • Enable GPIOD peripheral clock.
  • PD8 / PD9 cannot be configured until
  • the GPIOD clock is enabled. */ #define GPIODEN (1U << 3)

/*

  • RCC APB1ENR bit18:
  • Enable USART3 peripheral clock. */ #define USART3EN (1U << 18)

/*

  • ============================================================
  • UART Clock / Baud Configuration
  • ============================================================ */

/*

  • Target communication speed. */ #define DBG_UART_BAUDRATE 115200U

/*

  • This Bare-Metal project does not configure the PLL.
  • System clock:
  • HSI = 16 MHz
    

*/ #define SYS_FREQ 16000000U

/*

  • USART3 is connected to APB1.
  • In this project APB1 clock is also 16 MHz. */ #define APB1_CLK SYS_FREQ

/*

  • ============================================================
  • USART Register Bits
  • ============================================================ */

/*

  • USART CR1 bit3:
  • TE = Transmitter Enable
  • 0 → transmitter disabled
  • 1 → transmitter enabled */ #define CR1_TE (1U << 3)

/*

  • USART CR1 bit13:
  • UE = USART Enable
  • 0 → USART disabled
  • 1 → USART enabled */ #define CR1_UE (1U << 13)

/*

  • USART SR bit7:
  • TXE = Transmit Data Register Empty
  • 0 → DR is not ready for new data
  • 1 → next byte may be written to DR */ #define SR_TXE (1U << 7)

/*

  • ============================================================
  • Private Function Prototypes
  • ============================================================ */

/*

  • Send one byte.
  • This is kept static because it is used only
  • inside uart.c. */ static void uart_write(int ch);

/*

  • Calculate BRR value from peripheral clock
  • and target baud rate. */ static uint16_t compute_uart_bd(uint32_t periph_clk, uint32_t baudrate);

/*

  • Program the calculated baud rate into USART3 BRR. */ static void uart_set_baudrate(uint32_t periph_clk, uint32_t baudrate);

/*

  • ============================================================
  • printf Redirection
  • ============================================================ */

int __io_putchar(int ch) { /* * Redirect one character to USART3. */ uart_write(ch);

/*
 * Standard putchar-style function returns
 * the transmitted character.
 */
return ch;

}

/*

  • ============================================================
  • USART3 Initialization
  • ============================================================ */

void uart_init(void) { /* * -------------------------------------------------------- * Step 1. Enable GPIOD Clock * -------------------------------------------------------- * * USART3 VCP pins: * * PD8 → TX * PD9 → RX * * Before configuring these pins, * GPIOD must receive an AHB1 clock. */ RCC->AHB1ENR |= GPIODEN;

/*
 * --------------------------------------------------------
 * Step 2. Configure PD8 as Alternate Function
 * --------------------------------------------------------
 *
 * GPIO MODER uses 2 bits per pin.
 *
 * PD8:
 *
 *     MODER<span>[17:16]</span>
 *
 * Mode:
 *
 *     00 → Input
 *     01 → Output
 *     10 → Alternate Function
 *     11 → Analog
 *
 * First clear the field.
 */
GPIOD-&gt;MODER &amp;= ~(0x3UL &lt;&lt; (8 * 2));

/*
 * Then write 10b.
 *
 * PD8 becomes Alternate Function mode.
 */
GPIOD-&gt;MODER |=  (0x2UL &lt;&lt; (8 * 2));


/*
 * --------------------------------------------------------
 * Step 3. Configure PD9 as Alternate Function
 * --------------------------------------------------------
 *
 * PD9:
 *
 *     MODER<span>[19:18]</span>
 */
GPIOD-&gt;MODER &amp;= ~(0x3UL &lt;&lt; (9 * 2));
GPIOD-&gt;MODER |=  (0x2UL &lt;&lt; (9 * 2));


/*
 * --------------------------------------------------------
 * Step 4. Select AF7 for PD8
 * --------------------------------------------------------
 *
 * AFR register:
 *
 *     AFR<span>[0]</span> → Pin 0 ~ 7
 *     AFR<span>[1]</span> → Pin 8 ~ 15
 *
 * PD8 is therefore controlled by AFR<span>[1]</span>.
 *
 * Each pin uses 4 bits.
 *
 * PD8 position:
 *
 *     (8 - 8) × 4
 *     = bit <span>[3:0]</span>
 *
 * Clear existing AF field first.
 */
GPIOD-&gt;AFR<span>[1]</span> &amp;= ~(0xFUL &lt;&lt; ((8 - 8) * 4));

/*
 * AF7 = 0111b
 *
 * PD8 → USART3_TX
 */
GPIOD-&gt;AFR<span>[1]</span> |=  (0x7UL &lt;&lt; ((8 - 8) * 4));


/*
 * --------------------------------------------------------
 * Step 5. Select AF7 for PD9
 * --------------------------------------------------------
 *
 * PD9 position:
 *
 *     (9 - 8) × 4
 *     = bit <span>[7:4]</span>
 */
GPIOD-&gt;AFR<span>[1]</span> &amp;= ~(0xFUL &lt;&lt; ((9 - 8) * 4));

/*
 * PD9 → AF7 → USART3_RX
 */
GPIOD-&gt;AFR<span>[1]</span> |=  (0x7UL &lt;&lt; ((9 - 8) * 4));


/*
 * --------------------------------------------------------
 * Step 6. Enable USART3 Clock
 * --------------------------------------------------------
 *
 * USART3 belongs to APB1.
 *
 * RCC APB1ENR bit18:
 *
 *     USART3EN = 1
 */
RCC-&gt;APB1ENR |= USART3EN;


/*
 * --------------------------------------------------------
 * Step 7. Configure Baud Rate
 * --------------------------------------------------------
 *
 * Peripheral clock:
 *
 *     16 MHz
 *
 * Baud:
 *
 *     115200 bps
 *
 * Calculated BRR:
 *
 *     approximately 139
 */
uart_set_baudrate(APB1_CLK,
                  DBG_UART_BAUDRATE);


/*
 * --------------------------------------------------------
 * Step 8. Enable Transmitter
 * --------------------------------------------------------
 *
 * This chapter transmits only.
 *
 * TE = 1
 *
 * Note:
 * PD9 is configured for USART3_RX,
 * but RE(receiver enable) is not set here.
 *
 * Therefore actual communication in this exercise
 * is board → PC TX only.
 */
USART3-&gt;CR1 = CR1_TE;


/*
 * --------------------------------------------------------
 * Step 9. Enable USART3
 * --------------------------------------------------------
 *
 * UE = 1
 *
 * USART3 is now operational.
 */
USART3-&gt;CR1 |= CR1_UE;

}

/*

  • ============================================================
  • Send One Character
  • ============================================================ */

static void uart_write(int ch) { /* * -------------------------------------------------------- * Step 1. Wait Until DR Can Accept New Data * -------------------------------------------------------- * * TXE: * * 0 → transmit data register is not ready * 1 → transmit data register is empty * * CPU continuously checks TXE. * * This is polling / busy waiting. / while (!(USART3->SR & SR_TXE)) { / * Wait here until TXE becomes 1. */ }

/*
 * --------------------------------------------------------
 * Step 2. Write Data
 * --------------------------------------------------------
 *
 * Only the lower 8 bits are transmitted.
 */
USART3-&gt;DR = (ch &amp; 0xFF);

}

/*

  • ============================================================
  • Send String
  • ============================================================ */

void uart_write_string(const char s) { / * C string: * * H e l l o \0 * * Continue until null terminator '\0'. */ while (s) { / * Send current character. */ uart_write((int)*s);

    /*
     * Move pointer to next character.
     */
    s++;
}

}

/*

  • ============================================================
  • Baud Rate Calculation
  • ============================================================ */

static uint16_t compute_uart_bd(uint32_t periph_clk, uint32_t baudrate) { /* * Integer arithmetic would normally truncate * the fractional part. * * Adding baudrate / 2 before division gives * a rounding effect. * * Example: * * periph_clk = 16,000,000 * baudrate = 115,200 * * BRR = * * (16,000,000 + 57,600) * -------------------- * 115,200 * * ≈ 139 */ return (uint16_t) ((periph_clk + (baudrate / 2U)) / baudrate); }

/*

  • ============================================================
  • Program Baud Rate Register
  • ============================================================ */

static void uart_set_baudrate(uint32_t periph_clk, uint32_t baudrate) { /* * Store the calculated divider into BRR. */ USART3->BRR = compute_uart_bd(periph_clk, baudrate); }


7. main.c

#include "gpio.h"
#include "systick.h"
#include "uart.h"

int main(void) { /* * -------------------------------------------------------- * Step 1. Initialize LD1 * -------------------------------------------------------- * * NUCLEO-F439ZI: * * LD1 → PB0 */ led_init();

/*
 * --------------------------------------------------------
 * Step 2. Initialize USART3
 * --------------------------------------------------------
 *
 * Configuration:
 *
 *     USART3
 *     PD8  → TX
 *     PD9  → RX pin mapping
 *     AF7
 *     115200 bps
 */
uart_init();


while (1)
{
    /*
     * ----------------------------------------------------
     * Step 3. Send String to PC
     * ----------------------------------------------------
     *
     * uart_write_string() sends the string
     * one character at a time.
     *
     * '\r\n':
     *
     *     \r → Carriage Return
     *     \n → Line Feed
     *
     * Terminal output moves to the next line.
     */
    uart_write_string(
        "Hello from NUCLEO-F439ZI! "
        "(USART3, 115200bps)\r\n"
    );


    /*
     * ----------------------------------------------------
     * Step 4. Toggle LD1
     * ----------------------------------------------------
     *
     * Provides a visual indication that
     * the main loop is running.
     */
    led_toggle();


    /*
     * ----------------------------------------------------
     * Step 5. Wait 1 Second
     * ----------------------------------------------------
     *
     * Ch08 SysTick driver is reused.
     */
    systick_msec_delay(1000);
}

}


8. UART Initialization Flow

전체 초기화 순서:

uart_init()
│
├── GPIOD Clock Enable
│
├── PD8 → Alternate Function
│
├── PD9 → Alternate Function
│
├── PD8 → AF7
│
├── PD9 → AF7
│
├── USART3 Clock Enable
│
├── BRR = 139
│
├── TE = 1
│
└── UE = 1

이후:

USART3 Ready

상태가 된다.


9. Why Alternate Function?

GPIO pin 하나는 GPIO 기능만 수행하는 것이 아니다.

예를 들어 PD8은 설정에 따라 여러 peripheral function으로 연결될 수 있다.

PD8
│
├── GPIO
├── USART
├── Timer
└── Other Peripheral

MODER=10은:

이 pin을 GPIO output으로 직접 쓰지 않고
Peripheral에게 넘기겠다

는 의미다.

그 후:

GPIOD->AFR[1] = ...

에서 어떤 peripheral function을 연결할지 선택한다.

이번 설정:

PD8
↓
Alternate Function Mode
↓
AF7
↓
USART3_TX

10. Why AFR[1]?

Alternate Function register는 두 개다.

AFR[0]
→ Pin 0 ~ 7

AFR[1] → Pin 8 ~ 15

PD8과 PD9는 모두 pin number가 8 이상이다.

따라서:

GPIOD->AFR[1]

을 사용한다.

PD8 field:

AFR[1][3:0]

PD9 field:

AFR[1][7:4]

그래서 코드가:

((8 - 8) * 4)
((9 - 8) * 4)

형태가 된다.


11. Character Transmission

문자 'A'를 보낸다고 가정한다.

ASCII:

'A'

0x41

호출:

uart_write('A');

흐름:

CPU
↓
TXE 확인
↓
TXE = 1
↓
USART3->DR = 0x41
↓
USART Hardware
↓
Serial Frame 생성
↓
PD8 TX
↓
ST-LINK VCP
↓
PC Terminal
↓
A

12. TXE Polling

핵심:

while (!(USART3->SR & SR_TXE))
{
}

CPU가 계속:

TXE?
↓
0 → 기다림
↓
0 → 기다림
↓
1 → 다음 data write

를 수행한다.

여기서 TXE의 정확한 의미는:

Transmit Data Register Empty

이다.

즉:

다음 byte를 DR에 쓸 수 있다

는 뜻이다.

TXE와 **전체 serial frame 전송 완료를 의미하는 TC**는 구분해야 한다.

이번 문자열 연속 송신에서는:

TXE

를 이용하면 다음 character를 연속해서 공급할 수 있다.


13. String Transmission

uart_write_string("ABC");

memory:

'A' 'B' 'C' '\0'
↑
s

첫 번째 loop:

*s = 'A'
→ uart_write('A')
→ s++

두 번째:

*s = 'B'
→ uart_write('B')
→ s++

세 번째:

*s = 'C'
→ uart_write('C')
→ s++

마지막:

*s = '\0'

이면:

while (*s)

조건이 false가 되어 종료된다.


14. Why No printf()?

현재 bare-metal Make build는 표준 library 의존성을 최소화한다.

따라서 단순 출력에는:

uart_write_string()

을 직접 사용한다.

장점:

작은 코드
명확한 실행 흐름
Library dependency 최소화
UART hardware 동작 직접 확인

__io_putchar()는 이후 newlib/newlib-nano가 연결된 환경에서 printf() redirection에 활용할 수 있도록 남겨둔 함수다.


15. TX Only in This Practice

PD9까지 AF7로 설정하지만 현재 CR1에는:

TE

만 enable한다.

즉:

Transmitter Enable
= YES

Receiver Enable = NO

이다.

따라서 현재 기능:

Board
↓
PC

단방향이다.

RX까지 사용하려면 이후:

CR1.RE
SR.RXNE
DR Read

등이 추가로 필요하다.


16. Runtime Flow

main()
│
├── led_init()
│
├── uart_init()
│
└── while(1)
│
├── uart_write_string()
│      │
│      ├── TXE wait
│      ├── DR write
│      ├── TXE wait
│      ├── DR write
│      └── ...
│
├── LED toggle
│
├── SysTick 1000 ms
│
└── Repeat

17. Hardware Data Flow

CPU
│
│ write
▼
USART3->DR
│
▼
USART3 Transmitter
│
▼
PD8 / AF7
│
▼
ST-LINK VCP
│
▼
USB
│
▼
PC Terminal

18. Build Result

기존 실습 결과:

text = 1168 bytes
data = 0 bytes
bss  = 2560 bytes

경고 없이 build되었고, 실제 NUCLEO-F439ZI에 download/run하여 VCP 115200-8-N-1에서 1초마다 문자열 출력과 LD1 toggle을 확인했다.


19. Verification

Build

make clean
make

Expected:

Build PASS

text = 1168 data = 0 bss = 2560


Debugger — GPIO

확인:

GPIOD->MODER
GPIOD->AFR[1]

Expected:

PD8 MODER = 10
PD9 MODER = 10

PD8 AF = 7 PD9 AF = 7


Debugger — USART

확인:

USART3->BRR
USART3->CR1
USART3->SR

Expected:

BRR ≈ 139

CR1.TE = 1 CR1.UE = 1

TX 동작 중:

SR.TXE

변화를 확인할 수 있다.


PC Terminal

Terminal configuration:

Baud Rate : 115200
Data Bits : 8
Parity    : None
Stop Bits : 1
Flow Ctrl : None

즉:

115200-8-N-1

Expected output:

Hello from NUCLEO-F439ZI! (USART3, 115200bps)
Hello from NUCLEO-F439ZI! (USART3, 115200bps)
Hello from NUCLEO-F439ZI! (USART3, 115200bps)
...

약 1초 간격으로 출력된다.


20. If Nothing Appears

가장 먼저 확인할 부분:

Board
→ F439ZI 맞는가?

USART → USART3인가?

TX → PD8인가?

AF → AF7인가?

AFR → AFR[1]인가?

Clock → APB1 USART3EN bit18인가?

Baud → 115200인가?

Terminal → 올바른 ST-LINK COM Port인가?

특히 원서의:

USART2 / PA2

를 그대로 사용하면 F439ZI VCP에서는 출력이 보이지 않는 것이 핵심 porting issue다.


21. Screenshots to Add

images/ch10_build.png
images/ch10_gpio_af.png
images/ch10_usart_registers.png
images/ch10_terminal.png
images/ch10_board_led.gif

가장 중요한 증거:

USART3 Register
+
115200 Terminal Output
+
LD1 Toggle

22. Key Points

GPIOD Clock
↓
PD8 / PD9 AF Mode
↓
AF7
↓
USART3 Clock
↓
BRR = 139
↓
TE + UE
↓
TXE Polling
↓
DR Write
↓
PD8
↓
PC
  • NUCLEO-F439ZI의 ST-LINK VCP는 USART3를 사용한다.

  • USART3 TX는 PD8, RX는 PD9다.

  • PD8/PD9는 AF7로 설정한다.

  • Pin 8~15는 AFR[1]을 사용한다.

  • USART3는 APB1 peripheral이다.

  • 이번 project clock은 16MHz다.

  • 115200bps에서 BRR은 약 139다.

  • CR1.TE는 transmitter를 enable한다.

  • CR1.UE는 USART peripheral 자체를 enable한다.

  • SR.TXE는 다음 data를 DR에 쓸 수 있음을 나타낸다.

  • 문자열은 한 character씩 TXE를 polling하며 전송한다.

  • 이번 실습은 TX-only communication이다.

  • 실제 board에서 VCP 115200-8-N-1 출력까지 검증했다.

Clone this wiki locally