Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 47.630 phút đọc

UART

Sau bài này bạn sẽ làm được

  • Vẽ được khung truyền UART và tính thời gian một byte
  • Cấu hình tốc độ baud và hiểu sai số cho phép
  • Cài truyền nhận theo kiểu chờ cờ
  • Cài bộ đệm vòng nhận bằng ngắt, an toàn với một bên ghi một bên đọc

UART là cách đơn giản nhất để vi điều khiển nói chuyện với máy tính, và cũng là công cụ gỡ lỗi chính khi không có màn hình. Hai dây, không xung nhịp chung, và hai bên phải tự thỏa thuận tốc độ.

#Khung truyền

UART
Universal Asynchronous Receiver Transmitter. Truyền từng bit nối tiếp trên một dây, không có dây xung nhịp. Hai bên phải cấu hình cùng tốc độ, và bên nhận tự đồng bộ lại ở mỗi khung nhờ bit khởi đầu.
Một khung gồm bit khởi đầu mức thấp, tám bit dữ liệu gửi bit thấp trước, và bit kết thúc mức cao.
PhầnMứcSố bitVai trò
NghỉCaoKhông xác địnhĐường ở mức cao khi không truyền
Bit khởi đầuThấp1Báo cho bên nhận biết khung bắt đầu, để đồng bộ
Dữ liệuTùy5 tới 9, thường là 8Bit thấp gửi trước
Bit chẵn lẻTùy0 hoặc 1Kiểm lỗi đơn giản, thường không dùng
Bit kết thúcCao1, 1.5 hoặc 2Cho bên nhận thời gian xử lý
Tính thời gian
/* Thời gian một bit = 1 / baud

   115200 baud  ->  1 / 115200 = 8.68 micro giây
   9600 baud    ->  1 / 9600   = 104.2 micro giây

   Một khung 8-N-1 có 10 bit: 1 khởi đầu + 8 dữ liệu + 1 kết thúc

   115200 baud  ->  10 * 8.68  = 86.8 micro giây mỗi byte
                    1 / 86.8us = 11520 byte mỗi giây

   9600 baud    ->  10 * 104.2 = 1042 micro giây mỗi byte
                    960 byte mỗi giây

   Nhận xét: chia baud cho 10 là ra số byte mỗi giây. */

#Tính tốc độ baud và sai số

baud.c
/* Trên STM32F4, thanh ghi BRR là số dấu chấm cố định 12.4:
     12 bit phần nguyên ở bit [15:4]
      4 bit phần thập phân ở bit [3:0]

   Với chế độ lấy mẫu 16 lần mỗi bit, tức OVER8 = 0:

     USARTDIV = fCK / (16 * baud)

   và BRR chứa USARTDIV nhân 16, tức đúng bằng fCK / baud.

   Nên công thức thực dụng chỉ là: */

USART1->BRR = fCK / baud;

/* Ví dụ USART1 chạy trên APB2 ở 84 MHz, muốn 115200 baud:

     BRR = 84000000 / 115200 = 729.16...  ->  729

     Kiểm ngược: USARTDIV = 729 / 16 = 45.5625
                 baud thật = 84000000 / (16 * 45.5625) = 115226.3

     Sai số = (115226.3 - 115200) / 115200 = 0.023 phần trăm    */
terminal
./tinh-baud 84000000
fCK = 84000000 Hz

   baud     BRR   baud that   sai so
   9600    8750     9600.00   0.000%
  19200    4375    19200.00   0.000%
  38400    2188    38391.22  -0.023%
  57600    1458    57613.17   0.023%
 115200     729   115226.34   0.023%
 230400     365   230136.99  -0.114%
 460800     182   461538.46   0.160%
 921600      91   923076.92   0.160%
uart-khoi-tao.c
#include "stm32f4.h"

/* Cấu hình USART1 ở PA9 (TX) và PA10 (RX), 115200-8-N-1. */
void uart_khoi_tao(uint32_t fck, uint32_t baud)
{
    /* 1. Bật clock cho GPIOA và cho USART1 */
    RCC->AHB1ENR |= (1u << 0);      /* GPIOA */
    RCC->APB2ENR |= (1u << 4);      /* USART1 nằm trên APB2 */

    /* 2. Chuyển PA9 và PA10 sang chế độ chức năng thay thế */
    GPIOA->MODER &= ~((3u << (9 * 2)) | (3u << (10 * 2)));
    GPIOA->MODER |=  ((2u << (9 * 2)) | (2u << (10 * 2)));   /* 10 = AF */

    /* 3. Chọn chức năng AF7, tức USART1, cho hai chân đó.
          AFR[1] lo các chân 8 tới 15, mỗi chân bốn bit. */
    GPIOA->AFR[1] &= ~((0xFu << ((9 - 8) * 4)) | (0xFu << ((10 - 8) * 4)));
    GPIOA->AFR[1] |=  ((7u   << ((9 - 8) * 4)) | (7u   << ((10 - 8) * 4)));

    /* 4. Tốc độ baud */
    USART1->BRR = fck / baud;

    /* 5. Bật bộ phát, bộ thu, rồi bật cả khối. Bật khối SAU CÙNG. */
    USART1->CR1 = (1u << 3)      /* TE, transmit enable */
                | (1u << 2)      /* RE, receive enable */
                | (1u << 13);    /* UE, USART enable */
}

#Truyền nhận kiểu chờ cờ

uart-cho-co.c
#include "stm32f4.h"

#define USART_SR_TXE   (1u << 7)      /* thanh ghi phát trống */
#define USART_SR_TC    (1u << 6)      /* truyền xong hoàn toàn */
#define USART_SR_RXNE  (1u << 5)      /* thanh ghi thu có dữ liệu */
#define USART_SR_ORE   (1u << 3)      /* tràn, mất byte */
#define USART_SR_NE    (1u << 2)      /* nhiễu */
#define USART_SR_FE    (1u << 1)      /* lỗi khung */

void uart_gui_byte(uint8_t b)
{
    while (!(USART1->SR & USART_SR_TXE)) { }   /* chờ chỗ trống */

    USART1->DR = b;
}

void uart_gui_chuoi(const char *s)
{
    while (*s != '\0')
        uart_gui_byte((uint8_t)*s++);
}

void uart_gui_khoi(const uint8_t *d, uint16_t n)
{
    for (uint16_t i = 0; i < n; ++i)
        uart_gui_byte(d[i]);
}

uint8_t uart_nhan_byte(void)
{
    while (!(USART1->SR & USART_SR_RXNE)) { }  /* chờ có dữ liệu */

    return (uint8_t)USART1->DR;                /* đọc DR tự xóa cờ RXNE */
}

Nối vào máy tính

terminal
# Trên Linux, tìm cổng nối tiếp
ls /dev/ttyUSB* /dev/ttyACM*
/dev/ttyUSB0
# Mở với tốc độ đúng
screen /dev/ttyUSB0 115200
Xin chao tu STM32!
tick = 1000
tick = 2000
# Hoặc dùng picocom, thoát bằng Ctrl-A rồi Ctrl-X
picocom -b 115200 /dev/ttyUSB0
Terminal ready
Xin chao tu STM32!
# Ghi log ra tệp trong lúc vẫn xem được
picocom -b 115200 /dev/ttyUSB0 | tee nhat-ky.txt

#Nhận bằng ngắt với bộ đệm vòng

Chờ cờ để nhận là không dùng được trong thực tế: dữ liệu tới bất cứ lúc nào, và bạn không thể đứng chờ. Lời giải là ngắt ghi vào bộ đệm vòng, còn vòng lặp chính đọc ra khi rảnh. Đây đúng cấu trúc ở Bài 23.2.

uart-ngat.c
#include <stdint.h>

#include "stm32f4.h"

#define RX_SIZE 256u
#define RX_MASK (RX_SIZE - 1u)

_Static_assert((RX_SIZE & RX_MASK) == 0u, "RX_SIZE phai la luy thua cua 2");

/* Chỉ ISR ghi rx_ghi, chỉ vòng lặp chính ghi rx_doc.
   Nhờ vậy không cần khóa, xem Bài 23.2 và Bài 49.4. */
static volatile uint8_t  rx_dem[RX_SIZE];
static volatile uint16_t rx_ghi = 0u;
static volatile uint16_t rx_doc = 0u;
static volatile uint32_t rx_mat = 0u;      /* đếm số byte bị bỏ */

void uart_bat_ngat_nhan(void)
{
    USART1->CR1 |= (1u << 5);      /* RXNEIE, cho phép ngắt khi có dữ liệu */

    NVIC_SetPriority(USART1_IRQn, 2u);
    NVIC_EnableIRQ(USART1_IRQn);
}

void USART1_IRQHandler(void)
{
    uint32_t sr = USART1->SR;      /* đọc MỘT lần, xem Bài 47.2 */

    if (sr & (1u << 5)) {                       /* RXNE */
        uint8_t  b  = (uint8_t)USART1->DR;      /* đọc DR xóa cờ RXNE */
        uint16_t ke = (uint16_t)((rx_ghi + 1u) & RX_MASK);

        if (ke != rx_doc) {
            rx_dem[rx_ghi] = b;                 /* ghi dữ liệu TRƯỚC */
            rx_ghi         = ke;                /* rồi mới công bố */
        } else {
            ++rx_mat;                           /* đầy, bỏ byte này */
        }
    }

    if (sr & (1u << 3)) {                       /* ORE, tràn phần cứng */
        (void)USART1->DR;                       /* đọc DR để xóa cờ */
        ++rx_mat;
    }
}

/* Gọi từ vòng lặp chính. Trả về 1 nếu lấy được byte. */
int uart_doc(uint8_t *ra)
{
    if (rx_doc == rx_ghi) return 0;             /* rỗng */

    *ra    = rx_dem[rx_doc];
    rx_doc = (uint16_t)((rx_doc + 1u) & RX_MASK);

    return 1;
}

uint16_t uart_so_byte(void)
{
    return (uint16_t)((rx_ghi - rx_doc) & RX_MASK);
}
main.c
#include "uart.h"

int main(void)
{
    uart_khoi_tao(84000000u, 115200u);
    uart_bat_ngat_nhan();
    uart_gui_chuoi("San sang\r\n");

    for (;;) {
        uint8_t b;

        while (uart_doc(&b)) {
            uart_gui_byte(b);            /* dội lại cho người gõ thấy */

            if (b == '\r') uart_gui_chuoi("\r\n> ");
        }

        /* Vòng lặp chính vẫn làm việc khác, không bị chặn */
        cap_nhat_cam_bien();
        cap_nhat_den();
    }
}
terminal
picocom -b 115200 /dev/ttyUSB0
San sang
hello
> 

#Xử lý lỗi khung và tràn

Cờ lỗiNghĩaNguyên nhân thường gặpCách xóa
ORETràn, byte mới đè byte cũ chưa đọcISR bị chặn quá lâu, hoặc ngắt ưu tiên thấpĐọc SR rồi đọc DR
FELỗi khung, bit kết thúc không phải mức caoSai tốc độ baud, hoặc nhiễu, hoặc đứt dâyĐọc SR rồi đọc DR
NENhiễu, ba lần lấy mẫu trong một bit không giống nhauDây quá dài, nhiễu điện từ, thiếu nối đất chungĐọc SR rồi đọc DR
PESai bit chẵn lẻNhiễu, hoặc hai bên cấu hình chẵn lẻ khác nhauĐọc SR rồi đọc DR

Ba nguyên nhân thường gặp khi UART không chạy

Triệu chứngNguyên nhânCách kiểm
Không nhận được gì cảNhầm chân TX với RX, hoặc quên nối đất chungĐo bằng máy hiện sóng, hoặc thử đảo hai dây
Nhận được rác đều đặnSai tốc độ baudThử các tốc độ chuẩn, hoặc đo độ rộng một bit
Nhận được vài byte đầu rồi rácSai số baud quá lớn do bộ dao động nộiChuyển sang thạch anh ngoài
Chạy được rồi tự dừngCờ ORE không được xóaIn thống kê lỗi, xem mat_do_ore
Mất byte khi gửi nhanhBộ đệm quá nhỏ, hoặc ISR ưu tiên thấpTăng bộ đệm, nâng ưu tiên ngắt

Tự làm thử

  1. Tính BRR cho các tốc độ 9600, 115200 và 921600 với fCK bằng 84 MHz, rồi tính sai số từng trường hợp.
  2. Cấu hình UART và gửi một chuỗi bằng vòng lặp chờ cờ, đo thời gian chặn bằng cách bật một chân GPIO trước và tắt sau.
  3. Cài bộ đệm vòng nhận bằng ngắt, rồi gửi mười nghìn byte từ máy tính và đếm số byte nhận được.
  4. Cố tình bỏ phần xóa cờ ORE, gửi dữ liệu nhanh và xác nhận UART ngừng hoạt động sau vài chục byte.
  5. Đổi tốc độ baud ở một bên thành 111000 và quan sát chuỗi rác nhận được, rồi tính sai số phần trăm.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Một khung 8-N-1 gồm mười bit, nên tốc độ baud chia mười là số byte mỗi giây.
  • Trên STM32F4, BRR đặt bằng fCK / baud, và sai số nên giữ dưới hai phần trăm mỗi bên.
  • Cờ TXE nghĩa là ghi byte tiếp theo được, còn TC nghĩa là bit cuối đã ra khỏi chân. Chờ nhầm cờ làm mất byte cuối.
  • Nhận bằng ngắt với bộ đệm vòng là cách duy nhất dùng được thật, và ISR phải ghi dữ liệu trước rồi mới cập nhật chỉ số.
  • Không xóa cờ ORE làm UART chết hẳn, vì cờ RXNE không được đặt nữa cho tới khi ORE được xóa.