资讯动态

自制低成本旋转激光雷达:STM32驱动舵机与TOF模块实现环境扫描

发布时间:2026/8/21 5:03:03 来源:尧图企业网站定制
最近在做一个机器人导航项目需要获取周围环境的精确距离信息市面上的成品激光雷达要么太贵要么体积太大。于是萌生了自己动手做一个的想法核心目标就是低成本、可定制、学习原理。经过一番折腾终于用常见的模块拼出了一个能360度旋转扫描、并在OLED屏幕上实时显示“雷达图”的迷你装置效果相当不错今天就和大家完整分享一下从原理到代码的全过程。无论你是嵌入式爱好者、机器人初学者还是想深入了解测距与数据可视化的开发者这篇文章都能带你走通整个流程。你将掌握如何用单片机驱动舵机与激光测距模块协同工作如何处理串口数据以及如何将极坐标的距离信息实时转换为直观的平面图像。文末还会附上完整的代码和接线图保证你可以复现。1. 背景与核心概念在开始动手之前我们先厘清几个关键概念这有助于理解我们到底在做一个什么东西。1.1 什么是激光雷达激光雷达LiDAR是“激光探测与测距”的缩写。它通过向目标发射激光束并接收从物体反射回来的光通过计算激光往返的时间来精确测量距离。高级的激光雷达还能通过旋转镜面或自身旋转实现对周围环境360度的扫描生成一组由角度和距离构成的点云数据从而构建出环境地图。1.2 我们的“自制版”与商业产品的区别我们自制的这个装置更准确地应该称为“单点旋转式激光测距仪”。它与动辄数千元、每秒数万点的商业多线激光雷达有本质区别原理简化我们使用一个固定的激光测距模块如TOF或三角测距通过一个舵机带动其旋转逐点采集数据。数据率低受限于舵机速度和模块采样率扫描一圈可能需要几秒到十几秒无法实时构建高速运动场景的地图。成本极低核心部件成本可以控制在百元以内非常适合原理学习、静态环境测绘或低速机器人避障等场景。1.3 核心组件与工作流程整个系统的工作流程可以概括为控制核心单片机如STM32、ESP32或Arduino负责总调度。角度控制舵机接收单片机发出的PWM信号精确旋转到指定角度。距离采集激光测距模块在某个角度上通过串口UART或I2C向单片机返回当前距离值。数据处理与显示单片机将收到的角度和距离数据从极坐标(θ, r)转换为OLED屏幕上的直角坐标(x, y)并绘制出来。可视化OLED屏幕实时显示扫描出的“雷达图”通常以扫描线或点阵的形式呈现。理解了这些我们就知道接下来需要攻克哪些技术点了舵机控制、串口通信、坐标转换和图形绘制。2. 硬件准备与开发环境搭建2.1 硬件清单以下是制作所需的核心硬件型号可作为参考同类产品均可替代组件推荐型号说明主控板STM32F103C8T6 (蓝桥杯板/最小系统板)性能足够外设丰富成本低。也可用Arduino Uno或ESP32。激光测距模块VL53L0X / VL53L1X / TOF10120推荐VL53L0XI2C通信精度和速度较好。本文以串口输出的某款TOF模块为例。舵机SG90 / MG90S180度舵机即可用于水平旋转。需注意扭矩是否足够带动模块。OLED显示屏0.96寸 SSD1306 (I2C接口)128x64分辨率足够显示雷达图。其他杜邦线、舵机支架、激光模块支架、电源5V/2A以上结构固定和供电所需。接线示意图核心连接[OLED] [STM32] [激光模块] [舵机] GND ---------- GND ------------- GND ------------- 棕色线(GND) VCC ---------- 3.3V/5V --------- VCC ------------- 红色线(VCC) SDA ---------- PB7 (I2C1_SDA) SCL ---------- PB6 (I2C1_SCL) [激光模块] TX ---------- PA3 (USART2_RX) [激光模块] RX ---------- PA2 (USART2_TX) [舵机] 信号线(橙色) ---- PA0 (TIM2_CH1用于输出PWM)注具体引脚需根据你的主控板型号和代码配置进行调整。2.2 软件开发环境IDEKeil uVision5 (for STM32) 或 Arduino IDE (for Arduino/ESP32)开发库对于STM32HAL库需要配置好USART2用于串口通信TIM2用于产生PWM驱动舵机I2C1用于驱动OLED。对于Arduino需要安装Adafruit_SSD1306和Adafruit_GFX库来驱动OLED使用Servo库驱动舵机使用SoftwareSerial或硬件串口与激光模块通信。版本说明本文代码示例基于STM32 HAL库。不同版本的HAL库或Arduino核心库在函数名上可能略有差异但核心逻辑相通。重点是理解原理代码可以灵活调整。3. 核心原理与驱动实现3.1 舵机角度控制原理舵机通过PWM脉冲宽度调制信号控制角度。对于常见的180度舵机通常对应一个周期为20ms50Hz的PWM波其中高电平的脉宽在0.5ms到2.5ms之间变化对应0度到180度。0.5ms高电平-- 0度1.5ms高电平-- 90度2.5ms高电平-- 180度在STM32中我们需要配置一个定时器如TIM2的PWM输出通道并计算对应的ARR自动重装载值和CCR捕获/比较寄存器值来产生特定脉宽。3.2 激光测距模块通信协议市面上常见的串口激光模块输出协议通常是简单的ASCII字符串或二进制帧。例如我们假设模块每秒输出一次数据格式为D:1234\r\n其中“1234”是以毫米为单位的距离值。 我们需要编写串口中断服务函数或轮询读取函数来解析这个字符串提取出有效的数字距离值。3.3 极坐标到平面坐标的转换这是将数据可视化的关键一步。雷达扫描得到的是极坐标数据角度θ(舵机角度) 和 半径r(测量距离)。 要在OLED的直角坐标系中画点需要转换x r * cos(θ) y r * sin(θ)但需要注意OLED的原点(0,0)通常在左上角而雷达图通常希望原点在屏幕中心。角度θ在代码中一般是弧度制需要将舵机角度0-180度转换为弧度0-π。距离r需要根据OLED屏幕的物理像素范围进行缩放否则远处的点会画到屏幕外。转换并偏移到屏幕中心的公式为// 假设屏幕中心为 (center_x, center_y) 例如 (64, 32) // scale 是距离缩放系数例如 0.1 表示 1mm 距离用 0.1 像素表示 float radian angle * 3.14159 / 180.0; // 角度转弧度 int x_pos center_x (int)(distance_mm * scale * cos(radian)); int y_pos center_y (int)(distance_mm * scale * sin(radian)); // 注意sin和cos函数可能需要使用数学库并处理浮点运算。4. 完整代码实现与分步解析下面我们以STM32 HAL库为例分解关键代码。为了清晰将代码分块说明。4.1 工程与引脚配置在STM32CubeMX中或直接修改代码进行如下配置SYS: Debug 选择 Serial Wire。RCC: HSE 选择 Crystal/Ceramic Resonator。I2C1: 模式选择 I2C保持默认设置用于OLED。USART2: 模式选择 Asynchronous波特率设置为激光模块的波特率如9600。TIM2: 选择内部时钟通道1选择 PWM Generation CH1。参数计算Prescaler (PSC) 72 - 1(假设系统时钟72MHz分频后1MHz)Counter Period (ARR) 20000 - 1(产生周期20ms的PWM波1MHz / 50Hz 20000)初始的Pulse (CCR1)可设为1500对应1.5ms脉宽90度。GPIO: 根据接线配置相关引脚。 生成代码后进入Keil进行编写。4.2 主要变量与OLED驱动首先包含必要的头文件并定义全局变量。/* 头文件 */ #include main.h #include stdio.h #include string.h #include math.h // 用于sin/cos计算 extern I2C_HandleTypeDef hi2c1; extern TIM_HandleTypeDef htim2; extern UART_HandleTypeDef huart2; /* 全局变量 */ uint8_t uart_rx_buffer[64]; // 串口接收缓冲区 uint8_t uart_rx_len 0; // 接收数据长度 volatile uint8_t distance_ready 0; // 距离数据就绪标志 uint16_t current_distance 0; // 当前测量距离mm uint16_t current_angle 0; // 当前舵机角度0-180 uint16_t scan_angle_step 2; // 扫描步进角度度越小点越密 /* OLED相关函数声明 (需自行实现或使用现有库) */ void OLED_Init(void); void OLED_Clear(void); void OLED_ShowPoint(uint8_t x, uint8_t y); void OLED_Refresh(void);注OLED_ShowPoint等函数需要你根据使用的OLED驱动芯片如SSD1306的底层驱动库来实现。网上有大量现成的HAL库驱动代码。4.3 舵机控制函数编写一个函数用于设置舵机到指定角度。/** * brief 设置舵机角度 * param angle: 目标角度 (0-180) * retval None */ void Servo_SetAngle(uint16_t angle) { if(angle 180) angle 180; // PWM脉宽计算0.5ms(0度) - 2.5ms(180度)对应CCR值 脉宽(us) * 定时器频率(MHz) // 定时器频率 72MHz / (PSC1) 1MHz 1MHz即1us计数一次。 // 因此CCR值 脉宽(us)。 0.5ms500, 2.5ms2500。 // 线性映射CCR 500 angle * (2000 / 180) uint16_t ccr_val 500 (angle * 2000) / 180; __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, ccr_val); }4.4 激光模块串口数据解析启用串口接收中断并在中断回调函数中解析数据。// 在main.c中重写串口接收中断回调函数 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(huart-Instance USART2) { uint8_t rx_byte; HAL_UART_Receive_IT(huart2, rx_byte, 1); // 重新开启中断 if(rx_byte \r || rx_byte \n) { // 收到结束符处理一行数据 if(uart_rx_len 0) { uart_rx_buffer[uart_rx_len] \0; // 添加字符串结束符 // 假设数据格式为 D:1234 if(strncmp((char*)uart_rx_buffer, D:, 2) 0) { sscanf((char*)uart_rx_buffer2, %hu, current_distance); distance_ready 1; // 标记新数据可用 } uart_rx_len 0; // 重置缓冲区索引 } } else { // 存储数据字节 if(uart_rx_len sizeof(uart_rx_buffer)-1) { uart_rx_buffer[uart_rx_len] rx_byte; } } } }在main函数初始化部分启动串口接收中断HAL_UART_Receive_IT(huart2, uart_rx_buffer[0], 1);4.5 核心扫描与显示逻辑在main函数的超级循环中实现扫描和显示的主逻辑。int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_I2C1_Init(); MX_USART2_UART_Init(); MX_TIM2_Init(); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1); // 启动PWM输出 OLED_Init(); OLED_Clear(); // 在OLED中心画一个十字或点作为雷达中心 for(int i60; i68; i) OLED_ShowPoint(i, 32); for(int j28; j36; j) OLED_ShowPoint(64, j); OLED_Refresh(); Servo_SetAngle(0); // 舵机归零 HAL_Delay(1000); while (1) { // 从0度到180度扫描 for(current_angle 0; current_angle 180; current_angle scan_angle_step) { // 1. 移动舵机到指定角度 Servo_SetAngle(current_angle); HAL_Delay(50); // 等待舵机稳定时间根据舵机速度调整 // 2. 等待并获取距离数据 distance_ready 0; // 可以在此处发送一个读取指令给激光模块如果模块需要 // 例如HAL_UART_Transmit(huart2, (uint8_t*)R\r\n, 3, 100); // 然后等待一小段时间或通过标志位判断数据接收完成 uint32_t timeout HAL_GetTick(); while(!distance_ready (HAL_GetTick() - timeout 200)) { // 等待串口中断解析数据超时200ms } // 3. 如果成功获取距离则在OLED上画点 if(distance_ready current_distance 0 current_distance 4000) { // 设置有效距离范围 // 坐标转换与缩放 float scale 0.05f; // 缩放系数需要根据实际测试调整 float rad current_angle * 3.14159f / 180.0f; int x 64 (int)(current_distance * scale * cosf(rad)); int y 32 (int)(current_distance * scale * sinf(rad)); // 确保坐标在屏幕范围内 if(x 0 x 128 y 0 y 64) { OLED_ShowPoint(x, y); } } // 4. 刷新OLED显示可以每扫描几个点刷新一次以提高速度 OLED_Refresh(); } // 扫描完一圈后可以清屏或保留历史轨迹 // OLED_Clear(); // 重新画中心十字... HAL_Delay(1000); // 等待一秒后开始下一轮扫描 } }5. 常见问题与调试技巧在制作过程中你可能会遇到以下问题这里提供排查思路问题现象可能原因排查与解决思路舵机不转动或乱转1. PWM信号线接错或接触不良。2. PWM频率或脉宽计算错误。3. 电源功率不足舵机启动电流大。1. 用示波器或逻辑分析仪检查PWM引脚输出波形看周期是否为20ms脉宽是否随角度变化。2. 检查代码中CCR值的计算公式。3. 使用独立5V电源给舵机供电并与单片机共地。OLED屏幕不显示或花屏1. I2C地址错误常见0x3C或0x3D。2. I2C引脚接错或上拉电阻缺失。3. 初始化序列或刷新函数有误。1. 用I2C扫描程序确认OLED地址。2. 检查接线SCL和SDA通常需要4.7k上拉电阻到VCC。3. 确保OLED初始化函数正确并调用了清屏和刷新函数。激光模块无数据返回1. 串口波特率不匹配。2. TX/RX接反。3. 模块未正确供电或需要初始化指令。1. 用USB转TTL工具连接模块用串口助手测试其默认输出确认波特率和数据格式。2. 核对接线模块TX接单片机RX模块RX接单片机TX。3. 查阅模块数据手册看是否需要发送特定指令启动测量。距离数据跳动大或不准确1. 被测物体表面特性黑色、透明、高反光。2. 环境光干扰强太阳光。3. 模块测量频率与舵机运动不匹配。1. 针对不同物体进行测试了解模块局限性。2. 尽量避免强光直射测距头。3. 增加舵机到位后的稳定延时或对连续多次测量取中值滤波。雷达图显示形状扭曲1. 坐标转换公式错误角度未转弧度。2. 屏幕缩放系数scale不合适。3. sin/cos函数使用错误角度制/弧度制。1. 打印出转换后的x, y坐标值与手动计算对比。2. 放置一个已知距离的物体在正前方90度调整scale使显示距离符合预期。3. 确认C语言数学库使用的是sinf/cosf弧度制浮点数。扫描速度非常慢1. 舵机转动速度慢SG90等普通舵机本身速度有限。2. 等待距离数据超时时间过长。3. OLED刷新过于频繁。1. 换用速度更快的舵机如DS3225。2. 优化代码让舵机转动和等待数据并行如使用中断和状态机。3. 改为扫描完一圈或一定点数后再统一刷新OLED。6. 优化与进阶玩法基础功能实现后可以从以下几个方面进行优化和扩展让你的迷你雷达更强大1. 性能优化状态机设计将“舵机转动”、“等待稳定”、“发送测距指令”、“等待数据”、“处理显示”拆分成不同状态避免使用HAL_Delay进行阻塞等待提高系统响应效率。数据滤波对同一角度进行多次测量使用中值滤波或均值滤波减少数据噪声。双缓冲显示在内存中构建完整的雷达图帧缓冲区扫描完一圈或半圈后再一次性刷新到OLED避免闪烁。2. 功能扩展增加扫描模式除了0-180度扫描可以实现0-360度扫描需要能连续旋转的舵机或使用步进电机。数据上传通过串口将角度和距离数据实时发送到电脑如Python PySerial在PC端用Matplotlib或Processing绘制更精美的动态雷达图或点云图。添加声光提示当检测到近距离障碍物时让LED闪烁或蜂鸣器报警实现简单的避障传感器功能。集成到ROS如果你使用ESP32或树莓派Pico可以尝试将数据封装成ROS的LaserScan消息与ROS机器人系统集成。3. 机械结构改进稳定支架使用3D打印或亚克力板制作一个稳固的支架减少舵机转动时的整体晃动提高测量一致性。滑环供电如果需要360度连续旋转可以考虑使用滑环来解决导线缠绕问题。4. 算法应用简单地图构建连续扫描多圈将历史点云数据保存下来可以勾勒出房间的大致轮廓。障碍物检测通过分析连续角度的距离突变可以识别出障碍物的边缘。这个自制激光雷达项目虽然简单但它串联了嵌入式开发中的多个核心技能点PWM控制、串口通信、传感器数据采集、坐标变换和图形显示。通过动手实践你对这些技术的理解会比单纯阅读文档深刻得多。希望这份详细的教程能帮你成功复现并激发你更多的改造灵感。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价