📚 Điều kiện tiên quyết

Để hoàn thành tốt dự án thực hành này, bạn cần có nền tảng kiến thức từ các bài trước:

Học lý thuyết điện tử chỉ thực sự có ý nghĩa khi chúng ta áp dụng để thiết kế ra những sản phẩm thực tế chạy được ngoài đời thực. Trong phần thực hành này, chúng ta sẽ cùng nhau tự tay thiết kế và triển khai một dự án điện tử nhúng hoàn chỉnh: Mạch chữ chạy LED ma trận 8x8 (Scrolling Text) hiển thị nội dung động, sử dụng linh kiện phần cứng công nghiệp thật, tính toán kỹ thuật đúng đắn và viết chương trình bằng ngôn ngữ C cấp thấp để nạp vào chip.

1. Đề bài & Sơ đồ khối hệ thống

Yêu cầu dự án: Thiết kế hệ thống nhúng điều khiển màn hình LED ma trận 8x8 chạy dòng chữ JS-TOOLS cuộn từ phải qua trái. Thiết bị phải chạy bằng nguồn USB 5V thông dụng, hoạt động ổn định không bị nhấp nháy, dây nối tối giản và cho phép lập trình lại nội dung hiển thị dễ dàng.

Hệ thống được tổ chức thành 3 khối cơ bản:

  • Khối Điều khiển (MCU): Sử dụng chip MCU ATmega328P làm trung tâm để đọc dữ liệu font chữ và phát lệnh điều khiển thông qua giao thức SPI (Serial Peripheral Interface).
  • Khối Đệm & Quét LED (Driver): Sử dụng IC chuyên dụng MAX7219. IC này đóng vai trò bộ đệm RAM tĩnh 8-byte và tự động thực hiện quét cột (multiplexing) tốc độ cao để cấp dòng cho LED.
  • Khối Hiển thị: Ma trận LED 8x8 Common Cathode (Cực âm chung) hiển thị dữ liệu pixel bằng các LED đỏ.

2. Linh kiện phần cứng thực tế & Datasheet

Dưới đây là bảng linh kiện cần chuẩn bị để ráp mạch thật. Để làm việc chuyên nghiệp, ta luôn cần đối chiếu datasheet chính thức từ nhà sản xuất:

  • Vi điều khiển ATmega328P (Dưới dạng bo mạch Arduino Uno R3 hoặc Nano cho tiện cắm testboard): Chứa bộ truyền nhận SPI phần cứng ổn định. Datasheet chính thức từ Microchip.
  • IC điều khiển quét LED MAX7219: Driver quét hiển thị LED 8 chữ số hoặc ma trận 8x8, truyền dữ liệu nối tiếp 3 chân rất tiết kiệm pin ngoại vi của MCU. Datasheet chính thức từ Analog Devices.
  • LED Ma Trận 8x8 Common Cathode (mẫu 1088AS): Gồm 64 đèn LED đỏ ghép nối ma trận. Chân Cathode chung cho các hàng/cột (tùy sơ đồ). Datasheet chính thức của 1088AS.
  • Điện trở cấu hình dòng điện $R_{iset}$: Dùng để điều chỉnh cường độ sáng của tất cả LED.
  • Tụ điện lọc nguồn: Gồm 1 tụ hóa $10\mu\text{F}$ và 1 tụ gốm $0.1\mu\text{F}$ (100nF) mắc song song sát chân nguồn MAX7219 để lọc nhiễu tần số cao khi quét LED.

3. Tính toán thiết kế mạch & Rationale

Một lỗi cực kỳ phổ biến của người tự học là sao chép sơ đồ trên mạng một cách mù quáng mà không hiểu vì sao linh kiện lại có trị số như vậy. Dưới đây là phân tích tính toán kỹ thuật để thiết kế mạch:

3.1 Tính toán điện trở đặt dòng $R_{iset}$ cho LED

IC MAX7219 không dùng điện trở hạn dòng cho từng đèn LED như mạch thủ công. Thay vào đó, nó sử dụng một điện trở duy nhất ký hiệu là $R_{iset}$ nối từ chân 18 (ISET) lên nguồn $V_{cc}$ để thiết lập dòng điện đỉnh của phân đoạn (segment peak current).

Công thức và quy trình chọn $R_{iset}$ tuân theo các bước:

  1. Tra cứu datasheet của LED Ma trận 1088AS: Điện áp thuận điển hình là $V_F = 2.0\text{ V}$ đối với LED màu đỏ ở dòng điện $20\text{ mA}$.
  2. Quyết định dòng điện segment mong muốn: Ta chọn dòng điện liên tục trung bình cho mỗi LED khoảng $20\text{ mA}$ để đảm bảo độ sáng rõ mà không làm quá nhiệt hay giảm tuổi thọ của LED.
  3. Tra cứu bảng Table 11 (page 11) của Datasheet MAX7219: Tại dòng giao giữa $V_{led} = 2.0\text{ V}$ và dòng điện đỉnh mong muốn $I_{seg} = 20\text{ mA}$, trị số điện trở thiết lập dòng điện được khuyến nghị là: \[R_{iset} = 28.0\text{ k}\Omega\]
⚠️ Cảnh báo hỏng linh kiện

Tuyệt đối không chọn điện trở $R_{iset}$ nhỏ hơn $9.53\text{ k}\Omega$. Trị số quá nhỏ sẽ làm dòng điện tăng vọt trên $45\text{ mA}$ mỗi đoạn, gây quá tải nhiệt làm cháy các bóng LED bên trong ma trận hoặc phá hỏng IC MAX7219.

3.2 Tính toán điện năng & Nguồn cấp

Khi quét LED, tối đa chỉ có 1 cột (8 bóng LED) được sáng cùng một thời điểm cực kỳ ngắn. Do đó, dòng tiêu thụ cực đại của ma trận LED sẽ là: \[I_{matrix\_peak} = 8 \times I_{seg} = 8 \times 20\text{ mA} = 160\text{ mA}\] Cộng thêm dòng hoạt động nội tại của bản thân IC MAX7219 và vi điều khiển ATmega328P ($\approx 10\text{ mA}$ mỗi chip), tổng dòng điện cực đại yêu cầu cho mạch là: \[I_{total\_max} \approx 160\text{ mA} + 20\text{ mA} = 180\text{ mA}\] Dòng điện này nằm hoàn toàn trong khả năng cung cấp của cổng USB máy tính ($500\text{ mA}$) hoặc bộ nguồn sạc điện thoại 5V thông thường. Ta không cần thiết kế thêm mạch nguồn ngoài phức tạp.

3.3 Tính toán tần số quét khử nhấp nháy

MAX7219 tích hợp mạch dao động nội tự động multiplexing quét 8 cột tuần tự. Tần số quét hiển thị mặc định của IC là: \[f_{scan} \approx 800\text{ Hz}\] Vì có 8 cột được quét luân phiên, mỗi cột sẽ sáng trong khoảng thời gian bằng: \[t_{on} = \frac{1}{f_{scan}} = 1.25\text{ ms}\] Tần số làm tươi $800\text{ Hz}$ này vượt xa ngưỡng cảm nhận nhấp nháy của mắt người (tầm $60\text{ Hz}$ đến $100\text{ Hz}$), đảm bảo hiển thị mịn màng, sắc nét. Đồng thời, CPU của vi điều khiển được giải phóng hoàn toàn khỏi tác vụ quét LED liên tục, chỉ cần gửi dữ liệu qua SPI khi có thay đổi chữ.

4. Sơ đồ nguyên lý đấu nối thực tế

Dưới đây là sơ đồ nguyên lý chuẩn công nghiệp của mạch thực hành. Hãy đấu nối chính xác các chân tín hiệu SPI phần cứng của ATmega328P tới cổng vào nối tiếp của MAX7219:

ATmega328P (Arduino Uno) Pin 10 (SS/PB2) Pin 11 (MOSI/PB3) Pin 13 (SCK/PB5) 5V VCC GND LOAD/CS DIN CLK MAX7219 LED Driver LOAD (12) DIN (1) CLK (13) R_iset (28k) VCC (19) 10uF 100nF GND (4, 9) SEG A-G, DP (8) DIG 0-7 (8) LED MATRIX 8x8 (1088AS CC) Lưới 64 bóng LED

5. Bộ giả lập LED Ma trận & Xuất font chữ tự động

Để giúp bạn dễ dàng tuỳ biến nội dung hiển thị của riêng mình, hãy sử dụng bảng điều khiển tương tác bên dưới. Gõ dòng chữ bạn mong muốn (bằng tiếng Anh/không dấu), xem trước hiệu ứng chạy chữ trên bo mạch LED giả lập và sao chép trực tiếp code mảng điểm ảnh C được tự động sinh ra ở khung bên dưới:

GIẢ LẬP LED MA TRẬN 8x8 (RETRO VISUALIZER)

Mô phỏng tốc độ quét làm tươi 800Hz thực tế

BẢNG ĐIỀU KHIỂN & CODE GENERATOR

5.1 Giải mã cách biểu diễn font chữ thành byte nhị phân

Mỗi chữ cái được hiển thị trong không gian lưới kích thước $5 \times 7$ (rộng 5 cột, cao 7 hàng). Vì MAX7219 quét hiển thị theo từng cột (từ cột 1 đến 8), chúng ta sẽ chia chữ cái thành 5 cột dọc đứng. Mỗi cột dọc đứng được đại diện bởi một byte dữ liệu 8-bit (uint8_t):

  • Trọng số hàng: Hàng dưới cùng (Row 0) tương ứng với bit thấp nhất (bit LSB: $2^0 = 1$), tăng dần lên hàng trên cùng (Row 7) ứng với bit cao nhất (bit MSB: $2^7 = 128$).
  • Mỗi ô LED sáng biểu thị giá trị bit là 1, ô LED tắt biểu thị giá trị bit là 0.

Hãy cùng phân tích ví dụ cụ thể của Chữ J với mảng dữ liệu 5 cột: {0x02, 0x01, 0x21, 0x1E, 0x00}:

Hàng (Bit) Cột 1 (0x02) Cột 2 (0x01) Cột 3 (0x21) Cột 4 (0x1E) Cột 5 (0x00)
Hàng 7 (bit 7 - 128) 0 (tắt) 0 (tắt) 0 (tắt) 0 (tắt) 0 (tắt)
Hàng 6 (bit 6 - 64) 0 (tắt) 0 (tắt) 0 (tắt) 0 (tắt) 0 (tắt)
Hàng 5 (bit 5 - 32) 0 (tắt) 0 (tắt) 1 (SÁNG) 0 (tắt) 0 (tắt)
Hàng 4 (bit 4 - 16) 0 (tắt) 0 (tắt) 0 (tắt) 1 (SÁNG) 0 (tắt)
Hàng 3 (bit 3 - 8) 0 (tắt) 0 (tắt) 0 (tắt) 1 (SÁNG) 0 (tắt)
Hàng 2 (bit 2 - 4) 0 (tắt) 0 (tắt) 0 (tắt) 1 (SÁNG) 0 (tắt)
Hàng 1 (bit 1 - 2) 1 (SÁNG) 0 (tắt) 0 (tắt) 1 (SÁNG) 0 (tắt)
Hàng 0 (bit 0 - 1) 0 (tắt) 1 (SÁNG) 1 (SÁNG) 0 (tắt) 0 (tắt)
Tính giá trị (Thập phân) 2 1 32 + 1 = 33 16 + 8 + 4 + 2 = 30 0
Mã Hex tương ứng 0x02 0x01 0x21 0x1E 0x00

Nhìn vào các điểm 1 (SÁNG) trong bảng trên, bạn có thể dễ dàng nhận ra hình dáng của chữ J: Bắt đầu với một điểm cong ở hàng 1 cột 1, hạ xuống hàng 0 cột 2-3, vòng ngược lên hàng 5 cột 3, và thân cột đứng thẳng chạy dọc từ hàng 1 đến hàng 4 ở cột 4.

6. Lập trình AVR C điều khiển giao tiếp SPI thô

Thay vì dùng các thư viện đóng gói sẵn vốn che giấu bản chất kỹ thuật, chương trình dưới đây được viết bằng ngôn ngữ C thuần (AVR C) tương tác trực tiếp với các thanh ghi điều khiển SPI của chip ATmega328P. Điều này giúp bạn hiểu rõ cơ chế bắt tay truyền bit ở tầng vật lý phần cứng.

main.c — Lập trình điều khiển quét LED ma trận
#define F_CPU 16000000UL
#include <avr/io.h>
#include <util/delay.h>

// Định nghĩa các chân điều khiển SPI phần cứng của ATmega328P
#define SPI_DDR  DDRB
#define SPI_PORT PORTB
#define SPI_MOSI PB3
#define SPI_SCK  PB5
#define SPI_SS   PB2

// Các địa chỉ thanh ghi cấu hình của IC MAX7219
#define REG_DECODE_MODE  0x09
#define REG_INTENSITY    0x0A
#define REG_SCAN_LIMIT   0x0B
#define REG_SHUTDOWN     0x0C
#define REG_DISPLAY_TEST 0x0F

// Khởi tạo module SPI phần cứng trên chip MCU
void spi_init(void) {
    // Cấu hình chân MOSI, SCK và SS làm đầu ra (Output)
    SPI_DDR |= (1 << SPI_MOSI) | (1 << SPI_SCK) | (1 << SPI_SS);

    // Bật SPI phần cứng, cấu hình làm Master, tốc độ truyền = F_osc / 16 (1 MHz)
    SPCR = (1 << SPE) | (1 << MSTR) | (1 << SPR0);

    // Đặt chân SS lên mức cao (không chọn chip MAX7219)
    SPI_PORT |= (1 << SPI_SS);
}

// Truyền một gói tin gồm 16-bit (8-bit địa chỉ + 8-bit dữ liệu) tới MAX7219
void max7219_write(uint8_t reg_addr, uint8_t data_val) {
    // Kéo chân SS xuống mức thấp để bắt đầu truyền dữ liệu
    SPI_PORT &= ~(1 << SPI_SS);

    // Gửi byte địa chỉ thanh ghi
    SPDR = reg_addr;
    while (!(SPSR & (1 << SPIF))); // Chờ truyền xong

    // Gửi byte dữ liệu điều khiển/pixel
    SPDR = data_val;
    while (!(SPSR & (1 << SPIF))); // Chờ truyền xong

    // Kéo chân SS lên mức cao để khóa và chốt (latch) dữ liệu vào MAX7219
    SPI_PORT |= (1 << SPI_SS);
}

// Cấu hình ban đầu cho MAX7219 hoạt động
void max7219_init(void) {
    max7219_write(REG_DISPLAY_TEST, 0x00); // Tắt chế độ test hiển thị
    max7219_write(REG_DECODE_MODE, 0x00);  // Không giải mã BCD (dùng ma trận điểm ảnh)
    max7219_write(REG_SCAN_LIMIT, 0x07);   // Quét toàn bộ 8 cột (từ cột 0 đến 7)
    max7219_write(REG_INTENSITY, 0x04);    // Đặt độ sáng ở mức vừa phải (4/15)
    max7219_write(REG_SHUTDOWN, 0x01);     // Bật chế độ hoạt động (Normal operation)

    // Xoá sạch hiển thị ban đầu
    for (uint8_t i = 1; i <= 8; i++) {
        max7219_write(i, 0x00);
    }
}

// Dữ liệu bitmap font chữ chạy động (Ví dụ chạy dòng chữ JS-TOOLS)
// Mỗi ký tự rộng 5 cột + 1 cột trống ngăn cách
const uint8_t scroll_data[] = {
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Cột đệm đầu
    0x02, 0x01, 0x21, 0x1E, 0x00, 0x00,             // Chữ J
    0x12, 0x25, 0x25, 0x25, 0x12, 0x00,             // Chữ S
    0x08, 0x08, 0x08, 0x08, 0x08, 0x00,             // Dấu gạch ngang -
    0x20, 0x20, 0x3F, 0x20, 0x20, 0x00,             // Chữ T
    0x1E, 0x21, 0x21, 0x21, 0x1E, 0x00,             // Chữ O
    0x1E, 0x21, 0x21, 0x21, 0x1E, 0x00,             // Chữ O
    0x3F, 0x01, 0x01, 0x01, 0x01, 0x00,             // Chữ L
    0x12, 0x25, 0x25, 0x25, 0x12, 0x00,             // Chữ S
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00  // Cột đệm đuôi
};

#define DATA_SIZE (sizeof(scroll_data))

int main(void) {
    spi_init();
    max7219_init();

    uint16_t offset = 0; // Vị trí dịch chữ chạy

    while (1) {
        // Cập nhật 8 cột hiển thị từ mảng dữ liệu dựa theo vị trí offset
        for (uint8_t col = 0; col < 8; col++) {
            uint16_t idx = offset + col;
            uint8_t byte_to_send = 0;
            if (idx < DATA_SIZE) {
                byte_to_send = scroll_data[idx];
            }
            // Gửi dữ liệu hiển thị (MAX7219 quy định cột từ 1 đến 8)
            max7219_write(col + 1, byte_to_send);
        }

        // Tốc độ chạy chữ: dịch chuyển 1 cột sau mỗi 100ms
        _delay_ms(100);
        offset++;
        if (offset >= DATA_SIZE - 8) {
            offset = 0; // Quay lại từ đầu
        }
    }
    return 0;
}

7. Phương pháp mô phỏng & Debug kiểm tra trước khi nạp

Trước khi nạp trực tiếp file HEX xuống vi điều khiển thật (vốn có số lần ghi xóa giới hạn khoảng 10,000 lần và rất khó quan sát lỗi logic bên trong), ta nên thực hiện quy trình kiểm tra và gỡ lỗi (debug) theo 3 bước chuẩn công nghiệp nhúng dưới đây:

7.1 Mô phỏng kiểm tra hành vi mạch bằng phần mềm ảo

Mô phỏng là cách an toàn và nhanh nhất để xác minh logic điều khiển SPI và dòng quét dữ liệu:

  • Sử dụng Wokwi (Trực tuyến, Khuyên dùng): Bạn có thể truy cập trang web Wokwi.com, tạo dự án trống AVR C, kéo thả chip ATmega328P, IC MAX7219 và Module LED Ma trận 8x8. Wokwi giả lập chính xác ở cấp độ chu kỳ máy và hỗ trợ trực quan hóa tín hiệu dây nối. Copy mã nguồn C ở trên vào để kiểm tra hành vi chạy chữ xem có khớp 100% không.
  • Sử dụng Proteus ISIS (Chuyên nghiệp): Thiết kế sơ đồ nguyên lý trong Proteus, kết nối ATmega328P với MAX7219 qua cổng SPI, nhấp đúp vào chip MCU và trỏ đường dẫn nạp file main.elf hoặc main.hex vừa biên dịch (xem Mục 8 — Biên dịch & Nạp chip bên dưới) để chạy mô phỏng đồ họa mạch điện.

7.2 Thiết lập Gỡ lỗi logic bằng máy phân tích logic (Logic Analyzer)

Nếu mô phỏng chạy tốt nhưng mạch thật không sáng, vấn đề 99% nằm ở chất lượng dây kết nối hoặc nhiễu clock. Ta sử dụng một bộ Logic Analyzer mini USB giá rẻ kết nối song song vào 3 chân tín hiệu SPI:

  1. Kẹp các đầu đo của Logic Analyzer vào chân SCK, MOSI, và SS (LOAD).
  2. Mở phần mềm đo dạng sóng (như PulseView / Saleae Logic).
  3. Bấm nút ghi tín hiệu và so sánh biểu đồ xung nhịp nhị phân thực tế với giản đồ xung quy định trong datasheet MAX7219 (đảm bảo dữ liệu MOSI thay đổi ở sườn lên của clock SCK và chân SS kéo cao đúng lúc truyền xong byte thứ hai).

7.3 Kỹ thuật Debug phần mềm: Sử dụng giả lập GDB & SimulAVR

Nếu muốn theo dõi giá trị của biến số, con trỏ offset hoặc trạng thái các thanh ghi SPI bên trong CPU MCU, ta có thể khởi động GDB kết hợp với công cụ giả lập phần cứng SimulAVR:

  1. Khởi chạy SimulAVR làm máy chủ gỡ lỗi cổng 1212:
    simulavr -d atmega328p -g -P simulavr-disp
  2. Ở một cửa sổ terminal khác, khởi động AVR-GDB và kết nối vào giả lập:
    avr-gdb main.elf
    (gdb) target remote localhost:1212
    (gdb) load
  3. Bây giờ bạn có thể đặt điểm dừng (breakpoint), bước qua từng dòng lệnh (step), và xem giá trị thanh ghi SPI bằng lệnh GDB:
    (gdb) break main.c:84
    (gdb) print offset
    (gdb) info registers SPCR SPDR SPSR

8. Hướng dẫn biên dịch & Quy trình nạp chip

Khi làm việc thực tế với chip AVR cấp thấp không qua trình soạn thảo Arduino IDE, ta sử dụng dòng lệnh đầu cuối terminal để biên dịch trực tiếp ra mã hex máy nhằm tối ưu kích thước file:

  1. Biên dịch mã nguồn C: Chuyển mã nguồn C thành file mã đối tượng ELF dành cho dòng chip ATmega328P:
    avr-gcc -g -Os -mmcu=atmega328p -DF_CPU=16000000UL -c main.c -o main.o
    avr-gcc -g -mmcu=atmega328p main.o -o main.elf
  2. Trích xuất mã HEX: Lọc lấy phân đoạn bộ nhớ chương trình Flash hoạt động thực tế đưa ra định dạng tệp Intel HEX tiêu chuẩn:
    avr-objcopy -j .text -j .data -O ihex main.elf main.hex
  3. Nạp chương trình xuống chip: Sử dụng công cụ nạp mã nguồn mở avrdude phối hợp với mạch nạp USBasp chuyên dụng:
    avrdude -c usbasp -p m328p -U flash:w:main.hex:i
💡 Mẹo tự nạp bằng mạch Arduino làm Programmer

Nếu không có mạch nạp chuyên dụng USBasp, bạn có thể nạp chương trình ArduinoISP có sẵn trong ví dụ của Arduino IDE vào một bo mạch Arduino thứ hai. Khi đó, hãy kết nối mạch nạp này với chip mục tiêu và dùng lệnh:

avrdude -c avrisp -p m328p -P /dev/tty.usbmodem101 -b 19200 -U flash:w:main.hex:i

9. Câu hỏi trắc nghiệm kiểm tra kiến thức

Hãy làm các câu hỏi trắc nghiệm dưới đây để tự kiểm tra mức độ tiếp thu các phép tính và nguyên lý thiết kế của bạn:

Câu hỏi trắc nghiệm củng cố

Câu 1: Tại sao ta chọn điện trở đặt dòng $R_{iset} = 28\text{ k}\Omega$ cho mạch quét LED ma trận?

Câu 2: Trong hàm truyền SPI thô, chân SS (LOAD) được kéo lên mức cao (HIGH) để làm nhiệm vụ gì?

Câu 3: Tại sao ta cần đặt song song cả tụ điện hóa $10\mu\text{F}$ và tụ gốm $0.1\mu\text{F}$ ở chân nguồn của IC driver quét LED?

Tài liệu tham khảo chuyên ngành

BÀI TRƯỚC Bài 16: Ngắt, Timer & Dự án tổng hợp cuối series