1. 项目概述从寄存器到文件系统USB主机API的数据骨架在嵌入式系统里搞USB主机开发最让人头疼的往往不是协议本身而是如何把那一堆抽象的协议概念变成你代码里实实在在能用的变量和结构。手册里讲得天花乱坠什么端点、管道、传输真到写代码的时候面对着一堆_usb_host_xxx()的API函数第一个问题就是我该传什么参数进去这些参数又该怎么组织这就是数据结构的意义——它把USB协议里那些看不见摸不着的“规则”翻译成了C语言里看得见、能操作的“内存布局”。我手头这份飞思卡尔现在的NXPUSBHOST API参考手册就是一份典型的“翻译词典”。它不跟你讲太多高深的协议理论而是直接甩给你一堆typedef struct告诉你想发数据先填好这个TR_INIT_PARAM_STRUCT。想解析一个USB CDC通信设备类比如串口转换器设备那得看懂USB_CDC_DESC_ACM_PTR里每个字节是干嘛的。想通过U盘读写文件FATFS、FIL、DIR这几个结构体就是你跟文件系统打交道的全部家当。这些数据结构就是连接底层硬件驱动处理电气信号、位填充和上层应用逻辑我要读哪个文件、发什么AT命令的桥梁。理解它们你才能真正掌控USB主机这头“猛兽”而不是仅仅停留在调用几个黑盒函数。接下来我们就抛开手册里冰冷的字段列表从实际编程和调试的角度把这些关键数据结构掰开揉碎了讲清楚。2. 核心数据结构详解与设计逻辑USB主机API的数据结构大致可以分为三类传输控制类、设备描述符解析类和文件系统操作类。它们各自承担着不同的职责但又通过句柄Handle和指针相互关联共同构建起一个完整的USB主机栈。2.1 传输的基石管道与传输请求在USB通信里一切数据流动都基于“管道”Pipe。你可以把管道想象成一条连接主机和某个设备端点的虚拟线路。API用_usb_pipe_handle这个类型来代表一条管道它本质上可能就是一个整数索引或指针指向驱动内部管理该管道所有状态如带宽、调度、错误计数的一个控制块。但光有管道还不够你得告诉这条管道“现在请帮我发送/接收这么多数据”。这就是TR_INIT_PARAM_STRUCT传输请求初始化参数结构体的用武之地。这个结构体是你发起任何一次非控制传输批量、中断、同步的“任务单”。typedef struct { uint_32 TR_INDEX; // 本次传输在管道上的序号 uchar_ptr TX_BUFFER; // 发送数据缓冲区指针 uchar_ptr RX_BUFFER; // 接收数据缓冲区指针 uint_32 TX_LENGTH; // 要发送的数据长度 uint_32 RX_LENGTH; // 期望接收的数据长度 tr_callback CALLBACK; // 传输完成后的回调函数 pointer CALLBACK_PARAM; // 传给回调函数的参数 uchar_ptr DEV_REQ_PTR; // 控制传输的Setup包指针仅控制管道用 } TR_INIT_PARAM_STRUCT;关键字段实战解析TR_INDEX这个字段非常关键但它容易被忽略。它不是一个简单的序列号。在早期的USB主机控制器如OHCI/EHCI驱动中这个索引可能用于关联一个具体的传输描述符Transfer Descriptor, TD链表。当你的传输完成回调被触发时你可以通过这个TR_INDEX快速定位是哪个请求完成了尤其是在你同时发起多个异步传输时。实操心得建议用一个简单的自增计数器来管理它并把它和你应用层的任务上下文关联起来。TX_BUFFER与RX_BUFFER这里有个大坑。对于发送USB_SEND操作你只需要填充TX_BUFFER和TX_LENGTHRX_相关的字段可以置零或忽略。对于接收USB_RECV操作则填充RX_BUFFER和RX_LENGTH。但最重要的是这两个缓冲区必须保证在传输完成前一直有效且通常需要是物理上连续的内存特别是在使用DMA的主机控制器上。很多新手会犯的错误是在栈上分配一个局部数组然后把指针传进去函数一返回栈帧销毁DMA却还在往那个地址写数据结果就是内存踩踏系统崩溃。避坑指南务必使用全局数组、静态数组或从堆heap中动态分配并妥善管理的缓冲区。CALLBACK这是异步编程的核心。传输请求提交后API不会阻塞等待而是立即返回。当硬件真正完成传输或出错时驱动会在中断服务程序ISR或某个任务上下文中调用你注册的这个回调函数。回调函数的原型通常是void callback(uint_32 tr_index, uint_8 status, pointer param)。注意事项回调函数执行时间必须尽可能短不能进行大量计算或阻塞操作通常只做标记事件、释放信号量、将数据指针放入队列等轻量级工作具体的处理应交给另一个应用任务。DEV_REQ_PTR这是为控制传输特设的。控制传输是USB中最特殊、优先级最高的传输类型用于设备枚举、配置等。它分为Setup、Data可选、Status三个阶段。DEV_REQ_PTR就指向一个8字节的Setup包数据定义了这次控制请求是读设备描述符还是设置地址等。对于批量、中断、同步传输这个字段应设为NULL。2.2 设备类的语言CDC描述符结构当你的USB主机连接上一个CDC设备比如一个USB转串口芯片CP2102、FT232等你需要和它“对话”。对话的前提是你能听懂它的“自我介绍”也就是描述符。USB协议定义了标准描述符设备、配置、接口、端点而CDC类在此基础上定义了自己的类特定描述符。手册里列出了好几个CDC描述符结构我们挑最核心的USB_CDC_DESC_ACM_PTR抽象控制模型描述符和USB_CDC_DESC_UNION_PTR联合功能描述符来讲。USB_CDC_DESC_ACM_PTR这个描述符告诉你这个CDC设备具备哪些管理功能。typedef struct { uint_8 bFunctionLength; uint_8 bDescriptorType; // 固定为CS_INTERFACE (0x24) uint_8 bDescriptorSubtype; // 固定为ABSTRACT_CONTROL_MANAGEMENT (0x02) uint_8 bmCapabilities; } USB_CDC_DESC_ACM;核心在于bmCapabilities这个位图字段位0 (0x01)支持GetCommFeature/SetCommFeature/ClearCommFeature请求。这意味着主机可以通过控制传输来查询或设置一些通信特性如流控。位1 (0x02)支持SetLineCoding、SetControlLineState、GetLineCoding。这是串口功能的基石有了这个能力主机才能设置波特率、数据位、停止位、校验位通过USB_CDC_UART_CODING_PTR结构以及控制DTR/RTS信号。位2 (0x04)支持发送Break信号。位3 (0x08)支持网络连接状态通知主要用于USB网卡普通串口用不到。调试经验如果你发现你的CDC设备无法设置波特率第一件事就是去解析这个描述符看看bmCapabilities的位1是否为1。如果不是那这个设备可能不支持动态配置串口参数或者你需要用其他厂商特定的命令来配置。USB_CDC_DESC_UNION_PTR这个描述符解决了CDC设备一个关键问题接口关联。一个完整的USB CDC ACM设备通常包含两个接口一个“通信接口”Interface 0用于发送控制命令如设置波特率和一个“数据接口”Interface 1用于实际的数据收发。UNION描述符就是把这两个或多个接口“绑定”在一起声明它们属于同一个功能单元。typedef struct { uint_8 bFunctionLength; uint_8 bDescriptorType; // CS_INTERFACE uint_8 bDescriptorSubtype; // UNION (0x06) uint_8 bMasterInterface; // 主接口号通常是通信接口 uint_8 bSlaveInterface[]; // 从接口号数组通常是数据接口 } USB_CDC_DESC_UNION;为什么这很重要在设备枚举时主机驱动会遍历所有接口。当它发现一个接口的类代码是CDC (0x02)并且后面跟着一个UNION描述符时驱动就会明白“哦这个接口Master和另一个接口Slave是一伙的它们共同实现了一个CDC功能”。随后驱动才会为数据接口Slave创建对应的批量输入/输出管道而你之后的数据收发都是通过这个数据接口的管道进行的控制命令则通过通信接口的控制管道发送。2.3 跨越协议层文件系统结构体 (FATFS, FIL, DIR, FILINFO)当USB主机连接的是大容量存储设备U盘、移动硬盘时我们的目标就从“收发数据”变成了“管理文件”。这时协议栈的顶层就需要一个文件系统模块而FATFS就是这个模块的核心。这套结构体FATFS,FIL,DIR,FILINFO实际上定义了一个与底层物理驱动无关的通用FAT文件系统抽象层。它的设计非常经典在很多开源项目如FatFs中都能看到类似的身影。FATFS- 文件系统对象这个结构体代表一个已挂载的物理驱动器比如一个U盘上的一个分区。它保存了这个分区的所有“元信息”。typedef struct { uint_8 fs_type; // 文件系统类型: FS_FAT12, FS_FAT16, FS_FAT32 uint_8 drv; // 物理驱动器号 (0, 1, 2...) uint_8 csize; // 每簇的扇区数 (1,2,4,...,128) uint_32 n_fatent; // FAT表项总数 ( 簇数 2) uint_32 fatbase; // FAT表起始扇区号 uint_32 dirbase; // 根目录起始扇区号 (FAT32下是簇号) uint_32 database; // 数据区起始扇区号 uint_32 fsize; // 每个FAT表占用的扇区数 // ... 其他字段如空闲簇计数、最后分配的簇号等 } FATFS;fs_type这是最重要的字段之一。驱动在挂载时会自动检测分区类型。不同的FAT类型计算簇号和扇区号的公式略有不同。例如FAT32的根目录dirbase是一个簇号而FAT12/16的dirbase是一个固定的扇区号。fatbase,dirbase,database这三个LBA逻辑块地址是文件系统操作的“地图原点”。所有对文件、目录的访问最终都要通过公式换算成相对于database的扇区偏移量再加上database本身的值才能得到最终的物理扇区号交给底层的USB大容量存储驱动去读取。win[_MAX_SS]这是一个扇区大小的缓存窗口。文件系统为了效率不会每次读写一个字节都去访问磁盘。它会缓存最近访问的扇区比如FAT表的一个扇区或目录项所在扇区在这个窗口里。winsect就记录了当前窗口缓存的是哪个扇区的数据。这是一个典型的用空间换时间的优化。FIL- 文件对象这个结构体代表一个已打开的文件。所有针对这个文件的读写、定位操作都围绕这个对象进行。typedef struct { FATFS* fs; // 指向所属的FATFS对象 uint_32 fptr; // 文件读写指针 (当前偏移量) uint_32 fsize; // 文件大小 uint_32 org_clust; // 文件起始簇号 uint_32 curr_clust; // 文件当前指针所在的簇号 uint_32 dsect; // 文件当前指针所在簇对应的数据扇区号 uint_8 buf[_MAX_SS]; // 文件数据缓存区 // ... } FIL;链式访问FAT文件系统中文件的数据并不连续存储。org_clust是文件第一个簇的编号。要找到第N个簇的数据需要从FAT表中依次查找“簇链”。curr_clust和dsect就是用来加速这个查找过程的。当你在文件中fseek时驱动会根据目标偏移量fptr从org_clust开始沿着FAT表簇链一步步跳转直到找到对应的curr_clust然后根据簇号计算出数据扇区号dsect。buf[_MAX_SS]这是文件数据的缓存。当你用f_read读取数据时驱动会先检查你要读的数据是否已经在buf里即是否在当前的dsect。如果不是它会先从磁盘把目标扇区读到buf然后再从buf里拷贝数据到你的应用缓冲区。写操作同理是先写到buf等buf满了或者文件关闭时再一次性写回磁盘延迟写。这里有个性能陷阱频繁地读写少量数据比如每次1字节会导致大量的缓存未命中和磁盘IO性能极差。最佳实践是使用合理大小的缓冲区进行块读写。DIR与FILINFO- 目录遍历这两个结构体配合使用用于遍历目录下的文件和子目录。DIR保存了遍历目录的当前状态当前簇、当前扇区、当前目录项索引。FILINFO用于存放f_readdir()函数读出的一个目录项的信息包括文件名短名和长名、属性、大小、修改时间等。一个完整的文件操作流程示例f_mount(fatfs, “0:”, 1)挂载驱动器0。内部会读取该设备的MBR/分区表和DBR填充好FATFS结构体的所有字段。f_open(fil, “0:/test.txt”, FA_READ)打开文件。驱动会从根目录开始逐级查找路径找到文件对应的目录项从中读出org_clust和fsize初始化FIL对象。f_read(fil, buffer, 1024, bytes_read)读取1024字节。驱动根据fil.fptr计算目标簇和扇区如需则从磁盘加载数据到fil.buf再从buf拷贝到你的buffer并更新fptr、curr_clust、dsect。f_close(fil)关闭文件。如果有延迟写的数据会在此刻写回磁盘并更新文件的目录项大小、时间戳。3. 数据结构间的协同与驱动框架解析理解了单个数据结构后我们更需要看透它们是如何被驱动框架组织起来协同工作的。这就像看懂了单个齿轮还要看明白整个钟表的传动系统。3.1 设备生命周期的数据结构流转当一个USB设备插入主机端口一场由数据结构驱动的“交响乐”就开始了枚举与识别阶段主机控制器驱动HCD检测到端口连接变化发起复位和枚举流程。通过默认地址0的控制管道主机读取设备的设备描述符、配置描述符。这些描述符是原始的字节流驱动会将其解析并填充到内部对应的结构体数组中。根据设备描述符中的idVendor和idProduct或者接口描述符中的bInterfaceClass驱动会去查询一个名为驱动信息表的数组。这个表很可能就是由USB_HOST_DRIVER_INFO结构体组成的。typedef struct driver_info { uint_8 IDVENDOR[2]; uint_8 IDPRODUCT[2]; uint_8 BDEVICECLASS; // ... event_callback ATTACH_CALL; // 匹配成功后的回调函数 } USB_HOST_DRIVER_INFO;当找到匹配的驱动记录驱动框架就会调用对应的ATTACH_CALL回调。这个回调函数通常就是类驱动如大容量存储类驱动、HID驱动、CDC驱动的入口。配置与接口绑定阶段在ATTACH_CALL回调里类驱动开始工作。它首先会为设备选择一个配置f_set_configuration()然后遍历该配置下的所有接口。对于它感兴趣的接口比如接口类大容量存储它会调用_usb_host_init_pipe()之类的API为接口下的每个端点创建管道并获得_usb_pipe_handle。这些管道句柄、设备句柄、接口句柄会被打包进一个管道捆绑结构PIPE_BUNDLE_STRUCT。这个结构体是类驱动内部管理这个接口所有通信资源的“集线器”。typedef struct pipe_bundle_struct{ _usb_device_instance_handle dev_handle; _usb_interface_descriptor intf_handle; _usb_pipe_handle pipe_handle[4]; // 例如Bulk-IN, Bulk-OUT, Interrupt-IN } PIPE_BUNDLE_STRUCT;对于CDC设备类驱动在解析了UNION描述符后会为数据接口创建批量管道并将这些管道与通信接口的控制管道关联起来共同服务于一个虚拟串口。数据传输阶段应用层需要发送数据时比如向虚拟串口写一串字节它会调用类驱动提供的接口如usb_cdc_send_data()。类驱动内部会分配一个TR_INIT_PARAM_STRUCT填充好数据缓冲区、长度、回调函数然后调用_usb_host_send_data()并传入数据接口的Bulk-OUT管道句柄。USB主机API底层将这个传输请求排队硬件在合适的时机对于批量传输是在总线空闲时自动执行。完成后调用应用注册的回调通知发送完成。3.2 类驱动的抽象CLASS_CALL_STRUCT手册中提到的CLASS_CALL_STRUCT是一个非常重要的抽象。它有点像面向对象编程中的“对象”或“句柄”。typedef struct class_call_struct { _usb_class_intf_handle class_intf_handle; uint_32 code_key; pointer next; pointer anchor; } CLASS_CALL_STRUCT;class_intf_handle这是类驱动实例的句柄。当你打开一个设备比如挂载一个U盘或打开一个CDC串口时类驱动会创建一个内部实例来管理这个特定的设备接口并返回一个CLASS_CALL_STRUCT指针给你。code_key这很可能是一个“魔法数字”或校验码用于验证这个结构体指针是否有效防止应用层传入了野指针或已释放的指针导致驱动崩溃。next与anchor这暗示了驱动内部可能用一个链表来管理所有打开的类接口实例。anchor可能指向链表头。应用层视角作为开发者你通常不直接操作TR_INIT_PARAM_STRUCT或管道句柄。相反你通过类驱动提供的更高级的API来操作。例如大容量存储类你调用f_open()它内部使用CLASS_CALL_STRUCT找到对应的接口和管道构造SCSI命令包如READ10通过Bulk-OUT管道发送然后从Bulk-IN管道读取数据最后解析数据并填充FIL结构。CDC类你调用usb_cdc_set_line_coding()它内部使用CLASS_CALL_STRUCT找到通信接口的控制管道然后构造一个包含USB_CDC_UART_CODING数据的标准CDC类请求SET_LINE_CODING通过控制管道发送出去。4. 实战中的陷阱与高级调试技巧理解了数据结构只是万里长征第一步。在实际开发和调试中你会遇到各种稀奇古怪的问题。下面分享一些我踩过的坑和总结的技巧。4.1 内存对齐与字节序问题嵌入式开发中内存对齐Alignment是必须关注的问题。手册中的结构体定义其字段排列和大小是编译器相关的。例如一个uint_32类型的变量在32位ARM处理器上通常需要4字节对齐。如果你用memcpy()从一个字节流比如从USB描述符数据直接拷贝到结构体指针而该指针没有正确对齐在某些架构如ARM上会导致硬件异常Alignment Fault。解决方案使用编译器指令在定义结构体时使用#pragma pack(1)告诉编译器按1字节对齐即紧密排列。但要注意这可能会降低某些CPU访问未对齐数据的性能在x86上影响小在ARM上可能影响大甚至出错。手动解析更安全、可移植的方法是不要直接映射而是编写解析函数逐个字段地从字节流中读取并赋值给结构体成员。// 安全解析CDC ACM描述符的示例 void parse_acm_descriptor(const uint_8 *data, USB_CDC_DESC_ACM *acm) { acm-bFunctionLength data[0]; acm-bDescriptorType data[1]; acm-bDescriptorSubtype data[2]; acm-bmCapabilities data[3]; // 注意这里假设数据流是小端字节序且与CPU一致。 // 如果描述符数据来自网络或固定格式需考虑字节序转换。 }字节序EndiannessUSB协议本身是小端字节序Little-Endian的。这意味着一个16位的值0x1234在数据线上传输或存储在描述符中的顺序是低字节0x34在前高字节0x12在后。如果你的嵌入式处理器是大端如某些早期的PowerPC那么在处理uint_16、uint_32这样的多字节字段时例如描述符中的wTotalLength或FAT文件系统中的簇号必须进行字节序转换。常用的宏有LE16_TO_CPU()、CPU_TO_LE16()等。4.2 异步回调与资源管理USB传输是异步的。你提交请求(_usb_host_send_data)和请求完成回调函数被调用发生在不同的时间点甚至可能在不同的执行上下文如主循环和中断。这带来了复杂的并发问题。典型问题释放缓冲区过早void send_data() { uint8_t buffer[1024]; // ... 填充buffer ... TR_INIT_PARAM_STRUCT tr; tr.TX_BUFFER buffer; // 错误buffer是局部变量 tr.CALLBACK my_callback; _usb_host_send_data(pipe_handle, tr); // 函数返回buffer栈空间被回收但传输可能还没开始 } void my_callback(uint_32 tr_index, uint_8 status, pointer param) { // 此时DMA可能正在向已回收的buffer地址写数据导致内存错误。 }解决方案使用全局/静态缓冲区或动态分配并在回调中释放。更优雅的方式是设计一个传输请求池。启动时分配固定数量的TR_INIT_PARAM_STRUCT和对应的数据缓冲区。应用需要发送数据时从池中取一个空闲的请求结构填充后提交。在回调函数中根据tr_index或通过CALLBACK_PARAM找到对应的请求结构标记为空闲并通知应用层数据处理完成。这避免了动态内存分配的开销和碎片也简化了生命周期管理。4.3 调试描述符抓包与分析当你的设备无法被正确识别或配置时问题八成出在描述符上。最强大的调试工具就是USB协议分析仪如Beagle, Ellisys或软件方案如Wireshark配合USBPCap。它能捕获总线上所有的USB数据包。调试步骤连接分析仪捕获从设备插入到枚举失败的整个过程。在捕获的数据中找到主机发出的GET_DESCRIPTOR请求标准请求bRequest0x06。查看设备返回的描述符数据。对照USB协议规范逐个字节检查描述符长度bLength是否正确描述符类型bDescriptorType是否正确设备描述符是0x01配置描述符是0x02接口描述符是0x04端点描述符是0x05类特定描述符是0x24等配置描述符的总长度wTotalLength是否包含了其下所有接口、端点和类特定描述符的长度之和端点描述符的wMaxPacketSize是否合理对于高速批量端点应该是512对于全速中断端点最大是64。类特定描述符如CDC的Header, ACM, Union描述符的顺序和内容是否正确这是类驱动匹配失败的最常见原因。没有硬件分析仪怎么办可以在主机驱动代码中添加详细的日志在解析描述符的每一步都把读到的原始数据和解析出的结构体字段打印出来。虽然不如分析仪直观但也能定位大部分问题。4.4 文件系统操作的稳定性考量在USB大容量存储设备上操作文件系统稳定性挑战更大因为USB传输本身可能出错设备意外拔出、信号干扰且Flash存储有寿命和坏块问题。突然移除Sudden Removal这是最常见的问题。设备正在读写时被拔掉可能导致文件系统结构FAT表、目录项处于不一致的中间状态。对策实现disk_status()和disk_ioctl()函数。当底层USB驱动检测到设备移除时应向上层文件系统返回STA_NOINIT状态。FATFS模块在后续操作中检测到此状态会返回FR_NOT_READY或FR_DISK_ERR。应用层应处理这些错误关闭所有打开的文件f_close()并卸载文件系统f_unmount()。重要定期调用f_sync(fil)可以将文件的缓存数据写回磁盘减少数据丢失窗口。对于重要数据写完立即f_sync是个好习惯。写平衡与损耗均衡FAT文件系统本身不关心Flash特性。频繁更新同一个文件的同一个簇会导致对应Flash扇区过早损坏。对策这更多依赖于存储设备自身的控制器如U盘的主控。在嵌入式端我们可以做的是避免在嵌入式代码中频繁创建、删除小文件或频繁更新同一个配置文件。可以考虑将日志文件设计为循环覆盖或使用更适应Flash的文件系统如LittleFS、SPIFFS但这需要设备支持。长文件名LFN支持FATFS模块通过_USE_LFN宏来启用长文件名支持。启用后DIR和FILINFO结构体会包含lfn和lfname字段。注意长文件名在磁盘上以特殊的、隐藏的目录项序列存储。读写目录时需要正确处理这些特殊条目。如果自己实现底层的扇区读写必须保证写操作不会破坏这些隐藏的LFN条目。内存消耗启用LFN会显著增加DIR和FILINFO结构体的大小因为需要缓冲区来存储长文件名。需要根据你的RAM资源情况来配置_MAX_LFN长文件名最大长度。5. 从数据结构看USB主机栈的设计哲学回顾这些纷繁复杂的数据结构我们可以窥见USB主机栈设计的一些核心思想分层抽象最底层是TR_INIT_PARAM_STRUCT和管道句柄关心的是“如何传输一包数据”。中间层是类驱动和CLASS_CALL_STRUCT关心的是“如何与某一类设备对话”。最上层是FATFS/FIL关心的是“如何读写一个文件”。每一层都向上一层隐藏了复杂性提供了更简洁的接口。资源句柄化设备句柄、接口句柄、管道句柄、文件句柄……这种“句柄”Handle模式是系统编程的经典模式。句柄是一个不透明的标识符应用层通过它来引用内核或驱动管理的内部对象。这保证了内部数据结构的封装性和安全性也方便了资源的统一管理如通过句柄查找表。异步事件驱动通过回调函数tr_callback,event_callback将耗时的I/O操作与主程序执行流解耦。这使得单线程的嵌入式程序也能高效地处理多个并发的USB设备操作而不必轮询等待。描述符驱动USB设备的“即插即用”能力其奥秘全在描述符里。主机驱动不预设设备是什么而是通过读取并解析描述符这张“身份证”和“说明书”动态地加载合适的类驱动创建合适的数据结构管道、捆绑包来与之通信。这种设计极大地提高了系统的扩展性和灵活性。理解这些数据结构不仅仅是记住每个字段的含义更是理解这套运行机制和设计逻辑。当你再面对一个USB设备通信问题时你的思路会变得清晰是描述符没解析对是管道创建失败了是传输请求没填对还是回调函数里出了错沿着数据结构的线索去追踪问题总能被定位和解决。这份手册里的结构体就是你在USB世界里导航的罗盘和地图。