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.
| Phần | Mức | Số bit | Vai trò |
|---|---|---|---|
| Nghỉ | Cao | Không xác định | Đường ở mức cao khi không truyền |
| Bit khởi đầu | Thấp | 1 | Báo cho bên nhận biết khung bắt đầu, để đồng bộ |
| Dữ liệu | Tùy | 5 tới 9, thường là 8 | Bit thấp gửi trước |
| Bit chẵn lẻ | Tùy | 0 hoặc 1 | Kiểm lỗi đơn giản, thường không dùng |
| Bit kết thúc | Cao | 1, 1.5 hoặc 2 | Cho 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ỗi | Nghĩa | Nguyên nhân thường gặp | Cách xóa |
|---|---|---|---|
| ORE | Tràn, byte mới đè byte cũ chưa đọc | ISR bị chặn quá lâu, hoặc ngắt ưu tiên thấp | Đọc SR rồi đọc DR |
| FE | Lỗi khung, bit kết thúc không phải mức cao | Sai tốc độ baud, hoặc nhiễu, hoặc đứt dây | Đọc SR rồi đọc DR |
| NE | Nhiễu, ba lần lấy mẫu trong một bit không giống nhau | Dây quá dài, nhiễu điện từ, thiếu nối đất chung | Đọc SR rồi đọc DR |
| PE | Sai 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ứng | Nguyên nhân | Cá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 đặn | Sai tốc độ baud | Thử các tốc độ chuẩn, hoặc đo độ rộng một bit |
| Nhận được vài byte đầu rồi rác | Sai số baud quá lớn do bộ dao động nội | Chuyển sang thạch anh ngoài |
| Chạy được rồi tự dừng | Cờ ORE không được xóa | In thống kê lỗi, xem mat_do_ore |
| Mất byte khi gửi nhanh | Bộ đệm quá nhỏ, hoặc ISR ưu tiên thấp | Tăng bộ đệm, nâng ưu tiên ngắt |
Tự làm thử
- Tính
BRRcho các tốc độ 9600, 115200 và 921600 vớifCKbằng 84 MHz, rồi tính sai số từng trường hợp. - 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.
- 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.
- 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.
- Đổ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ằngfCK / baud, và sai số nên giữ dưới hai phần trăm mỗi bên. - Cờ
TXEnghĩa là ghi byte tiếp theo được, cònTCnghĩ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ờ
ORElàm UART chết hẳn, vì cờRXNEkhông được đặt nữa cho tới khiOREđược xóa.