09 - Ultrasonic Ranging Application
This chapter describes the ultrasonic ranging application example — sonic_display — on the Pico-G1 expansion board. The application demonstrates how to drive an HC-SR04 ultrasonic module through GPIO interfaces for distance measurement, how to control an RGB LED to show different colors based on distance (similar to a parking sensor), and how to display the distance and status in real time on a TFT screen. It is an advanced example for learning GPIO timing measurement, pulse-width detection, and multi-GPIO coordinated control.
The application source code is located in the SDK directory source/app/09_sonic_display/. It provides a complete implementation of GPIO pulse-width measurement and RGB LED control, and is a valuable reference for learning embedded sensor applications and state-machine programming.
1 Application Overview
1.1 Features
- Ultrasonic ranging: drives an HC-SR04 module through GPIO interfaces for accurate distance measurement
- Pulse-width measurement: measures the ECHO pulse width with a high-precision clock to compute distance
- RGB parking sensor: automatically controls the RGB LED color based on the measured distance
- Real-time display: shows the distance, status, and color legend on the TFT screen
- Smart color algorithm:
- Blue: no echo / out of range (< 0 cm or > 4 m)
- Red: very close (< 20 cm, danger warning)
- Yellow: close (20~50 cm, caution)
- Green: safe distance (≥ 50 cm)
- Timeout protection: a 30 ms timeout prevents measurements from getting stuck
1.2 Technical Specifications
| Parameter | Value |
|---|---|
Sensor model | HC-SR04 (compatible with JSN-SR04T) |
TRIG interface | GPIO6_7 (trigger pulse output) |
ECHO interface | GPIO7_0 (pulse-width input) |
RGB_R interface | GPIO6_6 (red LED) |
RGB_G interface | GPIO6_5 (green LED) |
RGB_B interface | GPIO5_3 (blue LED) |
Ranging range | 2 cm~4 m |
Measurement accuracy | ±3 mm |
Trigger pulse | ≥10 μs (20 μs in this driver) |
Refresh interval | 300 ms (configurable) |
TFT display | ST7789 240×240 |
1.3 Test Case List
| index | Name | Test command | Expected result (success) | Possible causes of failure |
|---|---|---|---|---|
| 1 | Basic ranging | ./sonic_display | Screen shows distance, RGB LED changes color with distance | Ultrasonic connection failed, wrong GPIO config |
| 2 | Distance test | Place obstacles at various distances | Accurate distance values, correct RGB colors | Measurement accuracy deviation, improper ECHO voltage divider |
| 3 | RGB test | Cover the ultrasonic sensor by hand | RGB LED shows blue (no echo) | Wrong RGB GPIO config |
| 4 | Over-range test | Measure a distance > 4 m | Shows "out of range" status | Normal behavior, as designed |
1.4 Directory Structure
source/app/09_sonic_display/
├── Makefile # Build script
├── main.c # Main program
├── sonic.c # Ultrasonic driver implementation
├── sonic.h # Ultrasonic driver header
├── rgb.c # RGB LED control implementation
├── rgb.h # RGB LED control header
├── gpio_hal.c # GPIO HAL layer implementation
├── gpio_hal.h # GPIO HAL layer header
├── spi_hal.c # SPI HAL layer implementation
├── spi_hal.h # SPI HAL layer header
├── st7789.c # ST7789 driver implementation
├── st7789.h # ST7789 driver header
├── font8x16.h # 8×16 ASCII bitmap font
└── README.md # Documentation2 Hardware Connection
2.1 Pin Definitions
| Signal | On-board GPIO | Node | Description |
|---|---|---|---|
| TRIG | GPIO6_7 | /dev/gpiochip6 line7 | Trigger pulse output (≥10 μs) |
| ECHO | GPIO7_0 | /dev/gpiochip7 line0 | Echo pulse-width input (voltage divider required) |
| RGB_R | GPIO6_6 | /dev/gpiochip6 line6 | Red LED control (common cathode) |
| RGB_G | GPIO6_5 | /dev/gpiochip6 line5 | Green LED control (common cathode) |
| RGB_B | GPIO5_3 | /dev/gpiochip5 line3 | Blue LED control (common cathode) |
| VCC | 5V | — | The HC-SR04 must be powered at 5V |
| GND | GND | — | Ground |
2.2 Hardware Circuit
Ultrasonic module wiring:
Pico-G1 HC-SR04 Module
┌───────────┐ ┌──────────────┐
│ │ │ │
│ GPIO6_7 ──┼────── TRIG ───┤ INT │
│ │ │ │
│ GPIO7_0 ──┼────── ECHO ────┤ ECHO │
│ │ │ │
│ 5V ────┼─────────────┤ VCC │
│ │ │ │
│ GND ───┼─────────────┤ GND │
│ │ │ VCC ─── GND │
└───────────┘ └──────────────┘RGB LED wiring (common cathode):
Pico-G1 RGB LED Module
┌───────────┐ ┌──────────────┐
│ │ │ │
│ GPIO6_6 ──┼────── R ─────┤ │
│ │ │ │
│ GPIO6_5 ──┼────── G ─────┤ │
│ │ │ │
│ GPIO5_3 ──┼────── B ─────┤ │
│ │ │ ┌──────┤ │
│ 3.3V ───┼─────────────┤ GND │ │ │
│ │ └─────┘ │ │
└───────────┘ └──────────────┘
Common cathodeECHO signal voltage divider
The ECHO pin of the HC-SR04 outputs a 5V high level and must be divided down to 3.3V:
- Recommended divider:
ECHO → 10kΩ → GPIO7_0 → 20kΩ → GND - If you use a 3.3V ultrasonic module (e.g. JSN-SR04T), ECHO can be connected directly
Common-cathode RGB LED connection
- Common cathode: the LED lights up when the GPIO outputs high and goes off when it outputs low
- This example uses a common-cathode RGB LED with the common pin tied to GND
- Color combinations: red+green=yellow, red+blue=purple, green+blue=cyan, red+green+blue=white
2.3 Pin Multiplexing
Pin multiplexing that needs to be configured:
| pad | Physical address | Value | Description |
|---|---|---|---|
| TRIG (GPIO6_7) | 0x100C0068 | 0x1000 | func0(GPIO) |
| ECHO (GPIO7_0) | 0x100C006C | 0x1000 | func0(GPIO) + input enable |
| RGB_R (GPIO6_6) | 0x100C0064 | 0x1000 | func0(GPIO) |
| RGB_G (GPIO6_5) | 0x100C0060 | 0x1000 | func0(GPIO) |
| RGB_B (GPIO5_3) | 0x100C0030 | 0x1000 | func0(GPIO) |
All pins default to the GPIO function, so no IOCFG register changes are required.
3 Build and Deployment
3.1 Prerequisites
Before building this application, make sure the following preparations are done:
- SDK environment is set up: refer to Development Environment Setup to configure the cross-compilation toolchain and the SDK
- Hardware is connected: the HC-SR04 module and RGB LED are correctly wired to the corresponding GPIO pins
3.2 Build the Application
# Set the toolchain path
export PATH=$PATH:<SDK>/tools/linux/toolchains/arm-gcc12.2.0-linux-uclibceabi/bin
# Enter the example directory
cd <SDK>/source/app/09_sonic_display
# Build
make
# Clean
make clean3.3 Deploy to the Board
# Transfer to the development board with SCP
scp sonic_display root@<board_ip>:/usr/bin/
# Or download via TFTP
tftp -g -r sonic_display <board_ip>3.4 Run the Application
# Add execute permission
chmod +x /usr/bin/sonic_display
# Run the ultrasonic ranging example
/usr/bin/sonic_displayAfter the application starts, the TFT screen shows the distance value and the RGB LED changes color according to the distance, refreshing every 300 ms. Press Ctrl+C to exit.
3.5 Expected Output
Console output
/mnt # ./sonic_display
[sonic] pad 复用:GPIO6_7/GPIO7_0 -> func0(GPIO)
[sonic] pad 0x100C0068 -> 0x00001000
[sonic] pad 0x100C006C -> 0x00001000
[sonic] pad 0x100C0064 -> 0x00001000
[sonic] pad 0x100C0060 -> 0x00001000
[sonic] pad 0x100C0038 -> 0x00001000
[sonic] 初始化 SPI 屏(/dev/spidev2.0)...
[spi] pad 0x100C0028 -> 0x00001004
[spi] pad 0x100C002C -> 0x00001004
[spi] pad 0x100C0030 -> 0x00001000
[spi] pad 0x100C0020 -> 0x00001005
[spi] pad 0x100C001C -> 0x00001005
[spi] opening /dev/spidev2.0 ...
[spi] spidev opened, fd=3
[spi] spidev mode/bits/speed set (MODE3/8b/24MHz)
[spi] chardev request DC @ /dev/gpiochip4 line 5 ...
[spi] chardev request RES @ /dev/gpiochip4 line 4 ...
[spi] chardev request CS @ /dev/gpiochip5 line 1 ...
[spi] chardev-verify: DC=0(expect0) RES=1(expect1) CS=1(expect1) ==> OK(chardev 真驱动了引脚)
[spi] spi_hal_init done
[tft] init: SLPOUT
[tft] init: SLPOUT +120ms ok
[tft] init: config cmds ok
[tft] init: DISPON
[tft] init: DISPON ok
[tft] init: clear-flush start
[tft] flush #1 start
[tft] flush #1 done
[tft] init: clear-flush done
[sonic] 初始化 HC-SR04(TRIG/ECHO)...
[sonic] TRIG=/dev/gpiochip6 line7, ECHO=/dev/gpiochip7 line0 就绪。
[sonic] 初始化 RGB(R=6_6 G=6_5 B=5_3)...
[rgb] R=GPIO6_6, G=GPIO6_5, B=GPIO5_3 就绪(共阴)。
[sonic] 测距中,每 80ms 刷新,Ctrl+C 退出。
[sonic] 2.9 cmTFT screen display

RGB colors at different distances:
| Distance range | RGB color | TFT display | Description |
|---|---|---|---|
| < 0 cm | Blue | BLUE - no echo | Out of range or no echo |
| 0~20 cm | Red | RED - danger | Very close, warning |
| 20~50 cm | Yellow | YELLOW - caution | Close, attention |
| ≥ 50 cm | Green | GREEN - safe | Safe distance |
Fixed and variable parts
- Fixed parts: the display format and layout (fixed by the code)
- Variable parts: the distance value and RGB color status (updated on every refresh)
4 RGB Parking-Sensor Function
4.1 Color Algorithm
The RGB LED automatically shows different colors based on the distance, like a car parking sensor:
// Distance-to-color thresholds (cm)
#define RGB_NEAR_RED_CM 20.0f /* < 20 cm shows red */
#define RGB_MID_YELLOW_CM 50.0f /* < 50 cm shows yellow */
#define RGB_SAFE_GREEN_CM 50.0f /* ≥ 50 cm shows green */
const char *rgb_by_distance(float d_cm)
{
if (d < 0.0f) {
rgb_set(0, 0, 1); /* Blue: no echo / out of range */
return "BLUE";
}
if (d < RGB_NEAR_RED_CM) {
rgb_set(1, 0, 0); /* Red: very close */
return "RED";
}
if (d < RGB_MID_YELLOW_CM) {
rgb_set(1, 1, 0); /* Yellow: close */
return "YELLOW";
}
rgb_set(0, 1, 0); /* Green: safe */
return "GREEN";
}4.2 RGB Pin Configuration
The RGB LED uses three GPIOs to control the red, green, and blue channels:
// RGB GPIO handle structures
static gpio_handle_t g_r = {
.chip_path = "/dev/gpiochip6",
.line_offset = 6, // GPIO6_6 = R
.gpio_mode = GPIOHANDLE_REQUEST_OUTPUT,
.default_value = 0,
.consumer_label = "rgb-r",
};
static gpio_handle_t g_g = {
.chip_path = "/dev/gpiochip6",
.line_offset = 5, // GPIO6_5 = G
.gpio_mode = GPIOHANDLE_REQUEST_OUTPUT,
.default_value = 0,
.consumer_label = "rgb-g",
};
static gpio_handle_t g_b = {
.chip_path = "/dev/gpiochip5",
.line_offset = 3, // GPIO5_3 = B
.gpio_mode = GPIOHANDLE_REQUEST_OUTPUT,
.default_value = 0,
.consumer_label = "rgb-b",
};4.3 Color Combination Logic
Color combinations of the common-cathode RGB LED:
| R | G | B | Resulting color | Hex |
|---|---|---|---|---|
| 0 | 0 | 0 | Off | 0x000000 |
| 1 | 0 | 0 | Red | 0xFF0000 |
| 0 | 1 | 0 | Green | 0x00FF00 |
| 0 | 0 | 1 | Blue | 0x0000FF |
| 1 | 1 | 0 | Yellow | 0xFFFF00 |
| 1 | 0 | 1 | Purple | 0xFF00FF |
| 0 | 1 | 1 | Cyan | 0x00FFFF |
| 1 | 1 | 1 | White | 0xFFFFFF |
5 Ranging Principles
5.1 Ultrasonic Ranging Principle
The HC-SR04 ultrasonic ranging works on the pulse-echo principle:
- Trigger stage: the TRIG pin outputs a high-level trigger pulse of ≥10 μs
- Emission stage: the module emits 8 pulses of 40 kHz ultrasound internally (8 pulses, about 40 μs in total)
- Receive stage: the ECHO pin goes high, indicating the sound wave has been sent
- Echo stage: after the echo reflected by the obstacle arrives, the ECHO pin goes low
- Calculation stage: measure the duration of the ECHO high level and compute the distance
Distance formula:
Distance (cm) = ECHO high-level time (μs) / 58Note: the speed of sound is about 340 m/s, so a 1 cm round trip takes about 58 μs.
5.2 Pulse-Width Measurement
Pulse-width measurement using a high-precision clock:
// Measure the ECHO pulse width
struct timespec start, current;
int pulse_width_us;
// Wait for ECHO to go high
while (gpio_get_value(ECHO_GPIO) == 0) {
clock_gettime(CLOCK_MONOTONIC_RAW, &start);
if (is_timeout(start, 30000)) return -1; // 30 ms timeout protection
}
// Record the start time
clock_gettime(CLOCK_MONOTONIC_RAW, &start);
// Wait for ECHO to go low
while (gpio_get_value(ECHO_GPIO) == 1) {
clock_gettime(CLOCK_MONOTONIC_RAW, ¤t);
if (is_timeout(start, 30000)) return -1; // 30 ms timeout protection
}
// Compute the pulse width (microseconds)
pulse_width_us = (current.tv_sec - start.tv_sec) * 1000000 +
(current.tv_nsec - start.tv_nsec) / 1000;
// Compute the distance
float distance_cm = pulse_width_us / 58.0f;5.3 Timeout Protection
To keep a measurement from getting stuck, a 30 ms timeout is applied:
static int is_timeout(struct timespec start, int timeout_ms)
{
struct timespec current;
clock_gettime(CLOCK_MONOTONIC_RAW, ¤t);
long elapsed_us = (current.tv_sec - start.tv_sec) * 1000000 +
(current.tv_nsec - start.tv_nsec) / 1000;
return elapsed_us >= (timeout_ms * 1000);
}
// Usage inside a wait loop
while (gpio_get_value(ECHO_GPIO) == 0) {
clock_gettime(CLOCK_MONOTONIC_RAW, &start);
if (is_timeout(start, 30000)) {
return -1; // 30 ms timeout
}
}6 Key Programming Points
6.1 GPIO Input/Output Operations
TRIG trigger pulse:
// Output a 20 μs trigger pulse
gpio_set_value(TRIG_GPIO, 1); // Pull high
usleep(20); // Hold for 20 μs
gpio_set_value(TRIG_GPIO, 0); // Pull lowECHO pulse-width reading:
// Read the GPIO input state
int echo_state = gpio_get_value(ECHO_GPIO);6.2 Multi-GPIO RGB LED Control
Controlling 3 GPIOs simultaneously:
void rgb_set(int r, int g, int b)
{
gpio_set_value(&g_r, r ? 1 : 0);
gpio_set_value(&g_g, g ? 1 : 0);
gpio_set_value(&g_b, b ? 1 : 0);
}Initializing the RGB GPIOs:
int rgb_init(void)
{
if (gpio_handle_init(&g_r) < 0) return -1;
if (gpio_handle_init(&g_g) < 0) return -1;
if (gpio_handle_init(&g_b) < 0) return -1;
rgb_off(); // Turn all LEDs off initially
return 0;
}6.3 Error Handling and Filtering
Median of multiple measurements:
#define SAMPLE_COUNT 5
float measure_distance_filtered(void)
{
float samples[SAMPLE_COUNT];
// Take 5 consecutive measurements
for (int i = 0; i < SAMPLE_COUNT; i++) {
samples[i] = sonic_measure_once();
usleep(50000); // 50 ms interval
}
// Sort and take the median
qsort(samples, SAMPLE_COUNT, sizeof(float), cmp_float);
return samples[SAMPLE_COUNT / 2];
}7 Troubleshooting
| Problem | Possible cause | Solution |
|---|---|---|
| Shows "no echo" | No obstacle ahead, too close (<2 cm), ECHO not connected or poorly divided | Test in the 10~50 cm range, check the ECHO connection and divider circuit |
| Shows "out of range" | Obstacle beyond 4 m or sound wave disperses | Move the obstacle closer, ensure a suitable reflecting surface |
| RGB LED off | Wrong GPIO config, RGB LED not initialized | Check GPIO6_6/6_5/5_3 config, confirm rgb_init() runs |
| Wrong RGB color | Improper color algorithm parameters | Check the RGB_NEAR_RED_CM and RGB_MID_YELLOW_CM thresholds |
| Jumpy values | User-space polling jitter, near-distance measurement error | Add filtering, avoid very close measurements |
| Program hangs | ECHO never signals, timeout protection ineffective | Check the ultrasonic module supply and connections |
Measurement distance tips
For best measurement results:
- The obstacle area should be ≥ 10 cm × 10 cm
- Measure within 10 cm ~ 300 cm
- The obstacle surface should be flat or slightly curved
- Avoid sound-absorbing materials (e.g. sponge, foam) as obstacles
8 Advanced Features
8.1 Data Filtering
#define FILTER_WINDOW 5
typedef struct {
float buffer[FILTER_WINDOW];
int index;
} DistanceFilter;
float filter_add_sample(DistanceFilter *f, float new_value)
{
f->buffer[f->index] = new_value;
f->index = (f->index + 1) % FILTER_WINDOW;
// Sort and take the median
float temp[FILTER_WINDOW];
memcpy(temp, f->buffer, sizeof(temp));
qsort(temp, FILTER_WINDOW, sizeof(float), cmp_float);
return temp[FILTER_WINDOW / 2];
}8.2 Multi-Level Parking-Sensor Alerts
typedef enum {
ALERT_SAFE = 0, // ≥ 50 cm green
ALERT_ATTENTION, // 20~50 cm yellow
ALERT_DANGER, // 10~20 cm yellow-red flashing
ALERT_CRITICAL, // < 10 cm red flashing
} alert_level_t;
alert_level_t get_alert_level(float distance)
{
if (distance < 10.0f) return ALERT_CRITICAL;
if (distance < 20.0f) return ALERT_DANGER;
if (distance < 50.0f) return ALERT_ATTENTION;
return ALERT_SAFE;
}8.3 Multiple Ultrasonic Sensors
Multi-sensor data fusion:
#define SONIC_SENSOR_COUNT 3
typedef struct {
int trig_gpio;
int echo_gpio;
float position_offset;
} SonicSensor;
float multi_sonic_measure(SonicSensor *sensors, int count)
{
float distances[SONIC_SENSOR_COUNT];
for (int i = 0; i < count; i++) {
distances[i] = sonic_measure_sensor(sensors[i]);
}
// Take the minimum as the final distance
float min_distance = distances[0];
for (int i = 1; i < count; i++) {
if (distances[i] < min_distance) {
min_distance = distances[i];
}
}
return min_distance;
}