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

volatile và atomic

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

  • Giải thích chính xác volatile ngăn tối ưu nào
  • Chỉ ra ba trường hợp volatile không đủ
  • Dùng sig_atomic_t và atomic_flag cho cờ chia sẻ
  • Biết khi nào cần rào cản bộ nhớ trên lõi nhiều nhân

Bài 47.3 đã nói volatile làm gì với thanh ghi phần cứng. Bài này nói về nó trong bối cảnh dữ liệu chia sẻ với ngắt: nó giải quyết được đúng một vấn đề, và có ba vấn đề khác nó hoàn toàn không chạm tới.

#volatile ngăn tối ưu nào

Tối ưuvolatile có ngăn khôngVí dụ
Giữ giá trị trong thanh ghi thay vì đọc lại RAMCóVòng lặp chờ cờ thành vô hạn
Xóa lệnh ghi bị lệnh ghi sau đè lênCóChuỗi mở khóa bị mất bước đầu
Xóa lệnh đọc mà kết quả không ai dùngCóĐọc DR để xóa cờ bị bỏ
Đổi thứ tự giữa hai truy cập volatileCóGhi dữ liệu rồi ghi chỉ số bộ đệm vòng
Đổi thứ tự giữa truy cập volatile và biến thườngKhôngPhép tính bị chuyển ra ngoài cặp lệnh đo thời gian
Gộp ba lệnh đọc sửa ghi thành thao tác nguyên tửKhôngBộ đếm bị mất một lần tăng
Ngăn bộ xử lý sắp xếp lại lệnhKhôngDMA đọc dữ liệu chưa ghi xong
co-tu-isr.c
/* Vấn đề duy nhất mà volatile giải quyết trọn vẹn:
   trình biên dịch không biết ISR tồn tại. */

static volatile uint8_t co_du_lieu = 0u;

void USART1_IRQHandler(void)
{
    (void)USART1->DR;
    co_du_lieu = 1u;
}

int main(void)
{
    while (co_du_lieu == 0u) { }      /* đọc lại RAM mỗi vòng */

    xu_ly();

    return 0;
}
terminal
arm-none-eabi-gcc -O2 -S khong-volatile.c -o - | sed -n '/^main:/,/bx/p'
main:
        ldr     r3, .L4
        ldrb    r3, [r3]        @ doc MOT lan
        cbnz    r3, .L2
.L3:
        b       .L3             @ lap vo han
arm-none-eabi-gcc -O2 -S co-volatile.c -o - | sed -n '/^main:/,/bx/p'
main:
        ldr     r2, .L5
.L2:
        ldrb    r3, [r2]        @ doc LAI moi vong
        cmp     r3, #0
        beq     .L2

#Ba trường hợp volatile không đủ

Trường hợp 1: phép đọc sửa ghi

khong-du-1.c
volatile uint32_t g_dem = 0u;

void TIM2_IRQHandler(void)
{
    TIM2->SR = ~(1u << 0);
    ++g_dem;              /* ISR tang */
}

void main_giam(void)
{
    --g_dem;              /* main giam */
}

/* Ca hai deu la ba lenh. Neu ngat chen vao giua ba lenh cua main:

     main: ldr  r3, [r0]      @ doc 100
     --- NGAT: g_dem thanh 101 ---
     main: subs r3, r3, #1    @ tinh 100 - 1 = 99
     main: str  r3, [r0]      @ ghi 99

   Ket qua 99 thay vi 100. Lan tang cua ISR bi MAT. */
terminal
> thu-dem --isr-tang 100000 --main-giam 100000
gia tri mong doi : 0
gia tri thuc te  : -347

347 lan tang bi mat.

Trường hợp 2: dữ liệu rộng hơn một lệnh

khong-du-2.c
volatile uint64_t g_moc;      /* 64 bit, doc bang hai lenh */

typedef struct { uint16_t x, y; } Diem;

volatile Diem g_vi_tri;       /* hai truong, phai nhat quan */

/* Ca hai truong hop deu can vung toi han, xem Bai 49.4.
   volatile chi bao dam moi lenh duoc sinh ra, khong bao dam
   chung chay lien mach. */

Trường hợp 3: thứ tự với biến thường

khong-du-3.c
uint32_t ket_qua;              /* bien THUONG */
volatile uint32_t co_xong;     /* volatile */

void tinh_toan(void)
{
    ket_qua = tinh_gi_do();    /* lenh thuong */
    co_xong = 1u;              /* lenh volatile */
}

/* Chuan C KHONG cam trinh bien dich chuyen lenh gan ket_qua
   xuong SAU lenh gan co_xong.

   Neu no lam vay, va ISR doc co_xong roi doc ket_qua ngay,
   thi ISR thay co_xong bang 1 nhung ket_qua chua duoc ghi. */

/* Cach chua 1: cho ket_qua cung volatile */
volatile uint32_t ket_qua;

/* Cach chua 2: rao can trinh bien dich giua hai lenh */
ket_qua = tinh_gi_do();
__asm volatile ("" ::: "memory");
co_xong = 1u;

#sig_atomic_t và atomic của C11

sig_atomic_t
Kiểu do chuẩn C định nghĩa trong <signal.h>, bảo đảm đọc hoặc ghi nó là một thao tác không thể bị cắt ngang bởi tín hiệu. Trên Cortex-M nó thường là int, tức 32 bit.
sig-atomic.c
#include <signal.h>

volatile sig_atomic_t co_du_lieu = 0;

void USART1_IRQHandler(void)
{
    (void)USART1->DR;
    co_du_lieu = 1;        /* ghi nguyen tu, bao dam boi chuan */
}

int main(void)
{
    while (co_du_lieu == 0) { }

    co_du_lieu = 0;
    xu_ly();

    return 0;
}

/* Van can volatile: sig_atomic_t bao dam TINH NGUYEN TU,
   khong bao dam trinh bien dich doc lai tu RAM.
   Hai thu khac nhau, va can ca hai. */
stdatomic.c
#include <stdatomic.h>

static atomic_uint g_dem = 0u;

void TIM2_IRQHandler(void)
{
    TIM2->SR = ~(1u << 0);

    atomic_fetch_add(&g_dem, 1u);      /* tang NGUYEN TU */
}

uint32_t doc_dem(void)
{
    return atomic_load(&g_dem);
}

/* Tren Cortex-M3 tro len, trinh bien dich sinh ra vong lap
   LDREX va STREX. Tren Cortex-M0 khong co hai lenh do,
   nen no goi mot ham thu vien tu tat ngat. */
terminal
arm-none-eabi-gcc -mcpu=cortex-m4 -O2 -S atomic.c -o - | sed -n '/TIM2_IRQHandler:/,/bx/p'
TIM2_IRQHandler:
        ldr     r2, .L4
.L2:
        ldrex   r3, [r2]
        adds    r3, r3, #1
        strex   r1, r3, [r2]
        cmp     r1, #0
        bne     .L2               @ that bai thi thu lai
        bx      lr
arm-none-eabi-gcc -mcpu=cortex-m0 -O2 -S atomic.c -o - | grep bl
        bl      __atomic_fetch_add_4
CáchChi phíDùng khi
volatile một mình0 chu kỳ phụCờ một byte, hoặc biến 32 bit chỉ một bên ghi
volatile sig_atomic_t0 chu kỳ phụCùng trên, nhưng viết rõ ý định hơn
atomic của C11, chip có LDREXKhoảng 5 chu kỳBộ đếm nhiều bên cùng sửa
atomic của C11, chip không có LDREXKhoảng 20 chu kỳ, có tắt ngắtCùng trên, nhưng cân nhắc dùng vùng tới hạn tường minh
Vùng tới hạn tường minhKhoảng 6 chu kỳ, cộng thời gian trong vùngNhiều biến phải nhất quán cùng lúc

#Rào cản trình biên dịch và rào cản bộ nhớ

Rào cản trình biên dịch
Một chỉ thị nói với trình biên dịch đừng chuyển lệnh qua điểm này. Nó không sinh ra lệnh máy nào.
Rào cản bộ nhớ
Một lệnh máy thật, nói với bộ xử lý và hệ thống bus rằng mọi truy cập trước nó phải hoàn tất trước khi truy cập sau nó bắt đầu.
rao-can.c
/* Rao can TRINH BIEN DICH: khong sinh lenh nao */
#define RAO_CAN_BIEN_DICH()   __asm volatile ("" ::: "memory")

/* Rao can BO NHO cua ARM, sinh lenh that: */
__DMB();      /* Data Memory Barrier: moi truy cap bo nho truoc no
                 phai xong truoc khi truy cap sau no bat dau */

__DSB();      /* Data Synchronization Barrier: manh hon, cho MOI
                 lenh truoc do hoan tat, ke ca ghi vao ngoai vi */

__ISB();      /* Instruction Synchronization Barrier: xoa duong ong
                 lenh, buoc doc lai lenh tu bo nho */
Rào cảnSinh lệnh máyDùng khi
Rào cản trình biên dịchKhôngChỉ cần giữ thứ tự trong mã, ví dụ đo thời gian bằng GPIO
__DMBCóTrước khi cho DMA đọc dữ liệu bạn vừa ghi
__DSBCóSau khi ghi thanh ghi điều khiển mà lệnh sau phụ thuộc vào
__ISBCóSau khi đổi bảng vector hoặc đổi cấu hình bộ nhớ
khi-nao-can.c
/* 1. Ghi du lieu roi cho DMA doc */
bo_dem[0] = 0x12u;
bo_dem[1] = 0x34u;

__DMB();                            /* bao dam hai lenh ghi da toi RAM */

DMA1_Stream0->CR |= 1u;             /* gio moi bat DMA */

/* 2. Xoa co ngat roi tra ve khoi ISR */
EXTI->PR = (1u << 0);

__DSB();                            /* bao dam lenh ghi da toi ngoai vi */

/* 3. Doi bang vector roi bat ngat */
SCB->VTOR = 0x08008000u;

__DSB();
__ISB();                            /* xoa duong ong, doc lai lenh */

__enable_irq();

/* 4. Bat MPU roi truy cap vung vua cau hinh */
MPU->CTRL = 1u;

__DSB();
__ISB();

Ví dụ cụ thể: đo thời gian bằng chân GPIO

do-thoi-gian.c
/* KHONG co rao can: trinh bien dich co the chuyen tinh_toan()
   ra ngoai cap lenh bat tat chan, va ban do ra 4 chu ky. */
GPIOA->BSRR = (1u << 7);
uint32_t v = tinh_toan();
GPIOA->BSRR = (1u << (7 + 16));

/* CO rao can: dam bao tinh_toan chay giua hai canh. */
GPIOA->BSRR = (1u << 7);
RAO_CAN_BIEN_DICH();

uint32_t v = tinh_toan();

RAO_CAN_BIEN_DICH();
GPIOA->BSRR = (1u << (7 + 16));
terminal
# do bang bo phan tich logic
khong rao can : do rong xung 0.05 us  (chi 4 chu ky, SAI)
co rao can    : do rong xung 2.41 us  (dung)

#Bảng chọn cuối cùng

Tình huốngCần gìChi phí
Cờ uint8_t, ISR đặt, main xóavolatileKhông có
Biến uint32_t, chỉ ISR ghi, main đọcvolatileKhông có
Biến uint32_t, cả hai cùng sửavolatile cộng atomic hoặc vùng tới hạn5 tới 20 chu kỳ
Biến uint64_t hoặc struct nhiều trườngvolatile cộng vùng tới hạn6 chu kỳ cộng thời gian trong vùng
Bộ đệm vòng một ghi một đọcvolatile cho hai chỉ số và mảngKhông có
Ghi bộ đệm rồi cho DMA đọcvolatile cộng __DMBVài chu kỳ
Đo thời gian bằng GPIORào cản trình biên dịchMất một phần tối ưu ở đoạn đó
Biến chỉ ISR dùngKhông cần gì, đừng dùng volatileKhông có
Biến chỉ main dùngKhông cần gìKhông có
tong-hop.c
#include <stdint.h>
#include <signal.h>

/* Co don gian: volatile la du */
static volatile sig_atomic_t co_tick = 0;

/* Bo dem vong: CA HAI chi so VA mang deu volatile */
#define N 256u
static volatile uint8_t  ring[N];
static volatile uint16_t ring_ghi = 0u;
static volatile uint16_t ring_doc = 0u;

/* Bo dem ca hai ben cung sua: can atomic */
static atomic_uint g_tong_byte = 0u;

/* Bien chi ISR dung: KHONG volatile */
static uint16_t isr_dem_noi_bo = 0u;

/* Bien chi main dung: KHONG volatile */
static uint32_t main_bo_dem_tam[64];

void USART1_IRQHandler(void)
{
    uint32_t sr = USART1->SR;

    if (sr & (1u << 5)) {
        uint8_t  b  = (uint8_t)USART1->DR;
        uint16_t ke = (uint16_t)((ring_ghi + 1u) & (N - 1u));

        if (ke != ring_doc) {
            ring[ring_ghi] = b;      /* ghi du lieu TRUOC */
            ring_ghi       = ke;     /* roi cong bo */

            atomic_fetch_add(&g_tong_byte, 1u);
        }
    }

    ++isr_dem_noi_bo;                /* chi ISR dung, khong can volatile */
}

Kiểm bằng công cụ

terminal
# Cppcheck bắt được vài trường hợp thiếu volatile
cppcheck --enable=all --platform=arm32-wchar_t2 src/
src/main.c:24:5: style: Variable 'co_du_lieu' is modified in ISR but not declared volatile. [unusedVariable]
# Tìm mọi biến toàn cục không phải static và không volatile
arm-none-eabi-nm -g --defined-only firmware.elf | grep ' [BbDd] '
20000004 B co_du_lieu
20000008 B g_tick
2000000c D g_cau_hinh

Ra soat tung cai xem co bi ISR dung khong.

Tự làm thử

  1. Viết cờ chia sẻ với ISR không có volatile, biên dịch với -O2, và tìm vòng lặp vô hạn trong mã hợp ngữ.
  2. Cho ISR tăng và vòng lặp chính giảm cùng một bộ đếm volatile, chạy một trăm nghìn lần mỗi bên và đếm số lần mất.
  3. Thay bộ đếm đó bằng atomic_uint và xác nhận kết quả cuối bằng 0.
  4. Biên dịch cùng mã atomic cho Cortex-M4 và Cortex-M0, so mã hợp ngữ sinh ra.
  5. Đo thời gian một hàm bằng chân GPIO, có và không có rào cản trình biên dịch, so hai kết quả.

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

  • volatile giải quyết đúng một vấn đề: trình biên dịch không biết ISR tồn tại nên tưởng biến không đổi.
  • Nó không làm phép đọc sửa ghi thành nguyên tử, không bảo vệ dữ liệu rộng hơn một lệnh, và không giữ thứ tự với biến thường.
  • sig_atomic_t bảo đảm đọc và ghi đơn lẻ là nguyên tử, còn phép tăng vẫn cần atomic của C11 hoặc vùng tới hạn.
  • Rào cản trình biên dịch không sinh lệnh nào, rào cản bộ nhớ có. Trên Cortex-M một lõi thì rào cản bộ nhớ hiếm khi cần.
  • Bốn câu hỏi quyết định: có chia sẻ không, cả hai có ghi không, dữ liệu có rộng hơn một lệnh không, có tác nhân ngoài CPU không.