Often, MCU developers might think, "I just want to read a distance value. Why make things so complex?" Indeed, obtaining a distance measurement from an HC-SR04 sensor can be easily achieved with a serial print, and a simple printf("%f", dist)
will do the job. However, when we start to seriously consider the stability of our measurements, merely looking at a list of numbers can be overwhelming. Furthermore, it becomes difficult to spot underlying patterns when occasional extreme measurement values appear.
This is why we often prefer to "turn data into a waveform." As the saying goes, "a picture is worth a thousand words." Visualizing continuously sampled results allows us to see the degree of jitter, periodic trends, or outliers at a single glance.
The APM32F402 microcontroller boasts rich peripheral resources and a high main frequency. Paired with the HC-SR04 ultrasonic module, it is well-suited for mid-to-short-range measurement tasks. Next, let's figure out how to "draw" this measurement data.
2. Waveform Output: All You Need is SerialPlot2.1 Why Choose SerialPlot?- Free and open-source, with a straightforward installation process (https://bitbucket.org/hyOzd/serialplot/src/default/).
- It can read serial port data in real-time and includes built-in waveform plotting features, saving the hassle of interfacing with MATLAB or other host computer software.
- With simple settings, it can acquire multiple channels of data simultaneously, for instance, displaying "raw distance" and "filtered distance" at the same time.
(1) Initialize the Serial Port: On the APM32F402, use a USART peripheral (e.g., USART1) and configure parameters like baud rate, data bits, and stop bits to match the USB-to-UART module on the PC. A baud rate of 115200 is commonly used.(2) Transmit Data: In the main loop, acquire data every N milliseconds (e.g., 20ms) and send it using printf()
. Note that the data format should be simple and consistent; use commas or spaces to separate multi-channel values, and end the line with \n
.(3) Receive Data with SerialPlot: Open SerialPlot, select the corresponding serial port and baud rate, and you can happily watch the data curve update in real-time.
Below is an example of "multi-channel" output. Anyone familiar with serial output will understand this logic. Compared to a basic setup, you'll notice the addition of outputs for various filtering results. We will add the filter code in the next section, but for now, let's look at the general usage.
int main(void)
{
USART_Config_T usartConfigStruct;
float rawDist = 0.0f; // Raw distance value
// (1) NVIC vector table and basic initialization
NVIC_ConfigVectorTable(NVIC_VECT_TAB_FLASH, 0x0000);
BOARD_LED_Config(LED3);
BOARD_Delay_Config();
/* (2) Configure USART */
USART_ConfigStructInit(&usartConfigStruct);
usartConfigStruct.baudRate = 115200;
usartConfigStruct.mode = USART_MODE_TX_RX;
usartConfigStruct.parity = USART_PARITY_NONE;
usartConfigStruct.stopBits = USART_STOP_BIT_1;
usartConfigStruct.wordLength = USART_WORD_LEN_8B;
usartConfigStruct.hardwareFlow = USART_HARDWARE_FLOW_NONE;
BOARD_COM_Config(COM1, &usartConfigStruct);
/* (3) Initialize HC-SR04 measurement module */
TMR_HCSR04_Init();
printf("APM32F402 & HC-SR04 Demo: Only rawDist output\r\n");
while (1)
{
// (4) Get raw distance from HC-SR04
rawDist = sonar_mm_tmr();
// (5) Print only rawDist
printf("%.2f\r\n", rawDist);
// (6) Toggle LED3 to indicate activity and delay
BOARD_LED_Toggle(LED3);
BOARD_Delay_Ms(20);
}
}
2.4 Setting up SerialPlotAfter launching the SerialPlot software, you can follow these steps to complete the basic configuration, ensuring the software correctly recognizes the data we output via the serial port and plots it as a waveform:
- Select the correct COM port: In the SerialPlot main interface, choose the serial device connected to your board (e.g., COM3 or COM4) from the dropdown menu.
- Configure Port Settings: Set the Baud Rate (commonly 115200), Data Bits (8), Stop Bits (1), and Parity (None) to match the USART parameters configured in our code.
Switch to the "Data Format" tab: The configuration here needs to match our code's output.
- Data Format: Select ASCII (because our code outputs text data via
printf("%f...\n")
). - Number of Channels: Set to 1 for now; we will add more channels later (as we are currently only outputting a single channel).
- Delimiter: Set to "comma". This should be consistent with using commas in our code for multi-channel output. (For single-channel output, the default newline delimiter also works, but it's best to use commas for future compatibility).
- Switch to the "Data Format" tab: The configuration here needs to match our code's output.Data Format: Select ASCII (because our code outputs text data via
printf("%f...\n")
).Number of Channels: Set to 1 for now; we will add more channels later (as we are currently only outputting a single channel).Delimiter: Set to "comma". This should be consistent with using commas in our code for multi-channel output. (For single-channel output, the default newline delimiter also works, but it's best to use commas for future compatibility).
Switch to the "Plot" tab: Here, you can customize the plotting parameters to make the graph easier to read.
- Channel Names: Give the current channel a clear name, such as "rawDist". Adjust the line color or other visual options. Different colors will make it easier to distinguish between channels later.
- Switch to the "Plot" tab: Here, you can customize the plotting parameters to make the graph easier to read.Channel Names: Give the current channel a clear name, such as "rawDist". Adjust the line color or other visual options. Different colors will make it easier to distinguish between channels later.
- Finally, click "Open" to open the COM port.
Once connected to the board with the code flashed, the SerialPlot interface will start refreshing the waveform!
As you excitedly open SerialPlot to watch the thrilling distance curve, you might find that the data, which you expected to be as "smooth" as a line from a math textbook, frequently jitters by ± a few millimeters and sometimes inexplicably spikes high or low.
- This is because the propagation of ultrasonic waves in the air is susceptible to environmental interference, especially air currents and multipath reflections.
- Alternatively, the target may be extremely close or far, making it difficult for the HC-SR04 itself to accurately capture the echo.
- And at the software level, the timer capture might have extreme errors. In short, there are many challenges.
When your system requires more precise and stable measurement results, you must introduce "filtering" to combat this noise and these jumps. Next, let's discuss several classic filtering methods—from the simplest moving average to the slightly smarter exponential smoothing, and finally, the elegant Kalman filter—and explore their respective pros and cons.
4.1 Mean Filter: The Most Straightforward "All-in-One" Approach- Algorithm Principle: Also known as Moving Average, it involves summing the last N measurements and dividing by N.
- Advantages: Simple to implement. Random noise over a short period is effectively canceled out.
- Disadvantages: It introduces latency when responding to abrupt signal changes. The larger the window, the more pronounced the delay; too small a window, and it fails to smooth effectively.
Simple Code Example (using a circular buffer):
#define MA_WINDOW_SIZE 5
typedef struct
{
float buffer[MA_WINDOW_SIZE];
uint32_t index;
float sum;
uint32_t count;
} MovingAverageFilter_t;
void MAFilter_Init(MovingAverageFilter_t* filter)
{
filter->sum = 0.0f;
filter->index = 0;
filter->count = 0;
// Initialize the buffer to 0
for(uint32_t i = 0; i < MA_WINDOW_SIZE; i++)
{
filter->buffer[i] = 0.0f;
}
}
float MAFilter_Update(MovingAverageFilter_t* filter, float newSample)
{
// Subtract the oldest sample from sum if buffer is full
if(filter->count >= MA_WINDOW_SIZE)
{
filter->sum -= filter->buffer[filter->index];
}
else
{
// If the buffer isn't full, just increase count
filter->count++;
}
// Add new sample to sum
filter->sum += newSample;
// Put new sample into buffer
filter->buffer[filter->index] = newSample;
// Update index (circular)
filter->index++;
if(filter->index >= MA_WINDOW_SIZE)
{
filter->index = 0;
}
// Calculate the average
float average = filter->sum / (float)(filter->count);
return average;
}
4.2 Exponential Smoothing: Giving New Data a Little "Special Treatment"When we want to use less memory and flexibly adjust the weights of "new data" versus "old data, " we can use "Exponential Smoothing." The update formula is often written as:
filtered(k) = α × newSample + (1 - α) × filtered(k-1)
- α ∈ (0, 1) is the smoothing factor. A large α results in a faster response, while a small α leads to smoother, more stable output.
- It also has lag, but it only requires storing the previous filtered value, making the code very concise.
Simple Example:
typedef struct
{
float alpha; // Smoothing factor
float prevFiltered; // Previous filtered value
uint8_t initFlag; // Initialization flag
} ExpSmoothFilter_t;
void ExpSmoothFilter_Init(ExpSmoothFilter_t* filter, float alpha, float initialVal)
{
filter->alpha = alpha;
filter->prevFiltered = initialVal;
filter->initFlag = 1;
}
float ExpSmoothFilter_Update(ExpSmoothFilter_t* filter, float newSample)
{
if(!filter->initFlag)
{
// If not initialized properly, we do it on the fly
filter->prevFiltered = newSample;
filter->initFlag = 1;
return newSample;
}
// filtered(k) = alpha * newSample + (1-alpha)*filtered(k-1)
float currentFiltered = filter->alpha * newSample +
(1.0f - filter->alpha) * filter->prevFiltered;
// Store the result for the next iteration
filter->prevFiltered = currentFiltered;
// Return filtered output
return currentFiltered;
}
4.3 Kalman Filter: Making Your Ranging Results "Elegant"In some scenarios, a mean or exponential smoothing filter cannot adequately balance smoothness and real-time responsiveness. This is where the Kalman Filter makes its grand entrance. It shines in high-precision fields like quadcopter flight control, robot localization, and VR/AR tracking.
It uses optimal state estimation theory. Given a known system model and noise statistical characteristics, it can simultaneously reduce random noise interference and maintain fast tracking of true value changes.
- Advantages: More adaptive, with a certain ability to suppress outliers.
- Disadvantages: Requires a proper noise model (e.g., Q, R), which can involve the "black art" of parameter tuning. One wrong move and the data might jitter like crazy or become extremely sluggish.
Example Code (Simple 1D Scenario):
void KalmanFilter_Init(KalmanFilter_t *kf, float initVal)
{
/*
* x = initVal
* p = 10000.0f (Initial large uncertainty)
* Q = 5.0f (Process noise: can be tuned up/down)
* R = 50.0f (Measurement noise: larger value means observation is less reliable)
*/
kf->x = initVal;
kf->p = 10000.0f;
kf->Q = 5.0f;
kf->R = 50.0f;
}
float KalmanFilter_Update(KalmanFilter_t *kf, float measurement)
{
/* 1) Prediction stage: x' = x, p' = p + Q */
float x_prime = kf->x;
float p_prime = kf->p + kf->Q;
/* 2) Update stage:
* K = p' / (p' + R)
* x = x' + K*(z - x')
* p = (1 - K)*p'
*/
float K = p_prime / (p_prime + kf->R);
kf->x = x_prime + K * (measurement - x_prime);
kf->p = (1.0f - K) * p_prime;
return kf->x;
}
float getFilteredDistance(float measurement)
{
/* Initialize the Kalman filter on the first call */
if (!s_kfInitFlag)
{
KalmanFilter_Init(&s_filter, 0.0f);
s_kfInitFlag = 1;
}
/* Use the incoming measurement to update the Kalman filter */
float filteredDist = KalmanFilter_Update(&s_filter, measurement);
/* Return the filtered distance */
return filteredDist;
}
5. Practical Comparison: Four Waveforms, Which is More Stable?In the main
loop from before, by calling the mean filter, exponential smoothing filter, and Kalman filter respectively, we can output four channels of data to SerialPlot at once:
- Raw distance
rawDist
- Kalman filtered
kalmanDist
- Mean filtered
maDist
- Exponentially smoothed
esDist
Then you will observe:
- The
rawDist
curve jumps around the most actively and occasionally produces outliers. - The
maDist
curve is noticeably smoother but is a step behind when the distance changes suddenly. esDist
also provides a smoothing effect, and by adjusting theα
value, you can fine-tune its sensitivity to new data.- With appropriate parameters,
kalmanDist
often achieves a good balance of noise suppression and fast response, but the relationship between Q and R must be well-tuned, or the result might backfire.
The APM32F402 uses an Arm® Cortex®-M4F core with a maximum frequency of up to 120MHz. It also includes an FPU (Floating Point Unit) and DSP instruction set. This gives it an edge over Cortex®-M3 and M0+ cores when frequent floating-point operations or digital signal processing (like filtering, Fourier transforms, control algorithms) are required. In other words, running various filtering algorithms on this "core" is highly efficient, improving real-time performance and reducing the overhead of software-based floating-point calculations.
- For small electronic projects where precision and real-time response are not overly strict, a mean filter or exponential smoothing filter can handle most noise scenarios.
- If the project operates in a complex environment that requires both real-time tracking and resistance to outliers, and you have some understanding of the system's motion or noise distribution, then the Kalman filter is the undisputed choice.
- Ultimately, the choice of filter should be based on specific requirements, resource constraints, and personal tuning habits. In the world of embedded development, "fit for purpose" is often better than "blindly pursuing the top configuration."
That's all for this sharing. Hopefully, it gives you some inspiration to make your ultrasonic data "behave" and helps you face fluctuating values with more confidence. Which filtering method do you think is best? Feel free to leave your thoughts in the comments.
Comments