Xe độ vs. Xe đua F1: 3 Bài học “đắt giá” từ Driver DHT chính thức của Zephyr

Ở bài trước, chúng ta đã thành công trong việc “chế” một driver cho DHT11. Nó chạy, nó đọc được nhiệt độ, độ ẩm, và chúng ta đã ăn mừng với meme “It’s alive!”.

Nhưng một kỹ sư giỏi không bao giờ ngừng học hỏi. Giờ là lúc mở “nắp capo” của driver dht.c chính thức trong mã nguồn Zephyr ra xem các “pháp sư” đã làm gì khác chúng ta.

Bài học 1: Đồng hồ Rolex vs. Đếm nhẩm (Precision Timing)

Cách chúng ta đo thời gian các xung tín hiệu là dùng một vòng lặp whilek_busy_wait(1), rồi tăng một biến đếm.

// --- Cách của chúng ta ---
uint32_t pulse_len_us = 0;
while (gpio_pin_get_dt(...) == 1) {
    k_busy_wait(1);
    pulse_len_us++;
}

Cách này giống như việc chúng ta tự đếm nhẩm “một micro-giây, hai micro-giây…”. Nó hoạt động, nhưng độ chính xác phụ thuộc vào tốc độ CPU và nhiều yếu tố khác.

Cách của ‘hãng’: Họ dùng k_cycle_get_32().

// --- Cách của 'hãng' (đơn giản hóa) ---
uint32_t start_cycles = k_cycle_get_32();

while (gpio_pin_get_dt(...) == 1) {
    // chờ...
}

uint32_t end_cycles = k_cycle_get_32();
uint32_t total_cycles = end_cycles - start_cycles;

// Dùng một hàm khác để đổi từ chu kỳ CPU ra micro-giây
uint32_t pulse_len_us = k_cyc_to_us_floor32(total_cycles);

Đây là cách dùng “đồng hồ Rolex” của chính CPU để đo thời gian. Nó chính xác hơn rất nhiều và không bị ảnh hưởng bởi các yếu tố bên ngoài.

Mind_blow

  • Bài học: Để có sự chính xác trong timing, hãy dùng các công cụ đo đếm của phần cứng mà kernel cung cấp.

Bài học 2: Thuật toán ‘Tắc kè hoa’ (Adaptive Bit Decoding)

Cách chúng ta giải mã bit là dùng một ngưỡng cố định: if (pulse_len_us > 40) { bit = 1; }

Cách này rất cứng nhắc. Nếu cảm biến hơi “chậm” hoặc “nhanh” một chút, driver của chúng ta sẽ giải mã sai. IsThisAPigeon

Cách của ‘hãng’ (cực kỳ thông minh):

  1. Họ không giải mã ngay. Họ đo và lưu lại độ dài của cả 40 xung CAO vào một mảng.
  2. Họ tìm ra xung ngắn nhất (min_duration) và dài nhất (max_duration) trong 40 xung đó.
  3. Họ tính ra một ngưỡng động: ngưỡng = (min_duration + max_duration) / 2.
  4. Họ lặp lại một lần nữa, so sánh độ dài của từng xung với cái ngưỡng động đó để quyết định bit 0 hay 1.

Thuật toán này có khả năng tự động hiệu chỉnh. Nó không quan tâm giá trị tuyệt đối là bao nhiêu, nó chỉ cần tìm ra “ranh giới” giữa nhóm xung ngắn và nhóm xung dài trong chính lần đọc đó. Điều này làm cho driver cực kỳ bền bỉ và đáng tin cậy.

  • Bài học: Khi đối mặt với các tín hiệu không ổn định, hãy nghĩ đến các thuật toán tự động thích ứng thay vì các giá trị cố định.

Bài học 3: Một Driver, Thống trị Tất cả (Flexibility)

Driver của chúng ta chỉ viết cho DHT11. Nếu ngày mai cậu muốn dùng DHT22 (một phiên bản cao cấp hơn), cậu sẽ phải viết một driver khác.

Cách của ‘hãng’: Cùng một file dht.c, họ hỗ trợ cả hai loại. Họ làm điều đó bằng cách:

  1. Trong binding.yaml: Cho phép một thuộc tính tùy chọn, ví dụ dht22-variant;.
  2. Trong DEFINE macro: Đọc thuộc tính đó từ Device Tree: .is_dht22 = DT_INST_PROP(inst, dht22_variant).
  3. Trong channel_get: Dùng một câu lệnh if:
    if (cfg->is_dht22) {
        // Dùng công thức tính toán phức tạp hơn cho DHT22
    } else {
        // Dùng công thức đơn giản cho DHT11
    }
    
  • Bài học: Hãy thiết kế driver của bạn sao cho linh hoạt và có thể tái sử dụng cho các phần cứng tương tự. Sử dụng Device Tree để truyền các thông tin cấu hình đó vào driver.

Kết luận: “Nâng tầm kỹ năng”

Cuộc phiêu lưu tự chế driver đã dạy chúng ta cách hoạt động của giao thức. Nhưng việc phân tích driver “chính hãng” đã cho chúng ta những bài học còn quý giá hơn về cách viết code chuyên nghiệp.

Một driver tốt không chỉ là “chạy được”, mà nó phải chính xác, bền bỉ, và linh hoạt. Đây là những mục tiêu chúng ta nên hướng tới cho các dự án driver trong tương lai.