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

Kiến trúc MCU

Sau bài này bạn sẽ làm được

  • Kể tên các khối chính của một vi điều khiển và vai trò từng khối
  • So sánh tài nguyên của MCU với máy tính trên tám tiêu chí
  • Nêu sáu quy tắc bắt buộc của lập trình nhúng và lý do từng quy tắc
  • Ước lượng được RAM và Flash mà một chương trình cần

Mọi thứ đã học về bit, con trỏ, volatile và bố cục bộ nhớ tới đây mới được dùng thật. Lập trình nhúng không có khái niệm nào mới, nó chỉ siết chặt mọi ràng buộc tới mức bạn buộc phải hiểu đúng.

#Bên trong một vi điều khiển

Vi điều khiển
Một con chip gói trọn bộ xử lý, bộ nhớ chương trình, bộ nhớ dữ liệu và các ngoại vi vào một khối. Khác với bộ vi xử lý của máy tính, nó không cần chip nhớ hay chip cầu bên ngoài để chạy.
Lõi, Flash, SRAM và mọi ngoại vi cùng nối vào một bus và cùng nằm trong một không gian địa chỉ.
KhốiVai tròDung lượng điển hình
Lõi CPUChạy lệnh, thường là ARM Cortex-M từ M0 tới M716 tới 480 MHz
FlashChứa mã chương trình và hằng số, giữ được khi mất điện16 KB tới 2 MB
SRAMChứa biến, ngăn xếp, vùng cấp phát động2 KB tới 512 KB
Ngoại viGPIO, UART, SPI, I2C, ADC, timer, DMAVài chục khối
NVICBộ điều khiển ngắt lồng nhau, thuộc lõi Cortex-MTới 240 nguồn ngắt

#Tám khác biệt so với lập trình máy tính

Máy tínhVi điều khiển
RAMVài gigabyteVài kilobyte tới vài trăm KB
Bộ nhớ chương trìnhVài trăm gigabyteVài chục KB tới vài MB
Hệ điều hànhCó, lo hết mọi thứThường không, chạy trần
mallocDùng thoải máiThường bị cấm hẳn, xem mục sau
printfMiễn phíTốn 5 tới 10 KB Flash, phải cân nhắc
Số thựcCó bộ tính số thực phần cứngThường không có, chậm hàng chục lần
Gỡ lỗiGDB, in ra màn hìnhSWD, UART, hoặc một cái đèn
Khi lỗiChương trình sập, khởi động lạiThiết bị chết ngoài hiện trường

Số thực đắt tới mức nào

do-so-thuc.c
/* Trên Cortex-M0, hoàn toàn không có bộ tính số thực.
   Mọi phép float được mô phỏng bằng thư viện phần mềm. */

float a = 1.5f, b = 2.5f;
float c = a * b;                  /* gọi __aeabi_fmul, hàng trăm chu kỳ */

/* Trên Cortex-M4F có bộ tính số thực 32 bit,
   nhưng chỉ cho float, còn double vẫn bằng phần mềm. */

double d = 1.5 * 2.5;             /* vẫn gọi thư viện, dù có FPU */

/* Cách đúng: dùng số nguyên với thang cố định, xem Bài 50.3 */
int32_t a_x100 = 150;             /* 1.50 nhân 100 */
int32_t b_x100 = 250;             /* 2.50 nhân 100 */
int32_t c_x100 = a_x100 * b_x100 / 100;   /* 375, tức 3.75 */
terminal
./do-phep-tinh --cortex-m0
phep tinh          so chu ky
int32 nhan               1
int32 chia              12
float cong             120
float nhan             145
float chia             480
double nhan            890
sqrt(float)           1240
# Cùng phép tính trên Cortex-M4F có bộ tính số thực
./do-phep-tinh --cortex-m4f
float cong               1
float nhan               1
float chia              14
double nhan            890   <- van bang phan mem

#Sáu quy tắc bắt buộc

#Đo tài nguyên chương trình dùng

Xem chương trình chiếm bao nhiêu
$ arm-none-eabi-size firmware.elf
   text    data     bss     dec     hex filename
  18240     108    4372   22720    58c0 firmware.elf

/* text : mã và hằng số, nằm trong FLASH
   data : biến toàn cục có giá trị khởi tạo khác 0
          chiếm chỗ ở CẢ Flash lẫn SRAM, xem Bài 14.3
   bss  : biến toàn cục bằng 0 hoặc chưa khởi tạo
          chỉ chiếm SRAM

   Flash dùng = text + data = 18240 + 108 = 18348 byte
   SRAM  dùng = data + bss  =   108 + 4372 = 4480 byte
                cộng thêm NGĂN XẾP, mà size không tính */

Đo mức dùng ngăn xếp cao nhất

do-ngan-xep.c
#include <stdint.h>

extern uint32_t _sstack, _estack;      /* do tệp linker định nghĩa */

#define MAU 0xC0DEC0DEu

/* Gọi ngay đầu main, trước khi ngăn xếp kịp sâu.
   Đổ đầy vùng ngăn xếp bằng một mẫu nhận ra được. */
void ngan_xep_do_day(void)
{
    uint32_t *p = &_sstack;
    uint32_t  hien_tai;

    /* Dừng cách vị trí ngăn xếp hiện tại một khoảng an toàn */
    while (p < (&hien_tai - 16))
        *p++ = MAU;
}

/* Gọi bất cứ lúc nào sau đó. Đếm số từ còn nguyên mẫu,
   phần còn lại là mức dùng cao nhất từ trước tới giờ. */
uint32_t ngan_xep_da_dung(void)
{
    uint32_t *p = &_sstack;

    while (p < &_estack && *p == MAU) ++p;

    return (uint32_t)((&_estack - p) * sizeof(uint32_t));
}
terminal
> stack
ngan xep cap phat : 4096 byte
da dung cao nhat  : 1284 byte  (31 phan tram)
con trong         : 2812 byte
# Sau khi gọi một hàm dùng printf
> printf-test; stack
da dung cao nhat  : 2216 byte  (54 phan tram)

printf mot minh an them 932 byte ngan xep

#Chọn vi điều khiển

DòngLõiRAM và Flash điển hìnhHợp với
STM32F0, C0Cortex-M04 tới 32 KB RAM, 16 tới 256 KB FlashĐiều khiển đơn giản, giá rẻ nhất
STM32F4Cortex-M4F128 tới 384 KB RAM, 512 KB tới 2 MB FlashĐa số dự án, có bộ tính số thực
STM32H7Cortex-M71 MB RAM, 2 MB FlashXử lý ảnh, âm thanh, tính toán nặng
ESP32Xtensa hoặc RISC-V520 KB RAM, 4 MB Flash ngoàiCần Wi-Fi hoặc Bluetooth sẵn
AVR, dòng Arduino UnoAVR 8 bit2 KB RAM, 32 KB FlashHọc, làm mẫu nhanh
RP2040Hai lõi Cortex-M0264 KB RAM, Flash ngoàiHai lõi, khối vào ra lập trình được

Tự làm thử

  1. Tra bảng dữ liệu của một vi điều khiển bạn có, ghi lại dung lượng Flash, SRAM, tần số lõi và số ngoại vi mỗi loại.
  2. Biên dịch một chương trình trống rồi chạy arm-none-eabi-size, sau đó thêm một lời gọi printf và đo lại.
  3. Viết hàm nhân hai số dùng float rồi dùng số nguyên thang cố định, đếm số lệnh máy sinh ra bằng arm-none-eabi-objdump -d.
  4. Cài hàm đổ đầy mẫu và đo mức dùng ngăn xếp cao nhất, rồi thử với một hàm đệ quy sâu dần.
  5. Biên dịch cùng một hàm với uint8_t và với uint_fast8_t cho biến cục bộ, so kích thước mã sinh ra.

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

  • Vi điều khiển gói lõi, Flash, SRAM và ngoại vi vào một chip, tất cả nằm trong cùng một không gian địa chỉ.
  • SRAM là tài nguyên eo hẹp nhất, và ngăn xếp chỉ vài kilobyte, tức nhỏ hơn máy tính khoảng một nghìn lần.
  • Không cấp phát động trong vòng lặp chính và không đệ quy, vì cả hai đều làm hỏng thứ không đoán trước được.
  • Số thực chậm hàng chục tới hàng trăm lần khi không có FPU, và một hằng số thiếu hậu tố f kéo cả phép tính sang double.
  • Lệnh size không tính ngăn xếp, nên phải tự đo bằng cách đổ đầy mẫu và đếm phần còn nguyên.