Bỏ qua điều hướng, tới nội dung chính
Học C
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íMacroHàm static inline
Kiểm tra kiểu tham sốKhôngCó
Đánh giá tham số một lầnKhông bảo đảmLuôn luôn
Che bóng biếnCó thểKhông bao giờ
Đặt điểm dừng khi gỡ lỗiKhôngĐược
Thông báo lỗi khi dùng saiRất khó đọcRõ ràng
Tốc độ với -O2Như nhauNhư nhau
Tốc độ với -O0Nhanh hơnCó lời gọi hàm thật
Dùng được trong biểu thức hằngCó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); }

#endif
main.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ânCách chữa
Hàm quá lớn theo ước lượng của trình biên dịchTá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àmKhô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ướcThườ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ácChuyể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ử

  1. Viết header đặt tên cho mọi bit của USART1->CR1 và dùng chúng viết lại hàm khởi tạo UART ở Bài 47.6.
  2. Viết macro DAT_TRUONG quê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.
  3. 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á.
  4. Khai báo inline trần trong header và xem lỗi liên kết, rồi đổi thành static inline.
  5. 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, -Og và -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 _Pos và _Msk là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 inline thắ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ằng objdump thay vì đoán.