Phased array siêu âm 40 kHz với ESP32 (phần 2): mạch đệm 12 V, đầu thu và quét búp sóng
- 1. Hệ thống gồm những gì?
- 2. Chuẩn bị
- 3. Mạch đệm 12 V với hai module L298N
- 4. Mạch thu: đầu thu và LM358
- 5. Đo biên độ 40 kHz bằng ADC chậm
- 6. Chương trình 1: đo biên độ, đổi góc bằng tay
- 7. Bố trí đo
- 8. Chương trình 2: tự quét góc lái
- 9. Kết quả mong đợi
- 10. Thử nghiệm thêm
- 11. Lưu ý an toàn
- 12. Xử lý lỗi thường gặp
- 13. Xem thêm
Phần 2 của loạt phased array siêu âm: thêm mạch đệm 12 V bằng hai module L298N, lắp mạch thu dùng LM358, đo biên độ 40 kHz bằng ADC của ESP32 và chạy chương trình tự quét góc lái để xem búp sóng.
Phần 1 đã dùng ESP32 tạo tám kênh xung 40 kHz lệch pha cho một mảng tám đầu phát siêu âm. Phần 2 hoàn thiện hệ thống để nhìn thấy kết quả: thêm mạch đệm 12 V cho mảng phát, lắp một mạch thu đơn giản và để chính ESP32 quét góc lái, đo biên độ ở từng góc rồi in ra giản đồ hướng của mảng.
Bài dùng lại mảng tám đầu phát, thứ tự chân và cách tính pha của phần 1. Nếu chưa lắp mảng phát, nên làm phần 1 trước.
1. Hệ thống gồm những gì?
- Phát: ESP32 tạo tám kênh xung 3,3 V; hai module L298N đệm lên 12 V để đầu phát phát mạnh hơn.
- Thu: một đầu thu siêu âm 40 kHz đặt cách mảng 1,5 đến 2 m. Tín hiệu thu chỉ cỡ vài chục mV nên cần một tầng khuếch đại dùng LM358.
- Đo: ngõ ra mạch khuếch đại nối vào chân ADC D34. ESP32 đổi góc lái từng bước và đo biên độ ở mỗi bước.
2. Chuẩn bị
- Mảng tám đầu phát và mạch ESP32 DevKit V1 30 chân của phần 1.
- Hai module L298N (loại có cọc vít 12V, GND, 5V và hàng chân IN1 đến IN4).
- Nguồn 12 V một chiều, từ 0,5 A. Có thể tạo 12 V từ một nguồn 5 V riêng bằng module tăng áp XL6009E1; không lấy nguồn cho mạch đệm từ chân VIN của ESP32.
- Một đầu thu siêu âm 40 kHz (trên thân thường in chữ R).
- Một IC LM358, điện trở 10 kΩ (3 chiếc), 100 kΩ (2 chiếc), 4,7 kΩ (1 chiếc), tụ 10 µF và tụ 100 nF.
- Breadboard và dây cắm.
3. Mạch đệm 12 V với hai module L298N
- L298N vốn là mạch điều khiển động cơ có bốn ngõ ra. Ở đây mỗi ngõ ra dùng như một tầng đệm độc lập: ngõ vào ở mức cao thì ngõ ra lên gần bằng điện áp nguồn, ở mức thấp thì ngõ ra xuống gần 0 V.
- Ngõ vào của L298N nhận được mức 3,3 V của ESP32, nên nối thẳng tám chân GPIO vào IN1 đến IN4 của hai module.
- Mỗi đầu phát nối giữa một ngõ ra OUT và GND, cùng chiều cực tính như ở phần 1.
- Giữ nguyên hai jumper ENA, ENB (cho phép ngõ ra) và jumper 5V-EN (module tự tạo 5 V cho phần logic).
- GND của nguồn 12 V, của hai module và của ESP32 phải nối chung. Không nối cọc 5V của module vào ESP32 khi ESP32 đang cắm USB.
- Mạch đệm làm mọi kênh trễ thêm một khoảng nhỏ. Hai module cùng loại nên các kênh trễ gần như nhau, độ lệch pha giữa các kênh hầu như giữ nguyên.
Do sụt áp bên trong L298N, xung trên đầu phát có biên độ khoảng 10 V. Phần lớn đầu phát 40 kHz chịu được mức này; nên xem thông số của loại đang dùng và không cấp nguồn cao hơn 12 V.
4. Mạch thu: đầu thu và LM358
- Điểm 1,65 V: R1, R2 chia đôi điện áp 3,3 V, tụ C1 giữ cho điểm này ổn định. Tín hiệu sau khuếch đại dao động quanh mức 1,65 V, nằm giữa dải đo của ADC.
- Khuếch đại: hệ số danh định là 1 + R4/R5, khoảng 22 lần. Ở 40 kHz hệ số thực tế thấp hơn vì băng thông của LM358 có hạn. Tụ C2 làm cho mạch chỉ khuếch đại phần xoay chiều.
- Nguồn: LM358 lấy 5 V từ chân VIN của ESP32 (chân 8) và GND (chân 4).
- Bảo vệ: R6 hạn dòng vào chân D34 trong trường hợp ngõ ra LM358 vượt quá 3,3 V.
- Nửa còn lại của LM358 không dùng: nối chân 6 với chân 7, chân 5 nối vào điểm 1,65 V.
- Lắp mạch thu trên một breadboard riêng, dây nối đầu thu thật ngắn và đặt xa mạch đệm để xung 12 V không lọt thẳng vào mạch thu.
5. Đo biên độ 40 kHz bằng ADC chậm
- Một chu kỳ sóng 40 kHz chỉ dài 25 µs, trong khi mỗi lần đọc ADC bằng
analogReadMilliVolts()mất nhiều thời gian hơn thế. ESP32 không thể lấy đủ mẫu để vẽ lại từng chu kỳ. - Tuy vậy, sóng lặp lại đều đặn. Nếu lấy vài trăm mẫu ở các thời điểm ngẫu nhiên, các mẫu sẽ rơi đều vào mọi vị trí của chu kỳ.
- Độ lệch chuẩn của các mẫu chính là giá trị hiệu dụng của phần xoay chiều; nhân với 1,414 ra biên độ đỉnh của sóng sin.
- Chương trình chèn một khoảng trễ ngẫu nhiên giữa hai lần đọc để các mẫu không bị rơi mãi vào cùng một vị trí của chu kỳ.
- Lúc khởi động, khi mảng chưa phát, chương trình đo nhiễu nền của mạch thu rồi trừ đi trong các phép đo sau.
Kết quả là một con số tỉ lệ với áp suất âm tại đầu thu. Con số này dùng để so sánh giữa các góc lái, không phải phép đo áp suất âm tuyệt đối. Kiến thức nền về ADC có trong bài đo điện áp bằng ADC của ESP32.
6. Chương trình 1: đo biên độ, đổi góc bằng tay
Chương trình phát ở góc lái 0° và in biên độ bốn lần mỗi giây. Dùng chương trình này để kiểm tra mạch thu và căn chỉnh vị trí đầu thu trước khi quét.
/*
PYWORLD.VN - Phased array siêu âm 40 kHz: đo biên độ tại đầu thu khi đổi góc lái bằng tay
ESP32 phát 8 kênh lệch pha qua mạch đệm; đầu thu siêu âm qua mạch khuếch đại nối vào D34
In "bien_do_mV:..,goc:.." bốn lần mỗi giây để xem bằng Serial Monitor hoặc Serial Plotter
*/
#include <driver/ledc.h>
#define CHAN_THU 34 // Ngõ ra mạch khuếch đại của đầu thu
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;
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
const float NHIET_DO_C = 30.0;
const float GOC_TOI_DA = 60.0;
float nenMv = 0; // Nhiễu nền đo lúc chưa phát
float gocHienTai = 0;
void datKenh(int kenh, uint32_t hpoint) {
bool dao = hpoint >= NUA_CHU_KY; // Nửa chu kỳ sau: lùi nửa chu kỳ rồi đảo tín hiệu ra
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;
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;
float buocSongMm = vanTocAm * 1000.0f / TAN_SO_HZ;
float lechMoiKenh = KHOANG_CACH_MM * sinf(gocDo * DEG_TO_RAD) / buocSongMm;
for (int i = 0; i < SO_KENH; i++) {
float soChuKy = i * lechMoiKenh;
float phanLe = soChuKy - floorf(soChuKy);
datKenh(i, (uint32_t)lroundf(phanLe * SO_BUOC) % SO_BUOC);
}
gocHienTai = gocDo;
}
// Đo giá trị hiệu dụng của phần xoay chiều tại chân thu, đơn vị mV.
// analogReadMilliVolts chậm hơn chu kỳ 25 micro giây của sóng, nhưng sóng lặp lại đều nên các mẫu
// lấy ở thời điểm ngẫu nhiên vẫn trải đều trên một chu kỳ: độ lệch chuẩn của chúng là giá trị hiệu dụng.
float doHieuDungMv(float *mucMotChieu) {
const int SO_MAU = 500;
double tong = 0, tongBinhPhuong = 0;
for (int i = 0; i < SO_MAU; i++) {
double mv = analogReadMilliVolts(CHAN_THU);
tong += mv;
tongBinhPhuong += mv * mv;
delayMicroseconds(random(0, 30));
}
double trungBinh = tong / SO_MAU;
double phuongSai = tongBinhPhuong / SO_MAU - trungBinh * trungBinh;
if (mucMotChieu != NULL) {
*mucMotChieu = trungBinh;
}
return phuongSai > 0 ? sqrt(phuongSai) : 0;
}
// Biên độ đỉnh của sóng 40 kHz sau khi trừ nhiễu nền
float doBienDoMv() {
float hieuDung = doHieuDungMv(NULL);
float daTruNen = hieuDung * hieuDung - nenMv * nenMv;
return daTruNen > 0 ? sqrtf(daTruNen) * 1.4142f : 0;
}
void setup() {
Serial.begin(115200);
analogSetPinAttenuation(CHAN_THU, ADC_11db);
// Đo nhiễu nền trước khi phát: lúc này tám kênh chưa có xung
delay(500);
float mucMotChieu = 0;
nenMv = doHieuDungMv(&mucMotChieu);
Serial.printf("\nMuc mot chieu %.0f mV, nhieu nen %.1f mV\n", mucMotChieu, nenMv);
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()) {
String dong = Serial.readStringUntil('\n');
dong.trim();
if (dong.length() > 0) {
float gocDo = dong.toFloat();
if (gocDo >= -GOC_TOI_DA && gocDo <= GOC_TOI_DA) {
laiBupSong(gocDo);
} else {
Serial.println("Goc phai tu -60 den 60");
}
}
}
Serial.printf("bien_do_mV:%.1f,goc:%.1f\n", doBienDoMv(), gocHienTai);
delay(250);
}
- Dòng đầu tiên cho biết mạch thu có ổn không:
Muc mot chieunên ở quanh 1650 mV,nhieu nenchỉ nên vài mV. - Xoay nhẹ đầu thu và mảng phát đến khi biên độ lớn nhất.
- Gõ 10 rồi Enter: búp sóng lệch khỏi đầu thu, biên độ giảm mạnh. Gõ 0 để quay lại.
- Nên giữ biên độ lớn nhất dưới khoảng 800 mV. LM358 không cho ra được sóng 40 kHz lớn hơn nhiều mức đó mà không méo; nếu vượt, đặt đầu thu xa hơn hoặc giảm R4 xuống 47 kΩ.
7. Bố trí đo
- Khoảng cách: công thức lái búp sóng giả định đầu thu ở đủ xa để sóng từ tám đầu phát gần như song song. Với mảng dài khoảng 8 cm ở 40 kHz, khoảng cách đó là từ 1,5 m trở lên.
- Độ cao: tâm đầu thu ngang với tâm hàng đầu phát, cả hai cao hơn mặt bàn ít nhất 20 cm.
- Phản xạ: hướng mảng ra khoảng trống, tránh tường và vật cứng ở gần đường truyền. Phủ khăn hoặc mút lên mặt bàn giữa mảng và đầu thu.
- Đứng yên khi đo: người di chuyển gần đường truyền và gió quạt đều làm biên độ thay đổi.
8. Chương trình 2: tự quét góc lái
Mỗi lượt quét, chương trình lái búp sóng từ -40° đến 40° theo bước 2°, chờ 30 ms cho sóng ổn định rồi đo biên độ. Kết quả in thành bảng 41 dòng, mỗi dòng kèm một thanh ngang tỉ lệ với biên độ.
/*
PYWORLD.VN - Phased array siêu âm 40 kHz: tự quét góc lái và vẽ giản đồ hướng trên Serial Monitor
Mỗi lượt quét từ -40 đến 40 độ, đo biên độ tại đầu thu (D34) ở từng góc rồi in bảng kèm thanh ngang
Nhấn Enter trong Serial Monitor để quét lượt mới
*/
#include <driver/ledc.h>
#define CHAN_THU 34 // Ngõ ra mạch khuếch đại của đầu thu
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;
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
const float NHIET_DO_C = 30.0;
const int GOC_QUET = 40; // Quét từ -GOC_QUET đến +GOC_QUET
const int BUOC_GOC = 2;
const int THOI_GIAN_ON_DINH_MS = 30; // Chờ sóng mới truyền tới đầu thu và ổn định
const int DO_DAI_THANH = 40; // Số ký tự của thanh dài nhất
const int SO_DIEM = 2 * GOC_QUET / BUOC_GOC + 1;
float bienDo[SO_DIEM];
float nenMv = 0;
void datKenh(int kenh, uint32_t hpoint) {
bool dao = hpoint >= NUA_CHU_KY; // Nửa chu kỳ sau: lùi nửa chu kỳ rồi đảo tín hiệu ra
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;
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;
float buocSongMm = vanTocAm * 1000.0f / TAN_SO_HZ;
float lechMoiKenh = KHOANG_CACH_MM * sinf(gocDo * DEG_TO_RAD) / buocSongMm;
for (int i = 0; i < SO_KENH; i++) {
float soChuKy = i * lechMoiKenh;
float phanLe = soChuKy - floorf(soChuKy);
datKenh(i, (uint32_t)lroundf(phanLe * SO_BUOC) % SO_BUOC);
}
}
// Giá trị hiệu dụng của phần xoay chiều tại chân thu (mV): độ lệch chuẩn của các mẫu lấy ở thời điểm ngẫu nhiên
float doHieuDungMv() {
const int SO_MAU = 500;
double tong = 0, tongBinhPhuong = 0;
for (int i = 0; i < SO_MAU; i++) {
double mv = analogReadMilliVolts(CHAN_THU);
tong += mv;
tongBinhPhuong += mv * mv;
delayMicroseconds(random(0, 30));
}
double trungBinh = tong / SO_MAU;
double phuongSai = tongBinhPhuong / SO_MAU - trungBinh * trungBinh;
return phuongSai > 0 ? sqrt(phuongSai) : 0;
}
// Biên độ đỉnh của sóng 40 kHz sau khi trừ nhiễu nền
float doBienDoMv() {
float hieuDung = doHieuDungMv();
float daTruNen = hieuDung * hieuDung - nenMv * nenMv;
return daTruNen > 0 ? sqrtf(daTruNen) * 1.4142f : 0;
}
void quetMotLuot() {
float lonNhat = 0;
int gocLonNhat = 0;
for (int i = 0; i < SO_DIEM; i++) {
int gocDo = -GOC_QUET + i * BUOC_GOC;
laiBupSong(gocDo);
delay(THOI_GIAN_ON_DINH_MS);
bienDo[i] = doBienDoMv();
if (bienDo[i] > lonNhat) {
lonNhat = bienDo[i];
gocLonNhat = gocDo;
}
}
laiBupSong(0);
Serial.println("\nGoc(do) Bien do(mV)");
for (int i = 0; i < SO_DIEM; i++) {
int soKyTu = lonNhat > 0 ? (int)lroundf(bienDo[i] / lonNhat * DO_DAI_THANH) : 0;
Serial.printf("%6d %10.1f ", -GOC_QUET + i * BUOC_GOC, bienDo[i]);
for (int k = 0; k < soKyTu; k++) {
Serial.print('#');
}
Serial.println();
}
Serial.printf("Lon nhat %.1f mV tai goc lai %d do\n", lonNhat, gocLonNhat);
Serial.println("Nhan Enter de quet lai");
}
void setup() {
Serial.begin(115200);
analogSetPinAttenuation(CHAN_THU, ADC_11db);
// Đo nhiễu nền trước khi phát: lúc này tám kênh chưa có xung
delay(500);
nenMv = doHieuDungMv();
Serial.printf("\nNhieu nen %.1f mV\n", nenMv);
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);
}
}
quetMotLuot();
}
void loop() {
if (!Serial.available()) {
return;
}
while (Serial.available()) {
Serial.read();
delay(2);
}
quetMotLuot();
}
9. Kết quả mong đợi
- Đầu thu ở chính diện (0°): bảng có một đỉnh rõ ở góc lái 0°. Biên độ giảm nhanh khi lái lệch vài độ, rồi lên xuống nhẹ theo các búp phụ.
- Dời đầu thu sang 20°: quét lại, đỉnh dời theo tới khoảng 20°. Đây là bằng chứng búp sóng được lái bằng pha: mảng phát không hề xoay.
- Đỉnh thứ hai: khi đầu thu ở 20°, bảng còn một đỉnh nữa ở khoảng -31°. Đó là lúc búp lặp của mảng quét qua đầu thu, đúng như tính toán ở phần 1.
- Vì đầu thu đứng yên trong suốt lượt quét, hướng tính riêng của đầu thu và của từng đầu phát không làm đổi hình dạng đường cong.
Số đo thật không đẹp như hình tính toán: đỉnh thấp hơn, đáy không về 0 và vị trí đỉnh có thể lệch vài độ. Nguyên nhân là phản xạ trong phòng, dung sai giữa các đầu phát và khoảng cách đo hữu hạn.
10. Thử nghiệm thêm
- Đổi
BUOC_GOCthành 1 để thấy rõ hơn bề rộng búp chính, khoảng 5,5° ở mức nửa công suất. - Đổi
SO_KENHthành 4, bớt bốn chân cuối trongCHAN_PHATvà rút dây bốn kênh đó: búp chính rộng ra gấp đôi. - Đảo cực một đầu phát rồi quét lại để thấy búp sóng xấu đi thế nào.
- Đặt đầu thu lần lượt ở 10°, 20°, 30° và ghi lại góc lái có biên độ lớn nhất để so với góc đặt thật.
11. Lưu ý an toàn
- Với mạch đệm 12 V, mảng phát mạnh hơn nhiều so với phần 1. Không áp sát mảng vào tai và không để mảng phát liên tục gần chó, mèo, vật nuôi.
- Ngắt nguồn 12 V trước khi sửa dây. L298N ấm lên khi chạy là bình thường; nếu nóng nhanh, kiểm tra chạm chập ở ngõ ra.
- Chân D34 chỉ chịu được điện áp trong khoảng 0 đến 3,3 V. Không bỏ điện trở R6 và không nối ngõ ra mạch đệm vào ESP32.
12. Xử lý lỗi thường gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
Muc mot chieu khác xa 1650 mV | Sai cầu phân áp R1, R2; chưa nối 3V3; LM358 cắm ngược hoặc chưa có nguồn | Đo điểm 1,65 V bằng đồng hồ; kiểm tra chân 8 và chân 4 của LM358 |
nhieu nen lớn (hàng chục mV) | Dây mạch thu dài, thiếu tụ C1, mạch thu đặt gần nguồn tăng áp | Rút ngắn dây, kiểm tra C1, đặt mạch thu xa nguồn và mạch đệm |
| Biên độ gần 0 ở mọi góc | Mạch đệm chưa có 12 V, jumper ENA hoặc ENB bị tháo, chưa nối GND chung, đầu thu nối sai | Kiểm tra nguồn 12 V, jumper, dây GND và hai chân đầu thu |
| Biên độ gần như không đổi khi lái | Xung 12 V lọt thẳng vào mạch thu qua dây dẫn, không qua không khí | Lấy tay che kín đầu thu: nếu biên độ không giảm thì là nhiễu điện; tách xa hai mạch, rút ngắn dây |
| Đầu thu ở chính diện nhưng đỉnh không ở 0° | Một đầu phát ngược cực, sai thứ tự kênh, hoặc KHOANG_CACH_MM không đúng thực tế | Thử từng kênh riêng; đối chiếu thứ tự dây với phần 1; đo lại khoảng cách tâm đầu phát |
| Có hai đỉnh cao gần bằng nhau | Đầu thu đang ở gần ±25°, nơi búp chính và búp lặp đối xứng nhau | Bình thường với mảng này; đặt đầu thu trong khoảng ±20° để có một đỉnh trội |
| Hai lượt quét cho kết quả khác nhau nhiều | Có người di chuyển, gió quạt, hoặc phản xạ mạnh từ vật ở gần | Giữ yên khi đo, tắt quạt, dọn vật cứng gần đường truyền |
13. Xem thêm
- ADC trên ESP32: đo điện áp.
- PWM trên ESP32: điều chỉnh độ sáng LED, bài nền về bộ LEDC.
- Các mạch ESP32 tại PYWORLD: danh mục ESP32.
Xem thêm