Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 47.428 phút đọc

GPIO

Sau bài này bạn sẽ làm được

  • Cấu hình một chân làm đầu ra theo đúng thứ tự năm bước
  • Giải thích vì sao quên bật clock là lỗi số một của người mới
  • Dùng BSRR thay cho phép đọc sửa ghi trên ODR
  • Đọc trạng thái đầu vào và hiểu vai trò điện trở kéo

GPIO là ngoại vi đơn giản nhất và cũng là nơi mọi người bắt đầu. Nó dạy đủ ba thứ sẽ lặp lại ở mọi ngoại vi khác: bật clock trước, cấu hình theo đúng thứ tự, và tránh phép đọc sửa ghi khi có thể.

#Năm bước cấu hình một chân

Thanh ghi MODER dành hai bit cho mỗi chân, nên chân 5 dùng bit 11 và bit 10.
gpio-cau-hinh.c
#include <stdint.h>

#include "stm32f4.h"

/* Cấu hình PA5 làm đầu ra đẩy kéo, tốc độ cao, không điện trở kéo.
   PA5 là chân nối với đèn trên bo Nucleo. */
void den_khoi_tao(void)
{
    /* 0. BẬT CLOCK cho cổng A. Quên bước này là mọi bước sau vô nghĩa. */
    RCC->AHB1ENR |= (1u << 0);

    /* 1. Chế độ: 01 tức đầu ra */
    GPIOA->MODER &= ~(3u << (5 * 2));      /* xóa hai bit của chân 5 */
    GPIOA->MODER |=  (1u << (5 * 2));      /* đặt 01 */

    /* 2. Kiểu đầu ra: 0 tức đẩy kéo */
    GPIOA->OTYPER &= ~(1u << 5);

    /* 3. Tốc độ: 11 tức rất cao */
    GPIOA->OSPEEDR |= (3u << (5 * 2));

    /* 4. Điện trở kéo: 00 tức không có */
    GPIOA->PUPDR &= ~(3u << (5 * 2));
}
  1. Bật clock cho cổng

    Thanh ghi RCC->AHB1ENR, mỗi bit một cổng. Trước bước này, mọi lệnh ghi vào thanh ghi GPIO đều bị bỏ qua và mọi lệnh đọc đều cho 0.

  2. Đặt chế độ trong MODER

    Hai bit mỗi chân: 00 vào, 01 ra, 10 chức năng thay thế, 11 tương tự. Phải xóa hai bit cũ trước khi đặt.

  3. Chọn kiểu đầu ra trong OTYPER

    Một bit mỗi chân: 0 là đẩy kéo, tức chân tự kéo lên cao và xuống thấp. 1 là cực máng hở, chỉ kéo được xuống thấp, dùng cho bus I2C và cho tín hiệu nhiều nguồn dùng chung.

  4. Chọn tốc độ trong OSPEEDR

    Tốc độ cao làm sườn tín hiệu dốc hơn nhưng phát nhiễu nhiều hơn và tốn điện hơn. Với một cái đèn thì tốc độ thấp là đủ.

  5. Chọn điện trở kéo trong PUPDR

    Với đầu ra thì thường để 00, tức không có. Với đầu vào đọc nút bấm thì rất quan trọng, xem mục dưới.

#Bật clock, lỗi số một của người mới

BusThanh ghi bật clockNgoại vi trên bus đó
AHB1RCC->AHB1ENRGPIOA tới GPIOK, DMA1, DMA2, CRC
AHB2RCC->AHB2ENRUSB OTG, camera, bộ sinh số ngẫu nhiên
APB1RCC->APB1ENRTIM2 tới TIM7, USART2, USART3, I2C, SPI2
APB2RCC->APB2ENRTIM1, TIM8, USART1, USART6, SPI1, ADC, SYSCFG

#BSRR thay cho đọc sửa ghi

Ghi bit vào nửa dưới thì đặt chân, ghi vào nửa trên thì xóa chân. Một lệnh ghi duy nhất.
Đọc sửa ghi trên ODR
GPIOA->ODR |= (1u << 5);

/* Sinh ra BA lệnh:
     ldr  r3, [r0, #20]      @ đọc ODR
     orr  r3, r3, #32        @ sửa
     str  r3, [r0, #20]      @ ghi lại

   Nếu một ISR chạy giữa lệnh 1 và lệnh 3, và ISR đó đổi
   một chân khác của cùng cổng, thì thay đổi của ISR bị
   lệnh 3 GHI ĐÈ mất. */
Ghi thẳng BSRR
GPIOA->BSRR = (1u << 5);          /* đặt chân 5 */
GPIOA->BSRR = (1u << (5 + 16));   /* xóa chân 5 */

/* Sinh ra MỘT lệnh:
     str  r3, [r0, #24]      @ ghi BSRR

   Không đọc gì, nên không có gì để mất.
   Ghi bit 0 vào các vị trí khác không có tác dụng,
   nên chân khác hoàn toàn không bị đụng tới. */
terminal
arm-none-eabi-gcc -O2 -S so-sanh.c -o - | sed -n '/dung_odr:/,/bx/p'
dung_odr:
        ldr     r3, [r0, #20]
        orr     r3, r3, #32
        str     r3, [r0, #20]
        bx      lr
arm-none-eabi-gcc -O2 -S so-sanh.c -o - | sed -n '/dung_bsrr:/,/bx/p'
dung_bsrr:
        mov     r3, #32
        str     r3, [r0, #24]
        bx      lr

Đảo mức chân

dao-muc.c
/* BSRR không có chế độ đảo, nên phải đọc ODR rồi quyết định. */
static inline void gpio_dao(GPIO_TypeDef *cong, uint8_t chan)
{
    uint32_t dang_bat = cong->ODR & (1u << chan);

    cong->BSRR = dang_bat ? (1u << (chan + 16u)) : (1u << chan);
}

/* Một số dòng STM32 có thanh ghi ODR ghi được trực tiếp,
   nên cách ngắn nhất vẫn là: */
cong->ODR ^= (1u << chan);      /* nhưng đây LẠI là đọc sửa ghi */

/* Nếu chỉ vòng lặp chính đụng vào cổng đó thì cách này an toàn.
   Nếu có ISR cùng đụng vào thì phải bảo vệ, xem Bài 49.4. */

#Đọc đầu vào và điện trở kéo

doc-nut.c
/* Cấu hình PC13 làm đầu vào có điện trở kéo lên.
   PC13 là nút người dùng trên bo Nucleo, nối xuống đất khi nhấn. */
void nut_khoi_tao(void)
{
    RCC->AHB1ENR |= (1u << 2);             /* clock cho cổng C */

    GPIOC->MODER &= ~(3u << (13 * 2));     /* 00 = đầu vào */

    GPIOC->PUPDR &= ~(3u << (13 * 2));
    GPIOC->PUPDR |=  (1u << (13 * 2));     /* 01 = kéo lên */
}

/* Nút nối xuống đất, nên nhả là mức CAO, nhấn là mức THẤP. */
uint8_t nut_dang_nhan(void)
{
    return (GPIOC->IDR & (1u << 13)) ? 0u : 1u;
}
Điện trở kéo
Một điện trở bên trong chip nối chân với nguồn hoặc với đất. Nó giữ cho chân có mức xác định khi không có gì điều khiển nó, tránh trạng thái lơ lửng.
PUPDRNghĩaKhi chân không nối gìDùng khi
00Không kéoLơ lửng, đọc ra giá trị ngẫu nhiênChân luôn được một mạch ngoài điều khiển
01Kéo lênMức caoNút nối chân xuống đất, phổ biến nhất
10Kéo xuốngMức thấpNút nối chân lên nguồn
11Dành riêngKhông dùngKhông bao giờ

Đọc nhiều chân cùng lúc

doc-nhieu-chan.c
/* Đọc 4 chân PB0 tới PB3 làm một số 4 bit,
   ví dụ từ một công tắc DIP hoặc bàn phím ma trận. */
uint8_t doc_dip(void)
{
    uint32_t idr = GPIOB->IDR;              /* đọc MỘT lần, mọi chân đồng thời */

    return (uint8_t)((~idr) & 0x0Fu);       /* đảo vì kéo lên, lấy 4 bit thấp */
}

#Gói lại thành thư viện nhỏ

gpio.h
#ifndef GPIO_H
#define GPIO_H

#include <stdint.h>

#include "stm32f4.h"

typedef enum {
    GPIO_VAO      = 0u,
    GPIO_RA       = 1u,
    GPIO_THAY_THE = 2u,
    GPIO_TUONG_TU = 3u,
} GpioCheDo;

typedef enum {
    GPIO_KHONG_KEO = 0u,
    GPIO_KEO_LEN   = 1u,
    GPIO_KEO_XUONG = 2u,
} GpioKeo;

void gpio_bat_clock(const GPIO_TypeDef *cong);
void gpio_cau_hinh(GPIO_TypeDef *cong, uint8_t chan,
                   GpioCheDo che_do, GpioKeo keo);

static inline void gpio_bat(GPIO_TypeDef *cong, uint8_t chan)
{
    cong->BSRR = (1u << chan);
}

static inline void gpio_tat(GPIO_TypeDef *cong, uint8_t chan)
{
    cong->BSRR = (1u << (chan + 16u));
}

static inline void gpio_ghi(GPIO_TypeDef *cong, uint8_t chan, uint8_t muc)
{
    cong->BSRR = muc ? (1u << chan) : (1u << (chan + 16u));
}

static inline uint8_t gpio_doc(const GPIO_TypeDef *cong, uint8_t chan)
{
    return (cong->IDR & (1u << chan)) ? 1u : 0u;
}

#endif
gpio.c
#include "gpio.h"

void gpio_bat_clock(const GPIO_TypeDef *cong)
{
    uint32_t bit;

    if      (cong == GPIOA) bit = 0u;
    else if (cong == GPIOB) bit = 1u;
    else if (cong == GPIOC) bit = 2u;
    else if (cong == GPIOD) bit = 3u;
    else                    return;

    RCC->AHB1ENR |= (1u << bit);

    volatile uint32_t tam = RCC->AHB1ENR;      /* chờ clock ổn định */
    (void)tam;
}

void gpio_cau_hinh(GPIO_TypeDef *cong, uint8_t chan,
                   GpioCheDo che_do, GpioKeo keo)
{
    if (chan > 15u) return;

    gpio_bat_clock(cong);

    /* Điện trở kéo TRƯỚC, chế độ SAU, để chân không phát mức lạ */
    uint32_t r = cong->PUPDR;

    r &= ~(3u << (chan * 2u));
    r |=  ((uint32_t)keo << (chan * 2u));
    cong->PUPDR = r;

    r  = cong->MODER;
    r &= ~(3u << (chan * 2u));
    r |=  ((uint32_t)che_do << (chan * 2u));
    cong->MODER = r;
}
main.c
#include "gpio.h"

#define DEN_CONG  GPIOA
#define DEN_CHAN  5u
#define NUT_CONG  GPIOC
#define NUT_CHAN  13u

int main(void)
{
    gpio_cau_hinh(DEN_CONG, DEN_CHAN, GPIO_RA,  GPIO_KHONG_KEO);
    gpio_cau_hinh(NUT_CONG, NUT_CHAN, GPIO_VAO, GPIO_KEO_LEN);

    for (;;) {
        uint8_t nhan = !gpio_doc(NUT_CONG, NUT_CHAN);   /* đảo vì kéo lên */

        gpio_ghi(DEN_CONG, DEN_CHAN, nhan);
    }
}
terminal
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -O2 -Wall -Wextra -c gpio.c main.c
arm-none-eabi-size gpio.o main.o
   text    data     bss     dec     hex filename
    148       0       0     148      94 gpio.o
     72       0       0      72      48 main.o

Tự làm thử

  1. Cấu hình một chân làm đầu ra và cho đèn nhấp nháy, rồi cố tình bỏ dòng bật clock và xác nhận đọc lại MODER cho 0.
  2. Viết hai hàm bật đèn, một dùng ODR |= và một dùng BSRR, rồi so mã hợp ngữ sinh ra.
  3. Đọc một chân đầu vào ở chế độ không kéo, in ra một trăm lần và ghi lại tỷ lệ số 0 và số 1.
  4. Cài thư viện GPIO nhỏ trong bài và dùng nó viết chương trình đèn sáng khi nhấn nút.
  5. Dùng BSRR đặt hai chân và xóa hai chân trong một lệnh ghi, rồi quan sát bằng máy hiện sóng hoặc bằng bộ phân tích logic.

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

  • Cấu hình một chân gồm năm bước, và bước bật clock trong RCC phải đứng trước mọi bước khác.
  • Không bật clock thì ngoại vi im lặng hoàn toàn: ghi vào trôi đi, đọc ra cho 0, không có lỗi nào báo.
  • Trường hai bit cần mặt nạ 3u, và phải xóa trước khi đặt. Dùng mặt nạ một bit là lỗi im lặng.
  • BSRR ghi một lệnh duy nhất nên nguyên tử, còn ODR |= là ba lệnh và có thể mất dữ liệu khi ISR xen vào.
  • Chân đầu vào không có điện trở kéo sẽ lơ lửng và đọc ra giá trị ngẫu nhiên. Luôn chọn kéo lên hoặc kéo xuống.