🌱 STM32 - 22. Tìm Hiểu Ngoại Vi ADC Trong STM32
Ở bài viết trước mình đã giới thiệu tổng quan về ADC, các loại ADC và tổng quát về bộ ADC trong Vi điều khiển STM32. Ở bài viết này, mình sẽ làm rõ hơn về một số đặc điểm các bạn cần lưu ý khi làm việc với ADC trong STM32, đó là Lấy mẫu ADC (Sampling), chuyển đổi Regular và Injected, Analog Watchdog, ...
Thông thường, đối với một bộ ADC, bên cạnh độ phân giải thì tốc độ chuyển đổi cũng rất quan trọng. Trong vi điều khiển STM32, chúng ta có thể tính được tốc độ chuyển đổi của bộ ADC bằng cách sau:
Tổng thời gian chuyển đổi = Thời gian lấy mẫu tín hiệu + Thời gian chuyển đổi
Trong đó, đối với bộ ADC xấp xỉ liên tiếp, với độ phân giải N-bit, thì thời gian chuyển đổi sẽ là N chu kỳ clock. Còn đối với Thời gian lấy mẫu tín hiệu, chúng ta có thể cấu hình như dưới đây.
👉 ADC Sampling (Lấy mẫu ADC)
Như bài trước đã giới thiệu thì ngoại vi ADC trong STM32 sử dụng nguồn cấp xung clock là APB2, với một bộ chia với hệ số /2/4/6/8, tạo thành tín hiệu ADCCLK. Nguồn clock này có thể cùng với cấu hình 3 bit SMP[2:0] của thanh ghi ADC_SMPR1 và ADC_SMPR2 để tạo ra các Sampling time từ 1.5 đến 239.5 chu kỳ clock.
👉 Regular and Injected Conversions
Vi điều khiển STM32 hỗ trợ 2 chế độ chuyển đổi ADC: Regular và Injected. Lý do có 2 chế độ này là STM32 hỗ trợ đến 19 kênh đầu vào ADC, nhưng chúng lại sử dụng chung một thanh ghi dữ liệu.
➤ Thực tế chúng ta cần xét đến 4 chế độ chuyển đổi dữ liệu:
- Single: ADC chỉ đọc 1 kênh duy nhất, và chỉ đọc khi nào được yêu cầu.
- Single Continuous: ADC sẽ đọc một kênh duy nhất, nhưng đọc dữ liệu nhiều lần liên tiếp (Có thể được biết đến như sử dụng DMA để đọc dữ liệu và ghi vào bộ nhớ).
- Scan: Multi-Channels: Quét qua và đọc dữ liệu nhiều kênh, nhưng chỉ đọc khi nào được yêu cầu.
- Scan: Continuous Multi-Channels Repeat: Quét qua và đọc dữ liệu nhiều kênh, nhưng đọc liên tiếp nhiều lần giống như Single Continuous.
Với các chế độ quét nhiều kênh, có thể thấy các kênh có thể được đọc lần lượt, và mỗi kênh sau khi chuyển đổi xong sẽ tạo ra một tín hiệu trigger báo chuyển đổi xong. Nếu như mọi thứ diễn ra bình thường và các kênh được đọc tuần tự, đó chính là Regular Conversion, các tín hiệu báo một kênh hoạt động là Regular Trigger.
➤ Một trường hợp khác (Ở đây chúng ta có thể tư duy giống như làm việc với ngắt), đó là nếu như các kênh đọc lần lượt như vậy, thì có thể lỡ đi những kênh quan trọng cần ưu tiên chuyển đổi trước.
➤ ADC cung cấp cho người dùng Injected Conversion, khi kênh có mức độ ưu tiên cao hơn có thể tạo ra một Injected Trigger. Khi gặp Injected Trigger thì ngay lập tức kênh đang hoạt động bị ngưng lại để kênh được ưu tiên kia có thể hoạt động, sau đó, kênh bị ngưng mới có thể hoạt động tiếp (Khá giống ngắt). Các Injected Conversion này có các thanh ghi lưu kết quả riêng (ADC_JDRx) và các thanh ghi offset.
👉 Analog Watchdog
Analog Watchdog là một tính năng khá hữu ích của ADC, nó dùng để cảnh báo khi tín hiệu điện áp đầu vào vượt qua một ngưỡng nào đó.
Thông thường, để kiểm tra một điện áp nào đó có vượt ngưỡng cho phép hay không, chúng ta phải liên tục đọc ADC và liên tục kiểm tra, gây mất thời gian và tài nguyên của Vi điều khiển. Rất nhiều ứng dụng chỉ cần sử dụng cảnh báo như vậy mà không cần quan tâm những mức điện áp khác: Mạch phát hiện điểm không, mạch cảnh báo cháy, cảnh báo quá nhiệt độ, quá mức nước, ...
➤ Tính năng Analog Watchdog cho phép việc kiểm tra triển khai trên phần cứng, và tiết kiệm thời gian hơn. Bạn chỉ cần setup thông số điện áp ngưỡng (Low Threshold Value và High Threshold Value), cài đặt chế độ Interrupt. Khi điện áp vào vượt quá một trong 2 giá trị ngưỡng thì sẽ sinh ra một ngắt.
Trên đây là một số tính năng cơ bản của ngoại vi ADC trong Vi điều khiển STM32 mà mình muốn giới thiệu, ngoài ra còn một số tính năng khác các bạn có thể xem thêm trong tài liệu Reference Manual của từng Vi điều khiển.
👉 Ví dụ lập trình ADC trên STM32F401
Dưới đây là đoạn code bare-metal minh họa cách sử dụng ADC trên STM32F401RE với Regular Conversion (PA0), Injected Conversion (PA1), và Analog Watchdog:
#include
// Định nghĩa địa chỉ cơ bản
#define RCC_BASE 0x40023800
#define GPIOA_BASE 0x40020000
#define ADC1_BASE 0x40012000
// Định nghĩa thanh ghi RCC
#define RCC_AHB1ENR (*(volatile uint32_t *)(RCC_BASE + 0x30))
#define RCC_APB2ENR (*(volatile uint32_t *)(RCC_BASE + 0x44))
// Định nghĩa thanh ghi GPIOA
#define GPIOA_MODER (*(volatile uint32_t *)(GPIOA_BASE + 0x00))
#define GPIOA_PUPDR (*(volatile uint32_t *)(GPIOA_BASE + 0x0C))
// Định nghĩa thanh ghi ADC1
#define ADC1_SR (*(volatile uint32_t *)(ADC1_BASE + 0x00))
#define ADC1_CR1 (*(volatile uint32_t *)(ADC1_BASE + 0x04))
#define ADC1_CR2 (*(volatile uint32_t *)(ADC1_BASE + 0x08))
#define ADC1_SMPR1 (*(volatile uint32_t *)(ADC1_BASE + 0x0C))
#define ADC1_SMPR2 (*(volatile uint32_t *)(ADC1_BASE + 0x10))
#define ADC1_JOFR1 (*(volatile uint32_t *)(ADC1_BASE + 0x14))
#define ADC1_HTR (*(volatile uint32_t *)(ADC1_BASE + 0x24))
#define ADC1_LTR (*(volatile uint32_t *)(ADC1_BASE + 0x28))
#define ADC1_SQR1 (*(volatile uint32_t *)(ADC1_BASE + 0x2C))
#define ADC1_SQR3 (*(volatile uint32_t *)(ADC1_BASE + 0x30))
#define ADC1_JSQR (*(volatile uint32_t *)(ADC1_BASE + 0x38))
#define ADC1_JDR1 (*(volatile uint32_t *)(ADC1_BASE + 0x3C))
#define ADC1_DR (*(volatile uint32_t *)(ADC1_BASE + 0x4C))
// Định nghĩa bit
#define RCC_AHB1ENR_GPIOAEN (1 << 0)
#define RCC_APB2ENR_ADC1EN (1 << 8)
#define ADC_CR1_AWDEN (1 << 23) // Bật Analog Watchdog
#define ADC_CR1_AWDIE (1 << 6) // Bật ngắt Analog Watchdog
#define ADC_CR1_JAUTO (1 << 10) // Tự động Injected sau Regular
#define ADC_CR2_ADON (1 << 0)
#define ADC_CR2_SWSTART (1 << 30)
#define ADC_CR2_JSTART (1 << 22)
#define ADC_SR_EOC (1 << 1)
#define ADC_SR_JEOC (1 << 2)
#define ADC_SR_AWD (1 << 0)
// Hàm cấu hình ADC
void ADC_Init(void) {
// Bật clock cho GPIOA và ADC1
RCC_AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
RCC_APB2ENR |= RCC_APB2ENR_ADC1EN;
// Cấu hình PA0 (Regular) và PA1 (Injected) thành chế độ analog
GPIOA_MODER |= (3 << 0) | (3 << 2); // PA0, PA1: Analog mode
GPIOA_PUPDR &= ~((3 << 0) | (3 << 2)); // Không pull-up/pull-down
// Cấu hình ADC1
ADC1_CR1 |= ADC_CR1_AWDEN | ADC_CR1_AWDIE | ADC_CR1_JAUTO; // Bật Watchdog, ngắt, tự động Injected
ADC1_CR1 &= ~(3 << 24); // Watchdog cho kênh Regular (kênh 0)
ADC1_SMPR2 = (3 << 0) | (3 << 3); // Kênh 0, 1: 56 chu kỳ lấy mẫu
ADC1_SQR1 = (1 << 20); // 1 kênh Regular
ADC1_SQR3 = 0; // Kênh 0 (PA0) cho Regular
ADC1_JSQR = (1 << 20) | 1; // 1 kênh Injected, kênh 1 (PA1)
ADC1_HTR = 3000; // Ngưỡng cao: ~2.4V (3000/4095 * 3.3V)
ADC1_LTR = 1000; // Ngưỡng thấp: ~0.8V (1000/4095 * 3.3V)
ADC1_CR2 |= ADC_CR2_ADON; // Bật ADC
}
// Hàm đọc giá trị Regular
uint16_t ADC_Read_Regular(void) {
ADC1_CR2 |= ADC_CR2_SWSTART; // Bắt đầu Regular Conversion
while (!(ADC1_SR & ADC_SR_EOC)); // Chờ EOC
return ADC1_DR; // Trả về giá trị Regular
}
// Hàm đọc giá trị Injected
uint16_t ADC_Read_Injected(void) {
while (!(ADC1_SR & ADC_SR_JEOC)); // Chờ JEOC (tự động sau Regular)
return ADC1_JDR1; // Trả về giá trị Injected
}
// Hàm xử lý ngắt Analog Watchdog
void ADC_IRQHandler(void) {
if (ADC1_SR & ADC_SR_AWD) {
// Xử lý khi điện áp vượt ngưỡng (ví dụ: bật LED cảnh báo)
ADC1_SR &= ~ADC_SR_AWD; // Xóa cờ ngắt
}
}
int main(void) {
ADC_Init();
// Bật ngắt toàn cục (giả sử NVIC đã được cấu hình)
while (1) {
uint16_t regular_value = ADC_Read_Regular(); // Đọc PA0
uint16_t injected_value = ADC_Read_Injected(); // Đọc PA1
// Giá trị từ 0-4095 tương ứng 0-3.3V (Vref = 3.3V)
(void)regular_value; (void)injected_value; // Biến tránh cảnh báo unused
}
}
>>>>>> Follow ngay <<<<<<<
Để nhận được những bài học miễn phí mới nhất nhé 😊
Chúc các bạn học tập tốt 😊
anh ơi anh có thể làm thêm về chế độ Dual ADC không ạ.
Trả lờiXóaNhững bài viết sau a sẽ làm nhé
Xóa