USB Transfer Types: Interrupt, Bulk, Control và Isochronous từ góc firmware

Giải thích về 4 loại USB transfer.

13 phút đọc
STM32 / Firmware cover

1. USB là hệ thống host-centric - hiểu điều này trước khi đọc tiếp

Nếu có một điều quan trọng nhất cần hiểu về USB trước khi đi vào từng loại transfer, đó là:

Host là bên duy nhất chủ động khởi xướng mọi transaction.

Device không bao giờ tự nhiên “gửi” data lên host. Device chỉ được phép gửi khi host hỏi - và phải trả lời ngay trong cùng transaction đó.

Điều này nghe có vẻ đơn giản nhưng dễ bị hiểu nhầm - đặc biệt với interrupt transfer, vì tên gọi gợi ý rằng device sẽ “interrupt” host khi có sự kiện. Thực tế hoàn toàn ngược lại.


2. Frame, Microframe và SOF - nhịp đập của bus USB

Trước khi nói về transfer type, cần hiểu về frame - đơn vị thời gian cơ bản của USB.

Full Speed (12 Mbps): mỗi frame = 1ms. Đầu mỗi frame, host gửi SOF (Start of Frame) packet - một packet đặc biệt mang frame number, broadcast đến tất cả device.

High Speed (480 Mbps): mỗi microframe = 125µs. Mỗi frame (1ms) chia thành 8 microframe.

Full Speed timeline:
|--Frame 0 (1ms)--|--Frame 1 (1ms)--|--Frame 2 (1ms)--|
SOF               SOF               SOF
 └─ transactions  └─ transactions   └─ transactions

SOF có hai vai trò quan trọng:

Vai trò 1: Clock synchronization - Device dùng SOF để sync internal clock với host. Quan trọng với isochronous transfer vì cần timing chính xác.

Vai trò 2: Keepalive signal - Khi host gửi SOF đều đặn, device biết kết nối vẫn active. Nếu device không thấy SOF trong 3ms (Full Speed), nó detect suspend state - có thể vào power-saving mode.

SOF và suspend/resume

Normal:    SOF SOF SOF SOF SOF SOF → active
Suspend:   SOF SOF SOF .... (3ms không có SOF) → device vào suspend
Resume:    .... RESUME signal → SOF SOF SOF → active lại

Với firmware, detect suspend để tắt peripheral không cần thiết (giảm power) và detect resume để khởi động lại:

/* Trong usbd_conf.c - CubeMX generate */
void HAL_PCD_SuspendCallback(PCD_HandleTypeDef *hpcd)
{
    /* Host ngừng gửi SOF - USB suspended */
    USBD_LL_Suspend(hpcd->pData);

    /* Optional: vào low power mode */
    /* HAL_PWREx_EnterSTOP1Mode(PWR_STOPENTRY_WFI); */
}

void HAL_PCD_ResumeCallback(PCD_HandleTypeDef *hpcd)
{
    /* Host gửi RESUME signal */
    USBD_LL_Resume(hpcd->pData);
}

Reset đột ngột - khi host gửi USB reset signal (D+ và D- kéo xuống cùng lúc ≥10ms), device phải reset toàn bộ USB state:

void HAL_PCD_ResetCallback(PCD_HandleTypeDef *hpcd)
{
    /* Host đã reset bus - phải re-enumerate */
    USBD_LL_SetSpeed(hpcd->pData, USBD_SPEED_FULL);
    USBD_LL_Reset(hpcd->pData);
    /* Device quay về Default state, address = 0 */
    /* Host sẽ enumerate lại từ đầu */
}

3. Control Transfer - nền tảng của mọi USB communication

Control transfer là loại transfer duy nhất bắt buộc với mọi USB device. Tất cả enumeration (GET_DESCRIPTOR, SET_ADDRESS, SET_CONFIGURATION) đều đi qua control transfer trên Endpoint 0.

Control transfer có 3 stage:

1. Setup Stage (8 bytes, luôn có):
   Host → Device: Setup token + Setup packet
   Setup packet mô tả request: loại, command, parameter, data length

2. Data Stage (optional):
   Nếu wLength > 0: trao đổi data theo hướng bmRequestType chỉ định
   IN: Device → Host (device gửi data lên host)
   OUT: Host → Device (host gửi data xuống device)

3. Status Stage (luôn có, hướng ngược với Data Stage):
   Zero-length packet để xác nhận hoàn tất

Timing guarantee: Control transfer có 10% bandwidth reserved trên Full Speed bus. Không có guaranteed latency - có thể bị delay nếu bus bận.

Firmware không thường xuyên handle control transfer trực tiếp - ST middleware lo phần lớn. Chỉ cần implement khi có class-specific hoặc vendor request (đã nói trong bài trước).


4. Interrupt Transfer - host poll, device trả lời

Đây là loại transfer hay bị hiểu nhầm nhất.

Hiểu nhầm phổ biến

“Interrupt transfer hoạt động như MCU interrupt - device gửi tín hiệu lên host khi có sự kiện.”

Sai hoàn toàn.

Trong MCU, interrupt là device-initiated - peripheral tự raise flag, CPU nhảy vào handler ngay lập tức.

Trong USB, interrupt transfer là host-initiated - host poll device theo chu kỳ cố định, device chỉ có thể gửi data khi được hỏi.

“Any one who has had experience of interrupt requests on microcontrollers will know that interrupts are device generated. However under USB, if a device requires the attention of the host, it must wait until the host polls it before it can report that it needs urgent attention.” - USB in a NutShell

Cơ chế thực sự

Mỗi bInterval ms:

Host:    IN token → [Endpoint address]

Device có data?    YES → DATA packet (data của report)

                   Host nhận OK → ACK

Device có data?    NO  → NAK

                   Host ghi nhận, không có gì để xử lý
                   Chờ đến chu kỳ poll tiếp theo

ACK vs NAK:

  • ACK: Device có data, đã gửi, host nhận thành công
  • NAK: Device không có data - không phải lỗi, chỉ là “không có gì mới”
  • STALL: Error condition - endpoint bị lỗi hoặc request không hợp lệ

DMA endpoint buffer và tại sao phải clear sau khi host poll

Firmware đặt data vào endpoint buffer (vùng RAM được USB controller quản lý). Khi host poll và buffer có data, USB hardware tự động gửi - CPU không cần làm gì thêm.

Vấn đề nếu không clear buffer sau khi host đã poll:

Firmware ghi report mới → buffer có data mới → host poll → ACK
Firmware KHÔNG clear buffer
Firmware chưa có data mới
Host poll lần tiếp → buffer VẪN có data cũ → ACK lại với data cũ!

Host nhận data cũ, nghĩ là data mới → stale data bug. Trên POS system, đầu đọc thẻ có thể gửi lại dữ liệu thẻ cũ mỗi khi host poll.

Cách fix: sau khi host poll thành công (TX complete callback), clear buffer hoặc chỉ ghi data mới vào buffer khi thực sự có sự kiện mới.

/* Trong usbd_hid.c, callback khi TX hoàn tất */
static uint8_t USBD_HID_DataIn(USBD_HandleTypeDef *pdev, uint8_t epnum)
{
    HID_HandleTypeDef *hhid = (HID_HandleTypeDef *)pdev->pClassData;
    hhid->state = HID_IDLE;  /* Đánh dấu buffer đã được consume */
    return USBD_OK;
}

/* Chỉ gửi report khi có data mới VÀ buffer idle */
uint8_t USB_HID_SendReport(uint8_t *report, uint8_t len)
{
    HID_HandleTypeDef *hhid = hUsbDeviceFS.pClassData;

    if (hhid->state == HID_IDLE) {
        hhid->state = HID_BUSY;
        /* Ghi vào buffer - host sẽ fetch trong chu kỳ poll tiếp theo */
        USBD_LL_Transmit(&hUsbDeviceFS, HID_EPIN_ADDR, report, len);
        return USBD_OK;
    }
    return USBD_BUSY;  /* Buffer chưa idle - chờ */
}

bInterval - không phải số bất kỳ

Full Speed và Low Speed interrupt endpoint: bInterval là số ms trực tiếp, từ 1 đến 255.

High Speed interrupt endpoint: bIntervalexponent, interval = 2^(bInterval-1) × 125µs. Vì vậy:

bInterval (HS)Interval thực tế
12^0 = 1 × 125µs = 125µs
42^3 = 8 × 125µs = 1ms
82^7 = 128 × 125µs = 16ms

Tại sao power-of-2?

Host controller tổ chức interrupt endpoint polling bằng binary tree scheduler. Mỗi level của tree tương ứng một power-of-2 interval. Device ở level 0 (bInterval=1ms) được poll mỗi frame. Device ở level 1 (bInterval=2ms) được poll mỗi 2 frame. Cấu trúc binary tree này giúp host controller schedule polling hiệu quả mà không cần mỗi endpoint một timer riêng.

Nếu đặt bInterval là giá trị không phải power-of-2 (ví dụ 3ms, 5ms), host controller sẽ round xuống power-of-2 gần nhất - bInterval=3 thực tế là 2ms, bInterval=5 thực tế là 4ms.

/* Trong usbd_hid.h - ST middleware mặc định */
#define HID_FS_BINTERVAL  0x0A  /* 10ms */

/* Nếu cần nhanh hơn cho card reader */
#define HID_FS_BINTERVAL  0x01  /* 1ms - poll mỗi frame */

/* Lưu ý: 0x03 (3ms) sẽ thực tế là 2ms */
/* Luôn dùng power-of-2: 1, 2, 4, 8, 16, 32, 64, 128 */

Bandwidth consideration: Mỗi interrupt endpoint với bInterval=1ms chiếm ~0.1% bandwidth Full Speed. Nhiều endpoint polling nhanh = ít bandwidth còn lại cho bulk.


5. Bulk Transfer - throughput cao, không guaranteed timing

Bulk transfer dùng khi cần truyền nhiều data và không quan tâm đến timing - firmware update, log dump, file transfer.

Cơ chế

Host gửi IN token, device trả data hoặc NAK. Khác interrupt: bulk không có reserved bandwidth - chỉ dùng bandwidth còn lại sau khi interrupt và isochronous đã được phục vụ. Khi bus bận, bulk bị delay không giới hạn.

Host:   IN token
Device: DATA0 (64 bytes) → Host: ACK
Host:   IN token
Device: DATA1 (64 bytes) → Host: ACK   ← toggle DATA0/DATA1
Host:   IN token
Device: NAK (buffer chưa sẵn sàng)
...
Host:   IN token (retry không giới hạn)
Device: DATA0 (64 bytes) → Host: ACK

DATA0/DATA1 toggle là cơ chế phát hiện duplicate packet - nếu device gửi DATA0, host expect DATA1 tiếp theo. Nếu nhận DATA0 liên tiếp, host biết packet bị duplicate và discard.

Framing - vấn đề không có trong spec

Bulk transfer gửi raw bytes - không có cấu trúc nào được định nghĩa ở USB level. Bạn phải tự định nghĩa protocol để host biết data bắt đầu và kết thúc ở đâu. Đây là điểm nhiều người không nghĩ đến khi thiết kế.

Vấn đề: bulk endpoint có max packet size 64 bytes (Full Speed). Một “message” có thể dài hơn 64 bytes → phải chia thành nhiều packet. Host nhận nhiều packet - làm sao biết đây là một message hay hai?

Strategy 1: Fixed length - mỗi message luôn đúng N bytes. Đơn giản nhưng kém linh hoạt.

/* Device: gửi luôn 64 bytes, pad nếu cần */
typedef struct {
    uint8_t  cmd;
    uint8_t  status;
    uint16_t data_len;
    uint8_t  data[60];
} __attribute__((packed)) BulkMsg_t;  /* Luôn 64 bytes */

Strategy 2: Length-prefixed - 2-4 byte đầu chứa tổng length của message.

/* Header: 4 bytes length, sau đó là payload */
typedef struct {
    uint32_t total_len;  /* Tổng số byte payload theo sau */
    uint8_t  payload[];  /* Variable length */
} __attribute__((packed)) BulkFrame_t;

/* Host: đọc 4 bytes header, rồi đọc thêm total_len bytes */

Strategy 3: Start/end marker - byte đặc biệt đánh dấu đầu và cuối.

#define FRAME_START  0x7E
#define FRAME_END    0x7F
#define FRAME_ESCAPE 0x7D  /* Escape byte nếu data chứa START/END */

/* Encoding: nếu data byte = 0x7E hoặc 0x7F → gửi 0x7D + (byte XOR 0x20) */

Cách này linh hoạt nhưng cần byte stuffing khi data chứa marker.

Strategy 4: CRC framing - header + payload + CRC. Host validate CRC để biết frame hoàn chỉnh.

typedef struct {
    uint8_t  magic[2];   /* 0xAA, 0x55 - start marker */
    uint16_t length;     /* Payload length */
    uint8_t  payload[]; /* Variable */
    /* uint16_t crc;    ← nằm sau payload */
} __attribute__((packed)) BulkFrame_t;

Tôi dùng trên POS: length-prefixed với CRC ở cuối - đơn giản, detect được corruption, không cần byte stuffing.

Short packet và ZLP

Quan trọng: nếu transfer size là bội số đúng của max packet size (64 bytes), host không biết transfer đã kết thúc hay chưa. Phải gửi thêm một Zero-Length Packet (ZLP) để báo hiệu end of transfer.

void BulkSend(const uint8_t *data, uint32_t len)
{
    /* Gửi data */
    CDC_Transmit_FS((uint8_t*)data, len);

    /* Nếu len là bội số của 64, gửi thêm ZLP */
    if (len % USB_FS_MAX_PACKET_SIZE == 0) {
        CDC_Transmit_FS(NULL, 0);  /* ZLP */
    }
}

ST middleware CDC thường handle ZLP tự động - nhưng với custom bulk class, phải tự xử lý.


6. Isochronous Transfer - real-time nhưng không retry

Isochronous dùng cho audio, video - data phải đến đúng giờ, không cần 100% accuracy. Nếu packet bị lỗi, nó bị discard - không có retry, không có ACK.

Tôi chưa có kinh nghiệm thực tế với loại này trên POS system nên không đi sâu. Nhưng có vài điểm đáng biết:

  • Isochronous có guaranteed bandwidth - được reserve mỗi frame/microframe
  • Không có handshake (ACK/NAK/STALL) - host gửi, device nhận, xong
  • bInterval trên Full Speed là 1 (mỗi frame, 1ms) - không thể chậm hơn
  • USB Audio Class dùng isochronous - microphone, speaker
  • Max 90% bandwidth của frame có thể dùng cho isochronous (Full Speed)
Full Speed isochronous: 1 packet per frame, max 1023 bytes/packet
High Speed isochronous: 1-3 packets per microframe, max 1024 bytes/packet

7. So sánh 4 loại transfer

Mục Giá trị Ghi chú
Control Endpoint 0, có structure Setup/Data/Status Enumeration bắt buộc. 10% bandwidth reserved. Firmware ít handle trực tiếp - middleware lo.
Interrupt Host poll theo bInterval, device ACK/NAK HID, card reader. Guaranteed max latency. bInterval phải là power-of-2. Phải clear buffer sau poll.
Bulk Best-effort, host retry không giới hạn CDC data, MSC, firmware update. Cao nhất throughput nhưng không guaranteed. Phải tự define framing.
Isochronous Guaranteed bandwidth, không retry Audio, video. Latency cực thấp. Không ACK/NAK. Packet lỗi = discard.

Chọn loại nào?

Cần gửi lệnh/config ngắn từ host?    → Control (vendor request)
Cần input device, card reader?        → Interrupt
Cần file transfer, log dump, update?  → Bulk
Cần audio/video streaming?            → Isochronous

POS system thường dùng: Control (vendor request để control device) + Interrupt (card reader, barcode) + Bulk (data transfer, update).


8. Suspend, Reset và cách firmware giữ liên kết

Suspend - USB idle quá 3ms

Normal:  |SOF|...|SOF|...|SOF|...|
Suspend: |SOF|.........(3ms+).........|
                 ↑ Device detect suspend

Khi suspend, device phải giảm current xuống dưới 2.5mA (bus-powered) hoặc 500µA (low-power suspend). Firmware nên deinit những thứ không cần thiết.

Một số gotcha quan trọng:

ST-Link debug tool giữ USB active - khi debug qua ST-Link, tool có thể giữ USB active, device không bao giờ suspend. Behavior thật trên production có thể khác.

Single-bank Flash erase gây pseudo-suspend - erase 25ms không có SOF → host có thể detect suspend. Sau khi erase xong, firmware phải handle resume correctly.

Reset - host muốn re-enumerate

Host gửi USB reset bằng cách kéo D+ và D- xuống 0V trong ít nhất 10ms. Sau reset, device quay về:

  • Address = 0
  • Configuration = 0 (unconfigured)
  • Tất cả endpoint về default state

Firmware phải sẵn sàng re-enumerate bất cứ lúc nào. Không nên lưu state phụ thuộc vào USB address hay configuration number.

Soft disconnect - firmware control

Đôi khi cần programmatically disconnect và reconnect USB (ví dụ: sau khi update firmware, muốn host re-enumerate với image mới):

void USB_SoftReconnect(void)
{
    /* Disconnect - pull-up D+ xuống */
    USBD_Stop(&hUsbDeviceFS);
    HAL_Delay(200);  /* Đủ để host detect disconnect */

    /* Reconnect */
    USBD_Start(&hUsbDeviceFS);
    /* Host sẽ tự enumerate lại */
}

Trên STM32G0, USBD_Stop() clear bit DPPU trong USB_BCDR - pull-up D+ bị disable → host thấy device unplug.


9. Tóm tắt - những điểm không được quên

Interrupt transfer:

  • Host poll, device ACK/NAK - không phải device interrupt host
  • bInterval phải là power-of-2 (1, 2, 4, 8…) vì host dùng binary tree scheduler
  • Phải clear/không-ghi buffer sau khi host poll xong để tránh stale data
  • NAK = không có data, không phải lỗi

Bulk transfer:

  • Không có framing - phải tự define protocol (length prefix, marker, CRC)
  • ZLP cần thiết khi transfer size là bội số của 64 bytes
  • Cần driver trên Windows (WinUSB/Zadig) - không có built-in class driver

SOF và timing:

  • SOF = keepalive - không có SOF 3ms → device detect suspend
  • Flash erase trên single-bank = không có SOF = host nghĩ device suspend
  • Sau reset, device phải re-enumerate từ đầu - không assume state cũ

Đọc tiếp


Tài liệu tham khảo

Thấy nội dung này hữu ích?

Lưu lại hoặc chia sẻ cho người cũng đang học firmware, BIOS/UEFI và embedded systems.

Nội dung liên quan

Một số bài viết, ghi chú hoặc project có liên quan đến nội dung bạn vừa đọc.

Đọc thêm về BIOS/UEFI

Khám phá các bài viết về BIOS/UEFI, embedded firmware, debugging và system-level thinking.