Phased array siêu âm 40 kHz với ESP32 (phần 1): nguyên lý lái búp sóng và tạo 8 kênh lệch pha
- 1. Phased array lái búp sóng thế nào?
- 2. Công thức độ trễ và độ lệch pha
- 3. Giới hạn: khoảng cách đầu phát và búp lặp
- 4. Chuẩn bị
- 5. Tạo xung lệch pha bằng LEDC và hpoint
- 6. Chương trình 1: hai kênh lệch pha, tự đo lại
- 7. Sơ đồ nối tám đầu phát
- 8. Chương trình 2: tám kênh, lái búp sóng theo góc
- 9. Kết quả mong đợi
- 10. Lưu ý khi thử nghiệm
- 11. Xử lý lỗi thường gặp
- 12. Xem thêm
Nguyên lý phased array và cách dùng ESP32 tạo 8 kênh xung 40 kHz lệch pha để lái búp sóng siêu âm: công thức độ trễ, giới hạn do khoảng cách đầu phát, cấu hình LEDC với hpoint, sơ đồ nối và code.
Phased array (mảng pha) là kỹ thuật lái búp sóng mà không cần xoay bộ phát: nhiều phần tử đứng yên phát cùng một tần số, chỉ khác nhau về thời điểm phát. Radar, máy siêu âm y tế, sonar và anten 5G đều dùng nguyên lý này. Bài viết dùng siêu âm 40 kHz trong không khí để thực hành: bước sóng khoảng 8,7 mm, linh kiện rẻ và ESP32 tự tạo được cả tám kênh xung lệch pha.
Đây là phần 1 của loạt hai bài: giải thích nguyên lý, công thức và cách dùng bộ LEDC của ESP32 tạo tám kênh 40 kHz có pha điều chỉnh được. Phần 2 thêm mạch đệm 12 V, đầu thu và chương trình quét để đo búp sóng.
1. Phased array lái búp sóng thế nào?
- Mỗi đầu phát tạo ra một sóng lan tỏa. Ở hướng mà các sóng này tới cùng pha, chúng cộng lại thành búp sóng mạnh; ở các hướng khác chúng triệt tiêu nhau một phần.
- Khi cả tám kênh phát cùng lúc, búp sóng đi thẳng theo pháp tuyến của mảng.
- Khi mỗi kênh phát trễ hơn kênh trước một khoảng τ, mặt sóng chung nghiêng đi và búp sóng lệch về phía kênh phát sau cùng.
- Đổi τ là đổi hướng búp sóng. Việc này thực hiện bằng chương trình nên nhanh hơn nhiều so với xoay cơ khí.
2. Công thức độ trễ và độ lệch pha
Với khoảng cách giữa hai đầu phát kề nhau là d, vận tốc âm thanh c và góc lái θ tính từ pháp tuyến:
- Độ trễ giữa hai kênh kề nhau: τ = d · sin θ / c.
- Độ lệch pha tương ứng: Δφ = 360° · d · sin θ / λ, với bước sóng λ = c / f.
- Vận tốc âm thanh trong không khí phụ thuộc nhiệt độ: c ≈ 331,3 + 0,606 · t (m/s, t tính bằng °C). Ở 30 °C, c ≈ 349,5 m/s và λ ≈ 8,74 mm tại 40 kHz.
- Sóng phát liên tục nên trễ thêm đúng một chu kỳ cũng cho kết quả như cũ. Chương trình chỉ cần phần lẻ của độ trễ tính theo chu kỳ.
| Góc lái θ | Độ trễ τ giữa hai kênh | Độ lệch pha Δφ |
|---|---|---|
| 5° | 2,53 µs | 36,5° |
| 10° | 5,05 µs | 72,7° |
| 20° | 9,94 µs | 143,2° |
| 30° | 14,54 µs | 209,3° |
Bảng tính cho d = 10,16 mm, tần số 40 kHz, nhiệt độ 30 °C. Một chu kỳ 40 kHz dài 25 µs.
3. Giới hạn: khoảng cách đầu phát và búp lặp
- Muốn chỉ có một búp sóng, khoảng cách d phải không lớn hơn nửa bước sóng, tức khoảng 4,4 mm ở 40 kHz.
- Đầu phát 40 kHz thông dụng có đường kính 10 mm hoặc 16 mm nên không thể xếp sát đến mức đó. Với d = 10,16 mm, mảng luôn có thêm búp lặp (grating lobe): các hướng khác mà sóng cũng tới cùng pha.
- Khi lái 0°, búp lặp nằm ở khoảng ±59°. Càng lái xa pháp tuyến, búp lặp càng tiến lại gần: lái 20° thì búp lặp ở khoảng -31°.
- Vùng lái không gây nhầm lẫn là khoảng ±25°. Vượt quá góc này, búp lặp nằm gần pháp tuyến hơn búp chính.
- Tám đầu phát cho búp chính rộng khoảng 5,5° ở mức nửa công suất. Thêm đầu phát thì búp hẹp hơn, nhưng vị trí búp lặp không đổi.
- Với đầu phát 16 mm xếp cách nhau 17,78 mm (7 lỗ), búp lặp xuất hiện ở ±29° ngay khi lái 0° và vùng lái dùng được chỉ còn khoảng ±14°.
4. Chuẩn bị
- Một mạch ESP32 DevKit V1 30 chân (hoặc bản dùng chip CH340) và cáp USB có truyền dữ liệu.
- Tám đầu phát siêu âm 40 kHz loại đường kính 10 mm. Trên thân thường in chữ T (đầu phát) hoặc R (đầu thu); mảng phát dùng loại T.
- Một miếng board đục lỗ bước 2,54 mm để hàn tám đầu phát thẳng hàng, tâm cách tâm 4 lỗ (10,16 mm).
- Breadboard và dây cắm.
- Arduino IDE 2.x với gói esp32 by Espressif Systems (bài dùng phiên bản 3.3.10). Không cần cài thêm thư viện.
Ở phần 1, mỗi đầu phát nối thẳng vào một chân GPIO và chạy ở 3,3 V. Mức này đủ để thử ở cự ly ngắn; phần 2 thêm mạch đệm 12 V để phát mạnh hơn.
5. Tạo xung lệch pha bằng LEDC và hpoint
- LEDC là bộ tạo xung PWM của ESP32, quen thuộc qua bài điều chỉnh độ sáng LED bằng PWM. Mỗi kênh gắn với một bộ đếm (timer); bộ đếm quyết định tần số.
- Mỗi kênh có tham số hpoint: bước đếm mà tại đó xung lên mức cao. Nhiều kênh dùng chung một bộ đếm nhưng khác hpoint sẽ lệch pha nhau một lượng cố định.
- Với độ phân giải 10 bit, một chu kỳ 25 µs chia thành 1024 bước. Mỗi bước là 24,4 ns, tương ứng 0,35° pha. Ở 40 kHz với xung nhịp 80 MHz, 10 bit là độ phân giải cao nhất dùng được.
- Hàm
ledcAttach()của Arduino luôn đặt hpoint bằng 0. Vì vậy chương trình gọi thẳng các hàmledc_timer_config()vàledc_channel_config()của ESP-IDF, có sẵn trong gói esp32. - Xung vuông 50% rộng 512 bước, nên hpoint chỉ đặt từ 0 đến 511 để xung nằm gọn trong một chu kỳ. Với nửa chu kỳ sau, chương trình lùi hpoint 512 bước rồi đảo tín hiệu ra: dạng xung thu được giống hệt.
6. Chương trình 1: hai kênh lệch pha, tự đo lại
Chương trình tạo hai kênh 40 kHz trên D23 và D22 và cho đặt độ lệch pha từ Serial Monitor. Để kiểm tra mà không cần máy hiện sóng, nối thêm hai dây: D23 với D34 và D22 với D35. ESP32 đọc cùng lúc hai chân D34, D35 ở 20.000 thời điểm ngẫu nhiên: hai xung lệch nhau bao nhiêu độ thì khác mức nhau trong bấy nhiêu phần của 180°.
/*
PYWORLD.VN - Tạo hai kênh xung 40 kHz lệch pha bằng LEDC trên ESP32
Hai kênh dùng chung một bộ đếm; độ lệch pha đặt bằng tham số hpoint của từng kênh
Gõ góc lệch pha (0 đến 359) vào Serial Monitor; ESP32 tự đo lại qua hai dây D23-D34 và D22-D35
*/
#include <driver/ledc.h>
#include <soc/soc.h>
#include <soc/gpio_reg.h>
#define CHAN_A 23 // Kênh chuẩn, pha 0
#define CHAN_B 22 // Kênh lệch pha
#define CHAN_DO_A 34 // Nối dây với D23 để đo lại
#define CHAN_DO_B 35 // Nối dây với D22 để đo lại
const uint32_t TAN_SO_HZ = 40000;
const uint32_t SO_BUOC = 1024; // Độ phân giải 10 bit: một chu kỳ chia thành 1024 bước
const uint32_t NUA_CHU_KY = SO_BUOC / 2;
// Cấu hình một kênh xung vuông 50%: lên mức cao ở bước hpoint, xuống mức thấp sau nửa chu kỳ.
// hpoint cộng độ rộng xung không được vượt quá 1024, nên với nửa chu kỳ sau
// chương trình lùi hpoint nửa chu kỳ rồi đảo tín hiệu ra: kết quả giống hệt.
void datKenh(ledc_channel_t kenh, int chan, uint32_t hpoint) {
bool dao = hpoint >= NUA_CHU_KY;
ledc_channel_config_t cauHinh = {};
cauHinh.gpio_num = chan;
cauHinh.speed_mode = LEDC_HIGH_SPEED_MODE;
cauHinh.channel = kenh;
cauHinh.intr_type = LEDC_INTR_DISABLE;
cauHinh.timer_sel = LEDC_TIMER_0; // Mọi kênh dùng chung bộ đếm 0 nên luôn đồng bộ với nhau
cauHinh.duty = NUA_CHU_KY;
cauHinh.hpoint = dao ? hpoint - NUA_CHU_KY : hpoint;
cauHinh.flags.output_invert = dao ? 1 : 0;
ledc_channel_config(&cauHinh);
}
// Đọc cùng lúc hai chân D34, D35 ở nhiều thời điểm ngẫu nhiên.
// Hai xung 50% lệch nhau X độ (X từ 0 đến 180) sẽ khác mức nhau trong X/180 thời gian.
float doLechPha() {
const int SO_MAU = 20000;
int soLanKhacMuc = 0;
for (int i = 0; i < SO_MAU; i++) {
uint32_t muc = REG_READ(GPIO_IN1_REG); // Thanh ghi mức vào của GPIO32 đến GPIO39
bool a = (muc >> (CHAN_DO_A - 32)) & 1;
bool b = (muc >> (CHAN_DO_B - 32)) & 1;
if (a != b) {
soLanKhacMuc++;
}
delayMicroseconds(random(3, 40));
}
return 180.0f * soLanKhacMuc / SO_MAU;
}
void datLechPha(int gocDo) {
uint32_t hpoint = (uint32_t)lroundf(gocDo / 360.0f * SO_BUOC) % SO_BUOC;
datKenh(LEDC_CHANNEL_1, CHAN_B, hpoint);
delay(20);
float treUs = 1000000.0f / TAN_SO_HZ * hpoint / SO_BUOC;
float mongDoi = gocDo <= 180 ? gocDo : 360 - gocDo; // Phép đo gập kết quả về khoảng 0 đến 180 độ
Serial.printf("Dat %d do -> hpoint %u, tre %.2f us | do lai %.1f do (mong doi %.1f)\n",
gocDo, (unsigned)hpoint, treUs, doLechPha(), mongDoi);
}
void setup() {
Serial.begin(115200);
pinMode(CHAN_DO_A, INPUT);
pinMode(CHAN_DO_B, INPUT);
ledc_timer_config_t boDem = {};
boDem.speed_mode = LEDC_HIGH_SPEED_MODE;
boDem.duty_resolution = LEDC_TIMER_10_BIT;
boDem.timer_num = LEDC_TIMER_0;
boDem.freq_hz = TAN_SO_HZ;
boDem.clk_cfg = LEDC_AUTO_CLK;
if (ledc_timer_config(&boDem) != ESP_OK) {
Serial.println("Khong cau hinh duoc bo dem LEDC");
while (true) {
delay(1000);
}
}
datKenh(LEDC_CHANNEL_0, CHAN_A, 0);
Serial.printf("\nTan so %u Hz, %u buoc moi chu ky\n",
(unsigned)ledc_get_freq(LEDC_HIGH_SPEED_MODE, LEDC_TIMER_0), (unsigned)SO_BUOC);
datLechPha(90);
Serial.println("Go goc lech pha tu 0 den 359 roi nhan Enter");
}
void loop() {
if (!Serial.available()) {
return;
}
String dong = Serial.readStringUntil('\n');
dong.trim();
if (dong.length() == 0) {
return;
}
int gocDo = dong.toInt();
if (gocDo < 0 || gocDo > 359) {
Serial.println("Goc phai tu 0 den 359");
return;
}
datLechPha(gocDo);
}
Phép đo này không phân biệt sớm pha với trễ pha, nên kết quả luôn nằm trong khoảng 0 đến 180°: đặt 270° sẽ đo ra khoảng 90°. Sai lệch một vài độ giữa các lần đo là bình thường vì đây là phép đo thống kê.
7. Sơ đồ nối tám đầu phát
- Xếp tám đầu phát thẳng hàng, cách đều. Kênh 0 ở một đầu hàng, kênh 7 ở đầu kia, theo đúng thứ tự.
- Cắm cả tám đầu phát cùng một chiều cực tính. Một đầu phát cắm ngược sẽ lệch pha 180° so với các đầu còn lại và làm hỏng búp sóng. Chân nối với vỏ nhôm (thường có dấu ở đế) nối về GND.
- Khoảng cách thực tế giữa hai tâm đầu phát phải trùng với hằng
KHOANG_CACH_MMtrong chương trình. - Có thể nối tiếp mỗi kênh một điện trở 100 Ω để hạn dòng nạp xả cho chân GPIO; điện trở này không ảnh hưởng đáng kể đến pha.
8. Chương trình 2: tám kênh, lái búp sóng theo góc
Gõ góc lái từ -60 đến 60 vào Serial Monitor. Chương trình tính độ trễ của từng kênh theo công thức ở mục 2, đổi sang hpoint và in bảng pha. Góc dương lái búp sóng về phía kênh 7, góc âm về phía kênh 0.
/*
PYWORLD.VN - Phased array siêu âm 40 kHz: ESP32 tạo 8 kênh xung lệch pha để lái búp sóng
Gõ góc lái (-60 đến 60 độ) vào Serial Monitor; chương trình tính độ trễ từng kênh và in bảng pha
Tám đầu phát xếp thẳng hàng, cách đều; góc dương lái búp sóng về phía kênh 7
*/
#include <driver/ledc.h>
const int SO_KENH = 8;
const int CHAN_PHAT[SO_KENH] = {23, 22, 21, 19, 18, 17, 16, 4}; // Kênh 0 đến kênh 7
const uint32_t TAN_SO_HZ = 40000;
const uint32_t SO_BUOC = 1024; // Độ phân giải 10 bit: mỗi bước là 0,35 độ pha
const uint32_t NUA_CHU_KY = SO_BUOC / 2;
const float KHOANG_CACH_MM = 10.16; // Khoảng cách tâm hai đầu phát kề nhau (4 lỗ của board đục lỗ 2,54 mm)
const float NHIET_DO_C = 30.0; // Nhiệt độ phòng, dùng để tính vận tốc âm thanh
const float GOC_TOI_DA = 60.0;
// Cấu hình một kênh xung vuông 50% bắt đầu ở bước hpoint của chu kỳ.
// Với hpoint từ nửa chu kỳ trở lên: lùi nửa chu kỳ rồi đảo tín hiệu ra, kết quả giống hệt.
void datKenh(int kenh, uint32_t hpoint) {
bool dao = hpoint >= NUA_CHU_KY;
ledc_channel_config_t cauHinh = {};
cauHinh.gpio_num = CHAN_PHAT[kenh];
cauHinh.speed_mode = LEDC_HIGH_SPEED_MODE;
cauHinh.channel = (ledc_channel_t)kenh;
cauHinh.intr_type = LEDC_INTR_DISABLE;
cauHinh.timer_sel = LEDC_TIMER_0; // Tám kênh dùng chung một bộ đếm nên luôn đồng bộ
cauHinh.duty = NUA_CHU_KY;
cauHinh.hpoint = dao ? hpoint - NUA_CHU_KY : hpoint;
cauHinh.flags.output_invert = dao ? 1 : 0;
ledc_channel_config(&cauHinh);
}
void laiBupSong(float gocDo) {
float vanTocAm = 331.3f + 0.606f * NHIET_DO_C; // m/s
float buocSongMm = vanTocAm * 1000.0f / TAN_SO_HZ;
// Kênh sau phải phát trễ hơn kênh trước một đoạn d x sin(góc) / bước sóng, tính theo số chu kỳ
float lechMoiKenh = KHOANG_CACH_MM * sinf(gocDo * DEG_TO_RAD) / buocSongMm;
Serial.printf("\nGoc lai %.1f do | buoc song %.2f mm | lech pha giua hai kenh ke nhau %.1f do\n",
gocDo, buocSongMm, lechMoiKenh * 360.0f);
Serial.println("Kenh GPIO Tre(us) Pha(do) hpoint");
for (int i = 0; i < SO_KENH; i++) {
float soChuKy = i * lechMoiKenh;
float phanLe = soChuKy - floorf(soChuKy); // Sóng phát liên tục nên chỉ cần phần lẻ của chu kỳ
uint32_t hpoint = (uint32_t)lroundf(phanLe * SO_BUOC) % SO_BUOC;
datKenh(i, hpoint);
Serial.printf("%4d %4d %7.2f %7.1f %6u\n",
i, CHAN_PHAT[i], phanLe * 1000000.0f / TAN_SO_HZ, phanLe * 360.0f, (unsigned)hpoint);
}
}
void setup() {
Serial.begin(115200);
ledc_timer_config_t boDem = {};
boDem.speed_mode = LEDC_HIGH_SPEED_MODE;
boDem.duty_resolution = LEDC_TIMER_10_BIT;
boDem.timer_num = LEDC_TIMER_0;
boDem.freq_hz = TAN_SO_HZ;
boDem.clk_cfg = LEDC_AUTO_CLK;
if (ledc_timer_config(&boDem) != ESP_OK) {
Serial.println("Khong cau hinh duoc bo dem LEDC");
while (true) {
delay(1000);
}
}
laiBupSong(0);
Serial.println("Go goc lai tu -60 den 60 roi nhan Enter");
}
void loop() {
if (!Serial.available()) {
return;
}
String dong = Serial.readStringUntil('\n');
dong.trim();
if (dong.length() == 0) {
return;
}
float gocDo = dong.toFloat();
if (gocDo < -GOC_TOI_DA || gocDo > GOC_TOI_DA) {
Serial.println("Goc phai tu -60 den 60");
return;
}
laiBupSong(gocDo);
}
9. Kết quả mong đợi
- Chương trình 1: mở Tools > Serial Monitor ở 115200 baud, chọn kiểu kết thúc dòng New Line. Gõ 60, 90, 180: giá trị đo lại bám sát giá trị đặt. Gõ 270: đo ra khoảng 90°.
- Chương trình 2: gõ 20. Bảng in ra độ trễ, pha và hpoint của tám kênh; hai kênh kề nhau lệch pha 143,2°.
- Siêu âm 40 kHz nằm ngoài ngưỡng nghe nên không nghe thấy gì khi mảng đang phát. Muốn xem trực tiếp dạng xung, dùng máy hiện sóng hai kênh hoặc bộ phân tích logic ở các chân GPIO. Búp sóng được đo ở phần 2.
10. Lưu ý khi thử nghiệm
- Chó, mèo và nhiều vật nuôi nghe được âm 40 kHz. Không để mảng phát liên tục gần vật nuôi và không áp sát mảng vào tai.
- GPIO16 và GPIO17 (RX2, TX2) không dùng được trên các module ESP32 có PSRAM (dòng WROVER). Khi đó đổi sang hai chân khác và sửa mảng
CHAN_PHAT. - Nhiệt độ phòng thay đổi 10 °C làm bước sóng đổi gần 2%. Sửa hằng
NHIET_DO_Ccho đúng nơi thử nghiệm. - Đầu phát siêu âm có dải tần hẹp quanh 40 kHz và dung sai giữa các đầu phát làm pha thực tế lệch vài độ so với tính toán. Sai lệch này làm búp sóng kém sắc nét hơn hình tính toán.
11. Xử lý lỗi thường gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
Giá trị do lai khác xa mong doi hoặc thay đổi lung tung | Chưa nối hai dây đo, hoặc nối nhầm chân | Nối D23 với D34 và D22 với D35 |
Giá trị do lai luôn gần 0 dù đặt góc nào | Hai dây đo cùng nối vào một kênh | Kiểm tra từng dây: D34 nối kênh trên D23, D35 nối kênh trên D22 |
In ra Khong cau hinh duoc bo dem LEDC | Đã đổi tần số hoặc độ phân giải sang tổ hợp không chia được từ xung nhịp 80 MHz | Ở 40 kHz dùng độ phân giải tối đa 10 bit; tần số cao hơn phải giảm số bit |
| Gõ số mà chương trình không phản hồi | Serial Monitor đặt kiểu kết thúc dòng là No Line Ending | Chọn New Line ở góc dưới Serial Monitor |
| Một kênh không có xung | Chân GPIO đó bị module dùng cho việc khác, hoặc dây hở | Đổi chân trong CHAN_PHAT; kiểm tra dây và mối hàn |
Lỗi biên dịch ở các hàm ledc_... | Gói esp32 quá cũ | Cập nhật gói esp32 trong Boards Manager |
12. Xem thêm
- PWM trên ESP32: điều chỉnh độ sáng LED, bài nền về bộ LEDC.
- Ngắt GPIO trên ESP32.
- Các mạch ESP32 tại PYWORLD: danh mục ESP32.
Xem thêm