Skip to content

8 Bit-band Operation

## **Giới thiệu về thao tác bit-band**

Để giảm số lần thực hiện thao tác “đọc – sửa – ghi” (read-modify-write), bộ xử lý Cortex-M4 cung cấp chức năng thao tác bit-band, cho phép thao tác trên từng bit đơn lẻ theo cách nguyên tử (atomic).

Bản đồ bộ nhớ (memory map) của vi điều khiển bao gồm hai vùng hỗ trợ thao tác bit-band:

  1. Vùng thấp nhất 1MB của SRAM,
  2. Vùng thấp nhất 1MB của các ngoại vi bên trong chip (Peripheral region).

Ngoài chức năng lưu trữ thông thường, hai vùng này còn có một “vùng ánh xạ bit-band alias” riêng biệt, dùng để ánh xạ từng bit thành một ô nhớ 32-bit.

Giải thích dễ hiểu: Vùng alias là vùng bộ nhớ đặc biệt, trong đó mỗi bit ở vùng gốc (SRAM hoặc peripheral) được ánh xạ thành một ô nhớ 32-bit. Khi bạn ghi giá trị 0 hoặc 1 vào ô đó, nó sẽ tự động tác động đến bit tương ứng trong vùng gốc.

Nói đơn giản, CPU không thể trực tiếp truy cập một bit cụ thể trong vùng bộ nhớ gốc. Thay vào đó, nó cần truy cập thông qua vùng alias (vùng ánh xạ) để thực hiện việc đọc/ghi từng bit. Đây chính là thao tác bit-band.

Mục đích của thao tác bit-band là giúp chúng ta có thể điều khiển các chân IO một cách trực tiếp và dễ dàng, giống như vi điều khiển 8051. Ví dụ:

c
PBOUT(2) = 1;

Tức là gán mức logic cao cho chân PB2 một cách trực tiếp bằng thao tác ghi 1 bit.

Địa chỉ bộ nhớ cho thao tác bit-band

Ở phần trước, chúng ta đã nói về việc bản đồ bộ nhớ (memory map) hỗ trợ hai vùng có thể thực hiện thao tác bit-band.

Hai vùng bộ nhớ hỗ trợ bit-band đó có phạm vi cụ thể như sau:

  • 0x2000_00000x200F_FFFF: Vùng 1MB đầu tiên của SRAM
  • 0x4000_00000x400F_FFFF: Vùng 1MB đầu tiên của các ngoại vi trên chip (Peripheral)

Tương ứng với hai vùng gốc ở trên, sẽ có hai vùng ánh xạ alias dùng cho thao tác bit-band:

  • Vùng ánh xạ alias của SRAM bắt đầu tại địa chỉ: 0x2200_0000
  • Vùng ánh xạ alias của ngoại vi bắt đầu tại địa chỉ: 0x4200_0000

💡 Giải thích: Khi bạn muốn điều khiển từng bit trong vùng SRAM hoặc thanh ghi ngoại vi bằng thao tác bit-band, bạn không truy cập trực tiếp vùng gốc, mà sẽ sử dụng các địa chỉ ánh xạ nằm trong vùng alias này. Bộ xử lý sẽ tự động chuyển thao tác ghi/đọc 32-bit ở vùng alias thành thao tác lên bit tương ứng ở vùng gốc.

Khi lập trình, bạn cần sử dụng chính xác các địa chỉ vùng alias này, vì việc tính toán địa chỉ alias sẽ dựa trên công thức liên quan đến chúng.

Ưu điểm của thao tác bit-band

  • Hiệu quả cao hơn → So với thao tác truyền thống (đọc – sửa – ghi), bit-band giúp thao tác trực tiếp trên từng bit mà không cần xử lý nhiều bước trung gian.
  • Dễ đọc hơn → Truy cập bit đơn giản và rõ ràng hơn trong code, chẳng hạn như thao tác với chân IO như PBOUT(2) = 1; giúp dễ hiểu và dễ bảo trì.
  • Tốc độ truy cập nhanh → Do thao tác được thực hiện trực tiếp bằng phần cứng, không cần tốn thời gian đọc và ghi toàn bộ thanh ghi.
  • Tương đối an toàn → Trong các hệ thống có hệ điều hành, khi đa nhiệm chạy song song, rất dễ xảy ra lỗi khi một task đang chỉnh sửa giá trị thì bị gián đoạn. Tuy nhiên, bit-band là thao tác nguyên tử (atomic) – phần cứng thực hiện toàn bộ việc ghi/đọc 1 bit một cách liền mạch, không bị gián đoạn bởi ngắt hay chuyển task, do đó an toàn hơn nhiều so với thao tác truyền thống kiểu đọc – sửa – ghi.
  • Tăng hiệu suất thực thi và tiết kiệm không gian bộ nhớ chương trình → Với các chương trình đơn giản, bạn có thể dùng thư viện hoặc truy cập thanh ghi trực tiếp. Nhưng trong các chương trình phức tạp, nên dùng thao tác bit-band để tối ưu hiệu suất và tiết kiệm dung lượng mã nguồn.

Cách cấu hình thao tác bit-band

Ở trên, chúng ta đã tìm hiểu về địa chỉ và ưu điểm của thao tác bit-band. Vậy làm thế nào để xác định địa chỉ trong vùng alias (vùng ánh xạ) tương ứng với một bit cụ thể trong bộ nhớ gốc?

Việc tính toán địa chỉ bit-band alias sẽ giúp bạn có thể truy cập trực tiếp vào một bit thông qua vùng ánh xạ 32-bit.

Thông tin chi tiết về thao tác bit-band được trình bày trong Trang 41 của User Manual, ví dụ minh họa được thể hiện trong hình dưới đây (trong tài liệu gốc).

img

img

Từ hình ảnh (trong tài liệu) có thể thấy, ta có thể tính toán địa chỉ ánh xạ bit-band alias tương ứng của một bit cụ thể bằng công thức sau:

ini


CopyEdit
bit_word_addr = bit_band_base + (byte_offset × 32) + (bit_number × 4)

Trong đó:

  • bit_band_base: Là địa chỉ bắt đầu vùng ánh xạ alias. → Với các thao tác bit-band trên ngoại vi, địa chỉ này là 0x42000000.
  • byte_offset: Là độ lệch byte giữa địa chỉ thanh ghi mục tiêu và địa chỉ gốc của vùng ngoại vi. → Tức là: byte_offset = địa chỉ thanh ghi - 0x40000000.
  • bit_number: Là chỉ số bit trong byte đó mà bạn muốn thao tác (bắt đầu từ 0).

Cách sử dụng công thức – Ví dụ với chân PB2:

Giả sử bạn muốn điều khiển chân PB2 (GPIOB, chân số 2).

Trường hợp điều khiển đầu ra (Output):

  • Thanh ghi tương ứng là GPIOx_OCTL (thanh ghi điều khiển đầu ra của cổng GPIO).

  • Địa chỉ offset của GPIOx_OCTL0x14.

  • Địa chỉ cơ bản của GPIOBGPIOB_BASE = 0x40020400.

  • Như vậy, địa chỉ thanh ghi bạn cần truy cập là:

    c
    0x40020400 + 0x14 = 0x40020414
  • Tính byte_offset:

    c
    byte_offset = 0x40020414 - 0x40000000 = 0x20414
  • Bit bạn cần thao tác là bit số 2.

  • Áp dụng công thức:

c
bit_word_addr = 0x42000000 + (0x20414 × 32) + (2 × 4)

Trường hợp đọc trạng thái đầu vào (Input):

  • Thanh ghi tương ứng là GPIOx_ISTAT (thanh ghi trạng thái đầu vào).

  • Offset của GPIOx_ISTAT0x10.

  • Địa chỉ thanh ghi: 0x40020400 + 0x10 = 0x40020410.

  • Tính byte_offset:

    0x40020410 - 0x40000000 = 0x20410
  • Bit cần đọc vẫn là bit số 2.

c
// Định nghĩa macro thao tác bit-band, sử dụng trực tiếp macro BIT_ADDR do bạn cung cấp
#define BIT_ADDR(byte_offset, bitnum)  \
    (volatile unsigned long *)(0x42000000 + (byte_offset * 32) + (bitnum * 4))

// Tính toán offset của thanh ghi GPIOB dùng để ánh xạ vào vùng bit-band alias
#define GPIOB_OCTL_OFFSET  ((GPIOB_BASE + 0x14) - 0x40000000)  // Offset của thanh ghi điều khiển đầu ra (OCTL)
#define GPIOB_ISTAT_OFFSET ((GPIOB_BASE + 0x10) - 0x40000000)  // Offset của thanh ghi trạng thái đầu vào (ISTAT)

// Định nghĩa macro PBout(n): ghi giá trị cho chân PBn
// Định nghĩa macro PBin(n): đọc giá trị của chân PBn
#define PBout(n)    *(BIT_ADDR(GPIOB_OCTL_OFFSET, n))  // Xuất tín hiệu mức cao/thấp ra chân PBn
#define PBin(n)     *(BIT_ADDR(GPIOB_ISTAT_OFFSET, n)) // Đọc tín hiệu từ chân PBn

Giải thích các macro đã định nghĩa

  • BIT_ADDR(byte_offset, bitnum) → Macro này dùng để tính địa chỉ vùng alias của một bit cụ thể theo cơ chế bit-band. Trong đó:

    • 0x42000000 là địa chỉ bắt đầu của vùng bit-band alias dành cho ngoại vi.
    • byte_offsetđộ lệch byte của thanh ghi mục tiêu so với địa chỉ cơ sở của vùng ngoại vi (0x40000000).
    • bitnumsố thứ tự của bit trong thanh ghi (từ 0 đến 31).

    Macro này trả về một con trỏ (pointer) trỏ đến địa chỉ ánh xạ của bit tương ứng.

  • GPIOB_OCTL_OFFSET → Đây là độ lệch (offset) của thanh ghi xuất dữ liệu (ODR) của GPIOB so với 0x40000000.

    • Thanh ghi này có offset là 0x14 trong GPIOB.
    • Dùng để điều khiển mức điện áp đầu ra của từng chân PBx.
  • GPIOB_ISTAT_OFFSET → Đây là độ lệch (offset) của thanh ghi trạng thái đầu vào (IDR) của GPIOB.

    • Offset là 0x10 trong GPIOB.
    • Dùng để đọc trạng thái mức điện áp (cao/thấp) tại từng chân PBx.
  • PBout(n)PBin(n) Hai macro này được dùng để truy cập trực tiếp bit số n trên port GPIOB:

    • PBout(n) dùng để ghi giá trị đầu ra (0 hoặc 1) cho chân PBn.
    • PBin(n) dùng để đọc giá trị đầu vào từ chân PBn. Cả hai macro này đều dựa trên BIT_ADDR(...) để ánh xạ đúng đến địa chỉ bit tương ứng.

Lưu ý khi dùng thao tác bit-band:

Khi sử dụng thao tác bit-band, biến hoặc thanh ghi được truy cập phải được khai báo với từ khóa volatile.

Vì trình biên dịch C không biết rằng cùng một bit có thể được truy cập qua hai địa chỉ khác nhau (gốc và alias). Nếu không dùng volatile, trình biên dịch có thể tối ưu hóa sai, dẫn đến không ghi/đọc giá trị thực sự trong bộ nhớ. Sử dụng volatile giúp đảm bảo rằng mỗi lần truy cập đều thực sự thực hiện thao tác với bộ nhớ, không bị bỏ qua hoặc lưu trong thanh ghi tạm.

Code mẫu thao tác bit-band cho GPIO

Đây là toàn bộ đoạn mã định nghĩa thao tác bit-band để truy cập các chân GPIO thông qua cơ chế ánh xạ bit.

c
/*
 * All hardware and software resources of Lichuang development boards and extension boards are fully open source.
 * Tất cả tài nguyên phần cứng và phần mềm của board phát triển Lichuang đều mã nguồn mở.

 * Official website: www.lckfb.com
 * Trang web chính thức: www.lckfb.com

 * Technical support available on forum, feel free to discuss any technical issues.
 * Hỗ trợ kỹ thuật thường trực trên diễn đàn, hoan nghênh trao đổi học hỏi.

 * Lichuang Forum: club.szlcsc.com
 * Diễn đàn Lichuang: club.szlcsc.com

 * Follow us on Bilibili: 【立创开发板】 for latest updates!
 * Theo dõi tài khoản Bilibili: 【立创开发板】 để cập nhật thông tin mới nhất!

 * Not profit-driven from selling boards, committed to cultivating Chinese engineers.
 * Không kiếm tiền từ bán board, mục tiêu là đào tạo kỹ sư Trung Quốc.

 *
 * Change Logs:
 * Date           Author       Notes
 * 2024-03-07     LCKFB-LP     First version
 *               LCKFB-LP     Phiên bản đầu tiên
 */

#ifndef __SYS_H__
#define __SYS_H__

#include "stm32f4xx.h"

// Macro for bit-band operations using BIT_ADDR
// Macro định nghĩa thao tác bit-band, sử dụng BIT_ADDR

#define BIT_ADDR(byte_offset, bitnum)  (volatile unsigned long*)(0x42000000 + (byte_offset * 32) + (bitnum * 4))

// Calculate bit-band alias address for GPIOx OCTL (output) register
// Tính toán địa chỉ alias cho thanh ghi xuất OCTL của từng GPIO

#define GPIOA_OCTL_OFFSET ((GPIOA_BASE + 0x14) - 0x40000000)
#define GPIOB_OCTL_OFFSET ((GPIOB_BASE + 0x14) - 0x40000000)
#define GPIOC_OCTL_OFFSET ((GPIOC_BASE + 0x14) - 0x40000000)
#define GPIOD_OCTL_OFFSET ((GPIOD_BASE + 0x14) - 0x40000000)
#define GPIOE_OCTL_OFFSET ((GPIOE_BASE + 0x14) - 0x40000000)
#define GPIOF_OCTL_OFFSET ((GPIOF_BASE + 0x14) - 0x40000000)
#define GPIOG_OCTL_OFFSET ((GPIOG_BASE + 0x14) - 0x40000000)

// Calculate bit-band alias address for GPIOx ISTAT (input status) register
// Tính toán địa chỉ alias cho thanh ghi trạng thái đầu vào ISTAT của từng GPIO

#define GPIOA_ISTAT_OFFSET ((GPIOA_BASE + 0x10) - 0x40000000)
#define GPIOB_ISTAT_OFFSET ((GPIOB_BASE + 0x10) - 0x40000000)
#define GPIOC_ISTAT_OFFSET ((GPIOC_BASE + 0x10) - 0x40000000)
#define GPIOD_ISTAT_OFFSET ((GPIOD_BASE + 0x10) - 0x40000000)
#define GPIOE_ISTAT_OFFSET ((GPIOE_BASE + 0x10) - 0x40000000)
#define GPIOF_ISTAT_OFFSET ((GPIOF_BASE + 0x10) - 0x40000000)
#define GPIOG_ISTAT_OFFSET ((GPIOG_BASE + 0x10) - 0x40000000)

// Input macros
// Macro đọc trạng thái chân (input)

#define PAin(n)     *(BIT_ADDR(GPIOA_ISTAT_OFFSET, n))   // PA input (đọc chân PA)
#define PBin(n)     *(BIT_ADDR(GPIOB_ISTAT_OFFSET, n))   // PB input
#define PCin(n)     *(BIT_ADDR(GPIOC_ISTAT_OFFSET, n))   // PC input
#define PDin(n)     *(BIT_ADDR(GPIOD_ISTAT_OFFSET, n))   // PD input
#define PEin(n)     *(BIT_ADDR(GPIOE_ISTAT_OFFSET, n))   // PE input
#define PFin(n)     *(BIT_ADDR(GPIOF_ISTAT_OFFSET, n))   // PF input
#define PGin(n)     *(BIT_ADDR(GPIOG_ISTAT_OFFSET, n))   // PG input

// Output macros
// Macro ghi trạng thái chân (output)

#define PAout(n)    *(BIT_ADDR(GPIOA_OCTL_OFFSET, n))    // PA output (ghi chân PA)
#define PBout(n)    *(BIT_ADDR(GPIOB_OCTL_OFFSET, n))    // PB output
#define PCout(n)    *(BIT_ADDR(GPIOC_OCTL_OFFSET, n))    // PC output
#define PDout(n)    *(BIT_ADDR(GPIOD_OCTL_OFFSET, n))    // PD output
#define PEout(n)    *(BIT_ADDR(GPIOE_OCTL_OFFSET, n))    // PE output
#define PFout(n)    *(BIT_ADDR(GPIOF_OCTL_OFFSET, n))    // PF output
#define PGout(n)    *(BIT_ADDR(GPIOG_OCTL_OFFSET, n))    // PG output

#endif

Thí nghiệm nhấp nháy đèn LED

Ở các chương trước, chúng ta đã học cách sử dụng hàm thư viện và bộ định thời SysTick để tạo hiệu ứng LED nhấp nháy.

Trong chương này, dựa trên nền tảng cũ, ta sẽ thay thế các hàm điều khiển mức điện áp cao/thấp bằng cách sử dụng thao tác bit-band để điều khiển trực tiếp.

Chỉ cần làm các bước sau:

  • Thay dòng:

    c
    GPIO_SetBits(GPIOB, GPIO_Pin_2);

    bằng:

    c
    PBout(2) = 1;
  • Thay dòng:

    c
    GPIO_ResetBits(GPIOB, GPIO_Pin_2);

    bằng:

    c
    PBout(2) = 0;

Sau đó biên dịch và nạp chương trình vào board là xong.


Mã nguồn chương này

Có thể tìm thấy trong thư mục:

https://github.com/leybme/stm32f407_resource/tree/main/C03%20Code/Code%20Sample/004Bit-band%20operation/STM32F407_ProjectTemplate

Kết quả

Sau khi nạp chương trình, bạn sẽ thấy LED2 trên board phát triển nhấp nháy với chu kỳ 1 giây sáng – 1 giây tắt liên tục.

Ngoài lề dành cho người mới

Sự khác nhau giữa Read-Modify-Write và Bit-band

1. Đọc – sửa – ghi (Read-Modify-Write - RMW)

Là cách phổ biến mà vi điều khiển dùng khi muốn thay đổi 1 bit trong một thanh ghi:

  • Đọc toàn bộ 32-bit của thanh ghi.
  • Sửa bit mong muốn (ví dụ: đặt thành 1).
  • Ghi lại toàn bộ 32-bit đó vào thanh ghi.

Vấn đề: Nếu có nhiều thao tác xảy ra cùng lúc (ví dụ trong ngắt hoặc đa luồng), thì có thể gây xung đột dữ liệu.

2. Bit-band (vùng ánh xạ từng bit)

Là một cơ chế đặc biệt trên vi điều khiển Cortex-M3/M4:

  • Mỗi bit trong bộ nhớ (RAM hoặc thanh ghi ngoại vi) được ánh xạ thành một địa chỉ riêng biệt trong một vùng nhớ đặc biệt.
  • Việc ghi hoặc đọc 1 bit = chỉ cần ghi hoặc đọc đúng 1 địa chỉ duy nhất.

Không cần RMW, không bị ảnh hưởng bởi thao tác khác đang xảy ra song song.

Ví dụ:

Giả sử bạn muốn bật đèn LED gắn ở bit 5 của GPIOB_ODR.

Cách 1: Dùng RMW

c
GPIOB->ODR |= (1 << 5); // đọc toàn bộ ODR, set bit 5, ghi lại toàn bộ

Cách 2: Dùng Bit-band

c
PBout(5) = 1; // chỉ tác động đúng bit 5, không ảnh hưởng bit khác

Kết luận:

Tiêu chíĐọc–Sửa–Ghi (RMW)Bit-band
Độ chính xác bitCó thể gây lỗi nếu bị ngắtAn toàn, không bị ảnh hưởng
Tốc độ xử lýChậm hơn một chútNhanh hơn
Cú phápPhức tạp hơnRõ ràng, đơn giản hơn