基于STM32F103+思岚A1激光雷达的扫描仪

news2024/12/24 20:39:37

前言

一个朋友在做服务机器人项目,用到思岚的激光雷达,于是便把淘汰的A1M8雷达送我一个,本着拿到啥就玩啥的态度,必须整一波。其实激光雷达还是搭配ROS才能发挥最大的作用,奈何资源有限,实力不足,只能依靠STM32开发板做一个及其简陋的地图扫描。此次主要是根据雷达的手册,通过USART和雷达通信,获取位置信息,绘图功能因受条件限制,显示效果较差,可通过导出数据进行excle绘图。

思岚A1M8激光雷达简介

在这里插入图片描述
这款激光雷达属于低成本的360度激光扫描测距雷达,外置电机,使用皮带带动雷达转台转动,实现360度的测距扫描,电机的转速由MCU发送PWM控制。
外部系统通过 TTL 电平的 UART 串口信号与 RPLIDAR 测距核心进行通讯。通过本文档定义的通讯协议,外部系统可以实时获取 RPLIDAR 的扫描数据、设备信息、设备健康状态。并且通过相关命令调整 RPLIDAR 的工作模式。
按照不同的请求类型, RPLIDAR 具有三种不同的请求/应答模式:

  • 标准的单次请求-单次应答模式
    在这里插入图片描述
  • 单次请求-多次应答模式

在这里插入图片描述

  • 单次请求/无应答模式
    在这里插入图片描述
    对于停止扫描、重启测距核心这类请求命令, RPLIDAR 采用单次请求,但不做应答的通讯模式。此时外部系统需要在发送请求后等待一定的时间,待RPLIDAR 完成了上一次请求操作后方可继续执行下一次请求。否则第二次的请求将可能被 RPLIDAR 丢弃。
    在此次应用中,主要采用后两种请求/应答模式,使用单次请求-多次应答模式采集测距数据,使用单次请求/无应答模式停止采样,进行数据的处理。
    在单次请求-多次应答模式采集测距数据时,MCU发送采集指令,雷达会先回复一条起使应答报文,之后便会循环回复数据应答报文。

    在这里插入图片描述
    请求报文及起始应答数据格式如下:
    在这里插入图片描述
    在回复起始应答之后,雷达会循环回复测距数据。长度为5bytes.
    在这里插入图片描述
    在这里插入图片描述
    例如测距数据为 3E D5 16 77 06
    第一个字节:3E,二进制为:0011 1110。代表信号质量为0x0f。信号质量不为零代表数据有效,起始标志位为0,代表不是新的一圈,该标志位只有在新的一圈的第一帧数据才会置一,该圈内的其余数据改为依旧是0。
    第二个字节:D5,角度数据低七位。
    第三个字节:16,角度数据高八位,加上第二个字节的低七位等于166A,再右移一位得B35。实际角度=835/64=44°,该角度表示与雷达零度的顺时针偏移角度,如下图。
    第四个字节:77,距离数据低八位。
    第五个字节:06,距离角度高八位。则此时距离为0x0677/4 = 413mm。

    在这里插入图片描述
    激光雷达测试:
    接线:
    雷达 ------------ MCU
    GND----------->GND
    RX------------->TX
    TX------------->RX
    V5.0----------->5V
    GND----------->GND
    MOTOCTL---->PWM
    VMOTO------->5V
    首先测试使用串口助手进行数据采集,这里将MOTOCTL接到5V电源,直接以最高速度进行采样。串口助手发送A5 20,可以看到数据滚动。
    在这里插入图片描述
    其中开头的七位数据对应起始应答,后面每5个字节一组,对应测距数据。雷达无损坏,开始连接开发板调试。

二、MCU代码:

既然是USART通信,我们先初始化USART,使用串口接收中断接收数据。

void USART_Config(void)
{
        GPIO_InitTypeDef GPIO_InitStructure;
        USART_InitTypeDef USART_InitStructure;
        NVIC_InitTypeDef NVIC_InitStructure;
        // 打开串口GPIO的时钟
        DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE);
        
        // 打开串口外设的时钟
        DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE);

        // 将USART Tx的GPIO配置为推挽复用模式
        GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);

  // 将USART Rx的GPIO配置为浮空输入模式
        GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
        GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);
        
        //Usart1 NVIC 配置
        NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2 ;//抢占优先级3
        NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;                //子优先级3
        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;                        //IRQ通道使能
        NVIC_Init(&NVIC_InitStructure);        //根据指定的参数初始化VIC寄存器
        
        // 配置串口的工作参数
        // 配置波特率
        USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;
        // 配置 针数据字长
        USART_InitStructure.USART_WordLength = USART_WordLength_8b;
        // 配置停止位
        USART_InitStructure.USART_StopBits = USART_StopBits_1;
        // 配置校验位
        USART_InitStructure.USART_Parity = USART_Parity_No ;
        // 配置硬件流控制
        USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
        // 配置工作模式,收发一起
        USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
        // 完成串口的初始化配置
        USART_Init(DEBUG_USARTx, &USART_InitStructure);

        USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启接收中断
        USART_ClearFlag(USART1,USART_FLAG_TC|USART_FLAG_RXNE);
//    USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);  // 开启串口DMA接收
        // 使能串口
        USART_Cmd(DEBUG_USARTx, ENABLE);            
}

然后编写中断服务函数

void USART1_IRQHandler(void)                        //串口1中断服务程序
{

        if(USART_GetITStatus(DEBUG_USARTx,USART_IT_RXNE)!=RESET)
        {
                rxbuff[Res] = USART_ReceiveData(DEBUG_USARTx);
                Res++;
                if(Res==1807)
                {
                        USART_ITConfig(USART1, USART_IT_RXNE, DISABLE);//开启接收中断
                        USART_SendData(USART1,0xA5);
                        while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_TXE) == RESET);        
                        USART_SendData(USART1,0x25);
                        while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_TXE) == RESET);
                        Data_Processing();
                        Res=0;
                        ClearFlag=1;
                }
//                MYDMA_Enable(DMA1_Channel5);//开始一次DMA传输!
        }
}

在串口中断服务函数中,需要采集1807个数据(360个测距点*5字节+起始7个字节)。我采用全速采样,即MOTOCTL直接接5V,这里采集360个数据点其实不止一圈的数据,但是因为每个360度都有无效数据,多采集点可以使后期画图更完整。在提取数据使用EXCEL分析以后,全速转一圈大概采样258个点左右,这个数据无法固定,每一圈采样数均不一样。
在采集数据完成后我们需要关闭采样,因为STM32F103的数据处理能力并不理想,这里需要一定的时间,于是通过串口发送指令A5 25让雷达停止采样,同时调用函数Data_Processing();进行数据处理以及在屏幕上画点。这里要注意,雷达在停止采样前会将最后一帧数据发送完整,我们在发送停止指令的期间,雷达可能已经在准备下一帧数据,在发送完停止指令之后,可能会存在这一帧数据的最后一位未触发中断,但是串口的数据寄存器中已经保存了这位数据,且已经改变了标志位,所以在下一次启动采样时会导致收到的第一个数据是上一次未接收完的数据。这个在进行处理。
在此之前我们还需要一个触发采样的按键。按下按键后触发采样,为了保持持续采样,在串口接收中断关闭采样并处理完数据后,可在主循环中再次开启。

void KEY1_IRQHandler(void)
{
        u8 RX;
  //确保是否产生了EXTI Line中断
        if(EXTI_GetITStatus(KEY1_INT_EXTI_LINE) != RESET) 
        {
        
                USART_SendData(USART1,0xA5);
                while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_TXE) == RESET);        
                USART_SendData(USART1,0x20);
                while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_TXE) == RESET);                        
                USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启空闲中断
                Res=0;
    //清除中断标志位
                EXTI_ClearITPendingBit(KEY1_INT_EXTI_LINE);     
        }  
}

在这里插入图片描述
数据处理如下:

void Data_Processing(void)
{
        u16 i,j=7;
        u8 quality;

        for(i=0;i<360;i++)
        {
                quality = rxbuff[j]>>2;
                if(quality!=0)
                {
                        data_rage1 = rxbuff[j+2]<<8;
                        data_rage2 = rxbuff[j+1];
                        angle[i] = (data_rage1 | data_rage2)>>1;
                        angle[i] = angle[i];
                        data_rage1 = rxbuff[j+4]<<8;
                        data_rage2 = rxbuff[j+3];
                        distance[i] = (data_rage1|data_rage2);        
//                Usart_SendHalfWord(USART2,angle[i]);        
//                Usart_SendHalfWord(USART2,distance[i]);        
                }
                
                j = j+5;
        }

        if(i==360) 
        {
                LCD_Draw();
                i=0;
//                
        }
}

从串口缓存数组中取出角度值和距离值,保存在数组angle[]和distance[]中。当360个数据点处理完,调用画图函数进行屏幕绘制。

void LCD_Draw(void)
{
        u16 i;
         ILI9341_Clear(0,0,LCD_X_LENGTH,LCD_Y_LENGTH);        /* 清屏,显示全黑 */
        LCD_SetTextColor(RED);
        for(i=0;i<360;i++)
        {
                x=return_x(angle[i], distance[i]/scale);
                y=return_y(angle[i], distance[i]/scale);
//                ILI9341_DrawLine(120,160,x,y);
                ILI9341_SetPointPixel(x,y);
                /*为了点更清楚,在点周围画辅助点*/
                ILI9341_SetPointPixel(x+1,y+1);
                ILI9341_SetPointPixel(x-1,y-1);
                ILI9341_SetPointPixel(x-1,y+1);
                ILI9341_SetPointPixel(x+1,y-1);
                ILI9341_SetPointPixel(x+2,y+2);
                ILI9341_SetPointPixel(x-2,y-2);
                ILI9341_SetPointPixel(x-2,y+2);
                ILI9341_SetPointPixel(x+2,y-2);                
        }        
}

画点直接调用野火的库,其中参数scale为地图放大倍数,因为屏幕大小有限,为了适应不同大小的地图,使用该参数进行地图放大。
return_x,return_y函数是将测距点转换为屏幕坐标。原函数如下

//x坐标转换函数
//ang:0~359度数,    d:距离
//返回:x坐标0~239
float return_x(u16 ang, signed int d)
{
        float x;
        double ang_deg,dd;
        ang_deg = ang/64;
        dd = d/4;
        if(dd!=0)
        {
                if(ang_deg <= 90)
                {
                        
                        x = dd*sin(ang_deg)+120;//角度转换成弧度
                }
                else if((ang_deg > 90) && (ang_deg <= 180))
                {
                        
                        x = 120+dd*sin(ang_deg);
                }
                else if((ang_deg > 180) && (ang_deg <= 270))
                {

                        x = 120-dd*sin(ang_deg);
                }
                else if((ang_deg > 270) && (ang_deg <= 359))
                {

                        x = 120-dd*sin(ang_deg);
                }        
        
        }

        if(x > 239)
                x = 239;
        if(x < 0)
                x = 0;
        return x;
}
//y坐标转换函数
//ang:0~359度数,    d:距离
//返回:y坐标0~319
float return_y(u16 ang, signed int d)
{
        float y,dd;
        double ang_deg;
        ang_deg = ang/64;
        dd = d/4;
        if(dd!=0)
        {
                if(ang_deg <= 90)
                {

                        y = 160-dd*cos(ang_deg);//角度转换成弧度
                }
                else if((ang_deg > 90) && (ang_deg <= 180))
                {
                
                        y = dd*cos(ang_deg)+160;
                }
                else if((ang_deg > 180) && (ang_deg <= 270))
                {

                        y = dd*cos(ang_deg)+160;
                }
                else if((ang_deg > 270) && (ang_deg <= 359))
                {
                
                        y = 160-dd*cos(ang_deg);
                }        
        }

        if(y > 319)
                y = 319;
        if(y < 0)
                y = 0;
        return y;
}

此时在屏幕上便可绘制出雷达采样点
在这里插入图片描述

三、测试效果调试

从上文可以看出该屏幕的显示的扫描地图是圆形,但是我的房间却不是圆的。这个地图明显是有问题。但是无论无如何调整算法,显示到屏幕上的测距点总是不正确。分析得出大概问题是出在屏幕上,因为屏幕分辨率有限,测的的尺寸为了能在屏幕上显示,不得已将尺寸缩小几十倍,导致数据严重失真。于是我将测距数据导出研究。此次用已知大小的物料箱将雷达倒扣在里面。物料箱的尺寸大约为36cm*45cm。手头没有卷尺,用一个小尺子量的,所以只是大概值。
在这里插入图片描述
雷达位于箱子中间,那么到最短到箱壁两边的距离大概是18和22.5厘米。
测试开始
在这里插入图片描述
使用串口二将原始角度和距离值打印到串口助手:
在这里插入图片描述
使用world文档将数据整理:
在这里插入图片描述
然后复制数据到excle,进行数据处理,将角度和距离分别提取;
在这里插入图片描述
根据真实角度值选取一整圈距离数据(mm),插入雷达图:
在这里插入图片描述
此图因为有无效点,取出零点以及错误点后得到如下图。
在这里插入图片描述
以看到此时的雷达图很接近我们的箱子真实形状,距离大小也符合箱子尺寸。此时才可以算作成功,虽然屏幕任然无法完整显示扫描地图,但是数据的处理并无问题,单片机速度跟不上,屏幕分辨率也不够,难受啊。。
源码下载链接:https://download.csdn.net/download/dy_ngmm/87881266
采集数据下载:https://download.csdn.net/download/dy_ngmm/87881276

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/623798.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

STM32 USB CDC VPC

STM32 USB CDC VPC 关键字 STM32,STM32CubeMX,HAL库,USB,虚拟串口,串口不定长接收 1.简介 通过使用stm32cubemx&#xff0c;实现USB CDC虚拟串口&#xff0c;并与硬件串口进行数据传输&#xff0c;实现了硬件串口数据的不定长接收&#xff0c;以及USB虚拟串口超过64字节的数…

钉钉H5微应用基础学习

钉钉开发文档 一、使用调试工具——IDE&#xff1a; 1、先下载调试工具&#xff0c;并且新建一个企业内部应用。 如果需要管理员权限&#xff0c;可以自己创建一个企业。 &#xff08;tips&#xff1a;一定要屏蔽自己创建的企业的消息&#xff0c;不然消息很多&#xff09; 2…

Burpsuite超详细安装教程

概述 Burp Suite 是用于攻击web 应用程序的集成平台&#xff0c;包含了许多工具。Burp Suite为这些工具设计了许多接口&#xff0c;以加快攻击应用程序的过程。所有工具都共享一个请求&#xff0c;并能处理对应的HTTP 消息、持久性、认证、代理、日志、警报。 接下来我来给大…

Linux4.8Nginx Rewrite

文章目录 计算机系统5G云计算第六章 LINUX Nginx Rewrite一、Nginx Rewrite 概述1.常用的Nginx 正则表达式2.rewrite和location3.location4.实际网站使用中&#xff0c;至少有三个匹配规则定义5.rewrite6.rewrite 示例 计算机系统 5G云计算 第六章 LINUX Nginx Rewrite 一、…

看完一位毕业的拼多多“P9”级别员工以及他的四页半简历,我悟了

前几天在脉脉上看到一个热帖&#xff0c;是刚从PDD毕业的P9级别员工吴可发的&#xff0c;同时附上了他的简历&#xff0c;这个简历很有意思&#xff0c;基本上和国内互联网这十多年来的发展步骤重叠&#xff0c;能够反映出&#xff0c;在这样一个跌宕起伏的时代里&#xff0c;个…

一次有关 DNS 解析导致 APP 慢的问题探究

一、业务背景 HTTTPDNS AWS Router53 APP 使用 HTTPDNS&#xff0c; 为解决 DNS 解析生效慢&#xff0c; DNS 劫持等问题。 我们 IOS 和安卓都是使用了 HTTPDNS。 域名托管在 AWS Router53。 域名有多个解析(基于延迟)&#xff0c;为了解决就近接入。 示例配置 ai.baidu.c…

网易Java后端面经(一面)

这是网易的Java一面&#xff0c;问的都很基础。 1.session过期怎么处理&#xff1f; session过期通常指用户在一段时间内没有进行任何操作而导致session失效。针对这种情况&#xff0c;可以采取以下措施&#xff1a; 1. 前端提示用户session即将过期&#xff0c;提醒其重新登录…

JavaScript对象的增强知识

Object.defineProperty ◼ 在前面我们的属性都是直接定义在对象内部&#xff0c;或者直接添加到对象内部的&#xff1a;  但是这样来做的时候我们就不能对这个属性进行一些限制&#xff1a;比如这个属性是否是可以通过delete删除的&#xff1f;这个属性是否在for-in遍历的时候…

微信能取代对讲机吗?区别在哪?

对讲机和微信的区别在哪&#xff1f;为什么大家在通讯方面选择对讲机而不是微信&#xff1f; 微信作为社交软件在多个领域都有着广泛的应用&#xff0c;不过在对讲机行业也在讨论一个话题&#xff1a;微信能否取代对讲机&#xff1f;下面河南宝蓝小编就和大家聊聊这个话题。 …

基于redis实现秒杀并防止超卖

基于redis实现秒杀并防止超卖 为什么基于redis针对秒杀商品库存为一个的情况setnx代码实现测试 针对有多个库存的商品实现测试 为什么基于redis 因为所有redis的操作&#xff08;这里指的是key的操作&#xff0c;像备份落盘之类的另算&#xff09;都是单线程的&#xff0c;所以…

一文读懂:LoRA实现大模型LLM微调

LoRA大模型LLM微调 为什么要进行微调&#xff1f;LoRA思路提高权重更新效率选择低的秩 实现LoRALoRA在LLaMA实现 为什么要进行微调&#xff1f; 在快速发展的人工智能领域中&#xff0c;以高效和有效的方式使用大型语言模型变得越来越重要。 预训练的大型语言模型通常被称为优…

02-启动 Vue 项目

一. 学习目标 掌握 Vue 项目的启动 二. 学习内容 掌握 Vue 项目的启动 三. 学习过程 项目的启动也有两种方式&#xff0c;一种是通过图形界面启动&#xff0c;另一种是通过命令行启动。 1.图形界面 打开vscode编辑器&#xff0c;点击 1.文件 ——>打开文件夹&#xff0c…

springboot实现支付宝支付(沙箱环境)

springboot实现支付宝支付 1. 获取应用id,应用私钥和支付宝公钥2. 开始开发3. 内网穿透4. 测试支付功能 1. 获取应用id,应用私钥和支付宝公钥 进入支付宝控制台:https://open.alipay.com/develop/manage 找到沙箱 这里可以看到应用id 可以看到应用私钥和支付宝公钥,获取这…

Zoho:集成ChatGPT、开发大型语言模型,加紧布局AI+SaaS

在企业的数字化转型进程中&#xff0c;管理层和员工的数字化意识会随着建设的推进而不断提高&#xff0c;对于办公场景的数字化应用需求也不断产生。传统的办公系统建设中&#xff0c;系统的应用能力需要支撑越来越丰富的场景需求。 《今日人工智能》采访到Zoho中国VP兼SaaS事业…

【编程语言 · C语言 · for语句】

for 语句 C语言中&#xff0c;使用for语句也可以控制一个循环&#xff0c;并且在每一次循环时修改循环变量。在循环语句中&#xff0c;for语句的应用最为灵活&#xff0c;不仅可以用循环次数已经确定的情况&#xff0c;而且可以用于循环次数不确定而只给出循环结束条件的情况。…

jenkins构建pipline无法执行shell命令原因

问题表现 新的服务器上&#xff0c;新安装的jenkins&#xff0c;在上面创建了一个pipline项目&#xff0c;脚本里有shell命令&#xff0c;但是jenkins每次执行都卡住&#xff0c;经过尝试&#xff0c;无论多简单的命令都执行不了&#xff0c;cp&#xff0c;mv等都不行&#xf…

华为路由器:ospf协议三张表及邻居建立过程

说明&#xff1a;本篇接上一篇继续讲解 拓扑图 为了方便&#xff0c;我把R1/2/3/4/5的router id改成了回环网卡的IP。 ospf协议三张表 邻居表&#xff08;neighbortable&#xff09; OSPF用邻居机制来发现和维持路由的存在&#xff0c;邻居表存储了双向通信的邻居关系OSPF路…

矩形图:数据之美在图形中展现

在当今信息爆炸的时代&#xff0c;数据已经成为决策和洞察的重要基石&#xff0c;但海量的数据如果不经过整理和呈现&#xff0c;往往难以得出有意义的结论。这时候&#xff0c;可视化工具的作用就变得尤为重要了。在众多可视化形式中&#xff0c;矩形图以其简洁直观的特点受到…

团队管理之性能实施团队日志9

最近项目进入胶着状态。 混合场景在有些项目组里已经可以开始了&#xff0c;但还是有两三个项目组现在是完全没办法混合起来的。 本周计划是把基准测试、容量测试跑完&#xff0c;稳定性测试每个项目组至少能跑一遍。 但是从进度上来看&#xff0c;容量测试至少有四个系统不能跑…