Bỏ qua, đến nội dung chính
Bài viếtFirmwareMới

STM32 Beginner Guide P4: ADC, PWM và DMA từ analog đến dữ liệu liên tục

Học cách đọc tín hiệu analog bằng ADC, điều khiển độ sáng LED bằng PWM và chuyển nhiều mẫu dữ liệu qua DMA trên STM32CubeIDE với HAL.

Chia sẻ

LinkedInFacebookX
STM32 Nucleo board with analog input, PWM LED and DMA test hardware on an electronics workbench

P3: UART, I2C và SPI, mình đã dùng các giao tiếp tuần tự để nói chuyện với laptop, sensor và display. P4 chuyển sang ba peripheral giúp firmware làm việc với tín hiệu analog, công suất điều khiển và luồng dữ liệu lớn hơn.

Đi trong series: P1: Bắt đầu với STM32 · P2: GPIO, Interrupt và Timer · P3: UART, I2C và SPI · P4 đang đọc

Mình sẽ đi theo một ví dụ tăng dần độ khó:

  • Đọc biến trở bằng ADC.
  • Dùng giá trị đó để thay đổi độ sáng LED bằng PWM.
  • Lấy nhiều mẫu ADC tự động vào buffer bằng DMA.

Các ví dụ vẫn dùng NUCLEO-G071RB, STM32CubeIDE và HAL. ST có các example ADC, timer/PWM và DMA trong STM32CubeG0; mình sẽ giữ code ở mức dễ đo và dễ debug trước khi tối ưu.

ADC: biến điện áp thành dữ liệu số

Board STM32 Nucleo nối biến trở và mạch analog trên breadboard để kiểm tra ADC.

ADC biến điện áp analog thành con số; nguồn tham chiếu, trở kháng nguồn và nhiễu đều ảnh hưởng đến kết quả.

ADC, hay Analog-to-Digital Converter, lấy một điện áp analog rồi biểu diễn nó bằng số nguyên. Một ADC 12-bit có 2^12 = 4096 mức, từ 0 đến 4095.

Nếu giả sử điện áp tham chiếu là 3,3 V, công thức lý tưởng là:

voltage = adc_value × 3.3 / 4095

Nhưng đây chỉ là mô hình lý tưởng. Điện áp tham chiếu thật, độ tuyến tính của ADC, nhiễu nguồn, trở kháng nguồn và thời gian sampling đều ảnh hưởng đến kết quả.

Nối biến trở

Với biến trở 10 kΩ, nối hai đầu ngoài vào 3V3GND, chân giữa vào một chân ADC:

3V3 ---- biến trở ---- GND
             |
             +-------- ADC input

Không đưa điện áp cao hơn giới hạn của chân ADC vào input. Nếu nguồn analog có thể vượt 3,3 V, hãy dùng mạch chia áp và tính lại dải đo.

Trong CubeMX, chọn một chân có chức năng ADC, bật channel tương ứng và bắt đầu với độ phân giải mặc định. Đừng chọn chân chỉ vì tên trên header trông giống A0; hãy kiểm tra alternate function của đúng MCU và board.

Đọc ADC bằng polling

Giả sử CubeMX sinh ra hadc1 và channel đã được cấu hình:

uint32_t adc_value = 0;

HAL_ADC_Start(&hadc1);

if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK)
{
    adc_value = HAL_ADC_GetValue(&hadc1);
}

HAL_ADC_Stop(&hadc1);

Đây là cách dễ hiểu nhất cho lần đọc đầu tiên. HAL_ADC_PollForConversion() chờ conversion hoàn thành, nên không nên gọi trong một vòng lặp với tốc độ quá cao mà không có kế hoạch thời gian.

Nếu muốn đổi sang millivolt:

uint32_t millivolts = (adc_value * 3300U) / 4095U;

Con số này chỉ có ý nghĩa tương đối nếu bạn chưa hiệu chuẩn và chưa xác nhận VREF+. Với sản phẩm cần độ chính xác, hãy xem calibration và datasheet của đúng dòng STM32.

Vì sao ADC bị nhiễu?

Một vài nguyên nhân mình thường kiểm tra:

  • Dây analog dài, chạy gần dây clock hoặc đường PWM.
  • Nguồn analog hoặc GND có dòng tải lớn chạy chung đường hồi.
  • Nguồn tín hiệu có trở kháng cao nhưng sampling time quá ngắn.
  • Chỉ đọc một mẫu rồi tin ngay vào kết quả.
  • LED hoặc motor đóng cắt đúng lúc ADC đang lấy mẫu.

Đọc nhiều mẫu rồi lấy trung bình giúp giảm một phần nhiễu ngẫu nhiên:

uint32_t sum = 0;

for (uint32_t i = 0; i < 16; i++)
{
    HAL_ADC_Start(&hadc1);
    HAL_ADC_PollForConversion(&hadc1, 100);
    sum += HAL_ADC_GetValue(&hadc1);
    HAL_ADC_Stop(&hadc1);
}

uint32_t average = sum / 16U;

Oversampling không sửa được lỗi nối nhầm chân, reference sai hoặc nhiễu có tính hệ thống. Nó chỉ giúp với một phần nhiễu ngẫu nhiên.

PWM: điều khiển bằng duty cycle

Board STM32 Nucleo điều khiển LED qua mạch transistor để kiểm tra PWM trên bàn điện tử.

PWM không tạo ra điện áp analog thật; nó thay đổi duty cycle của xung digital để điều khiển công suất trung bình.

PWM, hay Pulse-Width Modulation, bật và tắt một tín hiệu digital ở tần số cố định. Duty cycle là tỷ lệ thời gian tín hiệu ở mức HIGH trong một chu kỳ.

duty cycle = thời gian HIGH / chu kỳ

Với LED, mắt người tích hợp ánh sáng theo thời gian, nên duty cycle nhỏ hơn thường tạo cảm giác sáng yếu hơn. Với motor hoặc heater, duty cycle có thể điều khiển công suất trung bình, nhưng vẫn cần mạch driver phù hợp.

PWM không biến GPIO thành một nguồn analog sạch. Nếu cần điện áp analog thật, bạn cần DAC hoặc mạch lọc phù hợp và phải xem tải.

Cấu hình PWM trong CubeMX

Chọn một timer channel có alternate function PWM, đặt timer ở PWM Generation CHx, rồi cấu hình:

  • Prescaler để tạo tần số của counter.
  • Period để đặt độ phân giải duty cycle.
  • Pulse là giá trị compare ban đầu.
  • Polarity theo yêu cầu của tải.

Ví dụ, nếu Period = 999, giá trị compare từ 0 đến 999 cho khoảng duty gần 0–100%:

uint32_t duty = 500;
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, duty);

Đừng gọi HAL_TIM_PWM_Start() trong mỗi lần cập nhật. Start một lần sau khi khởi tạo, sau đó chỉ đổi compare:

HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);

while (1)
{
    uint32_t duty = (adc_value * 999U) / 4095U;
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, duty);
}

Trong code thật, nên đặt giới hạn và cập nhật theo timer/event thay vì chạy vòng lặp nhanh không kiểm soát.

Tần số PWM chọn thế nào?

Tần số quá thấp có thể làm LED nhấp nháy hoặc motor rung. Tần số quá cao làm giảm độ phân giải duty, tăng switching loss hoặc vượt giới hạn của driver.

Công thức phụ thuộc clock timer:

f_pwm = f_timer_clock / ((Prescaler + 1) × (Period + 1))

Với LED, vài trăm hertz đến vài kilohertz thường là vùng thử nghiệm hợp lý. Với motor, quạt, audio hoặc nguồn switching, hãy chọn theo datasheet và yêu cầu của hệ thống, không dùng một giá trị chung cho mọi tải.

DMA: để peripheral chuyển dữ liệu thay CPU

Board STM32 Nucleo nối oscilloscope và laptop trong phép thử DMA theo block dữ liệu.

DMA chuyển dữ liệu giữa peripheral và RAM theo block, để CPU có thể làm công việc khác trong lúc chờ.

DMA, hay Direct Memory Access, cho phép peripheral chuyển dữ liệu trực tiếp với RAM theo buffer. CPU cấu hình transfer, tiếp tục làm việc khác, rồi nhận interrupt khi transfer hoàn thành hoặc đi được nửa buffer.

Không có DMA, một chuỗi ADC nhiều mẫu thường phải được làm như sau:

  1. Start conversion.
  2. Chờ conversion xong.
  3. Đọc data register.
  4. Ghi giá trị vào array.
  5. Lặp lại hàng trăm hoặc hàng nghìn lần.

DMA có thể tự thực hiện phần lặp đó. Đây là lý do DMA hữu ích cho ADC sampling, UART stream, SPI display, audio và các peripheral tạo dữ liệu đều đặn.

ADC + DMA theo block

Tạo một buffer:

#define ADC_BUFFER_LENGTH 64

uint16_t adc_buffer[ADC_BUFFER_LENGTH];

Trong CubeMX, bật DMA cho ADC, chọn hướng peripheral-to-memory, memory increment và circular mode nếu muốn sampling liên tục. Sau đó start:

HAL_ADC_Start_DMA(&hadc1,
                  (uint32_t *)adc_buffer,
                  ADC_BUFFER_LENGTH);

Khi đầy buffer, callback báo cho application:

volatile uint8_t adc_block_ready = 0;

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    if (hadc->Instance == ADC1)
    {
        adc_block_ready = 1;
    }
}

Main loop xử lý block:

while (1)
{
    if (adc_block_ready)
    {
        adc_block_ready = 0;

        uint32_t sum = 0;
        for (uint32_t i = 0; i < ADC_BUFFER_LENGTH; i++)
        {
            sum += adc_buffer[i];
        }

        uint32_t average = sum / ADC_BUFFER_LENGTH;
        // Dùng average để điều khiển PWM hoặc gửi UART
    }
}

Callback chỉ nên ghi nhận trạng thái. Đừng tính trung bình 64 mẫu, format chuỗi và gửi UART dài bên trong callback nếu không cần thiết.

Circular DMA và half-transfer

Ở circular mode, DMA quay lại đầu buffer sau khi đi hết. Với stream liên tục, bạn có thể xử lý nửa đầu khi DMA đi được nửa buffer và nửa sau khi buffer đầy:

void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc)
{
    if (hadc->Instance == ADC1)
    {
        // Xử lý hoặc đánh dấu nửa đầu buffer
    }
}

Điều quan trọng là CPU phải xử lý xong một nửa trước khi DMA ghi đè lại phần đó. Nếu xử lý quá chậm, bạn sẽ gặp mất mẫu dù ADC và DMA vẫn báo đang chạy.

DMA không phải là “miễn phí”

DMA giảm số lần CPU phải copy dữ liệu, nhưng vẫn cần:

  • Buffer nằm ở vùng RAM mà DMA truy cập được.
  • Độ dài và data width khớp với peripheral.
  • Không đọc buffer trong khi DMA đang ghi cùng vùng nếu chưa có cơ chế đồng bộ.
  • Xử lý lỗi và dừng DMA đúng lúc.
  • Kiểm tra cache coherency trên các dòng MCU có data cache.

Với STM32G0 và ví dụ đơn giản, cache thường không phải vấn đề chính. Nhưng thói quen nghĩ về ownership của buffer từ sớm sẽ giúp bạn tránh lỗi khi chuyển sang STM32F7, H7 hoặc hệ thống có nhiều stream hơn.

Ghép ADC, PWM và DMA

Một pipeline nhỏ có thể chạy như sau:

Timer trigger -> ADC sample -> DMA buffer -> main loop xử lý -> PWM compare

Trong demo đơn giản, bạn có thể đọc giá trị trung bình của adc_buffer, scale từ dải ADC sang dải compare của PWM rồi cập nhật CCR:

uint32_t pwm_compare = (average * PWM_PERIOD) / ADC_MAX_VALUE;

if (pwm_compare > PWM_PERIOD)
{
    pwm_compare = PWM_PERIOD;
}

__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pwm_compare);

Timer trigger giúp khoảng cách giữa các mẫu đều hơn so với việc gọi ADC ngẫu nhiên trong main loop. Đây là bước đầu để đi từ một demo LED sang data logger hoặc bộ điều khiển có timing ổn định.

Lỗi dễ gặp

ADC luôn bằng 0 hoặc luôn ở mức tối đa

Kiểm tra chân đã thực sự là ADC channel chưa, biến trở có nối đúng 3V3/GND không, có GND chung không và input có bị short không. Đọc sai channel trong CubeMX cũng tạo ra triệu chứng giống như phần cứng hỏng.

ADC nhảy giá trị khi PWM chạy

Giảm tốc độ PWM hoặc đổi vị trí dây analog, cải thiện GND/decoupling, tăng sampling time và lấy trung bình nhiều mẫu. Nếu nhiễu đến từ đường hồi dòng của LED hoặc motor, xử lý layout sẽ hiệu quả hơn là chỉ thêm filter trong code.

PWM không có output

Kiểm tra đúng timer channel, alternate function của GPIO, đã gọi HAL_TIM_PWM_Start() chưa và chân output có bị peripheral khác chiếm không. Với board Nucleo, hãy chắc rằng bạn đang đo đúng chân header chứ không phải số thứ tự của connector khác.

PWM có tần số đúng nhưng duty sai

Kiểm tra Period, giá trị compare, polarity và giới hạn scaling. Pulse không phải lúc nào cũng là phần trăm; nó là số đếm so với timer.

DMA callback không chạy

Kiểm tra DMA request đúng peripheral/channel, NVIC, interrupt handler và việc gọi HAL_ADC_Start_DMA(). Nếu callback chạy một lần rồi dừng, kiểm tra mode normal/circular và cờ lỗi ADC/DMA.

Buffer DMA có dữ liệu cũ hoặc bị ghi đè

Không xử lý buffer đang được DMA ghi. Dùng half/full callback, double buffer hoặc protocol ownership rõ ràng. Đây là lỗi logic đồng bộ, không phải chỉ là lỗi kiểu dữ liệu.

Kết luận

ADC giúp STM32 đọc thế giới analog. PWM biến timer thành công cụ điều khiển công suất trung bình. DMA giúp các peripheral chuyển dữ liệu theo buffer mà CPU không phải tham gia vào từng mẫu.

Nếu mới học, hãy đi theo thứ tự: đọc một mẫu ADC bằng polling, tạo PWM cố định, ghép ADC vào PWM, rồi mới bật DMA. Mỗi bước nên được kiểm tra bằng multimeter, oscilloscope hoặc UART trước khi thêm bước tiếp theo.

Ở P5, mình sẽ ghép các phần đã học thành một ứng dụng nhỏ: timer kích hoạt sampling, DMA thu dữ liệu, UART in trạng thái và một output PWM phản hồi theo giá trị đo.

Tham khảo và mã nguồn mở

Chia sẻ

LinkedInFacebookX

Tiếp tục khám phá

Đọc tiếp

Bài viết liên quan

Xem thêm trong Bài viết

Nastrotek sử dụng cookie để phân tích truy cập và cá nhân hóa quảng cáo, giúp hiểu cách website được sử dụng. Bạn có thể chấp nhận hoặc từ chối các cookie không thiết yếu. Chính sách bảo mật