Kinetis SDK QSPI驱动架构解析与实战:从初始化到XIP模式优化

Kinetis SDK QSPI驱动架构解析与实战:从初始化到XIP模式优化
1. QSPI驱动核心架构与设计思路拆解在嵌入式开发中与外部存储器和高速外设通信是家常便饭。传统的SPISerial Peripheral Interface接口虽然简单可靠但其半双工或全双工单数据线的模式在需要高速、大数据量传输的场景下比如执行外部Quad SPI Flash中的代码或者从图像传感器读取数据就显得力不从心了。这时QSPIQuad SPI就成为了一个关键的技术选项。它不是简单地“更快”的SPI而是一种在硬件架构、总线协议和软件驱动模型上都进行了深度优化的通信接口。Kinetis SDK提供的QSPI驱动其设计核心在于将复杂的硬件操作抽象为清晰、分层的API同时兼顾灵活性与性能。整个驱动架构可以清晰地分为三个层次硬件抽象层HAL这是最底层直接面向QSPI外设的寄存器。SDK通过一系列内联静态函数如QSPI_EnableInterrupts,QSPI_GetStatusFlags封装了对控制寄存器、状态寄存器和数据寄存器的直接操作。这一层的API特点是功能单一、执行效率高但要求开发者对QSPI硬件有较深的理解通常用于极致的性能调优或实现非标准操作。功能驱动层这是驱动的主体提供了面向“功能”或“属性”的API。例如QSPI_Init用于模块的初始化和基础配置时钟源、波特率、水印值等QSPI_SetFlashConfig用于配置外部Flash的具体参数容量、命令查找表LUT、时序等QSPI_ExecuteIPCommand和QSPI_ExecuteAHBCommand用于触发预编程在LUT中的命令序列。这一层的API是驱动能力的核心体现它允许开发者精细地控制QSPI的每一种工作模式但需要开发者自行组织这些API调用的顺序和逻辑来构建完整的通信流程。事务处理层Transactional API这是最高层的抽象面向“数据传输”这个业务逻辑。它提供了阻塞式Blocking和非阻塞式Non-blocking通常配合eDMA两种传输接口。例如QSPI_TransferSendBlocking和QSPI_TransferReceiveEDMA。这一层的API极大简化了应用开发你只需要关注要发送/接收的数据缓冲区以及一个回调函数对于非阻塞模式底层的FIFO管理、状态轮询或DMA通道配置都由驱动完成。当代码体积和实时性要求不是极端苛刻时优先使用事务API能显著提升开发效率和代码可维护性。这种分层设计的好处显而易见新手可以通过事务API快速实现功能当遇到性能瓶颈或特殊需求时可以深入功能API进行定制而在进行底层调试或研究时硬件抽象层的API又提供了直接的观察和操作窗口。理解这个架构是高效使用Kinetis SDK QSPI驱动的第一步。注意在阅读SDK的fsl_qspi.h头文件时你会看到大量以static inline定义的函数。这些函数本质上就是宏的另一种安全、优雅的实现方式编译器会直接将函数体展开在调用处没有函数调用的开销。这意味着频繁调用它们如在状态轮询循环中对性能影响极小这也是驱动库追求效率的一个细节体现。2. 关键数据结构与枚举深度解析要驾驭QSPI驱动必须吃透其核心的数据结构。这些结构体不仅仅是参数的集合更是硬件工作模式的直接映射。配置错误轻则导致通信失败重则可能无法正确初始化外设。2.1 核心配置结构体qspi_config_t这个结构体决定了QSPI控制器本身的工作模式通常在初始化阶段通过QSPI_Init函数传入。typedef struct _qspi_config { uint32_t clockSource; // 时钟源选择例如 kCLOCK_BusClk 或 kCLOCK_MainClk uint32_t baudRate; // 串行Flash时钟频率波特率单位Hz uint8_t txWatermark; // 发送FIFO水印值 uint8_t rxWatermark; // 接收FIFO水印值 uint32_t AHBbufferSize[FSL_FEATURE_QSPI_AHB_BUFFER_COUNT]; // AHB缓冲区大小 uint8_t AHBbufferMaster[FSL_FEATURE_QSPI_AHB_BUFFER_COUNT]; // AHB缓冲区所属Master ID bool enableAHBbuffer3AllMaster; // 是否将AHB Buffer 3分配给所有Master qspi_read_area_t area; // 读取数据区域AHB缓冲区 或 IP FIFO bool enableQspi; // 初始化后是否立即使能QSPI模块 } qspi_config_t;关键参数解读与配置心得时钟源与波特率 (clockSource,baudRate)这是通信速率的基石。baudRate必须根据clockSource的频率来计算。例如主频为60MHz想要得到30MHz的SCK则baudRate应设为30000000。务必查阅芯片数据手册确认QSPI模块支持的最高时钟频率超频使用可能导致通信不稳定。水印值 (txWatermark,rxWatermark)这两个参数用于中断或DMA触发。当发送FIFO中的数据量低于txWatermark时可以触发Tx FIFO Fill事件用于DMA请求当接收FIFO中的数据量达到rxWatermark时可以触发Rx FIFO Watermark事件用于中断。合理设置水印值可以平衡响应速度和系统开销。对于大数据量传输结合DMA时水印值通常设置为FIFO深度的一半或四分之三以实现持续的流水线操作。AHB缓冲区配置 (AHBbufferSize,AHBbufferMaster)这是Kinetis QSPI的高级特性用于在AHB总线上实现类似“内存映射”式的高效访问。芯片通常有多个AHB缓冲区例如4个。你可以将它们分配给不同的总线Master如Core0, Core1, DMA等并指定每个缓冲区的大小。这允许多个总线主设备高效地并发访问QSPI Flash而无需软件仲裁。enableAHBbuffer3AllMaster是一个特殊选项将Buffer 3设置为对所有Master可见常用于共享的只读数据区。读取区域 (area)选择kQSPI_ReadAHB时数据通过AHB缓冲区读取适合CPU直接访问延迟低。选择kQSPI_ReadIP时数据从IP命令FIFO读取适用于通过IP命令序列读取的数据。在配置Flash执行内存映射XIP模式时必须选择kQSPI_ReadAHB。2.2 Flash专用配置结构体qspi_flash_config_t这个结构体描述了连接在QSPI总线上的具体Flash芯片的特性通过QSPI_SetFlashConfig函数配置。typedef struct _qspi_flash_config { uint32_t flashA1Size; // Flash A1端口设备大小 uint32_t flashA2Size; // Flash A2端口设备大小 uint32_t flashB1Size; // Flash B1端口设备大小 uint32_t flashB2Size; // Flash B2端口设备大小 uint32_t lookuptable[FSL_FEATURE_QSPI_LUT_DEPTH]; // 命令查找表(LUT) uint32_t dataHoldTime; // 数据保持时间 uint32_t CSHoldTime; // 片选保持时间 uint32_t CSSetupTime; // 片选建立时间 uint32_t cloumnspace; // 列地址空间大小用于HyperBus等 uint32_t dataLearnValue; // 数据学习模式采样值 qspi_endianness_t endian; // 数据字节序 bool enableWordAddress; // 是否使能字地址模式 } qspi_flash_config_t;核心中的核心命令查找表 (LUT) LUT是QSPI驱动Flexible的体现也是配置中最复杂的一环。它是一个由若干32位指令组成的表每条指令定义了一个完整的通信序列中的一个“步骤”。Kinetis QSPI的LUT指令格式非常灵活可以定义操作码Opcode发送给Flash的命令字节如读数据命令0x03(READ),0xEB(Fast Read Quad I/O)。操作码后的字节数地址周期、模式字节、空指令周期数。数据阶段读或写的数据位数以及使用的数据线数量1线2线4线。停止条件指令序列的结束。例如定义一个标准的Quad I/O Read命令序列0xEB, 24位地址4个空周期随后4线读取数据需要在LUT中填充多条指令。SDK通常会提供一些常用Flash命令的LUT定义宏但针对特定型号的Flash必须严格参照其数据手册的“指令集”章节来构建LUT。时序参数 (dataHoldTime,CSHoldTime,CSSetupTime) 这些参数用于微调Flash的时序以满足其建立/保持时间的要求。单位通常是QSPI模块时钟周期。在高速通信下如80MHz以上这些参数的配置变得至关重要。如果读取数据不稳定首先应该检查这些时序参数是否满足Flash芯片手册要求并考虑在PCB布局上是否存在信号完整性问题。2.3 状态与错误枚举_qspi_flags与_qspi_error_flags驱动提供了丰富的状态和错误标志这是调试QSPI通信问题的“仪表盘”。状态标志 (_qspi_flags)例如kQSPI_TxBufferFull,kQSPI_RxWatermark,kQSPI_Busy。在阻塞式传输函数中驱动内部就是通过轮询kQSPI_Busy这类标志来判断传输是否完成的。错误标志 (_qspi_error_flags)这是排查问题的关键。常见的错误包括kQSPI_TxBufferUnderrun发送速度过快FIFO已空但还在尝试发送。可能原因是SCK频率过高或CPU/DMA填充数据不及时。kQSPI_RxBufferOverflow接收速度过快FIFO已满但还有数据到来。同样可能与时序或数据处理速度有关。kQSPI_IllegalInstruction执行的LUT索引超出了有效范围或LUT表未正确编程。kQSPI_AHBSequenceError/kQSPI_AHBIllegalBurstSize在AHB访问模式下总线访问序列或突发长度不符合QSPI模块的配置。在初始化完成后或通信异常时调用QSPI_GetErrorStatusFlags()并检查这些标志位可以快速定位问题的大致方向。3. 从零开始QSPI驱动完整初始化与基础通信流程理论铺垫完毕我们进入实战环节。假设我们要驱动一颗常见的Quad SPI NOR Flash如Winbond W25Q128JV实现基本的擦除、编程和读取操作。3.1 硬件连接与引脚复用检查首先确保你的Kinetis MCU的QSPI引脚通常包括SCLK, CS#, IO0, IO1, IO2, IO3已正确连接到Flash芯片。然后在代码中需要配置这些引脚的复用功能。虽然输入材料中提供了PORT模块的API如PORT_SetPinMux但在实际使用Kinetis SDK时更推荐使用引脚配置工具如MCUXpresso Config Tools生成初始化代码或者使用CLOCK_EnableClock和IOCON_PinMuxSet等函数进行配置确保引脚被正确设置为QSPI功能。3.2 初始化步骤详解以下是基于Kinetis SDK的典型初始化代码框架#include fsl_qspi.h #include fsl_clock.h /* 1. 定义配置结构体变量 */ qspi_config_t qspiConfig; qspi_flash_config_t flashConfig; /* 2. 获取默认的QSPI控制器配置 */ QSPI_GetDefaultQspiConfig(qspiConfig); /* 3. 根据硬件设计修改关键参数 */ qspiConfig.baudRate 30000000UL; // 设置SCK为30MHz qspiConfig.rxWatermark 4; // 根据FIFO深度设置例如深度为8时设为4 qspiConfig.txWatermark 4; qspiConfig.area kQSPI_ReadAHB; // 准备用于内存映射读取 qspiConfig.enableQspi true; // 初始化后立即使能 /* 4. 计算并设置AHB缓冲区假设使用Buffer 0给Core访问 */ uint32_t ahbBufferSize EXAMPLE_QSPI_AMBA_BASE_ADDR FLASH_SIZE; qspiConfig.AHBbufferSize[0] ahbBufferSize; qspiConfig.AHBbufferMaster[0] 0; // Master ID 0 通常是Cortex-M内核 /* 5. 初始化QSPI控制器模块 */ QSPI_Init(EXAMPLE_QSPI, qspiConfig, EXAMPLE_QSPI_CLOCK_FREQ); /* 6. 配置外部Flash参数 */ memset(flashConfig, 0, sizeof(flashConfig)); flashConfig.flashA1Size FLASH_SIZE; // Flash总大小例如 16MBits 2MBytes flashConfig.endian kQSPI_64LittleEndian; // 根据Flash数据格式设置 /* 7. 配置Flash访问时序单位模块时钟周期 */ flashConfig.dataHoldTime 1; // 数据保持时间 flashConfig.CSHoldTime 1; // 片选保持时间 flashConfig.CSSetupTime 1; // 片选建立时间 /* 8. 构建并填充命令查找表(LUT) - 这是最关键也是最繁琐的一步 */ /* 假设LUT深度为16我们需要为几个基本操作定义序列 */ /* 索引0-3: 写使能 (WREN, 0x06) */ flashConfig.lookuptable[0] QSPI_LUT_SEQ(CMD_SDR, PINS1, 0x06, STOP, 0); /* 索引4-7: 扇区擦除 (SE, 0x20) - 需要地址 */ flashConfig.lookuptable[4] QSPI_LUT_SEQ(CMD_SDR, PINS1, 0x20, RADDR_SDR, PINS1, 24); /* 索引8-11: 页编程 (PP, 0x02) - 需要地址和数据 */ flashConfig.lookuptable[8] QSPI_LUT_SEQ(CMD_SDR, PINS1, 0x02, RADDR_SDR, PINS1, 24); flashConfig.lookuptable[9] QSPI_LUT_SEQ(WRITE_SDR, PINS1, 0, STOP, 0); /* 索引12-15: 快速四线I/O读取 (Fast Read Quad I/O, 0xEB) */ flashConfig.lookuptable[12] QSPI_LUT_SEQ(CMD_SDR, PINS1, 0xEB, RADDR_SDR, PINS4, 24); flashConfig.lookuptable[13] QSPI_LUT_SEQ(DUMMY_SDR, PINS4, 6, READ_SDR, PINS4, 0); /* 9. 应用Flash配置 */ QSPI_SetFlashConfig(EXAMPLE_QSPI, flashConfig);实操心得构建LUT表时QSPI_LUT_SEQ宏是得力助手但它内部的参数顺序和含义需要仔细查阅SDK用户手册。一个非常实用的调试技巧是在初始化完成后可以写一个简单的函数通过memcpy或直接寄存器读取的方式将编程到QSPI模块LUTRAM中的内容打印出来与你的预期序列进行逐条比对确保每一位都正确无误。很多通信失败的根本原因就是LUT表配置错误。3.3 执行命令与数据传输初始化完成后就可以通过QSPI与Flash交互了。阻塞式写入数据页编程status_t QSPI_FlashPageProgram(uint32_t address, uint32_t *data, size_t size) { status_t status kStatus_Success; /* 1. 写使能 */ QSPI_ExecuteIPCommand(EXAMPLE_QSPI, 0); // 执行LUT索引0的命令WREN /* 2. 等待Flash内部写操作完成轮询状态寄存器 */ while(QSPI_FlashIsBusy()) { // 可以加入超时机制 } /* 3. 设置IP命令地址和大小 */ QSPI_SetIPCommandAddress(EXAMPLE_QSPI, address); QSPI_SetIPCommandSize(EXAMPLE_QSPI, size); /* 4. 执行页编程命令序列LUT索引8并传输数据 */ /* 注意QSPI_WriteBlocking 会阻塞直到所有数据写入Tx FIFO */ QSPI_ExecuteIPCommand(EXAMPLE_QSPI, 8); // 启动页编程命令含地址阶段 QSPI_WriteBlocking(EXAMPLE_QSPI, data, size); // 写入数据阶段 /* 5. 再次等待操作完成 */ while(QSPI_FlashIsBusy()) { // 超时处理 } return status; }阻塞式读取数据快速读status_t QSPI_FlashFastRead(uint32_t address, uint32_t *buffer, size_t size) { /* 1. 设置IP命令地址和大小 */ QSPI_SetIPCommandAddress(EXAMPLE_QSPI, address); QSPI_SetIPCommandSize(EXAMPLE_QSPI, size); /* 2. 执行快速读命令序列LUT索引12 */ QSPI_ExecuteIPCommand(EXAMPLE_QSPI, 12); /* 3. 阻塞式读取数据 */ QSPI_ReadBlocking(EXAMPLE_QSPI, buffer, size); return kStatus_Success; }使用eDMA进行非阻塞传输 对于大数据量的连续读写例如更新显示屏帧缓冲区使用eDMA可以极大解放CPU。qspi_edma_handle_t g_qspiEdmaHandle; edma_handle_t g_edmaHandle; /* 初始化eDMA句柄需先配置eDMA控制器 */ EDMA_CreateHandle(g_edmaHandle, EXAMPLE_DMA_BASEADDR, EXAMPLE_QSPI_TX_DMA_CHANNEL); /* 创建QSPI的eDMA发送句柄 */ QSPI_TransferTxCreateHandleEDMA(EXAMPLE_QSPI, g_qspiEdmaHandle, QSPI_UserCallback, NULL, g_edmaHandle); /* 准备传输 */ qspi_transfer_t xfer; xfer.data (uint32_t*)source_buffer; xfer.dataSize data_length_in_bytes; /* 启动非阻塞eDMA传输 */ status_t status QSPI_TransferSendEDMA(EXAMPLE_QSPI, g_qspiEdmaHandle, xfer); /* 此时CPU可以处理其他任务传输完成后会调用 QSPI_UserCallback */4. 高级应用内存映射模式XIP与性能优化QSPI最强大的特性之一是支持内存映射模式eXecute-In-Place, XIP。在此模式下外部QSPI Flash的一部分地址空间会被映射到处理器的内存地址总线上。CPU可以像读取内部Flash一样直接使用指针访问外部Flash中的数据甚至从中取指执行代码无需调用任何驱动API。4.1 配置XIP模式的关键步骤正确的AHB缓冲区配置这是实现XIP的基础。必须确保qspi_config_t中的area字段设置为kQSPI_ReadAHB并且为AHB缓冲区分配了足够的、连续的内存空间其基地址EXAMPLE_QSPI_AMBA_BASE_ADDR需要与链接脚本中定义的Flash区域匹配。构建正确的读取命令LUT用于XIP的读取命令序列必须配置在LUT的特定位置通常是索引0。这个序列必须是“仅读”操作不包含写使能、擦除等命令。通常使用Fast Read Quad I/O (0xEB)命令以获得最高性能。配置Flash进入XIP模式一些Flash芯片如部分Adesto, Macronix型号有专门的“进入XIP”命令可以进一步优化性能。你需要先通过普通的IP命令发送这个模式切换命令之后AHB读取才会生效。链接脚本修改在IDE的链接脚本如GCC的.ld文件中需要定义一个内存区域其起始地址就是EXAMPLE_QSPI_AMBA_BASE_ADDR大小与分配的AHB缓冲区一致。然后将部分只读数据段如.text,.rodata放置到这个区域。4.2 性能调优实战经验即使通信通了要达到标称的最高性能还需要精细调优SCK时钟与时钟分频在QSPI_Init中设置的baudRate是理论值。实际最高稳定频率受限于PCB布线、Flash芯片本身性能以及MCU的I/O速度。建议从较低频率如10MHz开始测试逐步提高并使用逻辑分析仪或示波器观察SCK和数据线的波形是否清晰建立/保持时间是否足够。DQS数据选通模式对于支持DQS的Flash如HyperFlash或Octal Flash启用DQS可以更精确地锁存数据允许运行在更高频率。这需要配置qspi_dqs_config_t结构体并调用相关函数SDK可能提供QSPI_EnableDQS等。这涉及到相位延迟链Delay Tap的校准是一个比较高级的调试过程。AHB缓冲区策略如果系统有多个Master如核心、DMA控制器、其他总线桥合理分配AHB缓冲区可以避免访问冲突提升整体系统效率。例如将频繁执行的XIP代码区放在一个独占缓冲区将数据区放在另一个缓冲区。中断与DMA的使用对于实时性要求高的系统避免在中断服务程序ISR中使用阻塞式API。优先使用eDMA进行数据传输并通过回调函数或信号量通知任务。同时合理使能kQSPI_TxBufferUnderrunInterruptEnable等错误中断以便在通信异常时能快速响应。5. 调试陷阱与常见问题排查实录在实际项目中QSPI驱动不工作或者工作不稳定是常态。下面是我踩过的一些“坑”以及排查思路整理成速查表希望能帮你快速定位问题。现象可能原因排查步骤与解决方案初始化失败QSPI_Init后模块无响应1. 时钟未使能。2. 引脚复用未配置。3. 基地址QuadSPI_Type *base错误。1. 检查CLOCK_EnableClock(kCLOCK_Qspi0);是否调用。2. 使用调试器或IO口翻转确认SCK引脚是否有输出。检查引脚MUX配置。3. 核对芯片头文件中的QSPI外设基地址宏定义如QuadSPI0。可以发送命令但Flash无响应CS线正常1. LUT命令序列错误。2. Flash芯片未正确供电或未解复位。3. Flash处于深度省电模式。4. 时序参数不满足。1.首要步骤用逻辑分析仪抓取SPI总线波形比对命令、地址、数据是否与Flash手册一致。2. 检查Flash的VCC、HOLD#、WP#引脚电平。3. 尝试发送“释放省电/复位使能”命令如0x660x99。4. 增加CSSetupTime和CSHoldTime降低baudRate再试。读取数据全为0xFF或随机错误1. 读取命令LUT配置错误如数据线数量、 dummy cycles。2. Flash未擦除读出的就是0xFF。3. AHB缓冲区地址映射错误。4. 字节序 (endian) 设置错误。1. 再次核对Flash数据手册中读取命令的精确时序图特别是Dummy Cycle数量。2. 先尝试擦除一个扇区再读取。3. 确认qspi_config_t.area设置与读取方式匹配IP命令读 vs AHB映射读。4. 尝试切换kQSPI_64LittleEndian和kQSPI_32BigEndian。写入/擦除操作失败1. 写使能WREN命令未成功执行或未等待。2. Flash处于写保护状态WP#引脚为低。3. 地址不对齐页编程必须页对齐扇区擦除必须扇区对齐。4. 操作后未等待忙状态结束就进行下一步。1. 在发送编程/擦除命令前单步调试确保执行了WREN LUT序列并读取状态寄存器确认WEL位被置位。2. 检查硬件WP#引脚电平并读取状态寄存器的BP位确认软件写保护区域。3. 编程时确保size不超过一页地址是页大小的整数倍。4. 在每次写操作后循环读取状态寄存器直到BUSY位为0并添加超时退出机制。使用eDMA传输数据错位或丢失1. DMA传输的数据宽度8位/16位/32位与QSPI FIFO数据宽度不匹配。2. DMA传输大小字节数设置错误。3. 源/目标地址未对齐。4. 水印值设置不合理导致DMA请求过早或过晚。1. 检查eDMA通道的TCD配置确保ATTR.SSIZE和DSIZE与QSPI数据寄存器访问方式一致通常是32位。2.xfer.dataSize是字节数而DMA传输次数CITER应据此计算。3. 确保缓冲区地址是32位对齐的以获得最佳性能。4. 调整txWatermark/rxWatermark配合逻辑分析仪观察DMA请求信号如QSPI_TXDMAREQ与SCK的关系。启用XIP后程序跑飞或HardFault1. 链接脚本中XIP区域地址与AHBbufferSize不匹配。2. 放置在XIP区域的函数或数据其访问速度超过Flash最大允许频率。3. Cache未启用或配置错误导致取指延迟。4. AHB缓冲区被意外修改或覆盖。1. 仔细检查链接脚本确认VMA运行地址与EXAMPLE_QSPI_AMBA_BASE_ADDR完全一致。2. 在系统初始化早期如SystemInit函数中先以较低频率初始化QSPI和Flash完成Flash模式切换如进入XIP后再逐步提高系统主频和QSPI波特率。3. 如果MCU支持使能指令缓存I-Cache可以极大提升XIP性能。4. 避免在XIP运行时对用作XIP的AHB缓冲区对应的物理Flash区域进行写操作。一个经典的调试流程当你遇到任何QSPI问题时请按以下顺序排查硬件第一用万用表检查电源和地用示波器检查上电时序和复位信号。信号完整性用示波器或逻辑分析仪抓取SPI总线至少抓SCK, CS#, IO0。这是最直接的证据可以立刻看出时钟是否正常、命令格式是否正确、数据线是否有活动。软件配置对照芯片参考手册和Flash数据手册逐行检查初始化代码特别是LUT表和时序参数。分步测试先实现最简单的功能如读取Flash的JEDEC ID命令0x9F。这是一个只读命令不涉及擦写成功率高。一旦ID读取正确证明硬件链路和基础LUT配置是通的再逐步测试写使能、读状态、擦除、编程等复杂操作。利用SDK状态与错误标志在关键操作初始化、执行命令、传输数据前后调用QSPI_GetStatusFlags和QSPI_GetErrorStatusFlags将返回值打印出来与枚举值对比可以快速锁定是FIFO溢出、下溢还是命令序列错误。最后保持耐心。QSPI的调试尤其是高速模式和XIP的调试往往是硬件、软件、时序交织在一起的复杂问题。系统性的排查方法和清晰的逻辑分析仪波形是你最可靠的战友。当你看到CPU第一次从外部QSPI Flash中流畅地取指执行时那种成就感会让你觉得这一切都是值得的。