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

Thanh ghi và volatile

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

  • Khai báo thanh ghi bằng macro và bằng struct kiểu CMSIS
  • Giải thích chính xác volatile ngăn trình biên dịch làm gì
  • Chỉ ra ba lỗi kinh điển khi thiếu volatile
  • Biết volatile không bảo đảm tính nguyên tử và không thay được rào cản bộ nhớ

Từ khóa volatile nói với trình biên dịch rằng ô nhớ này có thể đổi giá trị mà không do mã của bạn gây ra. Thiếu nó, mã tối ưu đúng theo chuẩn C nhưng sai hoàn toàn với phần cứng.

#Hai cách khai báo thanh ghi

Cách 1: macro cho từng thanh ghi

macro.h
#include <stdint.h>

#define GPIOA_BASE    0x40020000UL

#define GPIOA_MODER   (*(volatile uint32_t *)(GPIOA_BASE + 0x00))
#define GPIOA_OTYPER  (*(volatile uint32_t *)(GPIOA_BASE + 0x04))
#define GPIOA_IDR     (*(volatile uint32_t *)(GPIOA_BASE + 0x10))
#define GPIOA_ODR     (*(volatile uint32_t *)(GPIOA_BASE + 0x14))
#define GPIOA_BSRR    (*(volatile uint32_t *)(GPIOA_BASE + 0x18))

/* Dùng: */
GPIOA_ODR |= (1u << 5);

Cách 2: struct kiểu CMSIS

stm32-gpio.h
#include <stdint.h>

typedef struct {
    volatile uint32_t MODER;      /* 0x00 chế độ chân */
    volatile uint32_t OTYPER;     /* 0x04 kiểu đầu ra */
    volatile uint32_t OSPEEDR;    /* 0x08 tốc độ */
    volatile uint32_t PUPDR;      /* 0x0C điện trở kéo */
    volatile uint32_t IDR;        /* 0x10 dữ liệu vào, chỉ đọc */
    volatile uint32_t ODR;        /* 0x14 dữ liệu ra */
    volatile uint32_t BSRR;       /* 0x18 đặt và xóa bit, chỉ ghi */
    volatile uint32_t LCKR;       /* 0x1C khóa cấu hình */
    volatile uint32_t AFR[2];     /* 0x20 chức năng thay thế */
} GPIO_TypeDef;

#define GPIOA ((GPIO_TypeDef *)0x40020000UL)
#define GPIOB ((GPIO_TypeDef *)0x40020400UL)
#define GPIOC ((GPIO_TypeDef *)0x40020800UL)

/* Dùng: */
GPIOA->ODR |= (1u << 5);
MacroStruct
Số dòng khai báo cho ba cổng2713
Thêm một cổng mớiChép lại 9 dòngMột dòng define
Viết hàm nhận cổng làm tham sốKhông đượcĐược, nhận GPIO_TypeDef *
Trình biên dịch kiểm được độ dờiKhôngCó, bằng _Static_assert
Nguy cơ sai độ dờiMỗi dòng một cơ hội saiChỉ cần thứ tự trường đúng
Là chuẩn của nhà sản xuấtKhôngCó, mọi header CMSIS đều vậy

#Vì sao volatile là bắt buộc

volatile
Nói với trình biên dịch: mỗi lần mã nguồn đọc biến này thì phải sinh ra một lệnh đọc thật, và mỗi lần ghi thì phải sinh ra một lệnh ghi thật. Không được gộp, không được bỏ, không được đổi thứ tự với các truy cập volatile khác.
Không có volatile
uint32_t *ODR = (uint32_t *)0x40020014;

*ODR |=  (1u << 5);      /* bật đèn */
delay(1000);
*ODR &= ~(1u << 5);      /* tắt đèn */

/* Trình biên dịch lập luận:
   ghi rồi ghi lại cùng ô, không ai đọc ở giữa,
   nên lần ghi đầu là thừa -> XÓA BỎ.
   Đèn không bao giờ sáng. */
Có volatile
volatile uint32_t *ODR = (volatile uint32_t *)0x40020014;

*ODR |=  (1u << 5);
delay(1000);
*ODR &= ~(1u << 5);

/* Cả hai lệnh ghi đều được sinh ra, đúng thứ tự. */
terminal
arm-none-eabi-gcc -O2 -S khong-volatile.c -o - | grep -c str
1
# Chỉ MỘT lệnh ghi được sinh ra, lệnh bật đèn bị xóa
arm-none-eabi-gcc -O2 -S co-volatile.c -o - | grep -c str
2

#Ba lỗi kinh điển khi thiếu volatile

Lỗi 1: vòng lặp chờ cờ bị tối ưu thành vô hạn

loi-1.c
/* Chờ thanh ghi phát trống rồi mới ghi byte tiếp theo */
uint32_t *SR = (uint32_t *)0x40011000;      /* thiếu volatile */

while (!(*SR & (1u << 7))) { }               /* chờ cờ TXE */

/* Trình biên dịch lập luận:
   *SR không đổi trong thân vòng lặp,
   nên đọc một lần rồi dùng lại kết quả.
   Nếu lần đọc đầu cho 0 thì vòng lặp thành while(1). */
terminal
arm-none-eabi-gcc -O2 -S loi-1.c -o -
cho_txe:
        ldr     r3, [r0]        @ doc MOT lan
        tst     r3, #128
        beq     .L2             @ neu bit 7 = 0 thi ...
.L2:
        b       .L2             @ ... lap vo han, khong doc lai
        bx      lr
# Thêm volatile
arm-none-eabi-gcc -O2 -S dung-1.c -o -
cho_txe:
.L2:
        ldr     r3, [r0]        @ doc LAI moi vong
        tst     r3, #128
        beq     .L2
        bx      lr

Lỗi 2: cờ đặt trong ISR không bao giờ thấy được

loi-2.c
int co_du_lieu = 0;      /* thiếu volatile */

void USART1_IRQHandler(void)
{
    co_du_lieu = 1;
}

int main(void)
{
    khoi_tao();

    while (co_du_lieu == 0) { }      /* treo vĩnh viễn */

    xu_ly();

    return 0;
}

/* Trình biên dịch không biết USART1_IRQHandler tồn tại,
   càng không biết nó chạy bất cứ lúc nào.
   Nó thấy co_du_lieu không đổi trong vòng lặp -> tối ưu thành while(1). */

Lỗi 3: hai lần ghi liên tiếp bị gộp thành một

loi-3.c
/* Nhiều ngoại vi cần chuỗi lệnh ghi cụ thể để mở khóa */
uint32_t *KEY = (uint32_t *)0x40002000;      /* thiếu volatile */

*KEY = 0xCA;      /* bước 1 của chuỗi mở khóa */
*KEY = 0x53;      /* bước 2 */

/* Trình biên dịch thấy lần ghi đầu bị lần sau ghi đè,
   nên XÓA nó. Chuỗi mở khóa không hoàn tất, ngoại vi vẫn khóa. */
LỗiTriệu chứngRất khó tìm vì
Vòng lặp chờ cờThiết bị đứng im ngay lúc khởi độngChạy đúng ở mức tối ưu O0, hỏng ở O2
Cờ từ ISRVòng lặp chính không bao giờ chạy tiếpCùng lý do trên
Ghi bị gộpMột ngoại vi cụ thể không hoạt độngMọi thứ khác vẫn chạy, chỉ một khối chết

#Điều volatile không bảo đảm

#Đặt volatile đúng chỗ

vi-tri.c
/* Bài 12.5 đã nói quy tắc đọc khai báo. Áp dụng cho volatile: */

volatile uint32_t *p;         /* con trỏ THƯỜNG, trỏ tới uint32_t volatile */
uint32_t *volatile q;         /* con trỏ VOLATILE, trỏ tới uint32_t thường */
volatile uint32_t *volatile r;/* cả hai đều volatile */

/* Với thanh ghi, ta muốn dạng ĐẦU TIÊN:
   giá trị ở địa chỉ đó thay đổi bất ngờ,
   còn bản thân biến con trỏ thì không. */

Đừng lạm dụng volatile

Tự làm thử

  1. Viết struct GPIO_TypeDef đầy đủ kèm sáu khẳng định lúc biên dịch, rồi cố tình bỏ một trường và xem lỗi.
  2. Viết vòng lặp chờ cờ không có volatile, biên dịch với -O0 và -O2, so mã hợp ngữ sinh ra.
  3. Viết cả ba lỗi kinh điển trong bài và xác nhận từng cái biến mất khi thêm volatile.
  4. Xem mã hợp ngữ của bo_dem++ với biến volatile và đếm số lệnh, rồi giải thích vì sao nó không nguyên tử.
  5. Đo thời gian một vòng lặp cộng dồn với biến thường và với biến volatile, ghi lại tỷ lệ.

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

  • Khai báo thanh ghi bằng struct kiểu CMSIS gọn hơn macro, cho viết hàm dùng chung, và kiểm được độ dời bằng _Static_assert.
  • volatile buộc trình biên dịch sinh ra đúng số lệnh đọc ghi như mã nguồn viết, không gộp và không bỏ.
  • Ba lỗi kinh điển khi thiếu nó: vòng lặp chờ cờ thành vô hạn, cờ từ ISR không thấy được, và chuỗi lệnh ghi bị gộp.
  • Cả ba lỗi chỉ xuất hiện khi bật tối ưu, nên hãy luôn phát triển ở đúng mức tối ưu sẽ dùng khi phát hành.
  • volatile không làm thao tác thành nguyên tử và không thay được rào cản bộ nhớ. Đừng dùng nó ngoài ba trường hợp cần thật.