Bài 48.126 phút đọc
Mẫu đọc sửa ghi
Sau bài này bạn sẽ làm được
- Chỉ ra vì sao chỉ dùng phép hoặc là sai với trường nhiều bit
- Viết mẫu xóa mặt nạ rồi đặt giá trị rồi ghi một lần
- Hiểu vì sao ghi đè cả thanh ghi phá cấu hình chân khác
- Biết khi nào một lần ghi thẳng là đúng
Một dòng như GPIOA->MODER |= (1 << 10) trông vô hại và có mặt trong hầu hết ví dụ trên mạng. Nó cũng là một trong những dòng sai phổ biến nhất trong lập trình nhúng, và bài này giải thích chính xác vì sao.
#Ba cách viết sai
Sai 1: chỉ đặt bit, không xóa trường cũ
Chỉ dùng phép hoặc
GPIOA->MODER |= (1u << 10); /* muốn đặt chân 5 thành 01, tức đầu ra */
/* Trường của chân 5 là bit [11:10], hai bit.
Phép hoặc chỉ ĐẶT bit 10 lên 1, không đụng tới bit 11.
Nếu bit 11 đang là 0 -> trường thành 01, đúng.
Nếu bit 11 đang là 1 -> trường thành 11, tức chế độ TƯƠNG TỰ.
Sau khi chip vừa reset thì MODER phần lớn bằng 0,
nên dòng này chạy đúng và ai cũng tưởng nó đúng. */Xóa rồi đặt
GPIOA->MODER &= ~(3u << 10); /* xóa CẢ HAI bit của trường */
GPIOA->MODER |= (1u << 10); /* rồi đặt giá trị mới */terminal
# Chân đang ở chế độ tương tự, tức MODER bit [11:10] = 11
> mode-test
truoc : MODER[11:10] = 11 (tuong tu) chi dung |=: MODER[11:10] = 11 (van tuong tu, DEN KHONG SANG) xoa roi dat: MODER[11:10] = 01 (dau ra, dung)
Sai 2: ghi đè cả thanh ghi
Gán đè cả thanh ghi
GPIOA->MODER = (1u << 10); /* dùng dấu bằng thay vì |= */
/* Đặt chân 5 thành đầu ra, và ĐỒNG THỜI đặt mọi chân khác
của cổng A về 00, tức đầu vào.
Nếu PA9 và PA10 đang là chân UART thì UART chết ngay.
Nếu PA13 và PA14 đang là chân gỡ lỗi SWD thì bạn MẤT
luôn khả năng nạp lại chương trình. */Chỉ sửa trường cần sửa
GPIOA->MODER &= ~(3u << 10);
GPIOA->MODER |= (1u << 10); /* chỉ đụng vào trường của chân 5 */Sai 3: đọc sửa ghi trên thanh ghi có tác dụng phụ
Đọc sửa ghi trên thanh ghi trạng thái
USART1->SR &= ~(1u << 3); /* muốn xóa cờ ORE */
/* Hai vấn đề:
1. Thanh ghi SR không xóa cờ bằng cách ghi 0 vào nó.
Với ORE thì phải đọc SR rồi đọc DR.
2. Phép đọc SR ở vế phải có thể tự nó xóa vài cờ khác. */Tra cách xóa của từng thanh ghi
/* Mỗi thanh ghi trạng thái có cách xóa riêng, tra datasheet:
Xóa bằng đọc SR rồi đọc DR -> USART SR
Xóa bằng ghi 1 vào đúng bit -> EXTI PR, DMA LIFCR
Xóa bằng ghi 0 vào đúng bit -> TIM SR
Tự xóa khi đọc -> nhiều thanh ghi ADC */
EXTI->PR = (1u << 13); /* ghi 1 để xóa, KHÔNG dùng |= */
TIM2->SR &= ~(1u << 0); /* ghi 0 để xóa, đúng cách với TIM */#Mẫu ba bước
Đọc giá trị hiện tại vào biến cục bộ
Một lần đọc duy nhất. Không đọc lại ở các bước sau, vì thanh ghi có thể đổi giữa chừng.
Xóa trường cũ rồi đặt giá trị mới, trên biến cục bộ
Mọi phép tính bit diễn ra trên biến thường, tức nhanh và không sinh ra truy cập bus nào.
Ghi biến cục bộ trở lại thanh ghi, một lần
Một lệnh ghi duy nhất, nên thanh ghi không bao giờ ở trạng thái trung gian mà phần cứng có thể nhìn thấy.
mau-chuan.c
#include <stdint.h>
#define GPIO_MODE_MASK(chan) (3u << ((chan) * 2u))
#define GPIO_MODE_VAL(chan, m) (((uint32_t)(m)) << ((chan) * 2u))
static inline void gpio_dat_che_do(GPIO_TypeDef *cong, uint8_t chan,
uint8_t che_do)
{
uint32_t r = cong->MODER; /* 1. đọc một lần */
r &= ~GPIO_MODE_MASK(chan); /* 2a. xóa trường cũ */
r |= GPIO_MODE_VAL(chan, che_do); /* 2b. đặt giá trị mới */
cong->MODER = r; /* 3. ghi một lần */
}Mẫu tổng quát cho trường bất kỳ
truong.h
/* Đặt một trường rộng bat_dau tới bat_dau + so_bit - 1 */
static inline void dat_truong(volatile uint32_t *tg,
uint8_t bat_dau, uint8_t so_bit,
uint32_t gia_tri)
{
uint32_t mask = ((1u << so_bit) - 1u) << bat_dau;
uint32_t r = *tg;
r &= ~mask;
r |= (gia_tri << bat_dau) & mask; /* & mask chặn giá trị quá lớn */
*tg = r;
}
static inline uint32_t doc_truong(const volatile uint32_t *tg,
uint8_t bat_dau, uint8_t so_bit)
{
return (*tg >> bat_dau) & ((1u << so_bit) - 1u);
}
/* Dùng: */
dat_truong(&GPIOA->MODER, 10u, 2u, 1u); /* chân 5 thành đầu ra */
dat_truong(&TIM3->CCMR1, 4u, 3u, 6u); /* OC1M thành chế độ PWM 1 */#Khi nào ghi thẳng là đúng
| Tình huống | Cách viết | Vì sao |
|---|---|---|
| Thanh ghi chỉ ghi, ví dụ BSRR | Ghi thẳng | Không có gì để đọc, và bit 0 không có tác dụng |
| Thanh ghi ghi 1 để xóa cờ, ví dụ EXTI PR | Ghi thẳng đúng bit cần xóa | Dùng |= sẽ xóa nhầm mọi cờ đang bật |
| Cấu hình lần đầu và biết mọi bit | Ghi thẳng cả thanh ghi | Rõ ràng hơn, và tránh phụ thuộc giá trị cũ |
| Thanh ghi dữ liệu, ví dụ USART DR | Ghi thẳng | Đọc sửa ghi ở đây vô nghĩa |
| Sửa một trường trong thanh ghi đang dùng | Mẫu ba bước | Phải giữ nguyên các trường khác |
ghi-thang.c
/* ĐÚNG: cấu hình lần đầu, biết chính xác mọi bit muốn đặt */
USART1->CR1 = (1u << 3) /* TE */
| (1u << 2) /* RE */
| (1u << 13); /* UE */
/* Cách này RÕ RÀNG hơn ba dòng |= riêng lẻ, vì nó nói:
"thanh ghi này có đúng ba bit được bật, còn lại là 0".
Nếu dùng |= thì người đọc phải tự hỏi các bit khác đang là gì. */
/* SAI: sửa một bit khi khối đang chạy */
USART1->CR1 = (1u << 5); /* muốn bật ngắt RXNEIE */
/* Dòng này TẮT luôn TE, RE và UE. UART chết. */
/* ĐÚNG: */
USART1->CR1 |= (1u << 5); /* chỉ bật thêm một bit */#Một lệnh ghi duy nhất
Nguyên tử với ngắt
Một thao tác không thể bị ngắt chen vào giữa. Trên Cortex-M, một lệnh máy đơn lẻ luôn nguyên tử theo nghĩa này, vì ngắt chỉ được nhận giữa hai lệnh.
| Cách viết | Số lệnh máy | Ngắt chen vào được không |
|---|---|---|
| GPIOA->BSRR = (1u << 5); | 2, gồm nạp hằng số | Được, nhưng vô hại |
| GPIOA->ODR |= (1u << 5); | 3 | Được, và mất dữ liệu |
| EXTI->PR = (1u << 13); | 2 | Được, nhưng vô hại |
| EXTI->PR |= (1u << 13); | 3 | Được, và xóa nhầm cờ |
mat-du-lieu.c
/* Vòng lặp chính bật đèn ở chân 5 */
void main_bat_den(void)
{
GPIOA->ODR |= (1u << 5);
/* ldr r3, [r0, #20] <-- đọc, giả sử ODR = 0x0000
--- NGẮT CHEN VÀO ĐÂY ---
orr r3, r3, #32 <-- sửa, r3 = 0x0020
str r3, [r0, #20] <-- ghi 0x0020, XÓA MẤT chân 7 */
}
/* ISR bật đèn báo lỗi ở chân 7 */
void TIM2_IRQHandler(void)
{
GPIOA->ODR |= (1u << 7); /* ghi 0x0080 */
}
/* Kết quả: ODR = 0x0020. Đèn báo lỗi vừa bật đã tắt,
và không có dấu vết gì để lần ra. */terminal
# Chạy một triệu vòng, ISR bật chân 7 mỗi mili giây
./thu-mat-du-lieu --dung-odr
so lan ISR bat chan 7 : 10000 so lan quan sat duoc : 9847 mat: 153 lan
./thu-mat-du-lieu --dung-bsrr
so lan ISR bat chan 7 : 10000 so lan quan sat duoc : 10000 mat: 0 lan
Khi không có thanh ghi kiểu BSRR
bao-ve.c
/* Nhiều ngoại vi không có thanh ghi đặt và xóa riêng.
Lúc đó phải bảo vệ bằng cách tắt ngắt, xem Bài 49.4. */
static inline void sua_an_toan(volatile uint32_t *tg,
uint32_t xoa, uint32_t dat)
{
uint32_t primask = __get_PRIMASK();
__disable_irq();
uint32_t r = *tg;
r &= ~xoa;
r |= dat;
*tg = r;
__set_PRIMASK(primask); /* khôi phục, không bật nếu vốn đang tắt */
}
/* Hoặc dùng lệnh độc quyền của lõi, không cần tắt ngắt: */
static inline void sua_doc_quyen(volatile uint32_t *tg,
uint32_t xoa, uint32_t dat)
{
uint32_t r;
do {
r = __LDREXW(tg); /* đọc độc quyền */
r &= ~xoa;
r |= dat;
} while (__STREXW(r, tg) != 0u); /* ghi, thất bại thì thử lại */
}#Thứ tự đọc và ghi
Đọc lại sau khi ghi để chờ phần cứng
cho-phan-cung.c
/* Vài thao tác cần vài chu kỳ để phần cứng thực hiện xong. */
/* 1. Bật clock ngoại vi rồi đọc lại, xem Bài 47.4 */
RCC->AHB1ENR |= (1u << 0);
volatile uint32_t tam = RCC->AHB1ENR;
(void)tam;
/* 2. Tắt một luồng DMA rồi chờ bit EN thật sự về 0 */
DMA2_Stream0->CR &= ~1u;
while (DMA2_Stream0->CR & 1u) { } /* phan cung can vai chu ky */
/* 3. Đổi cấu hình USART rồi chờ khối rảnh */
while (!(USART1->SR & (1u << 6))) { } /* cho TC truoc khi doi baud */
USART1->BRR = brr_moi;Tự làm thử
- Đặt một chân sang chế độ tương tự rồi dùng
|=để chuyển nó sang đầu ra, xác nhận nó không hoạt động. - Viết hàm
dat_truongtổng quát và dùng nó cấu hình cảMODERlẫnCCMR1của timer. - Viết chương trình có ISR bật một chân và vòng lặp chính bật chân khác bằng
ODR |=, rồi đếm số lần mất. - Dùng
EXTI->PR |=thay vì ghi thẳng, gây hai ngắt gần như cùng lúc và xác nhận mất một sự kiện. - Đo thời gian một hàm bằng cách bật tắt chân GPIO, thử có và không có rào cản trình biên dịch, so 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
- Với trường nhiều bit, chỉ dùng
|=là sai vì nó không xóa được bit đang bật. - Gán đè cả thanh ghi
MODERcó thể tắt luôn chân gỡ lỗi và làm chip không nạp lại được bằng cách thường. - Mẫu ba bước đọc một lần, sửa trên biến cục bộ, ghi một lần, tránh được cả trạng thái trung gian lẫn truy cập bus thừa.
- Thanh ghi ghi 1 để xóa cờ phải ghi thẳng. Dùng
|=sẽ xóa nhầm mọi cờ đang bật. - Dùng
BSRRluôn thay choODR |=, vì cửa sổ nguy hiểm của phép đọc sửa ghi rất hiếm nên rất khó tìm.