BÀI 44 – HC-SR04 NÂNG CAO
TIMER + INPUT CAPTURE + TIMEOUT + STATE MACHINE TRÊN STM32F103
Ở Bài 43, chúng ta đã làm được:
HC-SR04
↓
ECHO
↓
TIM2 Input Capture
↓
CCR1
↓
Thời gian ECHO
↓
Khoảng cách
Nhưng driver Bài 43 vẫn còn một điểm chưa tốt:
delay_ms(60);
Trong hệ thống thực tế, đặc biệt là robot tránh vật cản, ta không muốn CPU bị chặn bởi delay.
Bài 44 sẽ giải quyết vấn đề đó.
1. Mục tiêu của Bài 44
Sau bài này, driver HC-SR04 sẽ hoạt động như một máy trạng thái – State Machine:
IDLE
│
▼
TRIGGER
│
▼
WAIT_RISING
│
▼
WAIT_FALLING
│
▼
CALCULATE
│
▼
DONE
│
▼
IDLE
Nếu cảm biến không phản hồi:
WAIT_RISING
│
│ timeout
▼
ERROR
hoặc:
WAIT_FALLING
│
│ timeout
▼
ERROR
2. Tại sao phải dùng State Machine?
Hãy tưởng tượng robot đang chạy:
Robot
│
├── Motor PWM
│
├── HC-SR04
│
├── IR Remote
│
├── Encoder
│
└── Logic AUTO/MANUAL
Nếu viết:
HCSR04_Trigger();
delay_ms(60);
distance = HCSR04_Read();
thì trong 60 ms đó chương trình có thể bị chặn.
Trong robot, điều này không tốt.
3. Cách làm chuyên nghiệp hơn
Thay vì:
delay_ms(60);
ta làm:
Trigger
↓
CPU tiếp tục chạy
↓
Timer chờ
↓
Input Capture bắt ECHO
↓
Interrupt
↓
Driver cập nhật trạng thái
CPU không cần đứng chờ.
4. Tư duy quan trọng
Đây là một trong những tư duy quan trọng nhất khi lập trình embedded:
Không phải lúc nào cũng dùng vòng
whileđể chờ phần cứng.
Không tốt:
while(ECHO == 0);
while(ECHO == 1);
Tốt hơn:
Timer + Interrupt + State Machine
5. Kiến trúc mới
HC-SR04
│
│ ECHO
▼
TIM2 Input Capture
│
▼
TIM2 IRQ
│
▼
HCSR04 Driver
│
┌─────┴─────┐
│ │
State Distance
│
▼
Application
6. Hai Timer sẽ làm hai nhiệm vụ khác nhau
Đây là điểm rất quan trọng.
TIM2
Đo ECHO:
TIM2
↓
Input Capture
↓
CCR1
↓
Echo width
Một Timer tick 1 ms
Theo kiến thức Bài 36:
SysTick
↓
1 ms tick
↓
timeout / scheduling
Như vậy:
TIM2
→ đo thời gian chính xác đến µs
SysTick
→ quản lý timeout đến ms
Đây là kiến trúc rất hợp lý.
7. State Machine
Ta định nghĩa:
typedef enum
{
HCSR04_IDLE,
HCSR04_WAIT_RISING,
HCSR04_WAIT_FALLING,
HCSR04_DONE,
HCSR04_TIMEOUT
} HCSR04_State;
8. Ý nghĩa từng trạng thái
IDLE
Không đo.
sẵn sàng
WAIT_RISING
Đã gửi TRIG.
Đang chờ:
ECHO LOW → HIGH
WAIT_FALLING
Đã nhận Rising.
Đang chờ:
ECHO HIGH → LOW
DONE
Đã đo xong.
TIMEOUT
Cảm biến không phản hồi đúng thời gian.
9. Biến driver
Ví dụ:
volatile HCSR04_State hcsr04_state;
volatile uint16_t echo_start;
volatile uint16_t echo_end;
volatile uint16_t echo_time;
volatile uint16_t distance_cm;
volatile uint32_t timeout_counter;
10. Vì sao volatile?
Ví dụ:
volatile uint16_t echo_time;
Biến này có thể được thay đổi trong:
TIM2_IRQHandler()
nhưng được đọc trong:
main()
Compiler phải biết rằng giá trị có thể thay đổi bất kỳ lúc nào.
Do đó:
volatile
rất quan trọng trong chương trình interrupt.
11. State Machine nhìn bằng sơ đồ
┌─────────┐
│ IDLE │
└────┬────┘
│
Trigger
│
▼
┌───────────────────┐
│ WAIT_RISING │
└─────────┬─────────┘
│
Rising Edge
│
▼
┌───────────────────┐
│ WAIT_FALLING │
└─────────┬─────────┘
│
Falling Edge
│
▼
┌───────────────────┐
│ DONE │
└─────────┬─────────┘
│
▼
IDLE
Timeout:
WAIT_RISING ─────timeout────► TIMEOUT
WAIT_FALLING ────timeout────► TIMEOUT
12. Timer TIM2 cấu hình 1 µs
Ta giữ nguyên nguyên lý Bài 43.
STM32F103:
TIM2 clock = 72 MHz
Chọn:
TIM2->PSC = 71;
Ta có:
[
72MHz/(71+1)=1MHz
]
Do đó:
1 tick = 1 µs
13. ARR
TIM2->ARR = 0xFFFF;
Timer:
0
1
2
...
65535
0
1
...
14. Cấu hình PA0
Ta sử dụng:
PA0 = TIM2_CH1 = ECHO
và:
PA1 = TRIG
15. Khởi tạo GPIO
void HCSR04_GPIO_Init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
/*
PA0 = Input
*/
GPIOA->CRL &= ~(0xF << 0);
GPIOA->CRL |= (0x4 << 0);
/*
PA1 = Output Push-Pull 2 MHz
*/
GPIOA->CRL &= ~(0xF << 4);
GPIOA->CRL |= (0x2 << 4);
/*
TRIG LOW
*/
GPIOA->BRR = GPIO_BRR_BR1;
}
16. Khởi tạo TIM2
void HCSR04_TIM2_Init(void)
{
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
TIM2->PSC = 71;
TIM2->ARR = 0xFFFF;
TIM2->CNT = 0;
/*
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;
TIM2->CR1 |= TIM_CR1_CEN;
NVIC_EnableIRQ(TIM2_IRQn);
}
17. Trigger không dùng delay dài
TRIG chỉ cần khoảng:
10 µs
Ở đây ta có thể dùng một hàm delay_us() rất ngắn:
void HCSR04_Trigger(void)
{
GPIOA->BRR = GPIO_BRR_BR1;
delay_us(2);
GPIOA->BSRR = GPIO_BSRR_BS1;
delay_us(10);
GPIOA->BRR = GPIO_BRR_BR1;
}
Điểm quan trọng:
Chỉ có 10 µs delay để tạo xung TRIG; chúng ta không dùng
delay_ms(60)để chờ ECHO nữa.
18. Bắt đầu một phép đo
Ta viết:
void HCSR04_Start(void)
{
if (hcsr04_state != HCSR04_IDLE)
return;
timeout_counter = 0;
echo_start = 0;
echo_end = 0;
echo_time = 0;
/*
Rising edge
*/
TIM2->CCER &= ~TIM_CCER_CC1P;
hcsr04_state = HCSR04_WAIT_RISING;
HCSR04_Trigger();
}
19. Điều gì xảy ra sau HCSR04_Start()?
Trình tự:
HCSR04_Start()
│
▼
state = WAIT_RISING
│
▼
TRIG = HIGH 10 µs
│
▼
TRIG = LOW
│
▼
CPU tiếp tục làm việc
Không có:
delay_ms(60);
20. Khi Rising Edge xuất hiện
TIM2 tự động phát interrupt.
ECHO
LOW
│
│ Rising
▼
HIGH
Trong ISR:
if (hcsr04_state == HCSR04_WAIT_RISING)
{
echo_start = TIM2->CCR1;
TIM2->CCER |= TIM_CCER_CC1P;
hcsr04_state = HCSR04_WAIT_FALLING;
timeout_counter = 0;
}
21. Sau Rising
Bây giờ Timer chờ:
Falling Edge
ECHO
HIGH
│
│
│
│ Falling
▼
LOW
22. Khi Falling Edge xảy ra
else if (hcsr04_state == HCSR04_WAIT_FALLING)
{
echo_end = TIM2->CCR1;
echo_time =
echo_end - echo_start;
distance_cm =
echo_time / 58;
hcsr04_state = HCSR04_DONE;
timeout_counter = 0;
}
23. Công thức lại một lần nữa
Timer:
1 tick = 1 µs
Nếu:
echo_start = 10000
echo_end = 11740
thì:
[
1740µs
]
Khoảng cách:
[
1740/58=30cm
]
24. ISR hoàn chỉnh
void TIM2_IRQHandler(void)
{
if (TIM2->SR & TIM_SR_CC1IF)
{
uint16_t capture =
TIM2->CCR1;
if (hcsr04_state ==
HCSR04_WAIT_RISING)
{
echo_start = capture;
/*
Chuyển sang Falling
*/
TIM2->CCER |= TIM_CCER_CC1P;
hcsr04_state =
HCSR04_WAIT_FALLING;
timeout_counter = 0;
}
else if (hcsr04_state ==
HCSR04_WAIT_FALLING)
{
echo_end = capture;
echo_time =
echo_end - echo_start;
distance_cm =
echo_time / 58;
hcsr04_state =
HCSR04_DONE;
timeout_counter = 0;
/*
Chuẩn bị Rising
*/
TIM2->CCER &=
~TIM_CCER_CC1P;
}
/*
Clear flag
*/
TIM2->SR &=
~TIM_SR_CC1IF;
}
}
25. Nhưng còn Timeout?
Đây mới là phần quan trọng của Bài 44.
Giả sử:
Trigger
↓
WAIT_RISING
↓
HC-SR04 không phản hồi
Nếu không có timeout:
WAIT_RISING
WAIT_RISING
WAIT_RISING
...
Driver sẽ bị kẹt.
26. Dùng SysTick 1 ms
Bạn đã học Bài 36:
SysTick
↓
1 ms
Ta có:
volatile uint32_t system_ms;
ISR:
void SysTick_Handler(void)
{
system_ms++;
}
27. Không cần tăng timeout trong SysTick
Một cách tốt hơn là lưu:
start_time_ms
Ví dụ:
volatile uint32_t timeout_start;
Khi bắt đầu:
timeout_start = system_ms;
Sau đó kiểm tra:
if ((system_ms - timeout_start) > 30)
{
...
}
28. Tại sao dùng phép trừ?
Đây là kỹ thuật rất quan trọng:
system_ms - timeout_start
thay vì:
system_ms > timeout_start + 30
Cách phép trừ an toàn hơn khi uint32_t overflow.
Bạn sẽ gặp kỹ thuật này rất nhiều trong firmware thực tế.
29. Hàm Process
Ta viết:
void HCSR04_Process(void)
{
if (hcsr04_state ==
HCSR04_WAIT_RISING)
{
if ((system_ms - timeout_start) > 30)
{
hcsr04_state =
HCSR04_TIMEOUT;
}
}
else if (hcsr04_state ==
HCSR04_WAIT_FALLING)
{
if ((system_ms - timeout_start) > 30)
{
hcsr04_state =
HCSR04_TIMEOUT;
}
}
}
30. Main loop
Bây giờ main() có thể rất sạch:
while (1)
{
HCSR04_Process();
/*
Các task khác
*/
Motor_Process();
Button_Process();
IR_Process();
Robot_Process();
}
Không có:
delay_ms(60);
31. Chu trình hoạt động
Ví dụ robot đang chạy.
t = 0 ms
HCSR04_Start()
Sau đó:
t = 0.01 ms
TRIG LOW
CPU quay lại:
Motor
IR
Robot
...
Khi ECHO Rising:
TIM2 IRQ
CPU xử lý vài microsecond rồi quay lại công việc chính.
Khi ECHO Falling:
TIM2 IRQ
Driver tính:
echo_time
distance
32. Đây chính là Non-blocking
Blocking:
Trigger
↓
WAIT
↓
WAIT
↓
WAIT
↓
distance
Non-blocking:
Trigger
↓
return
↓
Motor
↓
IR
↓
Robot
↓
TIM IRQ
↓
Echo
↓
distance
Đây là một bước tiến rất lớn trong cách thiết kế firmware.
33. Trạng thái DONE
Sau khi đo xong:
hcsr04_state = HCSR04_DONE;
Application có thể đọc:
if (hcsr04_state == HCSR04_DONE)
{
uint16_t d = distance_cm;
hcsr04_state =
HCSR04_IDLE;
}
34. Tốt hơn: viết hàm kiểm tra kết quả
uint8_t HCSR04_IsReady(void)
{
return
hcsr04_state == HCSR04_DONE;
}
Sau đó:
if (HCSR04_IsReady())
{
distance = HCSR04_GetDistance();
HCSR04_Clear();
}
35. API của driver
Một driver tốt có thể có:
void HCSR04_Init(void);
void HCSR04_Start(void);
void HCSR04_Process(void);
uint8_t HCSR04_IsReady(void);
uint8_t HCSR04_IsTimeout(void);
uint16_t HCSR04_GetDistance(void);
Application không cần biết:
TIM2
CCR1
CCER
CCMR1
Đó là việc của driver.
36. Ví dụ API
if (HCSR04_IsReady())
{
uint16_t distance =
HCSR04_GetDistance();
if (distance < 30)
{
Robot_Stop();
Robot_Turn();
}
}
Đây chính là kiểu kiến trúc chúng ta muốn hướng tới.
37. Timeout bao nhiêu?
Ví dụ:
#define HCSR04_TIMEOUT_MS 30
Vì khoảng cách tối đa giả sử:
400 cm
thời gian ECHO khoảng:
23.2 ms
nên:
30 ms
là hợp lý.
38. Nhưng có một vấn đề
Nếu:
WAIT_RISING
timeout 30 ms thì được.
Nhưng nếu:
WAIT_FALLING
cũng timeout 30 ms thì tổng thời gian có thể hơi dài.
Có thể thiết kế:
#define RISING_TIMEOUT_MS 5
#define FALLING_TIMEOUT_MS 30
Ví dụ:
Rising phải xuất hiện nhanh
Falling có thể mất đến ~30 ms
39. State Machine nâng cao
Ta có thể định nghĩa:
typedef enum
{
HCSR04_IDLE,
HCSR04_TRIGGER,
HCSR04_WAIT_RISING,
HCSR04_WAIT_FALLING,
HCSR04_DONE,
HCSR04_TIMEOUT
} HCSR04_State;
40. Sơ đồ hoàn chỉnh
┌─────────────┐
│ IDLE │
└──────┬──────┘
│
Start
│
▼
┌─────────────┐
│ TRIGGER │
└──────┬──────┘
│
▼
┌─────────────────┐
│ WAIT_RISING │
└───────┬─────────┘
┌─────┴─────┐
Rising Timeout
│ │
▼ ▼
┌────────────────┐ ERROR
│ WAIT_FALLING │
└───────┬────────┘
┌────┴─────┐
Falling Timeout
│ │
▼ ▼
DONE ERROR
│
▼
IDLE
41. Một điều rất quan trọng: ISR phải ngắn
Không nên viết trong:
TIM2_IRQHandler()
những thứ như:
LCD_Print(...)
delay_ms(...)
Serial_Print(...)
Motor_Turn(...)
ISR nên làm rất ít:
Capture
↓
lưu giá trị
↓
đổi state
↓
clear flag
↓
return
42. Tại sao?
Nếu ISR mất quá nhiều thời gian:
TIM IRQ
↓
ISR dài
↓
Timer khác phát IRQ
↓
CPU chưa xử lý
có thể gây:
latency
missed event
jitter
Trong hệ thống real-time, đây là vấn đề nghiêm trọng.
43. Tách ISR và Application
ISR:
echo_time = ...
state = DONE;
Application:
if (state == DONE)
{
distance = ...
}
Đây là nguyên tắc rất tốt:
Interrupt thu thập dữ liệu; main xử lý logic.
44. Bài tập thực hành đầu tiên
Hãy nối:
STM32F103
PA1 → TRIG
PA0 → ECHO
qua cầu phân áp:
10k + 20k
Sau đó chạy:
HCSR04_Start()
và kiểm tra:
distance_cm
45. Bài tập thứ hai
Đưa tay:
10 cm
20 cm
30 cm
40 cm
và kiểm tra:
echo_time
Bạn nên thấy xấp xỉ:
10 cm → 580 µs
20 cm → 1160 µs
30 cm → 1740 µs
40 cm → 2320 µs
Không cần chính xác tuyệt đối vì tốc độ âm thanh thay đổi theo nhiệt độ và cảm biến có sai số.
46. Bài tập thứ ba – cố tình tạo Timeout
Không nối ECHO.
Chạy:
HCSR04_Start();
Sau khoảng:
30 ms
phải chuyển:
WAIT_RISING
↓
TIMEOUT
Nếu chương trình cứ đứng:
WAIT_RISING
WAIT_RISING
WAIT_RISING
thì driver của bạn đang thiếu hoặc sai timeout.
47. Bài tập thứ tư – robot
Giả sử:
#define OBSTACLE_DISTANCE 30
Robot:
if (distance < 30)
{
Robot_Stop();
Robot_TurnRight();
}
else
{
Robot_Forward();
}
Điểm quan trọng là không được dùng:
delay_ms(1000);
để robot rẽ trong driver HC-SR04.
Mỗi module nên non-blocking.
48. Bài học lớn nhất của Bài 44
Bạn vừa chuyển từ tư duy:
"Đọc cảm biến"
sang:
"Thiết kế driver cảm biến"
Hai thứ hoàn toàn khác nhau.
49. Từ đây firmware sẽ có cấu trúc
APPLICATION
│
┌──────┴──────┐
│ │
Robot Logic User Input
│ │
└──────┬──────┘
│
DRIVERS
│
┌─────────────┼─────────────┐
│ │ │
HCSR04 Motor IR
│ │ │
TIM PWM EXTI/Timer
│ │ │
└─────────────┴─────────────┘
│
HARDWARE
Đây là nền tảng để sau này bạn có thể viết firmware robot rất lớn mà code vẫn kiểm soát được.
50. Kiến thức cần thuộc sau Bài 44
Bạn không cần học thuộc từng dòng code.
Nhưng phải hiểu thật chắc 8 thứ này:
① Timer Prescaler
PSC = 71
→ 72 MHz thành 1 MHz.
② Timer Counter
CNT
→ đồng hồ thời gian.
③ Input Capture
CCR1
→ chụp giá trị CNT khi có cạnh.
④ Rising Edge
LOW → HIGH
→ bắt đầu ECHO.
⑤ Falling Edge
HIGH → LOW
→ kết thúc ECHO.
⑥ Pulse width
falling - rising
→ thời gian ECHO.
⑦ Timeout
Không có phản hồi
→ không được chờ vô hạn
⑧ State Machine
IDLE
→ WAIT
→ DONE
→ IDLE
→ hệ thống không bị blocking.
51. Bài 44 và robot của bạn
Đây chính là phần rất đáng chú ý đối với dự án robot mà bạn đang phát triển.
Thay vì:
HC-SR04
↓
delay
↓
đọc
↓
motor
ta có:
┌──────────────┐
│ HC-SR04 │
│ State Machine │
└──────┬───────┘
│
distance
│
▼
┌──────────────┐
│ Robot Logic │
└──────┬───────┘
│
┌──────┴───────┐
▼ ▼
Motor PWM Servo
Trong khi HC-SR04 đang chờ Echo:
CPU vẫn có thể:
✓ điều khiển PWM
✓ đọc IR
✓ xử lý nút
✓ cập nhật trạng thái robot
✓ kiểm tra Manual/Auto
Đó chính là lý do State Machine + Interrupt + Timer quan trọng đến vậy.
Bài tiếp theo
Sau Bài 44, bước rất hợp lý là:
BÀI 45 – TIMER ENCODER MODE TRÊN STM32F103
Đọc encoder bằng phần cứng Timer → đếm xung → xác định chiều → tính RPM → tính quãng đường
Khi đó chúng ta sẽ ghép:
HC-SR04
↓
khoảng cách
Encoder
↓
tốc độ + quãng đường
PWM
↓
điều khiển motor
và bắt đầu tiến rất gần tới bộ điều khiển chuyển động robot thực sự.