Định lý lấy mẫu Nyquist: tần số lấy mẫu bao nhiêu là đủ, chồng phổ và thí nghiệm trên ESP32

Định lý lấy mẫu Nyquist giải thích bằng hình: tần số Nyquist, hiện tượng chồng phổ khi lấy mẫu thiếu, cách chọn tần số lấy mẫu theo ứng dụng và thí nghiệm ESP32 thấy sóng 200 Hz thành 50 Hz.

Mọi phép đo bằng vi điều khiển đều là lấy mẫu: ADC đọc tín hiệu ở từng thời điểm rời rạc, ví dụ 1000 lần mỗi giây. Đọc thưa quá thì mất thông tin, tệ hơn nữa là thấy một tín hiệu không có thật. Định lý lấy mẫu Nyquist (còn gọi Nyquist–Shannon) cho biết phải lấy mẫu nhanh tới đâu. Bài này giải thích định lý bằng hình, hiện tượng chồng phổ khi lấy mẫu thiếu, cách chọn tần số lấy mẫu cho từng loại tín hiệu, và một thí nghiệm trên ESP32 chỉ cần một sợi dây để thấy tận mắt sóng 200 Hz biến thành 50 Hz.

1. Định lý Nyquist nói gì

Một tín hiệu không có thành phần nào cao hơn tần số fmax có thể được ghi lại và dựng lại đầy đủ từ các mẫu, nếu tần số lấy mẫu fs lớn hơn hai lần fmax:

fs > 2 × fmax

Nửa tần số lấy mẫu, fs/2, gọi là tần số Nyquist: đó là tần số cao nhất mà dãy mẫu còn biểu diễn đúng. Ví dụ ADC đọc 1000 lần mỗi giây thì chỉ ghi đúng các thành phần dưới 500 Hz.

Hai điều kiện hay bị quên:

  • Lớn hơn hẳn, không phải bằng: lấy mẫu sóng 50 Hz ở đúng 100 Hz có thể toàn rơi vào điểm qua 0, đọc ra đường thẳng.
  • Điều kiện áp dụng cho mọi thành phần có trong tín hiệu đi vào ADC, kể cả nhiễu, hài, nhiễu điện lưới, chứ không chỉ phần mình quan tâm.

2. Lấy mẫu thiếu thì chuyện gì xảy ra: chồng phổ

Hai đồ thị: trên là sóng 200 Hz lấy mẫu 2000 Hz, các chấm vẽ lại đúng sóng; dưới là cùng sóng lấy mẫu 250 Hz, các chấm nằm trên một sóng 50 Hz vẽ nét đứt màu đỏ
Cùng sóng 200 Hz lấy mẫu ở 2000 Hz và ở 250 Hz (hình mô phỏng).

Khi lấy mẫu chậm hơn 2 lần tần số tín hiệu, các mẫu vẫn có giá trị hợp lệ, nhưng chúng khớp hoàn hảo với một sóng tần số thấp hơn. Chương trình không có cách nào biết dãy mẫu đến từ sóng nào. Hiện tượng này gọi là chồng phổ (aliasing). Ở hình dưới, sóng 200 Hz lấy mẫu 250 Hz cho ra dãy mẫu giống hệt sóng 50 Hz.

Tần số nhìn thấy sau khi lấy mẫu tính bằng: lấy tần số thật trừ đi bội số gần nhất của fs, rồi lấy trị tuyệt đối. Ví dụ 200 Hz, fs = 250 Hz: |200 − 250| = 50 Hz.

Đồ thị răng cưa: trục ngang là tần số thật 0 đến 2000 Hz, trục dọc là tần số nhìn thấy 0 đến 500 Hz khi lấy mẫu 1000 Hz; vùng xanh dưới 500 Hz là đúng; điểm 700 Hz thành 300 Hz, 1200 Hz và 1800 Hz thành 200 Hz
Mọi tần số trên fs/2 bị gấp xuống dải 0 đến fs/2.

Chồng phổ nguy hiểm vì kết quả trông hợp lý. Ví dụ hay gặp: đo rung một máy quay 3000 vòng/phút (50 Hz) bằng gia tốc kế đọc 60 lần mỗi giây, số liệu cho thấy dao động 10 Hz rất đều. Không có gì sai trong code, nhưng 10 Hz là tần số không có thật.

3. Chọn tần số lấy mẫu bao nhiêu là đủ

Bảng chọn tần số lấy mẫu: nhiệt độ 1 mẫu mỗi giây, lún nghiêng công trình 1 mẫu mỗi phút đến mỗi giờ, điện lưới từ 2 đến 5 kHz, rung động 2,56 kHz hoặc 5 đến 10 kHz, tim ECG 250 đến 500 Hz, giọng nói 8 đến 16 kHz, âm nhạc 44,1 hoặc 48 kHz
Gợi ý tần số lấy mẫu theo loại tín hiệu.

Cách chọn:

  1. Xác định thành phần tần số cao nhất cần giữ lại (không phải tần số cơ bản). Đo điện lưới 50 Hz mà cần biết hài bậc 20 thì fmax là 1 kHz.
  2. Nhân với hệ số: tối thiểu là 2 về lý thuyết. Phân tích phổ bằng FFT thường dùng 2,56 lần. Muốn nhìn được dạng sóng trên đồ thị (đỉnh nhọn, sườn dốc) thì cần 5–10 lần.
  3. Kiểm tra phần cứng làm được không: thời gian đọc ADC, thời gian xử lý, dung lượng lưu, băng thông gửi lên mạng.

Ngược lại, tín hiệu chậm không cần lấy mẫu nhanh. Nhiệt độ bồn nước đọc mỗi giây đã dư; đọc 1000 lần mỗi giây chỉ tốn điện, tốn bộ nhớ. Có thể đọc nhanh rồi lấy trung bình để giảm nhiễu, nhưng chỉ lưu kết quả mỗi giây.

4. Chống chồng phổ: lọc trước khi lấy mẫu

Vì nhiễu cũng bị gấp xuống, cách đúng là lọc thông thấp trước ADC để cắt mọi thành phần trên fs/2, gọi là bộ lọc chống chồng phổ (anti-aliasing filter). Mạch đơn giản nhất là một điện trở nối tiếp và một tụ xuống GND (lọc RC), tần số cắt fc = 1 / (2π × R × C). Ví dụ R = 10 kΩ, C = 100 nF cho fc ≈ 159 Hz, hợp với lấy mẫu 1–2 kHz.

Lọc số trong phần mềm (trung bình trượt, EMA) chỉ làm mượt dãy mẫu đã có; nó không sửa được chồng phổ, vì thành phần sai đã nằm sẵn trong dãy mẫu. Bộ lọc chống chồng phổ chi tiết và cách lấy mẫu dư rồi lọc số có trong bài về aliasing cùng cụm chủ đề.

5. Thí nghiệm trên ESP32: thấy chồng phổ bằng một sợi dây

ESP32 có bộ tạo sóng cos bên trong, xuất ra chân DAC D25. Chương trình tạo sóng 200 Hz ở D25, rồi dùng ADC ở D34 lấy mẫu với sáu tần số từ 2000 Hz xuống 210 Hz, đếm số lần dãy mẫu cắt qua giá trị trung bình để ước tính tần số nhìn thấy, và in kèm tần số tính theo lý thuyết.

ESP32 DevKit V1 với một dây nối từ chân D25 là DAC kênh 0 sang chân D34 là ADC1 cùng phía trái mạch
Chỉ cần một dây cắm từ D25 sang D34 (hình minh họa).

Chuẩn bị ESP32 DevKit V1 và một dây cắm cái–cái. Bộ tạo sóng cos chỉ có trên ESP32 bản gốc (không có trên ESP32-S3, C3).

// PYWORLD.VN - Thí nghiệm định lý lấy mẫu Nyquist trên ESP32: DAC tạo sóng 200 Hz, ADC lấy mẫu ở nhiều tần số khác nhau
// PYWORLD.VN - Nối một dây từ D25 (DAC kênh 0, bộ tạo sóng cos trong chip) sang D34 (ADC1); không cần linh kiện khác
// PYWORLD.VN - Với mỗi tần số lấy mẫu fs, đếm số lần cắt qua giá trị trung bình để ước tính tần số "nhìn thấy" được

#include "driver/dac_cosine.h"

const int CHAN_ADC = 34;
const uint32_t F_TIN_HZ = 200;                  // tần số sóng tạo ra
const uint32_t DS_FS[] = {2000, 1000, 450, 300, 250, 210};
const int SO_MAU = 256;
const int TRE_MV = 40;                          // vùng trễ khi đếm cắt, chống nhiễu

int mau[SO_MAU];

float tanSoChongPho(float f, float fs) {        // tần số sau lấy mẫu theo lý thuyết, trong khoảng 0..fs/2
  float n = roundf(f / fs);
  return fabsf(f - n * fs);
}

float uocTinhTanSo(uint32_t fs) {
  uint32_t chuKyUs = 1000000UL / fs;
  uint32_t moc = micros();
  for (int i = 0; i < SO_MAU; i++) {
    while ((uint32_t)(micros() - moc) < chuKyUs) {
    }
    moc += chuKyUs;
    mau[i] = analogReadMilliVolts(CHAN_ADC);
  }
  long tong = 0;
  for (int i = 0; i < SO_MAU; i++) {
    tong += mau[i];
  }
  int tb = tong / SO_MAU;
  int lanCat = 0;
  bool tren = mau[0] > tb;
  for (int i = 1; i < SO_MAU; i++) {
    if (!tren && mau[i] > tb + TRE_MV) {
      tren = true;
      lanCat++;
    } else if (tren && mau[i] < tb - TRE_MV) {
      tren = false;
      lanCat++;
    }
  }
  float thoiGianS = (float)SO_MAU / fs;
  return lanCat / 2.0f / thoiGianS;            // hai lần cắt = một chu kỳ
}

void setup() {
  Serial.begin(115200);
  analogSetPinAttenuation(CHAN_ADC, ADC_11db);

  dac_cosine_handle_t kenh;
  dac_cosine_config_t cauHinh = {};
  cauHinh.chan_id = DAC_CHAN_0;                 // GPIO25
  cauHinh.freq_hz = F_TIN_HZ;
  cauHinh.clk_src = DAC_COSINE_CLK_SRC_DEFAULT;
  cauHinh.atten = DAC_COSINE_ATTEN_DB_6;        // nửa biên độ, nằm gọn trong dải đo của ADC
  cauHinh.phase = DAC_COSINE_PHASE_0;
  cauHinh.offset = 0;
  ESP_ERROR_CHECK(dac_cosine_new_channel(&cauHinh, &kenh));
  ESP_ERROR_CHECK(dac_cosine_start(kenh));
  delay(100);
  Serial.printf("Song vao: %u Hz tren D25, noi sang D34\n", (unsigned)F_TIN_HZ);
}

void loop() {
  for (uint32_t fs : DS_FS) {
    float doDuoc = uocTinhTanSo(fs);
    float lyThuyet = tanSoChongPho(F_TIN_HZ, fs);
    Serial.printf("fs = %4u Hz | fs/2 = %4u Hz | do duoc ~%6.1f Hz | ly thuyet %6.1f Hz %s\n",
                  (unsigned)fs, (unsigned)(fs / 2), doDuoc, lyThuyet,
                  fs > 2 * F_TIN_HZ ? "" : "<- chong pho");
  }
  Serial.println();
  delay(5000);
}

Giải thích:

  • dac_cosine_new_channel() và dac_cosine_start() là hàm của ESP-IDF có sẵn trong gói esp32; bộ tạo sóng chạy bằng phần cứng, không chiếm CPU. Tần số thấp nhất của nó khoảng 130 Hz, nên thí nghiệm dùng 200 Hz.
  • Hàm uocTinhTanSo() chờ đúng chu kỳ bằng micros() rồi đọc ADC, lấy 256 mẫu. Vùng trễ 40 mV khi đếm cắt giúp nhiễu ADC không làm đếm thừa.
  • tanSoChongPho() tính tần số nhìn thấy theo công thức ở mục 2 để so với số đo.
Serial Monitor: với fs 2000, 1000, 450 Hz đo được khoảng 199 Hz; với fs 300 Hz đo được 99,6 Hz, 250 Hz đo được 49,8 Hz, 210 Hz đo được 9,8 Hz, đều có chú thích chồng phổ
Kết quả mong đợi (số "do duoc" mô phỏng theo thuật toán của chương trình).

Ba dòng đầu, tần số lấy mẫu lớn hơn 400 Hz, đo đúng khoảng 200 Hz. Từ 300 Hz trở xuống, cùng một sóng 200 Hz được "nhìn thấy" là 100 Hz, 50 Hz rồi 10 Hz. Trên mạch thật, số đo lệch thêm vài Hz vì bộ tạo sóng cos không cho đúng 200 Hz và vòng chờ micros() có sai số nhỏ.

6. Những nhầm lẫn hay gặp

Nhầm lẫnThực tế
Lấy mẫu đúng 2 lần là đủPhải lớn hơn 2 lần, thực tế 2,5–10 lần
Chỉ cần quan tâm tần số của tín hiệu chínhNhiễu, hài, nhiễu điện lưới cũng bị gấp xuống
Lọc trung bình trong code sẽ sửa được chồng phổKhông; phải lọc tương tự trước ADC hoặc lấy mẫu nhanh hơn
delay(1) trong vòng lặp là lấy mẫu 1000 HzCộng thêm thời gian đọc ADC, gửi Serial; dùng timer hoặc mốc micros()
Càng lấy mẫu nhanh càng chính xácVới tín hiệu chậm chỉ tốn điện, bộ nhớ; độ chính xác do ADC và mạch đo quyết định

7. Xem thêm


Tin tức liên quan

ESP32 không nhận cổng COM, lỗi Failed to connect, phải giữ nút BOOT: nguyên nhân và cách sửa
ESP32 không nhận cổng COM, lỗi Failed to connect, phải giữ nút BOOT: nguyên nhân và cách sửa

Cách sửa ESP32 không nhận cổng COM, lỗi Failed to connect, Wrong boot mode, phải giữ nút BOOT, nạp xong không chạy: dây USB, driver, chân khởi động, tụ 10 µF ở chân EN, bảng tra thông báo lỗi.

CAT III là gì: cấp an toàn CAT II, CAT III, CAT IV của đồng hồ vạn năng và cách chọn
CAT III là gì: cấp an toàn CAT II, CAT III, CAT IV của đồng hồ vạn năng và cách chọn

CAT III đồng hồ vạn năng là gì, các cấp đo lường CAT O, II, III, IV theo vị trí trong hệ thống điện, điện áp xung phải chịu được, cách đọc ký hiệu và chọn đồng hồ theo công việc.

Dự án tổng hợp: đèn tự bật khi trời tối với quang trở, MOSFET và ESP32
Dự án tổng hợp: đèn tự bật khi trời tối với quang trở, MOSFET và ESP32

Dự án cuối loạt điện tử căn bản: quang trở trong cầu phân áp, lọc nhiễu, ngưỡng có trễ và chờ xác nhận, MOSFET điều khiển dải LED 12 V sáng dần bằng PWM, nguồn 12 V qua mạch hạ áp cho ESP32.

ADC 24 bit HX711, ADS1232, ADS1220, ADS1256: so sánh nhiễu, tốc độ và chọn loại nào
ADC 24 bit HX711, ADS1232, ADS1220, ADS1256: so sánh nhiễu, tốc độ và chọn loại nào

So sánh ADC 24 bit HX711, ADS1232, ADS1220, ADS1256 theo nhiễu datasheet, gain, tốc độ, giao tiếp; tính độ phân giải trên load cell, chọn theo ứng dụng, code ESP32 đo nhiễu HX711.

Logic analyzer 8 kênh và PulseView: bắt và giải mã UART, I2C, SPI, PWM của ESP32
Logic analyzer 8 kênh và PulseView: bắt và giải mã UART, I2C, SPI, PWM của ESP32

Dùng logic analyzer 8 kênh giá rẻ với PulseView: nối dây với ESP32, cài driver bằng Zadig, bắt tín hiệu bằng kích hoạt, giải mã UART, I2C, SPI, đo PWM và chọn tốc độ lấy mẫu.

Arduino, ESP32, STM32 hay Raspberry Pi: chọn nền tảng nào cho dự án, so sánh và gợi ý theo việc
Arduino, ESP32, STM32 hay Raspberry Pi: chọn nền tảng nào cho dự án, so sánh và gợi ý theo việc

Vi điều khiển và máy tính nhúng khác nhau thế nào; so sánh Arduino Nano, ESP32, STM32, Raspberry Pi Pico và Raspberry Pi 5; bốn câu hỏi để chọn nhanh, gợi ý theo dự án và cách dùng kết hợp.

Encoder tuyệt đối: cấu tạo, mã Gray, đơn vòng, đa vòng, cách chế tạo và đọc AS5600 bằng ESP32
Encoder tuyệt đối: cấu tạo, mã Gray, đơn vòng, đa vòng, cách chế tạo và đọc AS5600 bằng ESP32

Encoder tuyệt đối hoạt động thế nào: đĩa mã nhị phân và mã Gray, đơn vòng, đa vòng, giao tiếp SSI, encoder từ tính, cách chế tạo và đọc cảm biến góc AS5600 bằng ESP32.

Số nhị phân cho trẻ: đếm bằng 5 thẻ chấm, giải mật thư như máy tính
Số nhị phân cho trẻ: đếm bằng 5 thẻ chấm, giải mật thư như máy tính

Dạy trẻ số nhị phân bằng 5 thẻ chấm 1, 2, 4, 8, 16: viết số, đếm và tìm quy luật, giải mật thư chữ cái, liên hệ với bit, màn hình và mạch ESP32. Hoạt động không cần máy tính.


Bình luận
Đã thêm vào giỏ hàng