在项目中,经常使用到低电压小功率的步进电机,此类步进电机若采用驱动器控制不断成本高也过于复杂,我们可以直接使用场效应管或者达林顿管来实现对其的驱动。在本篇中,我们就来讨论一下基于ULN2003A达林顿管实现对步进电机的驱动。 1、功能概述 我们先来了解一下基本的功能。ULN2003A达林顿管为7个输出通道,当导通时该通道连接到负端,所以非常适合于驱动4相5线步进电机。 1.1、ULN2003A达林顿管 ULN2003A 器件是高电压大电流达林顿晶体管阵列。每 款器件均由7个NPN 达林顿对组成,这些达林顿对具有高压输出,带有用于开关感性负载的共阴极钳位二极管。 单个达林顿对的集电极电流额定值为500mA。将达林 顿对并联可以提供更高的电流。应用包括继电器驱动器、电锤驱动器、灯驱动器、显示驱动器(LED 和气体放电)、线路驱动器和逻辑缓冲器。其基本结构图如下: 1.2、步进电机基本原理 在我们的测试中,我们使用4相5线步进电机。所谓4相5线步进电机就是该电机具有4组线圈5根连接线,实际上可能不只5根线,但公共端不管抽出多少根线,实际状态与1根无异。 我们一般将这4组线圈记为A相、B相、C相和D相,当然,也可以用别的称呼,只要便于标记分别就好。4相5线步进电机一般采用单极性直流电源供电。只要对步进电机的各相绕组按合适的时序通电,就能使步进电机步进转动。一般电机都会提供控制表,具体如下所示: 结合ULN2003A结构和4相5线步进电机的驱动要求,我们可以设计ULN2003A达林顿管驱动4相5线步进电机的驱动电路。 1.3、步进电机驱动模式 步进电机的驱动虽然按照电机的驱动表就可以实现,但实际的驱动方式有多种,常见的如单波驱动方式、全步驱动方式、半步驱动方式以及微步驱动方式等。这里我们可以看一看前面三种比较简单的驱动方式。 单波驱动方式又称之为单四拍工作方式。此种方式按固定次序依次驱动每一个线圈以达到使电机转动的目的。其波形如下: 全步驱动方式又称之为双四拍工作方式。此种方式按固定次序依次驱动两组线圈以达到使电机转动的目的。其波形如下: 半步驱动方式又称之为八拍工作方式。此种模式实际上是前两种模式的组合,以固定的次序依次激励一组或两组线圈以达到驱动电机的目的。其波形如下: 上述波形即是在单波驱动方式、全步驱动方式以及半步驱动方式下使用示波器抓取的A相和C相的波形图,基本可以展示这几种驱动工作方式波形特征。 2、驱动设计与实现 我们已经了解了ULN2003A驱动4相5线步进电机的基本工作情况,接下来我们就需要据此来实现ULN2003A驱动4相5线步进电机驱动程序的设计与实现。 2.1、对象定义 我们依然是基于对象来实现相关的操作。所以我们首先要定义对象,出于适用性考虑,我们要定义对象的类型并将具体的对象实例化,接下来我们就来抽象对象类型和实例化对象的操作。 2.1.1、对象的抽象 对于一个对象最主要包括属性与方法两方面内容,所以我们先来考虑驱动一个步进电机对象具有哪些属性和方法,并抽象出较为通用的步进电机的对象类型。 首先,我们来考虑对象的属性情况。对电机的操作包括启停命令、方向命令、运行状态、实际运转方向、节拍数、周期等,这些信息控制电机的运转并表征其具体工作状态,所以我们将其作为对象的属性。驱动模式和运行模式用以配置电机的具体工作方式,所以我们也将其作为对象的属性,以完成对象的配置。 其次我们再来考虑对象的方法问题。对相位的具体操作与具体的硬件平台有关,根据对应的引脚定义相应的相位引脚。这依赖于具体的硬件和软件操作平台,所以我们将其定义为对象的方法。为了控制操作时序,我们需要延时处理,而延时操作函数同样依赖于具体的软硬件平台,所以我们也将其定义为对象的方法,通过回调函数的方式来实现。 根据上述对对象属性和方法的分析,我们可以定义步进电机对象的类型如下: /*定义步进电机对象类型*/ typedef struct StepperObject { uint8_t startStop; //启动停止命令 uint8_t runStatus; //运行状态 uint8_t directSet; //方向设定 uint8_t directRun; //当前方向 uint8_t beat; //当前节拍 uint8_t period; //速度控制周期 DriveModeType driveMode; //驱动模式 StepperModeType runMode; //运行模式 void (*PhaseAction)(uint8_t cmd); void (*Delayms)(uint32_t period); }StepperObjectType; 2.1.2、对象初始化 我们定义了对象类型,可以实现基于对象的操作,但定义的对象变量需要进行初始化才能让不同的对象按照我们的配置的方式去运行。所以在开始对象的使用之前我们先对其进行初始化,这就需要我们设计一个对象的初始化函数。 这个初始化函数,将构造一个具体的操作对象。对于步进电机来说,我们需要初始化其相关的参数,如:工作模式、运行模式以及相位操作函数等。具体的初始化函数如下: /*步进电机对象初始化*/ void StepperInitialization(StepperObjectType *stepper, //步进电机对象 DriveModeType driveMode, //驱动模式 StepperModeType runMode, //运行模式 uint8_t period, //速度控制周期 StepperPhaseActionType action, //相位操作回调函数 StepperDelaymsType delayms //延时操作回调函数 ) { if((stepper==NULL)||(action==NULL)||(delayms==NULL)) { return; } stepper->PhaseAction=action; stepper->Delayms=delayms; stepper->driveMode=driveMode; stepper->runMode=runMode; stepper->period=period>0?period:1; } 2.2、对象操作 接下来我们考虑对步进电机对象所要进行的操作问题。我们已经将相位的具体面向硬件平台的操作定义为对象的方法。我们需要实现对象在不同的模式下节拍操作的控制。具体实现如下: //步进电机节拍操作 static void StepperAction(StepperObjectType *stepper) { uint8_t Command[BEAT_NUM]={0x01,0x03,0x02,0x06,0x04,0x0C,0x08,0x09}; RunBeatType beat=BEAT_NUM; if(stepper->beat>=BEAT_NUM) { stepper->beat=0; if(stepper->driveMode==Full_Step) { stepper->beat=1; } } beat=stepper->directRun>0?((RunBeatType)(7-stepper->beat)):((RunBeatType)stepper->beat); if(beat>=BEAT_NUM) { return; } stepper->PhaseAction(Command[beat]); stepper->beat++; if((stepper->driveMode==Full_Step)||(stepper->driveMode==Single_Wave)) { stepper->beat++; } stepper->Delayms(stepper->period); } 3、驱动的应用 我们已经设计并实现了基于ULN2003A的步进电机驱动程序,接下来我们实现一个实例来验证这一驱动程序设定是否符合要求。 3.1、声明并初始化对象 在开始一切操作之前,首先我们需要一个对象。前面的设计中,我们已经定义了一个StepperObjectType对象类型,所以我们使用它定义一个对象变量。 StepperObjectType chamber; 定义了chamber对象变量之后,还没有办法使用,因为我们需要对其进行初始化。前面我们已经设计了对象初始化函数,我们需要使用这一函数来初始化chamber对象变量。初始化函数需要如下参数: StepperObjectType *stepper, //步进电机对象 DriveModeType driveMode, //驱动模式 StepperModeType runMode, //运行模式 uint8_t period, //速度控制周期 StepperPhaseActionType action, //相位操作回调函数 StepperDelaymsType delayms //延时操作回调函数 第1个参数为我们需要初始化的步进电机对象。而驱动模式、运行模式为枚举,根据实际使用要求输入即可。而速度周期为初始速度设定,只要不是0的整数就可以。主要需要考虑的是后面两个函数指针。其原型定义如下: typedef void (*StepperPhaseActionType)(uint8_t cmd); typedef void (*StepperDelaymsType)(uint32_t period); 根据函数指针的原型定义,以及我们项目中具体用到的硬件配置,我们可以实现相位操作函数为: /*步进电机相位操作*/ static void PhaseOperation(uint8_t cmd) { GPIO_PinState AP,BP,CP,DP; AP=(GPIO_PinState)(cmd&0x01); BP=(GPIO_PinState)((cmd>>1)&0x01); CP=(GPIO_PinState)((cmd>>2)&0x01); DP=(GPIO_PinState)((cmd>>3)&0x01); HAL_GPIO_WritePin(GPIOE, GPIO_PIN_9, BP); HAL_GPIO_WritePin(GPIOE, GPIO_PIN_11, DP); HAL_GPIO_WritePin(GPIOE, GPIO_PIN_13, AP); HAL_GPIO_WritePin(GPIOE, GPIO_PIN_14, CP); } 而延时函数我们直接使用HAL_Delay,所以我们就可以实现步进电机对象的初始化操作如下: /*步进电机对象初始化*/ StepperInitialization(&chamber, //步进电机对象 Half_Step, //驱动模式 Mode_Speed, //运行模式 10, //速度控制周期 PhaseOperation, //相位操作回调函数 HAL_Delay //延时操作回调函数 ); 3.2、基于对象进行操作 初始化之后,我们就可以使用该对象来实现我们想要的操作了。我们设计一个应用函数调用相关驱动实现操作。 我们可以实现位置控制模式如下: StepperPositionControl(&chamber,5000,Direct_CW); 我们可以实现速度控制模式如下: StepperSpeedControl(&chamber); 当然具体的操作模式需要在初始化函数中配置。
查看详情
#include <string.h> #include <stdio.h> #include <stdlib.h> class Light //电灯类 { public: void turnLight(int degree){ //调整灯光亮度,0表示关灯,100表示亮度最大 } }; class TV //电视机类 { public: void setChannel(int channel){ //调整电视频道 //0表示关机 //1表示开机并切换到1频道 } }; class Command //抽象命令类 { public: virtual void on()=0; virtual void off()=0; }; class RemoteController { protected: Command *commands[4];//四个遥控器按钮 public: void onPressButton(int button) { if (button%2 == 0) { commands[button]->on(); } else { commands[button]->off(); } } void setCommand(int button, Command *command) { commands[button] = command; } }; class LightCommand : public Command { protected: Light *light; public: LightCommand(Light *light){this->light = light;} void on(){light->turnLight(100);} void off(){light->turnLight(0);} }; class TVCommand : public Command { protected: TV *tv; public: TVCommand(TV *tv){this->tv = tv;} void on(){tv->setChannel(1);} void off(){tv->setChannel(0);} }; int main(int argc, char const *argv[]) { Light light; TV tv; LightCommand lightCommand(&light); TVCommand tvCommand(&tv); RemoteController remoteController; remoteController.setCommand(0, &lightCommand); remoteController.setCommand(1, &lightCommand); remoteController.setCommand(2, &tvCommand); remoteController.setCommand(3, &tvCommand); return 0; }
查看详情
红外线遥控目前在市面上用得比较的广泛,如遥控电视,遥控灯具,遥控玩具等等。 这里主要介绍一下红外遥控接收代码的编写,先看红外接收代码: 1 void user_gpio_init(void) 2 { 3 /*!< Output push-pull, low level, 10MHz */ 4 GPIO_Init(GPIOC, GPIO_PIN_LNIB, GPIO_MODE_OUT_PP_LOW_SLOW);//FI 0..3 5 GPIO_Init(GPIOC, GPIO_PIN_4, GPIO_MODE_OUT_PP_LOW_SLOW);//BI 6 /*!< Input pull-up, external interrupt */ 7 GPIO_Init(GPIOC, GPIO_PIN_7, GPIO_MODE_IN_PU_IT); //Data 8 /*@brief Set the external interrupt sensitivity of the selected port.*/ 9 /*!< Interrupt on Falling edge only */ 10 EXTI_SetExtIntSensitivity(EXTI_PORT_GPIOC, EXTI_SENSITIVITY_FALL_ONLY); 11 } 12 //interrupt routine 13 void portC_isr(void) 14 { 15 static u8 one_byte; 16 static u8 index; 17 static u8 bits_of_byte; 18 switch(IR_State) 19 { 20 case IDL: 21 { 22 time_125us=0; 23 IR_State++; 24 break; 25 } 26 case START: 27 { 28 if(time_125us>104)//ok , ready to receive data 29 { 30 time_125us=0; 31 one_byte = 0; 32 index=0; 33 bits_of_byte = 0; 34 IR_State++; 35 } 36 else IR_State--; 37 break; 38 } 39 case DATA: 40 { 41 if((time_125us<14)&&(time_125us>6))//0 42 { 43 one_byte <<=1; 44 bits_of_byte++; 45 } 46 else if((time_125us>14)&&(time_125us<23))//1 47 { 48 one_byte<<=1; 49 one_byte+=1; 50 bits_of_byte++; 51 } 52 else 53 { 54 IR_State = IDL; 55 } 56 time_125us=0; 57 if(bits_of_byte >= 8) // got one byte ready 58 { 59 g_u8arr_buf[index++]=one_byte; 60 time_release_125us=0; 61 if(index>=4) 62 { 63 //__disable_interrupt(); 64 if ((g_u8arr_buf[3]==0x13)&&(g_u8arr_buf[2]==0xEC)&&(g_u8arr_buf[1]==0xff)&&g_u8arr_buf[0]==0x00)//0x00FFEC13 65 { 66 if(g_motor_busy!=0x00) 67 { 68 MotorStop; 69 g_motor_busy=0; 70 } 71 } 72 else if ((g_u8arr_buf[3]==0x53)&&(g_u8arr_buf[2]==0xAC)&&(g_u8arr_buf[1]==0xff)&&g_u8arr_buf[0]==0x00)//0x00FFAC53 73 { 74 //time_release_125us=0; 75 if(g_motor_busy!=0x5a) 76 { 77 MotorForward; 78 g_motor_busy=0x5a; 79 } 80 } 81 else if ((g_u8arr_buf[3]==0x33)&&(g_u8arr_buf[2]==0xCC)&&(g_u8arr_buf[1]==0xff)&&g_u8arr_buf[0]==0x00)//0x00FFCC33 82 { 83 //time_release_125us=0; 84 if(g_motor_busy!=0x3c) 85 { 86 MotorBackward; 87 g_motor_busy=0x3c; 88 } 89 } 90 else 91 { 92 if(g_motor_busy!=0x00) 93 { 94 MotorStop; 95 g_motor_busy=0; 96 } 97 } 98 IR_State = IDL; 99 //__enable_interrupt(); 100 } 101 bits_of_byte=0; 102 one_byte=0; 103 } 104 break; 105 } 106 default: 107 { 108 IR_State = IDL; 109 break; 110 } 111 } 112 } 红外编码常用格式为:报头为13.5ms信号,之后才开始传送数据。数据的编码:1ms左右代表0或者1, 2ms左右代表1或者0。这个主要看自己的定义了。 这个算法的重点是调好定时器的精度,以及红外线连续读取的灵敏度。
查看详情
上淘宝买了两个红外线模块,一个是接收器,另一个是发送器。 问了卖家,没有资料,但是根据电路板上打印的信息,似乎是标准的模块。于是先插上树莓派试试看。 (update:上图的发射器没有三极管,所以只能做到1-2m) 看宝贝描述,这个模块是给Arduino用的,于是搜索了一下Arduino相关的资料,发现github上有 Arduino-IRremote这个项目,里面有各种协议包括NEC红外协议的实现。 举个例子,代码里面发送NEC的实现: 这里先发送一个头部,然后按比特发送数据, 比特为1发560us的PWM,然后等待1690us 比特位0的时候发送560us的PWM,然后等待560us 最后发送一个560us的PWM结束 手头上有一块树莓派和一个美的空调,所以先用这两个硬件下手,实际上使用STM32会更方便,因为树莓派自带了操作系统,控制硬件没有STM32这种单片机方便。后续准备将程序移植到STM32上面。 解析器 要知道美的空调的编码,首先要做一个解析器。实际上,使用逻辑分析仪也可以得知。一开始并没有拆开遥控器看看,先用IR接收器做了一个解析器。 做解析器的时候首先要了解红外头的工作原理,首先它里面有一个滤波器,有38K数据的时候输出'1',没有38K数据的时候输出'0'。外部供电3.3V。接下来就要对输入的数据进行处理了。 首先对其产生的跳变进行响应,这里就注册了一个中断,INT_EDGE_BOTH表示不论下降沿和上升沿都进行响应,这样就可以采集到数据了: wiringPiISR(IR_INPUT, INT_EDGE_BOTH, &ir_int) 接下来要对响应做计数。一开始用gettimeofday测试了一下,并不准确,查资料得知树莓派的ARM芯片内部有一个1MHZ的时钟可以用来做计数器。不过因为树莓派有linux操作系统,直接访问时钟地址是不行的。 首先要打开datasheet(网上可以搜到) BCM2835-ARM-Peripherals.pdf 里面提到了两种timer: 其中System Timer的定义 ARM timer的定义: 看它的描述还是使用System Timer比较好。 首先要知道System Timer的地址。 这里写的是 0x7E003000,但是看图说话(第5页),还是要转换成为0x20003000: 转换好之后就要解决怎么用了。首先想到的就是写一个驱动,不过写驱动需要花一些时间调试,搞不好kernel搞挂了,为了简单一点,这里有一个比较portable的办法,就是使用mmap直接读取/dev/mem的信息。详情见代码。 于是根据这个思路作了一个获取时间间隔的一个API: long long int get_timer_diff() { long long int ret; ret = *timer - prev_timer; prev_timer = *timer; return ret; } 这里为什么要用long long?因为这个timer是64bit的。 解析分为三步:1.解析波形,2.解析bit,3.解析Byte 解析完成之后就拿到我需要的东西了。 拿着遥控器对着红外头测试,发现美的空调的格式如下: 0xb2,0x4d,0xf5,0x0a,0xa5,0x5a 接下来查找了网上的资料,发现它的格式实际上是这个样子的: http://www.geek-workshop.com/thread-5707-1-1.html 发射器 接下来做一个发射器,俗话说,上山容易下山难,编码容易解码难,过了解码这个步骤,编码也挺容易了,不过做发射器的时候还是有一些问题需要考虑的。 做发射器的时候需要打开树莓派的PWM,并且使用1MHZ的时钟做一个API: void delay_us(int us) { long long int prev_timer; prev_timer = *timer; while (*timer - prev_timer < us) ; } 这样就可以逐个将数据 这样就可以逐个将数据发送出去了。其中开关PWM的时候发现一个问题,关闭PWM倒是很快,开启PWM有延时。于是在开启之前将关闭PWM的时间缩短解决。如果能直接控制UART,我想可以用UART模拟一样的波形,利用RTS-CTS快速关断,比PWM的效果好。 用函数开关PWM有200us的延时,这里是要考虑下的。开的时候是200ms之后就打开了, 关的时候会立马关闭,但是这个过程还是会延时200us。 目前的结果是,用两个IR模块对测,解析器也能解析出正确的结果,空调也能被控制。 波形 抓到的遥控器波形(管脚端),手工查看,得知: 0110010 01001101 00011111 11100000 01001000 10110111L (同一个波形发两次) 第一个bit是MSB: B2 4D 1F E0 48 B7 屏幕上显示的是24度,和前面提到的资料一致。 遥控器的波形并不完美,我自己产生的波形可以完美的多: 现在已经可以控制美的空调了,空调被控制的时候也会产生‘滴’的声音。不过控制距离只有1米, 可能是模块的电流比较小导致的。 【0817更新】 通过自己搭电路,已经可以解决距离太小的问题,解决办法是增加一个三极管,增大红外管的电流, 另外将图中的100欧姆换成10欧姆就可以了。 面包板搭建图如下:
查看详情
多传感器融合(一) 一.概述 “传感器融合技术”号称自动驾驶中的核心技术。 传感器是汽车感知周围的环境的硬件基础,在实现自动驾驶的各个阶段都必不可少。 自动驾驶离不开感知层、控制层和执行层的相互配合。摄像头、雷达等传感器获取图像、距离、速度等信息,扮演眼睛、耳朵的角色。 控制模块分析处理信息,并进行判断、下达指令,扮演大脑的角色。车身各部件负责执行指令,扮演手脚的角色。而环境感知是这一切的基础, 因此传感器对于自动驾驶不可或缺。 二.多传感器融合的必要性 为什么一定要多传感器融合呢?主要是扬长避短、冗余设计,提高整车安全系数。多传感器融合系统所实现的功能要远超这些独立系统能够实现的功能总和。使用不同的传感器种类可以在某一种传感器全都出现故障的环境条件下,额外提供一定冗余度。这种错误或故障可能是由自然原因(诸如一团浓雾)或是人为现象(例如对摄像头或雷达的电子干扰或人为干扰)导致。各传感器优缺点如下: 相机:对目标的颜色和纹理比较敏感,可以完成目标分类、检测、分割、识别等任务,但是不能得到精确的探测距离,而且易受光照、天气条件的影响。 LiDAR:可以获得目标精确的3D信息,检测范围也能够到达150米。对光照不敏感,晚上也可以正常工作。但是角分辨率大,目标稀疏,无法获得目标纹理,分类不准,而且在雨、雾、雪等恶劣天气中,性能会下降。对扬尘、水雾也比较敏感,易产生噪点。 radar:可以提供精确的距离和速度信息,探测距离也比较远,可以全天候工作,但分辨率较低,无法提供物体高度信息。 相关传感器对比如下表: 三.多传感器融合的先决条件 众多的传感器装在同一辆车上,如nuscenes中使用了6个camera、1个lidar、5个radar,使用同一个系统来采集并处理数据,为了将他们规范,我们需要对这些传感器统一坐标系和时钟,目的就是为了实现三同一不同:同一个目标在同一个时刻出现在不同类别的传感器的同一个世界坐标处。 统一时钟 在这里要做的就是同步不同传感器的时间戳: GPS时间戳的时间同步方法: 这个需要看传感的硬件是否支持该种方法,如果支持则传感器给出的数据包会有全局的时间戳,这些时间戳以GPS为基准,这样就使用了相同的时钟,而非各自传感器的时钟了。 但是还有一个问题,不同传感器的数据频率是不同的,如lidar为10Hz,camera为25/30Hz,那不同传感器之间的数据还是存在延迟,如下图所示。虽然可以通过找相邻时间戳的方法找到最近帧,但是如果两个时间戳差距较大,障碍物又在移动,最终会导致较大的同步误差。 如图:在 T1 时刻,sensor 2产生了一个数据,如果要进行时间同步,我们需要查找对应时刻的sensor 1和sensor 3的数据,而实际查找的方式就是找出与sensr 2时间差最近的传感器数据包。 硬同步方法:这种方法可以缓解查找时间戳造成的误差现象。该方法可以以激光雷达作为触发其它传感器的源头,当激光雷达转到某个角度时,才触发该角度的摄像头,这可以大大减少时间差的问题。这套时间同步方案可以做到硬件中,这样可以大大降低同步误差,提高数据对齐效果。 统一坐标系 统一坐标系有两步,一是运动补偿,二是传感器标定。 运动补偿主要针对长周期的传感器,如lidar,周期为100ms。由于所有的传感器都装在车上,车是运动的刚体。因此传感器在采集数据时,周期开始的时间点和结束时间点车辆是处于不同位置的,导致不同时刻采集的数据所处坐标系不同,因此需要根据车体的运动对传感器采集的数据进行运动补偿。如下图所示:虚线部分可以认为是世界坐标系,红色点代表一个静态的障碍物,在坐标系中有一个稳定的坐标(5,5)。蓝色部分代表自动驾驶车自己的局部坐标系,也就是说世界坐标系的(4,0)为局部坐标系的原点。在T+1时刻,这个局部坐标系移动到了(6,0)的位置,也就是自动驾驶车沿着X方向向前移动了2。也就是说,在T时刻,障碍物的在局部坐标系下的坐标是(1,5),而在T+1时刻,它的坐标变为了(-1,5)。 这个问题解决起来比较简单,因为自动驾驶车拥有比较准确的实时定位信息,它可提供T和T+1两个时刻内,车本身的姿态差距,利用姿态差,我们就可以比较容易补偿自身移动了多少。 传感器标定分为内参标定和外参标定,内参标定,解决的是单独的每个传感器与世界坐标系间的变换;外参标定是在世界坐标系下,解决的不同传感器间的变换。传感器外参校准依赖于传感器的精确内参校准。 四.融合方法 经常可以看到的不同的融合方法,这里仅做简单介绍,后续会专门介绍相关方法。 经过以上几步,可以拿到的信息有:做好运动补偿及时间同步的传感器源数据、传感器内参、传感器外参,有了这些信息后,我们可以做相应的融合方法了。到底如何做呢?下面举两个例子: 相机和lidar融合:激光雷达数据是包含了明确的(x,y,z)数据的3D观测,通过标定参数与照相机本身的内参,多传感器深度融合可以实现把3D点投到图像上,图像上的某些像素也就打上了深度信息,帮助感知系统进行基于图像的分割或者训练深度学习模型。 毫米波雷达和激光雷达融合:毫米波雷达和激光雷达的融合方式比较简单。在笛卡尔坐标系下,它们拥有完整的( x,y )方向的信息。因此在笛卡尔坐标系下,激光雷达和毫米波雷达可以实现基于距离的融合。另外,毫米波雷达还可以探测到障碍物速度,而激光雷达通过位置的追踪,也会得到对障碍物速度的估计,对这些速度的信息进行融合,更能帮助筛选错误的匹配候选集。 根据数据在整个流程中融合的不同位置,可以分为前融合和后融合。 前融合 如上图所示,在原始层把数据都融合在一起,融合好的数据就好比是一个Super传感器,而且这个传感器不仅有能力可以看到radar,还有能力可以看到摄像头或者RGB,也有能力看到LiDAR的三维信息,就好比是一双超级眼睛。方法上只有一个感知的算法,对融合后的多维综合数据进行感知。 后融合 如上图所示,每个传感器各自独立处理生成的目标数据;每个传感器都有自己独立的感知,比如激光雷达有激光雷达的感知,摄像头有摄像头的感知,毫米波雷达也会做出自己的感知。当所有传感器完成目标数据(如目标检测、目标速度预测)生成后,再使用一些传统方法融合所有传感器的结果,得到最终结果。 前融合,还是后融合? 到底哪种融合方式好呢?这里举个例子:假设在你手上有一个手机,激光雷达只能看到手机的一个角,摄像头只能看到第二个角,而毫米波雷达可以看到第三个角,那么大家可以想象,如果使用后融合算法,由于每个传感器只能看到它的一部分,这个物体非常有可能不被识别到,最终会被滤掉。而在前融合中,由于它是集合了所有的数据,也就相当于可以看到这个手机的三个角,那对于前融合来说,是非常容易能够识别出这是一台手机的。 五.解决GPS误差多等问题 传感器融合技术就是给汽车导航的,用来解决GPS导航误差多的问题。 GPS误差多,传感器融合技术来帮忙 都知道GPS是当前行车定位不可或缺的技术,但是由于GPS的误差、多路径,以及更新频率低等问题,不可以只依赖于GPS进行定位。相反,民用传感器拥有很高的更新频率,可以跟GPS形成很好的互补。使用传感器融合技术,可以融合GPS与惯性传感器数据,各取所长,以达到较好的定位效果。 简单的传感器融合技术 简单地说,传感器融合就是将多个传感器获取的数据、信息集中在一起综合分析以便更加准确可靠地描述外界环境,从而提高系统决策的正确性。 传感器各有优劣,难以互相替代,未来要实现自动驾驶,是一定需要多个传感器相互配合共同构成汽车的感知系统的。不同传感器的原理、功能各不相同,在不同的使用场景里可以发挥各自优势。 多传感器融合是人工智能未来趋势 多个同类或不同类传感器分别获得不同局部和类别的信息,这些信息之间可能相互补充,也可能存在冗余和矛盾,而控制中心最终只能下达唯一正确的指令,这就要求控制中心必须对多个传感器所得到的信息进行融合,综合判断。 随着机器人技术的不断发展,智能化已成为机器人技术的发展趋势,而传感器技术则是实现智能化的基础之一。 多传感器融合技术理念 由于单一传感器获得的信息有限,且还要受到自身品质和性能的影响,因此智能机器人通常配有数量众多的不同类型的传感器,以满足探测和数据采集的需要。 若对各传感器采集的信息进行单独、孤立地处理,不仅会导致信息处理工作量的增加,而且,割断了各传感器信息间的内在联系,丢失了信息经有机组合后可能蕴含的有关环境特征,造成信息资源的浪费,甚至可能导致决策失误。为了解决上述问题人们提出了多传感器融合技术。 多传感器融合又称多传感器信息融合,有时也称作多传感器数据融合,于1973年在美国国防部资助开发的声纳信号处理系统中被首次提出,它是对多种信息的获取、表示及其内在联系进行综合处理和优化的技术。它从多信息的视角进行处理及综合,得到各种信息的内在联系和规律,从而剔除无用的和错误的信息,保留正确的和有用的成分,最终实现信息的优化,也为智能信息处理技术的研究提供了新的观念。 六.到底有多精确 多传感器融合技术有多精确 简单的传感器融合不外乎就是每个传感器的数据能大致在空间跟时间上能得到对齐。而整个多传感器融合技术的核心就在于高精度的时间以及空间同步。精度到什么量级呢? 举个例子,比如时间上能得到10的-6次方,空间上能得到在一百米外3到5厘米的误差,这是一个典型的技术指标。 当然,多传感器同步技术的难度与时间和空间的要求是一个指数级的增加。在百米外能得到3cm的空间精度,换算成角度是0.015度左右。 大家也知道在无人驾驶当中,毫米波雷达、相机、激光雷达和超声波都是完全不同的传感器,让他们在时域跟空域上得到这样的精度是非常难的,需要对机器人技术以及机器学习优化技术有非常深的理解。 自动泊车、公路巡航控制和自动紧急制动等自动驾驶汽车功能在很大程度上也是依靠传感器来实现的。 多传感器融合技术使用方式 重要的不仅仅是传感器的数量或种类,它们的使用方式也同样重要。目前,大多数路面上行驶车辆内的ADAS都是独立工作的,这意味着它们彼此之间几乎不交换信息。只有把多个传感器信息融合起来,才是实现自动驾驶的关键。 现在路面上的很多汽车,甚至是展厅内的很多新车,内部都配备有基于摄像头、雷达、超声波或LIDAR等不同传感器的先进驾驶员辅助系统(ADAS)。 这些系统的数量将会随着新法案的通过而不断增加,例如在美国,就有强制要求安装后视摄像头的法案。此外,诸如车险打折优惠和美国公路交通安全管理局(NHTSA)、欧洲新车安全评鉴协会(Euro-NCAP)等机构做出的汽车安全评级正在使某些系统成为汽车的强制功能;另一方面,这也助长了消费者对它们的需求。 ADAS如何实现突破限制 目前,大多数路面上行驶车辆内的ADAS都是独立工作的,这意味着它们彼此之间几乎不交换信息。(没错,某些高端车辆具有非常先进的自动驾驶功能,不过这些功能还未普及)。后视摄像头、环视系统、雷达和前方摄像头都有它们各自的用途。通过将这些独立的系统添加到车辆当中,可以为驾驶员提供更多信息,并且实现自动驾驶功能。不过,你还可以突破限制,实现更多功能——参见图1。 ADAS以汽车内单个、独立的功能存在。 仅仅通过多次使用相同种类的传感器无法克服每种传感器的缺点。反之,需要将来自不同种类传感器的信息组合在一起。工作在可见光谱范围内的摄像头CMOS芯片在浓雾、下雨、刺眼阳光和光照不足的情况下会遇到麻烦。而雷达缺少目前成像传感器所具有的高分辨率。可以在每种传感器中找到诸如此类的优缺点。
查看详情
SHT3XX 系列的传感,常见的有三种:SHT 30、SHT 31、SHT 35。其中,比较便宜性价比较愉快的是 SHT 30。 DHT 11 模块也是检测温度、湿度的,但SHT 11 使用的不是我们常见的 i2c 等协议,而是用它自己特有的单数据线协议。因此使用 DHT11 你需要自己写通信协议或使用现有的库;另外,DHT 11 模块似乎反应不太灵敏,上电后还得等几秒钟才能读到稳定数据。最重要的一点,不知道是不是老周运气不好,买了三个 DHT 11,坏了两个。而 SHT 30,一直用着——老周把它弄成家用温度计,挂在家里长期运作。当然,不是用树莓派去控制。毕竟,你想想,刻意用树莓派去读个温度,这也太浪费资源了,最开始是和客厅的监控连在一起的。老周买了个摄像头,用一块2G内存的树莓派刷了 Motion ,做成了监控,供家里客厅使用。 为了完美的伪装,找了个旧手机的盒子,自己打几个孔,然后把树莓派放进盒子里,摄像头用双面胶贴在盒子上。伪装效果还行,外人进来了一般不会发现。装中盒子里就造成一个问题:SHT30 检测温度湿度就不准了。于是,老周就拿掉了SHT,买了一块山寨的 ESP-8266,体积也很小,功耗低,也便宜,搞几个干电池就能供电了,然后就用 ESP 8266 来控制 SHT 30,还能通过 Wi-fi 来传数据(就读个温度/湿度,安全性不重要,随便透传)。用了三天,ESP 8266 上的板载LED灯坏了,但开发板还能正常用。 如果你特别喜欢大草莓(树莓派),也可以买树莓派家族的微控制器开发板—— 树莓派 Pico,也可以叫它小草莓。小草莓和 Arduino 系列的板子有点像(和 Nano 体积差不多),有 Micro-USB 口,用一根安卓手机数据线就可以和电脑连接了,非常地友好。Pico 不带操作系统,就是一块单片机,所以功耗低,特省电,供电电压在 1.8V 到 5.5V 之间,可以用手机充电器供电,省事。 好了,扯远了,因为本文的内容比较 easy,所以老周就先扯些没用的,接下来咱们扯些有用的。 SHT30 很小巧,标准的 i2C 引脚——vcc、gnd、sda、scl。买的时候最好买已经焊接好引脚的,不然,自己焊的话真的需要经验,毕竟模块很小,技术不好容易弄坏,动作不够快也容易焊成连锡——各个引脚导通了。老周的焊功比较烂,不敢自己动手。 既然是标准的 I2C 引脚,这模块当然是用 I2C 来通信了。如果你没作更改,默认的从机地址是 0x44。 这一次,老周向各位介绍微软封装的另一个库——iot bindings。这个库是微软提供的,里面封装了很多常用模块的操作,我们不用每次都自己手动写硬件通信,直接引用,开箱即用,无需调校,免打孔,免安装。 在创建.NET 项目后,执行以下命令引用(也可以用VS的Nuget管理工具)。 dotnet add package System.Device.Gpio dotnet add package iot.device.bindings 封装的模块类位于命名空间 Iot.Device.XXX 中,其中XXX是各类模块的大类名。比如,我们这次用的 SHT 30, 它属于 SHT 30、31、35 系列,故命名空间为 Iot.Device.Sht3x。 在代码文件中,using 一下。 using System; using System.Device.I2c; using Iot.Device.Sht3x; 然后,很简单的几行代码调用。 I2cConnectionSettings set=new(4, (byte)I2cAddress.AddrLow); I2cDevice dev= I2cDevice.Create(set); using Sht3x sht= new Sht3x(dev); bool running = true; Console.CancelKeyPress += (_,_) => running=false; while(running) { // 温度 double temp = sht.Temperature.DegreesCelsius; // 湿度 double hui = sht.Humidity.Percent; Console.WriteLine("温度:{0:N1} ℃\n湿度:{1:N1} %RH", temp, hui); System.Threading.Thread.Sleep(2000); } 注意看 I2cAddress 枚举,它已经为我们定义好了 SHT 3x 传感器(模块)的地址: public enum I2cAddress : byte { AddrLow = 0x44, AddrHigh = 0x45 } 我们在用时选默认地址 0x44 即可。 Sht3x 类已封装好,访问 Temperature 获取温度值,DegreesCelsius 是摄氏度,其他的不知道什么单位,老周见识浅,没研究过。Humidity 属性是相对湿度,一般用百分比。 写完代码后,发布。 dotnet publish -c Release -r linux-arm --no-self-contained 如果你的大草莓上没有配置 dotnet 框架,那就把 --no-self-contained 去掉,让它生成全部类库,然后全部复制到大草莓上运行。 scp -r bin\Release\net5.0\linux-arm\publish\* pi@192.168.0.xxx:/home/pi/<你要放置的目录> 运行结果如下图所示。 是不是很简单呢?前面在写这系列文章时,老周没有提到这个 Iot.Device.Bindings 库,是因为想让大伙伴们对 GPIO 一些基本通信有所了解。咱们在学习的时候,不要急着拿现成的库来用,先自己试着写些简单的东东玩玩,对相关知识有一定了解后,再去寻找现成的库。这样既能学到原理性的东西,也能提高开发效率。 目前这个 bindings 库微软在不断地更新,支持的模块越来越多。尽管如此,有些模块还是没有收录进去。比如,上次老周介绍过的 MPU 6050 ,bindings 库里面就没有,库里面只有 MPU 6500 和 MPU 9250。
查看详情