HAL_Delay là một hàm có sẵn trong thư viện HAL của STM32. HAL_Delay hoạt động như thế nào, làm thế nào để tự tạo một hàm Delay, và cách sử dụng chúng trong STM32 ra sao? Tất cả sẽ được đề cập trong bài hôm nay.
Bài này nằm trong Học STM32 từ A tới Z
Delay là gì?
Delay trong tiếng Anh là sự chậm trễ, sự trì hoãn. Trong lập trình cũng tương tự như vậy: khi vi điều khiển làm xong 1 việc, nó sẽ nhảy ngay tới việc tiếp theo chỉ trong vài micro giây (tùy vào tần số hoạt động của MCU).
Để duy trì một trạng thái trong 1 khoảng thời gian, chúng ta dùng delay. Khi đó MCU sẽ không làm gì cả (thực ra là vẫn chạy, nhưng chỉ chạy lòng vòng trong hàm delay), thực hiện xong hàm delay mới thực hiện tiếp lệnh tiếp theo.
Cách sử dụng Systick trong STM32
Systick là gì?
Lõi Cortex-M3 của STM32 có một bộ định thời hệ thống (System Timer, gọi tắt là SysTick) 24 bit, đếm xuống và tự nạp lại. Nghĩa là mỗi khi đếm về 0, nó sẽ tự nạp lại giá trị trong thanh ghi SysTick Reload Value Register (SYST_RVR) rồi tiếp tục đếm xuống.
Mỗi khi bộ đếm của SysTick về 0 sẽ sinh ra ngắt, và hàm xử lý ngắt SysTick_Handler được gọi. Trong hàm này, biến đếm uwTick được tăng lên 1 (theo tần số tick) bởi hàm HAL_IncTick().
Mặc định khi khởi tạo code với CubeMX, ngắt SysTick xảy ra mỗi 1 mili giây. Bạn cứ hình dung uwTick giống như chiếc đồng hồ treo tường của cả chương trình, cứ 1ms lại nhích 1 nấc.


Sử dụng Systick trong HAL_Delay(ms)
Trong hàm HAL_Delay, khi bắt đầu vào hàm, giá trị tick ban đầu sẽ được đọc bằng hàm HAL_GetTick(). Trong vòng while sẽ so sánh giá trị tick tại thời điểm đó với thời điểm ban đầu, nếu hiệu số lớn hơn hoặc bằng giá trị delay thì thoát vòng lặp và kết thúc hàm delay.

Khi sử dụng hàm HAL_Delay, code của chúng ta chạy ở chế độ blocking, nghĩa là CPU không làm gì khác trong suốt thời gian delay (thực tế là nó chỉ đi kiểm tra điều kiện while đó). Khi HAL_Delay xong thì mới tiếp tục làm những việc khác.
Ưu điểm của phương pháp này: thời gian delay khá chính xác (sai số cỡ 1ms, vì HAL_Delay luôn cộng thêm 1 tick để đảm bảo chờ đủ tối thiểu thời gian yêu cầu).
Nhược điểm: vì code ở chế độ Blocking nên có thể làm chậm các tác vụ khác, ví dụ quét màn hình, hiển thị LED 7 đoạn…
Sử dụng HAL_GetTick() code delay chế độ non-Blocking
Phương pháp này cũng gần giống như HAL_Delay, chỉ khác là chúng ta sẽ sử dụng if thay cho while. Cấu trúc như thế này:
uint32_t last_time = HAL_GetTick();
while(1)
{
uint32_t current_time = HAL_GetTick();
if(current_time - last_time >= delay)
{
HAL_GPIO_TogglePin(GPIOA,GPIO_PIN_0);
last_time = current_time;
}
//do something
}
Giải thích:
Chúng ta tạo ra 2 biến: lưu giá trị tick lần cuối thực hiện (hay thời điểm bắt đầu delay) và giá trị tick hiện tại.
Mỗi lần vòng lặp chạy qua sẽ cập nhật giá trị current_time. Nếu giá trị hiện tại – giá trị bắt đầu >= thời gian delay, chương trình sẽ đảo trạng thái chân PA0, sau đó gán giá trị bắt đầu = giá trị hiện tại.
Lưu ý: Biến lưu tick phải là kiểu uint32_t (đúng kiểu trả về của HAL_GetTick()) và luôn so sánh bằng phép trừ current_time - last_time. Nhờ phép trừ số không dấu, chương trình vẫn chạy đúng khi tick bị tràn về 0 (sau khoảng 49 ngày). Ngoài ra trong C không được khởi tạo biến static bằng lời gọi hàm như static int last_time = HAL_GetTick();, trình biên dịch sẽ báo lỗi.
Phương pháp này thường được sử dụng trong các việc cần lặp lại liên tục và tuần tự.
Ưu điểm: chương trình vẫn làm được những việc khác mà không phải đứng chờ.
Nhược điểm: thời gian delay có thể bị lệch nếu đoạn code “do something” chạy quá lâu, không kịp quay về kiểm tra biến current_time.
Cả 2 phương pháp dùng SysTick đều chỉ tạo được delay theo mili giây, vậy để tạo delay micro giây chúng ta cần làm như thế nào? Hãy tiếp tục đọc nhé.
Giải thích vì sao không nên dùng HAL_Delay trong ngắt
Giả sử ta có một ngắt A có mức ưu tiên ngang bằng hoặc cao hơn ngắt SysTick, và trong hàm xử lý ngắt của A có gọi HAL_Delay().
Khi đó chương trình sẽ chạy vào HAL_Delay, sau khi biến tickstart lấy được giá trị trả về của hàm HAL_GetTick, chương trình chạy vào while. Tuy nhiên ngắt SysTick lúc này đang phải chờ (do ngắt A chưa chạy xong), nên giá trị trả về của HAL_GetTick không tăng được nữa, dẫn tới treo mãi tại while.
Để khắc phục hiện tượng này, chúng ta có thể giảm mức ưu tiên của ngắt A hoặc tăng mức ưu tiên của ngắt SysTick. Lúc này khi đang ở trong HAL_Delay, ngắt SysTick vẫn chen vào được, và chương trình hoạt động bình thường. Tuy vậy, cách tốt nhất vẫn là không gọi delay trong ngắt, chỉ bật cờ trong ngắt rồi xử lý trong vòng while.
Delay us với Timer trong STM32
Nếu các bạn chưa biết Timer là gì và cách khởi tạo Timer thì vui lòng đọc lại bài: Lập trình STM32 Timer nhé!
Ý tưởng: chúng ta sẽ tạo ra một Timer với mỗi lần đếm (Count Step) là 1us, đếm từ 0 tới 0xFFFF và lặp lại liên tục. Sau đó viết một hàm giống HAL_Delay để tạo delay chế độ blocking. Với non-Blocking thì không nên sử dụng vì chắc chắn sẽ sai thời gian (do mỗi lệnh của vi điều khiển xử lý đã mất vài us rồi).
Khởi tạo Timer trong CubeMX
Clock hệ thống chúng ta sử dụng trong bài này là 72MHz, nguồn thạch anh ngoài, nên sẽ setup RCC là Crystal/Ceramic Resonator.

Tiếp đó chỉnh Clock lên 72MHz.
Timer sử dụng Timer 2, cấu hình như sau:
- Bộ chia (Prescaler) 72-1: do bộ chia luôn được cộng thêm 1 với bất kì số nào được đưa vào. Clock của Timer 2 là 72MHz (APB1 chạy 36MHz nhưng clock cấp cho timer được nhân 2), nên tần số đếm sẽ là 1MHz, tương ứng với 1us mỗi lần đếm.
- Bộ nạp lại ARR để giá trị lớn nhất (0xFFFF, hoặc 0xFFFF – 1 như hình)
- Chọn chế độ đếm là đếm lên

Gen code và mở bằng Keil C.
Lập trình Delay_us với Keil C
Sử dụng HAL_TIM_Base_Start(&htim2); để khởi động Timer 2.
Tạo hàm delay_us như sau:
void delay_us (uint16_t us)
{
__HAL_TIM_SET_COUNTER(&htim2,0); // set the counter value a 0
while (__HAL_TIM_GET_COUNTER(&htim2) < us); // wait for the counter to reach the us input in the parameter
}
Giải thích: mỗi lần gọi delay, chúng ta sẽ xóa counter về 0, sau đó chờ trong while tới khi counter đạt giá trị delay thì mới thoát ra.
Lưu ý: Giá trị truyền vào delay_us() phải nhỏ hơn ARR. Nếu truyền vào giá trị lớn hơn hoặc bằng ARR, counter sẽ quay về 0 trước khi đạt tới giá trị đó và vòng while chạy mãi không thoát. Muốn delay lâu hơn thì gọi delay_us nhiều lần, hoặc dùng HAL_Delay cho phần mili giây.
Thực hành tạo xung với delay_us.
Khởi tạo PC13 là Output. Sau đó trong while như sau:
while (1)
{
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
delay_us(10);
}
Sử dụng Oscilloscope đo chân PC13, chúng ta sẽ thấy xung vuông có mỗi nửa chu kì khoảng 10us (chu kì 20us, tần số khoảng 50kHz, lệch một chút do thời gian thực thi lệnh).
Sử dụng thư viện Delay_Timer
Các bạn download thư viện delay theo hướng dẫn: Download tài liệu lập trình STM32
Add file .c vào project và chỉnh đường dẫn thư viện. Cái này làm nhiều quá rồi nên mình không nhắc lại nữa nhé!
Trong main() chúng ta khởi tạo Timer cho delay bằng hàm DELAY_TIM_Init(&htim2)
Sau đó dùng hàm DELAY_TIM_Us(&htim2, time_delay); để tạo trễ nhé.
Kết
HAL_Delay và delay_us được sử dụng rất phổ biến trong lập trình nhúng trên STM32. Hãy hiểu rõ và sử dụng chúng một cách linh hoạt để tránh việc bị treo hoặc chậm chương trình nhé.
Nếu cảm thấy bài viết này có ích hãy chia sẻ với bạn bè nhé. Đừng quên gia nhập hội những anh em Nghiện lập trình nhé!!

