BÀI 45 – TIMER ENCODER MODE TRÊN STM32F103
Đọc encoder bằng phần cứng Timer → xác định chiều → tính RPM → tính quãng đường
Đây là một bài rất quan trọng nếu mục tiêu của bạn là tiến tới lập trình robot di chuyển chính xác.
Ở các bài trước, chúng ta đã dùng Timer để:
Bài 36 → Timer Interrupt
Bài 39 → Input Capture
Bài 41 → PWM
Bài 42 → Servo
Bài 43 → HC-SR04
Bài 44 → HC-SR04 + State Machine
Bây giờ chúng ta dùng chính Timer cho một nhiệm vụ khác:
Đọc encoder trực tiếp bằng phần cứng Timer mà không cần CPU đếm từng xung.
1. Encoder là gì?
Encoder là cảm biến biến chuyển động quay thành tín hiệu điện.
Ví dụ một encoder có:
A
B
Hai tín hiệu này lệch pha nhau:
A: ──┐ ┌────┐ ┌────
└────┘ └────┘
B: ─────┐ ┌────┐ ┌──
└────┘ └────┘
Hai tín hiệu này gọi là:
Quadrature Encoder
2. Tại sao cần hai tín hiệu A và B?
Nếu chỉ có:
A
ta biết:
có quay
nhưng khó biết:
quay trái hay quay phải
Có hai tín hiệu:
A
B
ta có thể xác định chiều.
3. Nhìn vào hai tín hiệu
Ví dụ chiều 1:
A: ┌───┐ ┌───┐
────┘ └───┘ └──
B: ┌───┐ ┌───┐
──────┘ └───┘ └
Chiều ngược lại:
B: ┌───┐ ┌───┐
────┘ └───┘ └──
A: ┌───┐ ┌───┐
──────┘ └───┘ └
Sự lệch pha giữa A và B cho phép Timer xác định chiều quay.
4. Encoder Mode là gì?
STM32F103 có một chức năng rất hay:
TIMx
├── CH1 ← Encoder A
└── CH2 ← Encoder B
Sau đó Timer tự:
đếm xung
+
xác định chiều
Kết quả nằm trong:
TIMx->CNT
CPU không cần:
if (A) count++;
cho từng xung.
5. Đây là điểm khác biệt rất lớn
Cách thủ công
Encoder
↓
GPIO Interrupt
↓
CPU
↓
đọc A/B
↓
count++
Nếu encoder có rất nhiều xung:
CPU bận rất nhiều
Encoder Mode
Encoder
↓
TIM Encoder Interface
↓
CNT
Phần cứng Timer tự đếm.
CPU chỉ cần:
count = TIM2->CNT;
6. Sơ đồ
Ví dụ sử dụng:
TIM2
với:
PA0 = TIM2_CH1
PA1 = TIM2_CH2
Kết nối:
Encoder A ─────► PA0
Encoder B ─────► PA1
Encoder GND ───► GND
Encoder VCC ────► nguồn phù hợp
7. ⚠️ Chú ý điện áp encoder
Nếu encoder của bạn chạy:
5V
thì không được mặc định đưa tín hiệu 5V vào STM32.
Cần kiểm tra loại output:
Open collector
Push-pull
Line driver
và mức logic thực tế.
Với encoder 5V, nếu output có thể lên 5V, hãy dùng:
level shifting
hoặc mạch phù hợp trước khi đưa vào STM32.
8. Timer nào?
Trong bài học này:
TIM2
và:
PA0 → CH1
PA1 → CH2
rất thuận tiện.
STM32F103
PA0 ───────── Encoder A
│
TIM2_CH1
PA1 ───────── Encoder B
│
TIM2_CH2
9. Encoder Mode có những chế độ nào?
Thanh ghi:
TIM2->SMCR
có phần:
SMS
để chọn Slave Mode.
Encoder Interface có các mode chính:
SMS = 001
SMS = 010
SMS = 011
Trong thực tế ta thường dùng:
Encoder Mode 1
Encoder Mode 2
Encoder Mode 3
10. Encoder Mode 3
Đây là mode rất hay:
TI1 + TI2
Timer có thể đếm trên cả hai tín hiệu.
A ──► CH1
B ──► CH2
Khi đó độ phân giải tăng.
11. Ví dụ encoder 20 PPR
Giả sử:
20 pulse/revolution
Nếu dùng:
x1
thì:
20 count/rev
Nếu x2:
40 count/rev
Nếu x4:
80 count/rev
Đây là lý do quadrature encoder rất hữu ích.
12. PPR và CPR rất dễ gây nhầm
Bạn cần phân biệt:
PPR
CPR
Counts
Một encoder ghi:
100 PPR
không phải lúc nào cũng có nghĩa:
100 count/revolution
Nếu đọc quadrature x4:
100 × 4
=
400 counts/revolution
Vì vậy khi tính RPM, phải biết chính xác nhà sản xuất định nghĩa PPR/CPR như thế nào.
13. Cấu hình GPIO
Encoder A/B là input.
Ví dụ dùng Pull-up:
void Encoder_GPIO_Init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
/*
PA0 = TIM2_CH1
Input Pull-up
*/
GPIOA->CRL &= ~(0xF << 0);
GPIOA->CRL |= (0x8 << 0);
GPIOA->ODR |= GPIO_ODR_ODR0;
/*
PA1 = TIM2_CH2
Input Pull-up
*/
GPIOA->CRL &= ~(0xF << 4);
GPIOA->CRL |= (0x8 << 4);
GPIOA->ODR |= GPIO_ODR_ODR1;
}
Ở đây:
CNF = 10
MODE = 00
tức:
Input Pull-up / Pull-down
và:
ODR = 1
chọn:
Pull-up
14. Cấu hình TIM2
Đầu tiên bật clock:
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
Reset Timer:
TIM2->CR1 = 0;
TIM2->CR2 = 0;
TIM2->SMCR = 0;
TIM2->CCMR1 = 0;
TIM2->CCER = 0;
15. Cấu hình CH1 và CH2
Cả hai channel phải ở Input.
TIM2->CCMR1 |= TIM_CCMR1_CC1S_0;
TIM2->CCMR1 |= TIM_CCMR1_CC2S_0;
Tức:
CH1 → TI1
CH2 → TI2
16. Enable Capture channels
TIM2->CCER |= TIM_CCER_CC1E;
TIM2->CCER |= TIM_CCER_CC2E;
17. Chọn Encoder Mode
Ví dụ:
TIM2->SMCR |= TIM_SMCR_SMS_0;
TIM2->SMCR |= TIM_SMCR_SMS_1;
Ta có:
SMS = 011
→ Encoder Mode 3.
18. ARR
Để Counter chạy 16-bit:
TIM2->ARR = 0xFFFF;
Timer:
0
1
2
...
65535
0
1
...
19. Counter bắt đầu từ 0
TIM2->CNT = 0;
Sau đó:
TIM2->CR1 |= TIM_CR1_CEN;
Timer bắt đầu đếm encoder.
20. Hàm Init hoàn chỉnh
void Encoder_TIM2_Init(void)
{
/*
Clock GPIOA
*/
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
/*
Clock TIM2
*/
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
/*
PA0 = CH1
PA1 = CH2
Input Pull-up
*/
GPIOA->CRL &= ~(0xFF);
GPIOA->CRL |=
(0x8 << 0) |
(0x8 << 4);
GPIOA->ODR |=
GPIO_ODR_ODR0 |
GPIO_ODR_ODR1;
/*
Reset Timer
*/
TIM2->CR1 = 0;
TIM2->SMCR = 0;
TIM2->CCMR1 = 0;
TIM2->CCER = 0;
/*
CH1 = TI1
CH2 = TI2
*/
TIM2->CCMR1 |= TIM_CCMR1_CC1S_0;
TIM2->CCMR1 |= TIM_CCMR1_CC2S_0;
/*
Enable CH1 + CH2
*/
TIM2->CCER |=
TIM_CCER_CC1E |
TIM_CCER_CC2E;
/*
Encoder Mode 3
SMS = 011
*/
TIM2->SMCR |=
TIM_SMCR_SMS_0 |
TIM_SMCR_SMS_1;
/*
Counter range
*/
TIM2->ARR = 0xFFFF;
/*
Start from zero
*/
TIM2->CNT = 0;
/*
Start Timer
*/
TIM2->CR1 |= TIM_CR1_CEN;
}
21. Đọc encoder
Bây giờ chỉ cần:
uint16_t Encoder_GetCount(void)
{
return TIM2->CNT;
}
Ví dụ:
uint16_t count;
count = Encoder_GetCount();
22. Điều kỳ diệu nằm ở đây
Bạn không viết:
if (A)
{
count++;
}
Không cần:
attachInterrupt()
Không cần ISR cho từng xung.
Phần cứng làm:
A/B
↓
TIM2
↓
CNT
23. Khi quay thuận
Ví dụ:
CNT:
0
1
2
3
4
5
...
24. Khi quay ngược
Counter có thể:
100
99
98
97
96
...
Tức Timer tự biết chiều dựa trên quan hệ pha A/B.
25. Đây là Encoder Mode thực sự
Bạn có thể kiểm tra:
int16_t count =
(int16_t)TIM2->CNT;
Ví dụ:
+500
→ quay một chiều.
-500
→ chiều ngược lại.
26. Tại sao dùng int16_t?
TIM2->CNT là:
16-bit unsigned
nhưng khi dùng encoder, ta thường xem Counter như:
signed position
nên:
int16_t
rất tiện.
Ví dụ:
0x0005 → +5
0xFFFB → -5
Đây chính là two’s complement mà bạn đã học trong phần C và biểu diễn số.
27. Overflow không phải lỗi
Giả sử:
CNT = 65535
thêm một count:
CNT = 0
Nếu nhìn dưới dạng int16_t:
32767
→
-32768
Điều này có thể rất hữu ích khi tính delta count.
28. Đo vị trí bằng Encoder
Giả sử:
counts_per_rev = 400
Counter:
CNT = 100
thì góc:
[
Angle =
\frac{100}{400}\times360
]
[
Angle=90^\circ
]
29. Công thức tổng quát
Angle =
Count × 360 / CountsPerRevolution
Ví dụ:
float angle =
count * 360.0f /
COUNTS_PER_REV;
30. Đo RPM
Bây giờ đến phần rất quan trọng.
RPM:
Revolutions Per Minute – số vòng/phút.
Giả sử trong khoảng thời gian:
100 ms
ta đếm được:
40 counts
Encoder:
400 counts/rev
31. Tính số vòng
40 / 400
=
0.1 revolution
Trong:
0.1 second
32. Đổi sang RPM
0.1 vòng trong 0.1 s.
Trong 1 giây:
1 rev/s
Trong 1 phút:
60 rev/min
→
60 RPM
33. Công thức RPM
Nếu:
Δcount = số count thay đổi
Δt = thời gian đo tính bằng giây
CPR = counts/revolution
thì:
[
RPM=
\frac{\Delta count}{CPR}
\times
\frac{60}{\Delta t}
]
34. Ví dụ
Δcount = 40
CPR = 400
Δt = 0.1 s
[
RPM=
\frac{40}{400}
\times
\frac{60}{0.1}
]
[
RPM=60
]
35. Không nên đo RPM bằng cách đọc CNT một lần
Sai:
rpm = TIM2->CNT;
CNT chỉ cho biết:
vị trí tương đối
Muốn biết tốc độ phải biết:
count thay đổi bao nhiêu
trong bao lâu
36. Dùng SysTick 1 ms
Bạn đã có hệ thống:
SysTick = 1 ms
Ta có thể lấy mẫu mỗi:
100 ms
Ví dụ:
if (millis - last_measure >= 100)
{
...
}
37. Thuật toán đo RPM
t0
│
├── đọc CNT = count0
│
│ 100 ms
│
├── đọc CNT = count1
│
└── delta = count1 - count0
Sau đó:
delta
↓
CPR
↓
RPM
38. Code đo RPM
int16_t Encoder_GetDelta(void)
{
static int16_t last_count = 0;
int16_t current_count =
(int16_t)TIM2->CNT;
int16_t delta =
current_count - last_count;
last_count =
current_count;
return delta;
}
Sau mỗi:
100 ms
ta gọi:
delta = Encoder_GetDelta();
39. Tính RPM
float Encoder_GetRPM(
int16_t delta,
float sample_time_s,
uint16_t counts_per_rev)
{
return
((float)delta /
counts_per_rev)
*
(60.0f /
sample_time_s);
}
Ví dụ:
rpm =
Encoder_GetRPM(
delta,
0.1f,
400
);
40. Nhưng hãy cẩn thận với kiểu dữ liệu
Nếu:
delta < 0
thì:
RPM < 0
Ví dụ:
+60 RPM
→ thuận.
-60 RPM
→ ngược.
Đây là cách rất thuận tiện để đưa thông tin encoder vào điều khiển motor.
41. Đo quãng đường
Nếu encoder gắn vào bánh xe, ta có thể tính quãng đường.
Giả sử bánh xe:
đường kính = 65 mm
Chu vi:
[
C=\pi D
]
[
C\approx204.2mm
]
42. Với 400 counts/rev
Mỗi count tương ứng:
[
204.2/400
]
≈
0.51 mm
43. Nếu đếm được 4000 counts
Số vòng:
[
4000/400=10
]
Quãng đường:
[
10\times204.2
]
≈
2042 mm
≈
2.04 m
44. Công thức quãng đường
Distance =
Count / CPR × Wheel Circumference
hoặc:
[
Distance=
\frac{Count}{CPR}
\times
\pi D
]
45. Đây là nền tảng odometry
Khi robot có:
Encoder bánh trái
Encoder bánh phải
ta có:
Left Count
Right Count
từ đó có thể tính:
Left Distance
Right Distance
và suy ra:
Robot đã đi bao xa
Robot quay bao nhiêu
Robot đang ở đâu
Đây là odometry.
46. Robot hai bánh
Ví dụ:
FRONT
↑
┌───────────────┐
│ │
(L) (R)
│ │
└───────────────┘
Có:
Encoder Left
Encoder Right
Nếu:
Left = Right
→ đi thẳng.
Nếu:
Left > Right
→ robot cong về một phía.
Nếu:
Left < Right
→ cong về phía còn lại.
47. Đây là lý do encoder quan trọng hơn chỉ PWM
PWM nói:
Motor chạy 60%
nhưng không đảm bảo:
tốc độ thực = 60%
Vì:
tải
ma sát
điện áp
pin
mặt sàn
thay đổi.
Encoder cho biết:
Motor thực sự đang quay nhanh bao nhiêu.
48. Từ đó ta có Closed-loop Control
Không encoder:
PWM
↓
Motor
Đây là:
Open Loop
Có encoder:
┌──────────────┐
│ │
PWM ───► Motor ───► Encoder
▲ │
│ │
└── Controller ◄┘
Đây là:
Closed-loop control
49. Ví dụ điều khiển tốc độ
Muốn:
Target = 100 RPM
Đo được:
Actual = 80 RPM
Sai số:
[
Error=100-80=20
]
Controller tăng PWM.
Nếu:
Actual = 110 RPM
thì:
[
Error=100-110=-10
]
Controller giảm PWM.
Đây chính là nền tảng của:
PID
mà chúng ta có thể học sau này.
50. Bài tập thực hành cực kỳ quan trọng
Nếu bạn có encoder, hãy thử:
Bước 1
Để bánh xe đứng yên.
CNT = 0
Bước 2
Quay bánh bằng tay một vòng.
Xem:
CNT
Bước 3
Quay ngược lại.
Xem:
CNT
Bạn phải thấy:
chiều thuận → CNT tăng
chiều ngược → CNT giảm
51. Bài tập 2
Giả sử encoder:
100 PPR
và Timer Encoder Mode x4.
Hỏi:
Counts/revolution = ?
Đáp án:
[
100\times4=400
]
→ 400 counts/revolution
52. Bài tập 3
Encoder:
400 CPR
Trong:
100 ms
đếm:
80 counts
RPM:
[
RPM=
\frac{80}{400}
\times
\frac{60}{0.1}
]
[
RPM=120
]
53. Bài tập 4
Bánh xe:
D = 70 mm
Encoder:
400 CPR
Robot đi:
2000 counts
Khoảng cách:
[
2000/400=5 vòng
]
Chu vi:
[
\pi\times70
\approx219.9mm
]
Quãng đường:
[
5\times219.9
\approx1099.5mm
]
≈
1.10 m
54. Một vấn đề thực tế: Encoder nhiễu
Encoder cơ khí có thể bị:
bounce
noise
Nếu tín hiệu xấu:
A ──┐ ┌┐ ┌──┐
└─┘└─┘ └
Timer có thể đếm những chuyển đổi không mong muốn.
55. STM32 có Input Filter
Đây là một tính năng rất đáng học.
Trong:
TIM2->CCMR1
có:
IC1F
IC2F
để lọc tín hiệu Input Capture.
Ví dụ:
Encoder
↓
Digital Filter
↓
Timer
Điều này đặc biệt hữu ích với encoder cơ khí.
56. Nhưng không nên bật Filter một cách mù quáng
Filter càng mạnh:
noise ↓
nhưng:
đáp ứng chậm hơn
Nếu encoder tốc độ cao, filter quá lớn có thể làm mất xung.
Vì vậy:
Filter phải được chọn dựa trên tần số encoder thực tế.
57. Một lỗi rất thường gặp
Người mới thường tính:
RPM = CNT × ...
Điều này sai nếu Counter không được reset.
Đúng phải là:
ΔCNT = CNT_now - CNT_old
rồi:
ΔCNT / Δt
58. Kiến trúc driver nên như thế nào?
Sau Bài 44, chúng ta tiếp tục giữ nguyên tư duy module:
Encoder.c
Encoder.h
API:
Encoder_Init();
Encoder_GetCount();
Encoder_GetDelta();
Encoder_GetRPM();
Encoder_GetDistance();
Application không cần biết:
TIM2->SMCR
TIM2->CCMR1
TIM2->CCER
59. Sơ đồ phần mềm
APPLICATION
│
┌────────┴────────┐
│ │
Robot Control Display
│
▼
Encoder Driver
│
▼
TIM2
┌───────┐
│ │
CH1 CH2
│ │
A B
│ │
Encoder
60. Liên hệ với Bài 41 – PWM
Bây giờ chúng ta có hai chiều:
PWM
STM32
↓
TIM PWM
↓
Motor
Encoder
Motor
↓
Encoder
↓
TIM Encoder
↓
STM32
Ghép lại:
┌─────────────┐
│ │
PWM ───────────►│ MOTOR │
│ │
└──────┬──────┘
│
▼
ENCODER
│
▼
TIM2 CNT
│
▼
Controller
│
└────► PWM
Đây chính là vòng điều khiển tốc độ.
61. Một điều cần đặc biệt nhớ về TIM2
Trong bài này:
TIM2
được sử dụng cho:
Encoder Mode
Điều đó có nghĩa nếu bạn đồng thời muốn dùng:
TIM2_CH1 PWM
thì sẽ có xung đột tài nguyên.
Đây là một bài học rất thực tế:
Timer là tài nguyên phần cứng hữu hạn.
Bạn phải lập sơ đồ phân bổ Timer trước khi thiết kế firmware lớn.
62. Ví dụ phân bổ Timer cho robot
Một thiết kế có thể là:
TIM1
├─ PWM Motor Left
└─ PWM Motor Right
TIM2
├─ Encoder Left A
└─ Encoder Left B
TIM3
├─ Encoder Right A
└─ Encoder Right B
TIM4
└─ Servo / auxiliary
SysTick
└─ 1 ms scheduler
Đây là một kiến trúc rất mạnh cho robot hai bánh.
63. Nhưng STM32F103C8T6 còn giới hạn chân
Vì vậy khi thiết kế robot thật, phải tính:
GPIO
Timer
Channel
AFIO
Remap
PWM
Encoder
UART
I2C
SPI
trước khi chốt PCB.
Đây chính là lý do kiến thức:
STM32 Pin Mapping
+
Timer
+
Alternate Function
rất quan trọng.
64. Tóm tắt Bài 45
Bạn cần nhớ:
Encoder A
↓
TIM CH1
Encoder B
↓
TIM CH2
↓
Encoder Mode
↓
CNT
↓
┌──────┼────────┐
▼ ▼ ▼
Count Direction ΔCount
│
▼
RPM
│
▼
Distance
65. Công thức quan trọng nhất
Số vòng:
[
Revolution=
\frac{Count}{CPR}
]
Góc:
[
Angle=
\frac{Count}{CPR}\times360^\circ
]
RPM:
[
RPM=
\frac{\Delta Count}{CPR}
\times
\frac{60}{\Delta t}
]
Quãng đường:
[
Distance=
\frac{Count}{CPR}
\times
\pi D
]
66. Và kiến thức quan trọng nhất
Đừng chỉ nhớ:
TIM2->SMCR = ...
Hãy hiểu bản chất:
Encoder Mode biến Timer thành một bộ đếm quadrature bằng phần cứng.
Thay vì:
xung → CPU → đếm
ta có:
A/B → Timer Hardware → CNT
CPU chỉ đọc kết quả khi cần.
Bài 46 – bước tiếp theo
Sau khi hiểu Encoder Mode, bài tiếp theo rất đáng học là:
BÀI 46 – ĐIỀU KHIỂN TỐC ĐỘ MOTOR DC BẰNG ENCODER + PWM
Ta sẽ ghép trực tiếp:
PWM ───────► Motor
│
▼
Encoder
│
▼
TIM Encoder Mode
│
▼
Đo RPM thực
│
▼
Target RPM
│
▼
Sai số
│
▼
PWM điều chỉnh
và từ đó bắt đầu xây dựng bộ điều khiển tốc độ vòng kín, trước khi tiến tới PID tốc độ cho robot.