BÀI 13: ĐÈN GIAO THÔNG THÔNG MINH + HIỂN THỊ GIÂY ĐẾM NGƯỢC

BÀI 13: ĐÈN GIAO THÔNG THÔNG MINH

Trong bài học này, chúng ta sẽ xây dựng một hệ thống đèn giao thông thông minh với hai chế độ hoạt động: chế độ tự động (đèn tự chuyển đổi theo thời gian định sẵn) và chế độ thủ công (người dùng chuyển đèn bằng nút nhấn). Đặc biệt, hệ thống sẽ tích hợp module LED 7 đoạn để hiển thị thời gian đếm ngược trong chế độ tự động, và hiển thị "00" trong chế độ thủ công.

I. Mục tiêu

  • Hiểu và ứng dụng các chân điều khiển của Arduino cho đèn LED và nút nhấn.
  • Làm quen với IC ghi dịch 74HC595 để điều khiển nhiều module LED 7 đoạn chỉ với 3 chân Arduino.
  • Xây dựng hệ thống đèn giao thông hai chế độ: tự động và thủ công.
  • Hiển thị thông tin đếm ngược lên module LED 7 đoạn.
  • Thực hành kỹ thuật chống nhiễu (debounce) cho nút nhấn.

II. Chuẩn bị

  • Arduino Uno (hoặc bất kỳ board Arduino tương thích nào)
  • Module LED 7 đoạn (loại cathode chung, 2 chữ số hoặc 2 module 1 chữ số)
  • IC 74HC595 (1 cái)
  • Đèn LED giao thông (hoặc 3 đèn LED riêng lẻ: Đỏ, Vàng, Xanh lá)
  • Nút nhấn (2 cái)
  • Dây nối (đực - đực, đực - cái)
  • Breadboard

III. Sơ đồ kết nối

Dưới đây là sơ đồ kết nối tổng quát.

                  ARDUINO UNO
               +----------------+
               |                |
   LED ĐỎ -----| D2         D10 | ---> SDI (Module LED 7 đoạn)
   LED VÀNG ---| D3          D9 | ---> CLK (Module LED 7 đoạn)
   LED XANH ---| D4          D8 | ---> LOAD (Module LED 7 đoạn)
               |                |
NÚT CHẾ ĐỘ ----| D5 (INPUT_PULLUP)
NÚT HÀNH ĐỘNG--| D6 (INPUT_PULLUP)
               |                |
               |                |
               | GND            | ---> GND (Module LED 7 đoạn)
               | 5V             | ---> VCC (Module LED 7 đoạn)
               +----------------+

Bạn có tương tác bằng cách nhấn nút trên sơ đồ

Mode: Auto
VCC GND SDI SCLK LOAD VCC GND SDO SCLK LOAD Mode SW Light

Code

const int ledRed = 2;
const int ledYellow = 3;
const int ledGreen = 4;

const int modeButton = 5;
const int actionButton = 6;

const int latchPin = 8;
const int clockPin = 9;
const int dataPin = 10;

bool autoMode = true;
int autoState = 0;
int manualState = 0;

unsigned long lastChange = 0;
unsigned long lastDebounce = 0;
const unsigned long debounceDelay = 300;

byte digitCode[10] = {
    B11000000,  // 0
    B11111001,  // 1
    B10100100,  // 2
    B10110000,  // 3
    B10011001,  // 4
    B10010010,  // 5
    B10000010,  // 6
    B11111000,  // 7
    B10000000,  // 8
    B10010000   // 9
};

void setup() {
  // Khai báo LED
  pinMode(ledRed, OUTPUT);
  pinMode(ledYellow, OUTPUT);
  pinMode(ledGreen, OUTPUT);

  // Khai báo nút
  pinMode(modeButton, INPUT_PULLUP);
  pinMode(actionButton, INPUT_PULLUP);

  // Khai báo IC 74HC595
  pinMode(latchPin, OUTPUT);
  pinMode(clockPin, OUTPUT);
  pinMode(dataPin, OUTPUT);

  Serial.begin(9600);
  Serial.println("Chế độ: TỰ ĐỘNG");
}

void loop() {
  handleModeSwitch();  // Đổi chế độ

  if (autoMode) {
    handleAutoTraffic();  // Tự động

  } else {
    handleManualTraffic();  // Thủ công
  }
}

// 1. XỬ LÝ ĐỔI CHẾ ĐỘ
void handleModeSwitch() {
  static bool lastButtonState = HIGH;
  bool reading = digitalRead(modeButton);

  if (reading != lastButtonState && millis() - lastDebounce > debounceDelay) {
    if (reading == LOW) {
      autoMode = !autoMode;

      Serial.print("Chuyển chế độ: ");
      Serial.println(autoMode ? "TỰ ĐỘNG" : "THỦ CÔNG");

      allOff();
      autoState = 0;
      manualState = 0;
      lastChange = millis();
    }
    lastDebounce = millis();
  }

  lastButtonState = reading;
}

// 2. CHẾ ĐỘ TỰ ĐỘNG

void handleAutoTraffic() {
  unsigned long currentTime = millis();
  int remaining = 0;

  switch (autoState) {
    case 0:  // Xanh 15s
      digitalWrite(ledGreen, HIGH);
      digitalWrite(ledYellow, LOW);
      digitalWrite(ledRed, LOW);

      remaining = 15 - (currentTime - lastChange) / 1000;

      if (currentTime - lastChange >= 15000) {
        autoState = 1;
        lastChange = currentTime;
      }

      break;

    case 1:  // Vàng 3s
      digitalWrite(ledGreen, LOW);
      digitalWrite(ledYellow, HIGH);
      digitalWrite(ledRed, LOW);

      remaining = 3 - (currentTime - lastChange) / 1000;

      if (currentTime - lastChange >= 3000) {
        autoState = 2;
        lastChange = currentTime;
      }
      break;

    case 2:  // Đỏ 10s
      digitalWrite(ledGreen, LOW);
      digitalWrite(ledYellow, LOW);
      digitalWrite(ledRed, HIGH);

      remaining = 10 - (currentTime - lastChange) / 1000;

      if (currentTime - lastChange >= 10000) {
        autoState = 0;
        lastChange = currentTime;
      }
      break;
  }
  displayNumber(remaining, 2, false);
}

// 3. CHẾ ĐỘ THỦ CÔNG

void handleManualTraffic() {
  static bool lastActionButtonState = HIGH;

  bool reading = digitalRead(actionButton);

  if (reading != lastActionButtonState &&
      millis() - lastDebounce > debounceDelay) {
    if (reading == LOW) {
      manualState++;

      if (manualState > 2) manualState = 0;

      Serial.print("Thủ công chuyển đèn: ");

      if (manualState == 0) Serial.println("Xanh");
      if (manualState == 1) Serial.println("Vàng");
      if (manualState == 2) Serial.println("Đỏ");

      allOff();
      if (manualState == 0) digitalWrite(ledGreen, HIGH);
      if (manualState == 1) digitalWrite(ledYellow, HIGH);
      if (manualState == 2) digitalWrite(ledRed, HIGH);
    }

    lastDebounce = millis();
  }

  lastActionButtonState = reading;

  displayNumber(0);  // Không đếm trong thủ công, hiển thị 00
}

// 4. TẮT TOÀN BỘ LED

void allOff() {
  digitalWrite(ledRed, LOW);
  digitalWrite(ledYellow, LOW);
  digitalWrite(ledGreen, LOW);
}

// 5. HIỂN THỊ GIÂY

void displayNumber(int number) {
  int tens = number / 10;
  int ones = number % 10;

  digitalWrite(latchPin, LOW);

  shiftOut(dataPin, clockPin, MSBFIRST, digitCode[ones]);  // IC thứ nhất
  shiftOut(dataPin, clockPin, MSBFIRST, digitCode[tens]);  // IC thứ hai

  digitalWrite(latchPin, HIGH);
}

void displayNumber(int number, int numDigits, bool isAnode) {
  // Giới hạn để tránh lỗi
  if (numDigits < 1) numDigits = 1;
  if (numDigits > 5) numDigits = 5;

  byte digits[numDigits];

  // Cắt từng chữ số từ phải sang trái
  for (int i = 0; i < numDigits; i++) {
    int digit = number % 10;
    number /= 10;

    byte code = digitCode[digit];

    // Nếu là LED anode chung, đảo bit
    if (isAnode) {
      code = ~code;
    }

    digits[i] = code;
  }

  digitalWrite(latchPin, LOW);

  // Gửi từ hàng cao (trái) đến hàng thấp (phải)
  for (int i = 0; i < numDigits; i++) {
    shiftOut(dataPin, clockPin, MSBFIRST, digits[i]);
  }

  digitalWrite(latchPin, HIGH);
}

Mô phỏng

HO CHI MINH CITY MODE SW Light
Mode: Auto

IV. Giải thích Code

Đoạn code bạn cung cấp được tổ chức rất rõ ràng, tôi sẽ đi qua từng phần:

1. Khai báo Chân và Biến (const int, bool, int, unsigned long)

// ====== Chân LED giao thông ======
const int ledRed = 2;    // Chân LED đỏ
const int ledYellow = 3; // Chân LED vàng
const int ledGreen = 4;  // Chân LED xanh

// ====== Chân nút ======
const int modeButton = 5;   // Nút chuyển chế độ (tự động/thủ công)
const int actionButton = 6; // Nút hành động (chuyển đèn trong chế độ thủ công)

// ====== Chân IC 74HC595 ======
const int latchPin = 8;
const int clockPin = 9;
const int dataPin = 10;

// ====== Biến điều khiển trạng thái ======
bool autoMode = true; // Biến trạng thái: true = tự động, false = thủ công
int autoState = 0;    // Trạng thái hiện tại trong chế độ tự động (0: Xanh, 1: Vàng, 2: Đỏ)
int manualState = 0;  // Trạng thái hiện tại trong chế độ thủ công (0: Xanh, 1: Vàng, 2: Đỏ)

unsigned long lastChange = 0; // Thời điểm cuối cùng đèn đổi trạng thái (dùng cho chế độ tự động)
unsigned long lastDebounce = 0; // Thời điểm cuối cùng nút được nhấn (dùng chống nhiễu)
const unsigned long debounceDelay = 300; // Thời gian chờ chống nhiễu (miligiây)

// ====== Bảng mã LED 7 đoạn (Anode chung) ======
byte digitCode[10] = {
    B11000000,  // 0
    B11111001,  // 1
    B10100100,  // 2
    B10110000,  // 3
    B10011001,  // 4
    B10010010,  // 5
    B10000010,  // 6
    B11111000,  // 7
    B10000000,  // 8
    B10010000   // 9
};
  • const int: Định nghĩa các chân cố định cho LED, nút nhấn và IC 74HC595. Việc này giúp code dễ đọc và dễ thay đổi nếu bạn chuyển chân.
  • Biến điều khiển trạng thái: Các biến autoMode, autoState, manualState giúp theo dõi chế độ và trạng thái hiện tại của đèn.
  • lastChangelastDebounce: Quan trọng để quản lý thời gian và chống nhiễu cho nút nhấn. millis() được dùng để đo thời gian không chặn (non-blocking).
  • digitCode: Mảng này chứa các mã nhị phân để bật tắt các đoạn của LED 7 đoạn, tạo thành các chữ số từ 0 đến 9. Với module LED 7 đoạn cathode chung, một bit '1' tương ứng với đoạn sáng.

2. Hàm setup()

void setup() {
  // Khai báo chân LED là OUTPUT
  pinMode(ledRed, OUTPUT);
  pinMode(ledYellow, OUTPUT);
  pinMode(ledGreen, OUTPUT);

  // Khai báo chân nút là INPUT_PULLUP
  // Kích hoạt điện trở kéo lên bên trong Arduino, không cần điện trở ngoài
  pinMode(modeButton, INPUT_PULLUP);
  pinMode(actionButton, INPUT_PULLUP);

  // Khai báo chân IC 74HC595 là OUTPUT
  pinMode(latchPin, OUTPUT);
  pinMode(clockPin, OUTPUT);
  pinMode(dataPin, OUTPUT);

  Serial.begin(9600); // Khởi tạo giao tiếp Serial để debug
  Serial.println("Chế độ: TỰ ĐỘNG"); // In ra trạng thái khởi đầu
}
  • pinMode: Cấu hình các chân Arduino là OUTPUT (cho LED và 74HC595) hoặc INPUT_PULLUP (cho nút nhấn). INPUT_PULLUP giúp đơn giản hóa mạch nút nhấn bằng cách sử dụng điện trở kéo lên bên trong chip, nút sẽ nối chân với GND khi nhấn.
  • Serial.begin(9600): Khởi tạo giao tiếp Serial ở tốc độ 9600 baud, cho phép bạn xem thông báo trạng thái trên Serial Monitor của Arduino IDE.

3. Hàm loop()

void loop() {
  handleModeSwitch(); // Luôn kiểm tra nút chuyển chế độ

  if (autoMode) {
    handleAutoTraffic(); // Nếu ở chế độ tự động, xử lý đèn tự động
  } else {
    handleManualTraffic(); // Nếu ở chế độ thủ công, xử lý đèn thủ công
  }
}
  • Hàm loop() là nơi chương trình chạy lặp đi lặp lại.
  • Nó gọi handleModeSwitch() liên tục để kiểm tra xem người dùng có muốn chuyển chế độ không.
  • Sau đó, dựa vào giá trị của autoMode, nó sẽ gọi handleAutoTraffic() (chế độ tự động) hoặc handleManualTraffic() (chế độ thủ công).

4. Hàm handleModeSwitch() - Xử lý chuyển chế độ

void handleModeSwitch() {
  static bool lastButtonState = HIGH; // Lưu trạng thái nút trước đó (HIGH vì dùng INPUT_PULLUP)
  bool reading = digitalRead(modeButton); // Đọc trạng thái hiện tại của nút

  // Kiểm tra nếu nút đã thay đổi trạng thái VÀ đã đủ thời gian debounce
  if (reading != lastButtonState && millis() - lastDebounce > debounceDelay) {
    if (reading == LOW) { // Nếu nút được nhấn (từ HIGH xuống LOW)
      autoMode = !autoMode; // Đảo ngược chế độ
      Serial.print("Chuyển chế độ: ");
      Serial.println(autoMode ? "TỰ ĐỘNG" : "THỦ CÔNG");
      allOff(); // Tắt tất cả đèn khi chuyển chế độ
      autoState = 0; // Đặt lại trạng thái ban đầu cho cả hai chế độ
      manualState = 0;
      lastChange = millis(); // Đặt lại thời điểm thay đổi để đếm ngược chính xác
    }
    lastDebounce = millis(); // Cập nhật thời điểm debounce cuối cùng
  }
  lastButtonState = reading; // Lưu trạng thái hiện tại của nút
}
  • Hàm này sử dụng kỹ thuật chống nhiễu (debounce). Khi một nút nhấn được nhấn, nó thường tạo ra các tín hiệu "nảy" (bounce) trong một thời gian ngắn. millis() - lastDebounce > debounceDelay đảm bảo rằng chỉ một lần nhấn hợp lệ được ghi nhận sau khi nút đã ổn định.
  • Khi nút modeButton được nhấn (từ HIGH xuống LOW do INPUT_PULLUP), biến autoMode sẽ được đảo ngược, chuyển đổi giữa hai chế độ.
  • Các đèn được tắt hết và trạng thái của cả hai chế độ được reset để đảm bảo một khởi đầu sạch sẽ.

5. Hàm handleAutoTraffic() - Chế độ tự động

void handleAutoTraffic() {
  unsigned long currentTime = millis();
  int remaining = 0;

  switch (autoState) {
    case 0:  // Xanh 15s
      digitalWrite(ledGreen, HIGH);
      digitalWrite(ledYellow, LOW);
      digitalWrite(ledRed, LOW);

      remaining = 15 - (currentTime - lastChange) / 1000;

      if (currentTime - lastChange >= 15000) {
        autoState = 1;
        lastChange = currentTime;
      }

      break;

    case 1:  // Vàng 3s
      digitalWrite(ledGreen, LOW);
      digitalWrite(ledYellow, HIGH);
      digitalWrite(ledRed, LOW);

      remaining = 3 - (currentTime - lastChange) / 1000;

      if (currentTime - lastChange >= 3000) {
        autoState = 2;
        lastChange = currentTime;
      }
      break;

    case 2:  // Đỏ 10s
      digitalWrite(ledGreen, LOW);
      digitalWrite(ledYellow, LOW);
      digitalWrite(ledRed, HIGH);

      remaining = 10 - (currentTime - lastChange) / 1000;

      if (currentTime - lastChange >= 10000) {
        autoState = 0;
        lastChange = currentTime;
      }
      break;
  }
  displayNumber(remaining, 2, false); // number, số LED, anode chung
}
  • Sử dụng switch-case để quản lý các trạng thái của đèn giao thông (Xanh, Vàng, Đỏ).
  • Trong mỗi trạng thái, hàm sẽ:
    • Bật đèn LED tương ứng và tắt các đèn còn lại.
    • Tính toán thời gian còn lại (remaining) bằng cách lấy tổng thời gian của trạng thái trừ đi thời gian đã trôi qua.
    • Kiểm tra xem thời gian đã trôi qua có đủ để chuyển sang trạng thái tiếp theo không.
    • Gọi displayNumber(remaining) để hiển thị thời gian đếm ngược.

6. Hàm handleManualTraffic() - Chế độ thủ công

void handleManualTraffic() {
  static bool lastActionButtonState = HIGH;
  bool reading = digitalRead(actionButton);

  // Kiểm tra nút hành động với debounce
  if (reading != lastActionButtonState && millis() - lastDebounce > debounceDelay) {
    if (reading == LOW) { // Nếu nút được nhấn
      manualState++; // Tăng trạng thái lên 1
      if (manualState > 2) manualState = 0; // Nếu vượt quá 2 (Đỏ), quay lại 0 (Xanh)

      Serial.print("Thủ công chuyển đèn: ");
      if (manualState == 0) Serial.println("Xanh");
      if (manualState == 1) Serial.println("Vàng");
      if (manualState == 2) Serial.println("Đỏ");

      allOff(); // Tắt tất cả đèn
      if (manualState == 0) digitalWrite(ledGreen, HIGH);   // Bật đèn tương ứng với trạng thái
      if (manualState == 1) digitalWrite(ledYellow, HIGH);
      if (manualState == 2) digitalWrite(ledRed, HIGH);
    }
    lastDebounce = millis();
  }
  lastActionButtonState = reading;
  displayNumber(0); // Luôn hiển thị 00 trong chế độ thủ công
}
  • Cũng sử dụng debounce cho actionButton.
  • Mỗi lần actionButton được nhấn, manualState sẽ tăng lên và quay lại 0 sau khi đạt 2.
  • Đèn LED tương ứng với manualState sẽ được bật.
  • displayNumber(0) được gọi để luôn hiển thị "00" trên LED 7 đoạn trong chế độ này.

7. Hàm allOff() - Tắt tất cả LED giao thông

void allOff() {
  digitalWrite(ledRed, LOW);
  digitalWrite(ledYellow, LOW);
  digitalWrite(ledGreen, LOW);
}
  • Hàm đơn giản này tắt tất cả ba đèn LED giao thông. Được dùng khi chuyển chế độ hoặc khởi tạo.

8. Hàm displayNumber() - Hiển thị số lên LED 7 đoạn

void displayNumber(int number, int numDigits, bool isAnode) {
  // Giới hạn để tránh lỗi
  if (numDigits < 1) numDigits = 1;
  if (numDigits > 5) numDigits = 5;

  byte digits[numDigits];

  // Cắt từng chữ số từ phải sang trái
  for (int i = 0; i < numDigits; i++) {
    int digit = number % 10;
    number /= 10;

    byte code = digitCode[digit];

    // Nếu là LED anode chung, đảo bit
    if (isAnode) {
      code = ~code;
    }

    digits[i] = code;
  }

  digitalWrite(latchPin, LOW);

  // Gửi từ hàng cao (trái) đến hàng thấp (phải)
  for (int i = 0; i < numDigits; i++) {
    shiftOut(dataPin, clockPin, MSBFIRST, digits[i]);
  }

  digitalWrite(latchPin, HIGH);
}
  • Đây là hàm quan trọng để điều khiển module LED 7 đoạn thông qua IC 74HC595.
  • shiftOut(dataPin, clockPin, MSBFIRST, data): Hàm này là thư viện tích hợp của Arduino dùng để gửi dữ liệu nối tiếp.
    • dataPin: Chân dữ liệu (DS của 74HC595).
    • clockPin: Chân xung đồng hồ (SHCP của 74HC595).
    • MSBFIRST: Gửi bit có giá trị cao nhất trước (Most Significant Bit First).
    • data: Byte dữ liệu cần gửi.
  • Quy trình gửi dữ liệu cho 74HC595:
    1. Kéo latchPin (STCP) xuống LOW: Điều này khóa thanh ghi lưu trữ, chuẩn bị cho việc nhận dữ liệu mới.
    2. Gọi shiftOut() hai lần:
      • Lần thứ nhất gửi mã của chữ số hàng chục. Dữ liệu này sẽ được dịch vào thanh ghi dịch của 74HC595 và "đẩy" dữ liệu cũ ra (nếu có).
      • Lần thứ hai gửi mã của chữ số hàng đơn vị. Dữ liệu này sẽ đẩy mã hàng chục sang thanh ghi thứ hai (hoặc vào vị trí thứ hai của một thanh ghi lớn hơn nếu module 2 chữ số được nối tiếp bên trong) và dữ liệu mới (hàng đơn vị) sẽ vào vị trí đầu tiên.
    3. Kéo latchPin (STCP) lên HIGH: Điều này sẽ chuyển dữ liệu từ thanh ghi dịch sang thanh ghi lưu trữ, và dữ liệu sẽ xuất hiện đồng thời trên các chân Q0-Q7, làm sáng các đoạn LED tương ứng.

V. Các bước thực hiện

  1. Lắp mạch: Dựa vào sơ đồ kết nối ở trên, tiến hành lắp mạch trên breadboard. Hãy đảm bảo các kết nối đúng và chắc chắn.
  2. Mở Arduino IDE: Khởi động phần mềm Arduino IDE trên máy tính của bạn.
  3. Dán Code: Sao chép toàn bộ đoạn code đã cung cấp và dán vào cửa sổ Arduino IDE.
  4. Chọn Board và Port:
    • Vào Tools -> Board -> Chọn Arduino Uno (hoặc loại board bạn đang dùng).
    • Vào Tools -> Port -> Chọn cổng COM mà Arduino của bạn đang kết nối.
  5. Biên dịch và Tải lên: Nhấn nút Upload (mũi tên sang phải) để biên dịch và tải chương trình lên Arduino của bạn.
  6. Kiểm tra hoạt động:
    • Mở Serial Monitor (biểu tượng kính lúp ở góc trên bên phải của Arduino IDE) để theo dõi thông báo trạng thái.
    • Quan sát đèn giao thông và module LED 7 đoạn.
    • Nhấn nút modeButton để chuyển đổi giữa chế độ tự động và thủ công.
    • Trong chế độ tự động, đèn sẽ tự đổi theo thời gian và LED 7 đoạn sẽ đếm ngược.
    • Trong chế độ thủ công, nhấn nút actionButton để chuyển trạng thái đèn và LED 7 đoạn sẽ hiển thị "00".

VI. Nâng cao (Tùy chọn)

  • Điều chỉnh thời gian: Thay đổi các giá trị thời gian (15000, 3000, 10000) trong handleAutoTraffic() để điều chỉnh chu kỳ đèn.
  • Thêm chế độ ban đêm: Bạn có thể thêm một chế độ ban đêm, nơi đèn vàng nhấp nháy liên tục.
  • Thêm cảm biến: Tích hợp cảm biến (ví dụ: cảm biến quang trở) để tự động chuyển sang chế độ ban đêm khi trời tối.
  • Tăng số lượng đèn/ngã tư: Nếu bạn muốn điều khiển nhiều ngã tư, bạn sẽ cần thêm các cặp LED và có thể thêm IC 74HC595 hoặc các IC điều khiển khác.
  • Cải thiện hiển thị: Thay vì chỉ hiển thị thời gian, bạn có thể cân nhắc hiển thị số xe đang chờ hoặc các thông tin khác nếu có các cảm biến phù hợp.
  • Điều chỉnh mã LED 7 đoạn: Nếu module LED 7 đoạn của bạn có thứ tự chân khác hoặc là loại anode chung, bạn sẽ cần điều chỉnh mảng digitCode cho phù hợp.

Nhận xét

Mới hơn Cũ hơn