精华内容
下载资源
问答
  • 本质矩阵E求解及运动状态恢复

    千次阅读 2017-04-04 17:13:00
    注意:根据摄像机模型t=-RT,恢复运动状态,就是要计算t和R。 8点法 8点算法是计算基本矩阵的最简单的方法,它涉及构造并(最小二乘)解一个线性方程组。给定足够多的匹配点(至少7对),可解得基础矩阵F。 记和...

    为了获取本质矩阵,首先计算基础矩阵F。根据本质矩阵E,即可恢复得到运动的状态R和T。

    image可以根据匹配点得到F,然后根据image和相机内参,即可得到本质矩阵E。进而根据:

    image

    image

    注意:根据摄像机模型t=-RT,恢复运动状态,就是要计算t和R。

    8点法

    8点算法是计算基本矩阵的最简单的方法,它涉及构造并(最小二乘)解一个线性方程组。给定足够多的匹配点image(至少7对),可解得基础矩阵F。

    imageimage,每一组点匹配提供关于clip_image006的未知元素的一个线性方程。对应于一对点clip_image012clip_image014的方程是:

    image

    写成矩阵(矢量)的形式:

    image

    其中:

    image 

    image

    从而根据n对匹配的点集,我们可以得到线性方程组:

    image

    计算最小二乘解,clip_image002[6]的最小二乘解是对应于clip_image004[4]的最小奇异矩阵的奇异矢量,即是clip_image004[5]的SVD分解clip_image006[4]中矩阵clip_image008[4]的最后一列矢量。用这种方法得到的解矢量clip_image002[7]在条件clip_image011下取clip_image013的最小值。

    该方法步骤简单,易于实现,但由于其计算结果对输入数据异常敏感,从而直接影响它在实际中的应用。

    1997年,Hartley对原始8点算法进行改进,在构造解的方程之前对输入的数据进行适当的归一化。即在形成8点算法的线性方程组之前,图像点的一个简单变换(平移或变尺度)将使这个问题的条件极大地改善,从而提高结果的稳定性。而且进行这种变换所增加的计算复杂性并不显著。算法具体过程具体如下:

    1. 对原始图象坐标做一个平移变换,使原来以左上角为原点的图象坐标变成以所有图像点的重心为原点的图像坐标;
    2. 再对图象坐标做一个尺度变换,使得点到原点的平均距离为clip_image002[10]

    分别对两幅图像进行以上两步变换,然后将变换后的图像坐标作为输入数据计算基础矩阵。

    计算过程如下:

    1. 设两个独立的图像坐标变换分别为Tclip_image004[8],则变换后的图像坐标为clip_image006[6]clip_image008[6]
    2. 基于转换后的匹配点对image,利用八点算法计算基础矩阵clip_image012[4]
    3. 解除归一化,令clip_image014[4],矩阵F是对应于原始数据clip_image016的基本矩阵。

    注意:此时求得的F阵的秩并不保证严格为2。而且,由于噪声影响计算得到的F一般都是满秩的。

    进而,根据相机的内参信息得到本质矩阵E。

    运动恢复

    在二视图中,第一个相机的投影矩阵为:image,而第二个相机的投影矩阵为:image,投影矩阵的旋转矩阵R和平移矩阵t是要被计算出来的,计算的过程称为运动恢复。这里的“运动”就是第二帧相机相对于第一帧相机的运动变化。

    1992年,Hartley提出了从本质矩阵在相差一个尺度因子的情况下恢复出摄像机投影矩阵的四个可能解,然后从四个解中选择一个正确解的方法。证明过程可见“计算机视觉中的多视图几何”中文本174页。

    定理1:一个3*3的矩阵是本质矩阵的充要条件是它的奇异值中有两个相等而第三个是0。

    定理2:设本质矩阵image的SVD分解是:image,不考虑本质矩阵的符号,则存在两种可能的解:

    image,image

    image,image

    其中:

    image image

    推论:已知本质矩阵image和第一和摄像机矩阵image,那么第二个摄像机矩阵有下列四种可能的选择:

    image

    imageimage的最后一列。

    t的具体空间长度是不能恢复出来的,只能在相差一个尺度因子的情况下恢复平移单位向量。

    4个解的几何解释

    显然前面两个解的差别就是第一个摄像机到第二个摄像机的平移矢量是反向的。

    image

    上下两行的差别在于摄像机绕基线旋转了180度,上图中只有(a)时,重构点同时出现在两个摄像机的前面。

    从匹配点对中随机选择n对特征点,采用视图重建的方法,分别基于四种可能的摄像机矩阵重建出对应的三维点,满足景深为正的点数最多的解就是对应的P^{'},同时也可以确定相机的外参R和t。

    展开全文
  • 手机加速度计在MATLAB中的仿真,要求估计手机运动状态,需要仿真流程图及仿真程序,最好有程序注释,可以的话将文件发送到邮箱:202381193@qq.com,谢谢
  • 基于JPL DE405和IAU SOFA计算太阳系行星运动状态的Fortran平台搭建与测试 1. 背景 1.1 为什么要得到行星运动状态 1.2 JPL DE405是什么 1.3 IAU SOFA是什么 1.4 Fortran语言的优势 1.5 Fortran平台的构成 2. ...

    基于JPL DE405和IAU SOFA计算太阳系行星运动状态的Fortran平台搭建与测试

    一些铺垫

    • 我们为什么要得到行星运动状态
      行星的运动状态是指行星的位置和速度,这两个信息对我们非常重要,比如想要为人造航天器设计一条从地球去往火星的轨道,那么至少需要知道一段时间内地球以及火星相对于太阳的位置和速度,才能找到合适的时机(包括时间与位置)脱离地球以及进入火星。

    • JPL DE405是什么
      一种由美国Jet Propulsion Laboratory (JPL)提出的精确星历表,包括行星的章动和天平动。

    • IAU SOFA是什么
      Standard of fundamental astronomy (SOFA)是由国际组织International Astronomical Union(IAU)提出的一套基础天文学标准,可以通过其提供的软件来调用许多重要的功能。

    • Fortran语言的优势
      一种能够高效执行科学计算的编程语言。

    • Fortran平台的构成
      在windows 10操作系统上,采用了最新的Microsoft Visual Studio Enterprise (VS) 2017+Intel Parallel Studio XE (PSXE) 2018的方案,VS提供界面与项目管理,PSXE提供编译支持,这个方案虽然新但也不失稳定性。

    2. Visual Studio Enterprise 2017安装

    Visual Studio Enterprise 2017是Microsoft公司开发工具包系列产品,我们想要使用它的集成开发环境(IDE)。

    安装的VS版本为Visual Studio Enterprise 2017,需要激活(在网上容易搜到产品秘钥进行激活),

    下载地址为:https://visualstudio.microsoft.com/zh-hans/downloads/

    安装过程中,遇到以下选择工作负载的类似界面时,选择“Windows(3)”中的“使用C++的桌面开发”,确认后继续接下来的步骤,完成安装即可

    VS安装工作负载选择

    安装结束可以在windows菜单栏找到VS

    这里写图片描述

    3. Intel Parallel Studio XE 2018安装

    Parallel Studio XE是Intel公司推出的一款IDE软件,该程序具有强大的多线程编程功能,能够提供高效的Fortran编译支持。

    安装的版本为:parallel_studio_xe_2018_update1_cluster_edition。PSXE可以通过高校邮箱申请许可的序列码,具体步骤按照Intel官网的步骤操作即可,

    地址为:https://software.intel.com/en-us/parallel-studio-xe/choose-download

    开始安装PSXE,下面的步骤与帖子(感谢springer_)基本一致:

    将申请的序列码填入:

    这里写图片描述

    安装结束后,在windows菜单栏可以找到PSXE,如下图

    这里写图片描述

    可以看到,PSXE提供了两个DOS窗口:for IA-32 VS以及for Intel 64 VS,使用起来没有发现区别,我们下面测试JPL提供的DE405例程就从该窗口进入。

    4. JPL DE405的下载与测试

    JPL DE405是我们对JPL的行星星历工具包的称呼,它包括一定时间跨度的行星运动状态数据,以及调用的程序,其中最为核心的星历数据保存在DExxx.xxx(比如,比较常用的DE405.405,405是数据版本的编号)文件中。为了方便,我们用星历文件名来代表工具包。

    JPL DE405工具包下载的官网为:https://ssd.jpl.nasa.gov/?planet_eph_export

    至此,我们得到了所有需要的文件:

    这里写图片描述

    文件名ascYXXXX.405表示公元后(p)XXXX年的数据,一个文件存储了20年内的数据,读者根据自己的需要下载相应时间跨度的文件;asc2eph.f是ascii转二进制的程序文件;testeph.f是读取二进制文件并且相关计算的程序文件;userguide.txt是说明文件。

    在windows开始菜单,找到Intel Parallel Studio XE 2018,打开compiler 18.0 for Intel 64 VS的DOS窗口对JPL提供的Fortran原码进行测试:

    首先操作进入存放JPL DE405工具包的文件夹内,合并文件:

    copy header.405+ascp2000.405+ascp2020.405+ascp2040.405 infile.405

    此命令会产生一个infile.405的文件,继续在命令行界面输入:

    ifort asc2eph.f

    ifort是PSXE对应的表示编译的指令,通过asc2eph.f程序将infile读取(<)到控制台:

    asc2eph < infile.405 

    此命令会产生一个名为JPLEPH的文件,并有执行结果:

    这里写图片描述

    打开testeph.f文件,给所有的NRECL赋值为1(对应windows操作系统),FSIZER3中的KSIZE赋值为2036,并且取消子程序STATE中的调用FSIZER3的注释符,编译testeph.f文件:

    ifort testeph.f

    通过testeph.f程序将testpo.405文件读取到控制台:

    testeph < testpo.405

    这里写图片描述

    正常情况下,控制台会输出一系列的常用参数,以及和标准数据的比较结果,至此,测试成功。

    5. IAU SOFA的下载

    因为JPL提供的查询行星位置和速度的程序输入为儒略日(Julian Date),这与我们习惯使用的记时法格里高利历(Gregorian Calendar)不同,所以我们需要将GC转化为JD,幸运的是,IAU SOFA提供了Fortran的程序:cal2jd.f

    程序的下载地址为:http://www.iausofa.org/index.html

    值得注意的是,cal2jd.f输出为简约儒略日(MJD),但是可以通过简单的修改(+2400000.5)得到JD。在上述的网站,IAU SOFA还提供了许多有用的程序,比如epv00.f,它可以直接返回地球相对于太阳的速度与位置(稍后我们就采用了这个程序与JPL程序进行对比)。

    6. 基于VS项目管理的Fortran77程序

    在VS下新建Fortran工程,程序的结构为:

    这里写图片描述

    其中:

    • JPLEPH:存储星历的二进制文件
    • cal2jd.for:格里高利历转化为儒略历
    • CONST.for:查找星历文件里所有的常数
    • epv00.for:计算地球相对于太阳的位置和速度(对比程序)
    • FSIZER3.for:初始化
    • Get_ephemeris.for:主程序
    • INTERP.for:微分并插值Chebyshev系数
    • PLEPH.for:计算目标星相对于参考星的位置和速度
    • SPLIT.for:将双精度的实数拆分为整数与小数部分
    • STATE.for:读取并插值星历文件

    除了主程序Get_ephemeris.for,cal2jd.for与epv00.for来源于IAU SOFA原文件,其余的子程序均来自testeph.f文件。

    主程序的代码为:

    
          program Get_ephemeris
    
          implicit none
    C++++++++++++++++++++++++++++
    C     For routine iau_cal2jd(year,month,day,djm0,djm,djm_status)
    C     year,month,day    integer     year, month, day in Gregorian calendar    
    C     djm0        d     MJD zero-point: always 2400000.5
    C     djm         d     Julian Date for 0 hrs
    C     djm_status  i     status:
          INTEGER(4) year,month,day,djm_status
          DOUBLE PRECISION djm0,djm
    
    C++++++++++++++++++++++++++++
    C     For  routine iau_EPV00(djm1,djm2,pvh,pvb,jstat)  
    C     djm1     d        TDB date part A (Note 1)
    C     djm2     d        TDB date part B (Note 1)
    C     pvh      d(3,2)   heliocentric Earth position/velocity (au,au/day)
    C     pvb      d(3,2)   barycentric Earth position/velocity (au,au/day)
    C     jstat    i        status: 0 = OK
    C                              +1 = warning: date outside 1900-2100 AD   
          INTEGER jstat
          DOUBLE PRECISION djm1,djm2
          DIMENSION pvh(3,2),pvb(3,2)
          DOUBLE PRECISION pvh,pvb
    
    C++++++++++++++++++++++++++++
    C     For routine PLEPH(djm3,ntarg,ncent,rrd)
    C     ntarg = INTEGER NUMBER OF 'TARGET' POINT.
    C
    C     ncent = INTEGER NUMBER OF CENTER POINT.
    C
    C            THE NUMBERING CONVENTION FOR 'NTARG' AND 'NCENT' IS:
    C
    C                1 = MERCURY           8 = NEPTUNE
    C                2 = VENUS             9 = PLUTO
    C                3 = EARTH            10 = MOON
    C                4 = MARS             11 = SUN
    C                5 = JUPITER          12 = SOLAR-SYSTEM BARYCENTER
    C                6 = SATURN           13 = EARTH-MOON BARYCENTER
    C                7 = URANUS           14 = NUTATIONS (LONGITUDE AND OBLIQ)
    C                            15 = LIBRATIONS, IF ON EPH FILE
    C     rrd =  OUTPUT 6-WORD D.P. ARRAY CONTAINING POSITION AND VELOCITY
    C            OF POINT 'NTARG' RELATIVE TO 'NCENT'. 
          DOUBLE PRECISION djm3
          DIMENSION rrd(6)
          DOUBLE PRECISION rrd 
    
          year = 2011
          month = 5
          day = 1  
    
          call iau_cal2jd(year,month,day,djm0,djm,djm_status)
    
          djm1 = djm
          djm2 = 0D0
          call iau_EPV00(djm1,djm2,pvh,pvb,jstat)
    
          djm3 = djm
          call PLEPH(djm3,3,11,rrd)
    
          write(*,*) "查询地球相对于太阳位置速度的时间为(年/月/日):"
          write(*,*) year,month,day
          write(*,100)
          write(*,*) "地球相对于太阳的位置和速度为(au,au/y) (by IAU EPV00):"
          write(*,*) pvh(1,1),pvh(2,1),pvh(3,1),pvh(1,2),pvh(2,2),pvh(3,2)
          write(*,100)
          write(*,*) "地球相对于太阳的位置和速度为(au,au/y) (by JPL PLEPH):"
          write(*,*) rrd(1),rrd(2),rrd(3),rrd(4),rrd(5),rrd(6)
    100   format(/)
    
          stop
          end

    程序的运行结果为:
    这里写图片描述

    可以看到,通过IAU epv00与JPL PLEPH计算的结果基本一致,因此结果是可信的。

    展开全文
  • 在本系列的上一篇文章中介绍了如何让小车实现自动避障,本文作为本系列的第四篇文章,主要介绍蓝牙模块的使用,如何通过蓝牙进行数据传输,并通过手机向蓝牙模块发送指令,从而达到使用手机控制智能小车的运动状态,...

       我会通过本系列文章,详细介绍如何从零开始用51单片机去实现智能小车的控制,在本系列的上一篇文章中介绍了如何让小车实现自动避障,本文作为本系列的第四篇文章,主要介绍蓝牙模块的使用,如何通过蓝牙进行数据传输,并通过手机向蓝牙模块发送指令,从而达到使用手机控制智能小车的运动状态,本文以汇承HC-08蓝牙模块为例。

    本系列文章链接:

    -----------------------------------------------------------------------------

       详细介绍如何从零开始制作51单片机控制的智能小车(一)———让小车动起来
       详细介绍如何从零开始制作51单片机控制的智能小车(二)———超声波模块、漫反射光电管、4路红外传感器的介绍和使用
       详细介绍如何从零开始制作51单片机控制的智能小车(三)———用超声波模块和漫反射光电传感器实现小车的自动避障
       详细介绍如何从零开始制作51单片机控制的智能小车(四)———通过蓝牙模块实现数据传输以及通过手机蓝牙实现对小车运动状态的控制
       详细介绍如何从零开始制作51单片机控制的智能小车(五)———对本系列第四篇文章介绍的手机蓝牙遥控加减速异常的错误的介绍及纠正

    -----------------------------------------------------------------------------

    一、蓝牙模块的选择和基本设定

       1、工作原理简单介绍

    在这里插入图片描述

       以上图片来自汇承官方用户手册,HC-08模块用于代替全双工通信时的物理连线。左边的设备向模块发送串口数据,模块的 RXD 端口收到串口数据后,自动将数据以无线电波的方式发送到空中。右边的模块能自动接收到,并从 TXD 还原最初 左边设备所发的串口数据。从右到左也是一样的。

       2、测试模块是否正常工作,以及相关参数的设定

       建议新手朋友们,购买新手套餐,上面左图是测试架,用来实现模块与电脑的连接,右图为本文介绍的HC-08蓝牙模块,只需要把蓝牙模块插到测试架上,通过安卓数据线,就可以与电脑相接了,通过HID 串口助手(汇承官网有HC-08资料包,内部包含相关辅助工具及详细的资料) ,向蓝牙发送AT,若返回OK,则模块正常工作
       接着向蓝牙发送AT+RX 查看模块基本参数 ,主要关注两条信息,蓝牙是主机还是从机模式,设定的波特率是多少,默认出厂时是从机模式,波特率9600,恰好是本文我们需要的数值,若你单片机程序的通信波特率不是9600,可以通过HID 串口助手或者AT指令把蓝牙模块修改成你需要的波特率

       3、蓝牙模块与单片机的连接,与手机的通信

    在这里插入图片描述

       在这里需要确定你用的蓝牙模块的工作电压是多少,汇承HC-08有贴片装和带底板装两种,建议新手一定要买带底板的,如上图所示,可以省去很多麻烦。我们使用的51单片机供电电压是5v,而贴片装电压3.3v,不能直接与单片机连接,虽然本文介绍的这款单片机最小系统有3.3v的输出引脚,但是它串口TX RX依然是工作在5v电压下,而贴片装蓝牙模块的TX,RX需要工作在3.3v电压下,蓝牙模块的 RX 端需要串接一个 220Ω~1KΩ的电阻再接到单片机上,TX可以直接连接,对新手来说有点小麻烦,所以大家买的时候,直接买带底板的,支持3.2v-6v,也就是说带底板的可以直接与51单片机连接, 连接时只需要连4根线 Vcc连Vcc,GND连接GND,蓝牙模块TX接单片机 Rx (对于前文介绍的这款的单片机,RX为P30管脚,TX为P31管脚,如下图所示),蓝牙模块RX接单片机 Tx。
       蓝牙模块不能直接与手机连接需要通过HC-COM这个app进行连接,给模块上电后,开启手机蓝牙功能,打开手机上的HC-COM,点击扫描设备,找到蓝牙模块进行连接,连接成功显示connected。

       关于以上的第一部分,因为大家选择的蓝牙模块型号或者商家不同,会有一定的差异,在上文仅就本文需要用的地方进行了简单的介绍,蓝牙模块详细的用法步骤可以去看你购买的蓝牙的用户手册

    二、蓝牙模块串口通信程序的编写以及蓝牙遥控的实现

       1、与定时器相关函数的编写

       我们知道我们使用STC89C52单片机,只有两个定时器,在前文我们用定时器0,来实现电机的PWM输出,定时器1,来触发溢出中断,用于超声波模块障碍物的检测,而现在我们用蓝牙进行无线串口的通信,也需要用定时器,怎么办呢? 定时器0肯定是不能动的,只能对定时器1进行复用,我们设定一个变量char Work_Mode=0; 进行工作模式的选择 为0时为手机或者电脑等上位机对小车进行蓝牙遥控 ,为1时小车自动避障模式,在一开始的时候,我们把它设为0,此时对定时器1初始化为蓝牙模块所需的工作模式,在我们需要转化成自动避障模式时,通过手机发送指令让Work_Mode变为1,此时我们把定时器1初始化为超声波避障所需的工作模式,由于此步是在while(1)循环中进行的,但是初始化只需要初始一次,不能一直初始化,所以需要再设定一个变量Work_Mode2,初始值设为0,当执行一次把定时器1初始化为超声波避障所需的工作模式的初始化后,就让该变量为1,这样就可以实现只初始化一次了,相关代码如下:
    void Timer1Init()    //定时器1设定为为自动避障模式所需的初始化
    {
    	TMOD=0X11;//选择为定时器1模式,工作方式1,仅用TR1打开启动。选择为定时器0模式,工作方式1,仅用TR1打开启动
    	TH1=0;	
    	TL1=0;	
    	ET1=1;//打开定时器1中断允许
    	EA=1;//打开总中断
    	TR1=1;//打开定时器			
    }
    
    
    
    void Timer1Init2()   //定时器1设定为为蓝牙遥控模式所需的初始化
    {
    		SCON=0X50;			//设置为工作方式1,8位数据,可变波特率
    	  TMOD |=0X20;			//设置计数器工作方式2
    	  PCON=0X00;			//波特率不加倍
    	  TH1=0XFd;		    //计数器初始值设置,9600  @11.0592MHz
    	  TL1=0XFd;
    	  TR1=1;					//打开计数器
    	  ES = 1;         //开串口中断
        EA = 1;         //开总中断
    }
    
    char Work_Mode=0;   //工作模式的选择  为0时,为手机或者电脑等上位机对小车进行蓝牙遥控 ,为1时小车自动避障模式
    char Work_Mode2=0;
    
    
    void main()
    	{
    	Timer0Init();
    	Timer1Init2();	
    	Left_Speed_Ratio=5;   //设置左电机车速为最大车速的50%
    	Right_Speed_Ratio=5;	设置右电机车速为最大车速的50%
    	while(1)
    		{
    		  if(Work_Mode==1)
    			{
    			   	ES=0;   //关闭串口中断				  
    				 if(Work_Mode2==0)
    				 {
    					Timer1Init();
                        Work_Mode2=1;
    				 }
    				if(Echo==1)
    				{
    				TH1=0;	
    	            TL1=0;
    				TR1=1;			    //开启计数	
    				while(Echo);			//当RX为1计数并等待	
    				TR1=0;				//关闭计数	
    			    Conut();			//计算		
    			     }
    			  if(M_sensor==1)
    				{  run(); }
    		       else
    			  {
    			      if(L_sensor==1)
    	           {     left();       }
    			
    			         else if(R_sensor==1)
    	            {    right() ;      }
    			      else
    				  {    back();          }
    		     	 }
    					
    			}
    			else
    				
    			{
    				
    			switch(receive_real_data) 
            { 
            
    		      case '1': run(); break; 
    		      case '2': left(); break; 
    		      case '3': right(); break; 
    	     	  case '4': back(); break; 
    		      case '5': Speed_add(); break; 
    	     	  case '6': Speed_reduce(); break; 
    			 case '7': stop(); break; 
    	        case '8': Work_Mode=1; break; 
    	        }
    			
    				
    		    LED=1;
         }	
    		}
    }
    
    

       2、对上面的主函数的解释(蓝牙遥控的实现思路)

       本篇文章的功能是在已经完成自动避障的基础上进行扩展的,也就是让小车可以具备自动避障和蓝牙遥控两种工作模式,所以当Work_Mode=1时的程序,也就是工作在自动避障模式的程序,在对定时器1完成设定为自动避障模式所需的初始化后,跟本系列第三篇文章介绍的是相同的,已详细介绍过了,在这就不介绍了,一开始的时候Work_Mode=0,工作在蓝牙遥控模式,我设定的功能如上面的主函数所示,当手机或者电脑或者其他上位机对单片机(通过蓝牙模块)发送1的时候,让小车前行。发送2的时候 让小车左转,发送3的时候让小车右转,发送4的时候,让小车后退,发送5的时候,让小车加速,发送6的时候让小车减速,发送7的时候,停车,发送8的时候,把Work_Mode置为1,使小车进入自动避障模式,此时蓝牙遥控失去作用,按下单片机上的复位键,程序将重新加载,重新进入蓝牙遥控模式。新增的加速减速函数如下:
    void  Speed_add() //加速函数
    {
    	if(Left_Speed_Ratio<10) //限幅
    		{
    		Left_Speed_Ratio++;
    	  Right_Speed_Ratio++;	
        }
    }
    
    void  Speed_reduce() //减速函数
    {
    	if(Left_Speed_Ratio>0)  //限幅
    		{
    		Left_Speed_Ratio--;
    	  Right_Speed_Ratio--;	
        }
    }
    

       3、串口中断函数的编写

        上面呢我们已经介绍了,通过发送一些设定好了的指令,实现蓝牙遥控,那么怎么把指令发送出去,以及单片机是如何实现接收的呢?,这就要通过串口中断来实现了,下面是官方给的一个串口中断的参考代码,是有问题的,对于HC-COM来说,是不能正常工作的
    void Com_Int(void) interrupt 4
    {
    
      uchar receive_data;
    	
      EA = 0;
    	
      if(RI == 1) //当硬件接收到一个数据时,RI会置位
    	{ 		
    		RI = 0;
    		receive_data = SBUF;//接收到的数据
    			
    		if(receive_data == '1')	 
    		{
    				LED =0;//接收到1亮灯
    		}
    		else
    		{
    				LED =1; //其他情况灯灭
    		}
    		
    	}
    	  SBUF=receive_data;//将接收到的数据放入到发送寄存器
    	  while(!TI);			 //等待发送数据完成
    	  TI=0;						 //清除发送完成标志位		
    		EA = 1;
    }
    
        按照上面的代码,当我们利用手机上的app HC-COM向单片机发送1的的时候,接收的数据receive_data=1,此时呢LED应该等于0,也就是LED会被点亮,但是实际上它只会闪一下,这就说明,我们通过HC-COM发送1的时候,单片机先接收到我们发的1,之后又接收到其他的信息,在上面的程序中,我们让单片机把接收到的信息又通过蓝牙模块发送给手机,利用HC-COM进行显示,如下图所示:
        乍一看,我们发送1的时候,只返回了一个1 ,发送0的时候,只返回了一个0,那单片机接收的其他信息是从何而来的呢?,为啥手机没有显示返回的其他信息呢?对此我进行了大量的实验,最终发现这个app,它一次发送实际上是发送20位,什么意思呢? 当我们输入一个1点击发送的时候,它实际上发送的是1000 0000 0000 0000 0000,也就是说当我们输入的数据的位数不足20位时它会自动补零,这就解释了为什么LED灯不会常亮,只会闪一下,因为在接收完我们发送的1后,它又接收了19个0,把这20个数据返回我们手机上的时候,它这个app把它补得这19个0又以空格的格式进行显示,而不是显示0(有点坑)。
        为了让LED常亮,我们需要输入20个1,如上图所示,也就是不给它补零的机会。这种方法呢用起来十分的不方便,而且本文我们要通过HC-COM发送指令,实现对小车的遥控,也就说需要我们快速的输入指令,这种方法是不行的,怎么办呢?,对大部分人了说不具备修改这个app的能力,那么只能修改单片机的接收和发送函数了,因此我把上面的串口中断函数进行了简单的修改,如下:
    void Com_Int(void) interrupt 4
    {
        EA = 0;	
      
      if(RI == 1) //当硬件接收到一个数据时,RI会置位
    	{ 
       	LED=0;
    		RI = 0;
    		receive_data = SBUF;//接收到的数据
    		if(receive_data!=0)
    		receive_real_data=receive_data;
    	 
    		SBUF=receive_real_data;//将接收到的数据放入到发送寄存器
    	  while(!TI);			 //等待发送数据完成
    	  TI=0;						 //清除发送完成标志位
    		
    		
    }
        EA = 1;	
    }
    
        只有接收到的数据不为0时,才赋值给新的变量receive_real_data,我们通过判断receive_real_data的值,来控制小车,而不是直接用接收到的值receive _data,同样我们返回到手机的数据,也改为receive_real_data。这样我们只需要发送一位的 1、2、3、4、5、6、7、8、等就可以对小车进行控制,用起来很方便。

    三、各文件完整的程序代码

       1、main.c文件

    #include <car.h>
    
    extern unsigned char Left_Speed_Ratio;
    extern unsigned char Right_Speed_Ratio;
    unsigned int time=0; 
    unsigned int HC_SR04_time=0;
    extern unsigned char pwm_val_left;
    extern unsigned char pwm_val_right;
     bit   flag =0;
    extern char M_sensor;  
    char Work_Mode=0;   //工作模式的选择  为0时,为手机或者电脑等上位机对小车进行蓝牙遥控 ,为1时小车自动避障模式
    char Work_Mode2=0;
    unsigned char receive_data=0;
    unsigned char receive_real_data=0;
    void delay1s(void)   
    {
        unsigned char a,b,c;
        for(c=167;c>0;c--)
            for(b=171;b>0;b--)
                for(a=16;a>0;a--);
        _nop_();  
    }
    void delay1ms(void)   
    {
        unsigned char a,b,c;
        for(c=1;c>0;c--)
            for(b=142;b>0;b--)
                for(a=2;a>0;a--);
    }
    
    void Timer0Init()
    {
    	TMOD|=0X01;//选择为定时器0模式,工作方式1,仅用TR0打开启动。
    
    	TH0=0XFC;	//给定时器赋初值,定时1ms
    	TL0=0X18;	
    	ET0=1;//打开定时器0中断允许
    	EA=1;//打开总中断
    	TR0=1;//打开定时器			
    }
    
    void Timer1Init()
    {
    	TMOD=0X11;//选择为定时器1模式,工作方式1,仅用TR1打开启动。选择为定时器0模式,工作方式1,仅用TR1打开启动
    	TH1=0;	
    	TL1=0;	
    	ET1=1;//打开定时器1中断允许
    	EA=1;//打开总中断
    	TR1=1;//打开定时器			
    }
    
    
    
    void Timer1Init2()
    {
    		SCON=0X50;			//设置为工作方式1,8位数据,可变波特率
    	  TMOD |=0X20;			//设置计数器工作方式2
    	  PCON=0X00;			//波特率不加倍
    	  TH1=0XFd;		    //计数器初始值设置,9600  @11.0592MHz
    	  TL1=0XFd;
    	  TR1=1;					//打开计数器
    	  ES = 1;         //开串口中断
        EA = 1;         //开总中断
    }
    
    
    
    
    void timer0()interrupt 1 using 2 
    { 
    TH0=0XFC;	//给定时器赋初值,定时1ms
    TL0=0X18;
    time++; 
    pwm_val_left++; 
    pwm_val_right++; 
    pwm_out_left_moto(); 
    pwm_out_right_moto(); 
    	
    HC_SR04_time++;
    if(HC_SR04_time>=500)   //500ms 启动一次超声波测距
    {
    	HC_SR04_time=0;
    	StartModule();
    }
    } 
    
    void Timer1() interrupt 3
    {
    	flag=1;    //若定时器1溢出则flag置1
    }
    
    
    void Com_Int(void) interrupt 4
    {
        EA = 0;	
      
      if(RI == 1) //当硬件接收到一个数据时,RI会置位
    	{ 
       	LED=0;
    		RI = 0;
    		receive_data = SBUF;//接收到的数据
    		if(receive_data!=0)
    		receive_real_data=receive_data;
    	 
    		SBUF=receive_real_data;//将接收到的数据放入到发送寄存器
    	  while(!TI);			 //等待发送数据完成
    	  TI=0;						 //清除发送完成标志位
    		
    		
    }
        EA = 1;	
    }
    
    void main()
    	{
    	Timer0Init();
    	Timer1Init2();	
    	Left_Speed_Ratio=5;   //设置左电机车速为最大车速的50%
    	Right_Speed_Ratio=5;	设置右电机车速为最大车速的50%
    	while(1)
    		{
    		  if(Work_Mode==1)
    			{
    			   	ES=0;   //关闭串口中断
    				  
    				 if(Work_Mode2==0)
    				 {
    					
    					Timer1Init();
               Work_Mode2=1;
    					  
    				 }
    					 
    				
    						if(Echo==1)
    				{
    				TH1=0;	
    	      TL1=0;
    				TR1=1;			    //开启计数	
    				while(Echo);			//当RX为1计数并等待	
    				TR1=0;				//关闭计数	
    			  Conut();			//计算		
    			}
    				
    		  if(M_sensor==1)
    				{  run(); }
    		  else
    			  {
    			      if(L_sensor==1)
    				       {     left();       }
    			
    			      else if(R_sensor==1)
    				       {    right() ;      }
    			      else
    						   {    back();          }
    			
    			 }
    					
    			}
    			else
    				
    			{
    				
    			switch(receive_real_data) 
            { 
            
    		      case '1': run(); break; 
    		      case '2': left(); break; 
    		      case '3': right(); break; 
    	     	  case '4': back(); break; 
    		      case '5': Speed_add(); break; 
    	     	  case '6': Speed_reduce(); break; 
    				  case '7': stop(); break; 
    	        case '8': Work_Mode=1; break; 
    	        }
    			
    				
    		    LED=1;
         }	
    		}
    }
    
    
    

       2、motor_control.c文件

    #include <car.h>
    
    
    unsigned char pwm_val_left =0;
    unsigned char push_val_left =0; 
    unsigned char pwm_val_right =0;
    unsigned char push_val_right=0;
    unsigned char Left_Speed_Ratio;
    unsigned char Right_Speed_Ratio;
    
    bit Left_moto_stop =1;
    bit Right_moto_stop =1;
    
    
    void Left_moto_go()  //左电机正转
    {p34=0;p35=1;} 
    void Left_moto_back() //左电机反转
    {p34=1;p35=0;} 
    void Left_moto_stp()  //左电机停转
     {p34=1;p35=1;} 
    void Right_moto_go()  //右电机正转
    {p36=0;p37=1;} 
    void Right_moto_back() //右电机反转
    {p36=1;p37=0;}  
    void Right_moto_stp()  //右电机停转
    {p36=1;p37=1;} 
    
    
    void pwm_out_left_moto(void)    //左电机PWM
    { 
    if(Left_moto_stop) 
    { 
    if(pwm_val_left<=push_val_left) 
    Left_moto_pwm=1; 
    else 
    Left_moto_pwm=0; 
    if(pwm_val_left>=10) 
    pwm_val_left=0; 
    } 
    else 
    Left_moto_pwm=0; 
    } 
    
    void pwm_out_right_moto(void)    //右电机PWM
    { 
    if(Right_moto_stop) 
    { 
    if(pwm_val_right<=push_val_right) 
    Right_moto_pwm=1; 
    else 
    Right_moto_pwm=0; 
    if(pwm_val_right>=10) 
    pwm_val_right=0; 
    } 
    else 
    Right_moto_pwm=0; 
    } 
    
    
    void run(void)     //小车前行
    { 
    push_val_left =Left_Speed_Ratio;    
    push_val_right =Right_Speed_Ratio; 
    Left_moto_go(); 
    Right_moto_go(); 
     } 
    
     
    
    void back(void)   //小车后退
    { 
    push_val_left =Left_Speed_Ratio; 
    push_val_right =Right_Speed_Ratio; 
    Left_moto_back();
    Right_moto_back();
     } 
    
    
    
    void left(void)   //小车左转
    { 
    push_val_left =Left_Speed_Ratio; 
    push_val_right =Right_Speed_Ratio;
    Right_moto_go(); 
    Left_moto_back();
    } 
    
     void right(void) //小车右转
    { 
    push_val_left =Left_Speed_Ratio;
    push_val_right =Right_Speed_Ratio;
    Right_moto_back();
    Left_moto_go();
    } 
    
    void stop(void)  //小车停止
    { 
    push_val_left =Left_Speed_Ratio; 
    push_val_right =Right_Speed_Ratio; 
    Left_moto_stp();
    Right_moto_stp();
     } 
    
    void rotate(void) //小车原地转圈
    { 
    push_val_left =Left_Speed_Ratio; 
    push_val_right =Right_Speed_Ratio; 
    Left_moto_back();
    Right_moto_go();
     } 
    
    
    void  Speed_add() //加速函数
    {
    	if(Left_Speed_Ratio<10)
    		{
    		Left_Speed_Ratio++;
    	  Right_Speed_Ratio++;	
        }
    }
    
    void  Speed_reduce() //减速函数
    {
    	if(Left_Speed_Ratio>0)
    		{
    		Left_Speed_Ratio--;
    	  Right_Speed_Ratio--;	
        }
    }
    
    
    

       3、HC_SR04.c文件

    #include <car.h>
    
    float  S=0;
    extern bit  flag;
    unsigned int  measure_time;
    char M_sensor; 
     void  StartModule() 		         //启动超声波模块
      {
    	  Trig=1;			                
    	  _nop_(); 
    	  _nop_(); 
    	  _nop_(); 
    	  _nop_(); 
    	  _nop_(); 
    	  _nop_(); 
    	  _nop_(); 
    	  _nop_(); 
    	  _nop_(); 
    	  _nop_(); 
    	  _nop_(); 
    	  _nop_(); 
    	  _nop_(); 
    	  _nop_(); 
    	  _nop_(); 
    	  _nop_(); 
    	  _nop_();
    	  _nop_(); 
    	  _nop_(); 
    	  _nop_(); 
    	  _nop_();
    	  Trig=0;
      }
    	
     void Conut(void)
    	{
    	 measure_time=TH1*256+TL1;
    	 TH1=0;
    	 TL1=0;
    	 S=(measure_time*1.87)/100;     //算出来是CM
    	 if( flag==1||S>50||S<2)		    //超出测量
    	 {
    	  flag=0;
    	  LED=1;
    	 M_sensor=1;
    	 }
       else
    		 {
    	 LED=0;
    	 M_sensor=0;
    		 }
    	}	
    
    

       4、car.h文件

    #ifndef __car_H
    #define __car_H
    
    #include <reg52.h>
    #include <intrins.h>
    
    sbit Left_moto_pwm=P1^6 ;
    sbit Right_moto_pwm=P1^7;
    sbit p34=P3^4;
    sbit p35=P3^5; 
    sbit p36=P3^6;
    sbit p37=P3^7;
    sbit Trig= P1^4; //产生脉冲引脚
    sbit Echo= P1^5; //回波引脚
    sbit LED=P0^0;
    sbit L_sensor=P2^0;
    sbit R_sensor=P2^1;
    void Left_moto_go() ;
    void Left_moto_back() ;
    void Left_moto_stp() ;
    void Right_moto_go();
    void Right_moto_back(); 
    void Right_moto_stp(); 
    void delay(unsigned int k) ;
    void delay1s(void) ;
    void delay1ms(void);
    void pwm_out_left_moto(void) ;
    void pwm_out_right_moto(void);
    void run(void);
    void back(void);
    void left(void);
    void right(void);
    void stop(void);
    void rotate(void);
    void  StartModule() ;
    void Timer1Init();
    void Timer0Init();
    void Conut(void);
    void Timer1Init2();
    void  Speed_add() ;
    void  Speed_reduce();
    
    
    #endif
    

    四、控制效果的视频演示

          手机蓝牙控制效果视频展示链接

    或者直接访问如下网址:https://www.bilibili.com/video/bv1t54y1Q7zF

    五、智能小车的一些优化方向及思路

        到这里本系列文章就要告一段落了,本系列的四篇文章介绍了,如何从零开始,慢慢的,一步步的实现了,一个51单片机控制的一个具备自动避障和蓝牙遥控双工作模式的一个小车,本来还想利用本系列第五篇文章介绍一下如何利用上位机对小车的一些变量,比如速度的大小、各个传感器是否检测到障碍物、小车的运动状态,进行监控,并将其绘制成图像,进行分析,但是之后的一段时间我会比较忙,就先不进行详细介绍了,怎么实现呢?有兴趣的可以看一下下面这篇博文,链接如下:

       详细介绍如何从0开始写一个数据通信,将数据从单片机发送到上位机(或者虚拟示波器)进行数据或图像显示,以及常见问题或注意事项解答,本文主要以匿名上位机为例,适合新手和小白

        上面这篇文章介绍的很详细,大家照着去实现上面的功能应该不难,除此之外,在本系列的第三篇文章介绍了,这个智能小车在检测前面的障碍上尚有不足,有兴趣的可以尝试一下4个漫反射光电传感器+超声波模块的避障方案。

        本文介绍的内容,已经实现了手机跟单片机之间双向的数据传输,主要用的是手机向单片机发送控制指令,单片机返回发送的指令,其实除了发送控制指令,还可以发送查询指令,比如说我们向单片机发送一个9,单片机接收到9之后,不对小车的运动进行控制,返回的值也不让它返回9,我们可以让它返回当前小车的运动信息,比如当前的车速、各传感器的工作状态、小车的运动状态等等

        当然还有很多的优化方案及思路,大家可以自行进行思考,和继续优化,当大家感觉研究的差不多之后,就可以研究一下更高级的车了,无论是从硬件上,还是控制算法上都有非常大的提升空间,毕竟本文介绍的只是一个先手入门级的小车模型

    附小车照片:

      本文介绍的内容的完整代码的keil文件和蓝牙HC—08模块的资料包(包括HC-COM这个app在内)我会放到附件里,还是那句话,我放的时候都是免费的,但是它会自己涨,需要的可以评论区留言,我直接发给你
       欢迎大家积极交流,本文未经允许谢绝转载

    展开全文
  • 智能车运动状态实时监测系统设计及实现 {转载}系统功能及应用 本系统主要完成将智能车行驶过程中的各种状态信息(如传感器亮灭,车速,舵机转角,电池电量等)实时地以无线串行通信方式发送至上位机处理,并绘制各...

    智能车运动状态实时监测系统设计及实现  

    {转载}

    系统功能及应用

      本系统主要完成将智能车行驶过程中的各种状态信息(如传感器亮灭,车速,舵机转角,电池电量等)实时地以无线串行通信方式发送至上位机处理,并绘制各部分状态值关于时间的曲线。有了这些曲线就不难看出智能车在赛道各个位置的状态,各种控制参数的优劣便一目了然了。尤为重要的是对于电机控制PID参数的选取,通过速度—时间曲线可以很容易发现各套PID参数之间的差异。对于采用CCD 传感器的队伍来说,该系统便成为了调试者的眼睛,可以见智能车之所见,相信对编写循线算法有很大帮助。而且还可以对这些数据作进一步处理,例如求取一阶导数,以得到更多的信息。

      系统的硬、软件设计

      设计方案主要分成三部分:车载数据采集系统,无线数传系统,上位机数据处理系统。系统基本构建如图1所示。





    图1智能实时监测系统结构框图与流程图



      车载数据采集系统

      车载数据采集系统主要由单片机负责采集赛车行驶过程中的速度、传感器状态、电池电压、舵机转角等信息。为了使监测系统不占用S12单片机的内部资源并且支持热插拔,我们将除供电之外的监测系统与智能车控制系统相分离。我们选择ATMEL公司的ATMEGA16单片机作为该数据采集系统的处理器。二值型光电传感器信号的采集直接使用该单片机的I/O口进行采集,连续型光电传感器、电池电压及CCD摄像头信号则采用该单片机的A/D口进行采集,速度信息则通过光电编码器和该单片机的计数器来采集。采集到的数据每20ms向上位机发送一次。

      无线数传系统

      下位机向上位机传输的是以每20ms传输一组包含了光电编码器值(speed),电池电压(battery),舵机转角值(angle),传感器当前状态 (sensor)的数据,如果是在理想状态下,上位机接收到的应是上述各值循环出现的周期性数据,此时上位机只需将这些数据按顺序装入各自的数组并画图即可。但在实际的无线传输过程中可能出现数据丢失现象。因此加入适当的数据校验是必要的,否则会出现数据装入错误,造成画图的混乱。我们在实际过程中是在每组数据中加入0x00,0xff的帧头,当数据出现错误时,则舍弃该帧数据。

      数据的无线收发部分采用的是SUNRAY公司的QC96型无线收发模块,该模块可以收发波特率为9600bps的串行数据,距离可达100m。

      上位机的数据接收及处理

      上位机部份主要由四个模块构成:

    *数据接收模块
    功能:上位机通过串口采集下位机发送的原始数据。

    实现:VC++中实现串口通信一般有二种方式,分别是MSComm控件和Windows API函数。MSComm简洁易用,适用于比较简单的系统,Windows API函数应用较广但比较复杂繁琐,由于此模块的串口通信功能相对简单,本系统采用前者。在实际过程中使用的是事件驱动的方式,这种方法响应及时,可靠性高,并且比查询法占用更少的资源。

    *存储模块
    功能:可直接存储采集到的原始数据,以备以后更多的分析处理。

    实现:在每个OnComm事件被触发后,将串口接收到的数据直接存入temp临时文件,并且可以在用户的要求下将temp临时文件里的值存储到指定文件。

    *数据的分析和处理模块
    功能:将采集到的原始数据进行用户选定的分析和处理,主要包括丢弃错误数据,以及去除帧头并进行数据装入任务,同时也可以对已保存的数据进行分析和处理。

    实现:将temp临时文件或用户指定文件的数据读出并装入,以下为主要程序代码:
    void CChuankokjDlg::find_data(car_data c_data[], BYTE c_source[], int c_length)
    {
    int i=0;
    while(i {  if((c_source[i]==0x00)&&(c_source[i+1]==0 xff)&&(c_source[i+6]==0x0 0)&&(c_source[i+7]==0xff))//
    判断帧头并进行数据装入
      {
       c_data[length].speed_data=c_source[i+2];
       c_data[length].battary_data=c_source[i+3];
       c_data[length].direction_data=c_source[i+4];
       c_data[length].sensor_data=c_source[i+5];
       i=i+1;
      }
      else
       i++;
    }
    }


    *图形显示模块
    功能:以图形界面来显示处理后的数据,以便更直观地观察智能车的运行状态。

      实现:将上一模块中装入各数组的数据在用户的选择的模式下进行画图,可以只看一幅图,也可以将四幅图放在一起进行对比观看。实际的运行界面及效果如图2。





    图2  图形显示介面运行模块


        
      结语

      该系统通过添加无线收发模块,将智能车的实时状态信息传到上位机上,通过VC++编程,用图象直观形象地将其表现出来,很好地达到了对智能车状态实时监测的目的,极大地方便了我们调节PID等智能车参数,对赛道记忆算法的研究提供了很大的帮助。

    展开全文
  • 人体运动状态预测-实例分析

    千次阅读 2017-05-27 16:10:12
    • 当传感器采集到大量数据后,我们就可以通过对数据进行分析和建模,通过 各项特征的数值进行用户状态的判断,根据用户所处的状态提供给用户更加 精准、便利的服务。数据介绍:• 我们现在收集了来自 A, B, C, D, E ...
  • 当传感器采集到大量数据后,我们就可以通过对数据进行分析和建模,通过各项特征的数值进行用户状态的判断,根据用户所处的状态提供给用户更加精准、便利的服务。2 数据介绍我们现在收集了来自 A,B,C,D,E 5位用户的可...
  • 当传感器采集到大量数据后,我们就可以通过对数据进行分析和建模,通过各项特征的数值进行用户状态的判断,根据用户所处的状态提供给用户更加精准、便利的服务。 数据介绍 我们现在收集了来自A,B,C,D,E 5位...
  • 运动时目标心率范围:运动时最高心率×0.5~运动时最高心率×0.85 代码如下: //Exercise 3.16: Target-Heart-Rate Calculator,HeartRates.java //By Pandenghuang@163.com import javax.swing.JOptionPane;; // p
  • 3、可以检查到 五种状态 ,静止,步行 ,跑步,自行车,驾车 4、每次回调结果 ,都有 本次结果准确程度的 描述 低 ,中 ,高 三个等级 #import self.motionActivityManager=[[CMMotionActivityMana
  • 在微信的“微信运动”,可以看到自己和好友的运动状态,还能确切的看到微信运动步数。微信运动步数,虽然只是一个简单的数字,但是却可以从中解读出很多有趣的个人信息。一起来看下外媒对于“微信运动”步数的相关...
  • 运动控制 —— 强大的状态机工具

    千次阅读 2019-07-26 16:27:38
    状态机,通俗的讲可以理解为一种建模方法。当一个逻辑非常复杂的程序放在面前,是非常令人头大的,使用状态转移图(是一个有向图形,包括各节点和状态转移条件)可以很好的梳理流程。以博主的理解,实现状态转移图的...
  • 运动控制

    千次阅读 2019-07-01 17:49:34
    **运动控制:** 初始化控制卡(d2210_board_init)函数 ==》 脉冲模式设置(d2210_set_pulse_otmode)函数 中断控制函数(若有) <...程 运动状态检测(d2210_check_done)函数 处 理...
  • 状态空间

    千次阅读 2015-05-25 15:27:52
    状态变量(state variables)是指在系统中所含变量个数最少的变量,也就是决定系统状态的最小数目的变量的有序集合,有时也称为状态向量(state vector),例如表示天体运动状态的位置和速度的变量。状态变量表示系统...
  • 两轮差速移动机器人运动分析、建模和控制

    万次阅读 多人点赞 2018-11-02 21:24:35
    两轮差速运动分析及建模运动学分析三种运动状态分析函数模型仿真验证直线验证曲线验证旋转验证运动控制 运动学分析 运动特性为两轮差速驱动,其底部后方两个同构驱动轮的转动为其提供动力,前方的随动轮起支撑作用...
  • 码表控制器状态转换 对应逻辑表达式: N2=~stop store ~NewRecord ~S2 S1 ~S0 + ~stop store NewRecord ~S2 S1 ~S0 + ~start ~stop ~store ~reset ~NewRecord S2 S1 ~S0 + NewRecord S2 S1 ~S0 + ~start ~stop ~...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 126,457
精华内容 50,582
关键字:

运动状态