Bỏ qua điều hướng, tới nội dung chính
Học C
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 viSố dâyTốc độĐặc điểmDùng cho
UART2 cộng đất9600 tới 3 MbaudKhông xung nhịp chung, chỉ nối hai thiết bịGỡ lỗi, module GPS, module Bluetooth
SPI4 cộng đấtTới hàng chục MHzCó xung nhịp, song công, mỗi thiết bị một chân chọnThẻ nhớ, màn hình TFT, Flash ngoài, cảm biến nhanh
I2C2 cộng đất100k, 400k, 1MCó địa chỉ, nhiều thiết bị chung hai dâyCảm biến chậm, EEPROM, đồng hồ thời gian thực
ADC1 mỗi kênhTới vài triệu mẫu mỗi giâyĐổi điện áp thành số, thường 12 bitNhiệt độ, ánh sáng, biến trở, dòng điện
PWM1 mỗi kênhTùy cấu hìnhXung có tỷ lệ thời gian mức cao điều chỉnh đượcĐộ sáng đèn, tốc độ động cơ, servo, âm thanh
DMA0Tốc độ busChuyển dữ liệu không cần CPUADC 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íSPII2C
Số dây cho n thiết bị3 cộng n2, không đổi
Tốc độ điển hình10 tới 50 MHz100 kHz tới 1 MHz
Song côngCó, gửi và nhận cùng lúcKhô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ạiKhông đượcĐược, thiếu ACK
Cần điện trở kéo lên ngoàiKhôngCó, hai cái
Độ phức tạp giao thứcRất đơn giảnPhức tạp, nhiều trạng thái
Bị treo busKhô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

Cùng tần số, khác tỷ lệ thời gian mức cao. Đèn nhìn thấy giá trị trung bình.
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ụngTần số nên chọnVì sao
Độ sáng đènTrên 200 HzDưới đó mắt thấy nhấp nháy, nhất là khi mắt chuyển động
Động cơ một chiều16 tới 25 kHzVượt ngưỡng nghe được, tránh tiếng rít
Servo50 Hz, độ rộng xung 1 tới 2 msChuẩn công nghiệp, không đổi được
Bộ nguồn xung100 kHz tới 1 MHzCuộn cảm nhỏ hơn khi tần số cao hơn
Âm thanhTrên 100 kHzPhả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ừ ADCChu kỳ CPUGhi chú
Vòng lặp chờ cờKhoảng 500 000CPU bị chặn hoàn toàn
Ngắt mỗi mẫuKhoảng 40 0001000 lần vào ra ISR, mỗi lần khoảng 40 chu kỳ
DMAKhoảng 100Mộ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ử

  1. 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.
  2. 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.
  3. Đọ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ả.
  4. 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.
  5. 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.