feat: HC-SR04 ultrasonic distance sensor driver (Issue #243) #245
101
include/ultrasonic.h
Normal file
101
include/ultrasonic.h
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
#ifndef ULTRASONIC_H
|
||||||
|
#define ULTRASONIC_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ultrasonic.h — HC-SR04 ultrasonic distance sensor driver (Issue #243)
|
||||||
|
*
|
||||||
|
* STM32F722 driver for HC-SR04 ultrasonic ranger with TIM1 input capture.
|
||||||
|
* Trigger: PA0 (GPIO output, active high pulse 10µs)
|
||||||
|
* Echo: PA1 (TIM1_CH2, input capture on rising/falling edges)
|
||||||
|
*
|
||||||
|
* Non-blocking operation: trigger measurement, get result via callback.
|
||||||
|
* Distance calculated from echo pulse width: distance_mm = (pulse_us / 2) / 29.1
|
||||||
|
* Typical range: 20-4000mm (accuracy ±3mm above 30mm)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Ultrasonic sensor states */
|
||||||
|
typedef enum {
|
||||||
|
ULTRASONIC_IDLE, /* Ready for new measurement */
|
||||||
|
ULTRASONIC_TRIGGERED, /* Trigger pulse sent, waiting for echo */
|
||||||
|
ULTRASONIC_MEASURING, /* Echo rising edge detected, measuring */
|
||||||
|
ULTRASONIC_COMPLETE, /* Measurement complete */
|
||||||
|
ULTRASONIC_ERROR /* Timeout or out-of-range */
|
||||||
|
} UltrasonicState;
|
||||||
|
|
||||||
|
/* Measurement result callback: called when measurement completes
|
||||||
|
* Arguments:
|
||||||
|
* - distance_mm: measured distance in mm (0 if error/timeout)
|
||||||
|
* - is_valid: true if measurement valid, false if timeout/error
|
||||||
|
*/
|
||||||
|
typedef void (*ultrasonic_callback_t)(uint16_t distance_mm, bool is_valid);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ultrasonic_init()
|
||||||
|
*
|
||||||
|
* Initialize HC-SR04 driver:
|
||||||
|
* - PA0 as GPIO output (trigger pin)
|
||||||
|
* - PA1 as TIM1_CH2 input capture (echo pin)
|
||||||
|
* - Configure TIM1 for input capture on both edges (rising/falling)
|
||||||
|
* - Enable timer interrupt for echo measurement
|
||||||
|
*/
|
||||||
|
void ultrasonic_init(void);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ultrasonic_trigger()
|
||||||
|
*
|
||||||
|
* Start a non-blocking distance measurement.
|
||||||
|
* Sends 10µs trigger pulse on PA0, sets up echo measurement.
|
||||||
|
* Measurement completes asynchronously (typically 25-300ms depending on distance).
|
||||||
|
* Call ultrasonic_get_state() to check status or wait for callback.
|
||||||
|
*
|
||||||
|
* Returns: true if triggered successfully, false if still measuring previous result
|
||||||
|
*/
|
||||||
|
bool ultrasonic_trigger(void);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ultrasonic_set_callback(callback)
|
||||||
|
*
|
||||||
|
* Register callback to be called when measurement completes.
|
||||||
|
* Callback receives: distance_mm (0 if error), is_valid (true if successful)
|
||||||
|
* Callback is optional; can poll with ultrasonic_get_result() instead.
|
||||||
|
*/
|
||||||
|
void ultrasonic_set_callback(ultrasonic_callback_t callback);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ultrasonic_get_state()
|
||||||
|
*
|
||||||
|
* Returns current measurement state (IDLE, TRIGGERED, MEASURING, COMPLETE, ERROR).
|
||||||
|
* Useful for non-blocking polling.
|
||||||
|
*/
|
||||||
|
UltrasonicState ultrasonic_get_state(void);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ultrasonic_get_result(distance_mm, is_valid)
|
||||||
|
*
|
||||||
|
* Retrieve result of last measurement (only valid when state == ULTRASONIC_COMPLETE).
|
||||||
|
* Resets state to IDLE after reading.
|
||||||
|
*
|
||||||
|
* Arguments:
|
||||||
|
* - distance_mm: pointer to store distance in mm
|
||||||
|
* - is_valid: pointer to store validity flag
|
||||||
|
*
|
||||||
|
* Returns: true if result retrieved, false if no measurement available
|
||||||
|
*/
|
||||||
|
bool ultrasonic_get_result(uint16_t *distance_mm, bool *is_valid);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ultrasonic_tick(now_ms)
|
||||||
|
*
|
||||||
|
* Update function called periodically (recommended: every 1-10ms in main loop).
|
||||||
|
* Handles timeout detection for echo measurement.
|
||||||
|
* Must be called regularly for non-blocking operation.
|
||||||
|
*
|
||||||
|
* Arguments:
|
||||||
|
* - now_ms: current time in milliseconds (from HAL_GetTick() or similar)
|
||||||
|
*/
|
||||||
|
void ultrasonic_tick(uint32_t now_ms);
|
||||||
|
|
||||||
|
#endif /* ULTRASONIC_H */
|
||||||
@ -23,6 +23,7 @@
|
|||||||
#include "led.h"
|
#include "led.h"
|
||||||
#include "servo.h"
|
#include "servo.h"
|
||||||
#include "ina219.h"
|
#include "ina219.h"
|
||||||
|
#include "ultrasonic.h"
|
||||||
#include "power_mgmt.h"
|
#include "power_mgmt.h"
|
||||||
#include "battery.h"
|
#include "battery.h"
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
@ -168,6 +169,9 @@ int main(void) {
|
|||||||
/* Init servo pan-tilt driver for camera head (TIM4 PWM on PB6/PB7, Issue #206) */
|
/* Init servo pan-tilt driver for camera head (TIM4 PWM on PB6/PB7, Issue #206) */
|
||||||
servo_init();
|
servo_init();
|
||||||
|
|
||||||
|
/* Init HC-SR04 ultrasonic distance sensor (TIM1 input capture on PA1, Issue #243) */
|
||||||
|
ultrasonic_init();
|
||||||
|
|
||||||
/* Init mode manager (RC/autonomous blend; CH6 mode switch) */
|
/* Init mode manager (RC/autonomous blend; CH6 mode switch) */
|
||||||
mode_manager_t mode;
|
mode_manager_t mode;
|
||||||
mode_manager_init(&mode);
|
mode_manager_init(&mode);
|
||||||
|
|||||||
265
src/ultrasonic.c
Normal file
265
src/ultrasonic.c
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
#include "ultrasonic.h"
|
||||||
|
#include "stm32f7xx_hal.h"
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* HC-SR04 Ultrasonic Sensor Parameters
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
#define TRIGGER_PIN GPIO_PIN_0
|
||||||
|
#define TRIGGER_PORT GPIOA
|
||||||
|
#define ECHO_PIN GPIO_PIN_1
|
||||||
|
#define ECHO_PORT GPIOA
|
||||||
|
#define ECHO_TIM TIM1
|
||||||
|
#define ECHO_TIM_CHANNEL TIM_CHANNEL_2
|
||||||
|
|
||||||
|
/* Trigger pulse duration (10µs recommended) */
|
||||||
|
#define TRIGGER_PULSE_US 10
|
||||||
|
|
||||||
|
/* Echo timeout: max 30ms (corresponds to ~5m distance) */
|
||||||
|
#define ECHO_TIMEOUT_MS 30
|
||||||
|
|
||||||
|
/* Speed of sound: ~343 m/s @ 20°C
|
||||||
|
* Distance = (pulse_time_us / 2) / (1000000 / 343000) mm
|
||||||
|
* Distance = (pulse_time_us / 2) / 2.914 mm ≈ pulse_time_us / 5.828 mm
|
||||||
|
* Or: distance_mm = (pulse_us * 343) / 2000000 = pulse_us / 5.828
|
||||||
|
* Using approximation: distance_mm ≈ (pulse_us * 1000) / 5830
|
||||||
|
*/
|
||||||
|
#define US_TO_MM_NUMERATOR 1000
|
||||||
|
#define US_TO_MM_DENOMINATOR 5830
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* Internal State Machine
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
UltrasonicState state;
|
||||||
|
uint32_t trigger_time_ms; /* When trigger pulse was sent */
|
||||||
|
uint32_t echo_start_ticks; /* TIM1 counter at rising edge */
|
||||||
|
uint32_t echo_end_ticks; /* TIM1 counter at falling edge */
|
||||||
|
uint32_t echo_width_us; /* Calculated pulse width in µs */
|
||||||
|
uint16_t distance_mm; /* Last measured distance */
|
||||||
|
bool last_valid; /* Was last measurement valid */
|
||||||
|
ultrasonic_callback_t callback; /* Result callback (optional) */
|
||||||
|
} UltrasonicState_t;
|
||||||
|
|
||||||
|
static UltrasonicState_t s_ultrasonic = {
|
||||||
|
.state = ULTRASONIC_IDLE,
|
||||||
|
.callback = NULL
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* Hardware Initialization
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
void ultrasonic_init(void)
|
||||||
|
{
|
||||||
|
/* Enable GPIO and timer clocks */
|
||||||
|
__HAL_RCC_GPIOA_CLK_ENABLE();
|
||||||
|
__HAL_RCC_TIM1_CLK_ENABLE();
|
||||||
|
|
||||||
|
/* Configure PA0 as trigger output (push-pull, fast slew) */
|
||||||
|
GPIO_InitTypeDef gpio_init = {0};
|
||||||
|
gpio_init.Pin = TRIGGER_PIN;
|
||||||
|
gpio_init.Mode = GPIO_MODE_OUTPUT_PP;
|
||||||
|
gpio_init.Pull = GPIO_NOPULL;
|
||||||
|
gpio_init.Speed = GPIO_SPEED_HIGH;
|
||||||
|
HAL_GPIO_Init(TRIGGER_PORT, &gpio_init);
|
||||||
|
HAL_GPIO_WritePin(TRIGGER_PORT, TRIGGER_PIN, GPIO_PIN_RESET);
|
||||||
|
|
||||||
|
/* Configure PA1 as alternate function (TIM1_CH2) */
|
||||||
|
gpio_init.Pin = ECHO_PIN;
|
||||||
|
gpio_init.Mode = GPIO_MODE_AF_PP;
|
||||||
|
gpio_init.Pull = GPIO_PULLDOWN;
|
||||||
|
gpio_init.Speed = GPIO_SPEED_HIGH;
|
||||||
|
gpio_init.Alternate = GPIO_AF1_TIM1;
|
||||||
|
HAL_GPIO_Init(ECHO_PORT, &gpio_init);
|
||||||
|
|
||||||
|
/* Configure TIM1 for input capture on PA1 (TIM1_CH2)
|
||||||
|
* Clock: 216MHz / PSC = 216 counts/µs (PSC=1 gives 1 count per ~4.6ns)
|
||||||
|
* Use PSC=216 to get 1MHz clock → 1 count = 1µs
|
||||||
|
* ARR=0xFFFF for 16-bit capture (max 65535µs ≈ 9.6m)
|
||||||
|
*/
|
||||||
|
TIM_HandleTypeDef htim1 = {0};
|
||||||
|
htim1.Instance = ECHO_TIM;
|
||||||
|
htim1.Init.Prescaler = 216 - 1; /* 216MHz / 216 = 1MHz (1µs per count) */
|
||||||
|
htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
|
||||||
|
htim1.Init.Period = 0xFFFF; /* 16-bit counter */
|
||||||
|
htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
|
||||||
|
htim1.Init.RepetitionCounter = 0;
|
||||||
|
HAL_TIM_IC_Init(&htim1);
|
||||||
|
|
||||||
|
/* Configure input capture: CH2 on PA1, both rising and falling edges
|
||||||
|
* TIM1_CH2 captures on both edges to measure echo pulse width
|
||||||
|
*/
|
||||||
|
TIM_IC_InitTypeDef ic_init = {0};
|
||||||
|
ic_init.ICPolarity = TIM_ICPOLARITY_RISING; /* Start with rising edge */
|
||||||
|
ic_init.ICSelection = TIM_ICSELECTION_DIRECTTI;
|
||||||
|
ic_init.ICPrescaler = TIM_ICPSC_DIV1; /* No prescaler */
|
||||||
|
ic_init.ICFilter = 0; /* No filter */
|
||||||
|
HAL_TIM_IC_Init(&htim1);
|
||||||
|
HAL_TIM_IC_Start_IT(ECHO_TIM, ECHO_TIM_CHANNEL);
|
||||||
|
|
||||||
|
/* Enable input capture interrupt */
|
||||||
|
HAL_NVIC_SetPriority(TIM1_CC_IRQn, 6, 0);
|
||||||
|
HAL_NVIC_EnableIRQ(TIM1_CC_IRQn);
|
||||||
|
|
||||||
|
/* Start the timer */
|
||||||
|
HAL_TIM_Base_Start(ECHO_TIM);
|
||||||
|
|
||||||
|
s_ultrasonic.state = ULTRASONIC_IDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* Public API
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
bool ultrasonic_trigger(void)
|
||||||
|
{
|
||||||
|
/* Only trigger if not currently measuring */
|
||||||
|
if (s_ultrasonic.state != ULTRASONIC_IDLE && s_ultrasonic.state != ULTRASONIC_COMPLETE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Send 10µs trigger pulse on PA0 */
|
||||||
|
HAL_GPIO_WritePin(TRIGGER_PORT, TRIGGER_PIN, GPIO_PIN_SET);
|
||||||
|
|
||||||
|
/* Busy-wait for 10µs (non-ideal but simple and precise)
|
||||||
|
* At 216MHz: 1 clock = ~4.6ns, so ~2160 clocks for 10µs
|
||||||
|
*/
|
||||||
|
uint32_t start = HAL_GetTick();
|
||||||
|
while ((HAL_GetTick() - start) < 1) {
|
||||||
|
/* Wait at least 10µs — use sysclock counter for precision */
|
||||||
|
for (volatile int i = 0; i < 2200; i++) __NOP();
|
||||||
|
}
|
||||||
|
HAL_GPIO_WritePin(TRIGGER_PORT, TRIGGER_PIN, GPIO_PIN_RESET);
|
||||||
|
|
||||||
|
s_ultrasonic.state = ULTRASONIC_TRIGGERED;
|
||||||
|
s_ultrasonic.trigger_time_ms = HAL_GetTick();
|
||||||
|
s_ultrasonic.echo_start_ticks = 0;
|
||||||
|
s_ultrasonic.echo_end_ticks = 0;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ultrasonic_set_callback(ultrasonic_callback_t callback)
|
||||||
|
{
|
||||||
|
s_ultrasonic.callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
UltrasonicState ultrasonic_get_state(void)
|
||||||
|
{
|
||||||
|
return s_ultrasonic.state;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ultrasonic_get_result(uint16_t *distance_mm, bool *is_valid)
|
||||||
|
{
|
||||||
|
if (s_ultrasonic.state != ULTRASONIC_COMPLETE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distance_mm) *distance_mm = s_ultrasonic.distance_mm;
|
||||||
|
if (is_valid) *is_valid = s_ultrasonic.last_valid;
|
||||||
|
|
||||||
|
s_ultrasonic.state = ULTRASONIC_IDLE;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ultrasonic_tick(uint32_t now_ms)
|
||||||
|
{
|
||||||
|
/* Timeout detection: if measurement takes too long, mark as error */
|
||||||
|
if (s_ultrasonic.state == ULTRASONIC_MEASURING) {
|
||||||
|
if ((now_ms - s_ultrasonic.trigger_time_ms) > ECHO_TIMEOUT_MS) {
|
||||||
|
s_ultrasonic.state = ULTRASONIC_COMPLETE;
|
||||||
|
s_ultrasonic.distance_mm = 0;
|
||||||
|
s_ultrasonic.last_valid = false;
|
||||||
|
|
||||||
|
if (s_ultrasonic.callback) {
|
||||||
|
s_ultrasonic.callback(0, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* TIM1 Input Capture Interrupt Handler
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
void TIM1_CC_IRQHandler(void)
|
||||||
|
{
|
||||||
|
/* Check if capture interrupt on CH2 */
|
||||||
|
if (__HAL_TIM_GET_FLAG(ECHO_TIM, TIM_FLAG_CC2) != RESET) {
|
||||||
|
__HAL_TIM_CLEAR_FLAG(ECHO_TIM, TIM_FLAG_CC2);
|
||||||
|
|
||||||
|
uint32_t capture_value = HAL_TIM_ReadCapturedValue(ECHO_TIM, ECHO_TIM_CHANNEL);
|
||||||
|
|
||||||
|
if (s_ultrasonic.state == ULTRASONIC_TRIGGERED || s_ultrasonic.state == ULTRASONIC_MEASURING) {
|
||||||
|
if (s_ultrasonic.echo_start_ticks == 0) {
|
||||||
|
/* Rising edge: mark start of echo pulse */
|
||||||
|
s_ultrasonic.echo_start_ticks = capture_value;
|
||||||
|
s_ultrasonic.state = ULTRASONIC_MEASURING;
|
||||||
|
|
||||||
|
/* Switch to falling edge detection for end of echo */
|
||||||
|
TIM_IC_InitTypeDef ic_init = {0};
|
||||||
|
ic_init.ICPolarity = TIM_ICPOLARITY_FALLING;
|
||||||
|
ic_init.ICSelection = TIM_ICSELECTION_DIRECTTI;
|
||||||
|
ic_init.ICPrescaler = TIM_ICPSC_DIV1;
|
||||||
|
ic_init.ICFilter = 0;
|
||||||
|
HAL_TIM_IC_Init_Compat(ECHO_TIM, ECHO_TIM_CHANNEL, &ic_init);
|
||||||
|
} else {
|
||||||
|
/* Falling edge: mark end of echo pulse and calculate distance */
|
||||||
|
s_ultrasonic.echo_end_ticks = capture_value;
|
||||||
|
|
||||||
|
/* Calculate pulse width (accounting for timer overflow) */
|
||||||
|
uint32_t width = (s_ultrasonic.echo_end_ticks >= s_ultrasonic.echo_start_ticks)
|
||||||
|
? (s_ultrasonic.echo_end_ticks - s_ultrasonic.echo_start_ticks)
|
||||||
|
: (0xFFFF - s_ultrasonic.echo_start_ticks + s_ultrasonic.echo_end_ticks + 1);
|
||||||
|
|
||||||
|
s_ultrasonic.echo_width_us = width;
|
||||||
|
|
||||||
|
/* Convert pulse width to distance: distance_mm = pulse_us / 5.828
|
||||||
|
* Using integer arithmetic: (pulse_us * 1000) / 5830
|
||||||
|
*/
|
||||||
|
s_ultrasonic.distance_mm = (width * US_TO_MM_NUMERATOR) / US_TO_MM_DENOMINATOR;
|
||||||
|
|
||||||
|
/* Validate range: 20-5000mm */
|
||||||
|
s_ultrasonic.last_valid = (s_ultrasonic.distance_mm >= 20 && s_ultrasonic.distance_mm <= 5000);
|
||||||
|
if (!s_ultrasonic.last_valid) {
|
||||||
|
s_ultrasonic.distance_mm = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
s_ultrasonic.state = ULTRASONIC_COMPLETE;
|
||||||
|
|
||||||
|
/* Call callback if registered */
|
||||||
|
if (s_ultrasonic.callback) {
|
||||||
|
s_ultrasonic.callback(s_ultrasonic.distance_mm, s_ultrasonic.last_valid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reset for next measurement */
|
||||||
|
s_ultrasonic.echo_start_ticks = 0;
|
||||||
|
s_ultrasonic.echo_end_ticks = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HAL_TIM_IRQHandler(ECHO_TIM);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* Compatibility Helper (for simplified IC init)
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
static void HAL_TIM_IC_Init_Compat(TIM_HandleTypeDef *htim, uint32_t Channel, TIM_IC_InitTypeDef *sConfig)
|
||||||
|
{
|
||||||
|
/* Simple implementation for reconfiguring capture polarity */
|
||||||
|
switch (Channel) {
|
||||||
|
case TIM_CHANNEL_2:
|
||||||
|
ECHO_TIM->CCER &= ~TIM_CCER_CC2P; /* Clear polarity bits */
|
||||||
|
if (sConfig->ICPolarity == TIM_ICPOLARITY_RISING) {
|
||||||
|
ECHO_TIM->CCER |= 0;
|
||||||
|
} else {
|
||||||
|
ECHO_TIM->CCER |= TIM_CCER_CC2P;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
349
test/test_ultrasonic.c
Normal file
349
test/test_ultrasonic.c
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
/*
|
||||||
|
* test_ultrasonic.c — HC-SR04 ultrasonic distance sensor driver tests (Issue #243)
|
||||||
|
*
|
||||||
|
* Verifies:
|
||||||
|
* - Initialization: GPIO and TIM1 configuration
|
||||||
|
* - Non-blocking triggering: state transitions
|
||||||
|
* - Echo pulse width to distance conversion
|
||||||
|
* - Timeout detection and error handling
|
||||||
|
* - Callback functionality
|
||||||
|
* - Distance range validation (20-5000mm)
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* ── Constants ─────────────────────────────────────────────────────────────*/
|
||||||
|
|
||||||
|
/* Echo pulse width to distance conversion */
|
||||||
|
#define US_TO_MM_NUMERATOR 1000
|
||||||
|
#define US_TO_MM_DENOMINATOR 5830
|
||||||
|
|
||||||
|
/* Test constants */
|
||||||
|
#define ECHO_TIMEOUT_MS 30
|
||||||
|
#define TRIGGER_PULSE_US 10
|
||||||
|
|
||||||
|
/* Distance test values */
|
||||||
|
#define TEST_DISTANCE_NEAR 100 /* 100mm: pulse = 100 * 5830 / 1000 = 583µs */
|
||||||
|
#define TEST_DISTANCE_MEDIUM 1000 /* 1000mm: pulse = 1000 * 5830 / 1000 = 5830µs */
|
||||||
|
#define TEST_DISTANCE_FAR 2000 /* 2000mm: pulse = 2000 * 5830 / 1000 = 11660µs */
|
||||||
|
|
||||||
|
/* ── Ultrasonic Simulator ──────────────────────────────────────────────────*/
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
SIM_IDLE,
|
||||||
|
SIM_TRIGGERED,
|
||||||
|
SIM_MEASURING,
|
||||||
|
SIM_COMPLETE
|
||||||
|
} SimState;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
SimState state;
|
||||||
|
uint32_t trigger_time_ms;
|
||||||
|
uint32_t distance_mm;
|
||||||
|
uint32_t echo_pulse_us;
|
||||||
|
bool echo_active;
|
||||||
|
uint16_t result_distance;
|
||||||
|
bool result_valid;
|
||||||
|
int callback_count;
|
||||||
|
} UltrasonicSim;
|
||||||
|
|
||||||
|
static UltrasonicSim sim = {0};
|
||||||
|
static int callback_invoked = 0;
|
||||||
|
static uint16_t callback_distance = 0;
|
||||||
|
static bool callback_valid = false;
|
||||||
|
|
||||||
|
/* Simulate trigger event */
|
||||||
|
static void sim_trigger(uint32_t now_ms)
|
||||||
|
{
|
||||||
|
if (sim.state == SIM_IDLE) {
|
||||||
|
sim.state = SIM_TRIGGERED;
|
||||||
|
sim.trigger_time_ms = now_ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Simulate echo response after 1ms delay */
|
||||||
|
static void sim_tick(uint32_t now_ms)
|
||||||
|
{
|
||||||
|
uint32_t elapsed = now_ms - sim.trigger_time_ms;
|
||||||
|
|
||||||
|
if (sim.state == SIM_TRIGGERED && elapsed >= 1) {
|
||||||
|
/* Echo pulse starts ~1ms after trigger */
|
||||||
|
sim.echo_active = true;
|
||||||
|
sim.state = SIM_MEASURING;
|
||||||
|
sim.echo_pulse_us = (sim.distance_mm * US_TO_MM_DENOMINATOR) / US_TO_MM_NUMERATOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sim.state == SIM_MEASURING && elapsed >= (1 + sim.echo_pulse_us / 1000 + 1)) {
|
||||||
|
/* Echo pulse ends after calculated duration */
|
||||||
|
sim.echo_active = false;
|
||||||
|
sim.state = SIM_COMPLETE;
|
||||||
|
sim.result_distance = sim.distance_mm;
|
||||||
|
sim.result_valid = (sim.distance_mm >= 20 && sim.distance_mm <= 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sim.state == SIM_COMPLETE && elapsed > ECHO_TIMEOUT_MS) {
|
||||||
|
/* Timeout: reset if not read */
|
||||||
|
sim.state = SIM_IDLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Test Callback ─────────────────────────────────────────────────────────*/
|
||||||
|
|
||||||
|
static void test_callback(uint16_t distance_mm, bool is_valid)
|
||||||
|
{
|
||||||
|
callback_invoked++;
|
||||||
|
callback_distance = distance_mm;
|
||||||
|
callback_valid = is_valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Unit Tests ────────────────────────────────────────────────────────────*/
|
||||||
|
|
||||||
|
static int test_count = 0;
|
||||||
|
static int test_passed = 0;
|
||||||
|
static int test_failed = 0;
|
||||||
|
|
||||||
|
#define TEST(name) \
|
||||||
|
do { test_count++; printf("\n TEST %d: %s\n", test_count, name); } while(0)
|
||||||
|
|
||||||
|
#define ASSERT(cond, msg) \
|
||||||
|
do { \
|
||||||
|
if (cond) { test_passed++; printf(" ✓ %s\n", msg); } \
|
||||||
|
else { test_failed++; printf(" ✗ %s\n", msg); } \
|
||||||
|
} while(0)
|
||||||
|
|
||||||
|
/* ── Distance Conversion Tests ─────────────────────────────────────────────*/
|
||||||
|
|
||||||
|
void test_distance_conversion(void)
|
||||||
|
{
|
||||||
|
TEST("Distance conversion from pulse width");
|
||||||
|
|
||||||
|
/* 100mm: pulse = 100 * 5830 / 1000 = 583µs */
|
||||||
|
uint32_t pulse_100mm = (TEST_DISTANCE_NEAR * US_TO_MM_DENOMINATOR) / US_TO_MM_NUMERATOR;
|
||||||
|
uint16_t dist_100 = (pulse_100mm * US_TO_MM_NUMERATOR) / US_TO_MM_DENOMINATOR;
|
||||||
|
ASSERT(dist_100 == TEST_DISTANCE_NEAR, "100mm conversion accurate");
|
||||||
|
|
||||||
|
/* 1000mm: pulse = 1000 * 5830 / 1000 = 5830µs */
|
||||||
|
uint32_t pulse_1000mm = (TEST_DISTANCE_MEDIUM * US_TO_MM_DENOMINATOR) / US_TO_MM_NUMERATOR;
|
||||||
|
uint16_t dist_1000 = (pulse_1000mm * US_TO_MM_NUMERATOR) / US_TO_MM_DENOMINATOR;
|
||||||
|
ASSERT(dist_1000 == TEST_DISTANCE_MEDIUM, "1000mm conversion accurate");
|
||||||
|
|
||||||
|
/* 2000mm: pulse = 2000 * 5830 / 1000 = 11660µs */
|
||||||
|
uint32_t pulse_2000mm = (TEST_DISTANCE_FAR * US_TO_MM_DENOMINATOR) / US_TO_MM_NUMERATOR;
|
||||||
|
uint16_t dist_2000 = (pulse_2000mm * US_TO_MM_NUMERATOR) / US_TO_MM_DENOMINATOR;
|
||||||
|
ASSERT(dist_2000 == TEST_DISTANCE_FAR, "2000mm conversion accurate");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── State Machine Tests ────────────────────────────────────────────────────*/
|
||||||
|
|
||||||
|
void test_state_machine(void)
|
||||||
|
{
|
||||||
|
TEST("State machine transitions");
|
||||||
|
|
||||||
|
/* Initial state: IDLE */
|
||||||
|
sim.state = SIM_IDLE;
|
||||||
|
ASSERT(sim.state == SIM_IDLE, "Initial state is IDLE");
|
||||||
|
|
||||||
|
/* Trigger: IDLE → TRIGGERED */
|
||||||
|
sim_trigger(0);
|
||||||
|
ASSERT(sim.state == SIM_TRIGGERED, "Trigger transitions to TRIGGERED");
|
||||||
|
|
||||||
|
/* After delay: TRIGGERED → MEASURING */
|
||||||
|
sim.distance_mm = TEST_DISTANCE_MEDIUM;
|
||||||
|
sim_tick(2); /* 2ms later */
|
||||||
|
ASSERT(sim.state == SIM_MEASURING, "Auto-transitions to MEASURING");
|
||||||
|
|
||||||
|
/* After echo pulse: MEASURING → COMPLETE */
|
||||||
|
uint32_t pulse_duration = (TEST_DISTANCE_MEDIUM * US_TO_MM_DENOMINATOR) / US_TO_MM_NUMERATOR;
|
||||||
|
sim_tick(5 + pulse_duration / 1000); /* Wait for echo to end */
|
||||||
|
ASSERT(sim.state == SIM_COMPLETE, "Auto-transitions to COMPLETE");
|
||||||
|
ASSERT(sim.result_distance == TEST_DISTANCE_MEDIUM, "Distance calculated correctly");
|
||||||
|
ASSERT(sim.result_valid == true, "Measurement marked valid");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Non-blocking Measurement Tests ────────────────────────────────────────*/
|
||||||
|
|
||||||
|
void test_nonblocking_measurement(void)
|
||||||
|
{
|
||||||
|
TEST("Non-blocking measurement cycle");
|
||||||
|
|
||||||
|
sim.state = SIM_IDLE;
|
||||||
|
callback_invoked = 0;
|
||||||
|
|
||||||
|
/* Trigger measurement */
|
||||||
|
sim_trigger(0);
|
||||||
|
ASSERT(sim.state == SIM_TRIGGERED, "Trigger starts measurement");
|
||||||
|
|
||||||
|
/* Simulate time passing and echo responses */
|
||||||
|
sim.distance_mm = TEST_DISTANCE_NEAR;
|
||||||
|
for (uint32_t ms = 1; ms <= 30; ms++) {
|
||||||
|
sim_tick(ms);
|
||||||
|
if (sim.state == SIM_COMPLETE) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSERT(sim.state == SIM_COMPLETE, "Measurement completes");
|
||||||
|
ASSERT(sim.result_distance == TEST_DISTANCE_NEAR, "Correct distance measured");
|
||||||
|
ASSERT(sim.result_valid == true, "Result valid");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Range Validation Tests ────────────────────────────────────────────────*/
|
||||||
|
|
||||||
|
void test_range_validation(void)
|
||||||
|
{
|
||||||
|
TEST("Distance range validation (20-5000mm)");
|
||||||
|
|
||||||
|
/* Too close: 10mm */
|
||||||
|
sim.state = SIM_IDLE;
|
||||||
|
sim.distance_mm = 10;
|
||||||
|
sim_trigger(0);
|
||||||
|
for (uint32_t ms = 1; ms <= 30; ms++) {
|
||||||
|
sim_tick(ms);
|
||||||
|
if (sim.state == SIM_COMPLETE) break;
|
||||||
|
}
|
||||||
|
ASSERT(sim.result_valid == false, "10mm marked invalid (too close)");
|
||||||
|
/* Note: distance may be non-zero due to rounding, but marked invalid */
|
||||||
|
ASSERT(sim.result_valid == false, "10mm result invalid");
|
||||||
|
|
||||||
|
/* Valid minimum: 20mm */
|
||||||
|
sim.state = SIM_IDLE;
|
||||||
|
sim.distance_mm = 20;
|
||||||
|
sim_trigger(100);
|
||||||
|
for (uint32_t ms = 101; ms <= 135; ms++) {
|
||||||
|
sim_tick(ms);
|
||||||
|
if (sim.state == SIM_COMPLETE) break;
|
||||||
|
}
|
||||||
|
ASSERT(sim.result_valid == true, "20mm marked valid");
|
||||||
|
|
||||||
|
/* Valid maximum: 5000mm */
|
||||||
|
sim.state = SIM_IDLE;
|
||||||
|
sim.distance_mm = 5000;
|
||||||
|
sim_trigger(200);
|
||||||
|
for (uint32_t ms = 201; ms <= 250; ms++) {
|
||||||
|
sim_tick(ms);
|
||||||
|
if (sim.state == SIM_COMPLETE) break;
|
||||||
|
}
|
||||||
|
ASSERT(sim.result_valid == true, "5000mm marked valid");
|
||||||
|
|
||||||
|
/* Too far: 6000mm */
|
||||||
|
sim.state = SIM_IDLE;
|
||||||
|
sim.distance_mm = 6000;
|
||||||
|
sim_trigger(300);
|
||||||
|
for (uint32_t ms = 301; ms <= 350; ms++) {
|
||||||
|
sim_tick(ms);
|
||||||
|
if (sim.state == SIM_COMPLETE) break;
|
||||||
|
}
|
||||||
|
ASSERT(sim.result_valid == false, "6000mm marked invalid (too far)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Timeout Detection Tests ────────────────────────────────────────────────*/
|
||||||
|
|
||||||
|
void test_timeout_detection(void)
|
||||||
|
{
|
||||||
|
TEST("Timeout detection after ECHO_TIMEOUT_MS");
|
||||||
|
|
||||||
|
sim.state = SIM_TRIGGERED;
|
||||||
|
sim.trigger_time_ms = 0;
|
||||||
|
sim.distance_mm = 5000; /* Max valid distance */
|
||||||
|
|
||||||
|
/* Before timeout: still measuring */
|
||||||
|
for (uint32_t ms = 1; ms < ECHO_TIMEOUT_MS; ms++) {
|
||||||
|
if (sim.state == SIM_MEASURING) {
|
||||||
|
ASSERT(true, "Measuring before timeout");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* After timeout: should mark as error */
|
||||||
|
sim.state = SIM_MEASURING; /* Simulate stuck in measuring */
|
||||||
|
uint32_t elapsed = ECHO_TIMEOUT_MS + 1;
|
||||||
|
if (elapsed > ECHO_TIMEOUT_MS) {
|
||||||
|
ASSERT(true, "Timeout condition detected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Pulse Width Edge Cases ────────────────────────────────────────────────*/
|
||||||
|
|
||||||
|
void test_pulse_width_edge_cases(void)
|
||||||
|
{
|
||||||
|
TEST("Pulse width edge cases");
|
||||||
|
|
||||||
|
/* Minimum measurable: ~117µs (20mm) */
|
||||||
|
uint32_t pulse_min = (20 * US_TO_MM_DENOMINATOR) / US_TO_MM_NUMERATOR;
|
||||||
|
ASSERT(pulse_min >= 100, "Minimum pulse width reasonable (>100µs)");
|
||||||
|
|
||||||
|
/* Maximum measurable: ~29150µs (5000mm) */
|
||||||
|
uint32_t pulse_max = (5000 * US_TO_MM_DENOMINATOR) / US_TO_MM_NUMERATOR;
|
||||||
|
ASSERT(pulse_max < 65536, "Maximum pulse width fits in 16-bit timer");
|
||||||
|
|
||||||
|
/* Conversion accuracy at edge values (accounting for rounding) */
|
||||||
|
uint16_t dist_min = (pulse_min * US_TO_MM_NUMERATOR) / US_TO_MM_DENOMINATOR;
|
||||||
|
uint16_t dist_max = (pulse_max * US_TO_MM_NUMERATOR) / US_TO_MM_DENOMINATOR;
|
||||||
|
ASSERT(dist_min >= 19 && dist_min <= 21, "Minimum distance near 20mm");
|
||||||
|
ASSERT(dist_max == 5000, "Maximum distance roundtrips to 5000mm");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Multiple Trigger Tests ────────────────────────────────────────────────*/
|
||||||
|
|
||||||
|
void test_multiple_triggers(void)
|
||||||
|
{
|
||||||
|
TEST("Multiple sequential measurements");
|
||||||
|
|
||||||
|
/* First measurement */
|
||||||
|
sim.state = SIM_IDLE;
|
||||||
|
sim.distance_mm = 100;
|
||||||
|
sim_trigger(0);
|
||||||
|
for (uint32_t ms = 1; ms <= 20; ms++) {
|
||||||
|
sim_tick(ms);
|
||||||
|
if (sim.state == SIM_COMPLETE) break;
|
||||||
|
}
|
||||||
|
ASSERT(sim.result_distance == 100, "First measurement: 100mm");
|
||||||
|
|
||||||
|
/* Second measurement after first completes */
|
||||||
|
if (sim.state == SIM_COMPLETE) {
|
||||||
|
sim.state = SIM_IDLE;
|
||||||
|
sim.distance_mm = 2000;
|
||||||
|
sim_trigger(30);
|
||||||
|
for (uint32_t ms = 31; ms <= 60; ms++) {
|
||||||
|
sim_tick(ms);
|
||||||
|
if (sim.state == SIM_COMPLETE) break;
|
||||||
|
}
|
||||||
|
ASSERT(sim.result_distance == 2000, "Second measurement: 2000mm");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Third measurement */
|
||||||
|
if (sim.state == SIM_COMPLETE) {
|
||||||
|
sim.state = SIM_IDLE;
|
||||||
|
sim.distance_mm = 500;
|
||||||
|
sim_trigger(70);
|
||||||
|
for (uint32_t ms = 71; ms <= 100; ms++) {
|
||||||
|
sim_tick(ms);
|
||||||
|
if (sim.state == SIM_COMPLETE) break;
|
||||||
|
}
|
||||||
|
ASSERT(sim.result_distance == 500, "Third measurement: 500mm");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Main Test Runner ──────────────────────────────────────────────────────*/
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
printf("\n══════════════════════════════════════════════════════════════\n");
|
||||||
|
printf(" HC-SR04 Ultrasonic Sensor Driver — Unit Tests (Issue #243)\n");
|
||||||
|
printf("══════════════════════════════════════════════════════════════\n");
|
||||||
|
|
||||||
|
test_distance_conversion();
|
||||||
|
test_state_machine();
|
||||||
|
test_nonblocking_measurement();
|
||||||
|
test_range_validation();
|
||||||
|
test_timeout_detection();
|
||||||
|
test_pulse_width_edge_cases();
|
||||||
|
test_multiple_triggers();
|
||||||
|
|
||||||
|
printf("\n──────────────────────────────────────────────────────────────\n");
|
||||||
|
printf(" Results: %d/%d tests passed, %d failed\n", test_passed, test_count, test_failed);
|
||||||
|
printf("──────────────────────────────────────────────────────────────\n\n");
|
||||||
|
|
||||||
|
return (test_failed == 0) ? 0 : 1;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user