ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32 USB虚拟串口开发实战:从CubeMX配置到稳定通信全解析

STM32 USB虚拟串口开发实战:从CubeMX配置到稳定通信全解析 1. 项目概述为什么STM32的USB虚拟串口值得深挖如果你玩过STM32大概率用过串口打印调试信息。传统的做法是接一个USB转TTL模块占用一组USART引脚每次下载程序还得拔插跳线麻烦不说板子外设一多引脚资源就捉襟见肘。而STM32F103自带的USB接口其实是一块被很多人低估的宝藏。通过CubeMX配置成“USB CDC”通信设备类它就能在电脑上虚拟出一个标准的COM口实现和物理串口一模一样的功能——收发数据、打印日志、进行通信但省去了外部转换芯片释放了USART外设还让设备变成了“即插即用”的USB设备。听起来很美但实际动手坑可不少。我见过不少朋友在CubeMX里勾选了USB CDC生成的代码一编译就通过但插上电脑就是死活认不出这个COM口或者识别出来了却无法收发数据。这背后的原因远不是点几下鼠标那么简单。它涉及到USB协议栈的底层初始化时序、设备描述符的匹配、以及CubeMX生成代码中那些需要你手动补全的关键回调函数。这个项目就是要把STM32F103的USB虚拟串口从“能用”到“稳定好用”的每一步掰开揉碎不仅告诉你CubeMX怎么配置更要讲清楚每个配置项背后的含义以及生成代码后你必须亲手修改的那几处“命门”。对于嵌入式开发者尤其是从51或STM32标准库转向HAL库和CubeMX的朋友掌握USB虚拟串口是一项极具性价比的技能。它能让你的作品摆脱对专用串口调试工具的依赖简化硬件设计提升产品的集成度和美观性。接下来我会以一个最经典的STM32F103C8T6最小系统板为例带你走通从CubeMX工程创建、代码修改、到电脑端驱动安装和通信测试的全流程并附上我踩过的所有坑和解决方案。2. 核心思路与CubeMX工程创建2.1 硬件连接与时钟树考量首先明确硬件基础。STM32F103的USB模块是USB 2.0全速设备它依赖一个精确的48MHz时钟。对于F103系列这个时钟通常由系统主时钟经过PLL倍频后再通过一个专用的分频器USB预分频器得到。这是整个配置的基石时钟不对一切白费。在CubeMX中新建工程选择你的具体型号如STM32F103C8Tx。首先配置时钟树Clock Configuration在RCC配置中高速外部时钟HSE选择Crystal/Ceramic Resonator。转到Clock Configuration标签页。以最常见的8MHz外部晶振为例配置路径通常是HSE输入 -PLL source Mux选择HSE- 使能PLLCLK。在PLLMUL倍频系数下拉菜单中选择x9这样PLLCLK 8MHz * 9 72MHz。这就是系统主时钟SYSCLK。关键一步找到USB Clock的配置。在AHB Prescaler为1即HCLK72MHz的情况下APB1 Prescaler需要设置为2PCLK136MHz。然后USB Prescaler选项会出现必须选择PLLCLK divided by 1.5。计算一下PLLCLK是72MHz除以1.5正好是48MHz。这个48MHz时钟会专门供给USB模块。注意如果你的外部晶振不是8MHz或者想用内部RC振荡器HSI计算思路是一样的最终必须保证供给USB模块的时钟是精确的48MHz。HSI精度较差用于USB通信可能会不稳定强烈建议使用外部晶振。2.2 USB外设与中间件配置时钟配好后回到Pinout Configuration标签页。在左侧分类中找到Connectivity点击USB。在Mode中选择Device (FS)即全速设备模式。此时PA11USB_DM和PA12USB_DP引脚会自动被配置。接下来配置USB设备类。在左侧的Middleware and Software Packs分类下找到USB_DEVICE并点击。在Class For FS IP下拉菜单中选择Communication Device Class (Virtual Port Com)。这就是我们需要的“虚拟串口”功能。选择后下方会多出USB_DEVICE的配置子菜单。这里有几个重要参数Device FS保持默认FS全速。Product String建议修改成一个有辨识度的名字如STM32 Virtual COM Port。这个字符串会在电脑的设备管理器中显示。VID(Vendor ID) 和PID(Product ID)这是USB设备的“身份证”。ST公司为评估板预留了VID0x0483PID0x5740。如果你用的是自己设计的产品务必向USB-IF申请自己的VID或使用子VID/随机PID避免冲突。本项目为学习可暂时使用ST的默认ID。CDC设置InterfaceName可以改为VCP_Interface其他如Line Coding波特率、停止位等可以保持默认因为这些参数是在通信开始后由主机电脑通过CDC类特定请求动态设置的这里的默认值影响不大。2.3 工程管理与代码生成设置点击Project Manager标签页设置工程。Project子标签选择工程位置、设置工程名如F103_USB_VCP。Toolchain/IDE根据你的开发环境选择如MDK-ARM V5Keil或STM32CubeIDE。Code Generator子标签这是影响后续开发体验的关键。务必勾选Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral这会将每个外设的代码生成独立的文件结构更清晰。更重要的是一定要选择Copy all used libraries into the project folder。这样所有用到的HAL库、中间件文件都会被复制到你的工程目录下工程完全独立迁移到其他电脑或离线环境也不会报错。最后点击右上角的GENERATE CODE生成工程代码。首次生成会询问是否打开工程选择确认。3. 生成代码的关键修改与适配CubeMX生成的代码是一个框架对于USB CDC类有几个关键的函数需要我们自己实现。ST在中间件层预留了接口我们需要填充它们。这是项目成败的核心也是新手最容易懵的地方。3.1 定位并实现CDC类回调函数用IDE如Keil打开工程在项目文件树中找到Application/User组下的usb_device.c。这个文件是USB设备栈的入口但我们不直接改它。我们需要关注的是USB_DEVICE/App组下的两个文件usbd_cdc_if.c和usbd_cdc_if.h。usbd_cdc_if.c里面有一系列以CDC_开头的弱定义函数我们的工作就是根据应用需求实现它们。首先打开usbd_cdc_if.h确保以下函数声明存在uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len);这个函数是我们从STM32向电脑发送数据的核心API。然后打开usbd_cdc_if.c我们需要实现至少三个函数1.CDC_Control_FS函数处理类特定请求这个函数处理主机发来的控制请求比如设置串口参数波特率、数据位等。一个最简化的、能工作的实现如下static int8_t CDC_Control_FS(uint8_t cmd, uint8_t* pbuf, uint16_t length) { switch (cmd) { case CDC_SET_LINE_CODING: // 主机设置波特率等参数 // 参数存储在pbuf指向的缓冲区按顺序是波特率(4字节)、停止位(1)、校验位(1)、数据位(1) // 我们可以将其保存到全局变量供后续USART模拟或其他用途虚拟串口本身不一定需要。 lineCoding.bitrate (uint32_t)(pbuf[0] | (pbuf[1] 8) | (pbuf[2] 16) | (pbuf[3] 24)); lineCoding.format pbuf[4]; lineCoding.paritytype pbuf[5]; lineCoding.datatype pbuf[6]; break; case CDC_GET_LINE_CODING: // 主机获取当前参数 pbuf[0] (uint8_t)(lineCoding.bitrate); pbuf[1] (uint8_t)(lineCoding.bitrate 8); pbuf[2] (uint8_t)(lineCoding.bitrate 16); pbuf[3] (uint8_t)(lineCoding.bitrate 24); pbuf[4] lineCoding.format; pbuf[5] lineCoding.paritytype; pbuf[6] lineCoding.datatype; break; case CDC_SET_CONTROL_LINE_STATE: // 主机设置控制线状态如DTR、RTS // 通常用于检测串口是否打开。可以在此处设置一个标志位。 dtrState (pbuf[0] 0x01); // 检查DTR位 break; default: break; } return (USBD_OK); }你需要先在文件顶部定义lineCoding和dtrState这些全局变量结构体。2.CDC_Receive_FS函数处理接收到的数据当电脑通过虚拟串口向STM32发送数据时数据会通过这个回调函数传递进来。这是实现双向通信的关键。static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len) { // Buf: 指向接收数据缓冲区的指针 // *Len: 本次接收到的数据长度 // 示例将接收到的数据回环Echo发送回去 CDC_Transmit_FS(Buf, *Len); // 或者将数据存入你自己的应用缓冲区进行处理 // memcpy(myRxBuffer, Buf, *Len); // myRxLen *Len; // 重要重新启动接收准备接收下一包数据 USBD_CDC_SetRxBuffer(hUsbDeviceFS, Buf[0]); USBD_CDC_ReceivePacket(hUsbDeviceFS); return (USBD_OK); }这里有一个至关重要的细节在CDC_Receive_FS函数的末尾必须调用USBD_CDC_ReceivePacket来重新使能接收否则设备只会接收第一包数据之后就“哑巴”了。这是新手常踩的大坑。3. 实现CDC_Transmit_FS函数这个函数在.h文件中声明了但在.c文件中可能只有一个弱定义的骨架。我们需要一个健壮的实现处理USB传输的忙状态和超时。uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len) { uint8_t result USBD_OK; uint32_t timeout 0; // 等待上一次传输完成 while (hUsbDeviceFS.pClassData ! NULL ((USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassData)-TxState ! 0) { timeout; if(timeout 500000) // 简单超时机制防止死等 { return USBD_BUSY; } } // 启动新的传输 result USBD_CDC_SetTxBuffer(hUsbDeviceFS, Buf, Len); if (result ! USBD_OK) { return result; } result USBD_CDC_TransmitPacket(hUsbDeviceFS); return result; }这个函数先检查USB CDC层的发送状态是否空闲TxState 0如果忙则等待。这避免了数据覆盖导致的混乱。在实际应用中你可能需要将它封装成一个带更完善状态机和错误处理的应用层发送函数。3.2 主程序中的初始化和应用逻辑修改完CDC接口文件后我们来看主程序main.c。在main函数中CubeMX生成的代码会依次初始化HAL库、系统时钟、所有外设、中间件。USB设备的初始化在MX_USB_DEVICE_Init()函数中完成。一个关键点USB初始化完成后设备并不会立即被主机枚举需要你“启动”它。通常这由USB库自动处理但你需要确保主循环不被阻塞让USB任务如HAL_PCD_IRQHandler得以执行。一个典型的主循环应用示例如下int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USB_DEVICE_Init(); // 初始化USB设备栈虚拟串口就绪 uint8_t helloMsg[] Hello USB VCP!\r\n; CDC_Transmit_FS(helloMsg, sizeof(helloMsg)-1); // 上电发送一条消息 while (1) { // 示例每秒发送一次计数 static uint32_t lastTick 0; if (HAL_GetTick() - lastTick 1000) { lastTick HAL_GetTick(); char buf[50]; int len sprintf(buf, Tick: %lu\r\n, HAL_GetTick()); CDC_Transmit_FS((uint8_t*)buf, len); } // 接收处理已经在CDC_Receive_FS回调中完成例如回环 // 其他应用任务... HAL_Delay(10); // 适当延时避免CPU空转耗电 } }这个例子展示了上电后主动发送数据以及在主循环中定时发送数据。接收逻辑完全由中断驱动的回调函数CDC_Receive_FS处理主循环无需干预这是USB通信“事件驱动”特性的体现。4. 电脑端驱动安装与通信测试4.1 驱动安装与COM口识别将编译好的程序下载到STM32然后用USB线必须是数据线不能是充电线连接STM32的USB口PA11/PA12和电脑的USB口。第一次连接电脑通常会提示“正在安装设备驱动程序”。对于ST官方提供的USB CDC实现Windows 10及更高版本系统通常内置了通用的“USB串行设备USB Serial Device”驱动可以自动识别安装。安装成功后在设备管理器的“端口COM和LPT”下应该能看到一个类似USB Serial Device (COMx)的设备括号里的COMx如COM3、COM8就是虚拟出的串口号。常见问题1设备管理器里出现“未知设备”或“CDC设备”带黄色叹号这说明系统没有自动找到合适驱动。你需要手动安装ST提供的驱动。这个驱动文件通常在STM32Cube_FW_F1_Vx.x.x软件包的Drivers\CMSIS\Driver或Middlewares\ST\STM32_USB_Device_Library\Class\CDC\Drivers路径下是一个.inf文件。在设备管理器中右键点击带叹号的设备 - “更新驱动程序” - “浏览我的电脑以查找驱动程序” - 定位到该.inf文件所在文件夹即可完成安装。安装后设备会重新识别为串口。常见问题2COM口识别出来了但串口助手打开时提示“端口不存在”或“被占用”首先确认没有其他软件如另一个串口助手、IDE的串口终端已经打开了这个COM口。其次有些串口助手在打开端口时会发送DTR/RTS等控制信号。回顾我们之前实现的CDC_Control_FS函数如果收到了CDC_SET_CONTROL_LINE_STATE请求可以设置一个连接标志。更简单的做法是在串口助手软件中尝试取消勾选“DTR”、“RTS”选项再连接。4.2 使用串口助手进行双向通信测试推荐使用功能清晰的串口助手如Putty、SecureCRT或国产的XCOM、SSCOM。打开端口在串口助手软件中选择识别到的COM口如COM3。波特率可以任意设置如115200因为虚拟串口的波特率是主机软件和STM32内部lineCoding协商的物理上并无实际波特率发生器所以通常任何标准波特率都能连通。发送测试在发送区输入一些字符如Hello STM32点击发送。如果STM32端的CDC_Receive_FS函数实现了回环Echo功能你应该能在接收区立刻看到相同的内容。接收测试STM32主循环中如果有定时发送代码如之前的每秒发送Tick值你应该能在串口助手的接收区看到持续收到的数据。稳定性与压力测试尝试连续发送大量数据例如用软件的“定时发送”功能每100ms发送1KB数据观察是否会出现丢包、卡死。USB CDC的全速模式理论带宽约1MB/s8Mbps实际有效数据吞吐量在600-700KB/s左右对于大多数调试和中等数据量通信绰绰有余。如果出现丢包检查STM32端的发送函数CDC_Transmit_FS是否做好了忙等待和流控或者是否因为处理接收数据在回调函数中耗时太长影响了后续数据包的接收使能。4.3 替代方案使用VSPD创建虚拟串口对进行自发自收测试在没有第二台实体设备或想隔离测试STM32端代码时可以使用虚拟串口驱动如Virtual Serial Port Driver Pro, VSPD在电脑内部创建一对虚拟的、相互连接的COM口例如COM2和COM3。在VSPD软件中添加一对端口比如COM2-COM3。在STM32的程序中我们假设其虚拟出的端口是COM8通过USB连接真实硬件。打开两个串口助手窗口。窗口A连接COM8真实STM32窗口B连接COM2虚拟端口之一。由于COM2和COM3在电脑内部是直连的我们需要另一个软件或脚本连接COM3并转发数据。更简单的测试方法是直接用STM32COM8和其中一个虚拟口如COM2通信而COM2发送的数据会立刻出现在COM3上。你可以用第二个串口助手打开COM3来监控COM2发送的数据。这样就模拟了一个“自发自收”的环回测试环境可以用来验证STM32 USB代码的收发功能而无需实际焊接线路。实操心得VSPD工具在早期驱动逻辑验证时非常方便但它毕竟是在应用层模拟无法替代真实USB连接对底层驱动、枚举过程的测试。最终稳定性测试一定要在真实硬件上进行。5. 深入排查常见问题与解决方案实录即使按照步骤操作你可能还是会遇到一些棘手的问题。下面是我在多个项目中总结出来的“踩坑记录”。5.1 枚举失败电脑完全无法识别设备现象连接USB后电脑没有任何反应设备管理器里没有新设备或者短暂出现“未知USB设备”后消失。排查思路硬件检查这是第一步也是最容易忽略的一步。确认USB线是数据线测量STM32的VBUSPA9如果用作USB或直接来自USB口的5V电压是否正常检查PA11DM和PA12DP是否没有与其他引脚短路并且通过22欧姆电阻有些电路设计有连接到USB接口。使用示波器或逻辑分析仪查看DP/DM线上是否有数据活动是终极排查手段。时钟配置回头仔细检查CubeMX中的时钟树。确保USB Clock源是PLLCLK并且计算后精确为48MHz。一个常见的错误是APB1 Prescaler设为了172MHz导致USB Prescaler无法产生48MHz时钟。电源配置在CubeMX的Pinout Configuration-System Core-Power and Thermal (PWR)中确保USB选项如果存在被使能。对于某些型号USB模块需要独立的电源域使能。代码版本与库兼容性确认你使用的STM32CubeMX版本、HAL库版本和中间件USB Device Library版本是兼容且较新的。旧版本库可能存在已知的BUG。可以尝试在ST官网下载最新版的STM32CubeF1固件包并用CubeMX重新生成代码。5.2 驱动安装失败或COM口无法创建现象设备管理器中出现带叹号的“CDC设备”或“STM32 Virtual COM Port”手动安装.inf驱动失败。解决方案禁用驱动程序强制签名Windows 10/11对于未经微软认证的驱动有时需要此操作。在系统设置-恢复-高级启动中重启进入启动设置选择“禁用驱动程序强制签名”。彻底卸载旧驱动如果之前安装过其他版本或错误的驱动可能导致冲突。使用设备管理器在“查看”菜单中勾选“显示隐藏的设备”然后在“通用串行总线控制器”和“端口”下找到所有与STM32或之前失败安装相关的设备右键“卸载设备”并勾选“删除此设备的驱动程序软件”。重启电脑后再重新插拔设备。使用ST官方工具ST提供了一个独立的驱动安装程序STSW-STM32102可以在其官网搜索下载。这个安装包通常能更干净地安装CDC驱动。5.3 可以识别COM口但无法收发数据现象COM口出现串口助手能打开但发送数据无反应也收不到STM32发送的数据。排查步骤检查回调函数这是最高发的原因。确保usbd_cdc_if.c中的CDC_Receive_FS函数末尾一定调用了USBD_CDC_ReceivePacket(hUsbDeviceFS);。没有这行代码设备在接收第一包后就不会准备接收下一包。检查发送函数阻塞检查你实现的CDC_Transmit_FS函数。如果它在前一次发送未完成TxState ! 0时直接返回失败或死等超时设置不合理可能导致应用层发送失败。确保有合理的等待或状态检查机制。缓冲区大小在usbd_cdc.h位于Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc中定义了APP_RX_DATA_SIZE和APP_TX_DATA_SIZE。默认可能是64或256字节。如果你需要传输大量数据可以适当增大这两个值但注意不要超过USB端点缓冲区大小CDC类通常使用Bulk端点全速下最大包长为64字节但可以通过多次传输完成大块数据。主循环阻塞确保主while(1)循环中没有长时间的HAL_Delay()或阻塞式操作。USB的枚举和数据传输依赖于中断服务程序ISR。如果主循环长时间关闭全局中断或死循环USB通信会中断。使用非阻塞的编程模式或者将耗时任务拆分。使用WireShark进行USB协议抓包这是高级调试手段。安装USB抓包工具如WireShark配合USBPcap可以捕获USB总线上的所有数据包。你可以看到设备是否成功枚举主机是否发送了SET_LINE_CODING等CDC类请求以及数据端点IN/OUT上是否有数据流动。如果枚举过程的数据包都正常但应用数据没有问题就锁定在STM32的应用程序代码如果枚举请求都失败了问题就在设备描述符或底层USB驱动。5.4 数据传输不稳定、丢包或断连现象通信一段时间后数据停止收发需要重新插拔USB。分析与解决电源噪声USB线过长或板子电源滤波不好可能导致USB信号质量差。尽量使用屏蔽好、线径粗的USB线在STM32的USB DP/DM线上预留共模电感和ESD保护器件VBUS和GND加上足够的滤波电容如10uF和0.1uF并联。软件流控缺失在高速连续发送时如果STM32发送速度超过主机电脑处理速度或者STM32来不及处理接收数据会导致缓冲区溢出。虽然CDC协议支持硬件流控RTS/CTS但虚拟串口通常不实现。需要在应用层设计简单的流控协议例如“ACK”机制或者确保发送间隔合理。中断优先级冲突USB中断USB_LP_CAN1_RX0_IRQn等的优先级如果设置过低可能被其他高优先级中断长时间阻塞导致USB数据无法及时响应。在CubeMX的NVIC Configuration中适当提高USB相关中断的优先级数值越小优先级越高但要避免与系统关键中断如SysTick冲突。堆栈大小不足USB中间件和HAL库会使用一些内部缓冲区如果任务栈在RTOS中或主栈空间设置太小可能导致运行时栈溢出引发各种难以预测的故障。在IDE的启动文件或链接脚本中适当增大堆栈大小例如将Stack_Size从0x400增加到0x800。6. 进阶应用与性能优化当基础功能调通后可以考虑以下进阶方向让虚拟串口更好用。6.1 与FreeRTOS集成实现非阻塞通信在复杂的嵌入式应用中我们常使用RTOS如FreeRTOS。将USB虚拟串口集成到FreeRTOS中可以更好地管理通信任务。核心思路创建通信队列创建两个FreeRTOS队列Queue一个用于发送xTxQueue一个用于接收xRxQueue。改造接收回调在CDC_Receive_FS中断回调中不再直接处理数据而是将接收到的数据包指针和长度通过xQueueSendFromISR发送到xRxQueue。创建接收任务创建一个低优先级的任务如vTaskVCPReceive它阻塞在xQueueReceive上等待xRxQueue。一旦有数据就进行应用层解析处理。封装发送函数将CDC_Transmit_FS封装成一个任务安全的函数。应用任务想发送数据时将数据和长度打包成消息发送到xTxQueue。创建一个专用的发送任务或在一个循环中从xTxQueue取出消息调用底层的CDC_Transmit_FS进行发送。这样避免了多个任务同时调用发送函数导致的冲突。这种方式实现了中断与任务、任务与任务之间的解耦使得USB通信变得非常清晰和健壮也能充分发挥RTOS的多任务优势。6.2 实现DFU设备固件升级功能STM32的USB DFUDevice Firmware Upgrade是另一个强大的USB设备类。你可以将项目配置为复合设备Composite Device同时包含CDC虚拟串口和DFU接口。平时使用虚拟串口进行调试和通信当需要升级固件时通过一个特殊的命令如通过串口发送“ENTER DFU MODE”让设备软复位并跳转到系统存储器中内置的DFU引导程序或者跳转到你自定义的、包含DFU功能的应用程序区然后电脑就可以使用DfuSe等工具通过USB进行固件升级。这为产品开发提供了极大的便利无需再依赖JTAG/SWD调试器。在CubeMX中可以在USB_DEVICE的Class For FS IP下拉菜单下方点击Activate来激活多个设备类实现复合设备。但配置和代码处理会复杂很多需要仔细处理描述符和接口的分配。6.3 吞吐量优化与大数据传输对于需要高速数据传输的应用如图像、音频流需要对默认的CDC实现进行优化使用双缓冲或多缓冲在CDC_Receive_FS回调中不要直接处理数据而是快速将数据拷贝到另一个缓冲区然后立即重新使能接收。可以准备两个缓冲区A和B当回调使用A缓冲区时应用程序处理B缓冲区的内容反之亦然。增大端点缓冲区与数据包大小全速USB的最大包长是64字节这是硬件限制。但你可以通过让设备声明支持USB_MAX_EP0_SIZE更大的值实际上对于Bulk端点64是上限并确保每次传输都尽量填满一个包来减少协议开销。更有效的方法是使用Isochronous等时传输端点它可以保证带宽但可能丢数据适合音频等流媒体。优化发送逻辑避免频繁调用CDC_Transmit_FS发送极短的数据如单个字符。尽量在应用层攒够一定数量的数据例如凑够几十个字节后再一次性发送这样可以大幅减少USB事务开销提升有效吞吐量。关闭调试信息与优化等级在编译器中将优化等级提高到-O2或-Os尺寸优化并关闭所有不必要的打印调试可以减少代码执行时间让MCU更专注于处理USB数据流。调试一个稳定的USB虚拟串口就像在和一个严格的协议对话官打交道你必须遵循它的所有规则。从精确的48MHz时钟到那个必须调用的USBD_CDC_ReceivePacket每一个细节都关乎成败。这个过程虽然繁琐但一旦打通你会发现它带来的便利是巨大的——一根USB线同时解决了供电、程序下载配合DFU和调试通信三大问题。
返回列表