Hướng dẫn ESP32 PIR – Bật đèn khi có người và hẹn giờ tắt đèn

Trong bài viết ESP32 PIR này, IoTZone sẽ hướng dẫn bạn cách phát hiện có sự chuyển động thông qua cảm biến PIR để làm các dự án thú vị, ví dụ như tự động bật đèn trong một khoản thời gian đã được hẹn trước. Sau khoảng thời gian đó thì đèn tự tắt nhé!

Đây là một ứng dụng cơ bản thường thấy trong các dự án về Smart Home (nhà thông minh). Trong dự án này, mình cũng sẽ giới thiệu đến bạn 2 khái niệm quan trọng đó là bộ đếm giờ và ngắt (Interrupt)

Chuẩn bị

  • ESP32
  • Cảm biến chuyển động PIR
  • Đèn LED
  • Điện trở
  • Dây Jumper
  • Breadboard

Trước tiên, để làm dự án ESP32 PIR thì bạn cần cài tiện ích ESP32 trong Arduino IDE trước, theo hướng dẫn sau: Cách lập trình ESP32 bằng Arduino IDE (Windows, Linux, Mac OS X)

Khái niệm ngắt (Interrupts) trong Arduino là gì?

Ngắt – Interrupts trong Arduino là công cụ quan trọng bạn cần biết để kích hoạt sự kiện bằng cảm biến phát hiện chuyển động PIR. Interrupts giúp các công việc trong chương trình của vi điều khiển diễn ra hoàn toàn tự động và giải quyết các vấn đề liên quan đến thời gian.

Với Interrupts, bạn không cần phải kiểm tra trạng thái hiện tại của các chân kết nối. Vì khi đó, chỉ cần phát hiện một thay đổi thì một hàm sẽ được gọi, và sự kiện sẽ được diễn ra.

Để tạo Interrupts trong Arduino, bạn sử dụng câu lệnh bên dưới, vòi các thông tin cần có là chân GPIO, tên hàm và chế độ:

attachInterrupt(digitalPinToInterrupt(GPIO), function, mode);

GPIO trong Interrupt

Thông tin đầu tiên chúng ta cần điền là chân GPIO. Thông thường, chúng ta sẽ sử dụng cấu trúc digitalPinToInterrupt(GPIO) để chọn chân GPIO làm chân Interrupt.

Ví dụ, dưới đây mình muốn dùng chân GPIO làm chân Interrupt:

digitalPinToInterrupt(27)

Nếu bạn sử dụng mạch ESP32, thì dưới đây, IoTZone đã đánh dấu các chân phù hợp để chọn làm chân Interrupt (các chân trong hình chữ nhật màu đỏ), bạn tham khảo nhé! Mình sẽ dùng chân GPIO để làm chân Interrupt gắn với cảm biến PIR.

Các chân GPIO phù hợp làm chân Interrup trên ESP32 PIR

Tên hàm trong Interrupt

Đây được hiểu đơn giản là tên của hàm sẽ được gọi mỗi khi hệ thống kích hoạt Interrupt.

Mode trong Interrupt

Và thông tin đối số cuối cùng là mode (chế độ). Chúng ta có 5 loại mode khác nhau:

  • LOW: Kích hoạt Interrupt khi pin có trạng thái LOW
  • HIGH: Kích hoạt Interrupt khi pin có trạng thái HIGH
  • CHANGE: Kích hoạt Interrupt khi giá trị của pin thay đổi, ví dụ như từ LOW chuyển sang HIGH hoặc ngược lại
  • FALLING: Kích hoạt khi pin thay đổi từ HIGH về LOW
  • RISING: Kích hoạt khi pin thay đổi từ LOW lên HIGH

Mình sẽ lấy ví dụ với RISING cho bạn dễ hiểu nhé: Khi cảm biến ESP32 PIR phát hiện có người, chân GPIO sẽ thay đổi từ trạng thái LOW sang HIGH. Lúc đó, hệ thống sẽ kích hoạt Interrupt.

Khái niệm bộ đếm giờ (Timers)

Trước khi đi vào dự án về ESP32 PIR, chúng ta hãy cùng tìm hiểu thêm về bộ đếm giờ (Timers) nhé! Cụ thể, mình muốn khi phát hiện có sự chuyển động (khi có người), thì đèn LED chỉ bật 1 giây thôi, sau đó sẽ tắt.

Lúc này, thay vì sử dụng câu lệnh delay() khiến toàn bộ chương trình bị chậm lại (chương trình không thể hoạt động và xử lý vấn đề nào khác) trong một thời gian nhất định, thì chúng ta nên sử dụng bộ hẹn giờ Timers.

Giới thiệu về hàm delay()

Delay() là một cấu trúc lập trình phổ biến trong Arduino và khá đơn giản khi sử dụng. Chúng ta chỉ cần sử dụng một số nguyên duy nhất, sử dụng đơn vị mili giây. Chương trình sẽ đợi hết chừng đó thời gian trước khi chuyển sang dòng lệnh tiếp theo (1 giây = 1000 mili giây).

Delay() có cấu trúc như sau:

delay(time in milliseconds)

Ví dụ, khi bạn viết câu lệnh delay(1000) thì chương trình sẽ dừng ở dòng lệnh đó trong 1 giây.

Delay() là một chức năng chặn, chúng ngăn chặn chương trình thực hiện bất kỳ nhiệm vụ nào cho đến khi hết khoảng thời gian bạn đã quy định. Do đó, nếu bạn muốn chương trình xử lý nhiều nhiệm vụ cùng lúc, như với dự án ESP32 PIR này, thì chúng ta không thể sử dụng delay.

Thông thường, với đa số các dự án khác, bạn cũng nên hạn chế dùng delay, mà hãy dùng timer nhé!

Timers với hàm millis()

Hàm này cho phép chúng ta trả về số mili giây đã trôi qua, kể từ khi chương trình được chạy lần đầu:

millis()

Việc sử dụng phép toán cho phép trả về khoản thời gian trôi qua này, chúng ta có thể dễ dàng xem thử đã trôi qua bao nhiêu giây mà không ngăn chặn chương trình thực hiện các tác vụ khác.

Ví dụ, dưới đây mình có viết một đoạn code nhỏ để đèn LED nhấp nháy liên tục – bật LED trong 1000 mili giây rồi tắt, lặp lại liên tục:

void setup() {
  // set the digital pin as output:
  pinMode(ledPin, OUTPUT);
}

void loop() {
  // here is where you'd put code that needs to be running all the time.

  // check to see if it's time to blink the LED; that is, if the
  // difference between the current time and last time you blinked
  // the LED is bigger than the interval at which you want to
  // blink the LED.
  unsigned long currentMillis = millis();

  if (currentMillis - previousMillis >= interval) {
    // save the last time you blinked the LED
    previousMillis = currentMillis;

    // if the LED is off turn it on and vice-versa:
    if (ledState == LOW) {
      ledState = HIGH;
    } else {
      ledState = LOW;
    }

    // set the LED with the ledState of the variable:
    digitalWrite(ledPin, ledState);
  }
}

Mình sẽ giải thích kỹ hơn về chương trình nhấp nháy đèn LED này nhé, chúng ta sử dụng hàm millis() chứ không dùng đến delay().

Thực tế thì code trên trừ đi thời gian đã ghi trước đó (previousMillis) từ thời gian hiện tại (currentMillis). Nếu hiệu số này lớn hơn khoảng thời gian bạn đặt (trong trường hợp này là 1000), thì đoạn code sẽ cập nhật giá trị previousMillis theo thời gian hiện tại, rồi quyết định bật hay tắt đèn LED:

if (currentMillis - previousMillis >= interval) {
  // save the last time you blinked the LED
  previousMillis = currentMillis;
  (...)

Vì như mình đã giải thích, đoạn code này không ngăn chặn bất kỳ hành động nào của chương trình, nên câu lệnh if vẫn sẽ hoạt động bình thường.

Điều đó có nghĩa là bạn có thể thêm bất kỳ nhiệm vụ nào khác vào vòng lặp loop() và đèn LED của bạn vẫn sẽ nhấp nháp sau mỗi một giây.

Bạn hãy thử nạp chương trình mẫu mình đã viết bên trên vào mạch ESP32 và quan sát xem thử nhé!

Nhấp nháy đèn LED - Bước đầu để làm dự án ESP32 PIR
Nhấp nháy đèn LED – Bước đầu để làm dự án ESP32 PIR

Hướng dẫn ESP32 PIR

Sau khi đã hiểu 2 khái niệm trên, bây giờ chúng ta hãy quay lại dự án ESP32 PIR – phần chính trong bài này nhé!

Kết nối phần cứng

Bạn hãy kết nối đèn LED với GPIO 26, cảm biến ESP32 PIR kết nối GPIIO 27 (ở mức điện áp 3,3V) như hình:

Kết nối phần cứng cho dự án ESP32 PIR
Kết nối phần cứng cho dự án ESP32 PIR

Lưu ý quan trọng: Cảm biến AM312 PIR mình dùng hoạt động ở điện áp 3,3V. Nếu bạn dùng các cảm biến PIR khác như HC-SR501 thì chúng sẽ hoạt động ở mức 5V, lúc đó bạn cần cấp nguồn cho nó bằng chân Vin để đủ nguồn điện nhé!

Nạp chương trình ESP32 PIR

Mình đã chuẩn bị sẵn đoạn code sau, bạn hãy sao chép vào Arduino IDE và nạp vào mạch ESP32 nhé! Nếu cần thay đổi thời gian hẹn giờ bật đèn của LED thì bạn chỉ cần thay đổi biến timeSeconds bằng số giây bạn cần là được nhé:

#define timeSeconds 10

// Set GPIOs for LED and PIR Motion Sensor
const int led = 26;
const int motionSensor = 27;

// Timer: Auxiliary variables
unsigned long now = millis();
unsigned long lastTrigger = 0;
boolean startTimer = false;
boolean motion = false;

// Checks if motion was detected, sets LED HIGH and starts a timer
void IRAM_ATTR detectsMovement() {
  digitalWrite(led, HIGH);
  startTimer = true;
  lastTrigger = millis();
}

void setup() {
  // Serial port for debugging purposes
  Serial.begin(115200);
  
  // PIR Motion Sensor mode INPUT_PULLUP
  pinMode(motionSensor, INPUT_PULLUP);
  // Set motionSensor pin as interrupt, assign interrupt function and set RISING mode
  attachInterrupt(digitalPinToInterrupt(motionSensor), detectsMovement, RISING);

  // Set LED to LOW
  pinMode(led, OUTPUT);
  digitalWrite(led, LOW);
}

void loop() {
  // Current time
  now = millis();
  if((digitalRead(led) == HIGH) && (motion == false)) {
    Serial.println("MOTION DETECTED!!!");
    motion = true;
  }
  // Turn off the LED after the number of seconds defined in the timeSeconds variable
  if(startTimer && (now - lastTrigger > (timeSeconds*1000))) {
    Serial.println("Motion stopped...");
    digitalWrite(led, LOW);
    startTimer = false;
    motion = false;
  }
}

Cách chương trình hoạt động

Khai báo chân GPIO và chân cảm biến PIR:

// Set GPIOs for LED and PIR Motion Sensor
const int led = 26;
const int motionSensor = 27;

Tạo các biến cần thiết để hẹn giờ tắt đèn LED, sau khi phát hiện chuyển động từ cảm biến PIR:

// Timer: Auxiliar variables
long now = millis();
long lastTrigger = 0;
boolean startTimer = false;

Trong đó:

  • Biến now sẽ lưu giá trị thời gian hiện tại
  • LasttTrigger lưu thời gian khi phát hiện chuyển động từ cảm biến PIR
  • boolean startTimer là một biến boolean, cho phép khởi động bộ hẹn giờ Timer sau khi phát hiện có sự chuyển động

setting()

Tạo cổng Serial Monitor ở tốc độ 115200:

Serial.begin(115200);

Chọn cảm biến PIR làm INPUT PULLUP:

pinMode(motionSensor, INPUT_PULLUP);

Để cấu hình cảm biến PIR làm chân Interrupt, bạn viết như sau:

attachInterrupt(digitalPinToInterrupt(motionSensor), detectsMovement, RISING);

Chân phá hiện chuyển động là GPIO 27, hàm dudwojc gọi là detectsMovemtn, mode là RISING.

Ban đầu, đèn LED là output và có trạng thái LOW (tắt đèn):

pinMode(led, OUTPUT);
digitalWrite(led, LOW);

loop()

Cập nhật thời gian hiện tại vào biến now:

now = millis();

Khi phát hiện có sự chuyển động, hàm detectsMovemtn sẽ được gọi như câu lệnh trong Interrupt. Lúc đó, hàm này sẽ:

  • In một thông báo trong Serial Monitor để bạn quan sát
  • Bật đèn LED
  • Đổi giá trị biến boolean startTimer thành true
  • Cập nhật biến lastTrigger theo thời gian hiện tại
void IRAM_ATTR detectsMovement() {
  Serial.println("MOTION DETECTED!!!");
  digitalWrite(led, HIGH);
  startTimer = true;
  lastTrigger = millis();
}

Lưu ý: IRAM_ATTR được dùng để chạy Interrupt trong RAM. Nếu bạn không dùng cấu trúc này thì đoạn code sẽ được lưu trong bộ nhớ Flash, làm việc xử lý bị chậm hơn.

Sau bước này, chương trình sẽ quay lại câu lệnh trong vòng lặp loop().

Lúc này, biến startTimer có giá trị là true. Do đó, khi đã trôi qua một khoản thời gian nhất định kể từ khi phát hiện chuyển động, câu lệnh if bên dưới sẽ đúng, chương trình sẽ thực hiện các câu lệnh bên trong if:

if(startTimer && (now - lastTrigger > (timeSeconds*1000))) {
  Serial.println("Motion stopped...");
  digitalWrite(led, LOW);
  startTimer = false;
}

Serial Monitor sẽ in ra một thông báo Monitor, đèn LED tự tắt và biến startTimer đổi thành giá trị false.

Demo kết quả dự án ESP32 PIR

Hãy nạp chương trình vào mạch ESP32, sau đó mở Serial Monitor (tốc độ 115200) và quan sát nhé!

Bạn hãy thử đưa tay lại trước cảm biến PIR, đèn LED sẽ bật và trên màn hình Serial có in thông báo. Sau 10 giây, đèn LED sẽ tự tắt.

Lời kết

Trên đây, IoTZone đã hướng dẫn bạn cách thực hiện dự án ESP32 PIR, sử dụng bộ đếm giờ Timer chứ không cần dùng delay. Bạn hãy thử ứng dụng các khái niệm này vào các dự án khác nhé!

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *