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
#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));
}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.Đặt chế độ trong MODER
Hai bit mỗi chân:
00vào,01ra,10chức năng thay thế,11tương tự. Phải xóa hai bit cũ trước khi đặt.Chọn kiểu đầu ra trong OTYPER
Một bit mỗi chân:
0là đẩy kéo, tức chân tự kéo lên cao và xuống thấp.1là 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.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à đủ.
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
| Bus | Thanh ghi bật clock | Ngoại vi trên bus đó |
|---|---|---|
| AHB1 | RCC->AHB1ENR | GPIOA tới GPIOK, DMA1, DMA2, CRC |
| AHB2 | RCC->AHB2ENR | USB OTG, camera, bộ sinh số ngẫu nhiên |
| APB1 | RCC->APB1ENR | TIM2 tới TIM7, USART2, USART3, I2C, SPI2 |
| APB2 | RCC->APB2ENR | TIM1, TIM8, USART1, USART6, SPI1, ADC, SYSCFG |
#BSRR thay cho đọc sửa ghi
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. */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. */dung_odr:
ldr r3, [r0, #20]
orr r3, r3, #32
str r3, [r0, #20]
bx lrdung_bsrr:
mov r3, #32
str r3, [r0, #24]
bx lrĐảo mức chân
/* 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
/* 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;
}| PUPDR | Nghĩa | Khi chân không nối gì | Dùng khi |
|---|---|---|---|
| 00 | Không kéo | Lơ lửng, đọc ra giá trị ngẫu nhiên | Chân luôn được một mạch ngoài điều khiển |
| 01 | Kéo lên | Mức cao | Nút nối chân xuống đất, phổ biến nhất |
| 10 | Kéo xuống | Mức thấp | Nút nối chân lên nguồn |
| 11 | Dành riêng | Không dùng | Không bao giờ |
Đọc nhiều chân cùng lú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ỏ
#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#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;
}#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);
}
} text data bss dec hex filename
148 0 0 148 94 gpio.o
72 0 0 72 48 main.oTự làm thử
- 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
MODERcho 0. - Viết hai hàm bật đèn, một dùng
ODR |=và một dùngBSRR, rồi so mã hợp ngữ sinh ra. - Đọ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.
- 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.
- 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
RCCphả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. BSRRghi một lệnh duy nhất nên nguyên tử, cònODR |=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.