Bỏ qua điều hướng, tới nội dung chính
Học C
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

  1. Đọ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.

  2. 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.

  3. 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ốngCách viếtVì sao
Thanh ghi chỉ ghi, ví dụ BSRRGhi thẳngKhông có gì để đọc, và bit 0 không có tác dụng
Thanh ghi ghi 1 để xóa cờ, ví dụ EXTI PRGhi thẳng đúng bit cần xóaDùng |= sẽ xóa nhầm mọi cờ đang bật
Cấu hình lần đầu và biết mọi bitGhi thẳng cả thanh ghiRõ ràng hơn, và tránh phụ thuộc giá trị cũ
Thanh ghi dữ liệu, ví dụ USART DRGhi thẳngĐọc sửa ghi ở đây vô nghĩa
Sửa một trường trong thanh ghi đang dùngMẫu ba bướcPhả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ếtSố lệnh máyNgắ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ử

  1. Đặ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.
  2. Viết hàm dat_truong tổng quát và dùng nó cấu hình cả MODER lẫn CCMR1 của timer.
  3. 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.
  4. 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.
  5. Đ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 MODER có 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 BSRR luôn thay cho ODR |=, 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.