Vỏ lệnh UART
Sau bài này bạn sẽ làm được
- Ghép bộ đệm vòng nhận với vòng lặp chính
- Tách một dòng thành lệnh và tham số mà không cấp phát động
- Dùng bảng lệnh bằng con trỏ hàm thay cho chuỗi if
- Xử lý phím xóa lùi, phím Enter và dòng quá dài
Một vỏ lệnh qua UART là công cụ gỡ lỗi mạnh nhất mà bạn có thể tự làm. Nó cho phép đọc trạng thái, đổi tham số và kích hoạt chức năng mà không phải nạp lại chương trình, và nó dùng lại gần như mọi thứ đã học.
#Kiến trúc ba tầng
UART phan cung
|
v
[ISR nhan] ---> bo dem vong 256 byte <- Bai 47.6
|
v
[doc dong] ---> bo dem dong 64 byte, cho toi khi gap Enter
|
v
[tach lenh] ---> mang con tro toi tung tu
|
v
[bang lenh] ---> goi ham xu ly tuong ung <- Bai 16.2
|
v
[xu ly] ---> in ket qua qua UART| Tầng | Chạy ở đâu | Bộ nhớ | Trách nhiệm |
|---|---|---|---|
| ISR nhận | Ngắt USART1 | 256 byte bộ đệm vòng | Lấy byte khỏi phần cứng, không mất |
| Đọc dòng | Vòng lặp chính | 64 byte bộ đệm dòng | Gom byte cho tới khi gặp ký tự xuống dòng |
| Tách lệnh | Vòng lặp chính | 8 con trỏ | Chia dòng thành các từ, không cấp phát gì |
| Bảng lệnh | Vòng lặp chính | Bảng hằng trong Flash | Tìm hàm xử lý theo tên lệnh |
| Xử lý | Vòng lặp chính | Tùy lệnh | Làm việc thật và in kết quả |
#Đọc một dòng không chặn
#ifndef DONG_H
#define DONG_H
#include <stdint.h>
#define DONG_TOI_DA 64u
typedef struct {
char bo_dem[DONG_TOI_DA];
uint8_t n;
uint8_t qua_dai; /* cờ báo dòng vừa rồi bị cắt */
} BoDocDong;
void doc_dong_khoi_tao(BoDocDong *d);
char *doc_dong_cap_nhat(BoDocDong *d); /* NULL nếu chưa xong dòng */
#endif#include "dong.h"
#include "uart.h"
void doc_dong_khoi_tao(BoDocDong *d)
{
d->n = 0u;
d->qua_dai = 0u;
}
/* Gọi mỗi vòng lặp chính. Trả về con trỏ tới dòng đã kết thúc,
hoặc NULL nếu chưa có dòng nào hoàn chỉnh. */
char *doc_dong_cap_nhat(BoDocDong *d)
{
uint8_t b;
while (uart_doc(&b)) {
if (b == '\r' || b == '\n') {
if (d->n == 0u && !d->qua_dai) continue; /* dòng rỗng, bỏ qua */
uart_gui_chuoi("\r\n");
if (d->qua_dai) {
uart_gui_chuoi("loi: dong qua dai\r\n");
d->n = 0u;
d->qua_dai = 0u;
continue;
}
d->bo_dem[d->n] = '\0';
d->n = 0u;
return d->bo_dem;
}
if (b == 0x08u || b == 0x7Fu) { /* xóa lùi hoặc Delete */
if (d->n > 0u) {
--d->n;
uart_gui_chuoi("\b \b"); /* xóa ký tự trên màn hình */
}
continue;
}
if (b < 0x20u || b > 0x7Eu) continue; /* bỏ ký tự điều khiển */
if (d->n + 1u >= DONG_TOI_DA) {
d->qua_dai = 1u; /* nhớ để báo lỗi khi Enter */
continue;
}
d->bo_dem[d->n++] = (char)b;
uart_gui_byte(b); /* dội lại cho người gõ thấy */
}
return NULL;
}#Tách dòng thành lệnh và tham số
#include <stddef.h>
#define TU_TOI_DA 8u
/* Tách chuỗi thành các từ, ghi con trỏ vào mảng ra.
SỬA TẠI CHỖ: thay khoảng trắng bằng ký tự kết thúc chuỗi.
Trả về số từ tìm được. Không cấp phát gì. */
uint8_t tach_tu(char *dong, char *ra[], uint8_t toi_da)
{
uint8_t n = 0u;
char *p = dong;
while (*p != '\0' && n < toi_da) {
while (*p == ' ' || *p == '\t') ++p; /* bỏ khoảng trắng đầu */
if (*p == '\0') break;
ra[n++] = p; /* ghi lại đầu từ */
while (*p != '\0' && *p != ' ' && *p != '\t') ++p;
if (*p != '\0') *p++ = '\0'; /* cắt từ */
}
return n;
}so tu: 3 [0] = 'set' [1] = 'nguong' [2] = '1200'
so tu: 0
so tu: 8 <- cat o TU_TOI_DA [7] = 'h'
Đổi tham số sang số
#include <stdint.h>
/* Đổi chuỗi sang số nguyên không dấu.
Trả về 0 nếu thành công, -1 nếu chuỗi không hợp lệ hoặc tràn. */
int chuoi_sang_uint(const char *s, uint32_t *ra)
{
if (s == NULL || *s == '\0') return -1;
uint32_t v = 0u;
for (; *s != '\0'; ++s) {
if (*s < '0' || *s > '9') return -1; /* ký tự không phải chữ số */
uint32_t chu_so = (uint32_t)(*s - '0');
if (v > (0xFFFFFFFFu - chu_so) / 10u) return -1; /* sẽ tràn */
v = v * 10u + chu_so;
}
*ra = v;
return 0;
}'0' -> 0 ok '1200' -> 1200 ok '4294967295' -> 4294967295 ok '4294967296' -> loi tran '12a' -> loi ky tu '' -> loi rong '-5' -> loi ky tu
#Bảng lệnh bằng con trỏ hàm
#ifndef LENH_H
#define LENH_H
#include <stdint.h>
typedef void (*HamLenh)(uint8_t n, char *tham_so[]);
typedef struct {
const char *ten;
const char *mo_ta;
const char *cach_dung;
HamLenh ham;
} MucLenh;
void lenh_chay(char *dong);
#endif#include <string.h>
#include "lenh.h"
#include "uart.h"
static void lenh_help(uint8_t n, char *ts[]);
static void lenh_den(uint8_t n, char *ts[]);
static void lenh_adc(uint8_t n, char *ts[]);
static void lenh_status(uint8_t n, char *ts[]);
static void lenh_reset(uint8_t n, char *ts[]);
/* Bảng nằm trong Flash nhờ const, không tốn RAM */
static const MucLenh BANG[] = {
{ "help", "Liet ke moi lenh", "help", lenh_help },
{ "den", "Bat, tat hoac dao den", "den on|off|toggle", lenh_den },
{ "adc", "Doc gia tri ADC", "adc [kenh]", lenh_adc },
{ "status", "In trang thai he thong", "status", lenh_status },
{ "reset", "Khoi dong lai", "reset", lenh_reset },
};
#define SO_LENH (sizeof BANG / sizeof BANG[0])
static void lenh_help(uint8_t n, char *ts[])
{
(void)n; (void)ts;
for (uint8_t i = 0; i < SO_LENH; ++i) {
uart_gui_chuoi(" ");
uart_gui_chuoi_can(BANG[i].ten, 10u); /* căn lề 10 ký tự */
uart_gui_chuoi(BANG[i].mo_ta);
uart_gui_chuoi("\r\n");
}
}
static void lenh_den(uint8_t n, char *ts[])
{
if (n < 2u) {
uart_gui_chuoi("cach dung: den on|off|toggle\r\n");
return;
}
if (strcmp(ts[1], "on") == 0) gpio_bat(GPIOA, 5u);
else if (strcmp(ts[1], "off") == 0) gpio_tat(GPIOA, 5u);
else if (strcmp(ts[1], "toggle") == 0) gpio_dao(GPIOA, 5u);
else {
uart_gui_chuoi("tham so khong hieu: ");
uart_gui_chuoi(ts[1]);
uart_gui_chuoi("\r\n");
return;
}
uart_gui_chuoi("ok\r\n");
}
void lenh_chay(char *dong)
{
char *ts[8];
uint8_t n = tach_tu(dong, ts, 8u);
if (n == 0u) return;
for (uint8_t i = 0; i < SO_LENH; ++i)
if (strcmp(ts[0], BANG[i].ten) == 0) {
BANG[i].ham(n, ts);
return;
}
uart_gui_chuoi("lenh khong biet: ");
uart_gui_chuoi(ts[0]);
uart_gui_chuoi("\r\ngo 'help' de xem danh sach\r\n");
}Vo lenh, go 'help' > help help Liet ke moi lenh den Bat, tat hoac dao den adc Doc gia tri ADC status In trang thai he thong reset Khoi dong lai > den on ok > adc 0 kenh 0: 2048 = 1650 mV > abc lenh khong biet: abc go 'help' de xem danh sach
#Hoàn thiện: dội chữ, phím xóa, lịch sử
Dội chữ
Gửi lại mỗi ký tự nhận được để người gõ nhìn thấy. Đã có trong hàm đọc dòng ở trên. Nếu terminal của người dùng cũng tự hiện chữ thì họ thấy mỗi ký tự hai lần, nên nên có lệnh tắt dội.
Xóa lùi
Nhận mã 0x08 hoặc 0x7F, giảm chỉ số và gửi chuỗi ba ký tự
\b, khoảng trắng,\bđể xóa trên màn hình.Dấu nhắc
In một dấu nhắc sau mỗi lệnh để người dùng biết hệ thống đã sẵn sàng nhận lệnh tiếp theo.
Lịch sử lệnh
Giữ vài dòng gần nhất và cho gọi lại bằng phím mũi tên lên. Cần xử lý chuỗi thoát ANSI, xem dưới.
#include <string.h>
#define LICH_SU_N 4u
#define DONG_TOI_DA 64u
static char s_lich_su[LICH_SU_N][DONG_TOI_DA];
static uint8_t s_ls_moi_nhat = 0u; /* chỉ số ô sẽ ghi tiếp */
static uint8_t s_ls_dem = 0u; /* số dòng đã lưu, tối đa LICH_SU_N */
static uint8_t s_ls_dang_xem = 0u; /* 0 nghĩa là đang gõ dòng mới */
void lich_su_them(const char *dong)
{
if (dong[0] == '\0') return;
/* Không lưu nếu trùng dòng ngay trước */
if (s_ls_dem > 0u) {
uint8_t truoc = (uint8_t)((s_ls_moi_nhat + LICH_SU_N - 1u) % LICH_SU_N);
if (strcmp(s_lich_su[truoc], dong) == 0) return;
}
snprintf(s_lich_su[s_ls_moi_nhat], DONG_TOI_DA, "%s", dong);
s_ls_moi_nhat = (uint8_t)((s_ls_moi_nhat + 1u) % LICH_SU_N);
if (s_ls_dem < LICH_SU_N) ++s_ls_dem;
s_ls_dang_xem = 0u;
}
/* Lấy dòng thứ k từ mới nhất. k bằng 1 là dòng gần nhất. */
const char *lich_su_lay(uint8_t k)
{
if (k == 0u || k > s_ls_dem) return NULL;
uint8_t i = (uint8_t)((s_ls_moi_nhat + LICH_SU_N - k) % LICH_SU_N);
return s_lich_su[i];
}/* Phim mui ten gui BA byte: 0x1B, '[', roi 'A' hoac 'B'.
Phai nho trang thai qua ba lan goi, vi chung toi rieng le. */
typedef enum { ESC_KHONG, ESC_DA_1B, ESC_DA_NGOAC } TrangThaiEsc;
static TrangThaiEsc s_esc = ESC_KHONG;
/* Tra ve 1 neu byte da duoc xu ly nhu mot phan cua chuoi thoat. */
static uint8_t xu_ly_esc(uint8_t b, BoDocDong *d)
{
switch (s_esc) {
case ESC_KHONG:
if (b == 0x1Bu) { s_esc = ESC_DA_1B; return 1u; }
return 0u;
case ESC_DA_1B:
s_esc = (b == '[') ? ESC_DA_NGOAC : ESC_KHONG;
return 1u;
case ESC_DA_NGOAC:
s_esc = ESC_KHONG;
if (b == 'A') { goi_lai_lich_su(d, +1); return 1u; }
if (b == 'B') { goi_lai_lich_su(d, -1); return 1u; }
return 1u; /* mui ten trai phai: bo qua */
default:
s_esc = ESC_KHONG;
return 0u;
}
}> den on ok > adc 0 kenh 0: 2048 = 1650 mV > [mui ten len] > adc 0 > [mui ten len] > den on
Chương trình hoàn chỉnh
#include "dong.h"
#include "lenh.h"
#include "systick.h"
#include "uart.h"
int main(void)
{
BoDocDong doc;
systick_khoi_tao(84000000u);
uart_khoi_tao(84000000u, 115200u);
uart_bat_ngat_nhan();
doc_dong_khoi_tao(&doc);
uart_gui_chuoi("\r\nVo lenh, go 'help'\r\n> ");
for (;;) {
char *dong = doc_dong_cap_nhat(&doc);
if (dong != NULL) {
lich_su_them(dong);
lenh_chay(dong);
uart_gui_chuoi("> ");
}
/* Vong lap chinh van lam viec khac */
cap_nhat_den(tick_ms());
cap_nhat_cam_bien(tick_ms());
}
}text data bss dec hex filename 5412 24 712 6148 1804 shell.elf
20000120 00000040 b s_lich_su 20000160 00000040 b doc 200001a0 00000100 b rx_dem 64*4 + 64 + 256 = 576 byte cho ba bo dem
Tự làm thử
- Cài tầng đọc dòng và xác nhận nó xử lý đúng cả CR, LF và CRLF.
- Cài
tach_tuvà chạy nó trên máy tính với các chuỗi vào khó: nhiều khoảng trắng, chuỗi rỗng, và quá nhiều từ. - Cài
chuoi_sang_uintcó kiểm tràn, thử với4294967296và xác nhận nó báo lỗi. - Cài bảng lệnh với năm lệnh, rồi thêm lệnh thứ sáu và đếm số dòng phải sửa.
- Thêm lịch sử lệnh với phím mũi tên lên, xử lý chuỗi thoát bằng máy trạng thái ba trạng thái.
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
- Kiến trúc ba tầng: ISR ghi bộ đệm vòng, vòng lặp chính gom dòng, rồi tách và tra bảng lệnh.
- Không tầng nào cấp phát động. Mọi bộ đệm có kích thước cố định biết trước lúc biên dịch.
- Hàm tách từ sửa chuỗi tại chỗ nên chuỗi phải ghi được, và nó thay được
strtokmà không có trạng thái tĩnh. - Bảng lệnh khai báo
constnằm trong Flash, và thêm lệnh mới chỉ là viết một hàm cộng thêm một dòng. - Phím mũi tên gửi ba byte riêng lẻ, nên phải xử lý bằng máy trạng thái chứ không đọc ba byte liền.