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

本文介绍了如何使用STM32开发板通过USART与思岚A1M8激光雷达进行通信,获取位置信息,并进行简单的数据处理。由于资源和性能限制,不能使用ROS,而是直接在STM32上实现简单地图扫描。作者通过串口通信协议获取雷达数据,处理后在LCD屏幕上显示,但由于屏幕分辨率和单片机处理速度问题,显示效果不佳,建议通过Excel进一步处理数据以获得更准确的雷达图。
摘要由CSDN通过智能技术生成

前言

一个朋友在做服务机器人项目,用到思岚的激光雷达,于是便把淘汰的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

  • 2
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

呐咯密密

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值