Arduino Nâng Cao

Kỹ thuật lập trình chuyên sâu và ứng dụng thực tế

Mục Lục Bài Học

1. Ngắt (Interrupt) - Xử Lý Sự Kiện Nhanh

Interrupt cho phép Arduino tạm dừng chương trình chính để xử lý một sự kiện khẩn cấp ngay lập tức.

Các loại Interrupt:

Các chế độ External Interrupt:

Chế độ Mô tả
LOW Kích hoạt khi chân ở mức LOW
CHANGE Kích hoạt khi chân thay đổi trạng thái
RISING Kích hoạt khi chân chuyển từ LOW → HIGH
FALLING Kích hoạt khi chân chuyển từ HIGH → LOW
Ví dụ: Đếm số lần nhấn nút bằng Interrupt
const int BUTTON_PIN = 2;  // Phải là chân interrupt (D2 hoặc D3)
const int LED_PIN = 13;

volatile int counter = 0;  // volatile vì được dùng trong ISR
volatile bool ledState = false;

void setup() {
  Serial.begin(9600);
  pinMode(BUTTON_PIN, INPUT_PULLUP);  // Dùng pull-up nội
  pinMode(LED_PIN, OUTPUT);
  
  // Gắn interrupt vào chân 2, gọi hàm buttonPressed khi FALLING
  attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonPressed, FALLING);
  
  Serial.println("Nhấn nút để đếm!");
}

void loop() {
  // Chương trình chính chạy bình thường
  Serial.print("Số lần nhấn: ");
  Serial.println(counter);
  delay(1000);
}

// ISR (Interrupt Service Routine) - hàm xử lý ngắt
void buttonPressed() {
  counter++;
  ledState = !ledState;
  digitalWrite(LED_PIN, ledState);
}
⚠️ Lưu ý về ISR:
  • Hàm ISR phải ngắn gọn, thực thi nhanh
  • Không dùng delay() trong ISR
  • Biến được chia sẻ giữa ISR và loop() phải khai báo volatile
  • Hạn chế dùng Serial trong ISR

Ứng dụng: Đo tốc độ vòng quay (RPM)

Đo RPM bằng Interrupt
const int SENSOR_PIN = 2;
volatile unsigned long pulseCount = 0;
unsigned long lastTime = 0;
float rpm = 0;

void setup() {
  Serial.begin(9600);
  pinMode(SENSOR_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(SENSOR_PIN), countPulse, FALLING);
}

void loop() {
  // Tính RPM mỗi giây
  if (millis() - lastTime >= 1000) {
    // Tắt interrupt tạm thời để đọc biến
    noInterrupts();
    unsigned long pulses = pulseCount;
    pulseCount = 0;
    interrupts();
    
    // Tính RPM (giả sử có 1 pulse/vòng)
    rpm = pulses * 60.0;
    
    Serial.print("Tốc độ: ");
    Serial.print(rpm);
    Serial.println(" RPM");
    
    lastTime = millis();
  }
}

void countPulse() {
  pulseCount++;
}

2. Timer & Counter

Timer cho phép thực hiện tác vụ định kỳ mà không cần dùng delay(), giúp chương trình không bị blocking.

Kỹ thuật Blink Without Delay

LED nhấp nháy không dùng delay()
const int LED_PIN = 13;
unsigned long previousMillis = 0;
const long interval = 1000;  // 1 giây
bool ledState = LOW;

void setup() {
  pinMode(LED_PIN, OUTPUT);
}

void loop() {
  unsigned long currentMillis = millis();
  
  // Kiểm tra đã đủ thời gian chưa
  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;
    
    // Đảo trạng thái LED
    ledState = !ledState;
    digitalWrite(LED_PIN, ledState);
  }
  
  // Code khác có thể chạy ở đây mà không bị delay chặn
}

Nhiều Timer đồng thời

Điều khiển 3 LED với tốc độ khác nhau
const int LED1 = 8, LED2 = 9, LED3 = 10;

unsigned long timer1 = 0, timer2 = 0, timer3 = 0;
const long interval1 = 500;   // LED1 nháy nhanh (0.5s)
const long interval2 = 1000;  // LED2 trung bình (1s)
const long interval3 = 2000;  // LED3 chậm (2s)

bool state1 = LOW, state2 = LOW, state3 = LOW;

void setup() {
  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);
  pinMode(LED3, OUTPUT);
}

void loop() {
  unsigned long currentMillis = millis();
  
  // Timer cho LED1
  if (currentMillis - timer1 >= interval1) {
    timer1 = currentMillis;
    state1 = !state1;
    digitalWrite(LED1, state1);
  }
  
  // Timer cho LED2
  if (currentMillis - timer2 >= interval2) {
    timer2 = currentMillis;
    state2 = !state2;
    digitalWrite(LED2, state2);
  }
  
  // Timer cho LED3
  if (currentMillis - timer3 >= interval3) {
    timer3 = currentMillis;
    state3 = !state3;
    digitalWrite(LED3, state3);
  }
}

3. Giao Tiếp I2C (Inter-Integrated Circuit)

I2C là giao thức truyền thông nối tiếp chỉ cần 2 dây: SDA (dữ liệu) và SCL (xung nhịp).

Arduino Uno: SDA = A4, SCL = A5

Quét địa chỉ I2C

I2C Scanner - Tìm địa chỉ thiết bị
#include <Wire.h>

void setup() {
  Wire.begin();
  Serial.begin(9600);
  Serial.println("Bắt đầu quét I2C...");
}

void loop() {
  byte error, address;
  int deviceCount = 0;

  Serial.println("Đang quét...");

  for(address = 1; address < 127; address++) {
    Wire.beginTransmission(address);
    error = Wire.endTransmission();

    if (error == 0) {
      Serial.print("Tìm thấy thiết bị I2C tại địa chỉ 0x");
      if (address < 16) Serial.print("0");
      Serial.println(address, HEX);
      deviceCount++;
    }
  }

  if (deviceCount == 0)
    Serial.println("Không tìm thấy thiết bị I2C\n");
  else
    Serial.println("Hoàn thành!\n");

  delay(5000);
}

Ví dụ: OLED Display I2C (SSD1306)

Cài thư viện: Sketch → Include Library → Manage Libraries → Tìm "Adafruit SSD1306" và "Adafruit GFX"

Hiển thị text lên OLED 0.96"
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C  // Địa chỉ I2C (thường là 0x3C hoặc 0x3D)

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

void setup() {
  Serial.begin(9600);
  
  // Khởi tạo OLED
  if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println("Lỗi khởi tạo OLED!");
    while(true);
  }
  
  // Xóa màn hình
  display.clearDisplay();
  
  // Cài đặt màu chữ trắng
  display.setTextColor(SSD1306_WHITE);
  
  // Hiển thị text
  display.setTextSize(2);
  display.setCursor(0, 0);
  display.println("Arduino");
  
  display.setTextSize(1);
  display.setCursor(0, 20);
  display.println("Hoc lap trinh");
  display.println("Arduino & ESP32");
  
  // Cập nhật lên màn hình
  display.display();
}

void loop() {
  // Hiển thị thời gian chạy
  display.clearDisplay();
  display.setTextSize(1);
  display.setCursor(0, 40);
  display.print("Uptime: ");
  display.print(millis() / 1000);
  display.println("s");
  display.display();
  
  delay(1000);
}

Đọc cảm biến nhiệt độ/độ ẩm DHT11/DHT22 hiển thị lên OLED

Trạm thời tiết mini với OLED
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define DHTPIN 7
#define DHTTYPE DHT11

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
DHT dht(DHTPIN, DHTTYPE);

void setup() {
  Serial.begin(9600);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  dht.begin();
  
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.display();
}

void loop() {
  float temp = dht.readTemperature();
  float humid = dht.readHumidity();
  
  if (isnan(temp) || isnan(humid)) {
    Serial.println("Lỗi đọc cảm biến!");
    return;
  }
  
  display.clearDisplay();
  
  // Tiêu đề
  display.setTextSize(2);
  display.setCursor(15, 0);
  display.println("TRAM");
  display.setCursor(0, 16);
  display.println("THOI TIET");
  
  // Nhiệt độ
  display.setTextSize(1);
  display.setCursor(0, 38);
  display.print("Nhiet do: ");
  display.print(temp, 1);
  display.println(" C");
  
  // Độ ẩm
  display.setCursor(0, 50);
  display.print("Do am: ");
  display.print(humid, 1);
  display.println(" %");
  
  display.display();
  delay(2000);
}

4. Giao Tiếp SPI (Serial Peripheral Interface)

SPI là giao thức truyền thông nối tiếp tốc độ cao, dùng 4 dây chính:

Ví dụ: Thẻ nhớ SD Card qua SPI

Đọc/ghi file trên thẻ SD
#include <SPI.h>
#include <SD.h>

const int chipSelect = 10;  // Chân CS của SD Card Module

void setup() {
  Serial.begin(9600);
  
  Serial.print("Khởi tạo thẻ SD...");
  
  if (!SD.begin(chipSelect)) {
    Serial.println("Lỗi! Không tìm thấy thẻ SD");
    return;
  }
  Serial.println("Thẻ SD đã sẵn sàng.");
  
  // Ghi vào file
  File dataFile = SD.open("test.txt", FILE_WRITE);
  
  if (dataFile) {
    dataFile.println("Hello from Arduino!");
    dataFile.println("Thời gian: " + String(millis()));
    dataFile.close();
    Serial.println("Đã ghi dữ liệu vào test.txt");
  } else {
    Serial.println("Lỗi mở file!");
  }
}

void loop() {
  // Đọc file
  File dataFile = SD.open("test.txt");
  
  if (dataFile) {
    Serial.println("Nội dung file test.txt:");
    
    while (dataFile.available()) {
      Serial.write(dataFile.read());
    }
    
    dataFile.close();
  }
  
  delay(10000);  // Đọc lại mỗi 10 giây
}

7. Servo Motor - Điều Khiển Góc Quay

Servo motor SG90 có thể quay từ 0° đến 180°.

Kết nối: Dây đỏ → 5V, Dây nâu → GND, Dây cam → Chân PWM (ví dụ: chân 9)

Điều khiển Servo cơ bản
#include <Servo.h>

Servo myServo;
const int SERVO_PIN = 9;

void setup() {
  myServo.attach(SERVO_PIN);
  myServo.write(90);  // Vị trí giữa
}

void loop() {
  // Quét từ 0° đến 180°
  for (int pos = 0; pos <= 180; pos++) {
    myServo.write(pos);
    delay(15);
  }
  
  // Quét từ 180° về 0°
  for (int pos = 180; pos >= 0; pos--) {
    myServo.write(pos);
    delay(15);
  }
}

Điều khiển Servo bằng biến trở

Servo điều khiển bởi Potentiometer
#include <Servo.h>

Servo myServo;
const int POT_PIN = A0;
const int SERVO_PIN = 9;

void setup() {
  myServo.attach(SERVO_PIN);
}

void loop() {
  // Đọc giá trị biến trở (0-1023)
  int potValue = analogRead(POT_PIN);
  
  // Chuyển đổi sang góc (0-180)
  int angle = map(potValue, 0, 1023, 0, 180);
  
  // Điều khiển servo
  myServo.write(angle);
  
  delay(15);
}

8. Động Cơ DC - Điều Khiển Tốc Độ & Chiều

Sử dụng L298N Motor Driver để điều khiển động cơ DC.

Kết nối L298N:

Điều khiển động cơ DC với L298N
const int ENA = 9;   // PWM - điều khiển tốc độ
const int IN1 = 7;   // Điều khiển chiều
const int IN2 = 8;

void setup() {
  pinMode(ENA, OUTPUT);
  pinMode(IN1, OUTPUT);
  pinMode(IN2, OUTPUT);
  
  // Dừng động cơ ban đầu
  digitalWrite(IN1, LOW);
  digitalWrite(IN2, LOW);
}

void loop() {
  // Quay thuận với tốc độ tăng dần
  motorForward(100);
  delay(2000);
  
  motorForward(200);
  delay(2000);
  
  // Dừng
  motorStop();
  delay(1000);
  
  // Quay ngược
  motorBackward(150);
  delay(2000);
  
  motorStop();
  delay(1000);
}

// Hàm quay thuận
void motorForward(int speed) {
  digitalWrite(IN1, HIGH);
  digitalWrite(IN2, LOW);
  analogWrite(ENA, speed);
}

// Hàm quay ngược
void motorBackward(int speed) {
  digitalWrite(IN1, LOW);
  digitalWrite(IN2, HIGH);
  analogWrite(ENA, speed);
}

// Hàm dừng động cơ
void motorStop() {
  digitalWrite(IN1, LOW);
  digitalWrite(IN2, LOW);
  analogWrite(ENA, 0);
}

Robot di chuyển 2 động cơ

Robot 2 bánh với L298N
// Motor trái
const int ENA = 9;
const int IN1 = 7;
const int IN2 = 8;

// Motor phải
const int ENB = 10;
const int IN3 = 5;
const int IN4 = 6;

const int SPEED = 200;

void setup() {
  pinMode(ENA, OUTPUT);
  pinMode(IN1, OUTPUT);
  pinMode(IN2, OUTPUT);
  pinMode(ENB, OUTPUT);
  pinMode(IN3, OUTPUT);
  pinMode(IN4, OUTPUT);
}

void loop() {
  moveForward();
  delay(2000);
  
  stopRobot();
  delay(500);
  
  turnRight();
  delay(1000);
  
  stopRobot();
  delay(500);
}

void moveForward() {
  // Motor trái tiến
  digitalWrite(IN1, HIGH);
  digitalWrite(IN2, LOW);
  analogWrite(ENA, SPEED);
  
  // Motor phải tiến
  digitalWrite(IN3, HIGH);
  digitalWrite(IN4, LOW);
  analogWrite(ENB, SPEED);
}

void moveBackward() {
  digitalWrite(IN1, LOW);
  digitalWrite(IN2, HIGH);
  analogWrite(ENA, SPEED);
  
  digitalWrite(IN3, LOW);
  digitalWrite(IN4, HIGH);
  analogWrite(ENB, SPEED);
}

void turnRight() {
  // Motor trái tiến, motor phải lùi
  digitalWrite(IN1, HIGH);
  digitalWrite(IN2, LOW);
  analogWrite(ENA, SPEED);
  
  digitalWrite(IN3, LOW);
  digitalWrite(IN4, HIGH);
  analogWrite(ENB, SPEED);
}

void turnLeft() {
  digitalWrite(IN1, LOW);
  digitalWrite(IN2, HIGH);
  analogWrite(ENA, SPEED);
  
  digitalWrite(IN3, HIGH);
  digitalWrite(IN4, LOW);
  analogWrite(ENB, SPEED);
}

void stopRobot() {
  digitalWrite(IN1, LOW);
  digitalWrite(IN2, LOW);
  analogWrite(ENA, 0);
  
  digitalWrite(IN3, LOW);
  digitalWrite(IN4, LOW);
  analogWrite(ENB, 0);
}

Bài Tập Thực Hành Nâng Cao

  1. Robot tránh vật cản: Sử dụng HC-SR04 + L298N để tạo robot tự động tránh vật cản.
  2. Hệ thống tưới cây tự động: Dùng cảm biến độ ẩm đất + relay điều khiển bơm nước.
  3. Đồng hồ hiển thị OLED: Tạo đồng hồ thời gian thực với DS3231 RTC + OLED.
  4. Data Logger: Ghi dữ liệu cảm biến vào thẻ SD theo thời gian.