Repository navigation
Chapter 10
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원서의 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를 사용한다.
이번 코드에서 직접 사용하는 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
Peripheral clock:
APB1 Clock = 16 MHzTarget:
Baud Rate = 115200기존 실습 코드:
BRR = (periph_clk + baudrate / 2) / baudrate;따라서:
16,000,000 + 57,600
───────────────────
115,200
≈ 139
즉:
USART3->BRR ≈ 139를 사용한다.
baudrate / 2를 먼저 더하는 것은 정수 나눗셈에서 반올림 효과를 주기 위한 것이다.
Project:
Inc/
├── gpio.h
├── systick.h
└── uart.h
Src/
├── gpio.c
├── systick.c
├── uart.c
└── main.c
gpio.c와 systick.c는 앞 chapter에서 만든 driver를 재사용한다.
#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
#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->MODER &= ~(0x3UL << (8 * 2));
/*
* Then write 10b.
*
* PD8 becomes Alternate Function mode.
*/
GPIOD->MODER |= (0x2UL << (8 * 2));
/*
* --------------------------------------------------------
* Step 3. Configure PD9 as Alternate Function
* --------------------------------------------------------
*
* PD9:
*
* MODER<span>[19:18]</span>
*/
GPIOD->MODER &= ~(0x3UL << (9 * 2));
GPIOD->MODER |= (0x2UL << (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->AFR<span>[1]</span> &= ~(0xFUL << ((8 - 8) * 4));
/*
* AF7 = 0111b
*
* PD8 → USART3_TX
*/
GPIOD->AFR<span>[1]</span> |= (0x7UL << ((8 - 8) * 4));
/*
* --------------------------------------------------------
* Step 5. Select AF7 for PD9
* --------------------------------------------------------
*
* PD9 position:
*
* (9 - 8) × 4
* = bit <span>[7:4]</span>
*/
GPIOD->AFR<span>[1]</span> &= ~(0xFUL << ((9 - 8) * 4));
/*
* PD9 → AF7 → USART3_RX
*/
GPIOD->AFR<span>[1]</span> |= (0x7UL << ((9 - 8) * 4));
/*
* --------------------------------------------------------
* Step 6. Enable USART3 Clock
* --------------------------------------------------------
*
* USART3 belongs to APB1.
*
* RCC APB1ENR bit18:
*
* USART3EN = 1
*/
RCC->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->CR1 = CR1_TE;
/*
* --------------------------------------------------------
* Step 9. Enable USART3
* --------------------------------------------------------
*
* UE = 1
*
* USART3 is now operational.
*/
USART3->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->DR = (ch & 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);
}
#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);
}
}
전체 초기화 순서:
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상태가 된다.
GPIO pin 하나는 GPIO 기능만 수행하는 것이 아니다.
예를 들어 PD8은 설정에 따라 여러 peripheral function으로 연결될 수 있다.
PD8
│
├── GPIO
├── USART
├── Timer
└── Other PeripheralMODER=10은:
이 pin을 GPIO output으로 직접 쓰지 않고
Peripheral에게 넘기겠다는 의미다.
그 후:
GPIOD->AFR[1] = ...에서 어떤 peripheral function을 연결할지 선택한다.
이번 설정:
PD8
↓
Alternate Function Mode
↓
AF7
↓
USART3_TXAlternate 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)형태가 된다.
문자 '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핵심:
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를 연속해서 공급할 수 있다.
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가 되어 종료된다.
현재 bare-metal Make build는 표준 library 의존성을 최소화한다.
따라서 단순 출력에는:
uart_write_string()을 직접 사용한다.
장점:
작은 코드
명확한 실행 흐름
Library dependency 최소화
UART hardware 동작 직접 확인__io_putchar()는 이후 newlib/newlib-nano가 연결된 환경에서 printf() redirection에 활용할 수 있도록 남겨둔 함수다.
PD9까지 AF7로 설정하지만 현재 CR1에는:
TE만 enable한다.
즉:
Transmitter Enable
= YES
Receiver Enable
= NO
이다.
따라서 현재 기능:
Board
↓
PC단방향이다.
RX까지 사용하려면 이후:
CR1.RE
SR.RXNE
DR Read등이 추가로 필요하다.
main()
│
├── led_init()
│
├── uart_init()
│
└── while(1)
│
├── uart_write_string()
│ │
│ ├── TXE wait
│ ├── DR write
│ ├── TXE wait
│ ├── DR write
│ └── ...
│
├── LED toggle
│
├── SysTick 1000 ms
│
└── RepeatCPU
│
│ write
▼
USART3->DR
│
▼
USART3 Transmitter
│
▼
PD8 / AF7
│
▼
ST-LINK VCP
│
▼
USB
│
▼
PC Terminal기존 실습 결과:
text = 1168 bytes
data = 0 bytes
bss = 2560 bytes경고 없이 build되었고, 실제 NUCLEO-F439ZI에 download/run하여 VCP 115200-8-N-1에서 1초마다 문자열 출력과 LD1 toggle을 확인했다.
make clean
makeExpected:
Build PASS
text = 1168
data = 0
bss = 2560
확인:
GPIOD->MODER
GPIOD->AFR[1]Expected:
PD8 MODER = 10
PD9 MODER = 10
PD8 AF = 7
PD9 AF = 7
확인:
USART3->BRR
USART3->CR1
USART3->SRExpected:
BRR ≈ 139
CR1.TE = 1
CR1.UE = 1
TX 동작 중:
SR.TXE변화를 확인할 수 있다.
Terminal configuration:
Baud Rate : 115200
Data Bits : 8
Parity : None
Stop Bits : 1
Flow Ctrl : None즉:
115200-8-N-1Expected output:
Hello from NUCLEO-F439ZI! (USART3, 115200bps)
Hello from NUCLEO-F439ZI! (USART3, 115200bps)
Hello from NUCLEO-F439ZI! (USART3, 115200bps)
...약 1초 간격으로 출력된다.
가장 먼저 확인할 부분:
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다.
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 ToggleGPIOD Clock
↓
PD8 / PD9 AF Mode
↓
AF7
↓
USART3 Clock
↓
BRR = 139
↓
TE + UE
↓
TXE Polling
↓
DR Write
↓
PD8
↓
PCNUCLEO-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 출력까지 검증했다.