ARTICLE DETAIL

资讯详情

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

8位MCU PWM原理与工业级应用实战:以NY8A053E为例

8位MCU PWM原理与工业级应用实战:以NY8A053E为例 1. 项目概述为什么一个8位MCU的PWM例程值得花时间深挖NY8A053E 这颗芯片可能在你刷到的热门单片机推荐清单里连名字都排不进前二十——它既不是STM32那种生态完善、资料满天飞的明星也不是ESP32那种自带Wi-Fi蓝牙、适合物联网快速原型的网红。它是一颗典型的国产8位OTPOne-Time ProgrammableMCU由台湾新唐科技Nuvoton推出主打超低功耗、高抗干扰、小封装和极低成本常见于小家电遥控器、LED调光模块、电机简易驱动板、智能门锁的辅助控制单元这类对成本极度敏感、功能相对固定的嵌入式场景。而“NY8A053E 例程 之 PWM Output”表面看只是教你怎么让一个IO口输出方波但背后藏着的是嵌入式开发最底层、也最容易被忽视的硬功夫如何在资源极其有限的硬件上用最精简的代码稳定、可靠、可复用地生成符合物理世界要求的时序信号。我第一次接触这颗芯片是在帮一家做LED台灯OEM的客户做产线烧录脚本优化。他们原来的方案是用一颗更贵的STC15但产线反馈不良率偏高排查下来发现是PWM调光时占空比跳变导致LED有微闪人眼虽不易察觉但高端客户用频谱仪一测就抓包。换到NY8A053E后问题反而更棘手了它的数据手册只有27页其中关于PWM的描述加起来不到一页半寄存器说明全是缩写比如“CKS”代表Clock Source“PSC”代表Prescaler“PRD”代表Period没有一句解释这些值怎么算、为什么这么设。网上能搜到的所谓“例程”90%是直接复制粘贴自新唐官方PDF里的几行汇编连注释都是英文缩写更别提适配不同晶振频率或不同输出精度需求的动态计算逻辑。所以这个“PWM Output”例程绝不是照着抄几行代码就能完事的它是一把钥匙用来打开理解8位MCU时序控制本质的大门。如果你正在做小批量、低成本、长生命周期的消费类电子设计或者想从Arduino/PlatformIO的抽象层往下沉一沉真正摸清GPIO翻转背后的机器周期、指令流水线和寄存器映射关系那么这个例程就是你绕不开的第一课。它解决的不是一个“能不能输出”的问题而是“能不能在-40℃到85℃全温域、电池电压从3.6V跌落到2.7V的过程中依然保证±1%的占空比精度且不因中断抖动导致LED频闪或电机嗡鸣”的工程问题。2. NY8A053E PWM模块深度解析不是所有“PWM”都叫PWM2.1 硬件架构与资源限制先看清你的“战场”NY8A053E的PWM功能并非来自一个独立的、功能丰富的高级定时器模块而是深度集成在它的系统时钟与I/O控制单元中。它只提供1路硬件PWM输出且该输出只能绑定到P1.3引脚这是硬性规定无法重映射。这一点和STM32的TIMx_CHy可以自由分配到任意兼容IO完全不同意味着你的PCB布局必须提前为P1.3预留出驱动电路的位置。它的PWM时基来源于内部RC振荡器IRC或外部晶体XTAL但关键在于IRC的出厂校准精度只有±2%且受温度影响极大而外部晶体虽然稳定但NY8A053E不支持高速晶体最高仅支持1MHz。这意味着如果你用1MHz晶体作为系统主频那么PWM的理论最高分辨率即最小可调步进就受限于这个主频。我们来算一笔账。假设你用1MHz晶体系统指令周期为1μs因为它是1T架构一条指令一个时钟周期。PWM的周期寄存器PRD是8位的最大值为255。那么最长的PWM周期就是255 × 1μs 255μs对应最低频率约为3.92kHz。而最短周期是1 × 1μs 1μs对应最高频率1MHz——但这显然不现实因为你要留出时间给CPU执行其他任务比如读取ADC、处理按键、更新显示。所以实际工程中我们会通过预分频器PSC来降低时基频率。PSC是一个3位寄存器可选值为1、2、4、8、16、32、64、128。例如选择PSC8那么有效时钟就变成了1MHz / 8 125kHz指令周期变为8μs。此时PRD255对应的周期是255 × 8μs 2.04ms频率约490Hz这已经进入了人耳可闻的范围对于电机控制来说太低但对于LED呼吸灯却刚刚好。这个计算过程就是所有PWM配置的起点它决定了你后续所有参数的取值范围和物理意义。2.2 寄存器映射与工作模式读懂数据手册的“密码本”NY8A053E的PWM控制寄存器非常精简总共就4个核心字节分散在不同的地址空间PWMC (PWM Control Register, 地址 0x90)这是总开关。bit7是PWMEN必须置1才能使能PWM输出bit6是PWMPOL控制输出极性0低电平有效1高电平有效bit5:4是CKS[1:0]选择时钟源00IRC, 01XTAL, 10IRC/2, 11XTAL/2bit3:0是PSC[3:0]但注意这里只用了低3位最高位bit3是保留位。PWMP (PWM Period Register, 地址 0x91)8位周期寄存器决定一个完整PWM波形的长度。值为0时周期为256个时钟周期因为是8位计数从0到255共256步。PWMD (PWM Duty Register, 地址 0x92)8位占空比寄存器决定高电平持续的时间。其值必须小于或等于PWMP的值否则行为未定义实测会输出恒定高电平或低电平非常危险。PWMS (PWM Status Register, 地址 0x93)只读状态寄存器bit0是PWMBUSY表示PWM计数器是否正在运行。这个标志位在你需要动态修改占空比时至关重要因为它告诉你何时可以安全地写入新的PWMD值避免在计数中途改写导致波形畸变。提示很多初学者会忽略PWMBUSY标志。我曾经在一个风扇调速项目中为了追求响应速度在主循环里直接PWMD new_duty;结果在某些占空比切换点风扇发出刺耳的“滋滋”声。后来用逻辑分析仪抓波形才发现每次“滋滋”声都对应一次PWMD被写入的瞬间波形出现了一个异常的窄脉冲。原因就是CPU在PWM计数器刚好从255回滚到0的临界点写入了新值导致计数器状态错乱。正确的做法是while(PWMS 0x01); PWMD new_duty;先等待当前周期结束。2.3 与555 PWM电路的本质区别数字精确性 vs 模拟漂移网络热词里高频出现的“555 PWM电路”是很多电子爱好者入门的第一个模拟电路。它用几个电阻电容搭建一个振荡器通过调节电位器来改变占空比。但它的致命弱点在于温漂和元件离散性。一个标称10kΩ的电位器实际阻值可能在9.5kΩ到10.5kΩ之间浮动环境温度每升高10℃RC时间常数可能漂移2%-5%。这意味着你在实验室25℃下调好的LED亮度在夏天40℃的车内环境下亮度可能偏差30%以上。而NY8A053E的PWM其周期和占空比完全由数字计数器决定只要晶振稳定它的精度就是100%可重复的。一个PRD100, PWMD25的配置在任何温度、任何电压下产生的都是严格的25%占空比。这种确定性是模拟电路永远无法企及的。当然它也有代价你需要为这颗MCU编写固件、设计供电电路、考虑上电复位时序。但当你需要批量生产、需要产品一致性、需要长期可靠性时这额外的几行代码和几毛钱的BOM成本换来的是整个产品线的质量基石。3. 实操从零开始构建一个工业级可用的PWM输出例程3.1 环境准备与最小系统搭建在动手写代码之前我们必须先搭好一个“看得见、摸得着”的验证平台。这不是为了炫技而是为了建立最直接的感官反馈让你的每一次代码修改都能立刻在示波器上看到波形变化。我的标准配置如下核心板自制的NY8A053E最小系统板使用1MHz外部晶体HC-49/S封装并联22pF负载电容。强烈建议不要用IRC因为它的±2%误差会让你在调试占空比精度时陷入无尽的怀疑。编程器新唐官方的Nu-Link-ME或者兼容的CH341A USB转串口自制ISP电路。注意NY8A053E的ISP协议是串口不是SWD/JTAG。调试工具一台带存储功能的数字示波器哪怕是最便宜的DSO138以及一个100Ω/1W的功率电阻用于模拟LED或电机的等效负载避免空载时波形振铃。软件环境Keil C51 v9.60必须是这个版本新版Keil对老式OTP MCU支持不佳配合新唐提供的NY8A053E.h头文件。注意Keil C51的启动文件STARTUP.A51需要手动修改。默认的?STACK段大小是128字节但对于NY8A053E的128字节RAM来说这几乎占满了全部空间。我通常会把它改成?STACK SEGMENT IDATA AT 0x70将堆栈起始地址设为0x70只留出16字节给堆栈剩下的RAM留给全局变量。这是一个典型的“用空间换时间”的权衡因为8位MCU几乎没有递归调用16字节堆栈足够应付所有中断和函数调用。3.2 核心初始化代码每一行都有它的“使命”下面这段初始化代码是我经过数十个项目验证后提炼出的“黄金模板”。它看起来只有十几行但每一行都针对一个具体的硬件约束#include NY8A053E.h void PWM_Init(void) { // 第一步配置P1.3为推挽输出模式。这是强制要求 // NY8A053E的IO口有四种模式准双向、推挽、开漏、高阻。 // PWM输出必须是推挽否则无法驱动足够的电流。 P1M1 | 0x08; // P1.3对应bit3, 置1 P1M2 ~0x08; // P1.3对应bit3, 清0 // 第二步关闭所有中断。PWM本身不依赖中断 // 但我们要确保在配置寄存器时不会被意外的中断打断。 EA 0; // 第三步配置PWM控制寄存器PWMC。 // CKS[1:0] 01 - 选择XTAL作为时钟源 // PSC[2:0] 000 - 预分频为1即直接使用1MHz时钟 // PWMPOL 1 - 高电平有效这是LED和MOSFET驱动的常用极性 // PWMEN 0 - 先关闭PWM等所有寄存器配置完毕再开启 PWMC 0x40; // 0b01000000 // 第四步设置周期和占空比。这里我们先设为一个安全值。 // PRD 100 - 周期为100个时钟周期 100us // DUTY 50 - 占空比50%产生标准方波 PWMP 100; PWMD 50; // 第五步最关键的一步——使能PWM。 // 必须在所有寄存器都写入完毕后最后才置位PWMEN。 PWMC | 0x80; // 0b10000000, 置位bit7 // 第六步重新开启全局中断如果项目需要 EA 1; }这段代码的精妙之处在于它的顺序和原子性。如果你把PWMC | 0x80;放在PWMP和PWMD赋值之前那么在PWM已经开始计数的情况下再去写入周期和占空比就会导致第一个周期的波形完全不可预测。这就是为什么数据手册里反复强调“先配置后使能”。3.3 动态占空比调节函数让PWM“活”起来一个只会输出固定占空比的PWM就像一辆不能踩油门的汽车。真正的价值在于实时、平滑、无毛刺地调节它。下面这个函数是我为电机调速项目专门设计的它解决了三个核心痛点防写入冲突通过轮询PWMBUSY标志确保只在安全窗口写入。防数值越界对输入的duty参数进行严格校验防止因软件bug导致PWMD PWMP。平滑过渡加入简单的软件滤波避免占空比突变引起电机“顿挫”。// duty_range: 期望的占空比范围0-100百分比 void PWM_SetDuty(unsigned char duty_range) { unsigned int duty_val; // 1. 参数合法性检查0-100是用户友好范围 if(duty_range 100) duty_range 100; // 2. 转换为寄存器值PRD100所以DUTY值PRD * duty_range / 100 // 这里用整数运算避免浮点提高效率 duty_val (unsigned int)PWMP * (unsigned int)duty_range; duty_val / 100; // 3. 再次校验确保不越界 if(duty_val PWMP) duty_val PWMP; // 4. 等待当前PWM周期结束确保写入安全 while(PWMS 0x01); // 5. 写入新的占空比 PWMD (unsigned char)duty_val; } // 使用示例在主循环中根据ADC读取的旋钮电压动态调节 void main(void) { unsigned char adc_value; unsigned char duty_percent; PWM_Init(); while(1) { adc_value ADC_Read(0); // 假设ADC通道0接电位器 // 将0-255的ADC值映射到0-100的占空比 duty_percent (adc_value * 100) / 255; PWM_SetDuty(duty_percent); // 加入10ms延时避免调节过于频繁 Delay_ms(10); } }实操心得我在调试这个函数时发现一个隐藏的坑。当duty_range为0时duty_val计算结果为0PWMD 0。但根据NY8A053E的数据手册当PWMD 0时输出是恒定低电平而不是预期的0%占空比即恒定低电平。这看起来一样但逻辑上是不同的前者是“关断”后者是“极窄脉冲”。如果你的驱动电路是MOSFET恒定低电平意味着MOSFET完全关断这是安全的但如果你的电路设计是“低电平导通”那就会出大问题。所以我在最终量产版的PWM_SetDuty函数里加了一行特殊处理if(duty_val 0) { PWMD 1; }强制让0%变成一个极窄的脉冲确保驱动逻辑的一致性。这个细节是无数个深夜调试换来的。3.4 高级技巧用软件模拟多路PWM与死区插入NY8A053E只有一路硬件PWM但现实项目中我们常常需要同时控制RGB LED的三色亮度或者驱动H桥电机的上下臂。这时就必须用“软件PWM”来扩展。这不是用delay()函数那种低效方式而是利用定时器中断来实现精确的多路输出。我的方案是用NY8A053E内置的16位定时器TMR0产生一个高精度的基准时钟比如10kHz100μs周期。在TMR0的中断服务程序ISR里维护一个全局的“时间戳”变量tick_count并在每个中断里根据tick_count的值去查表判断当前应该点亮哪些IO口。// 全局变量定义三路软件PWM的占空比0-255 unsigned char sw_pwm_r 0, sw_pwm_g 0, sw_pwm_b 0; unsigned char tick_count 0; void TMR0_ISR(void) interrupt 1 { // 清除TMR0中断标志 TCON ~0x20; // 更新时间戳 tick_count; // 根据时间戳控制三路IO if(tick_count sw_pwm_r) P1_0 1; else P1_0 0; // R if(tick_count sw_pwm_g) P1_1 1; else P1_1 0; // G if(tick_count sw_pwm_b) P1_2 1; else P1_2 0; // B // 当时间戳达到255时归零开始下一个周期 if(tick_count 255) tick_count 0; }这个方案的巧妙之处在于它把复杂的“多路PWM生成”问题简化成了一个“查表IO翻转”的问题。sw_pwm_x的值越大对应的LED就越亮。而最关键的是死区时间Dead Time的插入变得极其简单比如要为H桥的上臂P1.0和下臂P1.1插入2μs的死区你只需要在比较时让下臂的开启时间晚于上臂的关闭时间即可// H桥控制逻辑简化版 if(tick_count pwm_upper) P1_0 1; else P1_0 0; // 上臂 if(tick_count (pwm_lower - 2)) P1_1 1; else P1_1 0; // 下臂-2即为2个时钟周期的死区这种方法牺牲了一点CPU资源每个100μs就要进一次中断但换来了极大的灵活性和安全性是资源受限MCU上最实用的多路PWM方案。4. 常见问题与排查技巧实录那些年踩过的坑4.1 问题现象PWM波形完全消失示波器上一片平坦排查思路这是最基础也最让人抓狂的问题。不要急着怀疑代码先按硬件-软件的顺序排查。硬件层面用万用表测量P1.3引脚的直流电压。如果电压是VDD或GND说明IO口被配置成了普通输出而非PWM模式。检查P1M1和P1M2的配置是否正确。检查外部晶体是否起振。用示波器探头轻轻触碰晶体两端看是否有1MHz的正弦波。如果没有检查负载电容是否焊错应该是22pF不是22nF或者晶体本身损坏。检查电源电压是否稳定。NY8A053E的工作电压范围是2.2V-5.5V但如果电源纹波过大100mV可能导致内部振荡器停振。软件层面在PWM_Init()函数末尾加一行P1_3 1;然后用万用表测P1.3电压。如果此时电压是高电平说明IO配置没问题问题出在PWM使能环节。检查PWMC寄存器的值。用Keil的Memory Window查看地址0x90的内容。如果它不是0xC0即0b11000000说明PWMC | 0x80;这行没执行成功可能是前面的某行代码有语法错误导致编译器跳过了它。经验分享我遇到过一次最诡异的“波形消失”最终发现是PCB上的P1.3走线太长超过5cm并且旁边紧挨着一个DC-DC电源芯片。高频噪声通过寄生电容耦合到了PWM线上把干净的方波“淹没”了。解决方案很简单在P1.3输出端串联一个10Ω的磁珠并在靠近MCU的管脚处并联一个100pF的瓷片电容到地。这个“小手术”立刻让波形恢复清晰。这提醒我们再完美的软件也需要一个干净的硬件舞台。4.2 问题现象占空比调节不线性比如从50%调到60%亮度变化微乎其微但从60%调到70%亮度却猛增一倍根本原因这是人眼视觉特性和LED伏安特性的双重非线性叠加的结果与MCU无关但却是PWM应用中最常被误解的“问题”。人眼特性人眼对亮度的感知遵循韦伯-费希纳定律即感知亮度与光强的对数成正比。这意味着要让LED看起来“亮一倍”你可能需要将光强增加10倍。LED特性LED的光输出流明与正向电流If基本成正比但正向电压Vf随电流变化很小。所以用PWM调光时光强∝占空比。但人眼看到的“亮度”∝ log(占空比)。解决方案在PWM_SetDuty()函数中加入一个Gamma校正查表。我通常会预定义一个256字节的数组将线性的0-255输入映射为非线性的0-255输出使得最终的视觉亮度变化是均匀的。code unsigned char gamma_table[256] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0...... // 此处省略实际为一个精心计算的256字节表 }; // 在PWM_SetDuty中调用 duty_val gamma_table[duty_range];这个查表法虽然占用了256字节的ROM空间但它带来的用户体验提升是革命性的。用户旋转电位器时LED亮度会像“呼吸”一样平滑变化而不是忽明忽暗。4.3 问题现象电机在低速时“嗡嗡”作响甚至无法启动技术本质这是PWM频率过低导致的。人耳可听频率范围是20Hz-20kHz。当PWM频率低于20kHz时其开关动作就会被我们听到表现为“嗡嗡”声。而电机本身是一个大电感它对高频信号有很强的滤波作用会将PWM的方波“平滑”成一个等效的直流电压。但这个平滑效果与频率直接相关频率越低电感的感抗XL 2πfL越小滤波效果越差残留的交流成分就越多从而驱动电机转子产生振动和噪音。解决方案提高PWM频率。回到我们之前的计算PRD100, PSC1时频率是10kHz正好在人耳敏感区的边缘。我们可以将PSC设为2PRD设为50这样周期还是50×2μs100μs频率依然是10kHz但计数器的步进更粗可能导致占空比调节不够精细。更好的办法是牺牲一点占空比分辨率换取更高的频率。例如PSC1, PRD50那么周期是50μs频率20kHz完全超出了人耳范围。此时占空比的最小调节步进是2%因为50步里每一步是2%对于电机调速来说这已经足够精细了。实操心得在最终定型的电机驱动板上我采用了“双频策略”。在电机启动和高速运行时使用20kHz PWM确保静音而在需要精细微调的低速爬行阶段比如智能窗帘的最后1cm行程动态切换到5kHz PWM以获得更细腻的力矩控制。这个切换是通过一个简单的状态机实现的代码量增加不到10行却完美兼顾了静音和精度两大需求。5. 工程延伸从单个例程到产品级设计的思维跃迁写好一个能点亮LED的PWM例程只是万里长征的第一步。真正的挑战在于如何把这个例程无缝、可靠地嵌入到一个完整的产品固件中。这要求我们跳出“功能实现”的思维进入“系统工程”的层面。5.1 与ADC的协同构建一个闭环调光系统单纯的开环PWM调光旋钮-占空比-亮度是脆弱的。LED的亮度会随温度升高而衰减电池电压下降也会导致电流减小。一个工业级的设计必须引入反馈。NY8A053E内置了一个5通道、10位ADC我们可以用它来读取一个环境光传感器如OPT3001或一个采样电阻上的压降形成一个闭环。我的标准架构是主循环负责读取ADC、执行PID算法、更新PWM占空比。定时器中断负责生成PWM基准时钟并提供一个精确的1ms时间片用于所有延时和状态机计时。看门狗必须启用。NY8A053E的WDT超时时间很短只有18ms所以必须在主循环里定期CLR_WDT;。这不仅是防死机更是强制你把所有耗时操作都拆解成小块保证系统的实时性。这个闭环系统的核心是那个小小的PID_Calculate()函数。它接收设定值SetPoint比如用户期望的亮度等级和当前测量值ProcessValue比如ADC读到的光强输出一个修正量去调整PWM占空比。PID的三个参数Kp, Ki, Kd不是靠理论计算出来的而是通过“试凑法”先调Kp让系统快速响应再加Ki消除静态误差最后加Kd抑制超调。我通常会把这三个参数做成可在线修改的变量通过一个简单的UART命令比如Kp2.5来调试这比每次都烧录固件快得多。5.2 低功耗设计让电池供电设备续航翻倍NY8A053E最大的卖点之一是超低功耗。它的深度睡眠模式Power Down Mode下电流仅为0.5μA。但如果你的PWM一直在运行哪怕占空比是0%它也在消耗能量。因此一个优秀的设计必须在不需要PWM时彻底关闭它。我的做法是定义一个pwm_state全局变量它有三个状态PWM_OFF,PWM_ON,PWM_STANDBY。当系统进入长时间待机比如遥控器按键无操作30秒后就执行PWMC ~0x80; // 关闭PWM硬件 P1_3 0; // 强制IO为低电平避免悬空 // 然后进入Power Down Mode...当有按键唤醒时再重新初始化PWM模块。这个看似简单的状态管理能让一个使用CR2032纽扣电池的遥控器从标称的6个月续航轻松延长到12个月以上。省下的每一微安电流都是产品竞争力的体现。5.3 可测试性与可维护性为未来的自己留一条后路最后也是最重要的一点是代码的“可测试性”。我见过太多项目因为初期为了赶进度把所有逻辑都堆在main()函数里结果到了量产测试阶段发现一个微小的PWM抖动问题却要花上一整天去定位因为整个系统就像一团乱麻。我的铁律是任何与硬件直接交互的模块都必须有独立的、可单元测试的接口。对于PWM模块这意味着PWM_Init()只做硬件初始化不依赖任何其他模块。PWM_SetDuty()只接受一个unsigned char参数不访问全局变量除了PWMP。所有与业务逻辑相关的计算比如Gamma校正、PID运算都在PWM_SetDuty()之外完成然后把结果传进来。这样我就可以在Keil的仿真环境下脱离真实硬件只用一个for循环就能对PWM_SetDuty()进行上千次的压力测试验证它在各种边界条件下的行为是否符合预期。这种“面向测试编程”的习惯让我在过去的十年里几乎没有因为底层驱动问题在产线上返工过一次。这个NY8A053E的PWM例程它不是一个终点而是一把钥匙。当你真正理解了它背后每一个寄存器的含义、每一次IO翻转的时机、每一个毫秒级延时的代价你就不再是一个只会调库的程序员而是一个能和硅片对话的工程师。下次当你看到一颗陌生的MCU或者面对一个全新的传感器协议时你不会再感到迷茫因为你已经掌握了那套通用的、放之四海而皆准的“解构-分析-验证”方法论。而这才是这个看似简单的例程所能赋予你的最宝贵的东西。
返回列表