# BÀI 36 – TIMER INTERRUPT TRÊN STM32F103: TỰ TẠO HỆ THỐNG `millis()` 1 ms
Đây là một bài **rất quan trọng** trong quá trình học STM32 của bạn.
Từ Bài 35, chúng ta đã dùng Timer để debounce nút nhấn. Bây giờ chúng ta sẽ tiến thêm một bước:
> **Tự xây dựng một bộ đếm thời gian 1 ms bằng Timer Interrupt, tương tự ý tưởng của `millis()` trong Arduino.**
Sau bài này, bạn sẽ hiểu rất rõ:
“`text
Clock STM32
↓
TIM2
↓
Interrupt mỗi 1 ms
↓
millis++
↓
Chương trình chính sử dụng thời gian
“`
Và quan trọng nhất:
> **Không cần `delay()` để tạo các khoảng thời gian trong chương trình.**
—
# 1. Vấn đề của `delay()`
Ví dụ bạn muốn LED nhấp nháy mỗi 1 giây.
Cách đơn giản:
“`c
while (1)
{
GPIOC->ODR ^= (1 << 13);
delay_ms(1000);
}
“`
Nhưng trong thời gian:
“`text
delay_ms(1000)
“`
CPU gần như chỉ:
“`text
CHỜ
CHỜ
CHỜ
CHỜ
…
“`
Nếu chương trình còn phải làm:
“`text
LCD
UART
Button
Sensor
Motor
ADC
“`
thì cách này rất bất tiện.
—
# 2. Cách tốt hơn: non-blocking
Thay vì:
“`c
delay_ms(1000);
“`
ta hỏi:
> “Đã đủ 1000 ms chưa?”
Ví dụ:
“`c
if (millis – last_time >= 1000)
{
last_time = millis;
LED_Toggle();
}
“`
CPU không phải đứng chờ.
Nó có thể tiếp tục:
“`text
LCD
↓
Button
↓
Sensor
↓
Motor
↓
UART
↓
kiểm tra millis
↓
lặp lại
“`
Đây gọi là:
> **Non-blocking programming**
—
# 3. `millis()` thực chất là gì?
Trong Arduino, bạn thường thấy:
“`c
millis();
“`
Ví dụ:
“`c
if (millis() – lastTime >= 1000)
{
lastTime = millis();
digitalWrite(LED, !digitalRead(LED));
}
“`
`millis()` trả về:
> Số mili-giây đã trôi qua kể từ khi hệ thống bắt đầu chạy.
Ví dụ:
“`text
0 ms
1 ms
2 ms
3 ms
…
1000 ms
1001 ms
…
“`
—
# 4. STM32F103 không tự nhiên có `millis()`
Đây là điều rất quan trọng.
Nếu bạn lập trình STM32F103 trực tiếp bằng thanh ghi:
“`c
STM32F103
“`
thì không có một thanh ghi phần cứng tên:
“`c
millis
“`
Chúng ta phải **tự tạo nó**.
Ý tưởng:
“`c
volatile uint32_t millis = 0;
“`
Sau mỗi 1 ms:
“`c
millis++;
“`
—
# 5. Ai làm nhiệm vụ tăng `millis`?
Chính là:
# TIM2 + Interrupt
Sơ đồ:
“`text
72 MHz
│
▼
TIM2
│
mỗi 1 ms
│
▼
TIM2 interrupt
│
▼
TIM2_IRQHandler()
│
▼
millis++
“`
—
# 6. Tại sao chọn TIM2?
STM32F103C8T6 có:
“`text
TIM1
TIM2
TIM3
TIM4
“`
Trong bài này chúng ta dùng:
“`text
TIM2
“`
TIM2 rất tiện để học vì nó là **general-purpose timer** và không cần sử dụng chân GPIO để tạo tick nội bộ.
—
# 7. Timer clock là bao nhiêu?
Với cấu hình STM32F103 phổ biến:
“`text
SYSCLK = 72 MHz
“`
Nhưng cần cẩn thận:
> Clock cấp cho Timer không phải lúc nào cũng đơn giản là SYSCLK.
Với cấu hình APB1 thông dụng:
“`text
SYSCLK = 72 MHz
APB1 = 36 MHz
“`
Do APB1 prescaler khác 1, clock Timer trên APB1 được nhân 2:
“`text
TIM2 clock = 72 MHz
“`
Vì vậy trong cấu hình chuẩn 72 MHz:
“`text
TIM2CLK = 72 MHz
“`
—
# 8. Công thức Timer
Nhắc lại công thức:
[
f_{timer}=
\frac{f_{TIM}}
{(PSC+1)(ARR+1)}
]
Muốn interrupt mỗi:
“`text
1 ms
“`
thì:
[
f = 1000Hz
]
Ta cần:
[
\frac{72,000,000}
{(PSC+1)(ARR+1)}
=1000
]
Hay:
[
(PSC+1)(ARR+1)=72000
]
—
# 9. Chọn Prescaler
Ta chọn:
“`c
PSC = 7199;
“`
Khi đó:
[
72,MHz / 7200 = 10,kHz
]
Tức Timer đếm:
“`text
10.000 tick / giây
“`
Một tick:
[
1/10000 = 100\mu s
]
—
# 10. Chọn ARR
Muốn tạo:
“`text
1 ms
“`
mà mỗi tick:
“`text
100 µs
“`
thì cần:
“`text
10 tick
“`
Do ARR tính từ 0:
“`c
ARR = 9;
“`
Vậy:
“`c
TIM2->PSC = 7199;
TIM2->ARR = 9;
“`
Tạo:
“`text
10 × 100 µs = 1 ms
“`
—
# 11. Đây là hai con số bạn cần nhớ
“`c
TIM2->PSC = 7199;
TIM2->ARR = 9;
“`
với:
“`text
TIM2 clock = 72 MHz
“`
ta có:
“`text
1 interrupt ≈ 1 ms
“`
—
# 12. Bật clock cho TIM2
TIM2 nằm trên APB1.
Vì vậy:
“`c
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
“`
Đây là bước đầu tiên.
Nếu quên:
“`text
TIM2 không có clock
↓
Timer không chạy
↓
Không có interrupt
↓
millis không tăng
“`
—
# 13. Cấu hình Prescaler và Auto Reload
“`c
TIM2->PSC = 7199;
TIM2->ARR = 9;
“`
Sau đó:
“`c
TIM2->CNT = 0;
“`
đưa bộ đếm về 0.
—
# 14. Cho phép Update Interrupt
Thanh ghi:
“`c
TIM2->DIER
“`
là:
> DMA/Interrupt Enable Register
Ta bật Update Interrupt:
“`c
TIM2->DIER |= TIM_DIER_UIE;
“`
Ý nghĩa:
“`text
Timer overflow/update
↓
Cho phép tạo interrupt
↓
CPU có thể nhận interrupt
“`
—
# 15. Bật NVIC
Đây là phần chúng ta học ở Bài 34.
“`c
NVIC_EnableIRQ(TIM2_IRQn);
“`
Có thể đặt priority:
“`c
NVIC_SetPriority(TIM2_IRQn, 2);
“`
Sau đó:
“`c
NVIC_EnableIRQ(TIM2_IRQn);
“`
—
# 16. Start Timer
Cuối cùng:
“`c
TIM2->CR1 |= TIM_CR1_CEN;
“`
`CEN`:
> Counter Enable
Khi:
“`text
CEN = 1
“`
Timer bắt đầu chạy.
—
# 17. Timer sẽ hoạt động như thế này
“`text
CNT = 0
↓
1
↓
2
↓
3
↓
…
↓
9
↓
Update Event
↓
CNT quay về 0
↓
Interrupt
“`
Chu kỳ:
“`text
1 ms
“`
—
# 18. ISR của TIM2
Chúng ta viết:
“`c
volatile uint32_t millis = 0;
void TIM2_IRQHandler(void)
{
if (TIM2->SR & TIM_SR_UIF)
{
TIM2->SR &= ~TIM_SR_UIF;
millis++;
}
}
“`
Mỗi lần Timer tạo interrupt:
“`text
millis++
“`
—
# 19. Kết quả
Sau khi chạy:
“`text
t = 0 ms
millis = 0
t = 1 ms
millis = 1
t = 2 ms
millis = 2
t = 3 ms
millis = 3
…
t = 1000 ms
millis = 1000
“`
Vậy chúng ta vừa **tự xây dựng `millis()`**.
—
# 20. Tại sao phải dùng `volatile`?
Ta khai báo:
“`c
volatile uint32_t millis = 0;
“`
vì:
“`text
main()
│
├── đọc millis
│
└── ISR thay đổi millis
“`
Giá trị có thể thay đổi bất kỳ lúc nào do interrupt.
Nếu không dùng `volatile`, compiler có thể tối ưu hóa việc đọc biến theo cách không phù hợp với tình huống này.
Vì vậy:
“`c
volatile
“`
rất quan trọng.
—
# 21. Chương trình đầu tiên
Chúng ta có thể viết:
“`c
#include “stm32f10x.h”
volatile uint32_t millis = 0;
void TIM2_Init(void)
{
/* Enable TIM2 clock */
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
/* 72 MHz / 7200 = 10 kHz */
TIM2->PSC = 7199;
/* 10 ticks = 1 ms */
TIM2->ARR = 9;
TIM2->CNT = 0;
/* Enable update interrupt */
TIM2->DIER |= TIM_DIER_UIE;
/* NVIC */
NVIC_SetPriority(TIM2_IRQn, 2);
NVIC_EnableIRQ(TIM2_IRQn);
/* Start timer */
TIM2->CR1 |= TIM_CR1_CEN;
}
void TIM2_IRQHandler(void)
{
if (TIM2->SR & TIM_SR_UIF)
{
TIM2->SR &= ~TIM_SR_UIF;
millis++;
}
}
int main(void)
{
TIM2_Init();
while (1)
{
}
}
“`
Đây là **bộ đếm thời gian 1 ms tối thiểu**.
—
# 22. Nhưng `millis` chưa làm được gì?
Hiện tại:
“`c
millis++;
“`
nhưng:
“`c
while (1)
{
}
“`
chưa sử dụng nó.
Bây giờ chúng ta sẽ dùng `millis`.
—
# 23. Điều khiển LED mỗi 1 giây
Giả sử:
“`text
PC13 = LED onboard
“`
Ta có:
“`c
uint32_t last_time = 0;
“`
Trong `while`:
“`c
if (millis – last_time >= 1000)
{
last_time = millis;
GPIOC->ODR ^= (1 << 13);
}
“`
Kết quả:
“`text
1 giây
↓
LED toggle
1 giây
↓
LED toggle
1 giây
↓
LED toggle
“`
—
# 24. Đây chính là non-blocking
Chúng ta không viết:
“`c
delay_ms(1000);
“`
mà viết:
“`c
if (millis – last_time >= 1000)
“`
CPU có thể làm nhiều việc khác.
Ví dụ:
“`c
while (1)
{
Read_Button();
Read_ADC();
Update_LCD();
Process_UART();
if (millis – last_time >= 1000)
{
last_time = millis;
LED_Toggle();
}
}
“`
Không có:
“`c
delay()
“`
—
# 25. Đây là một bước cực kỳ lớn
Bạn vừa chuyển từ:
“`text
Blocking
“`
sang:
“`text
Non-blocking
“`
Ví dụ blocking:
“`c
LED_ON();
delay_ms(1000);
LED_OFF();
delay_ms(1000);
“`
CPU bị giữ trong delay.
Còn non-blocking:
“`c
if (millis – last >= 1000)
{
last = millis;
LED_Toggle();
}
“`
CPU luôn có thể tiếp tục xử lý.
—
# 26. Chạy nhiều tác vụ cùng lúc
Đây là lúc `millis()` thực sự trở nên mạnh.
Ví dụ:
“`c
uint32_t t_led = 0;
uint32_t t_sensor = 0;
uint32_t t_lcd = 0;
“`
Trong `while`:
“`c
if (millis – t_led >= 500)
{
t_led = millis;
LED_Toggle();
}
if (millis – t_sensor >= 100)
{
t_sensor = millis;
Read_Sensor();
}
if (millis – t_lcd >= 1000)
{
t_lcd = millis;
LCD_Update();
}
“`
Ta có:
“`text
LED → 500 ms
Sensor → 100 ms
LCD → 1000 ms
“`
Tất cả chạy trong cùng một `while`.
—
# 27. Không cần 3 Timer
Đây là điểm rất hay.
Bạn có:
“`text
1 Timer
↓
1 ms system tick
↓
nhiều software timer
“`
Ví dụ:
“`text
TIM2
│
└── millis
│
├── LED timer
├── Sensor timer
├── LCD timer
├── Button timer
├── Motor timer
└── UART timeout
“`
Đây là kiến trúc rất phổ biến.
—
# 28. `millis()` giống như một chiếc đồng hồ hệ thống
Hãy tưởng tượng:
“`text
millis = 123456
“`
nghĩa là hệ thống đã chạy khoảng:
“`text
123.456 giây
“`
Từ đó mọi module có thể sử dụng cùng một mốc thời gian.
—
# 29. Cách kiểm tra thời gian đúng
Bạn nên viết:
“`c
if (millis – last_time >= interval)
“`
chứ không nên viết:
“`c
if (millis >= last_time + interval)
“`
Tại sao?
Vì `uint32_t` sẽ **overflow** sau một khoảng thời gian rất dài.
—
# 30. Overflow của `uint32_t`
`uint32_t` có giá trị tối đa:
“`text
4,294,967,295
“`
Nếu `millis` tiếp tục tăng:
“`text
4,294,967,295
↓
0
“`
Đây gọi là:
> **Integer overflow / wrap-around**
Nếu bạn viết:
“`c
millis – last_time
“`
với kiểu `uint32_t`, phép tính modulo giúp cách kiểm tra khoảng thời gian vẫn hoạt động đúng trong điều kiện khoảng thời gian cần đo nhỏ hơn chu kỳ wrap-around.
Đây là lý do pattern:
“`c
if ((uint32_t)(millis – last_time) >= interval)
“`
rất tốt.
—
# 31. Đây là pattern nên học thuộc
“`c
uint32_t last_time = 0;
if ((uint32_t)(millis – last_time) >= 1000)
{
last_time = millis;
// Task
}
“`
Đây là một trong những pattern quan trọng nhất trong embedded programming.
—
# 32. Ví dụ Timer 500 ms
“`c
uint32_t last_led = 0;
while (1)
{
if ((uint32_t)(millis – last_led) >= 500)
{
last_led = millis;
GPIOC->ODR ^= (1 << 13);
}
}
“`
—
# 33. Ví dụ đọc cảm biến mỗi 100 ms
“`c
uint32_t last_sensor = 0;
while (1)
{
if ((uint32_t)(millis – last_sensor) >= 100)
{
last_sensor = millis;
Sensor_Read();
}
}
“`
—
# 34. Ví dụ LCD mỗi 1 giây
“`c
uint32_t last_lcd = 0;
while (1)
{
if ((uint32_t)(millis – last_lcd) >= 1000)
{
last_lcd = millis;
LCD_Update();
}
}
“`
—
# 35. Một `millis` có thể điều khiển cả hệ thống
Ví dụ firmware:
“`c
while (1)
{
if ((uint32_t)(millis – t_led) >= 500)
{
t_led = millis;
LED_Toggle();
}
if ((uint32_t)(millis – t_sensor) >= 50)
{
t_sensor = millis;
Sensor_Read();
}
if ((uint32_t)(millis – t_motor) >= 10)
{
t_motor = millis;
Motor_Task();
}
if ((uint32_t)(millis – t_lcd) >= 1000)
{
t_lcd = millis;
LCD_Update();
}
}
“`
Ta có:
“`text
Motor → 10 ms
Sensor → 50 ms
LED → 500 ms
LCD → 1 s
“`
mà chỉ cần:
“`text
TIM2
“`
làm system tick.
—
# 36. Một vấn đề: ISR chạy mỗi 1 ms
Có nghĩa là:
“`text
1000 interrupt / giây
“`
Điều này có đáng lo không?
Với STM32F103 chạy 72 MHz:
“`text
1000 interrupt/s
“`
thường là hoàn toàn khả thi nếu ISR rất ngắn.
ISR của chúng ta chỉ:
“`c
millis++;
“`
và xử lý cờ.
Do đó thời gian CPU sử dụng rất nhỏ.
—
# 37. ISR tuyệt đối không nên làm việc nặng
Không nên:
“`c
void TIM2_IRQHandler(void)
{
LCD_Print(“Hello”);
ADC_ReadManyTimes();
Motor_LongCalculation();
Delay();
millis++;
}
“`
Nên:
“`c
void TIM2_IRQHandler(void)
{
if (TIM2->SR & TIM_SR_UIF)
{
TIM2->SR &= ~TIM_SR_UIF;
millis++;
}
}
“`
**ISR càng ngắn càng tốt.**
—
# 38. Một kiến trúc tốt hơn
Ta có thể dùng Timer ISR để tạo:
“`text
millis
“`
nhưng mọi công việc thực tế nằm ở `main()`:
“`text
TIM2 ISR
↓
millis++
↓
main()
├── LED task
├── Button task
├── Sensor task
├── Motor task
└── LCD task
“`
Đây là kiến trúc rất dễ mở rộng.
—
# 39. Tích hợp với Bài 35
Bạn có thể thấy Bài 35 và Bài 36 liên kết với nhau rất đẹp.
Bài 35:
“`text
EXTI
↓
Timer
↓
Debounce
“`
Bài 36:
“`text
Timer
↓
1 ms tick
↓
millis
↓
software timers
“`
Sau này chúng ta có thể viết debounce mà **không cần start/stop Timer cho từng nút**.
Ví dụ:
“`c
if (millis – button_time >= 20)
{
// kiểm tra nút
}
“`
Đây là cách tiếp cận rất mạnh.
—
# 40. Thậm chí nhiều nút
Ví dụ:
“`text
PA0 → Button 1
PA1 → Button 2
PA2 → Button 3
“`
Ta có thể quản lý:
“`text
button1_time
button2_time
button3_time
“`
với cùng một:
“`text
millis
“`
Không cần:
“`text
TIM2 → Button1
TIM3 → Button2
TIM4 → Button3
“`
—
# 41. Toàn bộ kiến trúc hiện tại
Sau Bài 36:
“`text
STM32F103
│
┌──────────┴──────────┐
│ │
EXTI TIM2
│ │
▼ ▼
NVIC 1 ms interrupt
│ │
▼ ▼
Button event millis++
│ │
└──────────┬──────────┘
▼
main()
│
┌─────────────┼─────────────┐
▼ ▼ ▼
LED task Sensor task Motor task
│ │ │
500 ms 100 ms 10 ms
“`
Đây chính là nền móng cho một **firmware multitasking kiểu đơn giản** mà chưa cần RTOS.
—
# 42. Code hoàn chỉnh: TIM2 + millis + LED
Đây là phiên bản bạn nên tự gõ và chạy trên Blue Pill:
“`c
#include “stm32f10x.h”
volatile uint32_t millis = 0;
void GPIO_Init(void)
{
/* Enable GPIOC clock */
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
/* PC13 = Output Push-Pull 2 MHz */
GPIOC->CRH &= ~(0xF << 20);
GPIOC->CRH |= (0x2 << 20);
}
void TIM2_Init(void)
{
/* Enable TIM2 clock */
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
/*
TIM2 clock = 72 MHz
72 MHz / (7199 + 1)
= 10 kHz
10 kHz / (9 + 1)
= 1 kHz
1 kHz = 1 ms
*/
TIM2->PSC = 7199;
TIM2->ARR = 9;
TIM2->CNT = 0;
/* Enable update interrupt */
TIM2->DIER |= TIM_DIER_UIE;
/* NVIC */
NVIC_SetPriority(TIM2_IRQn, 2);
NVIC_EnableIRQ(TIM2_IRQn);
/* Start Timer */
TIM2->CR1 |= TIM_CR1_CEN;
}
void TIM2_IRQHandler(void)
{
if (TIM2->SR & TIM_SR_UIF)
{
TIM2->SR &= ~TIM_SR_UIF;
millis++;
}
}
int main(void)
{
GPIO_Init();
TIM2_Init();
uint32_t last_led = 0;
while (1)
{
if ((uint32_t)(millis – last_led) >= 1000)
{
last_led = millis;
GPIOC->ODR ^= (1 << 13);
}
}
}
“`
—
# 43. Một lưu ý về `TIM2->SR`
Bạn sẽ thấy:
“`c
TIM2->SR &= ~TIM_SR_UIF;
“`
Trong một số dòng STM32, cách xử lý cờ Timer cần chú ý đến cơ chế của từng flag và thao tác đọc/ghi thanh ghi.
Trong ví dụ học tập này, mục tiêu là giúp bạn hiểu:
“`text
UIF = Update Interrupt Flag
“`
và phải **xử lý cờ interrupt** trước khi thoát ISR.
—
# 44. Bài tập thực hành
### Bài tập 1 – LED 1 giây
Cho PC13:
“`text
ON 1 giây
OFF 1 giây
“`
—
### Bài tập 2 – LED 100 ms
Đổi:
“`c
>= 1000
“`
thành:
“`c
>= 100
“`
Quan sát tốc độ.
—
### Bài tập 3 – hai tác vụ
“`text
LED → 500 ms
LED khác → 1000 ms
“`
Bạn có thể dùng hai GPIO hoặc một GPIO với hai task khác nhau.
—
### Bài tập 4 – ba task
Tạo:
“`c
uint32_t t1 = 0;
uint32_t t2 = 0;
uint32_t t3 = 0;
“`
và:
“`text
Task 1 → 10 ms
Task 2 → 100 ms
Task 3 → 1000 ms
“`
—
# 45. Bài tập quan trọng nhất
Hãy thử tự viết:
“`c
uint32_t last_time = 0;
while (1)
{
if ((uint32_t)(millis – last_time) >= 1000)
{
last_time = millis;
// công việc
}
}
“`
Sau đó **không được sử dụng `delay()`**.
Nếu bạn làm được bài này thì bạn đã nắm được tư duy:
> **”Đừng bắt CPU chờ. Hãy để CPU làm việc khác và kiểm tra thời gian khi cần.”**
—
# 46. Bài 36 liên kết với những bài trước như thế nào?
“`text
Bài 30
Tự viết GPIO Driver
↓
Bài 32
GPIO Input + Pull-up
↓
Bài 33
EXTI
↓
Bài 34
NVIC
↓
Bài 35
Debounce + Timer
↓
Bài 36
Timer Interrupt + millis
↓
Bài 37
Timer nâng cao + nhiều tác vụ
“`
Và từ đây chúng ta bắt đầu tiến tới một firmware STM32 thực sự có cấu trúc.
—
## 47. 8 điều bạn cần nhớ sau Bài 36
“`text
1. TIM2 có thể tạo interrupt mỗi 1 ms.
2. ISR của TIM2 tăng biến millis.
3. millis phải khai báo volatile.
4. Không cần delay() để tạo thời gian.
5. Dùng:
millis – last_time >= interval
6. Có thể chạy nhiều task bằng một system tick.
7. ISR nên cực kỳ ngắn.
8. Đây là nền tảng của non-blocking firmware.
“`
### Công thức quan trọng nhất của bài:
“`c
if ((uint32_t)(millis – last_time) >= interval)
{
last_time = millis;
// Task
}
“`
Hãy đặc biệt ghi nhớ dòng này. **Sau này khi chúng ta làm robot, stepper 28BYJ-48, đọc cảm biến, LCD, UART hoặc điều khiển nhiều thiết bị cùng lúc trên Blue Pill, kỹ thuật này sẽ được sử dụng liên tục.**