Bài 48.224 phút đọc
Macro và hàm nội tuyến cho thanh ghi
Sau bài này bạn sẽ làm được
- Viết macro mặt nạ và macro đặt trường an toàn
- Chuyển macro thành hàm static inline và nêu bốn lợi ích
- Dùng khẳng định lúc biên dịch để chặn tham số ngoài miền
- Kiểm mã sinh ra vẫn gọn như viết tay
Một dòng TIM3->CCMR1 |= (6u << 4) không nói gì cả. Cùng dòng đó viết là TIM_OC1M_PWM1 thì ai đọc cũng hiểu. Bài này về cách đặt tên cho bit, và về việc hàm nội tuyến gần như luôn tốt hơn macro.
#Đặt tên cho từng bit
Số bit trần
USART1->CR1 = (1u << 3) | (1u << 2) | (1u << 13);
/* Ba số ma thuật. Sáu tháng sau chính bạn cũng phải mở
reference manual mới biết chúng là gì. */Tên có nghĩa
#define USART_CR1_RE (1u << 2) /* Receiver Enable */
#define USART_CR1_TE (1u << 3) /* Transmitter Enable */
#define USART_CR1_UE (1u << 13) /* USART Enable */
USART1->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;stm32-bit.h
/* Tự viết cho một ngoại vi, khi header nhà sản xuất chưa có
hoặc khi bạn muốn tên tiếng Việt cho dễ đọc. */
/* GPIO MODER, hai bit mỗi chân */
#define GPIO_MODER_Pos(chan) ((uint32_t)(chan) * 2u)
#define GPIO_MODER_Msk(chan) (0x3u << GPIO_MODER_Pos(chan))
#define GPIO_MODE_VAO (0x0u)
#define GPIO_MODE_RA (0x1u)
#define GPIO_MODE_THAY_THE (0x2u)
#define GPIO_MODE_TUONG_TU (0x3u)
/* GPIO OTYPER, một bit mỗi chân */
#define GPIO_OTYPER_Msk(chan) (0x1u << (chan))
#define GPIO_OTYPE_DAY_KEO (0x0u)
#define GPIO_OTYPE_MANG_HO (0x1u)
/* GPIO PUPDR, hai bit mỗi chân */
#define GPIO_PUPDR_Pos(chan) ((uint32_t)(chan) * 2u)
#define GPIO_PUPDR_Msk(chan) (0x3u << GPIO_PUPDR_Pos(chan))
#define GPIO_PUPD_KHONG (0x0u)
#define GPIO_PUPD_LEN (0x1u)
#define GPIO_PUPD_XUONG (0x2u)#Macro mặt nạ an toàn
mask.h
/* Đặt một trường: xóa mặt nạ rồi đặt giá trị. */
#define DAT_TRUONG(tg, msk, pos, val) \
do { \
uint32_t _r = (tg); \
_r &= ~(uint32_t)(msk); \
_r |= (((uint32_t)(val) << (pos)) & (msk)); \
(tg) = _r; \
} while (0)
/* Đọc một trường */
#define DOC_TRUONG(tg, msk, pos) (((uint32_t)(tg) & (msk)) >> (pos))
/* Dùng: */
DAT_TRUONG(GPIOA->MODER, GPIO_MODER_Msk(5), GPIO_MODER_Pos(5),
GPIO_MODE_RA);
uint32_t che_do = DOC_TRUONG(GPIOA->MODER, GPIO_MODER_Msk(5),
GPIO_MODER_Pos(5));#Hàm nội tuyến thay macro
reg.h
#include <stdint.h>
static inline void reg_dat_truong(volatile uint32_t *tg, uint32_t msk,
uint8_t pos, uint32_t val)
{
uint32_t r = *tg;
r &= ~msk;
r |= (val << pos) & msk;
*tg = r;
}
static inline uint32_t reg_doc_truong(const volatile uint32_t *tg,
uint32_t msk, uint8_t pos)
{
return (*tg & msk) >> pos;
}
static inline void reg_bat_bit(volatile uint32_t *tg, uint32_t msk)
{
*tg |= msk;
}
static inline void reg_tat_bit(volatile uint32_t *tg, uint32_t msk)
{
*tg &= ~msk;
}
static inline int reg_co_bit(const volatile uint32_t *tg, uint32_t msk)
{
return (*tg & msk) != 0u;
}| Tiêu chí | Macro | Hàm static inline |
|---|---|---|
| Kiểm tra kiểu tham số | Không | Có |
| Đánh giá tham số một lần | Không bảo đảm | Luôn luôn |
| Che bóng biến | Có thể | Không bao giờ |
| Đặt điểm dừng khi gỡ lỗi | Không | Được |
| Thông báo lỗi khi dùng sai | Rất khó đọc | Rõ ràng |
| Tốc độ với -O2 | Như nhau | Như nhau |
| Tốc độ với -O0 | Nhanh hơn | Có lời gọi hàm thật |
| Dùng được trong biểu thức hằng | Có | Không |
Gói lại thành API cho một ngoại vi
tim-pwm.h
#ifndef TIM_PWM_H
#define TIM_PWM_H
#include <stdint.h>
#include "stm32f4.h"
#define TIM_CCMR1_OC1M_Pos (4u)
#define TIM_CCMR1_OC1M_Msk (0x7u << TIM_CCMR1_OC1M_Pos)
#define TIM_OC_DONG_BANG (0x0u)
#define TIM_OC_BAT_KHI_KHOP (0x1u)
#define TIM_OC_TAT_KHI_KHOP (0x2u)
#define TIM_OC_DAO_KHI_KHOP (0x3u)
#define TIM_OC_PWM1 (0x6u)
#define TIM_OC_PWM2 (0x7u)
static inline void tim_dat_che_do_ra1(TIM_TypeDef *t, uint8_t che_do)
{
reg_dat_truong(&t->CCMR1, TIM_CCMR1_OC1M_Msk,
TIM_CCMR1_OC1M_Pos, che_do);
}
static inline void tim_dat_chu_ky(TIM_TypeDef *t, uint16_t arr)
{
t->ARR = arr;
}
static inline void tim_dat_do_rong1(TIM_TypeDef *t, uint16_t ccr)
{
t->CCR1 = ccr;
}
static inline void tim_bat(TIM_TypeDef *t) { reg_bat_bit(&t->CR1, 1u); }
static inline void tim_tat(TIM_TypeDef *t) { reg_tat_bit(&t->CR1, 1u); }
#endifmain.c
#include "tim-pwm.h"
int main(void)
{
pwm_chan_khoi_tao(); /* cấu hình GPIO */
tim_dat_che_do_ra1(TIM3, TIM_OC_PWM1);
tim_dat_chu_ky(TIM3, 999u);
tim_dat_do_rong1(TIM3, 500u); /* 50 phần trăm */
tim_bat(TIM3);
for (;;) { }
}#Kiểm tham số lúc biên dịch
kiem.h
#include <assert.h>
/* Chặn chân ngoài miền 0 tới 15 ngay lúc biên dịch,
nhưng chỉ khi tham số là hằng số. */
#define GPIO_KIEM_CHAN(chan) \
_Static_assert((chan) <= 15u, "so chan GPIO phai tu 0 toi 15")
/* Dùng: */
GPIO_KIEM_CHAN(5); /* ok */
GPIO_KIEM_CHAN(20); /* lỗi biên dịch */terminal
arm-none-eabi-gcc -std=c17 -c kiem.c
kiem.c:14:1: error: static assertion failed: "so chan GPIO phai tu 0 toi 15"
14 | GPIO_KIEM_CHAN(20);
| ^~~~~~~~~~~~~~Kiểm kiểu chặt hơn bằng kiểu liệt kê
kieu-chat.h
/* Thay uint8_t bang kieu liet ke, trinh bien dich se canh bao
khi truyen nham thu tu tham so. */
typedef enum {
GPIO_MODE_VAO = 0u,
GPIO_MODE_RA = 1u,
GPIO_MODE_THAY_THE = 2u,
GPIO_MODE_TUONG_TU = 3u,
} GpioCheDo;
typedef enum {
GPIO_PUPD_KHONG = 0u,
GPIO_PUPD_LEN = 1u,
GPIO_PUPD_XUONG = 2u,
} GpioKeo;
void gpio_cau_hinh(GPIO_TypeDef *cong, uint8_t chan,
GpioCheDo che_do, GpioKeo keo);
/* Truyen nham thu tu hai tham so cuoi: */
gpio_cau_hinh(GPIOA, 5, GPIO_PUPD_LEN, GPIO_MODE_RA);terminal
arm-none-eabi-gcc -std=c17 -Wall -Wextra -Wenum-conversion -c main.c
main.c:12:32: warning: implicit conversion from 'GpioKeo' to 'GpioCheDo' [-Wenum-conversion]
12 | gpio_cau_hinh(GPIOA, 5, GPIO_PUPD_LEN, GPIO_MODE_RA);
| ^~~~~~~~~~~~~#Kiểm mã sinh ra
Lo ngại thường gặp khi bọc mọi thứ vào hàm là mã sẽ chậm và to hơn. Với -O2 thì gần như không bao giờ, nhưng đừng tin, hãy kiểm.
so-sanh.c
#include "reg.h"
#include "stm32f4.h"
/* Bản viết tay */
void bat_den_tay(void)
{
GPIOA->BSRR = (1u << 5);
}
/* Bản qua hai lớp hàm nội tuyến */
void bat_den_ham(void)
{
gpio_bat(GPIOA, 5u);
}
/* Bản đặt trường qua hàm tổng quát */
void dat_che_do_ham(void)
{
reg_dat_truong(&GPIOA->MODER, GPIO_MODER_Msk(5),
GPIO_MODER_Pos(5), GPIO_MODE_RA);
}
/* Bản đặt trường viết tay */
void dat_che_do_tay(void)
{
uint32_t r = GPIOA->MODER;
r &= ~(3u << 10);
r |= (1u << 10);
GPIOA->MODER = r;
}terminal
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -O2 -c so-sanh.c
arm-none-eabi-objdump -d so-sanh.o
00000000 <bat_den_tay>: 0: 4b02 ldr r3, [pc, #8] 2: 2220 movs r2, #32 4: 619a str r2, [r3, #24] 6: 4770 bx lr 00000008 <bat_den_ham>: 8: 4b01 ldr r3, [pc, #4] a: 2220 movs r2, #32 c: 619a str r2, [r3, #24] e: 4770 bx lr Giong het nhau, tung lenh mot.
arm-none-eabi-size -A so-sanh.o | head -4
section size addr .text 64 0 4 ham, 16 byte moi ham, khong co ham nao lon hon ham khac
Khi nào nội tuyến không xảy ra
| Nguyên nhân | Cách chữa |
|---|---|
| Hàm quá lớn theo ước lượng của trình biên dịch | Tách phần nóng ra hàm nhỏ riêng, hoặc dùng __attribute__((always_inline)) |
| Hàm được gọi qua con trỏ hàm | Không chữa được, đó là bản chất của gọi gián tiếp |
| Hàm có vòng lặp mà số vòng không biết trước | Thường không đáng nội tuyến, để nguyên |
| Đang biên dịch với -O0 hoặc -Os quá gắt | Đổi mức tối ưu |
| Hàm định nghĩa ở tệp .c khác | Chuyển vào header, hoặc bật tối ưu toàn chương trình -flto |
bat-buoc-noi-tuyen.h
/* Khi that su can, ep noi tuyen. Chi dung khi da DO va thay can. */
#define LUON_NOI_TUYEN __attribute__((always_inline)) static inline
LUON_NOI_TUYEN void gpio_bat(GPIO_TypeDef *cong, uint8_t chan)
{
cong->BSRR = (1u << chan);
}
/* Nguoc lai, chan noi tuyen khi ham lon va goi nhieu cho: */
__attribute__((noinline)) void xu_ly_goi_tin(const uint8_t *d, uint16_t n)
{
...
}
/* Ca hai deu la mo rong cua GCC va Clang, khong thuoc chuan C. */Tự làm thử
- Viết header đặt tên cho mọi bit của
USART1->CR1và dùng chúng viết lại hàm khởi tạo UART ở Bài 47.6. - Viết macro
DAT_TRUONGquên bọc ngoặc, rồi gọi nó với tham số là một phép cộng và xem kết quả sai. - Viết cùng chức năng bằng macro và bằng hàm nội tuyến, truyền tham số có tác dụng phụ và đếm số lần nó được đánh giá.
- Khai báo
inlinetrần trong header và xem lỗi liên kết, rồi đổi thànhstatic inline. - So mã hợp ngữ của bản viết tay và bản qua hàm nội tuyến với
-O0,-Ogvà-O2.
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
- Đặt tên cho từng bit theo quy ước
_Posvà_Msklàm mã đọc được mà không cần mở reference manual. - Macro phải bọc mọi tham số trong ngoặc, bọc thân trong
do while (0), và đặt tên biến cục bộ có gạch dưới. - Hàm
static inlinethắng macro ở kiểm tra kiểu, số lần đánh giá tham số, khả năng gỡ lỗi, và chất lượng thông báo lỗi. - Macro chỉ thật sự cần khi bạn cần biểu thức hằng lúc biên dịch hoặc cần toán tử
#và##. - Với
-O2, hàm nội tuyến sinh ra mã giống hệt bản viết tay. Hãy kiểm bằngobjdumpthay vì đoán.