UART 송신

2023년 7월 20일

UART 송수신 핀맵

GD32VF103CBT6에는 UART 포트가 총 세 개(USART0/1/2) 존재합니다. 각각의 송수신 핀맵은 다음 표와 같습니다. 아무것도 설정하지 않으면 기본 핀맵이 사용되며, AFIO 레지스터를 통해 대체 핀맵으로 변경이 가능합니다.

GD32VF103CBT6 UART 송수신 핀맵
포트 기본 핀맵 대체 핀맵
USART0 Tx PA9 PB6
Rx PA10 PB7
USART1 Tx PA2 PD5
Rx PA3 PD6
USART2 Tx PB10 PC10 PD8
Rx PB11 PC11 PD9

이 글에서는 예제로 USART0의 기본 핀맵을 사용하겠습니다. 따라서 송신 핀은 PA9입니다.

UART 송신 설정

USART0를 통해 송신하려면 먼저 송신 핀이 있는 GPIOA와 USART0에 클럭을 공급해야 합니다. 둘 다 APB2에 연결되어 있으니 RCU_APB2EN 레지스터를 살펴보면 됩니다.

그 다음 송신 핀을 AFIO 모드로 선택해야 합니다. 이 부분은 GPIO 기초에서 설명하였으므로 생략합니다. 여기서는 AFIO, 50 MHz, push-pull로 설정합니다.

핀 설정이 끝났으면 본격적으로 프로토콜을 정의하고 UART를 활성화할 차례입니다. GD32VF103 유저 매뉴얼에서는 다음과 같은 절차를 안내하고 있습니다.

  1. USART_CTL0 레지스터의 WL 필드에 데이터 비트 길이를 설정합니다.
  2. USART_CTL1 레지스터의 STB 필드에 정지 비트 길이를 설정합니다.
  3. USART_BAUD 레지스터에 보 레이트(baud rate)를 설정합니다.
  4. USART_CTL0 레지스터의 TEN 필드를 설정하여 송신을 활성화합니다.
  5. USART_CTL0 레지스터의 UEN 필드를 설정하여 UART를 활성화합니다.

패리티 비트와 하드웨어 흐름 제어(hardware flow control) 설정이 빠져 있는데 적당히 송신 활성화 이전에 해주면 되겠습니다. 또한 UART가 활성화된 상태에선 위와 같이 UART 설정을 변경하는 것이 불가능합니다. 이 경우 RCU의 페리퍼럴 리셋 기능을 활용해야 합니다. USART0는 APB2에 연결되어 있다고 했으므로 RCU_APB2RST 레지스터의 USART0RST 필드를 건드리면 되겠죠.

보 레이트 설정을 더 자세히 설명하자면, 먼저 다음과 같은 값을 계산합니다.

\[ \mathrm{USARTDIV} = \frac{\mathrm{UCLK}}{16\times(\mathrm{Baud\ Rate})} \]

여기서 UCLK는 UART와 연결된 버스의 클럭으로, USART0와 연결된 APB2의 클럭은 아무것도 설정하지 않았을 시 8 MHz입니다. 이때 보 레이트를 115200으로 설정한다면 USARTDIV는 4.3402777…가 됩니다.

USART_BAUD 레지스터는 이 값을 12비트 정수부(INTDIV)와 4비트 소수부(FRADIV)로 나눠 저장합니다. 위 예에서 정수부는 4이고, 소수부는 4비트니까 0.3402777…에 16을 곱한 값인 5.444…를 반올림하여 5를 선택합니다. 소수부를 4비트로 정확히 표현할 수 없기 때문에 이렇게 설정할 경우 보 레이트는 115942 정도로 원하는 값보다 살짝 크게 오차가 생깁니다. 대개 UART는 3퍼센트 정도의 보 레이트 오차까지는 정상적으로 통신이 가능하므로 큰 문제는 없다고 보아도 무방합니다.

UART 데이터 송신

송신할 데이터는 송신 버퍼에 쌓인 후, 현재 송신이 진행중이지 않다면 송신 시프트 레지스터로 옮겨져 설정한 송신 핀을 통해 전달됩니다. 송신 버퍼에 새로운 데이터를 쌓으려면 USART_DATA 레지스터에 데이터를 쓰면 됩니다. 단, 버퍼에 쌓을 수 있는지를 먼저 검사해야 하는데, 이는 USART_STAT 레지스터의 TBE 비트가 1인지를 검사하면 됩니다.

UART의 비활성화나 절전 모드 진입 등은 USART_STAT 레지스터의 TC 비트가 1일 때만 가능합니다. 이 비트는 TBE 비트가 1이고 송신이 진행중이지 않을 때 1이 됩니다.

구현

위 내용대로 UART를 설정하고 일정 주기로 문자열 Hello, world!를 송신하는 코드를 구현하면 다음과 같습니다. UART 헤더 파일은 GitHub을 참고하세요.


#include "gd32vf103.h"

/* USART0 Tx: PA9 (default). */

#define DELAY_COUNT (1000000UL)

static void send_string(const char *s);
static void delay(uint32_t count);

int main(void) {
    /* Enable GPIOA and USART0. */
    RCU->APB2EN |= RCU_APB2EN_PAEN | RCU_APB2EN_USART0EN;

    /* Alternative input/output, max speed 50 MHz, push-pull. */
    GPIOA->CTL1 &= ~(GPIO_CTL1_CTL9 | GPIO_CTL1_MD9);
    GPIOA->CTL1 |= ((0x2UL << GPIO_CTL1_CTL9_Pos)
                    | (0x3UL << GPIO_CTL1_MD9_Pos));

    /* Reset USART0. */
    RCU->APB2RST |= RCU_APB2RST_USART0RST;
    RCU->APB2RST &= ~RCU_APB2RST_USART0RST;

    /* 8 data bits. */
    USART0->CTL0 &= ~USART_CTL0_WL;
    /* No parity bit. */
    USART0->CTL0 &= ~USART_CTL0_PCEN;
    /* 1 Stop bit. */
    USART0->CTL1 &= ~USART_CTL1_STB;
    /* No hardware flow control. */
    USART0->CTL2 &= ~(USART_CTL2_RTSEN | USART_CTL2_CTSEN);
    /* Set baud-rate to 115200: USARTDIV = PCLK / (16 * 115200) = 4.3402777...
        where PCLK is PCLK2 for USART0 and is 8 MHz by default. The integer part
        of the baud-rate (INTDIV) is 4 and fractional part (FRADIV) is
        0.3402777... * 16 = 5 (take the nearest 4-bit integer). The error is
        0.644%, which is acceptable. */
    USART0->BAUD = ((4UL << USART_BAUD_INTDIV_Pos)
                    | (5UL << USART_BAUD_FRADIV_Pos));
    /* Enable transmitter. */
    USART0->CTL0 |= USART_CTL0_TEN;
    /* Enable USART. Do not configure USART registers from now on. */
    USART0->CTL0 |= USART_CTL0_UEN;

    while (1) {
        send_string("Hello, world!\n");
        delay(DELAY_COUNT);
    }
}

static void send_string(const char *s) {
    const char *ptr = s;
    while (*ptr) {
        /* Wait for TBE to be asserted. */
        while (!(USART0->STAT & USART_STAT_TBE)) {}
        /* Write to the data register. */
        USART0->DATA = ((*ptr) << USART_DATA_DATA_Pos) & USART_DATA_DATA_Msk;
        ptr++;
    }
    /* Wait for TC to be asserted. */
    while (!(USART0->STAT & USART_STAT_TC)) {}
}

static void delay(uint32_t count) {
    for (uint32_t i = 0UL; i < count; i++) {
        __asm__("nop");
    }
}

main의 내용은 위에서 설명한 UART 설정과 관련한 내용들입니다. 문자열을 송신하는 함수 send_string은 각 문자에 대해 TBE 비트를 대기한 후 USART_DATA 레지스터에 문자를 쓰고, 제일 마지막에는 TC 비트를 대기하도록 구현하였습니다.