BÀI 43 – HC-SR04 BẰNG INPUT CAPTURE TRÊN STM32F103
Đo khoảng cách bằng Timer thay vì delay() và pulseIn()
Đây là bài rất quan trọng vì chúng ta sẽ ghép những gì đã học từ Bài 39 → 42 thành một ứng dụng thực tế:
Bài 39 → Input Capture
Bài 40 → Output Compare
Bài 41 → PWM
Bài 42 → Servo
Bài 43 → HC-SR04
Và sau bài này, bạn sẽ hiểu được cách STM32 đo một khoảng thời gian rất chính xác bằng phần cứng Timer.
1. HC-SR04 hoạt động như thế nào?
HC-SR04 có 4 chân:
┌─────────────┐
│ HC-SR04 │
│ │
│ VCC │
│ TRIG │
│ ECHO │
│ GND │
└─────────────┘
Kết nối:
| HC-SR04 | STM32 |
|---|---|
| VCC | 5V |
| GND | GND |
| TRIG | GPIO Output |
| ECHO | Timer Input Capture |
2. Quy trình đo
STM32 gửi một xung TRIG:
TRIG
┌──────┐
──────┘ └────────
10 µs
HC-SR04 phát sóng siêu âm:
)))))))))) →
Sóng gặp vật:
vật
│
▼
)))))))) → █
█
)))))))) ← █
Sau đó HC-SR04 đưa chân ECHO lên HIGH.
ECHO
┌────────────────┐
──────┘ └──────
←──── thời gian ────→
Thời gian ECHO HIGH chính là thời gian sóng siêu âm đi và quay trở lại.
3. Công thức khoảng cách
Tốc độ âm thanh khoảng:
[
v \approx 343m/s
]
Nếu thời gian đo được là:
[
t
]
thì quãng đường sóng đi được:
[
S=v\times t
]
Nhưng sóng đi:
STM32 → vật
vật → STM32
nên khoảng cách thật:
[
D=\frac{v\times t}{2}
]
4. Công thức HC-SR04 thường dùng
Nếu thời gian t tính bằng microsecond:
[
D(cm)\approx\frac{t(\mu s)}{58}
]
Ví dụ:
ECHO = 580 µs
thì:
[
D\approx10cm
]
5. Tại sao phải chia 2?
Ví dụ vật cách:
10 cm
Sóng phải đi:
10 cm → vật
10 cm ← STM32
Tổng:
20 cm
Do đó thời gian ECHO tương ứng với 20 cm đường đi, không phải 10 cm.
6. Cách đơn giản nhất nhưng chưa tối ưu
Arduino thường có:
pulseIn(ECHO, HIGH);
Nó chờ:
ECHO HIGH
↓
đo thời gian
↓
ECHO LOW
Nhược điểm:
CPU phải chờ
Trong khi STM32 có Timer.
Ta sẽ làm:
ECHO
↓
Timer Input Capture
↓
CCR
↓
tính thời gian
CPU không cần ngồi chờ bằng vòng while.
7. Kiến trúc bài hôm nay
STM32F103
│
┌─────────┴─────────┐
│ │
GPIO TIM
│ │
TRIG Input Capture
│ │
▼ ▼
HC-SR04 ECHO ───────► CCR
│
▼
thời gian
│
▼
khoảng cách
8. Chúng ta sử dụng Timer nào?
Để dễ học, dùng:
TIM2_CH1
với:
PA0
TIM2_CH1:
PA0
sẽ nhận tín hiệu:
HC-SR04 ECHO
9. TRIG dùng chân nào?
Có thể chọn một GPIO output bình thường.
Ví dụ:
PA1 → TRIG
PA0 → ECHO
Nhưng hãy chú ý:
PA0 = TIM2_CH1
nên:
PA0 → ECHO
10. Sơ đồ kết nối
STM32F103
┌─────────────┐
│ │
PA1 ──────┼─────────────┼──── TRIG
│ │
PA0 ◄─────┼─────────────┼──── ECHO
│ │
GND ──────┼─────────────┼──── GND
└─────────────┘
│
│
HC-SR04
VCC HC-SR04:
5V
11. ⚠️ Cực kỳ quan trọng: ECHO là 5V
Đây là vấn đề bạn không được bỏ qua khi dùng HC-SR04 với STM32F103.
HC-SR04 thường cấp:
VCC = 5V
và chân:
ECHO ≈ 5V
Trong khi STM32F103 chạy:
3.3V
Vì vậy:
Không nên đưa trực tiếp ECHO 5V vào chân STM32.
12. Dùng cầu phân áp
Ví dụ:
HC-SR04 ECHO
│
10k
│
├────────► PA0
│
20k
│
GND
Điện áp tại PA0:
[
V_{out}
5V\times\frac{20k}{10k+20k}
]
[
V_{out}\approx3.33V
]
Phù hợp với logic 3.3V.
13. Sơ đồ đầy đủ
HC-SR04
┌─────────┐
5V ─────►│ VCC │
GND ─────►│ GND │
STM32 PA1 ─────►│ TRIG │
│ │
STM32 PA0 ◄─────│ ECHO │
└────┬────┘
│
10k
│
├────────► PA0
│
20k
│
GND
14. Bước 1 – tạo TRIG
HC-SR04 yêu cầu một xung HIGH khoảng:
10 µs
Dạng:
TRIG
┌──────┐
──────┘ └──────
10µs
15. Không cần Timer để tạo TRIG ở bài đầu
Để tập trung vào Input Capture, trước tiên ta có thể tạo TRIG bằng một delay rất ngắn.
Ví dụ:
void HCSR04_Trigger(void)
{
GPIOA->BSRR = GPIO_BSRR_BS1;
delay_us(10);
GPIOA->BRR = GPIO_BRR_BR1;
}
Trong đó:
PA1 = TRIG
16. Nhưng Input Capture mới là phần quan trọng
Ta cấu hình:
PA0
↓
TIM2_CH1
↓
Input Capture
Timer sẽ ghi:
CNT → CCR1
khi ECHO thay đổi trạng thái.
17. Có hai cạnh cần đo
ECHO:
rising
↓
─────────────┐
│
│
│
└─────────────
↑
falling
Ta cần:
Rising edge
↓
start time
Falling edge
↓
end time
Sau đó:
[
t=t_{fall}-t_{rise}
]
18. Cách 1 – đổi cực tính Capture
STM32F103 TIM có thể Capture cạnh:
Rising
hoặc:
Falling
Ta có thể:
Rising → capture
↓
đổi polarity
↓
Falling → capture
Đây là phương pháp dễ hiểu khi mới học.
19. Timer cần chạy với độ phân giải bao nhiêu?
HC-SR04 không cần độ phân giải nano giây.
Chúng ta chọn:
1 tick = 1 µs
rất tiện.
TIM2 clock:
72 MHz
chọn:
PSC = 71;
thì:
[
72MHz/72=1MHz
]
→
1 tick = 1 µs
20. Cấu hình Timer
TIM2->PSC = 71;
Cho:
1 µs/tick
Có thể để:
TIM2->ARR = 0xFFFF;
để Timer có khoảng đo rất rộng.
21. Cấu hình Input Capture CH1
CH1 phải là Input:
TIM2->CCMR1 &= ~TIM_CCMR1_CC1S;
TIM2->CCMR1 |= TIM_CCMR1_CC1S_0;
CC1S = 01:
CC1 mapped to TI1
tức:
PA0 → TIM2_CH1 → Capture
22. Capture cạnh lên
Ban đầu:
TIM2->CCER &= ~TIM_CCER_CC1P;
CC1P = 0:
Rising Edge
Sau đó:
TIM2->CCER |= TIM_CCER_CC1E;
Enable Capture.
23. Đợi Rising
Khi ECHO chuyển:
LOW → HIGH
Timer chụp:
TIM2->CCR1
Ví dụ:
CCR1 = 12500
Ta lưu:
start = TIM2->CCR1;
24. Sau đó đổi sang Falling
TIM2->CCER |= TIM_CCER_CC1P;
Bây giờ:
CC1P = 1
→ Capture Falling Edge.
25. Khi ECHO xuống LOW
Timer lại chụp:
CCR1
Ví dụ:
CCR1 = 13080
Thời gian:
13080 - 12500
= 580 µs
26. Khoảng cách
580 µs
dùng:
[
D=\frac{580}{58}
]
→
10 cm
27. Vấn đề Timer Overflow
Giả sử:
start = 65000
sau đó Timer overflow:
65535 → 0
và:
stop = 500
Nếu tính:
stop - start
với kiểu unsigned phù hợp, phép trừ modulo có thể xử lý overflow.
Ví dụ:
500 - 65000
trong uint16_t:
1036 µs
Đây là một kỹ thuật rất quan trọng khi làm Timer.
28. Nhưng HC-SR04 có khoảng đo hữu hạn
Không cần Timer chạy vô hạn.
Ví dụ khoảng cách:
400 cm
thời gian ECHO khoảng:
[
400\times58=23200µs
]
chỉ khoảng:
23.2 ms
uint16_t với:
1 µs/tick
đủ để đo tới khoảng:
65 ms
29. Code Input Capture
Đây là firmware nền tảng để bạn hiểu nguyên lý:
#include "stm32f10x.h"
volatile uint16_t echo_start;
volatile uint16_t echo_end;
volatile uint16_t echo_time;
volatile uint8_t capture_state = 0;
void GPIO_Init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
/*
PA0 = TIM2_CH1
Input Floating
*/
GPIOA->CRL &= ~(0xF << 0);
GPIOA->CRL |= (0x4 << 0);
/*
PA1 = TRIG
General Purpose Output
Push-Pull
*/
GPIOA->CRL &= ~(0xF << 4);
GPIOA->CRL |= (0x3 << 4);
}
void TIM2_InputCapture_Init(void)
{
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
/*
72 MHz / 72
= 1 MHz
1 tick = 1 us
*/
TIM2->PSC = 71;
TIM2->ARR = 0xFFFF;
/*
CH1 = Input Capture
CC1S = 01
*/
TIM2->CCMR1 &= ~TIM_CCMR1_CC1S;
TIM2->CCMR1 |= TIM_CCMR1_CC1S_0;
/*
Rising edge
*/
TIM2->CCER &= ~TIM_CCER_CC1P;
/*
Enable capture
*/
TIM2->CCER |= TIM_CCER_CC1E;
/*
Enable CC1 interrupt
*/
TIM2->DIER |= TIM_DIER_CC1IE;
/*
Start Timer
*/
TIM2->CR1 |= TIM_CR1_CEN;
/*
Enable NVIC
*/
NVIC_EnableIRQ(TIM2_IRQn);
}
void TIM2_IRQHandler(void)
{
if (TIM2->SR & TIM_SR_CC1IF)
{
/*
Rising edge
*/
if (capture_state == 0)
{
echo_start = TIM2->CCR1;
capture_state = 1;
/*
Switch to falling edge
*/
TIM2->CCER |= TIM_CCER_CC1P;
}
/*
Falling edge
*/
else
{
echo_end = TIM2->CCR1;
echo_time =
echo_end - echo_start;
capture_state = 0;
/*
Switch back to rising
*/
TIM2->CCER &= ~TIM_CCER_CC1P;
}
/*
Clear flag
*/
TIM2->SR &= ~TIM_SR_CC1IF;
}
}
30. Nhưng code trên vẫn thiếu một phần
Chúng ta mới đo:
echo_time
chứ chưa gửi:
TRIG
và chưa tính:
distance
31. Hàm Trigger
Ví dụ:
void HCSR04_Trigger(void)
{
/*
LOW trước
*/
GPIOA->BRR = GPIO_BRR_BR1;
delay_us(2);
/*
HIGH 10 us
*/
GPIOA->BSRR = GPIO_BSRR_BS1;
delay_us(10);
/*
LOW
*/
GPIOA->BRR = GPIO_BRR_BR1;
}
32. Hàm tính khoảng cách
Nếu:
1 tick = 1 µs
thì:
uint16_t HCSR04_GetDistanceCm(void)
{
return echo_time / 58;
}
Ví dụ:
echo_time = 1160
thì:
distance = 1160 / 58
= 20 cm
33. Một vấn đề cần chú ý: echo_time có thể chưa hợp lệ
Khi mới khởi động:
echo_time = 0
Không được hiểu:
0 cm
mà phải hiểu:
chưa có kết quả đo
Vì vậy thực tế nên có:
volatile uint8_t measurement_ready = 0;
34. Sau Falling Edge
Ta đặt:
measurement_ready = 1;
Ví dụ:
else
{
echo_end = TIM2->CCR1;
echo_time = echo_end - echo_start;
capture_state = 0;
TIM2->CCER &= ~TIM_CCER_CC1P;
measurement_ready = 1;
}
35. main() sẽ làm gì?
CPU không cần chờ ECHO.
Ví dụ:
while (1)
{
HCSR04_Trigger();
delay_ms(60);
if (measurement_ready)
{
measurement_ready = 0;
distance =
echo_time / 58;
}
}
Đây đã khác rất nhiều so với:
pulseIn()
36. Tại sao phải chờ khoảng 60 ms?
HC-SR04 cần thời gian để hoàn thành phép đo và tránh việc xung siêu âm của lần trước ảnh hưởng lần sau.
Một chu kỳ đo khoảng:
60 ms
là lựa chọn an toàn khi mới thử nghiệm.
37. Nhưng có một vấn đề sâu hơn
Code trên dùng:
delay_ms(60);
Nó vẫn còn polling/delay ở mức ứng dụng.
Tốt hơn nữa, chúng ta có thể dùng:
Timer
↓
Trigger
↓
Input Capture
↓
Interrupt
↓
measurement_ready
hoàn toàn không cần delay_ms().
Đây sẽ là bước nâng cao sau này.
38. Tại sao Input Capture rất phù hợp với HC-SR04?
Vì HC-SR04 thực chất đang cho chúng ta:
một xung có độ rộng thay đổi
Mà Input Capture sinh ra chính là để:
đo độ rộng xung
Do đó:
HC-SR04 ECHO
↓
Input Capture
↓
CCR
↓
Pulse Width
↓
Distance
39. So sánh với pulseIn()
Arduino pulseIn()
CPU
│
├── chờ HIGH
│
├── bắt đầu đếm
│
├── chờ LOW
│
└── trả về thời gian
STM32 Input Capture
ECHO
│
├──────────────► Timer Hardware
│ │
│ CCR1
│ │
└──────────────► CCR1
│
▼
ISR
│
▼
echo_time
Đây là cách tiếp cận phần cứng thực sự.
40. Một lỗi rất phổ biến
Nhiều người viết:
if (GPIOA->IDR & GPIO_IDR_IDR0)
{
...
}
rồi tự đếm:
while(ECHO);
Đây lại trở thành polling.
Bạn đang học Timer nên hãy tập tư duy:
Để phần cứng Timer đo thời gian, CPU chỉ xử lý kết quả.
41. Tại sao cần Rising và Falling?
Nếu chỉ Capture Rising:
ECHO HIGH
↑
capture
ta biết:
thời điểm bắt đầu
nhưng chưa biết:
thời điểm kết thúc
Nếu chỉ Falling:
ECHO LOW
↑
capture
ta chỉ biết:
thời điểm kết thúc
Muốn tính độ rộng xung:
Pulse Width =
Falling Time - Rising Time
42. Đây chính là kiến thức Bài 39
Bài 39:
Input Capture
cho phép:
đo thời gian giữa các sự kiện
HC-SR04 chỉ là một ứng dụng thực tế của nó.
43. Một cách khác: dùng 2 Channel
Ta cũng có thể thiết kế:
CH1 → Rising
CH2 → Falling
hoặc dùng các kỹ thuật Timer khác.
Nhưng với STM32F103, cách:
CH1
Rising → Falling
dễ hiểu và tiết kiệm Channel hơn.
44. Một vấn đề quan trọng: Timeout
Giả sử HC-SR04:
không nhận được echo
thì:
Falling Edge
có thể không xuất hiện.
Nếu chương trình chỉ chờ:
measurement_ready
thì nó có thể chờ mãi.
Đây là lý do hệ thống thực tế phải có:
Timeout
45. Timeout hoạt động thế nào?
Ví dụ:
Trigger
↓
Rising
↓
chờ Falling
↓
nếu quá 30 ms
↓
Timeout
Khi đó:
distance = INVALID
thay vì treo hệ thống.
46. Đây là tư duy rất quan trọng khi lập trình nhúng
Không bao giờ giả định:
“Thiết bị ngoại vi chắc chắn sẽ trả lời.”
Thực tế:
Sensor lỗi
Cable đứt
Vật quá xa
Nhiễu
Nguồn lỗi
đều có thể xảy ra.
Do đó driver tốt phải có:
Success
Timeout
Error
47. HC-SR04 và robot
Đây chính là ứng dụng rất gần với robot mà bạn đang làm.
Ta có:
STM32
│
┌──────┼──────┐
│ │ │
PWM Input GPIO
│ Capture │
│ │ │
Motor HC-SR04 ...
Sau khi đo:
distance = 30 cm
robot có thể:
if (distance < 30)
{
stop();
turn();
}
48. Nhưng hãy tách Driver và Logic
Không nên viết tất cả vào:
main()
Nên tổ chức:
HCSR04.c
HCSR04.h
Driver làm:
Trigger
Capture
Distance
Timeout
Robot logic làm:
if distance < threshold
turn
Đây chính là cách viết firmware chuyên nghiệp hơn.
49. Kiến trúc phần mềm tốt
Application
│
▼
Robot Control
│
▼
HCSR04 Driver
│
▼
TIM2 Input Capture
│
▼
STM32 Hardware
Sau này bạn có thể thay:
HC-SR04
bằng:
VL53L0X
mà không phải sửa toàn bộ robot logic.
50. Công thức cần nhớ
Nếu Timer:
1 tick = 1 µs
thì:
[
Distance(cm)=\frac{EchoTime(\mu s)}{58}
]
Ví dụ:
580 µs → 10 cm
1160 µs → 20 cm
1740 µs → 30 cm
2320 µs → 40 cm
2900 µs → 50 cm
51. Bảng thực hành
| Khoảng cách | ECHO khoảng |
|---|---|
| 5 cm | 290 µs |
| 10 cm | 580 µs |
| 20 cm | 1160 µs |
| 30 cm | 1740 µs |
| 40 cm | 2320 µs |
| 50 cm | 2900 µs |
| 100 cm | 5800 µs |
| 200 cm | 11600 µs |
| 400 cm | 23200 µs |
Bạn có thể dùng bảng này để kiểm tra oscilloscope/logic analyzer.
52. Bài tập 1
Timer:
TIM2CLK = 72 MHz
PSC = 71
Hỏi:
Timer tick bao nhiêu?
Đáp án:
1 µs
53. Bài tập 2
HC-SR04 cho:
ECHO = 1740 µs
Khoảng cách?
[
1740/58=30cm
]
→ 30 cm
54. Bài tập 3
HC-SR04 cho:
ECHO = 2900 µs
Khoảng cách:
[
2900/58=50cm
]
→ 50 cm
55. Bài tập 4
Nếu vật cách:
100 cm
thời gian ECHO khoảng:
[
100\times58=5800µs
]
→ 5.8 ms
56. Bài tập 5 – tư duy Timer
Giả sử:
Rising CCR1 = 12000
Falling CCR1 = 13740
Hỏi:
Echo time = ?
Ta có:
[
13740-12000=1740
]
→
1740 µs
Khoảng cách:
30 cm
57. Bài tập 6 – Overflow
Giả sử:
start = 65000
stop = 500
Timer 16-bit overflow giữa hai lần Capture.
Nếu dùng:
uint16_t
thì:
uint16_t time = stop - start;
kết quả:
1036 µs
Điều này cho thấy tại sao kiểu dữ liệu unsigned rất hữu ích khi xử lý Timer.
58. Một điểm rất quan trọng về uint16_t
Bạn đã học C ở các bài đầu.
Bây giờ kiến thức đó quay trở lại:
uint16_t
có:
0 → 65535
Khi:
65535 + 1
nó quay về:
0
Đây gọi là:
wrap-around / modulo arithmetic
và Timer cũng hoạt động theo nguyên lý tương tự.
59. Bài 43 kết nối toàn bộ kiến thức Timer
Chúng ta bắt đầu từ:
Bài 36
Timer Interrupt
đến:
Bài 39
Input Capture
rồi:
Bài 40
Output Compare
Bài 41
PWM
Bài 42
Servo
và bây giờ:
Bài 43
HC-SR04
Bạn có thể thấy Timer không phải chỉ là:
“Một bộ đếm.”
Mà nó là một khối xử lý thời gian phần cứng rất mạnh.
60. Sơ đồ tổng hợp
STM32 TIMER
│
┌───────────┼───────────┐
│ │ │
Input Capture Output PWM
│ Compare │
│ │ │
▼ ▼ ▼
HC-SR04 Events Servo/Motor
│
▼
Echo Width
│
▼
Distance
│
▼
Robot Control
61. Một lưu ý về phiên bản STM32
Ở các ví dụ trên, tôi đang viết theo hướng thanh ghi trực tiếp, phù hợp với cách chúng ta đã học từ Bài 16 trở đi:
RCC->APB1ENR
TIM2->PSC
TIM2->ARR
TIM2->CCR1
TIM2->CCMR1
TIM2->CCER
TIM2->DIER
TIM2->SR
Điều này rất tốt để bạn hiểu:
STM32 thực sự làm gì bên trong, thay vì chỉ gọi một hàm thư viện.
Sau khi hiểu chắc, chúng ta có thể chuyển cùng bài này sang:
STM32CubeMX
↓
STM32CubeIDE
↓
HAL_TIM_IC_Start_IT()
và bạn sẽ thấy HAL thực chất chỉ là lớp trừu tượng phía trên những thanh ghi mà chúng ta vừa học.
62. Bạn đã hoàn thành một mốc rất quan trọng
Đến đây chuỗi Timer của bạn đã trở thành:
36 – Timer Interrupt
↓
37 – Timer nâng cao
↓
38 – Timer Architecture
↓
39 – Input Capture
↓
40 – Output Compare
↓
41 – PWM
↓
42 – Servo
↓
43 – HC-SR04
Và kiến thức quan trọng nhất cần giữ lại là:
INPUT CAPTURE
External signal
↓
CCR
↓
đo thời gian
OUTPUT COMPARE
CNT == CCR
↓
Event
PWM
PSC → tốc độ đếm
ARR → chu kỳ
CCR → Duty
HC-SR04
ECHO width
↓
Input Capture
↓
time
↓
distance
63. Bài tiếp theo nên học gì?
Sau Bài 43, tôi khuyên chúng ta không nên nhảy ngay sang một peripheral hoàn toàn mới. Bước hợp lý nhất là đi sâu hơn vào chính HC-SR04:
BÀI 44 – HC-SR04 NÂNG CAO: TIMER + INPUT CAPTURE + TIMEOUT + STATE MACHINE
Ở bài này chúng ta sẽ bỏ kiểu:
delay_ms(60);
và xây một driver thực sự:
IDLE
↓
TRIGGER
↓
WAIT_RISING
↓
WAIT_FALLING
↓
CALCULATE
↓
DONE
↓
TIMEOUT → ERROR
Khi đó HC-SR04 sẽ trở thành một driver non-blocking, rất phù hợp để ghép trực tiếp với robot tránh vật cản của bạn: trong lúc cảm biến đang chờ ECHO, CPU vẫn có thể đồng thời chạy PWM motor, xử lý nút/IR và logic AUTO/MANUAL.