Trong các hệ thống nhúng, việc sử dụng cổng nối tiếp (UART) để nhận lệnh điều khiển từ máy tính hoặc thiết bị ngoại vi là một kỹ thuật phổ biến. Bài viết này trình bày cách xây dựng một module nhận lệnh qua UART trên vi điều khiển STM32, phân tích cú pháp chuỗi ký tự và thực thi thao tác bật/tắt đèn LED tương ứng.
1. Định Nghĩa Cấu Hình Và Bộ Đệm
Để tối ưu hóa quá trình xử lý, chúng ta sẽ quy ước trước các chuỗi lệnh điều khiển. Thay vì xử lý dữ liệu thô, hệ thống sẽ dựa vào các macro và bộ đệm có kích thước cố định để lưu trữ dữ liệu nhận được. Dưới đây là các định nghĩa cơ bản:
#define TURN_LED_ON() GPIO_ResetBits(GPIOC, GPIO_Pin_13)
#define TURN_LED_OFF() GPIO_SetBits(GPIOC, GPIO_Pin_13)
#define UART_BUFFER_SIZE 128
volatile uint8_t uart_rx_buffer[UART_BUFFER_SIZE];
volatile uint8_t rx_write_index = 0;
Do thư viện chuẩn của STM32 không tích hợp sẵn kiểu dữ liệu logic, ta có thể tự định nghĩa thông qua enum:
typedef enum {
LOGIC_FALSE = 0,
LOGIC_TRUE = 1
} bool_t;
2. Hàm Trì Hoãn Sử Dụng SysTick
Để tạo độ trễ chính xác mà không phụ thuộc vào vòng lặp rỗng, bộ đếm SysTick được cấu hình trực tiếp. Hai hàm dưới đây hỗ trợ trễ theo micro giây và mili giây:
void sys_tick_delay_us(uint32_t us)
{
uint32_t ctrl_status;
SysTick->LOAD = 9 * us;
SysTick->VAL = 0x00;
SysTick->CTRL = 0x01;
do {
ctrl_status = SysTick->CTRL;
} while ((ctrl_status & 0x01) && !(ctrl_status & (1 << 16)));
SysTick->CTRL = 0x00;
SysTick->VAL = 0x00;
}
void sys_tick_delay_ms(uint16_t ms)
{
uint32_t ctrl_status;
SysTick->LOAD = 9000 * ms;
SysTick->VAL = 0x00;
SysTick->CTRL = 0x01;
do {
ctrl_status = SysTick->CTRL;
} while ((ctrl_status & 0x01) && !(ctrl_status & (1 << 16)));
SysTick->CTRL = 0x00;
SysTick->VAL = 0x00;
}
3. Khởi Tạo Ngoại Vi
Trước khi vận hành, các khối chức năng bao gồm GPIO cho LED và USART1 kèm ngắt nhận dữ liệu cần được cấu hình đúng trình tự.
3.1. Cấu Hình GPIO Cho LED
void configure_led_gpio(void)
{
GPIO_InitTypeDef gpio_cfg;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
gpio_cfg.GPIO_Pin = GPIO_Pin_13;
gpio_cfg.GPIO_Speed = GPIO_Speed_50MHz;
gpio_cfg.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOC, &gpio_cfg);
}
3.2. Cấu Hình USART1 Và Ngắt NVIC
void configure_uart1(uint32_t baud_rate)
{
GPIO_InitTypeDef gpio_cfg;
USART_InitTypeDef uart_cfg;
NVIC_InitTypeDef nvic_cfg;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
USART_DeInit(USART1);
// Chân TX (PA9)
gpio_cfg.GPIO_Pin = GPIO_Pin_9;
gpio_cfg.GPIO_Mode = GPIO_Mode_AF_PP;
gpio_cfg.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_cfg);
// Chân RX (PA10)
gpio_cfg.GPIO_Pin = GPIO_Pin_10;
gpio_cfg.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &gpio_cfg);
uart_cfg.USART_BaudRate = baud_rate;
uart_cfg.USART_WordLength = USART_WordLength_8b;
uart_cfg.USART_StopBits = USART_StopBits_1;
uart_cfg.USART_Parity = USART_Parity_No;
uart_cfg.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
uart_cfg.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &uart_cfg);
nvic_cfg.NVIC_IRQChannel = USART1_IRQn;
nvic_cfg.NVIC_IRQChannelPreemptionPriority = 2;
nvic_cfg.NVIC_IRQChannelSubPriority = 2;
nvic_cfg.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&nvic_cfg);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
USART_Cmd(USART1, ENABLE);
}
4. Xử Lý Ngắt UART Và Phân Tích Lệnh
Khi dữ liệu đến chân RX, ngắt USART_IT_RXNE được kích hoạt. Trình phục vụ ngắt sẽ đọc từng byte, lưu vào bộ đệm vòng và kiểm tra ký tự kết thúc dòng (\r\n). Khi nhận đủ một dòng lệnh, hệ thống sẽ gọi hàm phân tích.
void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
uint8_t received_byte = USART_ReceiveData(USART1);
// Kiểm tra tràn bộ đệm và reset chỉ số
if (rx_write_index >= UART_BUFFER_SIZE) {
rx_write_index = 0;
}
uart_rx_buffer[rx_write_index++] = received_byte;
// Phát hiện ký tự xuống dòng \r\n
if (rx_write_index >= 2 &&
uart_rx_buffer[rx_write_index - 1] == '\n' &&
uart_rx_buffer[rx_write_index - 2] == '\r')
{
uart_rx_buffer[rx_write_index - 2] = '\0'; // Kết thúc chuỗi
execute_uart_command(uart_rx_buffer);
rx_write_index = 0; // Xóa bộ đệm cho lần nhận tiếp theo
}
}
}
Hàm phân tích lệnh sẽ so sánh chuỗi nhận được với các từ khóa định sẵn. Nếu khớp, thao tác GPIO tương ứng được thực thi và phản hồi trạng thái qua UART.
int execute_uart_command(uint8_t *cmd_str)
{
if (strcmp((char*)cmd_str, "LED_ON") == 0) {
TURN_LED_ON();
uart_transmit_string((uint8_t*)"SUCCESS: LED ENABLED\r\n");
return 0;
}
else if (strcmp((char*)cmd_str, "LED_OFF") == 0) {
TURN_LED_OFF();
uart_transmit_string((uint8_t*)"SUCCESS: LED DISABLED\r\n");
return 0;
}
else {
uart_transmit_string((uint8_t*)"INVALID_CMD\r\n");
return -1;
}
}
5. Hàm Truyền Dữ Liệu Qua UART
Việc gửi dữ liệu được thực hiện bằng cách chờ cờ TXE (Transmit Data Register Empty) trước khi đẩy byte tiếp tục vào thanh ghi dữ liệu.
void uart_transmit_byte(uint8_t byte_data)
{
USART_SendData(USART1, byte_data);
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
void uart_transmit_string(uint8_t *str_data)
{
while (*str_data != '\0') {
uart_transmit_byte(*str_data++);
}
}
6. Hàm Chính
Vòng lặp chính chỉ cần duy trì hoạt động của hệ thống. Toàn bộ logic điều khiển đã được xử lý bất đồng bộ thông qua cơ chế ngắt UART.
int main(void)
{
bool_t system_ready = LOGIC_FALSE;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
configure_uart1(115200);
configure_led_gpio();
system_ready = LOGIC_TRUE;
while (1)
{
sys_tick_delay_ms(500);
// Các tác vụ nền khác có thể được thêm tại đây
}
}