CEtut – Part 6: Timer – Timer Mode

Bài viết này sẽ giới thiệu cách sử dụng bộ định thời (Timer) trên vi điều khiển STM32 để thực hiện các tác vụ điều khiển thời gian thực. Cùng với đó, chúng ta sẽ triển khai một sample về timer và ứng dụng kỹ thuật lập trình interrupt mô phỏng đèn giao thông và có thể hiện lên màn hình thời gian chờ hiện tại một cách chính xác
Share

Bài viết này sẽ giới thiệu cách sử dụng bộ định thời (Timer) trên vi điều khiển STM32 để thực hiện các tác vụ điều khiển thời gian thực. Cùng với đó, chúng ta sẽ triển khai một sample về timer và ứng dụng kỹ thuật lập trình interrupt mô phỏng đèn giao thông và có thể hiện lên màn hình thời gian chờ hiện tại một cách chính xác

Timer là gì?

Dịch từ tiếng anh thì timer có nghĩa là đếm giờ vậy thì ở đây trong một con vi điều khiển thì timer nó chính là một bộ đếm. Theo các bạn thì bộ đếm này có chức năng là gì? và tại sao phải đếm như vậy.

Như các bạn đã được học thì chắc hẳn ai cũng sẽ biết xung CLK rồi và tất cả mọi thứ cũng đều phải thực hiện dựa trên xung CLK và bộ timer này cũng không ngoại lệ. Có thể hiểu một cách đơn giản timer là mạch digital logic có vai trò đếm mỗi chu kỳ clock (đếm lên hoặc đếm xuống) cho tới khi giá trị đó đạt ngưỡng xác định thì xảy ra sự kiện tràn.

Trong một hệ thống, timer có nhiều vai trò và ứng dụng như: đo các khoảng thời gian, điều khiển động cơ, đếm thời gian, đếm sản phẩm, đồng bộ hệ thống…

1. STM32f103C8T6 có những timer nào?

STM32F1xx, cụ thể như STM32f103C8T6 có tổng cộng 14 timer (TIM1 đến TIM14)

  • TIM1 và TIM8: Advanced-control timers
  • TIM2 đến TIM5: General Purpose
  • TIM9 đến TIM14: General Purpose
  • TIM6 và TIM7: Basic Timer

2. Các thành phần trong timer

a. Time-base unit (Khối cơ sở của bộ Timer)

Thành phần chính của timer chính là bộ đếm – counter (CNT), với các ngưỡng trên được thiết lập bởi thanh ghi Auto Reload (ARR). Counter có thể đếm lên lên hoặc đếm xuống. Clock đưa vào bộ đếm có thể được chia bởi một bộ chia tần – Prescaler. Người dùng có thể thực hiện các lệnh đọc, ghi vào các thanh ghi CNT, ARR và PSC để cấu hình cho khối cơ sở của mỗi bộ Timer.

Có bốn thanh ghi các bạn cần để ý:

– Counter Register (TIMx_CNT):

Khi hoạt động, thanh ghi này tăng hoặc giảm giá trị theo mỗi xung clock đầu vào. Tùy vào bộ timer mà counter này có thể là 16bit hoặc 32bit.

– Prescaler Register (TIMx_PSC):

Giá trị của thanh ghi bộ chia tần (16bit) cho phép người dùng cấu hình chia tần số đầu vào (CK_PSC) cho bất kì giá trị nào từ [0- 65535]. Sử dụng kết hợp bộ chia tần của timer và của RCC giúp chúng ta có thể thay đổi được thời gian của mỗi lần CNT thực hiện đếm, giúp tạo ra được những khoảng thời gian, điều chế được độ rộng xung phù hợp với nhu cầu.

– Auto-Reload Register (TIMx_ARR):

Giá trị của ARR được người dùng xác định sẵn khi cài đặt bộ timer, làm cơ sở cho CNT thực hiện nạp lại giá trị đếm mỗi khi tràn (overflow khi đếm lên – CNT vượt giá trị ARR, underflow khi đếm xuống – CNT bé hơn 0). Tùy vào bộ timer mà counter này có thể là 16bit hoặc 32bit.

– Capture / Compare Register (TIMx_CCRx):

Được sử dụng trong các chế độ của timer, cụ thể là Input capture, Output Compare và PWM. Tuỳ thuộc vào chế độ mà chức năng của thanh ghi sẽ được thay đổi.

b. Những kênh chức năng trong timer

Mỗi bộ timer có 4 kênh độc lập, mỗi kênh này phối hợp Time-base unit có thể tạo ra các tính năng sau:

– Input caputer:

Ở chế độ Input capture, thanh ghi CCR của kênh đầu vào tương ứng sẽ được sử dụng để lưu giá trị của CNT khi phát hiện sự thay đổi mức logic (sườn lên/ sườn xuống) như được cấu hình trước đó. Từ đó có thể biết được khoảng thời gian giữa 2 lần có sườn lên hoặc sườn xuống.

– Output compare:

Chế độ này thường được sử dụng để điều khiển đầu ra của 1 I/O PIN khi timer đạt được một chu kỳ thời gian, cũng chính là khi giá trị CNT đếm tới giá trị bằng với giá trị thanh ghi capture/campare (đã được nạp sẵn). Người dùng có thể cài đặt I/O Pin tương ứng với các giá trị logic: mức 1, mức 0 hoặc đảo giá trị logic hiện tại. Đồng thời, cờ ngắt được bật lên và yêu cầu ngắt cũng được tạo ra nếu người dùng cấu hình cho phép ngắt.

– PWM generation:

Tính năng điều chế độ rộng xung cho phép tạo ra xung với tần số được xác định bởi giá trị của thanh ghi ARR, và chu kỳ nhiệm vụ (Duty cycle) được xác định bởi giá trị thanh ghi CCR.

3. Những hướng đếm của Timer

Gồm có 3 hướng chính:+ Upcounting mode (chế độ đếm lên): Ở chế độ này, CNT đếm lên từ 0 (hoặc một giá trị nào đó được người dùng ghi vào CNT trước) đến giá trị của thanh ghi ARR , sau đó CNT bắt đầu lại từ 0. Lúc này có sự kiện tràn counter – overflow, sự kiện này có thể tạo yêu cầu ngắt nếu người dùng cấu hình cho phép ngắt. Một ví dụ với ARR = 36:

+ Downcouting mode (chế độ đếm xuống): Ở chế độ này, CNT đếm xuống từ giá trị thanh ghi ARR (hoặc 1 giá trị nào đó do người dùng ghi trực tiếp vào CNT trước) đến 0, sau đó CNT bắt đầu lại từ giá trị ARR, lúc này có sự kiện tràn counter – underflow, sự kiện này có thể tạo yêu cầu ngắt nếu người dùng cấu hình cho phép ngắt. Một ví dụ với ARR = 36:

+ Center-Aligned mode (chế độ đếm lên và xuống): Ở chế độ này, counter sẽ đếm lên từ 0 (hoặc một giá trị nào đó được người dùng ghi vào CNT trước) đến giá trị thanh ghi ARR – 1, lúc này xuất hiện sự kiện tràn counter – overflow, tiếp theo CNT  sẽ đếm xuống từ ARR tới 1, lúc này có sự kiện tràn counter – underflow, sau đó CNT sẽ về giá trị 0 và bắt đầu lại quá trình đếm lên. Một ví dụ với ARR = 06:

4. Những giá trị cần quan tâm

Dưới đây là hình ảnh tổng quát về Timer

Có ba giá trị mà khi thao tác với TIMER chúng ta cần phải lưu ý.

  • System Clock: Tần số hệ thống, ta cần quan tâm timer dùng tần số ban đầu timer bắt nguồn từ đâu
  • Prescaler: Bộ chia tần số sẽ chia System CLock thành tần số nguồn cho bộ Timer
  • Counter Period (giá trị của thanh ghi ARR): Giá trị ngưỡng của bộ Timer để đáp ứng tuỳ vào nhu cầu sử dụng.

5. Công thức tính

Sau khi đã xác định được tần số cấp cho timer tương ứng (system clock) thì sẽ điều chỉnh hai giá trị còn lại là Prescaler Counter Period lại sao cho phù hợp dựa trên các công thức bên dưới:

Tần số sau khi qua bộ chia tần Presccaler sẽ là: Fclock = Fsys/(Prescaler+1)

  • Fclock : là tần số mong muốn sau bộ chia.
  • Fsys: clock hệ thống được chia đến bộ timer đó.
  • Prescaler: là giá trị prescaler của timer đó, có thể được tuỳ chỉnh cài đặt

Timer có Prescaler 16-bit nên bộ chia có gia trị từ 0 đến 65355, có rất nhiều độ chia tương ứng, Prescaler bit càng cao thì bộ chia sẽ càng mịn.

Và sau khi có được tần số mình mong muốn sau khi chia, ta sẽ tính được chu kì sự kiện tràn:

  • Teven: Chu kì xảy ra sự kiện
  • CounterPeriod: Giá trị thanh ghi ARR

Hoặc chúng ta có thể dùng công thức rút gọn:

Ví dụ: System CLK = 16Mhz, muốn thiết lập để Timer tạo ra CLK với tần số là 1 giây thì cần:

Prescaler = 1599 và Counter Period = 9999

Lưu ý: Có thể sử dụng bộ số Prescaler và Counter Period khác để tạo Timer 1s Ví dụ: Prescaler = 15999 và Period = 999

6. Phương thức

Có ba phương thức chính khi tương tác với timer và các giao thức khác:

  • Base: phương thức mặc định, không có sử dụng Interrupt và DMA
  • Interupt: kết hợp sử dụng với interupt, ngắt khi sự kiện xảy ra (có sử dụng hàm Callback)
  • DMA (Direct memory Access): kết hợp sử dụng với DMA

Mẹo: Cách gọi hàm và quy tắc viết tên hàm

“Tên thư viện”_“Giao thức”_“chế độ”_“chức năng”_(“phương thức” nếu có)

VD: HAL_TIM_Base_Start_IT

Timer mode (Base)

1. Tổng quan

Đây là chế độ cơ bản nhất của stm32, chức năng chính dùng để tạo delay tương ứng với xung clock mới phù hợp với các yêu cầu.

Ở chế độ này Timer sẽ liên tục đếm cho đến khi giá trị ở thanh ghi CNT bằng giá trị của thanh ghi ARR (giá trị ngưỡng) sẽ đổi trạng thái của xung từ LOW → HIGH hoặc ngược lại và trong chế độ này chúng ta sẽ không sử dùng giá trị của thanh ghi CCR.

Vậy tại sao không sử dụng hàm HAL_delay để tạo độ trễ mà cần sử dụng Timer? Chúng khác nhau như thế nào?

  • Độ chính xác: Delay sử dụng Timer thường có độ chính xác cao hơn so với HAL_Delay do HAL_delay sử dụng hệ thống bộ đếm thời gian (system tick timer)
  • Tối ưu tài nguyên: Khi timer đếm thời gian trên chu kỳ xung clock, hệ thống có thể thực thi những lệnh khác (đa tác vụ) hoặc vào chế độ ngủ (sleep mode). Ngược lại, khi sử dụng HAL_delay, CPU phải hoạt động để chờ đợi cho đến khi độ trễ kết thúc (có thể nói CPU bị “treo”)

2. Các hàm và phương thức

Đầu tiên cần phải khai báo Timer (sau khi generate code đã có sẵn)

Cấu hình Timer:

  • Hàm: MX_TIMx_Base_Init() với x là số timer sử dụng

Nếu sử dụng chế độ Interupt của Timer thì cần phải sử dụng thêm function callback Handler.

Ngắt Timer (interrupt) Handler:

  • Hàm:void HAL_TIM_PeriodElapsedCallback
  • Tham số:TIM_HandleTypeDef *htim (Con trỏ đến biến cấu trúc của Timer).

Về phương thức trong Timer mode, ta có 3 phương thức chính:

  • Timer base:
    • Hàm: HAL_TIM_Base_Start
    • Tham số:TIM_HandleTypeDef *htim (Con trỏ đến biến cấu trúc của Timer)
  • Timer Interupt:
    • Hàm: HAL_TIM_Base_Start_IT
    • Tham số: TIM_HandleTypeDef *htim (Con trỏ đến biến cấu trúc của Timer)
  • Timer DMA:
    • Hàm: HAL_TIM_DMA_Start
    • Tham số:
      • TIM_HandleTypeDef *htim: Con trỏ đến biến cấu trúc của Timer.
      • uint32_t Channel: Kênh DMA được sử dụng (vd: TIM_DMA_UPDATE).

3. Thực hành

a. Linh kiện

b. Cấu hình

Cấu hình Hercules

Sủ dụng baud rate = 9600

Cấu hình STM32

Bước 1: Chọn Serial Wire để nạp code

Bước 2: Click chuột trái lần lượt vào các chân PB13, PB14 và PB15 tích vào ô GPIO_Output

Bước 3: Đặt tên label cho các chân ở mục System Core → chọn GPIO → đặt tên ở mục user label

Bước 4: Vào mục Timers → chọn TIM3 để đổi các chế độ đèn giao thông mỗi 5 giây

Chọn Internal Clock để sử dụng thạch anh nội (tần số 8 MHz)

Tạo tần số 1000Hz (chu kì 1 ms):

8*10^6/(Prescaler+1) = 1000 Hz ⇒ Prescaler = 7999

Khi đó thanh ghi Counter sẽ đếm 1 giá trị tương ứng 1 ms

Vậy 5 giây cần đếm đến 999 (0 → 4999)

Với Prescaler = 7999 và Period = 4999, ta sẽ có ngắt Timer = 5s

Bước 5: Click vào mục Timers → chọn TIM2 được dùng đếm ngược thời gian từ 5

Tính toán giá trị như bước 4 ta sẽ được prescaler 7999 và Counter Period 4999

Bước 6: Click vào tab NVIC Setting ở mục System Core, click chọn Enabled cho TIM2 và TIM3 Interrupt, kích hoạt ngắt cho Timer để khi sự kiên tràn xảy ra thì sẽ nhảy vô hàm callback và thực hiện function mong muốn.

Bước 7: Thiết lập mức độ ưu tiên giữa các ngắt, khi ngắt TIM2 và TIM3 cùng xảy ra, TIM3 sẽ được ưu tiên vào hàm ngắt trước (Ví dụ cụ thể có ở bài external interrupt)

Nhấp vào TIM2 global interrupt chọn Preemption Priority = 1 (mức độ ưu tiên thấp hơn TIM3)

Bước 8: Cấu hình UART, thiết lập baud rate = 9600

Bước 9: Save setting và generate code

c. Code

Bước 1: Tạo biến toàn cục mode, xác định các chế độ LED
Tạo các chuỗi đèn giao thông tương ứng và chuỗi count để đếm số giây hiện tại

Bước 2: Sử dụng hàm callback để thực hiện đèn giao thông

Dùng câu điều kiện if(htim->Instance == TIM3) để xác định cụ thể Timer nào đã gây ra ngắt (ở đây là Timer 3)

Thiết lập giá trị chuỗi count bằng 5 giây để bắt đầu đếm

Dùng lệnh switch case ứng với các mode để hiển thị LED tương ứng

Bước 3: Truyền dữ liệu, hiện thị lên màn hình số giây hiện tại của đèn

Bước 4: Kích hoạt Timer với chức năng interrupt

d. Video demo

Thực hiện bài viết: Nguyễn Thiên Hoàn Phúc