UART trong STM32 (UART Software & Hardware)

UART (Universal Asynchronous Receiver / Transmitter) là một trong những chuẩn giao tiếp nối tiếp phổ biến nhất trong lập trình nhúng. UART trong STM32 có thể được triển khai theo hai cách: UART Software (mô phỏng giao thức bằng GPIO) và UART Hardware (dùng ngoại vi USART tích hợp sẵn). Bài viết này hướng dẫn chi tiết cả hai phương pháp trên STM32F1 kèm code thực tế bằng thư viện SPL.

Sơ đồ đấu nối UART TX-RX chéo giữa hai thiết bị và chung GND
UART dùng 2 đường TX/RX đấu chéo và chung GND

Về bản chất, UART là giao tiếp bất đồng bộ (asynchronous) – không có đường clock chung. Hai thiết bị phải thống nhất trước về tốc độ baudrate, số bit dữ liệu, bit parity và bit stop để hiểu được nhau.

I. UART Software – mô phỏng bằng GPIO

Hầu hết các dòng vi điều khiển đều tích hợp bộ truyền thông UART. Tuy nhiên, để hiểu rõ bản chất giao thức, ta có thể mô phỏng UART bằng GPIO (bit-banging). Cách này còn hữu ích khi số chân UART cứng không đủ dùng.

Cấu hình GPIO

UART chỉ sử dụng 2 chân để truyền: TX (Transmit) và RX (Receive). UART Software không yêu cầu chân cụ thể nên có thể dùng chân bất kỳ:

#define TX_Pin     GPIO_Pin_9
#define RX_Pin     GPIO_Pin_10
#define UART_GPIO  GPIOA

TX là chân truyền nên được cấu hình OUTPUT (Push-Pull), RX là chân nhận nên cấu hình INPUT (Floating):

void GPIO_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    /* TX -> Output Push-Pull */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Pin   = TX_Pin;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(UART_GPIO, &GPIO_InitStructure);

    /* RX -> Input Floating */
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_IN_FLOATING;
    GPIO_InitStructure.GPIO_Pin   = RX_Pin;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(UART_GPIO, &GPIO_InitStructure);
}

Khung truyền UART và baudrate

Tốc độ baudrate được xác định bởi thời gian truyền đi 1 bit. Bài này dùng tốc độ phổ thông 9600 bits/s, ứng với mỗi bit là ~105µs:

/* Baudrate = 9600 bits/s -> 0.10467ms cho 1 bit = 104.67us
   => time delay ~~ 105us */
#define BRateTime 105
Khung truyền UART gồm idle, start bit, data bits, parity và stop bit
Cấu trúc khung truyền UART: start – data – parity – stop

Ở chế độ nghỉ (không truyền), đường TX được giữ ở mức cao. Hàm UART_Config() thiết lập trạng thái nghỉ cho đường truyền:

void UART_Config(void)
{
    GPIO_SetBits(UART_GPIO, TX_Pin);
    delay_us(1);
}

Hàm truyền (Transmit)

Hàm truyền gửi lần lượt 8 bit trong byte dữ liệu, sau khi tín hiệu start được gửi đi. Trình tự: (1) tạo start, delay 1 period; (2) truyền từng bit dữ liệu, mỗi bit 1 period; (3) dịch bit; (4) tạo stop, delay tương ứng số bit stop.

void UART_Transmit(const char DataValue)
{
    /* Send Start Bit */
    GPIO_WriteBit(UART_GPIO, TX_Pin, Bit_RESET);
    delay_us(BRateTime);

    for (unsigned char i = 0; i < 8; i++) {
        if (((DataValue >> i) & 0x1) == 0x1)
            GPIO_WriteBit(UART_GPIO, TX_Pin, Bit_SET);
        else
            GPIO_WriteBit(UART_GPIO, TX_Pin, Bit_RESET);
        delay_us(BRateTime);
    }

    /* Send Stop Bit */
    GPIO_WriteBit(UART_GPIO, TX_Pin, Bit_SET);
    delay_us(BRateTime);
}

Tạo bit Parity

Dữ liệu truyền đi có thể được thêm bit parity tùy theo cấu hình chẵn/lẻ hay không dùng:

typedef enum {
    Parity_Mode_NONE,
    Parity_Mode_ODD,
    Parity_Mode_EVEN
} Parity_Mode;

uint8_t Parity_Generate(uint8_t data, Parity_Mode Mode)
{
    uint8_t count = 0;
    for (int i = 0; i < 8; i++) {
        if (data & 0x01) count++;
        data >>= 1;
    }
    switch (Mode) {
        case Parity_Mode_ODD:
            return (count % 2) ? ((data << 1) | 1) : (data << 1);
        case Parity_Mode_EVEN:
            return (!(count % 2)) ? ((data << 1) | 1) : (data << 1);
        case Parity_Mode_NONE:
        default:
            return data;
    }
}

Hàm nhận (Receive)

Hàm nhận đọc lần lượt 8 bit. Trình tự: (1) chờ tín hiệu start từ thiết bị gửi; (2) delay 1,5 period (để lấy mẫu giữa bit); (3) đọc data trên RX rồi ghi vào biến; (4) dịch bit và delay 1 period; (5) delay 0,5 period rồi đợi stop bit.

unsigned char UART_Receive(void)
{
    unsigned char DataValue = 0;

    /* Chờ start bit (đường RX kéo xuống thấp) */
    while (GPIO_ReadInputDataBit(UART_GPIO, RX_Pin) == 1);

    delay_us(BRateTime);
    delay_us(BRateTime / 2);   /* căn giữa bit */

    for (unsigned char i = 0; i < 8; i++) {
        if (GPIO_ReadInputDataBit(UART_GPIO, RX_Pin) == 1)
            DataValue += (1 << i);
        delay_us(BRateTime);
    }

    /* Kiểm tra stop bit */
    if (GPIO_ReadInputDataBit(UART_GPIO, RX_Pin) == 1) {
        delay_us(BRateTime / 2);
        return DataValue;
    }
    return DataValue;
}

Sau khi nhận được data, có thể tiến hành kiểm tra chẵn/lẻ bằng hàm Parity_Check():

uint8_t Parity_Check(uint8_t data, Parity_Mode Mode)
{
    uint8_t count = 0;
    for (int i = 0; i < 8; i++) {
        if (data & 0x01) count++;
        data >>= 1;
    }
    switch (Mode) {
        case Parity_Mode_ODD:   return (count % 2);
        case Parity_Mode_EVEN:  return (!(count % 2));
        case Parity_Mode_NONE:  return 1;
        default:                return 0;
    }
}

II. UART Hardware – dùng ngoại vi USART

Với UART Hardware, STM32 dùng ngoại vi USART tích hợp sẵn để tự động xử lý việc tạo khung truyền, baudrate, parity… CPU chỉ cần ghi/đọc thanh ghi dữ liệu. Cách này chính xác, nhanh và tiết kiệm tài nguyên hơn nhiều so với bit-banging.

Cấu hình GPIO

Các bộ UART trong STM32F1 được xác định sẵn chân GPIO. Ví dụ với USART1:

ChânChức năng UARTChế độ GPIO
PA9USART1_TXAF Push-Pull (AF_PP)
PA10USART1_RXInput Floating (IN_FLOATING)

Tương tự UART Software: TX là OUTPUT (chế độ Alternate Function Push-Pull) và RX là INPUT:

void GPIO_Config(void)
{
    GPIO_InitTypeDef GPIOInitStruct;

    /* RX (PA10) -> Input Floating */
    GPIOInitStruct.GPIO_Pin  = GPIO_Pin_10;
    GPIOInitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIOInitStruct);

    /* TX (PA9) -> Alternate Function Push-Pull */
    GPIOInitStruct.GPIO_Pin   = GPIO_Pin_9;
    GPIOInitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIOInitStruct.GPIO_Mode  = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOA, &GPIOInitStruct);
}

Cấu hình USART

Tương tự các ngoại vi khác, các tham số UART được cấu hình trong struct USART_InitTypeDef:

Tham sốMô tả
USART_ModeChế độ hoạt động: USART_Mode_TxUSART_Mode_Rx hoặc cả hai
USART_BaudRateTốc độ baudrate (vd 9600, 115200)
USART_HardwareFlowControlChế độ bắt tay (flow control)
USART_WordLengthSố bit mỗi lần truyền (8b/9b)
USART_StopBitsSố lượng stop bit
USART_ParityBit kiểm tra chẵn/lẻ
void UART_Config(void)
{
    USART_InitTypeDef USARTInitStruct;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);

    USARTInitStruct.USART_BaudRate            = 9600;
    USARTInitStruct.USART_WordLength          = USART_WordLength_8b;
    USARTInitStruct.USART_StopBits            = USART_StopBits_1;
    USARTInitStruct.USART_Parity              = USART_Parity_No;
    USARTInitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USARTInitStruct.USART_Mode                = USART_Mode_Rx | USART_Mode_Tx;
    USART_Init(USART1, &USARTInitStruct);

    USART_Cmd(USART1, ENABLE);
}

Các hàm truyền / nhận và cờ trạng thái

  • USART_SendData(USARTx, Data): truyền data từ UARTx (đã tự thêm bit chẵn/lẻ theo cấu hình).
  • USART_ReceiveData(USARTx): nhận data từ UARTx.
  • USART_GetFlagStatus(USARTx, USART_FLAG): trả về trạng thái cờ tương ứng.
Cờ (Flag)Ý nghĩa
USART_FLAG_TXECờ truyền – set lên 1 khi quá trình truyền hoàn tất (thanh ghi rỗng)
USART_FLAG_RXNECờ nhận – set lên 1 khi quá trình nhận hoàn tất (có data mới)
USART_FLAG_IDLECờ báo đường truyền đang ở chế độ Idle
USART_FLAG_PECờ báo lỗi Parity

Quá trình truyền/nhận có thể mô tả như sau: khi bắt đầu, UART xóa data trong thanh ghi DR để đảm bảo dữ liệu đúng. Khi truyền, gửi từng byte rồi đợi cờ TXE bật lên. Khi nhận, đọc data từ bộ UART sau khi cờ RXNE bật. Với mảng dữ liệu, lặp lại quá trình cho từng byte.

/* Truyền 1 byte */
void UART_SendByte(uint8_t data)
{
    USART_SendData(USART1, data);
    while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}

/* Nhận 1 byte */
uint8_t UART_ReadByte(void)
{
    while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
    return (uint8_t)USART_ReceiveData(USART1);
}

/* Truyền cả chuỗi */
void UART_SendString(const char *str)
{
    while (*str) UART_SendByte(*str++);
}

So sánh UART Software và UART Hardware

Tiêu chíUART Software (GPIO)UART Hardware (USART)
Chân sử dụngBất kỳ chân GPIO nàoChân cố định (PA9/PA10…)
Độ chính xác timingPhụ thuộc delay, dễ sai lệchChính xác nhờ phần cứng
Tải CPUCao (CPU bận delay từng bit)Thấp (phần cứng xử lý)
Tốc độ tối đaThấp (thường ≤ 9600)Cao (115200 trở lên)
Ứng dụngHọc hiểu bản chất, thiếu chân UARTSản phẩm thực tế

Lưu ý khi sử dụng UART trong STM32

  • Hai thiết bị phải cùng baudrate, số data bit, parity và stop bit – nếu lệch sẽ nhận sai (lỗi framing).
  • Đấu nối TX ↔ RX chéo nhau và chung GND giữa hai thiết bị.
  • Với UART Hardware, nhớ cấp clock cho USART (APB2 với USART1, APB1 với USART2/3) và cấu hình TX ở chế độ AF_PP.
  • Nên dùng ngắt UART (RXNE) hoặc DMA để nhận dữ liệu thay vì polling, tránh chiếm CPU.
  • UART Software phù hợp để học, nhưng sản phẩm thực tế nên ưu tiên UART Hardware vì ổn định và nhanh hơn.

Kết luận

UART trong STM32 là kỹ năng nền tảng cho hầu hết ứng dụng nhúng – từ debug qua cổng serial, giao tiếp với module (Bluetooth, GPS, GSM) đến truyền dữ liệu giữa các vi điều khiển. Qua bài viết, bạn đã nắm được cả hai cách triển khai: UART Software mô phỏng bằng GPIO giúp hiểu rõ khung truyền, start/stop bit và parity; còn UART Hardware dùng ngoại vi USART cho hiệu năng và độ ổn định tối ưu.

Nắm vững cấu trúc khung truyền và quy trình cấu hình (GPIO → USART → truyền/nhận theo cờ TXE/RXNE) sẽ giúp bạn tự tin ứng dụng UART trong các dự án automotive embedded như giao tiếp ECU, đọc dữ liệu cảm biến hay xây dựng giao thức truyền thông tùy biến.

Leave a Reply