Bài 47.730 phút đọc
SPI, I2C, ADC, PWM và DMA
Sau bài này bạn sẽ làm được
- So sánh SPI và I2C trên sáu tiêu chí và chọn đúng cho từng cảm biến
- Đọc ADC và đổi sang đơn vị vật lý mà không dùng số thực
- Cấu hình PWM để điều khiển độ sáng và động cơ
- Hiểu DMA chuyển dữ liệu mà không cần CPU
Sáu ngoại vi còn lại. Bài này không đi sâu vào từng thanh ghi, mà tập trung vào thứ quan trọng hơn: chọn cái nào cho việc gì, và những cái bẫy chỉ lộ ra khi mạch đã hàn xong.
#Bảng chọn ngoại vi
| Ngoại vi | Số dây | Tốc độ | Đặc điểm | Dùng cho |
|---|---|---|---|---|
| UART | 2 cộng đất | 9600 tới 3 Mbaud | Không xung nhịp chung, chỉ nối hai thiết bị | Gỡ lỗi, module GPS, module Bluetooth |
| SPI | 4 cộng đất | Tới hàng chục MHz | Có xung nhịp, song công, mỗi thiết bị một chân chọn | Thẻ nhớ, màn hình TFT, Flash ngoài, cảm biến nhanh |
| I2C | 2 cộng đất | 100k, 400k, 1M | Có địa chỉ, nhiều thiết bị chung hai dây | Cảm biến chậm, EEPROM, đồng hồ thời gian thực |
| ADC | 1 mỗi kênh | Tới vài triệu mẫu mỗi giây | Đổi điện áp thành số, thường 12 bit | Nhiệt độ, ánh sáng, biến trở, dòng điện |
| PWM | 1 mỗi kênh | Tùy cấu hình | Xung có tỷ lệ thời gian mức cao điều chỉnh được | Độ sáng đèn, tốc độ động cơ, servo, âm thanh |
| DMA | 0 | Tốc độ bus | Chuyển dữ liệu không cần CPU | ADC liên tục, UART tốc độ cao, làm mới màn hình |
#SPI và I2C
So sánh cách nối dây
SPI, ba thiet bi:
MCU ---- SCK ----+----+----+
---- MOSI ----+----+----+
---- MISO ----+----+----+
---- CS1 -----+ | |
---- CS2 ----------+ |
---- CS3 ---------------+
Tong 3 + 3 = 6 chan. Moi thiet bi them mot chan CS.
I2C, ba thiet bi:
MCU ---- SCL ----+----+----+
---- SDA ----+----+----+
dc 0x48 0x50 0x68
Tong 2 chan cho MOI so thiet bi. Phan biet bang DIA CHI.| Tiêu chí | SPI | I2C |
|---|---|---|
| Số dây cho n thiết bị | 3 cộng n | 2, không đổi |
| Tốc độ điển hình | 10 tới 50 MHz | 100 kHz tới 1 MHz |
| Song công | Có, gửi và nhận cùng lúc | Không |
| Xác nhận từ thiết bị | Không có | Có, bit ACK mỗi byte |
| Phát hiện thiết bị không tồn tại | Không được | Được, thiếu ACK |
| Cần điện trở kéo lên ngoài | Không | Có, hai cái |
| Độ phức tạp giao thức | Rất đơn giản | Phức tạp, nhiều trạng thái |
| Bị treo bus | Không bao giờ | Có, và cần thủ tục cứu |
i2c-doc-thanh-ghi.c
/* Mẫu chuẩn để đọc một thanh ghi của cảm biến I2C:
ghi địa chỉ thanh ghi, rồi khởi động lại và đọc. */
int i2c_doc_thanh_ghi(uint8_t dc_tb, uint8_t dc_tg, uint8_t *ra, uint8_t n)
{
if (i2c_bat_dau() != 0) return -1;
if (i2c_gui_dia_chi(dc_tb, I2C_GHI) != 0) goto loi;
if (i2c_gui_byte(dc_tg) != 0) goto loi;
if (i2c_bat_dau_lai() != 0) goto loi; /* KHÔNG dừng */
if (i2c_gui_dia_chi(dc_tb, I2C_DOC) != 0) goto loi;
for (uint8_t i = 0; i < n; ++i)
ra[i] = i2c_nhan_byte(i + 1u < n); /* ACK trừ byte cuối */
i2c_dung();
return 0;
loi:
i2c_dung();
return -1;
}#ADC và số học không dùng số thực
adc.c
#include "stm32f4.h"
void adc_khoi_tao(void)
{
RCC->APB2ENR |= (1u << 8); /* clock cho ADC1 */
RCC->AHB1ENR |= (1u << 0); /* clock cho GPIOA */
GPIOA->MODER |= (3u << (0 * 2)); /* PA0 sang chế độ tương tự, 11 */
ADC1->SQR3 = 0u; /* kênh 0 là bước chuyển đổi đầu tiên */
ADC1->SMPR2 |= (7u << 0); /* thời gian lấy mẫu dài nhất cho kênh 0 */
ADC1->CR2 |= (1u << 0); /* ADON, bật ADC */
}
uint16_t adc_doc(void)
{
ADC1->CR2 |= (1u << 30); /* SWSTART, bắt đầu chuyển đổi */
while (!(ADC1->SR & (1u << 1))) { } /* chờ EOC */
return (uint16_t)ADC1->DR; /* đọc DR tự xóa cờ EOC */
}Dùng số thực
float dien_ap = adc * 3.3f / 4095.0f;
/* Hai phép chia số thực. Trên Cortex-M0 mất khoảng 960 chu kỳ.
Và nếu quên hậu tố f thì thành double, mất khoảng 1800 chu kỳ. */Dùng số nguyên đơn vị nhỏ hơn
uint32_t mv = (uint32_t)adc * 3300u / 4095u;
/* Một phép nhân và một phép chia số nguyên, khoảng 15 chu kỳ.
Kết quả tính bằng mili vôn, đủ độ phân giải cho mọi việc thực tế. */Đổi sang đơn vị vật lý
cam-bien.c
/* LM35 cho ra 10 mV mỗi độ C. */
int32_t lm35_doc_do_c_x10(void)
{
uint32_t adc = adc_doc();
uint32_t mv = adc * 3300u / 4095u;
return (int32_t)(mv * 10u / 10u); /* mV chia 10 mV mỗi độ, nhân 10 */
}
/* Chia nhiệt áp với điện trở nhiệt NTC: cần bảng tra, không dùng công thức.
Bảng 32 mục cộng nội suy tuyến tính đủ chính xác và rất nhanh. */
static const int16_t BANG_NTC[] = {
/* mỗi mục cách nhau 128 đơn vị ADC, giá trị là nhiệt độ nhân 10 */
1250, 1080, 950, 850, 770, 700, 640, 590,
540, 500, 460, 420, 380, 350, 310, 280,
250, 220, 180, 150, 120, 90, 50, 20,
-20, -60, -100, -150, -210, -280, -370, -500,
};
int16_t ntc_doc_do_c_x10(uint16_t adc)
{
uint16_t chi_so = (uint16_t)(adc >> 7); /* chia 128 */
uint16_t du = (uint16_t)(adc & 127u);
if (chi_so >= 31u) return BANG_NTC[31];
int32_t a = BANG_NTC[chi_so];
int32_t b = BANG_NTC[chi_so + 1u];
return (int16_t)(a + (b - a) * (int32_t)du / 128); /* nội suy */
}#Timer và PWM
pwm.c
#include "stm32f4.h"
/* PWM trên TIM3 kênh 1, chân PA6.
Tần số ra = fCK / ((PSC + 1) * (ARR + 1)) */
void pwm_khoi_tao(uint32_t fck, uint32_t tan_so, uint16_t so_muc)
{
RCC->APB1ENR |= (1u << 1); /* clock cho TIM3 */
RCC->AHB1ENR |= (1u << 0); /* clock cho GPIOA */
/* PA6 sang chức năng thay thế AF2, tức TIM3_CH1 */
GPIOA->MODER &= ~(3u << (6 * 2));
GPIOA->MODER |= (2u << (6 * 2));
GPIOA->AFR[0] &= ~(0xFu << (6 * 4));
GPIOA->AFR[0] |= (2u << (6 * 4));
/* Chia trước rồi đếm, để đạt tần số mong muốn */
uint32_t chia = fck / (tan_so * (uint32_t)so_muc);
TIM3->PSC = (uint16_t)(chia - 1u);
TIM3->ARR = (uint16_t)(so_muc - 1u);
/* Chế độ PWM 1: mức cao khi bộ đếm nhỏ hơn CCR1 */
TIM3->CCMR1 |= (6u << 4) | (1u << 3); /* OC1M = 110, OC1PE = 1 */
TIM3->CCER |= (1u << 0); /* bật kênh 1 */
TIM3->CR1 |= (1u << 7); /* ARPE, đệm thanh ghi ARR */
TIM3->CCR1 = 0u; /* bắt đầu ở mức 0 */
TIM3->EGR = 1u; /* nạp lại các thanh ghi đệm */
TIM3->CR1 |= (1u << 0); /* CEN, bật bộ đếm */
}
void pwm_dat(uint16_t muc)
{
TIM3->CCR1 = muc; /* nhỏ hơn ARR + 1 */
}| Ứng dụng | Tần số nên chọn | Vì sao |
|---|---|---|
| Độ sáng đèn | Trên 200 Hz | Dưới đó mắt thấy nhấp nháy, nhất là khi mắt chuyển động |
| Động cơ một chiều | 16 tới 25 kHz | Vượt ngưỡng nghe được, tránh tiếng rít |
| Servo | 50 Hz, độ rộng xung 1 tới 2 ms | Chuẩn công nghiệp, không đổi được |
| Bộ nguồn xung | 100 kHz tới 1 MHz | Cuộn cảm nhỏ hơn khi tần số cao hơn |
| Âm thanh | Trên 100 kHz | Phải cao hơn nhiều lần tần số âm thanh cao nhất |
Timer làm tick hệ thống
systick.c
#include "stm32f4.h"
static volatile uint32_t g_tick = 0u;
/* SysTick là bộ đếm 24 bit nằm trong lõi Cortex-M, luôn có mặt. */
void systick_khoi_tao(uint32_t fck)
{
SysTick->LOAD = (fck / 1000u) - 1u; /* ngắt mỗi 1 ms */
SysTick->VAL = 0u;
SysTick->CTRL = (1u << 2) /* dùng clock lõi */
| (1u << 1) /* cho phép ngắt */
| (1u << 0); /* bật bộ đếm */
}
void SysTick_Handler(void)
{
++g_tick; /* việc duy nhất, xem Bài 49.3 */
}
uint32_t tick_ms(void)
{
return g_tick;
}#DMA
DMA
Direct Memory Access. Một khối phần cứng chuyển dữ liệu giữa ngoại vi và bộ nhớ, hoặc giữa hai vùng bộ nhớ, mà không cần CPU tham gia từng byte. CPU chỉ cấu hình một lần rồi làm việc khác.
| Cách chuyển 1000 byte từ ADC | Chu kỳ CPU | Ghi chú |
|---|---|---|
| Vòng lặp chờ cờ | Khoảng 500 000 | CPU bị chặn hoàn toàn |
| Ngắt mỗi mẫu | Khoảng 40 000 | 1000 lần vào ra ISR, mỗi lần khoảng 40 chu kỳ |
| DMA | Khoảng 100 | Một lần cấu hình, một lần ngắt báo xong |
dma-adc.c
#include "stm32f4.h"
#define SO_MAU 256u
static volatile uint16_t bo_dem_adc[SO_MAU];
/* ADC chuyển đổi liên tục, DMA chép mỗi kết quả vào mảng,
và tự quay lại đầu mảng khi đầy. CPU hoàn toàn không tham gia. */
void adc_dma_khoi_tao(void)
{
RCC->AHB1ENR |= (1u << 22); /* clock cho DMA2 */
DMA2_Stream0->CR = 0u; /* tắt trước khi cấu hình */
while (DMA2_Stream0->CR & 1u) { } /* chờ tắt hẳn */
DMA2_Stream0->PAR = (uint32_t)&ADC1->DR;
DMA2_Stream0->M0AR = (uint32_t)bo_dem_adc;
DMA2_Stream0->NDTR = SO_MAU;
DMA2_Stream0->CR = (0u << 25) /* kênh 0, tức ADC1 */
| (1u << 13) /* MSIZE = 01, bộ nhớ 16 bit */
| (1u << 11) /* PSIZE = 01, ngoại vi 16 bit */
| (1u << 10) /* MINC, tăng địa chỉ bộ nhớ */
| (1u << 8) /* CIRC, chế độ vòng */
| (1u << 0); /* EN, bật */
ADC1->CR2 |= (1u << 8) /* DMA, cho phép yêu cầu DMA */
| (1u << 9) /* DDS, yêu cầu liên tục */
| (1u << 1); /* CONT, chuyển đổi liên tục */
ADC1->CR2 |= (1u << 30); /* bắt đầu */
}
/* Vòng lặp chính chỉ việc đọc mảng bất cứ lúc nào. */
uint32_t adc_trung_binh(void)
{
uint32_t tong = 0u;
for (uint16_t i = 0; i < SO_MAU; ++i)
tong += bo_dem_adc[i];
return tong / SO_MAU;
}Tự làm thử
- Nối một cảm biến I2C và viết hàm quét bus in ra mọi địa chỉ có thiết bị trả lời.
- Cài thủ tục cứu bus I2C, rồi mô phỏng treo bằng cách reset chip giữa lúc đang đọc cảm biến.
- Đọc ADC và đổi sang mili vôn bằng số nguyên, thử cả hai thứ tự nhân chia và ghi lại kết quả.
- Cài PWM điều khiển độ sáng đèn với bảng gamma và với thang tuyến tính, so cảm nhận bằng mắt.
- Cài ADC chạy liên tục qua DMA ở chế độ vòng, rồi đo tải CPU so với cách chờ cờ.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- SPI nhanh và đơn giản nhưng tốn một chân cho mỗi thiết bị. I2C chỉ hai dây cho mọi thiết bị nhưng chậm hơn và phức tạp hơn.
- I2C bắt buộc có điện trở kéo lên, và mọi sản phẩm dùng I2C nên có thủ tục cứu bus lúc khởi động.
- Đổi giá trị ADC bằng số nguyên, luôn nhân trước chia sau, và ép sang kiểu rộng hơn trước phép nhân khi có thể tràn.
- Bảng tra cộng nội suy nhanh hơn công thức số thực hàng trăm lần và tốn ít Flash hơn nhiều.
- DMA giảm tải CPU từ gần một trăm phần trăm xuống dưới một phần trăm, nhưng bộ đệm phải tĩnh và phải
volatile.