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);| Macro | Struct | |
|---|---|---|
| Số dòng khai báo cho ba cổng | 27 | 13 |
| Thêm một cổng mới | Chép lại 9 dòng | Mộ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ời | Không | Có, bằng _Static_assert |
| Nguy cơ sai độ dời | Mỗi dòng một cơ hội sai | Chỉ cần thứ tự trường đúng |
| Là chuẩn của nhà sản xuất | Không | Có, 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 lrLỗ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ỗi | Triệu chứng | Rất khó tìm vì |
|---|---|---|
| Vòng lặp chờ cờ | Thiết bị đứng im ngay lúc khởi động | Chạy đúng ở mức tối ưu O0, hỏng ở O2 |
| Cờ từ ISR | Vòng lặp chính không bao giờ chạy tiếp | Cùng lý do trên |
| Ghi bị gộp | Một ngoại vi cụ thể không hoạt động | Mọ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ử
- 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. - Viết vòng lặp chờ cờ không có
volatile, biên dịch với-O0và-O2, so mã hợp ngữ sinh ra. - 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. - Xem mã hợp ngữ của
bo_dem++với biếnvolatilevà đếm số lệnh, rồi giải thích vì sao nó không nguyên tử. - Đ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. volatilebuộ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.
volatilekhô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.