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

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ầngChạy ở đâuBộ nhớTrách nhiệm
ISR nhậnNgắt USART1256 byte bộ đệm vòngLấy byte khỏi phần cứng, không mất
Đọc dòngVòng lặp chính64 byte bộ đệm dòngGom byte cho tới khi gặp ký tự xuống dòng
Tách lệnhVòng lặp chính8 con trỏChia dòng thành các từ, không cấp phát gì
Bảng lệnhVòng lặp chínhBảng hằng trong FlashTìm hàm xử lý theo tên lệnh
Xử lýVòng lặp chínhTùy lệnhLàm việc thật và in kết quả

#Đọc một dòng không chặn

dong.h
#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
dong.c
#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ố

tach.c
#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;
}
terminal
./thu-tach 'set nguong 1200'
so tu: 3
  [0] = 'set'
  [1] = 'nguong'
  [2] = '1200'
./thu-tach ' '
so tu: 0
./thu-tach 'a b c d e f g h i j'
so tu: 8   <- cat o TU_TOI_DA
  [7] = 'h'

Đổi tham số sang số

doi-so.c
#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;
}
terminal
./thu-doi-so
'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

lenh.h
#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
lenh.c
#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");
}
terminal
picocom -b 115200 /dev/ttyUSB0
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ử

  1. 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.

  2. 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.

  3. 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.

  4. 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.

lich-su.c
#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.c
/* 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;
    }
}
terminal
picocom -b 115200 /dev/ttyUSB0
> 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

main.c
#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());
    }
}
terminal
arm-none-eabi-size shell.elf
   text    data     bss     dec     hex filename
   5412      24     712    6148    1804 shell.elf
# Trong đó bộ đệm chiếm phần lớn RAM
arm-none-eabi-nm --size-sort -S shell.elf | tail -5
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ử

  1. Cài tầng đọc dòng và xác nhận nó xử lý đúng cả CR, LF và CRLF.
  2. Cài tach_tu và 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ừ.
  3. Cài chuoi_sang_uint có kiểm tràn, thử với 4294967296 và xác nhận nó báo lỗi.
  4. 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.
  5. 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 strtok mà không có trạng thái tĩnh.
  • Bảng lệnh khai báo const nằ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.