📚 Điều kiện tiên quyết

Để tiếp thu bài học này tốt nhất, bạn nên ôn lại:

Từ Bài 1 đến Bài 14, mọi mạch trong series đều là mạch cứng: hành vi của nó cố định vĩnh viễn theo cách bạn hàn dây và chọn linh kiện. Muốn đổi hành vi — phải hàn lại mạch. Vi điều khiển (MCU) phá vỡ giới hạn đó: cùng một con chip vật lý, chỉ cần nạp một đoạn mã lệnh khác là toàn bộ hành vi mạch thay đổi hoàn toàn — không cần chạm vào dây dẫn hay linh kiện nào.

Bài học này mở ra nửa sau của series: thay vì thiết kế mạch tương tự (analog) hay số (digital) cố định, bạn sẽ học cách một chương trình phần mềm điều khiển trực tiếp phần cứng thông qua các thanh ghi đặc biệt, và tự tay lập trình một MCU ảo để nhấp nháy đèn LED — cellphone, máy giặt, hay chính bàn phím bạn đang gõ đều chạy trên nguyên lý giống hệt.

1. Kiến trúc tổng quan bên trong vi điều khiển

Một MCU gói gọn cả một "máy tính mini" vào một con chip duy nhất: bộ xử lý trung tâm (CPU) để thực thi lệnh, bộ nhớ Flash để lưu chương trình vĩnh viễn (giữ dữ liệu kể cả khi mất điện), và SRAM để lưu biến tạm thời trong lúc chạy (mất hết khi ngắt điện — bạn đã gặp SRAM 6-transistor ở Bài 13). Ba khối này nói chuyện với nhau qua một bus hệ thống — một "xa lộ" dây dẫn chung mang địa chỉ, dữ liệu và tín hiệu điều khiển qua lại.

Sơ đồ khối kiến trúc bên trong một MCU Vỏ chip MCU CPU (Fetch-Decode-Execute) Flash Chương trình (vĩnh viễn) SRAM Biến tạm (mất khi cúp điện) Thanh ghi I/O DDRx / PORTx / PINx (mục 2–3 bên dưới) bus PB5 PB0

Điểm khác biệt cốt lõi so với "vi xử lý" (như CPU trong máy tính bàn) là MCU tích hợp luôn Flash + SRAM + các khối ngoại vi (I/O, Timer, ADC...) vào một chip duy nhất — không cần RAM rời, ổ cứng rời. Cái giá phải trả là tài nguyên rất hạn chế: một MCU 8-bit phổ biến trong lớp học chỉ có vài KB SRAM (so với hàng GB của máy tính) — đủ cho các tác vụ điều khiển thiết bị nhưng không đủ chạy hệ điều hành đầy đủ.

1.1 Kiến trúc Harvard vs Von Neumann: vì sao Flash và SRAM là hai "thế giới" khác nhau

Sơ đồ khối ở trên vẽ Flash và SRAM là hai ô riêng biệt — đó không chỉ là cách vẽ cho dễ nhìn, mà phản ánh đúng một lựa chọn thiết kế phần cứng thật gọi là kiến trúc Harvard: chương trình (Flash) và dữ liệu (SRAM) nằm trên hai không gian địa chỉ hoàn toàn tách biệt, mỗi loại đi trên một bus riêng. Địa chỉ 0x25 trong Flash (một lệnh máy) và địa chỉ 0x25 trong SRAM (thanh ghi PORTB ở mục 3) là hai ô nhớ vật lý hoàn toàn khác nhau, dù trùng số. Đây là lý do CPU không thể vô tình "thực thi nhầm" dữ liệu thành lệnh — một lớp bảo vệ tự nhiên mà kiến trúc Von Neumann (máy tính bàn, và cả nhiều MCU ARM hiện đại) không có, vì ở đó chương trình và dữ liệu chia sẻ chung một không gian địa chỉ.

Tiêu chí Kiến trúc Harvard Kiến trúc Von Neumann
Không gian địa chỉ Chương trình và dữ liệu tách biệt (2 bus riêng) Chương trình và dữ liệu chung một không gian (1 bus)
Tốc độ Nhanh hơn — CPU có thể đọc lệnh tiếp theo và đọc/ghi dữ liệu cùng lúc Chậm hơn — lệnh và dữ liệu tranh nhau cùng một bus (Von Neumann bottleneck)
An toàn CPU không thể thực thi nhầm vùng dữ liệu thành mã lệnh Về lý thuyết có thể (nền tảng của một số lỗ hổng bảo mật thực thi mã trên máy tính bàn)
Ví dụ Hầu hết MCU 8-bit: AVR (ATmega328P), PIC CPU máy tính bàn (x86); nhiều MCU ARM Cortex-M dùng biến thể "Harvard cải tiến" cho vùng cache

1.2 So sánh vài họ MCU phổ biến trong thực tế

"MCU" không phải một sản phẩm duy nhất — đây là cả một phổ rộng từ chip 8-bit giá vài nghìn đồng đến chip 32-bit đa lõi mạnh ngang điện thoại đời đầu. Độ rộng thanh ghi ($8$-bit, $16$-bit hay $32$-bit) quyết định trực tiếp độ rộng của các thanh ghi DDRx/PORTx/PINx mà bạn sẽ thao tác ở mục 3 — MCU 8-bit thì mỗi thanh ghi GPIO rộng đúng 8 bit (khớp 8 chân một cổng) như trong toàn bộ bài học này; MCU 32-bit thường gộp nhiều chân hơn vào một thanh ghi rộng hơn.

Họ MCU Kiến trúc CPU Xung nhịp điển hình SRAM điển hình Ứng dụng tiêu biểu
ATmega328P (AVR) 8-bit, Harvard $16\text{ MHz}$ $2\text{ KB}$ Arduino Uno — học tập, dự án nhỏ
PIC16F877A 8-bit, Harvard $20\text{ MHz}$ $368\text{ B}$ Công nghiệp, thiết bị nhúng đơn giản
STM32F103 (ARM Cortex-M3) 32-bit $72\text{ MHz}$ $20\text{ KB}$ IoT, thiết bị đo lường, robot
ESP32 (Xtensa, 2 lõi) 32-bit, Harvard $240\text{ MHz}$ $520\text{ KB}$ IoT có WiFi/Bluetooth tích hợp

Series này chọn mô hình 8-bit kiểu AVR (giống Arduino Uno) làm chuẩn xuyên suốt Bài 15 & 16 vì tập lệnh đơn giản, thanh ghi 8-bit trực quan để vẽ tay từng bit — nhưng mọi nguyên lý (memory-mapped I/O, DDR/PORT/PIN, đọc-sửa-ghi bitwise) áp dụng y hệt trên MCU 32-bit, chỉ khác độ rộng thanh ghi và tên gọi thanh ghi cụ thể.

💡 Xung nhịp (clock) quyết định tốc độ thực thi
CPU không "chạy liên tục" mà tiến từng bước theo nhịp của một bộ dao động thạch anh (giống bộ dao động LC bạn đã học ở Bài 11, chỉ khác tần số). Mỗi lệnh máy tốn một số chu kỳ máy (machine cycle) cố định để hoàn tất chu trình Fetch–Decode–Execute. Với xung nhịp $16\text{ MHz}$, CPU thực hiện khoảng 16 triệu chu kỳ mỗi giây — đủ nhanh để một chương trình chớp tắt LED "nhìn như" phản hồi tức thời, dù thực chất vẫn đang xử lý tuần tự từng lệnh một. Bạn sẽ dùng chính bộ đếm chu kỳ này để tạo Timer chính xác ở Bài 16, thay vì đếm bằng vòng lặp delay() tốn CPU như demo ở mục 5.

2. Memory-Mapped I/O — địa chỉ hoá phần cứng

Đây là ý tưởng quan trọng nhất của bài học: các thanh ghi điều khiển chân vật lý được đặt trong CÙNG một không gian địa chỉ với SRAM. Với CPU, ghi một byte vào ô nhớ 0x25 và ghi một byte vào "thanh ghi điều khiển chân GPIO" là cùng một thao tác — chỉ khác địa chỉ đích. Cách tiếp cận này gọi là Memory-Mapped I/O.

Nhắc lại Series C — con trỏ: một con trỏ chỉ là một địa chỉ bộ nhớ. Khi thư viện MCU viết PORTB = 0x20;, trình biên dịch thực chất dịch nó thành:

register-macro.c — PORTB thực chất là gì phía sau hậu trường
// "PORTB" chỉ là một macro cho con trỏ trỏ tới địa chỉ cố định 0x25
#define PORTB (*(volatile uint8_t *)0x25)

// Viết PORTB = 0x20 thực chất là:
*(volatile uint8_t *)0x25 = 0x20;
// Ghi thẳng byte 0x20 vào ô nhớ 0x25 — CPU không hề biết (và không cần biết)
// ô nhớ đó là RAM hay một thanh ghi điều khiển chân vật lý.
🔬 Vì sao từ khoá volatile bắt buộc phải có
Trình biên dịch C thường "thông minh hoá" code: nếu thấy một biến được đọc nhiều lần mà không có dòng lệnh nào (mà nó biết) thay đổi giá trị, nó sẽ tối ưu bằng cách chỉ đọc một lần rồi dùng lại kết quả cũ trong thanh ghi CPU (cache). Nhưng giá trị của PINB có thể đổi bất cứ lúc nào do tín hiệu điện bên ngoài — một sự kiện trình biên dịch không thấy được. Từ khoá volatile ra lệnh cho trình biên dịch: "luôn đọc/ghi thật từ địa chỉ này, không được tối ưu, không được dùng giá trị cũ trong cache". Thiếu volatile, code đọc chân nút nhấn có thể bị "đóng băng" ở giá trị đọc lần đầu tiên mãi mãi.

3. Bộ ba thanh ghi GPIO: DDRx, PORTx, PINx

Mỗi cổng GPIO 8-chân (ví dụ cổng B) được điều khiển bởi đúng 3 thanh ghi 8-bit, mỗi bit ứng với một chân vật lý:

Thanh ghi Vai trò Bit = 1 nghĩa là gì Bit = 0 nghĩa là gì
DDRx (Data Direction Register) Chọn chiều của chân: input hay output Chân là OUTPUT Chân là INPUT
PORTx Nếu chân là output: điều khiển mức áp ra chân Chân xuất ra mức HIGH (thường ~5V/3.3V) Chân xuất ra mức LOW (0V)
PORTx (khi chân là input) Bật/tắt điện trở kéo lên nội bộ (internal pull-up) Bật pull-up (chân "nghỉ" ở mức HIGH khi không nối gì) Thả nổi hoàn toàn (floating — dễ nhiễu, xem cạm bẫy bên dưới)
PINx Chỉ để đọc — phản ánh mức điện áp thật đang có ở chân Chân đang đo được mức HIGH Chân đang đo được mức LOW

Nói cách khác: DDRx quyết định "vai trò" của chân (nói hay nghe), còn PORTx và PINx là hai chiều dữ liệu khác nhau của cùng một chân — PORTx là "miệng" (ghi/xuất), PINx là "tai" (đọc/nhập). Chúng không thể dùng thay nhau.

3.1 Bảng địa chỉ thật: memory-mapped I/O không phải lý thuyết suông

Để thấy rõ "3 thanh ghi mỗi cổng" không phải là ẩn dụ mà là các ô nhớ thật với địa chỉ cụ thể, đây là trích đoạn bản đồ bộ nhớ dữ liệu (data memory map) thật của chip ATmega328P (chip trên Arduino Uno) — địa chỉ lấy từ chính datasheet của Microchip:

Địa chỉ (hex) Tên thanh ghi Cổng
0x23 PINB Cổng B (PB0–PB7, gồm cả PB5 dùng trong bài này)
0x24 DDRB
0x25 PORTB
0x26 PINC Cổng C (PC0–PC6)
0x27 DDRC
0x28 PORTC
0x29 PIND Cổng D (PD0–PD7)
0x2A DDRD
0x2B PORTD

Quan sát một quy luật rất đẹp: cứ mỗi cổng chiếm đúng 3 địa chỉ liên tiếp, luôn theo thứ tự PIN → DDR → PORT, rồi lặp lại cho cổng kế tiếp. Đây chính xác là lý do vì sao macro PORTB ở mục 2 trỏ đúng vào 0x25 — không phải con số ngẫu nhiên, mà nhà sản xuất chip đã "đổ khuôn" các cổng GPIO liên tiếp nhau trong không gian địa chỉ để trình biên dịch dễ tạo mã tối ưu.

read-button.c — Đọc nút nhấn bằng pull-up nội bộ (không cần điện trở ngoài)
#define PB0 0

void setup(void) {
  DDRB  &= ~(1 << PB0);  // PB0 là INPUT (mặc định, nhưng ghi rõ cho dễ đọc)
  PORTB |=  (1 << PB0);  // Chân INPUT: bit PORT này bật điện trở pull-up nội bộ
                         // → chân "nghỉ" ở mức HIGH khi nút nhấn chưa được bấm
}

int docNutNhan(void) {
  // Nút nhấn thường đấu một đầu xuống GND, đầu kia vào chân PB0.
  // Khi CHƯA bấm: pull-up kéo chân lên HIGH → PINB bit 0 = 1.
  // Khi ĐANG bấm: nút nối thẳng chân xuống GND → PINB bit 0 = 0 (logic đảo ngược!).
  return (PINB & (1 << PB0)) == 0;  // trả về 1 (true) khi nút ĐANG được bấm
}

Chú ý logic bị đảo ngược so với trực giác: nút nhấn đang bấm lại đọc được mức LOW, còn chưa bấm đọc được mức HIGH — vì điện trở pull-up mặc định kéo chân lên cao, nút nhấn chỉ có tác dụng "kéo tuột" chân xuống đất khi đóng mạch. Đây là mẫu đọc nút nhấn phổ biến nhất trong thực tế vì không cần thêm điện trở rời bên ngoài.

🔬 Mẹo phần cứng: ghi 1 vào PINx để đảo bit PORTx (toggle atomic)
Trên nhiều dòng MCU hiện đại (như AVR), PINx tuy là thanh ghi "chỉ đọc" nhưng có một hành vi đặc biệt ở phần cứng: ghi giá trị 1 vào một bit của PINx sẽ đảo (toggle) đúng bit đó của PORTx — không đụng tới các bit khác. Đây là cách nhấp nháy LED nhanh hơn và an toàn hơn viết PORTB ^= (1 << PB5); vì nó không cần đọc giá trị cũ trước (tránh race condition khi có ngắt xảy ra giữa chừng — khái niệm bạn sẽ gặp lại ở Bài 16).

4. Kỹ thuật đọc–sửa–ghi bằng phép toán bitwise

Vì một thanh ghi 8-bit điều khiển 8 chân cùng lúc, bạn gần như không bao giờ được phép ghi đè nguyên byte (trừ khi chủ đích muốn đổi cả 8 chân). Thay vào đó, dùng phép toán bitwise để chỉ sửa đúng bit mình cần, giữ nguyên 7 bit còn lại:

bitwise-ops.c — Bốn thao tác bit nền tảng trên thanh ghi GPIO
// Giả sử LED nối chân PB5 (giống hệt LED tích hợp sẵn trên board Arduino Uno)
#define PB5 5

// 1. SET một bit (đưa lên 1, giữ nguyên các bit khác) — dùng OR
PORTB |= (1 << PB5);   // (1 << 5) = 0b00100000 → chỉ bật riêng bit 5

// 2. CLEAR một bit (đưa về 0, giữ nguyên các bit khác) — dùng AND với số đã đảo bit
PORTB &= ~(1 << PB5);  // ~(0b00100000) = 0b11011111 → chỉ tắt riêng bit 5

// 3. TOGGLE một bit (đảo trạng thái) — dùng XOR
PORTB ^= (1 << PB5);   // bit 5: 0→1 hoặc 1→0, các bit khác giữ nguyên

// 4. ĐỌC một bit (kiểm tra chân đang HIGH hay LOW) — dùng AND rồi so 0
if (PINB & (1 << PB0)) {
  // Chân PB0 đang ở mức HIGH (ví dụ: nút nhấn đang được thả ra nhờ pull-up)
}
Ví dụ tính toán: Cấu hình DDRB cho 2 chân output, còn lại input

Đề bài: Board có LED nối chân PB5 và Relay nối chân PB3, các chân PB còn lại (PB0–PB2, PB4, PB6–PB7) dùng làm input đọc cảm biến. Hãy tính giá trị nhị phân và thập lục phân cần ghi vào DDRB để cấu hình đúng một lần duy nhất lúc khởi động.

Lời giải

1. Đánh số bit từ 7 (trái) xuống 0 (phải): DDRB = [b7 b6 b5 b4 b3 b2 b1 b0]. Bit output (=1) là bit 5 (LED) và bit 3 (Relay); tất cả bit còn lại là input (=0).

2. Xếp thành chuỗi nhị phân 8-bit:

$$DDRB = 0b\underbrace{0}_{7}\underbrace{0}_{6}\underbrace{1}_{5}\underbrace{0}_{4}\underbrace{1}_{3}\underbrace{0}_{2}\underbrace{0}_{1}\underbrace{0}_{0} = 0b00101000$$

3. Đổi sang thập lục phân (nhóm 4 bit một): $0010_2 = 2_{16}$, $1000_2 = 8_{16}$

$$DDRB = 0b00101000 = 0x28 = 40_{10}$$

Kết luận: chỉ cần một dòng lệnh duy nhất DDRB = 0x28; lúc khởi động — hoặc rõ ràng hơn về mặt ý nghĩa: DDRB = (1 << PB5) | (1 << PB3);.

🕳️ Cạm bẫy: quên cấu hình DDR trước khi ghi PORT
Đây là lỗi phổ biến nhất của người mới học GPIO: ghi PORTB |= (1 << PB5); để bật LED nhưng LED không sáng — vì chân vẫn đang ở chế độ mặc định (thường là INPUT, DDR bit = 0). Khi DDR bit là 0, giá trị PORT bit không điều khiển điện áp ra chân nữa mà chỉ bật/tắt điện trở pull-up nội bộ — dòng điện qua điện trở pull-up quá yếu để làm sáng LED công suất bình thường. Luôn cấu hình DDR trước, PORT sau, không bao giờ đảo thứ tự.
❌ Sai vs ✅ Đúng — hai lỗi kinh điển khi lập trình GPIO
// ❌ SAI #1: Quên set DDR trước — LED không bao giờ sáng
PORTB |= (1 << PB5);   // Chân PB5 vẫn đang là INPUT (DDR mặc định = 0)!

// ✅ ĐÚNG #1: Luôn cấu hình chiều DDR trước khi điều khiển PORT
DDRB  |= (1 << PB5);   // Bước 1: khai báo PB5 là OUTPUT
PORTB |= (1 << PB5);   // Bước 2: bây giờ mới bật mức HIGH cho LED

// ❌ SAI #2: Gán đè cả byte — xoá mất cấu hình PB3 (Relay) đã set trước đó
DDRB = (1 << PB5);      // Ghi đè toàn bộ 8 bit! PB3 vô tình bị đưa về 0 (input)

// ✅ ĐÚNG #2: Dùng OR để chỉ bật thêm bit mới, giữ nguyên các bit đã cấu hình
DDRB |= (1 << PB5);     // Chỉ bật thêm bit 5, bit 3 (Relay) vẫn nguyên vẹn

5. Ứng dụng thực tế: điều khiển nhiều chân GPIO cùng lúc

Sức mạnh thật sự của thao tác thanh ghi lộ rõ khi điều khiển nhiều chân cùng lúc bằng một lệnh duy nhất — thay vì gọi hàm bật/tắt từng LED riêng lẻ (như các thư viện cấp cao thường làm), bạn có thể ghi thẳng cả một mẫu bit vào PORTx để đổi trạng thái toàn bộ 8 chân trong đúng một chu kỳ máy.

5.1 Bộ đếm nhị phân hiển thị trên 4 LED

Nối 4 LED vào 4 chân thấp nhất của cổng D (PD0–PD3). Đếm nhị phân từ $0$ đến $15$ chỉ đơn giản là gán liên tiếp giá trị đếm vào PORTD:

binary-counter.c — Đếm nhị phân 0–15 hiển thị trên 4 LED (PD0–PD3)
void setup(void) {
  DDRD |= 0b00001111;  // 4 bit thấp (PD0-PD3) là OUTPUT, 4 bit cao giữ nguyên INPUT
}

void demNhiPhan(void) {
  for (uint8_t dem = 0; dem <= 15; dem++) {
    // Chỉ ghi đè đúng 4 bit thấp, GIỮ NGUYÊN 4 bit cao (tránh lặp lại sai lầm ở mục 4!)
    PORTD = (PORTD & 0b11110000) | (dem & 0b00001111);
    delay(300);  // Mỗi bước đếm cách nhau 300ms để mắt kịp nhìn
  }
  // dem = 13 = 0b1101 → LED PD3,PD2 tắt, PD1,PD0 sáng — thử tính nhẩm các giá trị còn lại!
}

Chú ý dòng PORTD = (PORTD & 0b11110000) | (dem & 0b00001111); — đây là kỹ thuật kết hợp AND và OR để chỉ thay 4 bit thấp, khoá cứng 4 bit cao, tổng quát hoá chính xác cạm bẫy "ghi đè cả byte" ở mục 4 cho trường hợp nhiều bit cùng lúc thay vì chỉ 1 bit.

5.2 Hiệu ứng đèn chạy (Knight Rider / Larson Scanner)

Hiệu ứng đèn chạy qua lại kinh điển — một chấm sáng "chạy" từ trái sang phải rồi ngược lại trên dãy LED — chỉ cần đúng một toán tử dịch bit (<</>>, khác với &/|/^ đã học ở mục 4):

knight-rider.c — Hiệu ứng đèn chạy trên 8 LED bằng dịch bit
void hieuUngDenChay(void) {
  uint8_t mau = 0b00000001;  // Bắt đầu từ chân PD0
  int8_t chieu = 1;          // 1 = dịch phải→trái, -1 = dịch trái→phải

  while (1) {
    PORTD = mau;
    delay(100);

    if (chieu == 1) {
      mau <<= 1;               // Dịch chấm sáng sang trái 1 bit
      if (mau == 0b10000000) chieu = -1;  // Chạm biên phải → đổi chiều
    } else {
      mau >>= 1;               // Dịch chấm sáng sang phải 1 bit
      if (mau == 0b00000001) chieu = 1;   // Chạm biên trái → đổi chiều
    }
  }
}

Vòng lặp while (1) chạy vĩnh viễn bằng cách "đợi bận" (busy-wait) qua delay() — trong lúc đó CPU không làm được việc gì khác, kể cả đọc một nút nhấn. Đây chính là giới hạn bạn sẽ vượt qua ở Bài 16: dùng ngắt Timer phần cứng để CPU vẫn rảnh xử lý việc khác trong lúc đèn vẫn chạy đều đặn — không cần vòng lặp chờ tốn tài nguyên như ở đây.

Ví dụ tính toán: Tính mẫu bit PORTD cho dãy LED hiển thị số 13

Đề bài: Với mạch bộ đếm nhị phân ở mục 5.1 (4 LED nối PD0–PD3, PD0 là bit thấp nhất), hãy tính giá trị nhị phân và hex cần gán cho 4 bit thấp của PORTD để hiển thị số thập phân $13$, giả sử 4 bit cao của PORTD đang giữ nguyên giá trị 0000.

Lời giải

1. Đổi $13$ sang nhị phân 4-bit bằng cách trừ dần luỹ thừa của 2:

$$13 = 8 + 4 + 1 = 2^3 + 2^2 + 2^0 \Rightarrow 13_{10} = 1101_2$$

2. Gán từng bit theo đúng thứ tự PD3 PD2 PD1 PD0 (từ trái sang phải):

$$\text{PD3}=1,\ \text{PD2}=1,\ \text{PD1}=0,\ \text{PD0}=1$$

3. Vì 4 bit cao đang là 0000, toàn byte PORTD là:

$$PORTD = 0b00001101 = 0x0D = 13_{10}$$

Kết luận: LED tại PD3, PD2, PD0 sẽ sáng (giá trị bit = 1), riêng PD1 tắt (giá trị bit = 0) — đúng như mã gợi ý trong chú thích code ở mục 5.1.

6. Trình giả lập: Lập trình GPIO cho MCU ảo

Bảng dưới đây mô phỏng một MCU ảo với LED nối ở chân PB5 (giống hệt LED tích hợp sẵn trên board Arduino Uno). Bấm ▶ Chạy từng lệnh để thực thi tuần tự đoạn mã chớp tắt LED, quan sát giá trị 3 thanh ghi DDRB/PORTB/PINB đổi theo từng dòng lệnh và đèn LED thật sự sáng/tắt tương ứng. Nút ❌ Bỏ qua bước set DDR tái hiện chính xác cạm bẫy ở mục 4.

Chương trình (chớp tắt LED)

DDRB
0x00
PORTB
0x00
PINB
0x00
MCU PB5 → LED
Bấm "Chạy từng lệnh" để bắt đầu — dòng lệnh đang thực thi sẽ tô sáng bên trái.

7. Câu hỏi trắc nghiệm ôn tập

Câu 1: Bạn ghi PORTB |= (1 << PB5); để bật LED nối chân PB5 nhưng LED hoàn toàn không sáng. Nguyên nhân nhiều khả năng nhất là gì?

Đáp án đúng: Quên cấu hình DDR trước. Nếu DDR bit vẫn là 0 (mặc định INPUT), giá trị PORT bit không điều khiển điện áp ra chân mà chỉ bật/tắt điện trở pull-up nội bộ — dòng quá yếu để LED sáng rõ.

Câu 2: Vì sao thanh ghi PINB (đọc trạng thái chân) thường phải khai báo với từ khoá volatile trong C?

Đáp án đúng: Ngăn trình biên dịch tối ưu sai. Không có volatile, biến có thể bị "đóng băng" ở giá trị đọc lần đầu, không phản ánh thay đổi thật từ phần cứng bên ngoài.

Câu 3: Bạn cần bật thêm chân PB2 làm output mà không ảnh hưởng đến PB5 (LED) đã được cấu hình output từ trước. Dòng lệnh nào đúng?

Đáp án đúng: DDRB |= (1 << PB2); Toán tử OR chỉ bật thêm bit 2, giữ nguyên bit 5 đã set trước đó. Ba đáp án còn lại đều ghi đè hoặc dùng sai toán tử, xoá mất cấu hình PB5.