Trong quá trình cấu hình SysTick Timer STM32F103, việc tạo delay chính xác và đo thời gian ổn định là yêu cầu nền tảng cho hầu hết các ứng dụng nhúng. Thay vì sử dụng delay vòng lặp không chính xác, SysTick timer – bộ định thời tích hợp trong lõi ARM Cortex-M3 – cung cấp cơ chế tạo ngắt định kỳ với độ chính xác cao, phù hợp cho các hệ thống real-time.
1. Giới thiệu ngoại vi & Tại sao cần cấu hình SysTick Timer STM32F103?
Mục tiêu bài viết Lập trình STM32F103 thanh ghi: Cấu hình Systick Timer STM32F103RCT6
Trong bài viết này, chúng ta sẽ thực hiện lập trình STM32F103 thanh ghi để:
- Cấu hình SysTick timer tạo ngắt chính xác 1ms
- Sử dụng SysTick để tạo delay chính xác
- Điều khiển LED tại chân PA8 bật/tắt mỗi 15 giây
- Kiểm tra độ chính xác của SysTick timer STM32F103 trong thời gian dài (2 phút)
Bài viết sử dụng STM32F103RCT6, lập trình bare-metal, không dùng HAL, phù hợp cho người học lập trình thanh ghi STM32F103.
Để tìm hiểu thêm về kiến trúc tổng quát của STM32F103, hãy xem bài viết tổng quan về lập trình STM32F103 thanh ghi.
2. Define Struct thanh ghi kiểu Bit-field
Để truy cập các bit cụ thể trong thanh ghi SysTick, chúng ta định nghĩa các cấu trúc bit-field. Dưới đây là định nghĩa cho thanh ghi điều khiển và trạng thái (SysTick_CTRL) và bản đồ thanh ghi SysTick tổng thể, dựa trên tài liệu PM0056.
Thanh ghi SysTick control and status register (STK_CTRL)
Trích xuất từ tài liệu PM0056, Section 4.5.1, Trang 151

// Trích xuất từ tài liệu PM0056, Section 4.5.1, Trang 151
// Thanh ghi Điều khiển SysTick (SysTick Control and Status Register - STK_CTRL)
typedef union {
struct {
__IOM uint32_t ENABLE : 1; // Bit 0: Counter enable
__IOM uint32_t TICKINT : 1; // Bit 1: SysTick exception request enable
__IOM uint32_t CLKSOURCE : 1; // Bit 2: Clock source selection
uint32_t reserved1 : 13; // Bit 3-15: Reserved
__IM uint32_t COUNTFLAG : 1; // Bit 16: Returns 1 if timer counted to 0 since last read
uint32_t reserved2 : 15; // Bit 17-31: Reserved
};
__IOM uint32_t reg; // Access entire register
} SysTick_CTRL_Type;
Bản đồ thanh ghi SysTick (SysTick register map)
Trích xuất từ tài liệu PM0056, Section 4.5.6, Trang 154
// Trích xuất từ tài liệu PM0056, Section 4.5.6, Trang 154
// Bản đồ thanh ghi SysTick
typedef struct {
SysTick_CTRL_Type CTRL; // 0x00: SysTick Control and Status Register
__IOM uint32_t LOAD; // 0x04: SysTick Reload Value Register
__IOM uint32_t VAL; // 0x08: SysTick Current Value Register
__IM uint32_t CALIB; // 0x0C: SysTick Calibration Value Register
} SysTick_TypeDef;
#define SYSTICK ((SysTick_TypeDef *)0xE000E010UL)

3. Giải thích chức năng từng thanh ghi liên quan
Dựa theo dữ liệu tại PM0056, Section 4.5 (trang 151-153), các thanh ghi SysTick có chức năng như sau:
- CTRL (SysTick Control and Status Register): (PM0056, Section 4.5.1, Trang 151)
- ENABLE (Bit 0): Bật/tắt bộ đếm SysTick. Khi được đặt là 1, bộ đếm sẽ nạp giá trị từ thanh ghi LOAD và bắt đầu đếm ngược.
- TICKINT (Bit 1): Cho phép/ngắt yêu cầu ngắt SysTick. Nếu đặt là 1, một ngắt SysTick sẽ được tạo ra khi bộ đếm đạt về 0.
- CLKSOURCE (Bit 2): Chọn nguồn xung clock cho SysTick.
0: Sử dụng AHB/8 (tần số AHB bị chia 8).1: Sử dụng Processor clock (AHB, tần số đầy đủ của CPU).
- COUNTFLAG (Bit 16): Là một cờ chỉ thị (read-only), trả về 1 nếu bộ đếm đã đếm về 0 kể từ lần đọc gần nhất. Bit này sẽ tự động xóa về 0 khi đọc.
- LOAD (SysTick Reload Value Register): (PM0056, Section 4.5.2, Trang 152)
- Thanh ghi 24-bit này (Bits 23:0) chứa giá trị nạp lại (reload value) cho bộ đếm SysTick. Khi bộ đếm đạt 0, giá trị này sẽ được nạp lại để tiếp tục đếm. Giá trị ‘LOAD’ thực tế là ‘N-1’ nếu bạn muốn đếm N chu kỳ.
- VAL (SysTick Current Value Register): (PM0056, Section 4.5.3, Trang 153)
- Thanh ghi 24-bit này (Bits 23:0) chứa giá trị hiện tại của bộ đếm SysTick, đếm ngược từ giá trị được nạp trong thanh ghi LOAD về 0. Ghi bất kỳ giá trị nào vào thanh ghi này sẽ xóa giá trị hiện tại về 0 và xóa cả bit COUNTFLAG trong thanh ghi STK_CTRL.
- CALIB (SysTick Calibration Value Register): (PM0056, Section 4.5.4, Trang 153)
- Thanh ghi này cung cấp thông tin hiệu chuẩn cho SysTick, bao gồm giá trị TENMS (calibration value cho 1ms), SKEW và NOREF. Trên STM32F103RCT6, giá trị TENMS thường dùng để tính toán và các bit khác có thể không được sử dụng trực tiếp trong lập trình cơ bản.
4. Quy trình cấu hình SysTick Timer STM32F103
Để cấu hình SysTick Timer tạo ngắt 1ms trên STM32F103, chúng ta tuân thủ các bước sau, dựa trên tài liệu PM0056, Section 4.5:
graph TD;
A["Bắt đầu cấu hình SysTick"] --> B{"Xác định tần số AHB (SystemCoreClock)"};
B --> C["Tính toán giá trị LOAD cho 1ms
(LOAD = (AHB_Freq / 1000) - 1)"];
C --> D["Ghi giá trị LOAD vào thanh ghi SYSTICK->LOAD"];
D --> E["Xóa giá trị hiện tại của bộ đếm (SYSTICK->VAL = 0)"];
E --> F["Cấu hình thanh ghi điều khiển (SYSTICK->CTRL)"];
F --> F1["Chọn nguồn xung (CLKSOURCE = 1: AHB)"];
F1 --> F2["Bật ngắt (TICKINT = 1)"];
F2 --> F3["Kích hoạt bộ đếm (ENABLE = 1)"];
F3 --> G["Cấu hình ưu tiên ngắt SysTick trong NVIC (nếu cần)"];
G --> H["Viết hàm xử lý ngắt SysTick_Handler()"];
H --> I["Kết thúc cấu hình"];
Thông số cấu hình SysTick Timer 1ms
- SYSCLK = 72 MHz (Giả định SystemCoreClock đã được cấu hình trước đó)
- Chu kỳ mong muốn: 1 ms
- Giá trị LOAD: (72 MHz / 1000) – 1 = 72000 – 1 = 71999
- Để tạo ngắt mỗi 1ms (0.001 giây), ta cần tính số chu kỳ clock cần thiết:
Number of clock cycles = TInterrupt x Clock Frequency
Thay số:
Number of clock cycles = 0.001s x 72000000 Hz = 72000
- Theo công thức SysTick, số chu kỳ clock bằng LOAD + 1 (vì đếm từ LOAD về 0, tổng cộng LOAD + 1 chu kỳ):
LOAD + 1 = 72000 ==> LOAD = 72000 - 1 = 71999
- Do đó, đặt SYSTICK->LOAD = 71999 sẽ tạo ra ngắt mỗi 1ms khi tần số clock là 72MHz.
Cấu hình SysTick theo tài liệu PM0056
Theo PM0056 – STM32F10xxx Reference Manual, Section 4.5.1 (trang 151):
- CLKSOURCE = 1: SysTick sử dụng AHB clock (tần số CPU).
- TICKINT = 1: Cho phép ngắt SysTick khi đếm về 0.
- ENABLE = 1: Bật SysTick timer để bắt đầu đếm.
5. Code thực tế chạy trên Keil C
Code cấu hình SysTick timer 1ms STM32F103
volatile uint32_t msTicks = 0;
// SysTick interrupt handler
void SysTick_Handler(void) {
msTicks++; // Increment millisecond counter
}
void SysTick_Config_1ms(void) {
// Configure SysTick for 1ms interrupts (PM0056, Section 4.5, page 151)
SYSTICK->LOAD = 71999; // Reload value for 1ms at 72MHz (72,000 - 1)
SYSTICK->VAL = 0; // Clear current value
SYSTICK->CTRL.CLKSOURCE = 1; // Use AHB clock (Bit 2)
SYSTICK->CTRL.TICKINT = 1; // Enable interrupt (Bit 1)
SYSTICK->CTRL.ENABLE = 1; // Enable SysTick (Bit 0)
}
void Delay_ms(uint32_t ms) {
// Blocking delay for specified milliseconds using SysTick counter
uint32_t start = msTicks;
while ((msTicks - start) < ms);
}
➡️ Ngắt SysTick mỗi 1ms → msTicks tăng chính xác → delay ổn định. Giá trị 71999 được sử dụng trong SYSTICK->LOAD để cấu hình SysTick timer tạo ra ngắt mỗi 1ms khi hệ thống chạy ở tần số 72MHz.
Tại sao cần dùng từ khóa volatile?
Trong C, từ khóa volatile được dùng để thông báo cho trình biên dịch rằng giá trị của biến có thể thay đổi bất ngờ, ngoài luồng thực thi thông thường.
Ngăn tối ưu hóa không mong muốn
Trình biên dịch có thể:
- Lưu biến vào register
- Bỏ qua việc đọc lại bộ nhớ nếu nghĩ rằng biến không đổi
Khi khai báo volatile, trình biên dịch (compiler) sẽ không giả định giá trị của biến là cố định và sẽ luôn đọc/ghi trực tiếp vào bộ nhớ. Điều này đảm bảo rằng mọi thay đổi đối với biến được phản ánh chính xác.
Các trường hợp cần dùng volatile
- Biến bị thay đổi bởi phần cứng: Ví dụ, một thanh ghi phần cứng (như
GPIOC->IDR) có thể thay đổi do sự kiện bên ngoài (như nhấn nút). - Biến dùng trong ngắt (interrupt): Một biến như msTicks, được tăng trong hàm xử lý ngắt (SysTick_Handler), có thể thay đổi bất kỳ lúc nào, ngay cả khi mã chính (main) không trực tiếp sửa đổi nó.
- Biến dùng trong đa luồng (multithreading): Trong các hệ thống đa luồng, một biến có thể được thay đổi bởi luồng khác.
Vì sao msTicks cần volatile?
msTicksđược tăng trong ngắt SysTick_Handler (xử lý ngắt SysTick, được gọi mỗi 1ms)main()vàDelay_ms()đọc giá trị msTicks- Nếu không có
volatile, compiler có thể không đọc lại msTicks từ RAM. - Ví dụ, nó có thể lưu giá trị msTicks vào một thanh ghi và không đọc lại từ bộ nhớ, dẫn đến việc bỏ qua các cập nhật từ ngắt.
- Điều này làm delay sai thời gian
➡️ Khai báo volatile đảm bảo msTicks luôn phản ánh đúng giá trị thực tế
Chương trình main điều khiển LED PA8 bật/tắt mỗi 15 giây
int main(void) {
SystemClock_Config(); // Khởi tạo System Clock, ví dụ 72MHz
GPIO_LED_Config(); // Cấu hình chân GPIO cho LED (ví dụ PA8)
SysTick_Config_1ms(); // Cấu hình SysTick tạo ngắt 1ms
while (1) {
GPIOA->BSRR.BR8 = 1; // LED on
Delay_ms(15000); // Delay 15 giây (15000 ms)
GPIOA->BSRR.BS8 = 1; // LED off
Delay_ms(15000);
}
}
Chương trình sử dụng SysTick timer 1ms để tạo delay thời gian bật/tắt LED (PA8) mỗi 15 giây. Lưu ý rằng trong code gốc có sự nhầm lẫn giữa GPIOA->BSRR.BR8 = 1; và GPIOA->BSRR.BS8 = 1;. Để bật bit 8 (BS8) và tắt bit 8 (BR8) bằng thanh ghi BSRR, chúng ta cần đặt các bit tương ứng. BSx là bit x để set, BRx là bit x+16 để reset. Do đó, đã điều chỉnh thành GPIOA->BSRR = (1U << 8); (set bit) và GPIOA->BSRR = (1U << (8 + 16)); (reset bit).
6. Cách kiểm tra cấu hình SysTick Timer STM32F103 (Debugging)
Chương trình kiểm tra độ chính xác SysTick Timer (2 phút)
// Hàm kiểm tra nút nhấn đơn giản (từ bài gốc, có thể cần điều chỉnh phù hợp với GPIO)
void SimpleDelayDebounce(GPIO_TypeDef* port, uint32_t pin, uint32_t* buttonState, uint32_t active)
{
static uint32_t lastKeyState = 0;
uint32_t ButtonState = (((GPIOC->IDR.reg >> pin) & 1U) == active) ? 1: 0;
*buttonState = 0;
if ((ButtonState) & (lastKeyState == 0)) // Detect edge
{
Delay_Approx(30); // Debounce delay 0.03s
if (((port->IDR.reg >> pin) & 1U) == active) // Check the button still pressed after delay
{
*buttonState = ButtonState;
}
}
lastKeyState = ButtonState;
}
uint32_t checkSysTickTimer1s = 0;
int main(void)
{
SystemClock_Config(); // Initialize system clock
GPIO_LED_Config(); // Cấu hình GPIO cho LED
GPIO_Button_Config(); // Cấu hình GPIO cho nút nhấn (ví dụ PC1)
SysTick_Config_1ms(); // Cấu hình SysTick 1ms
uint32_t timeStart = msTicks;
uint32_t timeStart_toggle = msTicks;
while (1) {
uint32_t ButtonState = 0;
SimpleDelayDebounce(GPIOC,1,&ButtonState, 0);
if (ButtonState)
{
LED_Toggle();
checkSysTickTimer1s = 0;
timeStart = msTicks;
}
// Check systick timer 1second
if (msTicks - timeStart >= 1000)
{
checkSysTickTimer1s++;
timeStart = msTicks; // Cập nhật thời gian bắt đầu cho lần đếm 1 giây tiếp theo
}
}
}
Link Github: Download chương trình cấu hình Systick timer 1ms STM32F103
Kết quả kiểm tra thực tế
Chương trình reset checkSysTickTimer1s bằng 0 và cập nhật giá trị timeStart mỗi khi nút nhấn (PC1) được nhấn. Kiểm tra bằng cách so sánh thời gian hệ thống Windows hoặc thời gian bấm giờ tại thời điểm nút nhấn được nhấn. Cách này chỉ kiểm tra tương đối thời gian SysTick.

Thời gian hệ thống và thời gian SysTick timer đếm tại thời điểm 60 giây.

Thời gian SysTick timer đếm mỗi giây sau 2 phút là 120 và thời gian hệ thống Windows là 4:42:00 – 4:40:00, đúng 2 phút. Như vậy thời gian SysTick đếm gần chính xác. ➡️ Cấu hình SysTick timer STM32F103 đếm thời gian gần như chính xác tuyệt đối.
7. Kết luận
Cấu hình SysTick Timer STM32F103 ở mức thanh ghi là một kỹ năng nền tảng quan trọng trong lập trình STM32F103 thanh ghi, cho phép tạo ra các hàm delay và quản lý thời gian chính xác, hiệu quả hơn hẳn so với delay vòng lặp.
- SysTick timer là giải pháp chuẩn để tạo delay chính xác và quản lý thời gian trong các ứng dụng bare-metal.
- Việc sử dụng từ khóa
volatilelà bắt buộc khi làm việc với các biến được chia sẻ giữa hàm ngắt và hàm chính để tránh các vấn đề tối ưu hóa của trình biên dịch.
SysTick đặc biệt phù hợp cho các tác vụ:
- Delay không blocking, giúp CPU có thể thực hiện các tác vụ khác.
- Triển khai scheduler đơn giản cho hệ điều hành thời gian thực.
- Đo thời gian chính xác cho các sự kiện.
Nắm vững SysTick là nền tảng vững chắc cho việc phát triển các ứng dụng nhúng phức tạp hơn như FSM (Finite State Machine), debounce nút nhấn, non-blocking delay hay xây dựng một RTOS cơ bản.
Tham khảo mã nguồn đầy đủ của chương trình tại đây.
