Cách làm máy ấp trứng mini bằng thùng xốp và ESP32: giữ 37,5 °C bằng PI, giữ ẩm, đảo trứng tự động
Cách làm máy ấp trứng gà mini bằng thùng xốp, bóng đèn và ESP32: cấu tạo, lịch 21 ngày, giữ 37,5 °C bằng PI qua SSR, giữ ẩm theo ngày, đảo trứng bằng servo, đếm ngày qua mất điện, còi báo.
Máy ấp trứng mini tự làm chỉ cần một thùng xốp, một bóng đèn sưởi, một quạt nhỏ, khay nước và một mạch điều khiển giữ nhiệt độ thật ổn định. Bài này hướng dẫn cách làm máy ấp trứng gà khoảng 30 quả bằng thùng xốp và ESP32: cấu tạo, lịch nhiệt độ, độ ẩm 21 ngày, cách giữ 37,5 °C bằng bộ điều khiển PI qua SSR (kèm mô phỏng nhiệt để chọn hệ số), giữ ẩm theo ngày, đảo trứng tự động bằng servo, đếm ngày lưu trong flash để mất điện không mất ngày, và còi báo khi nhiệt độ lệch.
An toàn: bóng sưởi và SSR dùng điện 220 V; phần 220 V đặt ngoài thùng, trong hộp kín, có cầu chì. Thùng xốp gần bóng đèn có thể bị nóng chảy: giữ bóng cách thành thùng, dùng chụp bóng bằng kim loại.
1. Cấu tạo

- Thùng xốp 40–60 lít có nắp, khoét một ô kính nhỏ để nhìn và vài lỗ thông gió đường kính 1 cm (phôi cần không khí).
- Sưởi: bóng sợi đốt 40–60 W hoặc tấm sưởi PTC, điều khiển bằng SSR. Không chiếu thẳng vào trứng.
- Quạt 12 V chạy liên tục để nhiệt độ đều khắp thùng; không có quạt, chỗ gần bóng có thể nóng hơn chỗ xa 2–3 °C.
- Cảm biến DHT22 đặt ngang tầm trứng, không gần bóng. Có thêm một nhiệt kế đối chứng đặt cạnh trứng để kiểm tra.
- Khay trứng gắn trục, servo nghiêng ±45° để đảo; hoặc đảo tay 3–5 lần mỗi ngày nếu không làm servo.
- Độ ẩm: khay nước rộng; muốn tự động thì thêm máy tạo ẩm siêu âm nhỏ qua relay.
2. Lịch nhiệt độ, độ ẩm 21 ngày

Nhiệt độ là yếu tố quan trọng nhất: giữ 37,5 °C, lệch quá 1 °C kéo dài làm tỉ lệ nở giảm rõ; quá 39–40 °C vài giờ có thể làm chết phôi. Từ ngày 19 ngừng đảo, tăng độ ẩm để màng vỏ không khô, gà mổ vỏ dễ hơn; hạn chế mở nắp.
3. Giữ nhiệt bằng PI qua SSR

Bật tắt bóng đèn theo ngưỡng (dưới 37,3 bật, trên 37,7 tắt) cũng chạy được, nhưng nhiệt độ dao động lên xuống liên tục vì bóng còn nóng sau khi tắt. Chương trình dùng bộ PI: tính phần trăm công suất cần cấp từ độ lệch nhiệt độ và tổng độ lệch theo thời gian, rồi bật SSR đúng phần đó của mỗi chu kỳ 10 giây. SSR (relay bán dẫn) đóng cắt êm, chịu được hàng triệu lần; relay cơ đóng cắt mỗi 10 giây sẽ mòn tiếp điểm nhanh.
- Kp = 0,5: lệch 1 °C thì thêm 50 % công suất. Ki = 0,001: phần tích phân bù dần độ lệch còn lại.
- Phần tích phân chỉ cộng khi nhiệt độ đã gần mức đặt (lệch dưới 1,5 °C) và công suất chưa bão hòa, để lúc khởi động từ nhiệt độ phòng nhiệt độ không vọt quá nhiều.
- Mất cảm biến 3 lần liên tiếp thì tắt sưởi và kêu còi: thà để trứng nguội một lúc còn hơn nấu chín.
Hệ số trên chọn bằng mô phỏng nhiệt dưới đây. Thùng thật khác (thùng to, nhiều trứng, bóng khác công suất): đo xem từ nhiệt độ phòng mất bao lâu để nóng lên khi bật bóng liên tục, ước lượng hai thông số thùng rồi chạy lại script để kiểm tra:
# PYWORLD.VN - Mô phỏng nhiệt máy ấp trứng thùng xốp với bộ điều khiển PI giống chương trình ESP32: Kp 0,5, Ki 0,001, cửa sổ SSR 10 giây
# PYWORLD.VN - Mô hình một khối nhiệt: C dT/dt = P_sưởi − UA (T − T_phòng); cảm biến trễ bậc nhất 30 giây; mở nắp ở giờ thứ 3 làm tụt 3 °C
# PYWORLD.VN - Chạy: python mo_phong_nhiet.py [C J/K] [UA W/K] [P W] (mặc định 8000 1.2 60: thùng xốp 50 L, 30 trứng, bóng 60 W)
"""Số liệu thùng, trứng, bóng đèn là giả định để chọn Kp, Ki; thùng thật nên đo thời gian nóng lên rồi chỉnh lại."""
import sys
DAT, KP, KI, VUNG, CUA_SO, CHU_KY = 37.5, 0.50, 0.001, 1.5, 10.0, 2.5
def mo_phong(c=8000.0, ua=1.2, p_max=60.0, t_phong=28.0, gio=5.0, dt=0.2, mo_nap_gio=3.0):
t_thung = t_cam_bien = t_phong
tich_phan, cong_suat, ds = 0.0, 0.0, []
lan_cuoi = 0.0
k = 0
while k * dt <= gio * 3600:
t = k * dt
if abs(t - mo_nap_gio * 3600) < dt / 2:
t_thung -= 3.0 # mở nắp xem trứng
if t - lan_cuoi >= CHU_KY - 1e-9: # như loop(): đọc cảm biến, tính PI mỗi 2,5 s
giay, lan_cuoi = t - lan_cuoi, t
sai = DAT - t_cam_bien
thu = KP * sai + KI * (tich_phan + sai * giay)
if 0 < thu < 1 and abs(sai) < VUNG:
tich_phan += sai * giay
cong_suat = min(1.0, max(0.0, KP * sai + KI * tich_phan))
bat = (t % CUA_SO) < cong_suat * CUA_SO
t_thung += ((p_max if bat else 0.0) - ua * (t_thung - t_phong)) * dt / c
t_cam_bien += (t_thung - t_cam_bien) * dt / 30.0 # cảm biến trễ khoảng 30 giây
if k % 150 == 0: # lưu mỗi 30 giây
ds.append((t, t_thung, cong_suat))
k += 1
return ds
if __name__ == "__main__":
c, ua, p = (float(sys.argv[1]), float(sys.argv[2]), float(sys.argv[3])) if len(sys.argv) > 3 else (8000.0, 1.2, 60.0)
ds = mo_phong(c, ua, p)
toi = next((t for t, x, _ in ds if x >= DAT - 0.1), None)
on_dinh = [x for t, x, _ in ds if 2 * 3600 <= t < 3 * 3600]
sau_nap = [x for t, x, _ in ds if t >= 3 * 3600]
hoi_phuc = next((t for t, x, _ in ds if t > 3 * 3600 + 60 and x >= DAT - 0.2), None)
tb = sum(cs for t, _, cs in ds if 2 * 3600 <= t < 3 * 3600) / len(on_dinh)
print(f"Thung C {c:g} J/K, UA {ua:g} W/K, bong {p:g} W")
print(f"Toi {DAT - 0.1:.1f} C sau {toi / 60:.0f} phut; cao nhat {max(x for _, x, _ in ds):.2f} C")
print(f"Gio 2-3: {min(on_dinh):.2f} .. {max(on_dinh):.2f} C, cong suat trung binh {tb * 100:.0f} %")
print(f"Mo nap gio 3: thap nhat {min(sau_nap):.2f} C, hoi phuc ve {DAT - 0.2:.1f} C sau {(hoi_phuc - 3 * 3600) / 60:.0f} phut")
Về PID và cách chỉnh hệ số có thêm ở bài giữ tốc độ động cơ ổn định bằng PID.
4. Chương trình ESP32

Chương trình đọc DHT22 không cần thư viện (cách đọc giải thích ở bài DHT11, DHT22 với ESP32 không cần thư viện), dùng thêm thư viện Adafruit SSD1306 cho màn hình.
// PYWORLD.VN - Máy ấp trứng gà tự làm với ESP32: giữ 37,5 °C bằng PI qua SSR, giữ ẩm theo ngày, đảo trứng 2 giờ một lần bằng servo
// PYWORLD.VN - Ngày 1-18: ẩm 50-55 %, đảo trứng; ngày 19-21: ẩm 65-70 %, ngừng đảo; đếm ngày lưu flash nên mất điện không mất ngày
// PYWORLD.VN - DHT22 GPIO27 (kéo lên 10 kΩ); SSR sưởi GPIO26; relay tạo ẩm GPIO33 (kích mức thấp); servo GPIO13; còi GPIO25; nút GPIO4; OLED D21, D22
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Preferences.h>
const int CHAN_DHT = 27, CHAN_SSR = 26, CHAN_AM = 33, CHAN_SERVO = 13, CHAN_COI = 25, CHAN_NUT = 4;
const float NHIET_DAT = 37.5f; // °C cho trứng gà
const float KP = 0.50f, KI = 0.001f; // phần công suất trên 1 °C; trên 1 °C·giây
const float VUNG_TICH_PHAN = 1.5f; // °C: chỉ tích phân khi gần nhiệt độ đặt, giảm vọt nhiệt lúc khởi động
const uint32_t CUA_SO_MS = 10000; // SSR bật một phần của mỗi 10 giây
const uint32_t DAO_MS = 2UL * 3600 * 1000; // đảo trứng mỗi 2 giờ
const int NGAY_KHOA = 19, NGAY_NO = 21; // từ ngày 19 ngừng đảo, tăng ẩm
Adafruit_SSD1306 oled(128, 64, &Wire, -1);
Preferences nvs;
portMUX_TYPE khoaNgat = portMUX_INITIALIZER_UNLOCKED;
float nhietDo = NAN, doAm = NAN, tichPhan = 0, congSuat = 0;
uint32_t phutDaAp = 0, lucPhut = 0, lucDoc = 0, lucCuaSo = 0, lucDao = 0, lucNhanNut = 0;
int loiDoc = 0, gocDao = 45;
bool amBat = false;
static int32_t choHetMuc(int muc, uint32_t toiDa) {
uint32_t batDau = micros();
while (digitalRead(CHAN_DHT) == muc)
if (micros() - batDau > toiDa) return -1;
return (int32_t)(micros() - batDau);
}
bool docDht22() { // như bài đọc DHT22 không cần thư viện
uint8_t du[5] = {0}, cao[40];
pinMode(CHAN_DHT, OUTPUT);
digitalWrite(CHAN_DHT, LOW);
delay(2);
pinMode(CHAN_DHT, INPUT_PULLUP);
bool ok = true;
portENTER_CRITICAL(&khoaNgat);
if (choHetMuc(HIGH, 100) < 0 || choHetMuc(LOW, 100) < 0 || choHetMuc(HIGH, 100) < 0) ok = false;
for (int i = 0; i < 40 && ok; i++) {
int32_t c = (choHetMuc(LOW, 80) < 0) ? -1 : choHetMuc(HIGH, 100);
if (c < 0) ok = false; else cao[i] = c;
}
portEXIT_CRITICAL(&khoaNgat);
if (!ok) return false;
for (int i = 0; i < 40; i++) du[i / 8] = (du[i / 8] << 1) | (cao[i] > 48 ? 1 : 0);
if (((du[0] + du[1] + du[2] + du[3]) & 0xFF) != du[4]) return false;
doAm = ((du[0] << 8) | du[1]) * 0.1f;
nhietDo = (((du[2] & 0x7F) << 8) | du[3]) * 0.1f * ((du[2] & 0x80) ? -1 : 1);
return true;
}
void datServo(int goc) { // 0,5-2,5 ms trong chu kỳ 20 ms, độ phân giải 14 bit
uint32_t us = 500 + (uint32_t)goc * 2000 / 180;
ledcWrite(CHAN_SERVO, us * 16383 / 20000);
}
void datAm(bool bat) {
amBat = bat;
digitalWrite(CHAN_AM, bat ? LOW : HIGH); // relay kích mức thấp
}
int ngayHienTai() { return phutDaAp / 1440 + 1; }
void tinhPI(float giay) {
float sai = NHIET_DAT - nhietDo;
float thu = KP * sai + KI * (tichPhan + sai * giay);
if (thu > 0 && thu < 1 && fabsf(sai) < VUNG_TICH_PHAN) tichPhan += sai * giay; // chống tích phân quá mức
congSuat = constrain(KP * sai + KI * tichPhan, 0.0f, 1.0f);
}
void hienThi() {
int ngay = ngayHienTai();
bool dao = ngay < NGAY_KHOA;
uint32_t conLai = dao ? (DAO_MS - (millis() - lucDao)) / 60000 : 0;
oled.clearDisplay();
oled.setCursor(0, 0);
oled.printf("Ngay %d/%d ", ngay, NGAY_NO);
if (dao) oled.printf("dao %lu:%02lu", (unsigned long)(conLai / 60), (unsigned long)(conLai % 60)); else oled.print("khoa trung");
oled.setCursor(0, 16);
oled.printf("T %.1f C (dat %.1f)", nhietDo, NHIET_DAT);
oled.setCursor(0, 32);
oled.printf("Am %.0f %% (dat %s)", doAm, ngay < NGAY_KHOA ? "50-55" : "65-70");
oled.setCursor(0, 48);
oled.printf("Suoi %3.0f%% Am %s", congSuat * 100, amBat ? "BAT" : "TAT");
oled.display();
}
void setup() {
Serial.begin(115200);
pinMode(CHAN_AM, INPUT); // gói esp32 3.x: đặt mức tắt trước khi chuyển sang OUTPUT
digitalWrite(CHAN_AM, HIGH);
pinMode(CHAN_AM, OUTPUT);
pinMode(CHAN_SSR, OUTPUT);
digitalWrite(CHAN_SSR, LOW);
pinMode(CHAN_COI, OUTPUT);
pinMode(CHAN_NUT, INPUT_PULLUP);
ledcAttach(CHAN_SERVO, 50, 14);
datServo(gocDao);
Wire.begin(21, 22);
oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);
oled.setTextColor(SSD1306_WHITE);
nvs.begin("aptrung", false);
phutDaAp = nvs.getUInt("phut", 0); // tiếp tục đếm sau khi mất điện
lucPhut = lucCuaSo = lucDao = millis();
Serial.printf("Bat dau o ngay %d (%u phut)\n", ngayHienTai(), (unsigned)phutDaAp);
}
void loop() {
uint32_t bayGio = millis();
// Giữ nút 3 giây: bắt đầu mẻ trứng mới (đếm lại từ ngày 1)
if (digitalRead(CHAN_NUT) == LOW) {
if (lucNhanNut == 0) lucNhanNut = bayGio;
if (bayGio - lucNhanNut >= 3000) {
phutDaAp = 0;
nvs.putUInt("phut", 0);
lucNhanNut = 0;
Serial.println("Me trung moi: ngay 1");
while (digitalRead(CHAN_NUT) == LOW) delay(10);
}
} else {
lucNhanNut = 0;
}
if (bayGio - lucPhut >= 60000) { // đếm phút; ghi flash mỗi 10 phút
lucPhut += 60000;
if (++phutDaAp % 10 == 0) nvs.putUInt("phut", phutDaAp);
}
if (bayGio - lucDoc >= 2500) { // DHT22 cần ít nhất 2 giây giữa hai lần đọc
float giay = (bayGio - lucDoc) / 1000.0f;
lucDoc = bayGio;
if (docDht22()) {
loiDoc = 0;
tinhPI(giay);
int ngay = ngayHienTai();
float amDuoi = ngay < NGAY_KHOA ? 50 : 65, amTren = ngay < NGAY_KHOA ? 55 : 70;
if (doAm < amDuoi) datAm(true);
else if (doAm > amTren) datAm(false);
Serial.printf("Ngay %d | T %.1f C | am %.0f %% | suoi %.0f %% | tao am %s\n", ngay, nhietDo, doAm, congSuat * 100, amBat ? "BAT" : "TAT");
} else if (++loiDoc >= 3) {
congSuat = 0; // mất cảm biến: tắt sưởi để không nấu chín trứng
Serial.println("Loi doc DHT22 3 lan lien tiep: tat suoi");
}
hienThi();
}
// Sưởi theo cửa sổ thời gian: bật SSR trong congSuat × 10 giây đầu mỗi cửa sổ
if (bayGio - lucCuaSo >= CUA_SO_MS) lucCuaSo += CUA_SO_MS;
digitalWrite(CHAN_SSR, (bayGio - lucCuaSo) < congSuat * CUA_SO_MS ? HIGH : LOW);
// Đảo trứng tới ngày 18
if (ngayHienTai() < NGAY_KHOA && bayGio - lucDao >= DAO_MS) {
lucDao = bayGio;
gocDao = (gocDao == 45) ? 135 : 45;
datServo(gocDao);
Serial.printf("Dao trung sang %d do\n", gocDao);
}
bool baoDong = loiDoc >= 3 || (!isnan(nhietDo) && (nhietDo < 36.5f || nhietDo > 38.5f) && phutDaAp > 60);
digitalWrite(CHAN_COI, baoDong && (bayGio / 500) % 2 ? HIGH : LOW);
}
- Đếm ngày: mỗi phút cộng một, ghi vào flash mỗi 10 phút (Preferences). Mất điện rồi có điện lại, máy tiếp tục từ ngày đang ấp, chỉ có thể lùi tối đa 10 phút.
- Mẻ mới: giữ nút 3 giây để đếm lại từ ngày 1.
- Độ ẩm: bật máy tạo ẩm khi dưới mức thấp, tắt khi trên mức cao của giai đoạn đó.
- Đảo trứng: servo đổi giữa 45° và 135° mỗi 2 giờ, ngừng từ ngày 19.
- Còi kêu khi nhiệt độ ngoài 36,5–38,5 °C (sau giờ đầu khởi động) hoặc mất cảm biến.
- Relay tạo ẩm kích mức thấp được đặt về mức tắt trước khi chuyển chân sang ngõ ra, để relay không đóng lúc ESP32 khởi động.

Trước khi cho trứng vào, chạy máy không tải 1–2 ngày, so số trên màn hình với nhiệt kế đối chứng đặt ở nhiều chỗ trong thùng. Lệch thì dời cảm biến hoặc thêm hệ số bù.
5. Lỗi hay gặp
| Hiện tượng | Nguyên nhân | Cách xử lý |
|---|---|---|
| Nhiệt độ không lên tới 37,5 °C | Bóng yếu so với thùng, phòng lạnh, lỗ thông gió quá to | Bóng lớn hơn, bọc thêm xốp, thu nhỏ lỗ thông gió |
| Nhiệt độ dao động ±0,5 °C trở lên | Không có quạt, cảm biến gần bóng | Lắp quạt trộn gió; dời cảm biến ngang tầm trứng |
| Màn hình đúng nhưng trứng không nở | Cảm biến đọc lệch so với nhiệt độ thật ở trứng | So với nhiệt kế đối chứng đã kiểm tra |
| Độ ẩm tăng mãi không giảm | Thùng kín, máy tạo ẩm quá mạnh | Thêm lỗ thông gió nhỏ; giảm mặt nước |
| Servo rung, kêu khi đứng yên | Nguồn yếu, chung nguồn với ESP32 | Nguồn 5 V riêng cho servo, GND chung |
| Báo lỗi DHT22 liên tục | Dây dài, thiếu điện trở kéo lên, hơi nước đọng | Dây ngắn, điện trở 10 kΩ; đặt cảm biến tránh chỗ đọng nước |
Sản phẩm dùng trong bài: ESP32 DevKit V1 30 chân, module relay, màn hình OLED 0.96 inch.
Xem thêm: nhà kính mini với ESP32: đo nhiệt độ, độ ẩm, tự bật quạt, đèn.
Bài cùng cụm chủ đề: chọn relay hay SSR; an toàn khi tự làm mạch 220 V.
Xem thêm