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 ưu | volatile có ngăn không | Ví dụ |
|---|---|---|
| Giữ giá trị trong thanh ghi thay vì đọc lại RAM | Có | Vòng lặp chờ cờ thành vô hạn |
| Xóa lệnh ghi bị lệnh ghi sau đè lên | Có | Chuỗi mở khóa bị mất bước đầu |
| Xóa lệnh đọc mà kết quả không ai dùng | Có | Đọc DR để xóa cờ bị bỏ |
| Đổi thứ tự giữa hai truy cập volatile | Có | 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ường | Không | Phé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ông | Bộ đếm bị mất một lần tăng |
| Ngăn bộ xử lý sắp xếp lại lệnh | Không | DMA đọ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 hanarm-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 lrarm-none-eabi-gcc -mcpu=cortex-m0 -O2 -S atomic.c -o - | grep bl
bl __atomic_fetch_add_4
| Cách | Chi phí | Dùng khi |
|---|---|---|
| volatile một mình | 0 chu kỳ phụ | Cờ một byte, hoặc biến 32 bit chỉ một bên ghi |
| volatile sig_atomic_t | 0 chu kỳ phụ | Cùng trên, nhưng viết rõ ý định hơn |
| atomic của C11, chip có LDREX | Khoảng 5 chu kỳ | Bộ đếm nhiều bên cùng sửa |
| atomic của C11, chip không có LDREX | Khoảng 20 chu kỳ, có tắt ngắt | Cù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 minh | Khoảng 6 chu kỳ, cộng thời gian trong vùng | Nhiề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ản | Sinh lệnh máy | Dùng khi |
|---|---|---|
| Rào cản trình biên dịch | Không | Chỉ cần giữ thứ tự trong mã, ví dụ đo thời gian bằng GPIO |
| __DMB | Có | Trước khi cho DMA đọc dữ liệu bạn vừa ghi |
| __DSB | Có | Sau khi ghi thanh ghi điều khiển mà lệnh sau phụ thuộc vào |
| __ISB | Có | 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ống | Cần gì | Chi phí |
|---|---|---|
| Cờ uint8_t, ISR đặt, main xóa | volatile | Không có |
| Biến uint32_t, chỉ ISR ghi, main đọc | volatile | Không có |
| Biến uint32_t, cả hai cùng sửa | volatile cộng atomic hoặc vùng tới hạn | 5 tới 20 chu kỳ |
| Biến uint64_t hoặc struct nhiều trường | volatile cộng vùng tới hạn | 6 chu kỳ cộng thời gian trong vùng |
| Bộ đệm vòng một ghi một đọc | volatile cho hai chỉ số và mảng | Không có |
| Ghi bộ đệm rồi cho DMA đọc | volatile cộng __DMB | Vài chu kỳ |
| Đo thời gian bằng GPIO | Rào cản trình biên dịch | Mất một phần tối ưu ở đoạn đó |
| Biến chỉ ISR dùng | Không cần gì, đừng dùng volatile | Không có |
| Biến chỉ main dùng | Khô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ử
- 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ữ. - 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. - Thay bộ đếm đó bằng
atomic_uintvà xác nhận kết quả cuối bằng 0. - Biên dịch cùng mã atomic cho Cortex-M4 và Cortex-M0, so mã hợp ngữ sinh ra.
- Đ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
volatilegiả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_tbảo đảm đọc và ghi đơn lẻ là nguyên tử, còn phép tăng vẫn cầnatomiccủ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.