小海's profileFlier's SkyPhotosBlogListsMore Tools Help

Blog


    September 28

    NXT 学习笔记 [3] 获取 NXT 信息 (C++)

        这个系列文章因为开始涉及到太多技术细节,跟这儿的定位不符合,改到另外一个 blog 上连载了,呵呵
     

      作为一个开放式扩展平台,LEGO 提供了 NXT 相关的非常详尽的资料。而作为二次开发不可或缺的 SDK,当然也包括在其中。在其网站的 NXT'reme 栏目中可以下载到针对 C++ 开发环境的 Driver SDK,支持 Windows 和 MAC 系统。
        Driver SDK (fantomSDK1.0.2f0.zip 2.21MB)
       
      因为 NXT 提供了 USB 和蓝牙两种连接方式,所以为了提供同一的编程接口,LEGO 通过一个 DLL 对其进行了封装。这个 DLL 有一个很 cool 的名字 —— 饭桶 (fantom.dll) -_-b
      对 USB 方式的连接,LEGO 提供的驱动 fantom.sys 与 NXT 直接进行通讯; 对蓝牙方式,则是在进行配对 (pair) 后,把 NXT 设备映射到一个 COM 端口,然后通过 Win32 文件方式访问端口进行通讯。Microsoft 在  Robotics Studio 中对 NXT 的支持就是通过后者的方式实现的。前者的好处是速度快且一次安装后使用时无需配置;后者则更为灵活,能够无线通讯,且不需要依赖特定驱动的安装。
      对于我们日常开发来说,直接使用 fantom.dll 可以很大程度上简化工作,下面我们看看如何通过 fantom 来获取 NXT 的各种信息。
      
      安装 Drvier SDK 后,在 document\fantom.chm 里是基本封装类型的使用帮助,以及一个简单的示例程序代码;includes 里是 fantom 的头文件,以及 labview 的 vi 支持库,这个后面有机会再详细介绍。targets\win32U\i386\msvc71\release 里是需要 link 的库文件。
      
      首先我们需要建立一个 iNXTIterator 对象,此对象能够对注册到系统上的 NXT 设备进行遍历。

    int _tmain(int argc, _TCHAR* argv[])
    {
      tStatus status;
      std::cout << "枚举 NXT 设备...";
      // 建立一个迭代子遍历所有的 NXT 实例
      iNXTIterator *nxtIteratorPtr = iNXT::createNXTIterator(
        false /* 不搜索通过蓝牙连接的 NXT (仅应用 USB 连接 ) */,
        0     /* 忽略蓝牙搜索的超时时间 */,
        status );
      if (status.isFatal())
      {
        std::cout << "失败,错误码:" << status.getCode() << std::endl;
        return status.getCode();
      }
      else
      {
        std::cout << "成功!" << std::endl << std::endl;
      }
      for (; status.isNotFatal(); nxtIteratorPtr->advance(status))
      {
        iNXT* nxtPtr = nxtIteratorPtr->getNXT(status);
        if (nxtPtr && status.isSuccess())
        {
         // 获取 NXT 设备的信息
       
          iNXT::destroyNXT( nxtPtr );
        }
      }
      iNXT::destroyNXTIterator(nxtIteratorPtr);
    }


       这里的 tStatus 对象是对各种错误状态的封装,因为 LEGO 提供的是调试版本的 DLL,所以还可以通过此对象取得错误发生所在的文件和行号 :S 不知道 LEGO 是不是准备后期把这块的代码也开放了。
       值得注意的是,在操作 NXT 时创建的各种 iterator 或普通对象,使用完后都必须调用相应的 destroy 方法释放,最好是用 C++ 封装一个类来管理,这里为了简洁暂且不管。
      
       在获得了 NXT 对象后,可以调用其 getDeviceInfo 和 getFirmwareVersion 获得设备的详细信息  

    const size_t MAX_DEVICENAME_LEN = 16;
    void dump(iNXT& nxt)
    {
       ViChar name[MAX_DEVICENAME_LEN];
       ViByte bluetoothAddress[6];
       ViUInt8 signalStrength[4];
       ViUInt32 availableFlash;
       tStatus status;
       nxt.getDeviceInfo(name, bluetoothAddress, signalStrength, availableFlash, status);
       if (status.isFatal())
       {
         std::cerr << "获取 NXT 设备信息失败,错误码:" << status.getCode() << std::endl;
         return;
       }
       ViUInt8 protocolVersionMajor, protocolVersionMinor;
       ViUInt8 firmwareVersionMajor, firmwareVersionMinor;
       nxt.getFirmwareVersion(protocolVersionMajor, protocolVersionMinor,
         firmwareVersionMajor, firmwareVersionMinor, status);
       if (status.isFatal())
       {
         std::cerr << "获取 NXT 设备固件版本失败,错误码:" << status.getCode() << std::endl;
         return;
       }
     
       std::cout << "设备 " << (char *)name
         << ", protocol=" << (int)protocolVersionMajor << "." << (int) protocolVersionMinor
         << ", firmware=" << (int)firmwareVersionMajor << "." << (int)firmwareVersionMinor
         << ", Flash=" << std::setprecision(4) << (double)availableFlash / 1024 << "KB"
         << std::endl;
    }

      
      因为前面指定只枚举 USB 方式连接的设备,因此这儿的蓝牙地址等信息暂时无用。
      
      输出信息大概如下:

    D:\Study\Robot\LegoDemo\Debug>LegoDemo.exe
    枚举 NXT 设备...成功!
    设备 NXT, protocol=1.124, firmware=1.3, Flash=50.32KB
      对每个 NXT 设备来说,都安装了很多缺省的模块用于对各种外设提供支持,我们可以通过 iModuleIterator 获取获得支持的模块列表。

    const size_t MAX_MODULENAME_LEN = 20;
    void dump(iModule& mod)
    {
      ViChar name[MAX_MODULENAME_LEN];
      mod.getName(name);
      std::cout << "    模块 " << std::setw(sizeof(name)) << (char *)name
        << "\tid=0x" << std::hex << mod.getModuleID() << std::dec
        << "\tsize=" << mod.getModuleSize()
        << "\tio=" << mod.getModuleIOMapSize() << std::endl;
    }
    int _tmain(int argc, _TCHAR* argv[])
    {
     // 枚举 NXT 设备
     std::cout << "  枚举设备支持模块...";
     
     iModuleIterator *modIteratorPtr = nxtPtr->createModuleIterator("*.mod", status);
     
     if (!modIteratorPtr || status.isFatal())
     {
       std::cout << "失败,错误码:" << status.getCode() << std::endl;
     }
     else
     {
       std::cout << "成功!" << std::endl;
     
       for (; status.isNotFatal(); modIteratorPtr->advance(status))
       {
         iModule *modPtr = modIteratorPtr->getModule(status);
     
         if (modPtr && status.isSuccess())
         {
           dump(*modPtr);        
     
           nxtPtr->destroyModule(modPtr);
         }
       }
     
       nxtPtr->destroyModuleIterator(modIteratorPtr);
     } 
    }


      在我的 8527 上的模块列表大致如下,此列表在以后刷 firmware 的时候有可能会变化。
      id 是模块的唯一编号;size 是模块的大小,不过似乎没用上;io 是此模块映射的 I/O 地址空间大小。

    设备 NXT, protocol=1.124, firmware=1.3, Flash=50.32KB
      枚举设备支持模块...成功!
        模块             Comm.mod   id=0x50001      size=0  io=1896
        模块            Input.mod   id=0x30001      size=0  io=80
        模块           Button.mod   id=0x40001      size=0  io=36
        模块          Display.mod   id=0xa0001      size=0  io=1720
        模块           Loader.mod   id=0x90001      size=0  io=8
        模块        Low Speed.mod   id=0xb0001      size=0  io=167
        模块           Output.mod   id=0x20001      size=0  io=100
        模块            Sound.mod   id=0x80001      size=0  io=30
        模块           IOCtrl.mod   id=0x60001      size=0  io=2
        模块          Command.mod   id=0x10001      size=0  io=32820
        模块               Ui.mod   id=0xc0001      size=0  io=40

      此外另一类重要信息是 NXT 上的文件列表,遍历的方式非常类似
    const size_t MAX_FILENAME_LEN = 20;
    void dump(iFile& file)
    {
      tStatus status;
      ViChar name[MAX_FILENAME_LEN];
      file.getName(name);
      std::cout << "    文件 " << std::setw(sizeof(name)) << std::right << (char *)name << std::left
        << "\tsize=" << std::setw(10) << std::left << file.getSize(status)
        << "\tavailable=" << file.getAvailableSize(status) << std::endl;
    }
    int _tmain(int argc, _TCHAR* argv[])
    {
     // 枚举 NXT 设备
      status.clear();
      std::cout << "  枚举设备包含文件...";
      iFileIterator *fileIteratorPtr = nxtPtr->createFileIterator("*.*", status);
      if (!fileIteratorPtr || status.isFatal())
      {
        std::cout << "失败,错误码:" << status.getCode() << std::endl;
      }
      else
      {       
        std::cout << "成功!" << std::endl;
        for (; status.isNotFatal(); fileIteratorPtr->advance(status))
        {
          iFile *filePtr = fileIteratorPtr->getFile(status);
          if (filePtr && status.isSuccess())
          {
            dump(*filePtr);
            nxtPtr->destroyFile(filePtr);
          }
        }
        nxtPtr->destroyFileIterator(fileIteratorPtr);
      }
    }


      注意在两次枚举之间,需要用 status.clear 显式重置状态,否则调用会失败。
      
      一个典型的文件列表如下,此列表包括在 My Files 菜单下的所有类型文件
      size 是文件的实际大小;available 是文件的可用大小,不过似乎也没有支持。

      枚举设备包含文件...成功!
        文件           config.txt   size=77         available=0
        文件        Timeglass.ric   size=76         available=0
        文件           hands2.ric   size=834        available=0
        文件             main.rxe   size=16930      available=0
        文件          Program.tmp   size=13         available=0
        文件         NVConfig.sys   size=1          available=0
        文件        RPGReader.sys   size=14346      available=0
        文件             Demo.rxe   size=9436       available=0
        文件        Try-Touch.rtm   size=3788       available=0
        文件        Try-Light.rtm   size=4456       available=0
        文件        Try-Sound.rtm   size=6864       available=0
        文件   Try-Ultrasonic.rtm   size=3756       available=0
        文件        Try-Motor.rtm   size=2630       available=0
        文件            Woops.rso   size=4699       available=0
        文件         faceopen.ric   size=316        available=0
        文件       faceclosed.ric   size=316        available=0
        文件        ! Startup.rso   size=8161       available=0
        文件          ! Click.rso   size=451        available=0
        文件      ! Attention.rso   size=1755       available=0

     
      实际上这种种的 API 调用,最终实现上都是通过相同的一套通讯协议完成的,也就是前面 NXT 信息中 protocol 版本所指定的协议。LEGO 在 Bluetooth Developer Kit (BDK) 中有一份 Appendix 1-LEGO MINDSTORMS NXT Communication protocol 文档详细介绍了这个协议。
      因此我们也可以直接使用此协议,向 NXT 发送命令,这也是 Microsoft Robotics Studio 之所以能通过蓝牙方式,跳过必须安装驱动的限制的原因。

    void dump(iNXT& nxt)
    {
     // 打印 NXT 基本信息
     
       ViUInt8 cmdPlayTone[] = { 0x03, 0x00, 0x18, 0x10, 0x00 };
       
       nxt.sendDirectCommand(
         false /* 此命令不需要等待响应 */,
         reinterpret_cast< ViByte* >( cmdPlayTone ), sizeof( cmdPlayTone ),
         NULL, 0 /* 没有响应缓冲区 */,
         status );
    }

      上述代码会向 NXT 设备发送一个 Play Tone 的直接命令,发出一个短促的响声。回头有机会再详细分析吧 :)
    September 26

    NXT 学习笔记 [2] 运行时数据

        前面曾经提到 NXT 的可执行文件由运行时数据 (Run-time Data)、调度 (Scheduling) 和字节码 (ByteCodes) 三部分组成。其中运行时数据可进一步分为数据空间目录 (Dataspace table of contents - DSTOC) 和数据空间缺省值 (Dataspace Defaults)。VM 在加载一个可执行文件时,会在对应的 32K 内存中划出一块专门用于存储数据空间 (Dataspace),这块内存是由一系列不同类型的数据项组成,直接对应与可执行文件中的 DSTOC,如果有缺省值的话则自动进行初始化。因而后期字节码访问数据空间时,可以直接使用进程唯一的 DSTOC 索引号定位实际数据。与字节码指令类似,可执行文件中的 DSTOC 和缺省值不被加载到内存中,直接在 Flash 卡上进行地址映射。
        而在数据空间中存储的数据项,其类型也是由 VM 预定义好的,直接在指令一级提供支持。
      
        最常见的简单数据类型就是整数了,NXT 支持 8/16/32 位有符号或无符号整数,以及一系列针对整数的算术操作符。布尔类型在 NXT 中直接以 unsigned byte 表示,0 表示 FALSE,其它值均为 TRUE。而浮点数、分数甚至复数之类都不在直接支持的范畴内,不过可以通过整数运算进行模拟,只是语法一级的问题罢了。
        较为复杂的数据类型是集合类型,NXT 提供内建数组 (array) 和簇 (cluster) 的支持。
        数组 (array) 是一组具有相同数据类型子元素的集合,并支持在运行时调整大小。例如可以定义一个长度为 0 的数组表示一组点,在运行时根据传感器采样进行扩展和填充。ASCII 字符串是一种特殊的 unsigned byte 数组,为兼容 C 的习惯其末尾会有一个 NULL(0) 作为结束符。
        簇 (cluster) 这是类似于其它语言中结构 (struct) 的概念,由一组不同类型子元素组合而成,一般被用于将相关数据封装到一起。
        NXT 中聚合类型是可以相互嵌套的,可以在一个簇里面定义几个不同类型的数组,也可以在将数组的元素类型定义为数组来模拟多维数组。
        此外 NXT 为多任务支持还提供了一种互斥量 (mutex) 类型。这种类型虽然实现上由一个 32bit 整数表示,但不能被用作聚集类型的子元素,因此只能独立存在并通过内建 OP_ACQUIRE/OP_RELEASE 指令进行操作。
      
        既然 DSTOC 将程序可用到的数据及其类型都已定义清楚,从 VM 角度就可以将之进一步分为运行时内存布局不会发生变化的静态数据,以及运行时需要进行调整的动态数据。前者包括 DSTOC 中绝大多数类型的数据,它们在编译期就有编译器根据情况确定了布局;后者则包括所有的数组类型,因为 NXT 提供动态调整数组大小的能力,所以它们在 DSTOC 中只是一个入口,由 VM 在运行器进行管理。
        落实到实现上,VM 将静态数据和动态数据放到两个不同的内存池中,前者在内存低地址区域固定,后者在内存高地址区域向下增长。如果动态内存区域超出内存池限额,系统会显示一个 File Error 的错误(不知道原因的话谁猜得出来)。
        而对程序的字节码来说,这些都是透明的,指令只需要使用全局唯一的 DSTOC 索引号即可访问到相应的数据项
      
        数据项地址 = 数据空间开始地址 + DSTOC[数据空间索引].offset
      
        对 VM 来说,静态数据基本上在编译期就确定了,它所需要关心的只是如何对动态数据进行管理。NXT 中引入了一个 dope vector (DV) 的概念,来针对每个动态数据项的变化进行跟踪。DV 结构的伪代码如下:
      
      typedef struct DopeVector {
       short Offset,    // 数组数据在内存中的偏移
       short ElementSize, // 数组子元素的大小
       short ElementCount, // 数组子元素的个数
       short BackPointer, // 未用,用于建立双向链表?
       short LinkIndex   // 下一个 DV 的索引
      } DV, *PDV;
      
        VM 维护了一个全局唯一的 dope vector array (DVA) 来跟踪在动态数据区域中分配的所有内存,所有 DV 又通过 DV.LinkIndex 以链表形式组织在一起,便于系统进行遍历和维护。当然这个 DVA 的存在对字节码来说是完全透明的,只是最大限度利用现有机制来实现自我管理。
        当字节码需要通过 DSTOC 索引访问数组时,通过 DSTOC 获得的内存偏移地址实际上指向的是一个 DV 索引,然后再根据 DV 索引从 DVA 中获取到实际的数组偏移地址。计算的方式如下:
      
      DV 索引地址 = 数据空间开始地址 + DSTOC[数据空间索引].offset
      数据项地址 = 数据空间开始地址 + DVA[DV 索引].offset
      
        通过这样一个两级索引定位,VM 可以在不需要对 DSTOC 和字节码做任何调整的情况下,根据运行时指令的需求修改数组的大小,因此所有的 DV 索引会跟 DSTOC 中的数组数据项绑定。
      
        此外对于嵌套实现的多维数组,除了第一维数组是在 DSTOC 中固定死的,其它每个子数组实际上在上一维中只是一个 DV 而已,字节码可以通过 OP_INDEX 等指令间接任意访问。

    NXT 学习笔记 [1] 系统架构

        熟悉了基本功能,接着可以看看一些技术层面的内容了,呵呵,毕竟买回来不是看手册拼模型的 :P
        Lego 这方面非常厚道,从系统结构到指令集,设置关键部件的线路图都有提供,看来是拼足了劲要把这摊子事搞大啊,呵呵
        这个手册虽然号称是可执行文件标准,但实际上对整个系统架构都做了比较详细的介绍。下面的大部分内容都是我通过阅读此手册总结或者翻译过来的,呵呵
     
        NXT 的运算核心是一个 32bit 的 ARM7TDMI 嵌入式 RISC 处理器,主频一般在 115MHz(0.18 μm)-133MHz(0.13 μm) 之间,理论上回头可以自己把它换成 236MHz(90 nm) 的 ARM7,甚至接口兼容的其它型号,ARM 9/9E/10 都提供对 ARM7 程序的二进制兼容性。存储则包括 256K Flash 和 64K RAM,容量上跟现在动辄几 G 的系统没有可比性,不过对于嵌入式系统来说勉强也算够用。此外还集成了一个 8bit 的 AVR 嵌入式 RISC 处理器,以及对应的 4K Flash 和 512B RAM (-_-b),估计是用于 I/O 或步进电机控制。
        NXT 的软件系统,主要是一个扮演 OS 角色的 ANSI C 编写的 Frimware,提供对各种传感器/步进电机的控制,并为二次开发提供虚拟机(VM)支持。用户编写的程序会被编译成 NXT 的字节码(Byte Code)指令,以后缀为 .RXE 的特定可执行文件格式存储在 Flash 上,在执行时由 VM 加载并解析,并提供 32K 的 RAM 供执行。
        NXT 的可执行文件主要分为运行时数据 (Run-time Data)、调度 (Scheduling) 和字节码 (ByteCodes) 三部分。而在执行时 VM 会根据需求,为前两部分提供对应的 RAM 空间,存储可变的运行时状态信息。
        NXT 的字节码允许用户执行常规的数学/逻辑运算、指令流控制以及 I/O 操作等等,基本上是一个精简版本的 RISC 指令集。这些指令将根据功能以名为 clump 的方式组织在一起,类似子过程。NXT 中较为特殊的指令调度信息,将提供各个 clump 分别包括哪些字节码、clump 的运行时状态以及当前 clump 执行后应加载的其它 clump。VM 会根据这些记录,在 clump 所依赖数据就绪后再调用之。clump 之间也可以相互调用,但这个调用过程必须是同步的,调用者需挂起直到调用返回。此外 clump 本身是不可重入的,如果两个不同 clump 需要调用一个共享 clump,则调用者必须通过互斥量 (Mutex) 确保不会重入,因此 clump 调用自身的递归操作也是不被支持的。
        NXT 之所以采用这样一个机制,估计是放弃了基于堆栈的思路,通过 clump 为单位来简化 VM 的设计。与之相对应的是其可视化编程环境中,常用操作都是被一个个小方块(Lego 积木?)封装起来,然后通过拖拉和逻辑组合来实现算法;这样一来实现上就较为简单,常用操作映射到 clump,组合逻辑映射到调度 (Scheduling)。不过基于其字节码支持访问内存的考虑,理论上不排除能对其架构进行改进,通过内存操作模仿堆栈来实现 clump 重入和递归的可能性。
        此外一个好消息是 VM 能够提供多个 clump 并行执行的支持,完成多个任务的 clump 在数据就绪的情况下能够逻辑上并行执行;同时一个坏消息则是虽然 VM 能在指令一级帮助解决资源冲突,但还是需要自己来判断并处理竞争条件。
      
        NXT 的程序运行可以分为四个阶段:
      
        1.验证 (Validation): 加载文件并验证版本和其它文件头信息
        2.激活 (Activation): 在 RAM 中分配并初始化运行时数据
        3.执行 (Execution): 解析文件中的字节码指令,根据调度信息运行相应 clump
        4.停止 (Deactivation): 重新初始化所有 I/O 系统和 RAM 中的数据结构
      
        值得注意的是在执行过程中,所有的字节码都不会被载入到内存,而是通过直接映射 Flash 卡到地址空间完成。这样的好处是内存只被用来保存可变得程序数据,坏处则是代码在执行的过程中无法被改变。
     
    to be continue...
     

    成功进入机器人时代 :D

        经过在 eBay 上近半个月的坚苦搏杀,终于赶在十一前拿到了向往已久的 Lego Mindstorms NXT。与那些貌似有趣但本质上只会摇尾巴的所谓娱乐机器人不同,这次 Lego 发布的 NXT 绝对是一个划时代的产品。内建 32bit ARM7 处理器,自带雷达接触传感器,最大三路步进电机输出,支持蓝牙/USB接口。最为关键的是,除了自带的图形化可视编程界面外,提供了较为完整的各种开发SDK,而 MS 更是推出了支持此平台的基于 C# 的开发环境 Microsoft Robotics studio。这意味着 NXT 已经脱离了一个玩具的范畴,第一次让机器人变得平民化,是迈向机器人时代的坚实的一步。回想当年 Intel 推出的 8086/8088 正是扮演了类似的角色,让 PC 真正成为平民化的工具,相信机器人时代的到来也不再是科幻小说中的情节。
        说了半天废话,让我们先来看看 NXT 的第一印象,好大一个包装盒,呵呵

     

        完整版本的 NXT 在 Lego 自己网站上标价 $250,但实际上通过 eBay 等方式 $200-$210 就能拿下,不过比较郁闷的是从米国邮寄回来大概需要 $50-$60,如果有朋友能从国外或香港带是最合适不过了。

        拆开大盒子,其实里面东西不算太多。最多的是大把大把的 Lego 塑料积木,这是组成各种不同机器人造型的基础原料,种类我可以负责任的说是相当的多,光十字形连接杆就有9种不同长度,各种转接头的种类更是繁多,搞得我大部分时间用在寻找合适的元件上,回头得去买个厨房用的佐料盒分门别类 :P

        除此之外在几个小盒子里面装的是 NXT 的核心部件,缺省自带一个主机兼控制器、四种传感器、三个步进电机以及一对乱七八糟的连接线啥的。具体的功效回头等我弄明白了再单独解说。:P

        再来一张核心部件的分解图,呵呵

        主机上提供是个传感器输入接口(1,2,3,4),三个步进电机输出接口(A, B, C),一个 USB 接口用于从 PC 上下载程序,一个小 LCD 黑白屏用于基本控制和状态显示,此外还内建蓝牙接口用于与外部通讯。能源部分使用六节 5 号电池,没有发现外接电源的接口。可以说整个系统设计的非常简介,在满足基本功能的前提下一点多余都没有,回头有时间将之拆开再仔细分析一把 :D 

     
        作为老牌的玩具厂商,LEGO 的可视化编程环境做的非常简单易用。从左边工具条中将各种常见操作(如A口步进电机正向转3圈,或者声音传感器收到一定大小的声音)拖动到中央工作区,然后用逻辑条件进行组合,最后点击一个 Download 按钮就会自动编译并下载到 NXT 的 Flash 上。此外右边还通过 Flash 方式提供了非常详细的 Step by Step 快速入门,可以根据上面的说明快速熟悉各种部件和环境的使用方法。
         这是我根据 Quick Start 拼装出来的 TriBot 机器人,呵呵。它首先会用雷达探测面前是否有目标(一个蓝色小球);如果有的话就冲过去,直到碰撞传感器告诉它已经到跟前;接着它会等你的暗号,击掌一声后,用两个大钳子夹起球;掉转180度最大速度跑开(没人追你啊);碰到地上有条黑线后放下球。
        呵呵,功能是弱了点,不过好歹是偶的 NXT 第一次露面,来两张特写先 :D