激光测距模块在嵌入式系统中应用广泛,STP-23作为一款高性价比的单点激光测距模块,通过串口输出数据,非常适合与STM32系列MCU配合使用。我最近在一个智能仓储项目中就用到了这个组合,实测下来稳定性相当不错。
硬件连接其实非常简单,STP-23模块通常有4个引脚:VCC、GND、TX和RX。由于我们只需要接收数据,所以只需要连接模块的TX到STM32的RX引脚即可。这里有个小细节要注意:STP-23的工作电压是5V,而STM32的IO口是3.3V电平,如果直接连接可能会损坏MCU。我的做法是使用一个简单的电平转换电路,或者选择支持3.3V电平的模块版本。
在硬件布局上,建议将激光模块与STM32尽量靠近,减少串口线的长度。我在第一次测试时就因为线缆过长导致数据丢包,后来缩短到15cm以内就再没出现过问题。另外,给模块供电时最好单独加一个100μF的电容,可以明显减少电源噪声对测距精度的影响。
使用CUBEMX配置STM32项目可以省去大量底层初始化工作,对于新手特别友好。打开CUBEMX后,首先选择正确的MCU型号,我这里用的是STM32F401CCU6,和原始文章一致。
时钟配置是第一个关键点。我建议先配置好时钟树,确保USART的时钟源正确。对于F4系列,通常使用APB2总线时钟,记得在Clock Configuration选项卡中检查USART1的时钟频率是否正确。
接下来是USART的配置:
还有一个容易忽略的地方:DMA配置。虽然本文用的是中断方式,但我建议同时配置好DMA,方便后续优化。在DMA Settings选项卡中添加USART1_RX的DMA通道,模式选择Circular,这样可以为以后改用DMA接收预留空间。
在正式处理激光数据前,建议先用普通串口打印验证硬件连接是否正确。我在usart.c中添加了标准库的printf支持:
c复制#include <stdio.h>
int fputc(int ch, FILE *f) {
HAL_UART_Transmit(&huart6, (uint8_t*)&ch, 1, 1000);
return ch;
}
记得在Project Manager选项卡中勾选"Use MicroLIB",否则printf会无法正常工作。然后在主循环中测试:
c复制printf("Initialization complete!\n");
HAL_Delay(1000);
如果收不到数据,建议按以下步骤排查:
我在调试时发现,某些USB转串口芯片在921600波特率下工作不稳定,换成FTDI芯片就解决了问题。这个坑值得注意。
STP-23模块的数据格式比较特殊,每帧包含12个测距点。我们需要在中断服务函数中解析这些数据。首先定义几个关键变量:
c复制uint16_t receive_cnt; // 接收计数器
uint16_t distance; // 计算后的距离值
在main.c的初始化部分,必须显式开启接收中断:
c复制__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);
中断服务函数的实现是整个项目的核心。我优化了原始代码的状态机结构,使其更易读:
c复制void USART1_IRQHandler(void) {
static uint8_t state = 0; // 状态机状态
static uint8_t crc = 0; // CRC校验值
static uint8_t point_cnt = 0; // 当前点数计数
uint8_t temp_data;
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE)) {
temp_data = (uint8_t)(huart1.Instance->DR & 0xFF);
__HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_RXNE);
// 状态机处理
switch(state) {
case 0: // 等待帧头
if(temp_data == 0x54) {
crc = CrcTable[temp_data];
state++;
}
break;
// 其他状态处理...
default:
if(state >= 6 && state < 42) {
// 处理12个测距点的数据
process_point_data(temp_data, &state, &crc, &point_cnt);
} else if(state >= 42 && state <= 46) {
// 处理帧尾数据
process_frame_tail(temp_data, &state, &crc);
}
}
}
HAL_UART_IRQHandler(&huart1);
}
STP-23模块每帧数据末尾都有一个CRC8校验字节。我们使用查表法进行校验,效率比实时计算高很多:
c复制static const uint8_t CrcTable[256] = {
0x00, 0x4d, 0x9a, 0xd7, 0x79, 0x34, 0xe3, 0xae,
// ...完整的CRC表
};
在数据校验通过后,还需要进行滤波处理。我采用了均值滤波结合无效数据剔除的方法:
c复制void data_process(void) {
static uint8_t frame_cnt = 0;
static uint16_t valid_count = 0;
static uint32_t distance_sum = 0;
for(int i=0; i<12; i++) {
if(Pack_Data.point[i].distance > 30 &&
Pack_Data.point[i].distance < 5000) { // 有效距离范围30-5000mm
valid_count++;
distance_sum += Pack_Data.point[i].distance;
}
}
if(++frame_cnt >= 5) { // 累计5帧数据计算一次平均值
if(valid_count > 0) {
distance = distance_sum / valid_count;
}
frame_cnt = 0;
valid_count = 0;
distance_sum = 0;
}
}
这种滤波方式在实际测试中表现很好,能有效抑制单点跳变。如果环境噪声较大,可以增加累计帧数或者改用卡尔曼滤波。
经过前面的基础实现后,还可以做一些优化提升性能:
实测时建议先用固定距离物体测试,比如放在距离模块1米的位置,观察输出是否稳定。我通常会用以下测试步骤:
在代码中加入自诊断功能也很实用:
c复制printf("Distance: %dmm | Temp: %.1fC | Points: %d\n",
distance,
Pack_Data.temperature/10.0,
valid_count);
这样不仅能看距离,还能监控模块温度和有效点数,对调试很有帮助。