Mở đầu: làm mờ một LED mà không có "núm vặn điện áp" nào cả
MCU chỉ biết đúng 2 mức: 0V hoặc VCC (thường 3.3V) — không có "0.5V", không có "70% độ sáng" nào ghi thẳng vào một chân output cả. Vậy làm sao đèn LED trên xe đạp, màn hình laptop, hay động cơ quạt điều chỉnh tốc độ mượt mà chỉ bằng vi điều khiển thuần digital? Và làm sao một con buzzer bé xíu (chỉ có thể "kêu" hoặc "im") lại phát ra được nốt nhạc có cao độ nghe rõ ràng?
Câu trả lời là PWM — bật/tắt cực nhanh với tỷ lệ thời gian bật khác nhau, đánh lừa cả mắt lẫn tai. Bài này mổ xẻ ngoại vi tạo ra PWM — Timer đa năng (TIM) — từ toán học prescaler/auto-reload đến 2 cạm bẫy kinh điển khi tính tần số, rồi áp dụng vào ứng dụng thật: điều khiển góc quay servo RC. Cuối bài, bạn tự tay kéo 3 thanh trượt và NGHE buzzer đổi cao độ thật.
1. Timer đa năng khác gì SysTick?
Bài 4 đã giới thiệu SysTick — một bộ đếm CỐ ĐỊNH trong lõi ARM, chỉ có đúng 1 việc: giữ "giờ hệ thống"
(millis()). TIM1 là một ngoại vi HOÀN TOÀN KHÁC — đặc thù riêng của hãng
chip (giống GPIOA), với vai trò khác hẳn: tạo ra dạng sóng (PWM, xung vuông tần số bất
kỳ) và đo đạc thế giới ngoài (đếm xung ngoài, đo độ rộng xung — gọi là input capture, bài
này không đi sâu). Hai vai trò không đổi chỗ được: SysTick không tạo được PWM, TIM1 không thích hợp làm
đồng hồ hệ thống toàn cục (mỗi vendor có số lượng TIM khác nhau, không chuẩn hoá như SysTick).
TIM1 có 4 thanh ghi cốt lõi cho bài này:
| Thanh ghi | Vai trò |
|---|---|
CR1 |
Control Register — bit 0 (CEN) bật/tắt bộ đếm, giống SYST_CSR Bài 4 |
PSC |
Prescaler — chia bớt xung nhịp đầu vào TRƯỚC khi bộ đếm nhận |
ARR |
Auto-Reload Register — bộ đếm đếm từ 0 tới đây rồi tự quay về 0 (đặt CHU KỲ) |
CCR1 |
Capture/Compare Register kênh 1 — đặt NGƯỠNG so sánh (đặt ĐỘ RỘNG xung cao trong PWM) |
2. Toán prescaler/ARR: 2 cạm bẫy kinh điển
Bộ đếm TIM1 không đếm trực tiếp xung nhịp gốc (thường rất nhanh, 72MHz trên STM32F103) — nó đếm xung nhịp ĐÃ được chia bởi prescaler. Công thức tần số cập nhật (mỗi lần bộ đếm tràn từ ARR về 0):
$$f_{update} = \dfrac{f_{clk}}{(PSC+1)(ARR+1)}$$
Chú ý dấu +1 ở CẢ HAI vế — đây chính là cạm bẫy đầu tiên. Vài ví dụ số thật (với
f_clk = 72MHz, xung nhịp APB2 timer thật của STM32F103):
| PSC | ARR | Tần số | Độ phân giải duty (ARR+1 bậc) |
|---|---|---|---|
| 7199 | 9999 | 1 Hz | 10.000 bậc |
| 71 | 999 | 1000 Hz | 1.000 bậc (mịn) |
| 7199 | 9 | 1000 Hz (CÙNG tần số!) | 10 bậc (rất thô) |
| 0 | 3599 | 20.000 Hz | 3.600 bậc |
Hai dòng giữa bảng là điểm mấu chốt: cùng một tần số 1000Hz có thể đạt được bằng 2 cặp
PSC/ARR hoàn toàn khác nhau — nhưng độ phân giải duty (số bậc sáng LED có thể phân biệt được) lệch nhau
tới 100 lần. Số đo thật này lấy trực tiếp từ self-test node vmcu.js (Mục 5).
f_clk / (PSC * ARR) (bỏ quên cả 2
dấu +1) thay vì công thức đúng, hậu quả xuất hiện NGAY LẬP TỨC khi PSC = 0
(giá trị prescaler mặc định/tối thiểu, nghĩa thật là "chia cho 1") — công thức sai tính ra
f_clk / 0, một phép chia cho 0 (verify bằng self-test: kết quả
Infinity). (2) Chọn PSC quá lớn: để đạt tần số thấp (nghe được bằng tai,
cỡ trăm Hz), nếu chọn PSC lớn thay vì ARR lớn, ARR còn lại quá nhỏ — như dòng 3 trong bảng trên, PWM chỉ
còn 10 bậc sáng phân biệt được thay vì 1000 bậc, LED nhấp nháy giật cục thay vì mờ dần mượt mà. Nguyên
tắc: ưu tiên ARR lớn, PSC nhỏ nhất có thể đạt tần số mong muốn.
3. PWM — giả lập analog bằng digital thuần
PWM là kỹ thuật bật/tắt một chân digital cực nhanh, với tỷ lệ thời gian ở mức cao trong 1 chu kỳ gọi là duty cycle:
$$D = \dfrac{CCR}{ARR}$$
LED nối vào chân PWM có vẻ "mờ dần" không phải vì điện áp thay đổi (vẫn chỉ 0V/3.3V) — mà vì mắt người tích phân ánh sáng theo thời gian: nếu chu kỳ đủ nhanh (trên vài trăm Hz, mắt không phân biệt được nhấp nháy riêng lẻ), LED bật 25% thời gian trong 1 chu kỳ trông mờ hơn LED bật 75% thời gian, dù về bản chất điện, LED vẫn chỉ ở 1 trong 2 trạng thái tại bất kỳ thời điểm nào. Buzzer thì khác — tai không "tích phân" theo cách đó, mà nghe được TẦN SỐ dao động trực tiếp là cao độ âm thanh; đó là lý do PWM điều khiển buzzer chủ yếu qua tần số (Mục 2), còn PWM điều khiển LED chủ yếu qua duty cycle.
4. Ứng dụng kinh điển: điều khiển servo RC
Servo RC (loại phổ biến trong robot/mô hình) đọc góc quay từ ĐỘ RỘNG XUNG (không phải duty cycle phần trăm): khung tín hiệu lặp lại mỗi 20ms (50Hz), độ rộng xung mức cao từ 1ms đến 2ms mã hoá góc quay từ 0° đến 180° (1.5ms thường là góc giữa 90°). Tính PSC/ARR để có tick đúng 1 micro-giây giúp việc tính CCR cực đơn giản — không cần quy đổi gì thêm:
// f_clk = 72MHz. Chon PSC=71 -> tick = 72MHz/(71+1) = 1MHz -> 1 tick = 1us.
#define TIM1_PSC (*(volatile uint32_t *)0x40012C28)
#define TIM1_ARR (*(volatile uint32_t *)0x40012C2C)
#define TIM1_CCR1 (*(volatile uint32_t *)0x40012C34)
TIM1_PSC = 71; // tick = 1us
TIM1_ARR = 19999; // chu ky = 20000 tick = 20000us = 20ms = 50Hz (dung chuan servo)
TIM1_CCR1 = 1000; // 1000 tick = 1000us = 1ms -> goc servo MIN (0 do)
TIM1_CCR1 = 1500; // 1500 tick = 1500us = 1.5ms -> goc servo GIUA (90 do)
TIM1_CCR1 = 2000; // 2000 tick = 2000us = 2ms -> goc servo MAX (180 do)
Vì tick đã đúng 1 micro-giây, CCR1 chính là số micro-giây của độ rộng xung — không cần quy
đổi. Chiều ngược lại (đo độ rộng xung ĐẾN từ bên ngoài, gọi là input capture) cũng tồn tại trên cùng phần
cứng TIM, nhưng bài này không đi sâu.
f_clk = 72MHz, chọn PSC = 71 luôn cho tick 1us (vì
72.000.000 / 72 = 1.000.000 tick/giây = 1MHz); đây là lý do PSC = 71 xuất hiện
lặp lại trong cả bảng ví dụ Mục 2 lẫn tính toán servo Mục 4.
5. VMCU: TIM1 + PWM đã "sống"
VMCU thêm ngoại vi TIM1 vào vùng Peripheral (địa chỉ 0x40012C00, đúng số thật STM32F103), với
4 thanh ghi ở Mục 1. VMCU KHÔNG mô phỏng bộ đếm chạy sống theo thời gian thực (không cần cho bài này — chỉ
cần TÍNH tần số/duty từ 4 thanh ghi, khác SysTick cần đếm tick thật):
tim1Frequency() {
// dung CONG THUC DUNG - co "+1" o ca 2 thanh phan
return TIM1_CLK_HZ / ((this.tim1Psc + 1) * (this.tim1Arr + 1));
}
tim1DutyCycle() {
if (this.tim1Arr === 0) return 0;
return this.tim1Ccr1 / this.tim1Arr; // duty = CCR/ARR
}
Số đo thật từ self-test (node vmcu.js) xác nhận cạm bẫy Mục 2:
| Phép tính | Kết quả |
|---|---|
| Công thức ĐÚNG, PSC=0, ARR=3599 | 20.000 Hz |
| Công thức SAI (quên +1), cùng PSC=0, ARR=3599 | Infinity (chia cho 0) |
| Servo: PSC=71, ARR=19999 → tần số khung | 50 Hz (đúng chuẩn servo) |
| Servo: CCR1=1000 → độ rộng xung | 1000us = 1ms |
| Servo: CCR1=2000 → độ rộng xung | 2000us = 2ms |
6. Thực hành: kéo 3 thanh trượt, NGHE tần số thật
Kéo PSC/ARR/CCR bên dưới — công thức cập nhật SỐ THẬT theo tay kéo, LED đổi độ sáng theo duty cycle, và bấm "Phát âm" để NGHE buzzer đổi cao độ thật theo tần số tính được (dùng Web Audio, sóng vuông — đúng dạng sóng PWM thật):
Kéo ARR/CCR khi PSC nhỏ để nghe âm cao (tần số lớn); kéo PSC lớn để nghe âm trầm (tần số nhỏ) — nhưng chú ý độ phân giải duty (số bậc sáng LED) giảm mạnh khi PSC lớn, đúng cạm bẫy Mục 2.
// Dung o tab "Xem truoc": PSC/ARR/CCR nguoi dung keo tuong ung 3 thanh ghi that
TIM1_PSC = psc_slider_value;
TIM1_ARR = arr_slider_value;
TIM1_CCR1 = ccr_slider_value;
TIM1_CR1 |= (1 << 0); // bat CEN
// Tan so xuat ra buzzer, duty cycle dieu khien do sang LED
float freq = 72000000.0f / ((psc_slider_value + 1.0f) * (arr_slider_value + 1.0f));
float duty = (float)ccr_slider_value / (float)arr_slider_value;
import { VMCU, TIM1_PSC_ADDR, TIM1_ARR_ADDR, TIM1_CCR1_ADDR } from './vmcu.js';
const cpu = new VMCU();
function onSliderChange(psc, arr, ccr) {
cpu.write32(TIM1_PSC_ADDR, psc);
cpu.write32(TIM1_ARR_ADDR, arr);
cpu.write32(TIM1_CCR1_ADDR, ccr);
const freq = cpu.tim1Frequency(); // f_clk / ((PSC+1)(ARR+1))
const duty = cpu.tim1DutyCycle(); // CCR / ARR
led.style.background = duty > 0 ? `rgba(34,197,94,${duty})` : '#334155';
oscillator.frequency.value = freq; // sóng vuông Web Audio = PWM thật
Tóm lược
- ✅ TIM1 khác SysTick (Bài 4): SysTick giữ giờ hệ thống cố định trong lõi ARM, TIM1 tạo dạng sóng/đo đạc — đặc thù riêng hãng chip, không đổi vai cho nhau được.
-
✅ Công thức tần số
f = f_clk/((PSC+1)(ARR+1))— quên dấu "+1" gây chia cho 0 khi PSC=0 (verified:Infinity). - ✅ Cùng 1 tần số có thể đạt bằng nhiều cặp PSC/ARR khác nhau, nhưng độ phân giải duty (ARR+1 bậc) lệch nhau rất lớn — ưu tiên ARR lớn, PSC nhỏ.
- ✅ PWM: duty = CCR/ARR — LED mờ dần nhờ mắt tích phân ánh sáng, buzzer đổi cao độ nhờ tai nghe trực tiếp tần số. Đủ tốt cho LED/động cơ/buzzer, KHÔNG thay được DAC cho audio chất lượng cao.
- ✅ Servo RC: khung 50Hz, xung 1-2ms mã hoá góc — verified: PSC=71/ARR=19999 cho tick=1us, CCR=1000/2000 cho đúng 1ms/2ms.
Trắc nghiệm ôn tập
Câu 1
Vai trò của TIM1 khác gì so với SysTick (Bài 4)?
Câu 2
Nếu tính tần số bằng công thức SAI f_clk / (PSC * ARR) (quên cả 2 dấu "+1"), điều gì xảy ra
khi PSC = 0?
Câu 3
PSC=71/ARR=999 và PSC=7199/ARR=9 đều cho ra CÙNG tần số 1000Hz. Vì sao vẫn nên chọn cặp đầu tiên?
Câu 4
Với PSC=71 (tick=1us), ARR=19999 (khung 50Hz), cần đặt CCR1 bằng bao nhiêu để tạo xung
1.5ms (góc servo giữa, 90°)?
Tải file code thực hành minh họa bài học
File JavaScript VMCU — engine MCU ảo dùng xuyên suốt cả 16 bài, Bài 6 vừa thêm TIM1 (PSC,
ARR, CCR1, tính tần số/duty cycle PWM), kèm self-test đối chiếu đúng mọi hành vi trong bài (chạy
node vmcu.js, không cần cài thêm gì):
Bình luận