ARTICLE DETAIL

资讯详情

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

嵌入式面试必考:UART、SPI、I2C、RS485、Modbus与CAN协议核心解析

嵌入式面试必考:UART、SPI、I2C、RS485、Modbus与CAN协议核心解析 嵌入式面试中有一道“必答题”级别的题说说你做过项目的通讯协议。很多人的回答是“我用了 UART 收发数据”然后就没有然后了。真正写过工业设备的工程师会知道这句回答背后涉及的东西非常多物理层电平是不是匹配、帧和帧之间怎么分辨、校验错误怎么处理、总线上一台设备掉线会不会影响整条链路、重传策略会不会导致总线风暴。这些内容既是“八股”也是每天都在发生的工程问题。这篇“嵌入式八股3-通讯协议”我的目标不是让你背几道题而是在面试和实战之间架一座桥。读完你至少要能答出三件事主流通讯协议在物理层、链路层、应用层各解决什么问题UART、I2C、SPI、RS485、Modbus、CAN 这些协议的核心机制和典型坑点以及给你一个真实场景你能独立设计一版可靠的帧协议。接下来按“协议分层 → 逐协议拆解 → 状态机代码 → 面试应答 → 工程排查”的顺序展开中间有几段可直接复制的 C 代码方便你先跑通再深入。1. 通讯协议为什么是嵌入式面试的“必考题”很多时候面试官问通讯协议不是要考察你背了多少术语而是在判断你有没有完整的“链路意识”。他关心的是当你写一个串口程序的时候知不知道从 MCU 引脚出去之后电信号经过了什么、到了对端之后数据怎么对齐当总线上挂很多设备的时候你是不是只会站在 main 的 while 循环里挨个轮询一遍。我总结一个个人判断能把协议分层讲清楚的人代码一般是模块化的答不出分层的人往往把收发逻辑堆在主循环里。这个判断在绝大多数候选人身上都成立。所以通讯协议在嵌入式面试里地位高的原因主要有三个它直接反映你有没有系统设计意识。协议不是“收发函数”那么简单而是链路、时序、可靠性和状态机设计的整体。它是真实项目中集成和联调的主要矛盾。MCU 程序写完后真正的联调时间一大半花在通讯上。它是快速区分“三年经验”的重要领域。同样是做串口有人只会轮询读寄存器有人会上 DMA 加空闲中断加协议解析状态机。读这篇文章之前建议先建立一个认知通讯不是“把数据发出去”而是“把数据可靠地从一个节点搬移到另一个节点并让双方对数据的语义理解一致”。这个定义会贯穿全文。2. 协议分层先有物理层再有应用层嵌入式通讯常见的分层模型有三种观察视角物理层解决信号怎么传输。比如 RS232 电平、RS485 差分、CAN 差分、单片机的 IO 电平。链路层/驱动层解决收发帧、寻址和基本错误检测。比如串口的起始位停止位、CAN 的报文过滤、以太网的 MAC 帧。应用层解决业务语义。比如 Modbus 的功能码、MQTT 的主题、HTTP 的 URL。初学者最容易犯的错就是把这三层混在一起。举一个很典型的例子有人问“RS485 和 Modbus 有什么区别”这没法直接回答因为 RS485 是物理层标准Modbus 是应用层协议。RS485 规定了发送端用差分信号表示 0 和 1支持多点组网而 Modbus 规定了一帧报文里地址、功能码、数据、CRC 各占什么位置。可以把 Modbus 跑在 RS485 上也可以把 Modbus 跑在以太网上Modbus TCP。把概念分开之后开发和面试都会清晰很多。建议把这张表记牢层典型协议/标准要解决的核心问题易混淆点物理层RS232、RS485、CAN 物理层、以太网 PHY信号如何表示、传输距离、抗干扰RS485 和 Modbus 混为一谈数据链路层UART 帧格式、CAN 数据帧、以太网 MAC帧边界、寻址、错误检测把串口波特率和帧格式当成协议本身应用层Modbus RTU/TCP、MQTT、HTTP业务语义、寄存器含义、命令格式不知道协议还要规定语义这一章的小结论就是面试时先分层工程上也要按层拆分代码结构这样通讯问题会清晰很多。3. 串行总线三兄弟UART、I2C、SPI这三者几乎出现在所有嵌入式开发板上必须熟练掌握。3.1 UART最基础的异步串口UART 全称是通用异步收发器。核心特征是异步也就是收发双方共用一个波特率但没有时钟线。关键参数有波特率每秒传输的码元数常见 9600、115200。数据位一般 8 位。校验位可选无校验、奇校验、偶校验。停止位1 位或 2 位。帧结构通常是起始位1bit低电平 数据位LSB first 校验位可选 停止位高电平。空闲时总线为高电平发送端拉低产生起始位这样接收方就能同步。工程中真正的坑点有三个波特率误差和晶振精度。当双方波特率误差过大长时间接收就会出现偶发乱码。一般误差在 2% 以内可以工作但建议把目标定在 1% 以下。线路接反或电平不匹配。MCU 的 UART 一般是 TTL 电平直接接 RS232 口会损坏设备必须经过电平转换芯片。接收端怎么知道一帧结束。这是最经典的协议问题。常见方案固定长度帧、超时判帧、帧头帧尾判帧。3.2 I2C只有两根线的多主总线I2C 的特点是使用两根线SDA 数据和 SCL 时钟接口一般是开漏输出加外部上拉电阻因此支持线与逻辑可以实现多主仲裁。回答面试时建议抓住几个核心为什么需要上拉电阻因为开漏结构本身不能输出高电平必须由外部上拉电阻提供。上拉电阻大小影响信号上升时间和总线速度常见值 4.7kΩ、2.2kΩ。地址怎么确定7 位或 10 位地址。同一总线上两个设备地址冲突时需要通过地址引脚改地址或换设备。时钟拉伸如果从机太慢可以主动拉低 SCL 让主机等待。传输数据按 8 位字节组织每字节后跟 1 位应答位 ACK。常见坑忘记上拉、SCL 和 SDA 接反、地址算错、总线挂死且没有恢复逻辑。3.3 SPI高速全双工主从总线SPI 最多四根线SCLK、MOSI、MISO、CS。特点是全双工、高速、主从模式通信时主机产生时钟。面试中经常被问 SPI 有几种模式这考的是 CPOL 和 CPHA模式CPOLCPHA采样/发送时机Mode 000时钟空闲低电平第一个边沿采样Mode 101时钟空闲低电平第二个边沿采样Mode 210时钟空闲高电平第一个边沿采样Mode 311时钟空闲高电平第二个边沿采样实际项目里 SPI 和传感器、Flash、显示屏配合最多。坑点在于从机对时钟极性相位要求不一定一样配置错了读出来全是 0xFFCS 片选时序不对不满足数据手册里的建立时间和保持时间数据也会错。串行总线三兄弟虽然常用但物理机制完全不同UART 是异步串行I2C 是开漏加时钟线SPI 是全双工主从。面试回答时不要把三者机制背混。4. 从 RS485 物理层到 Modbus RTU 应用层这部分对做工业控制、仪表设备、储能、充电桩的人极其重要。做嵌入式开发迟早会遇到工业总线场景。4.1 RS485 物理层RS485 利用双绞线的差分信号传输数据两条线 A、B用两根线的电压差表示逻辑电平。最大优势是可以多点组网一条总线挂多个节点传输距离远。工程要点半双工发和收不能同时进行需要控制方向引脚 DE/RE。方向切换的时序要小心太快会在发送结束后截断最后一个字节。终端电阻总线两端各并一个 120Ω 终端电阻匹配阻抗防止信号反射。A 端上拉、B 端下拉可以在总线空闲时保持确定电平避免误收乱码。共地问题远端设备的共模电压差异可能导致通信异常工业上常加隔离收发器。面试高频点为什么差分传输抗干扰因为干扰会同时作用在两条线上接收端比较的是电压差值共模干扰被抵消双绞线本身也能降低磁耦合噪声。4.2 Modbus RTU 报文格式Modbus RTU 的报文结构[地址(1)] [功能码(1)] [数据区(N)] [CRC16低字节] [CRC16高字节]地址范围 1-247 是设备地址0 是广播地址功能码告诉设备做什么CRC16 校验整个报文。常用功能码功能码名称读/写用途0x01读线圈读读取离散输出0x02读离散输入读读取开关输入0x03读保持寄存器读读取 16 位寄存器最常用0x04读输入寄存器读读取只读寄存器0x06写单寄存器写写一个 16 位寄存器0x10写多个寄存器写批量写寄存器帧间间隔很关键。RTU 模式要求帧内字节间隔小于 1.5 个字符时间帧间间隔大于 3.5 个字符时间。工程上常用串口空闲中断或定时器判断 3.5 个字符时间是否到来以此确定一帧结束。4.3 CRC16-Modbus 计算CRC16-Modbus 是 Modbus RTU 做链路校验的算法。核心多项式是 0x8005逆向算法常用 0xA001。一个可参考的直接计算函数如下// crc16_modbus.c #include stdint.h uint16_t crc16_modbus(const uint8_t *data, uint16_t len) { uint16_t crc 0xFFFF; while (len--) { crc ^ *data; for (int i 0; i 8; i) { if (crc 0x0001) { crc (crc 1) ^ 0xA001; } else { crc 1; } } } return crc; }发送时 CRC 先发低字节再发高字节。很多人在这里踩坑直接把 CRC 以大端方式填进缓冲导致对端校验失败。正确做法是uint16_t crc crc16_modbus(frame, len_without_crc); frame[len_without_crc] (uint8_t)(crc 0xFF); // 低字节在前 frame[len_without_crc 1] (uint8_t)(crc 8); // 高字节在后面试时能写出这个函数并主动说清楚“多项式是 0x8005逆向算法是 0xA001发送低字节在前”就是一个加分动作。4.4 Modbus RTU 从机接收状态机实际项目里串口接收 Modbus 帧常用 DMA 加空闲中断或者用状态机解析。下面给一个用状态机接收的简化示例。核心思路是先收地址和功能码再按功能码推断数据长度最后收 CRC然后校验。// modbus_slave_rx.c #include stdint.h #define RX_BUF_SIZE 256 typedef enum { RX_IDLE, RX_FUNC, RX_DATA, RX_CRC_LO, RX_CRC_HI } rx_state_t; static uint8_t rx_buf[RX_BUF_SIZE]; static uint16_t rx_len; static uint16_t rx_expect_data; static rx_state_t rx_state RX_IDLE; // 根据功能码估算数据区长度 // 注意0x0F/0x10 实际需要二次解析字节数这里为演示做了简化 static uint16_t modbus_data_len_by_func(uint8_t func) { switch (func) { case 0x01: case 0x02: case 0x03: case 0x04: return 4; // 起始地址(2) 数量(2) case 0x05: case 0x06: return 4; // 地址(2) 值(2) case 0x0F: case 0x10: return 5; // 地址(2) 数量(2) 字节数(1)后续还需继续收数据 default: return 0; } } // 串口收到一个字节时调用 void modbus_rx_byte(uint8_t byte) { switch (rx_state) { case RX_IDLE: if (byte ! 0x00) { // 为简化跳过广播地址真实产品按需处理 rx_buf[0] byte; rx_len 1; rx_state RX_FUNC; } break; case RX_FUNC: rx_buf[1] byte; rx_len 2; rx_expect_data modbus_data_len_by_func(byte); rx_state RX_DATA; break; case RX_DATA: rx_buf[rx_len] byte; if (rx_len 2 rx_expect_data) { rx_state RX_CRC_LO; } break; case RX_CRC_LO: rx_buf[rx_len] byte; rx_state RX_CRC_HI; break; case RX_CRC_HI: rx_buf[rx_len] byte; if (rx_len 4) { uint16_t crc_calc crc16_modbus(rx_buf, rx_len - 2); uint16_t crc_recv rx_buf[rx_len - 2] | (rx_buf[rx_len - 1] 8); if (crc_calc crc_recv) { modbus_frame_process(rx_buf, rx_len); } } rx_state RX_IDLE; break; default: rx_state RX_IDLE; break; } }这里的关键点在于功能码会影响数据长度。如果接收请求帧写多个寄存器的长度不能在收到功能码时就完全确定真实产品会分成“收请求头 → 按长度收数据 → 收 CRC”三个阶段。上面为控制篇幅做了简化但状态机结构是对的。如果面试时你能把这一版状态机讲清楚并主动补充“用 DMA 加空闲中断效率更高”基本说明你有过产品级开发经验。RS485 加 Modbus 的组合核心需要掌握四点差分电平、半双工方向控制、报文格式、CRC 校验。5. CAN 总线为什么它不需要主从CAN 在汽车、储能、航空航天等领域很常见。和 UART、I2C、SPI 完全不同的地方是它是一种多主、无主从、基于优先级仲裁的总线。5.1 显性位、隐性位和仲裁CAN 物理层也是差分传输两条线是 CANH 和 CANL。逻辑上分为显性和隐性显性位优先级高隐性位优先级低。当多个节点同时发送时某个节点在发送显性位时看到总线上是隐性位就会退出仲裁。面试时建议按这个思路讲CAN 仲裁并不破坏数据它是在传输过程中比较总线状态与本地发送状态高优先级报文发送成功低优先级报文的节点自动退出发送。这样既实现了冲突检测又保证高优先级报文不被打断。5.2 CAN 报文类型CAN 标准帧由这些字段组成SOF 起始帧仲裁字段标准帧 11 位 ID扩展帧 29 位 ID控制字段包含 DLC 数据长度数据场0-8 字节CRC 场ACK 位位填充规则在连续 5 个相同电平后插入一个反相位保证接收方能持续同步时钟。注意CAN 是短帧数据场最多 8 字节。如果需要传更大数据块需要上层分包协议比如汽车里常用的 ISO-TP或者自己设计分包机制。5.3 实际工程注意点终端电阻CAN 总线两端各一个 120Ω。波特率所有节点必须一致常见有 125kbps、250kbps、500kbps、1Mbps。过滤器接收端通过 ID 过滤器只接收关心的消息减少 CPU 开销。错误处理和 Bus Off总线严重错误时节点会进入 Bus Off常见做法是统计错误计数并在应用层上报状态必要时做恢复。CAN FD扩展了可变速率数据场最大 64 字节但需要所有节点支持不能和标准 CAN 混用。CAN 的核心理解放在三个词上无主从、位仲裁、错误处理。6. 协议设计的关键点帧格式、字节序、校验与超时很多嵌入式面试题会变着法子问你如果自己定义一个通讯协议你怎么保证可靠性这类题没有标准答案但考察的判断力是统一的。6.1 一帧报文的通用结构最稳的设计是帧头(0xAA 0x55) 版本/类型(1B) 数据长度(2B) 数据域 CRC16 帧尾(0x0D 0x0A)帧头的作用是让接收方找到帧起始位置长度字段解决变长数据的边界CRC 保证数据没有被改错帧尾是保险冗余。每帧都必须定义最大长度超过最长时间直接丢弃并重新搜索帧头防止数据涌进来导致缓存溢出。6.2 字节序大小端是面试大坑多字节数值在传输前要约定字节序。嵌入式底层常见小端网络协议和 Modbus 等应用协议常用大端。实际项目里见过的问题是两边都觉得自己发的是标准序结果联调时数值完全错乱。建议在协议设计文档里必须写一句“多字节字段统一采用大端序”或“采用小端序”。协议头可以放一个协议版本字段这样后期修改字节序时至少有版本号可回溯。6.3 重传与超时可靠性的兜底确定应用层协议时推荐采用“请求-响应 超时重传”模式。从机收到完整帧后处理再回响应帧。主机在超时计数器到点后判定失败并重发。但重发次数要限制比如连续 3 次失败就报错停止避免造成总线风暴。设计时还要考虑发送序号和响应匹配避免旧响应影响了新请求。7. 一套真实可用的串口 Modbus 从机最小工程这一节把前面的 CRC16 和状态机组合起来目标是在串口上接收一个 03 功能码读保持寄存器的 Modbus RTU 请求回复对应寄存器数据。下面给出一个平台无关的简化示例假设串口接收字节回调已经在中断里把字节喂给 modbus_rx_byte。// modbus_demo.c #include stdint.h #include string.h // ---------- 寄存器区 ---------- static uint16_t holding_registers[4] {0x1000, 0x1001, 0x1002, 0x1003}; // ---------- CRC16-Modbus ---------- uint16_t crc16_modbus(const uint8_t *data, uint16_t len) { uint16_t crc 0xFFFF; while (len--) { crc ^ *data; for (int i 0; i 8; i) { crc (crc 0x0001) ? ((crc 1) ^ 0xA001) : (crc 1); } } return crc; } // ---------- 通过串口发送响应 ---------- void uart_send(uint8_t *data, uint16_t len); // ---------- 处理收到的帧 ---------- void modbus_frame_process(const uint8_t *frame, uint16_t len) { uint8_t resp[64]; uint16_t resp_len 0; uint16_t start_addr; uint16_t reg_count; uint16_t crc; if (frame[1] 0x03) { // 03 读保持寄存器 start_addr (frame[2] 8) | frame[3]; reg_count (frame[4] 8) | frame[5]; resp[0] frame[0]; // 从机地址 resp[1] 0x03; // 功能码 resp[2] (uint8_t)(reg_count * 2); // 字节数 resp_len 3; for (uint16_t i 0; i reg_count i 4; i) { uint16_t reg_value; if (start_addr i 4) { reg_value holding_registers[start_addr i]; } else { reg_value 0x0000; // 越界保护避免数组越界 } resp[resp_len] (uint8_t)(reg_value 8); resp[resp_len] (uint8_t)(reg_value 0xFF); } crc crc16_modbus(resp, resp_len); resp[resp_len] (uint8_t)(crc 0xFF); resp[resp_len] (uint8_t)(crc 8); uart_send(resp, resp_len); } } // ---------- 接收状态机 ---------- typedef enum { RX_IDLE, RX_FUNC, RX_DATA, RX_CRC_LO, RX_CRC_HI } rx_state_t; static uint8_t rx_buf[64]; static uint16_t rx_len; static uint16_t rx_expect_data; static rx_state_t rx_state RX_IDLE; static uint16_t data_len_by_func(uint8_t func) { switch (func) { case 0x01: case 0x02: case 0x03: case 0x04: return 4; case 0x05: case 0x06: return 4; case 0x0F: case 0x10: return 5; default: return 0; } } void modbus_rx_byte(uint8_t byte) { switch (rx_state) { case RX_IDLE: if (byte ! 0x00) { rx_buf[0] byte; rx_len 1; rx_state RX_FUNC; } break; case RX_FUNC: rx_buf[1] byte; rx_len 2; rx_expect_data data_len_by_func(byte); rx_state RX_DATA; break; case RX_DATA: rx_buf[rx_len] byte; if (rx_len 2 rx_expect_data) { rx_state RX_CRC_LO; } break; case RX_CRC_LO: rx_buf[rx_len] byte; rx_state RX_CRC_HI; break; case RX_CRC_HI: rx_buf[rx_len] byte; if (rx_len 4) { uint16_t crc_calc crc16_modbus(rx_buf, rx_len - 2); uint16_t crc_recv rx_buf[rx_len - 2] | ((uint16_t)rx_buf[rx_len - 1] 8); if (crc_calc crc_recv) { modbus_frame_process(rx_buf, rx_len); } } rx_state RX_IDLE; break; default: rx_state RX_IDLE; break; } }运行验证建议用串口工具发送报文地址 01 功能码 03 起始地址 00 00 寄存器数量 00 02 CRC。请求帧内容为01 03 00 00 00 02 C4 0B这是标准 Modbus 工具生成的读两个保持寄存器请求。期望响应为01 03 04 10 00 10 01 32 F3即 4 字节数据加 CRC。如果响应不对第一步先抓串口发出来的字节确认 CRC 是否低字节在前。第二步检查寄存器数组里的值和地址范围。8. 面试高频问题与应答思路这一部分列几个最常见的通讯协议面试题给出“一句话回答 展开点”。1. UART 和 USART 有什么区别答USART 比 UART 多了同步功能可以提供时钟线做同步串行通信。绝大多数场景仍然用它做异步串口。注意不要答成“USART 就比 UART 快”。2. I2C 为什么要上拉电阻答I2C 器件输出是开漏结构不能主动输出高电平上拉电阻是总线高电平的来源。开漏结构也使多个设备可以“线与”支持多主仲裁和时钟拉伸。展开时可以提上拉电阻越小上升沿越快但功耗越大。3. SPI 有几种模式答4 种由 CPOL时钟极性和 CPHA相位组合。从设备数据手册会指定模式配置错会读到 0xFF 或 0x00。4. RS485 为什么比 RS232 传输距离更远、更抗干扰答RS485 用差分信号接收端比较两根线的电位差共模干扰被抵消RS232 是单端传输干扰很容易叠加到信号上。展开时可以补充半双工、多点组网、终端电阻。5. Modbus RTU 怎么判断一帧结束答RTU 标准规定帧间间隔大于 3.5 个字符时间工程上常用串口空闲中断或定时器判断 3.5 个字符时间是否到来。展开时可以提 DMA 加空闲中断的接收方案。6. CAN 为什么可以多主通信答CAN 基于位仲裁机制多个节点同时发送时显性位优先节点在仲裁字段通过位比对自动退出竞争实现非破坏性仲裁。展开时可以讲 ID 越小优先级越高胜出节点继续发完整帧。7. 如何设计一个可靠的通讯协议答固定格式加帧头加长度字段加校验加超时重传加版本字段。展开时要强调帧边界、校验、重传上限、字节序、最大帧长。8. 遇到过大小端问题吗答遇到过多字节数值在协议里要约定字节序接收后要正确转换。展开时举一个具体例子协议定义 uint16 统一使用大端发送时手动移位接收时再拼回来。这些答题思路要背熟但更重要的是理解背后的场景才能在追问时接住。9. 常见问题排查表与工程最佳实践9.1 排查表问题现象可能原因排查方式解决方案串口输出乱码波特率不一致、晶振误差、电平不匹配逻辑分析仪看波形或降低波特率对比统一波特率检查时钟配置加电平转换只有首帧能收到之后全部超时接收缓存未清空、状态机卡住打印状态机当前状态增加状态机超时复位错误字节时回到 IDLEI2C 读不到设备上拉缺失、地址错误、总线挂死示波器看 SDA/SCL 波形检查地址位补上拉电阻用扫描代码枚举地址SPI 读回全 0xFFCPOL/CPHA 配置错误或 CS 时序不对对照从机数据手册时序图调整 SPI 模式加长 CS 建立保持时间RS485 接收乱码缺失终端电阻、A/B 接反、方向切换太快示波器量差分波形去掉干扰源两端加 120Ω检查 DE/RE 控制时序Modbus 请求后无响应设备地址、功能码、CRC 错误串口抓包对比 CRC用 Modbus 工具验证帧格式CAN 节点偶发 Bus Off波特率不一致、线路干扰、错误恢复设置查看错误计数示波器观察 CAN_H/CAN_L统一波特率加终端电阻调整错误处理9.2 工程最佳实践串口收发建议配合 DMA 加空闲中断减少 CPU 占用。协议解析放在独立模块中不要在中断里做业务处理中断只负责把字节放进环形队列。状态机必须带超时复位防止收到半帧后卡死。CRC 写完要确认字节序Modbus RTU 和很多工业协议要求 CRC 低字节在前。日志里打印帧内容时用统一模板[TX] len8 01 03 00 00 00 02 C4 0B联调时可以快速对比。协议文档里写明字节序、帧间隔、超时时间、最大帧长、版本号。多实例场景不要用全局状态变量而是用结构体封装接收状态方便多串口复用。联调时用逻辑分析仪或串口助手把每一帧都记录下来不要凭肉眼数数。变更协议时加版本字段并保留旧版本兼容期。PCB 走线上RS485、CAN 尽量按差分对并排走少打过孔线径和线距保持一致。10. 总结与后续学习方向这篇“嵌入式八股3-通讯协议”从面试和工程双角度把主流协议梳理了一遍。核心要点可以浓缩成三句话协议要先分层理解物理层、链路层、应用层各管一块。常用协议背后都有“为什么这么设计”的工程原因面试要多讲机制而不是背名词。可靠通讯最终靠的是完整状态机、CRC、超时重传和日志可观测而不是某一次偶然通信成功。后续建议按这个顺序继续深入用逻辑分析仪抓一次 I2C 或 SPI 总线波形和芯片数据手册里的时序图对比。自己写一个从 UART 字节流中解析 Modbus RTU 的完整状态机能处理异常帧和超时。把串口 DMA 加空闲中断和一个环形队列结合起来实现一个产品级串口驱动。如果做汽车电子或电池管理重点学 CAN FD、ISO-TP 和 UDS如果做工业控制重点学 Modbus TCP 等常见工业协议。但不要贪多先把一个协议写透。通讯协议这章八股背下来只是开始能写成一个不卡死的状态机才是面试官的真正考题。
返回列表