📘 BÀI 16 – BIT, BYTE, MASK, &, |, ^, ~, <<, >>
CÁCH STM32 THỰC SỰ ĐIỀU KHIỂN THANH GHI
Đây là một trong những bài quan trọng nhất trong quá trình học C để lập trình STM32.
Từ bài này, bạn bắt đầu chuyển từ:
"Học cú pháp C"
sang:
"Điều khiển phần cứng bằng C"
Đặc biệt với STM32F103C8T6 Blue Pill của bạn, kiến thức này sẽ giúp bạn hiểu sâu hơn cách GPIO, Timer, UART, ADC… thực sự hoạt động.
1. Trước tiên: BIT là gì?
Một máy tính chỉ làm việc với:
0
1
Một bit chỉ có thể có hai trạng thái:
0 hoặc 1
Ví dụ một byte 8 bit:
Bit: 7 6 5 4 3 2 1 0
─────────────────
1 0 1 1 0 0 1 0
Giá trị:
10110010
Trong đó:
bit 7 = 1
bit 6 = 0
bit 5 = 1
bit 4 = 1
bit 3 = 0
bit 2 = 0
bit 1 = 1
bit 0 = 0
2. BYTE là gì?
Thông thường:
1 byte = 8 bit
Ví dụ:
00000000
là 0.
00000001
là 1.
00000010
là 2.
00000100
là 4.
00001000
là 8.
Bạn hãy chú ý quy luật:
Bit: 7 6 5 4 3 2 1 0
│ │ │ │ │ │ │ │
Giá trị:128 64 32 16 8 4 2 1
Vì vậy:
00001010
=
8 + 2 = 10
3. Tại sao STM32 cực kỳ quan tâm đến BIT?
Bởi vì các peripheral của STM32 được điều khiển bằng register – thanh ghi.
Một thanh ghi có thể có 32 bit:
31 0
┌────────────────────────────────┐
│00000000000101010000100000000101│
└────────────────────────────────┘
Mỗi bit hoặc nhóm bit có thể mang một ý nghĩa khác nhau.
Ví dụ giả tưởng:
Register CONTROL
Bit 0 → Enable
Bit 1 → Interrupt
Bit 2 → Reset
Bit 3 → Direction
Bit 4 → Mode
...
Muốn bật một chức năng, bạn phải thay đổi đúng bit.
4. Toán tử & – AND
Ví dụ:
A = 1010
B = 1100
AND:
1010
1100
----
1000
Quy tắc:
0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1
Chỉ khi cả hai đều bằng 1 thì kết quả mới là 1.
5. & dùng để kiểm tra BIT
Đây là một ứng dụng cực kỳ quan trọng.
Giả sử:
uint8_t value = 0b00001010;
Ta muốn kiểm tra bit 3.
Tạo mask:
uint8_t mask = 0b00001000;
Sau đó:
if (value & mask)
{
// bit 3 đang bằng 1
}
Tại sao?
value = 00001010
mask = 00001000
--------
00001000
Kết quả khác 0 → bit 3 đang bằng 1.
6. | – OR
Quy tắc:
0 | 0 = 0
0 | 1 = 1
1 | 0 = 1
1 | 1 = 1
Ví dụ:
1010
1100
----
1110
OR rất hữu ích để SET bit = đưa bit lên 1.
7. Ví dụ SET một bit
Có:
uint8_t REG = 0;
Muốn bật bit 3:
REG = REG | (1 << 3);
hoặc viết ngắn:
REG |= (1 << 3);
Ban đầu:
REG
00000000
Tính:
1 << 3
được:
00001000
Sau:
00000000
00001000
--------
00001000
Bit 3 được bật.
8. << – dịch trái
Đây là toán tử bạn phải rất quen.
1 << 0
=
00000001
1 << 1
=
00000010
1 << 2
=
00000100
1 << 3
=
00001000
1 << 4
=
00010000
Bảng:
| Biểu thức | Nhị phân | Giá trị |
|---|---|---|
1 << 0 |
00000001 |
1 |
1 << 1 |
00000010 |
2 |
1 << 2 |
00000100 |
4 |
1 << 3 |
00001000 |
8 |
1 << 4 |
00010000 |
16 |
1 << 5 |
00100000 |
32 |
1 << 6 |
01000000 |
64 |
1 << 7 |
10000000 |
128 |
Vì vậy:
(1 << n)
là cách cực kỳ phổ biến để tạo mask cho bit n.
9. >> – dịch phải
Ngược lại:
0b10000000 >> 1
→
01000000
Tiếp:
00100000
Ví dụ:
uint8_t value = 0b00100000;
value >> 5
→
00000001
10. Tại sao >> hữu ích?
Giả sử register chứa:
10110100
Bạn muốn lấy bit 4.
Ta có thể:
uint8_t bit4 = (value >> 4) & 1;
Ví dụ:
value
10110100
dịch phải 4:
00001011
AND với 1:
00001011
00000001
--------
00000001
→ bit 4 = 1.
11. ~ – NOT
~ đảo tất cả bit.
Ví dụ:
value = 10110010
thì:
~value = 01001101
Quy tắc:
0 → 1
1 → 0
12. ^ – XOR
XOR có quy tắc:
0 ^ 0 = 0
0 ^ 1 = 1
1 ^ 0 = 1
1 ^ 1 = 0
Điểm đặc biệt:
Hai bit giống nhau → 0; khác nhau → 1.
Ví dụ:
1010
1100
----
0110
13. XOR rất hay dùng để TOGGLE bit
Giả sử:
uint8_t REG = 0;
Muốn đảo bit 3:
REG ^= (1 << 3);
Lần đầu:
00000000
00001000
--------
00001000
Bit 3:
0 → 1
Lần nữa:
00001000
00001000
--------
00000000
Bit 3:
1 → 0
Vậy:
REG ^= (1 << 3);
có nghĩa:
Toggle bit 3.
14. Đây là 4 thao tác bạn phải thuộc
SET bit
REG |= (1 << n);
→ bit n = 1
CLEAR bit
REG &= ~(1 << n);
→ bit n = 0
TOGGLE bit
REG ^= (1 << n);
→ đảo bit n
READ bit
(REG >> n) & 1
→ lấy giá trị bit n
15. CLEAR bit – cực kỳ quan trọng
Giả sử:
REG = 10110111
Muốn xóa bit 4.
Tạo:
(1 << 4)
=
00010000
Đảo:
~(1 << 4)
=
11101111
AND:
10110111
11101111
--------
10100111
Bit 4:
1 → 0
Vì vậy:
REG &= ~(1 << 4);
16. MASK là gì?
Mask là một giá trị nhị phân được tạo ra để thao tác với một hoặc một nhóm bit.
Ví dụ:
#define BIT0_MASK (1 << 0)
#define BIT1_MASK (1 << 1)
#define BIT2_MASK (1 << 2)
#define BIT3_MASK (1 << 3)
hoặc:
#define GPIO_ENABLE_MASK (1 << 0)
#define GPIO_ERROR_MASK (1 << 1)
#define GPIO_MODE_MASK (3 << 2)
Mask là một khái niệm bạn sẽ gặp liên tục khi đọc Reference Manual của STM32.
17. Không chỉ một bit – có thể có một nhóm bit
Ví dụ register:
Bit 7 6 5 4 3 2 1 0
─────── ───────
MODE CONTROL
Giả sử:
Bit 2-3 = MODE
Có 2 bit:
00
01
10
11
→ có 4 trạng thái.
Mask:
#define MODE_MASK (0b11 << 2);
=
00001100
18. Ghi giá trị vào một nhóm bit
Đây là kỹ thuật rất quan trọng.
Giả sử:
uint8_t REG = 0b10110011;
Ta muốn đặt:
bit 2-3 = 01
Đầu tiên xóa hai bit:
REG &= ~(0b11 << 2);
Sau đó đưa giá trị mới vào:
REG |= (0b01 << 2);
Đây chính là cách thao tác với field trong register.
19. Tại sao STM32 cần kỹ thuật này?
Bởi một register có thể chứa rất nhiều chức năng.
Ví dụ giả tưởng:
REGISTER
31 0
┌────┬────┬────┬────┬──────┐
│MODE│IRQ │EN │DIR │... │
└────┴────┴────┴────┴──────┘
Bạn muốn thay đổi MODE.
Bạn không được phép vô tình thay đổi IRQ hoặc EN.
Vì vậy phải:
1. Xóa đúng các bit MODE
2. Ghi giá trị MODE mới
3. Giữ nguyên các bit khác
Đây chính là sức mạnh của:
&
|
~
<<
20. Liên hệ trực tiếp với STM32
Trong STM32F103, các peripheral có rất nhiều thanh ghi.
Ví dụ GPIO có các register như:
CRL
CRH
IDR
ODR
BSRR
BRR
Mỗi register lại có các bit/field riêng.
Ví dụ bạn muốn cấu hình GPIO:
Input
Output
Pull-up
Pull-down
Push-pull
Open-drain
50 MHz
10 MHz
2 MHz
...
Tất cả cuối cùng đều được biểu diễn bằng các bit trong register.
21. Ví dụ với LED PC13
Bạn đang dùng Blue Pill có LED onboard ở:
PC13
Ở mức register, việc điều khiển GPIO cuối cùng vẫn quy về:
đọc/ghi các bit của register
Ví dụ ý tưởng:
GPIOC->ODR |= (1 << 13);
có nghĩa:
GPIOC->ODR
│
└── set bit 13
Tức:
GPIOC->ODR |= (1U << 13);
22. Tắt bit 13
GPIOC->ODR &= ~(1U << 13);
Tư duy:
ODR
↓
xóa bit 13
↓
PC13 output = 0
Tuy nhiên, trên Blue Pill LED onboard thường là active-low, nên:
PC13 = LOW → LED sáng
PC13 = HIGH → LED tắt
Đây là ví dụ rất hay cho thấy:
Logic của phần cứng không nhất thiết giống logic “1 = ON”.
23. Một cách khác: BSRR
STM32F1 còn có register:
GPIOx_BSRR
Register này được thiết kế để set/reset GPIO theo cơ chế phần cứng.
Ví dụ ý tưởng:
GPIOC->BSRR = (1U << 13);
set chân.
Reset có thể dùng vùng bit reset tương ứng của BSRR.
Điểm quan trọng ở đây:
Bạn đang bắt đầu nhìn thấy cách các thư viện HAL/LL cuối cùng cũng phải thao tác với register và bit.
24. HAL không làm mất kiến thức Register
Ví dụ bạn viết:
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
Bạn không trực tiếp viết:
GPIOC->ODR
nhưng bên dưới thư viện vẫn phải cuối cùng điều khiển phần cứng thông qua register.
Tư duy:
HAL
↓
Driver
↓
Register
↓
Bit
↓
Silicon STM32
↓
GPIO PC13
Vì vậy học bitwise operation không hề thừa, ngay cả khi bạn dùng CubeMX + HAL.
25. Bảng “phải thuộc”
| Mục đích | Code |
|---|---|
Set bit n |
`REG |
Clear bit n |
REG &= ~(1U << n); |
Toggle bit n |
REG ^= (1U << n); |
Read bit n |
(REG >> n) & 1U |
Tạo mask bit n |
(1U << n) |
Hãy thuộc 5 dòng này.
26. Một ví dụ tổng hợp
Giả sử:
uint8_t REG = 0;
Bật bit 2
REG |= (1U << 2);
Kết quả:
00000100
Bật tiếp bit 5
REG |= (1U << 5);
Kết quả:
00100100
Kiểm tra bit 5
if (REG & (1U << 5))
{
// bit 5 = 1
}
Xóa bit 2
REG &= ~(1U << 2);
Kết quả:
00100000
Toggle bit 5
REG ^= (1U << 5);
Kết quả:
00000000
27. Một điều bạn cần đặc biệt chú ý
Không nên viết:
REG = (1U << 3);
nếu mục tiêu của bạn chỉ là set bit 3.
Vì câu lệnh này ghi đè toàn bộ register.
Ví dụ:
REG ban đầu:
10110100
Bạn muốn bật bit 3.
Nếu:
REG = (1U << 3);
thì:
00001000
Toàn bộ các bit khác bị mất.
Trong khi:
REG |= (1U << 3);
sẽ giữ nguyên các bit khác:
10110100
00001000
--------
10111100
Đây là điểm cực kỳ quan trọng khi thao tác register.
28. Bài tập Bài 16
🧪 Bài 1 – tính nhị phân
Không dùng máy tính, hãy tính:
1 << 0
1 << 1
1 << 2
1 << 3
1 << 4
1 << 5
🧪 Bài 2 – SET
Cho:
uint8_t reg = 0b00000000;
Hãy set:
bit 1
bit 4
bit 7
Kết quả cuối cùng là bao nhiêu?
🧪 Bài 3 – CLEAR
Cho:
uint8_t reg = 0b11111111;
Xóa:
bit 2
bit 5
Kết quả?
🧪 Bài 4 – TOGGLE
Cho:
uint8_t reg = 0b10101010;
Thực hiện:
reg ^= (1U << 1);
Kết quả?
Sau đó:
reg ^= (1U << 7);
Kết quả?
29. 🧪 Bài 5 – cực kỳ quan trọng
Cho:
uint8_t reg = 0b10110100;
Hãy viết code để:
- Set bit 0
- Clear bit 4
- Toggle bit 6
- Kiểm tra bit 3
- Kiểm tra bit 7
Không được sử dụng if để thực hiện thao tác set/clear/toggle.
30. 🧪 Bài 6 – STM32 Blue Pill
Hãy thử suy nghĩ về câu lệnh:
GPIOC->ODR |= (1U << 13);
Giải thích từng thành phần:
GPIOC
↓
ODR
↓
|
↓
1U
↓
<<
↓
13
Bạn phải hiểu được toàn bộ câu lệnh này, không chỉ học thuộc.
🎯 BÀI 16 CẦN THUỘC LÒNG
Nếu chỉ được phép mang theo 5 công thức vào Bài 17, hãy nhớ:
REG |= (1U << n); // SET
REG &= ~(1U << n); // CLEAR
REG ^= (1U << n); // TOGGLE
(REG >> n) & 1U // READ
(1U << n) // MASK
Và hãy nhớ tư duy:
STM32
│
▼
REGISTER
│
┌───────┼───────┐
▼ ▼ ▼
BIT BIT BIT
│ │ │
└───────┼───────┘
▼
PHẦN CỨNG
Bài 17 chúng ta sẽ nối tất cả những gì đã học từ Bài 1 → 16 vào một chủ đề cực kỳ quan trọng:
🔥 BÀI 17 – MEMORY MAP STM32F103: FLASH, SRAM, PERIPHERAL, REGISTER VÀ ĐỊA CHỈ 0x40000000 THỰC SỰ CÓ Ý NGHĨA GÌ?
Ở bài đó, bạn sẽ bắt đầu hiểu tại sao một câu như:
GPIOC->ODR
lại có thể điều khiển một transistor bên trong con chip STM32.