Ở bài trước, chúng ta đã “múa” thành công những đường GDB cơ bản. Nhưng một cao thủ thực thụ không chỉ biết dùng vũ khí, mà còn phải biết cách “độ” vũ khí của mình. Trong bài cuối cùng của series này, chúng ta sẽ thực hiện một nâng cấp tối thượng: biến cổng USB-OTG thành một con dao đa năng Thụy Sĩ, và giải mã “ma thuật” đằng sau nó để không còn cảm thấy bỡ ngỡ.

[MEME: Một người thợ rèn đang nâng cấp vũ khí trong game]


“Bẻ Ghi Đường Ray” - Thử Thách Chuyển Hướng Console qua USB

Vấn Đề Hiện Tại: Nỗi Đau Thể Xác Lẫn Tinh Thần

Mặc định, khi đại ca dùng ESP32-S3, JTAG debug thì đi qua cổng “USB-OTG”, nhưng các thông điệp printk lại đi ra cổng “COM” (là cổng USB-to-UART tích hợp trên bo devkit). Điều này bắt chúng ta phải cắm 2 sợi cáp vào máy tính, mở 2 chương trình terminal riêng biệt (một cho GDB, một cho minicom hoặc putty). Ôi trời, nó phiền phức và trông “lúa” không thể tả!

Phân Tích Gốc Rễ: “Bảng Chỉ Dẫn” /chosen

Thủ phạm của sự phiền phức này nằm ở đâu? Nó nằm trong file Device Tree gốc của bo mạch, tại một node đặc biệt tên là /chosen. Node này giống như một cái “bảng chỉ dẫn” chung cho cả hệ thống. Trong đó có một dòng: zephyr,console = &uart0;. Dòng này có nghĩa là: “Này cả nhà, ai muốn in cái gì ra console thì cứ gửi hết cho cái cổng uart0 nhé!”. Và uart0 chính là cái cổng COM phiền phức kia.

Kế Hoạch Hành Động: Sửa Bảng Chỉ Dẫn và Bật Công Tắc

Để giải quyết, chúng ta cần làm 2 việc:

  1. Hành Động 1: Tạo app.overlay để “bẻ lái” console Tạo một file app.overlay (hoặc boards/esp32s3_devkitc.overlay) trong thư mục dự án với nội dung:
    / {
        chosen {
            /* Ghi đè, trỏ console sang cổng USB ảo thay vì cổng UART vật lý */
            zephyr,console = &uart_virtual;
        };
    };
    
    /* Kích hoạt cổng USB ảo */
    &uart_virtual {
        status = "okay";
    };
    

    Hành động này nói với Zephyr: “Quên cái bảng chỉ dẫn cũ đi. Giờ ai muốn in ra console thì gửi hết qua cổng USB ảo (uart_virtual) cho tôi!”.

  2. Hành Động 2: Cập nhật prj.conf để “Cấp Điện” cho USB Để cổng USB ảo hoạt động, chúng ta cần bật các “công tắc” cần thiết trong file prj.conf:
    # Bật USB device stack
    CONFIG_USB_DEVICE_STACK=y
    # Bật driver cho cổng COM ảo qua USB (CDC ACM)
    CONFIG_USB_DEVICE_CDC_ACM=y
    CONFIG_UART_LINE_CTRL=y
    

Kết Quả “Tối Thượng”

Sau khi làm xong 2 bước trên và build lại firmware, một điều kỳ diệu sẽ xảy ra. Cả JTAG debug và printk log bây giờ đều đi qua CÙNG MỘT SỢI CÁP USB-OTG. Trên Linux, nó sẽ hiện ra thành 2 thiết bị: /dev/ttyACMx (cho log) và một cổng nữa cho JTAG. Giờ thì đại ca có thể vứt bớt một sợi cáp đi được rồi!


“Nhưng… tại sao nó lại hoạt động?” - Giải Mã Ma Thuật USB

Câu hỏi “tại sao một sợi dây lại làm được nhiều việc” là một câu hỏi cực hay. Câu trả lời nằm ở bản chất của USB, nó không phải là một “con đường làng”.

Sự Thật về “Siêu Xa Lộ Đa Làn”

Hãy quên đi ảo tưởng về “một đường dây” của các giao thức cũ như UART. USB là một siêu xa lộ có nhiều làn xe.

  • Gói tin (Packets): Dữ liệu được chia nhỏ thành các “xe container”, mỗi xe có địa chỉ đi và địa chỉ đến rõ ràng.
  • Giao diện (Interfaces): Một thiết bị USB có thể có nhiều “khu vực chức năng” ảo. Ví dụ, điện thoại của bạn có thể vừa là “ổ đĩa” (để chép file), vừa là “modem” (để chia sẻ mạng). Con ESP32-S3 của chúng ta cũng vậy, nó có một “khu vực” cho JTAG và một “khu vực” cho cổng COM ảo.
  • Điểm cuối (Endpoints): Mỗi “khu vực chức năng” lại có các “làn xe” (endpoint) riêng cho luồng dữ liệu đi vào (OUT) và đi ra (IN).

Bằng Chứng Không Thể Chối Cãi: lsusb -v

Hãy gõ lsusb -v -d 303a:1001 để xem bằng chứng. Đại ca sẽ thấy một output dài, nhưng đây là những phần quan trọng nhất (đã được rút gọn):

Bus 001 Device 008: ID 303a:1001 Espressif Systems USB JTAG/serial debug unit
...
  bNumInterfaces          3
  ...
  Interface Descriptor:
    bInterfaceNumber        0
    bInterfaceClass         2 Communications
    ...
    Endpoint Descriptor:
      bEndpointAddress     0x81  EP 1 IN
    ...
    Endpoint Descriptor:
      bEndpointAddress     0x01  EP 1 OUT
  ...
  Interface Descriptor:
    bInterfaceNumber        2
    bInterfaceClass       255 Vendor Specific Class
    ...
    Endpoint Descriptor:
      bEndpointAddress     0x82  EP 2 IN
    ...
    Endpoint Descriptor:
      bEndpointAddress     0x02  EP 2 OUT
  • Phân tích: Đại ca thấy không? Có ít nhất 2 Interface Descriptor khác nhau.
    • Interface 0 có class Communications, đây chính là cổng Serial (CDC-ACM) của chúng ta. Nó dùng 2 “làn xe” là EP 1 INEP 1 OUT.
    • Interface 2 có class Vendor Specific, đây chính là cổng JTAG độc quyền của Espressif. Nó dùng 2 “làn xe” khác là EP 2 INEP 2 OUT.
  • Kết luận: Máy tính và ESP32-S3 đang “nói chuyện” với nhau trên nhiều “làn” khác nhau qua cùng một “con đường” vật lý. Thật là vi diệu!

(Bonus) Mở Rộng Tư Duy: USB, SPI, I2C - Ai Là “Sếp”?

Sau khi thấy USB quá bá đạo, nhiều người sẽ hỏi: “Vậy nó có thay thế SPI và I2C được không?”. Câu trả lời ngắn gọn là KHÔNG.

Nó giống như so sánh một Giám đốc đối ngoại (USB) với các Trưởng phòng nội bộ (SPI, I2C).

  • USB: Chuyên giao tiếp với thế giới bên ngoài (máy tính, điện thoại). Rất mạnh, linh hoạt, nhưng phức tạp và tốn tài nguyên.
  • SPI/I2C: Chuyên giao tiếp “nội bộ” giữa các con chip trên cùng một bo mạch. Đơn giản, nhanh, hiệu quả cho các tác vụ cụ thể.

Chúng không cạnh tranh mà bổ sung cho nhau để tạo nên một hệ thống hoàn chỉnh.


Tổng Kết Series - “Giờ Đại Ca Đã Là ‘Pháp Sư’”

Vậy là chúng ta đã đi hết hành trình 5 bài. Từ một “tân binh” chỉ biết chèn printf trong sợ hãi, giờ đây đại ca đã có thể:

  • Tự tin “múa” các chiêu GDB cơ bản.
  • Hiểu và xử lý các lỗi kết nối OpenOCD như một chuyên gia.
  • Tự tay cấu hình lại cả hệ thống I/O của Zephyr để biến cổng USB thành vũ khí tối thượng.

Debug không còn là một công việc, nó đã trở thành một nghệ thuật. Và đại ca, với những vũ khí và kiến thức này, chính là một người nghệ sĩ. Hãy tiếp tục “độ” và “phá” theo cách của riêng mình!

[MEME: Một nhân vật giơ cúp chiến thắng hoặc một pháp sư đang thi triển phép thuật cao cấp]