简介:这是一份面向STM32开发者的QN8035 FM单芯片驱动资源,解决嵌入式平台通过模拟I2C控制调频接收、音频处理与RDS解码等问题,适合车载音响、便携收音机及其他音频设备的开发者使用,既能作为学习样例,也能用于实际项目快速原型。压缩包共含4个文件,包括2个C源文件与2个头文件,对应驱动主体、寄存器操作与接口定义,整体仅12KB,轻量易读,便于快速移植到自己的工程中。已有771人学习浏览,说明该方案的参考价值受到嵌入式爱好者认可。示例代码已经过STM32平台运行验证,覆盖初始化、寄存器设置、数据收发、错误处理与RDS数据读取等关键环节,能帮助开发者避开常见I2C时序坑,快速实现频率调整、音量控制与电台文本显示等功能。对于需要显示电台名称、节目信息或天气预报的应用,驱动中的RDS解析逻辑也提供了很好的参考。
1. QN8035 FM 驱动:与其调 I2C,不如先啃透 RDS 解析
QN8035 这颗 FM+RDS 单芯片,市面上能找到的资料不少,但真正能“拿来就改”的完整驱动并不多见。我第一次拿到带这颗芯片的某开发板时,最大的感受是:I2C 读写并不难,难的是那串初始化序列完全像个黑匣子;真正让我翻车的却是 RDS 数据——它不是一串干净的文本,而是一股 1187.5bps 的原始位流,解出来是什么全靠自己动手解析。如果你正想给项目加一个 FM 收台能力,或者打算把电台名、节目文本显示到自己的屏幕上,这份驱动可以帮你把初始化、调频、RDS 解析、串口调试台一次配齐。它适合正在写嵌入式驱动的一线开发者,也适合把收音机功能塞进物联网设备的人。
2. 硬件与通信协议:QN8035 的 I2C 链路和它周围的最小硬件
2.1 芯片定位:一颗低功耗 FM+RDS 单芯片到底集成了什么
QN8035 的定位是一颗高度集成的调频接收芯片,射频前端、混频、中频解调、立体声解码和 RDS 解调全部做在片内,主控只需要通过 I2C 总线写寄存器来控制频率、读取状态和数据。它对外暴露的接口非常少:天线输入、晶振、I2C、供电和音频输出,几乎就是一个“天线进、音频出、控制面走 I2C”的完整收音机。
从内部数据通路看,信号从天线进来之后经过低噪声放大、混频、中频滤波、FM 解调,然后兵分两路:一路走音频输出,另一路进 RDS 解调器,把 1187.5bps 的副载波数据流解出来放进数据寄存器。主控侧能做的就三件事:写配置、写频率、读状态和读数据。这颗芯片的典型参数如下表,具体数值以你手上的手册为准:
| 参数项 | 常见值 | 说明 |
|---|---|---|
| 频率范围 | 76MHz – 108MHz | 覆盖校园电台和常规 FM 广播 |
| 频率步进 | 50kHz / 100kHz | 由初始化寄存器配置 |
| RDS/RBDS | 支持 | 可解析 PS、RT、PI 等信息 |
| 控制接口 | I2C | 7 位地址常见为 0x12 或 0x13 |
| 供电范围 | 1.8V – 3.6V | 典型接 3.3V |
2.2 最小硬件电路:电源、晶振、天线、I2C 上拉
基于这颗芯片搭建最小系统时,常见的做法是:VDD 接 3.3V,旁边并一颗 100nF 和一颗 10uF 去耦电容;晶振用 32.768kHz,两端各加 22pF 负载电容到地;天线端可以直连天线座,也可以串一个几 pF 的耦合电容再入芯片,目的是隔离直流偏置。I2C 的 SCL 和 SDA 需要上拉电阻到 VDD,阻值一般取 2.2kΩ 到 4.7kΩ 之间,上拉太小会让总线边沿变缓,太大则信号抗干扰差。
音频输出脚可以直接接耳机或后级功放,但输出幅度和驱动能力有限,接喇叭前建议加一级功放。一个容易忽略的点是,天线位置的铺铜要尽量少,芯片底下不要大面积覆铜,否则会抬高噪声底,弱台收起来全是沙沙声。另外,如果板子上有 DCDC 电源,建议给芯片单独加一个 LC 滤波,FM 频段对电源纹波比较敏感,这算是我在这类小芯片上攒下的血泪经验。
2.3 I2C 读写出参:地址、寄存器宽度与时序
QN8035 的控制面是标准的 I2C 从机,寄存器宽度为 8 位,数据也是 8 位,写操作就是“先发寄存器地址,再发数据”。不同应用笔记里 I2C 地址写法不一样,常见的有 7 位地址 0x12 和 0x13 两种版本,我拿到板子后第一件事就是用 i2cdetect 扫一遍,确认实际地址,避免初始化序列写进一个不存在的设备。读操作则是先写寄存器地址,然后发起读事务,连续读时要注意芯片是否支持 burst 模式。
我习惯把 I2C 访问封装成三个基础函数,驱动层不直接接触具体平台的总线操作,这样后面换 MCU 时只改 HAL 文件,不用动驱动逻辑。代码如下:
/* hal_i2c.h */ #ifndef HAL_I2C_H #define HAL_I2C_H #include <stdint.h> #define QN8035_I2C_ADDR 0x12 /* 7位地址,按实际手册修正 */ int hal_i2c_write_reg(uint8_t reg, uint8_t val); int hal_i2c_read_reg(uint8_t reg, uint8_t *val); int hal_i2c_read_burst(uint8_t reg, uint8_t *buf, uint8_t len); #endif这里hal_i2c_read_burst是给 RDS 数据读取准备的,一次事务连续读完多个寄存器,比循环单字节读效率高很多,也避免频繁切换 I2C 方向导致时序抖动。注意每次调用后要检查返回值,对 I2C 无应答的情况做重试,而不是直接当作成功处理。
2.4 接口抽象:为什么要把 I2C 做成一个 HAL 层
很多初学驱动的人会把 I2C 读写直接写进芯片驱动里,换一块板子就要到处改。常见的做法是定义一个操作结构体,把所有硬件相关函数挂进去,驱动代码只认这个结构体。这样做还有一个额外的好处:可以在 PC 上伪造一个 HAL 实现,用抓回来的 RDS 原始数据做回归测试,不用每次调解析逻辑都烧板子。
typedef struct { int (*write_reg)(uint8_t reg, uint8_t val); int (*read_reg)(uint8_t reg, uint8_t *val); int (*read_burst)(uint8_t reg, uint8_t *buf, uint8_t len); } qn8035_hal_t;后续所有驱动函数,比如初始化、调频、RDS 读取,都只跟这个结构体打交道。实际接入时,把某开发板的 I2C 驱动封装成这三个函数指针,初始化时传进去即可。这套抽象让整个驱动保持在 2000 行以内,且不依赖任何具体平台头文件,移植成本很低。
3. 核心驱动拆解:初始化序列、调频计算与状态机
3.1 初始化序列:一组长数组背后的寄存器分组逻辑
QN8035 上电后并不是直接就能收台,必须先写一串初始化寄存器。很多资料把这串数据直接甩给你,不解释每项的含义,导致改一个参数就全乱。我把这串初始化序列按功能分成了三类:全局配置、音频通路、RDS 使能。全局配置负责工作模式和频段范围;音频通路管去加重、输出增益、软静音;RDS 使能则决定后面能不能读到数据。下面是一个典型的初始化结构:
/* qn8035_init.c */ static const uint8_t qn8035_init_seq[][2] = { {0x00, 0x01}, /* 进入 FM 模式并触发软复位 */ {0x01, 0x0A}, /* 频段/步进配置:76-108MHz,50kHz */ {0x03, 0x11}, /* 去加重 50us + 打开 RDS 功能 */ {0x04, 0x00}, /* 音频通路初始增益 */ {0x08, 0x80}, /* 软静音与噪声抑制开启 */ /* 后续寄存器按实际应用笔记补齐,不要随意省略 */ };这段数组看起来像一串魔法数字,但每一项都有明确用途。第二行{0x01, 0x0A}决定了后续频率换算的基准,如果你把步进改成 100kHz,那么后面调频算法里的换算系数也要跟着改,否则频率会偏出一大截。第四行到第五行属于音频相关,做产品时如果需要音频功放静音配合,可以在这两行后面追加寄存器。
初始化写入时要按数组顺序逐条执行,每写一条最好加一个极小的延时让内部逻辑稳定。全套初始化的常见写法是循环遍历上述数组,hal_i2c_write_reg(seq[i][0], seq[i][1]),并检查每次返回码。不要试图把整个数组打包成一次 burst 写,很多芯片的寄存器写入需要逐个生效。
3.2 频率换算:从 MHz 到寄存器值的那一步计算
QN8035 的频率寄存器通常由高字节和低字节组成,写入的值代表“当前频率相对起始频率的偏移步进数”。常见的计算方式是用(目标频率 - 起始频率)除以步进。例如起始频率 76MHz、步进 50kHz,要调到 101.1MHz,偏移就是(101.1 - 76) / 0.05 = 502,寄存器值就是 502。这个数值会拆成高低两个字节写入,并且在写入后触发一次频率生效命令。
/* qn8035_tune.c */ #define QN8035_START_MHZ 76 #define QN8035_STEP_KHZ 50 uint16_t qn8035_calc_freq_reg(uint16_t freq_mhz) { return (uint16_t)((freq_mhz - QN8035_START_MHZ) * 1000) / QN8035_STEP_KHZ; } void qn8035_tune(qn8035_dev_t *dev, uint16_t freq_mhz) { uint16_t reg = qn8035_calc_freq_reg(freq_mhz); dev->hal.write_reg(QN8035_REG_FREQ_H, (reg >> 8) & 0xFF); dev->hal.write_reg(QN8035_REG_FREQ_L, reg & 0xFF); dev->hal.write_reg(QN8035_REG_CMD, 0x02); /* 触发频率写入生效 */ }这里的qn8035_calc_freq_reg是很多移植踩坑的重灾区,因为不同芯片的起始频率和步进单位不一样,有人把步进 50kHz 当成 0.05 处理,有人当成 50 处理,就差一个数量级。建议在串口打印里直接输出算出来的reg值,跟已知强台频率对比,验证换算是否对。
3.3 调频主流程与状态机:从空闲到播放的完整链路
调频不只是一次寄存器写入,而是一个完整的状态过程:写频率、等待锁定、检查信号强度、确认 RDS 数据有效。为了不让主流程变成一团乱麻,我用一个简单的状态机来管理。状态分四个:关机、搜索中、已锁定、失败。每次发起调台,就把状态切到搜索中,轮询芯片状态寄存器里的锁定标志,超时则进入失败态。
typedef enum { QN8035_STATE_OFF, QN8035_STATE_SCANNING, QN8035_STATE_TUNED, QN8035_STATE_FAIL } qn8035_state_t; int qn8035_tune_and_wait(qn8035_dev_t *dev, uint16_t freq_mhz) { dev->state = QN8035_STATE_SCANNING; qn8035_tune(dev, freq_mhz); for (int i = 0; i < 50; i++) { uint8_t st = 0; dev->hal.read_reg(QN8035_REG_STATUS, &st); if (st & QN8035_STATUS_LOCKED) { dev->state = QN8035_STATE_TUNED; return 0; } hal_delay_ms(10); /* 轮询间隔 10ms,总超时 500ms */ } dev->state = QN8035_STATE_FAIL; return -1; }轮询间隔 10ms 是我在实际调试中试出来的,太快会频繁占用 I2C 总线,影响 RDS 数据的读取;太慢则让搜台体验变得迟钝。锁定标志只代表芯片进入了锁定状态,不等于信号一定清晰,还需要结合信号强度寄存器做二次判断。状态机的好处是上层 UI 可以清晰感知当前处于什么阶段,不会在搜台时误触其他操作。
4. RDS 数据解析:从 I2C 原始位流到电台名、节目文本
4.1 RDS 数据通路:QN8035 如何把 1187.5bps 的流交给你
RDS 的数据率是 1187.5bps,调制在副载波上,QN8035 内部的 RDS 解调器会把比特流逐位解码,然后拼成一个个“组”,放进数据寄存器。一组 RDS 数据包含 4 个块,每个块由 16 位数据区和 10 位校验区组成,总共 104 位。芯片不会帮你做语义解析,它只负责把原始二进制数据放到寄存器里,剩下的拼组、校验、解析都要主控完成。
这也解释了为什么很多新手拿到 RDS 数据会一脸懵:直接按字节打印出来,能看到一堆看似无意义的 0xFF、0x00 之类,跟电台名对不上。正确的处理流程是:轮询 RDS 数据可用标志,读到原始字节流,先把 4 个块切出来,校验每个块的 CRC,确认无误后再解析组类型和内容。QN8035 的寄存器一次未必能装下一整组数据,常见做法是分两次读取两个块,或者用 burst 模式一次读完。
4.2 原始组数据读取:状态位、连续读与字节对齐
写 RDS 驱动时,我习惯把“读到原始数据”和“解析原始数据”分成两个函数,这样抓回来的原始数据可以直接保存成文件,放到 PC 上反复回放测试。读取的常见逻辑是:先读状态寄存器,检查 RDS 数据就绪位,然后通过 burst 读取连续寄存器,把原始组数据放进一个缓冲区。
/* rds_read.c */ #define QN8035_REG_RDS_STATUS 0x10 #define QN8035_REG_RDS_DATA 0x11 #define RDS_DATA_READY 0x01 int rds_read_group(qn8035_dev_t *dev, uint8_t *raw) { uint8_t status = 0; if (dev->hal.read_reg(QN8035_REG_RDS_STATUS, &status) < 0) return -1; if (!(status & RDS_DATA_READY)) return 0; /* 暂无数据,正常情况 */ return dev->hal.read_burst(QN8035_REG_RDS_DATA, raw, RDS_RAW_LEN); }这里的RDS_RAW_LEN取决于你打算一次读多少数据。如果按完整组读,需要 13 字节容纳 104 位;但实际寄存器排列可能不是简单按字节对齐,必须对照手册确认哪个寄存器位对应哪个块。我踩过的一个坑是,芯片手册里 RDS 数据寄存器的字节序跟实际输出不一致,读出来的块顺序是乱的,后来靠抓取已知电台的 PI 码做比对才纠正过来。建议在调试阶段把原始字节流原样打印出来,先别急着解析。
4.3 块同步与类型识别:B 块的偏移角和组类型位
RDS 标准的 4 个块各有不同的校验偏移角,芯片在解调时通常会把偏移角修正,但主控侧仍然需要做校验,否则解析到的是一堆被噪声破坏的乱码。每个块的 16 位数据区后跟 10 位校验码,用多项式 0x1D5 做 CRC 计算,可以用来判断这个块是否有效。实际代码里,我一般简化成只校验关键块 B,因为 B 块承载了组类型和段地址,B 块错了整组信息都无法信任。
/* rds_parser.c */ uint16_t rds_crc10(const uint8_t *bits, int len) { uint16_t crc = 0; for (int i = 0; i < len; i++) { int bit = (bits[i / 8] >> (7 - (i % 8))) & 1; crc ^= (bit << 9); if (crc & 0x200) crc ^= 0x1D5; /* RDS 标准生成多项式 */ crc <<= 1; } return crc & 0x3FF; }这个函数输入是一段按位排列的数据,len通常取 16 位数据区长度,返回 10 位校验值。注意bits数组的位序是从一个字节的最高位开始取的,如果你的 I2C 读法把字节序搞反了,这个地方算出来的 CRC 永远是错的。更简单的替代方案是只检查 B 块的偏移角,把解析逻辑跟标准 CRC 分组对齐,能省不少调试时间。
拿到有效的 B 块数据后,组类型就藏在 B 块的高位里,组类型高 4 位加上 A/B 标志位共同决定这一组属于哪个类型。0A 组是最常见的电台名分组,它的组类型码是 0,A/B 标志为 0。如果 A/B 标志为 1,则是 0B 组,功能不同,不能混用。
4.4 0A 组拼接:PS 电台名与 RT 节目文本的还原
0A 组的负载分布是固定的:Block A 是 PI 码(电台标识),Block B 的高 4 位是组类型,低 4 位里既有 TA/TP 标志,也有 PS 段地址。Block C 和 D 各携带 2 个字符,PS 电台名一共 8 个字符,需要 4 组 0A 组才能拼完。数据不是按顺序发送的,而是按段地址循环发送,所以驱动里要维护一个 8 字符缓冲区,根据段地址把收到的字符填到对应位置。
/* rds_parser.c */ typedef struct { uint8_t ps[8]; /* 8 字符电台名 */ uint16_t pi; /* 电台标识码 */ uint8_t seg_mask; /* 已收到的段位图 */ } rds_ps_state_t; void rds_parse_0a(rds_ps_state_t *ps, const uint8_t *raw) { uint16_t block_b = (raw[2] << 8) | raw[3]; uint8_t seg_addr = block_b & 0x03; /* 段地址取低 2 位 */ uint8_t ab = (block_b >> 11) & 0x01; /* A/B 标志 */ if (ab != 0) return; /* 非 0A 组丢弃 */ ps->ps[seg_addr * 2 + 0] = raw[4]; ps->ps[seg_addr * 2 + 1] = raw[5]; ps->seg_mask |= (1 << seg_addr); if (ps->seg_mask == 0x0F) { /* 4 个段全部收齐,PS 可用 */ } }seg_addr是 0A 组的段地址,范围 0 到 3,每组提供两个字符。ps->seg_mask用来判断四个段是否到齐,只有全部到齐才认为 PS 完整,否则显示出来就是缺字符的乱码。实际广播中段地址不一定按 0、1、2、3 顺序到达,经常是乱序的,所以位图判断比单纯计数器更可靠。RT 节目文本的解析逻辑与此类似,只是组类型是 2A,需要 16 组才能拼出完整文本,缓冲区和段位图都要相应扩大。
5. 编译运行与避坑排查:从串口空读到稳定收台的调试记录
5.1 工程目录与构建:标准 C 驱动加 PC 单元测试的跑法
为了让驱动不绑定具体开发板,我把工程拆成驱动层、HAL 层和应用层三部分。驱动层只写 QN8035 和 RDS 解析逻辑;HAL 层提供 I2C 和延时接口;应用层放主流程。PC 上可以编一个模拟 HAL,把抓回来的原始 RDS 数据一条条喂给解析器,验证解析结果后再烧到板子上,省去大量重复烧录时间。
qn8035_project/ ├── CMakeLists.txt ├── src/qn8035.c ├── src/qn8035.h ├── src/rds_parser.c ├── src/rds_parser.h ├── hal/hal_i2c_linux.c ├── hal/hal_delay.c ├── app/main.c └── tools/rds_replay.cCMakeLists.txt 里把 HAL 层换成对应的实现文件即可,嵌入式环境用交叉编译器也只改一个文件。下面是最简构建配置:
add_executable(qn8035_demo app/main.c src/qn8035.c src/rds_parser.c hal/hal_i2c_linux.c ) target_compile_options(qn8035_demo PRIVATE -Wall -Wextra)PC 上运行cmake -B build && make就能编出qn8035_demo,配合tools/rds_replay.c读入保存的原始数据文件做回归测试。嵌入式平台则把hal_i2c_linux.c替换成某开发板的 I2C 封装,其他代码一行不用动。
5.2 串口调试台:把它变成一台能看的“收音机仪表盘”
调试这类芯片,最怕的就是干等数据。我习惯在 main 循环里把频率、锁定状态、信号强度、RDS 的 PI 码和 PS 字符串全部打印出来,串口直接变成一台收音机仪表盘。这样搜台时能看到每个频率点的信号变化,不用靠耳朵猜。以下是主循环的骨架:
/* app/main.c */ int main(void) { qn8035_dev_t dev = { .hal = { .write_reg = hal_i2c_write_reg, .read_reg = hal_i2c_read_reg, .read_burst = hal_i2c_read_burst } }; qn8035_init(&dev); while (1) { qn8035_tune_and_wait(&dev, CURRENT_FREQ); printf("[%3d.%dMHz] state=%d rssi=%d\n", CURRENT_FREQ / 10, CURRENT_FREQ % 10, dev.state, qn8035_read_rssi(&dev)); rds_dump(&dev); hal_delay_ms(1000); } }CURRENT_FREQ 以 0.1MHz 为单位存储,比如 1011 代表 101.1MHz,打印时用除法和取余拆开。rds_dump内部调用 RDS 读取函数,解析成功后打印 PI 码和 PS 字符串。串口重定向在嵌入式平台上通常只需要重写_write,不同编译器语法略有差异,但思路一致。
5.3 常见问题排查:我在这颗芯片上踩过的五个坑
坑一:I2C 扫描不到芯片。现象是i2cdetect里一片空白,写寄存器返回无应答。原因是芯片还没起振或者地址不对。解决方法是确认 VDD 电压稳定后延时 100ms 再初始化,同时扫描 0x12 和 0x13 两个地址,不要只信手册里的一个。
坑二:有信号但 RSSI 一直为 0。现象是能听到声音,但读信号强度寄存器始终是 0。原因是信号强度寄存器可能不是直接百分比,有的版本需要先读状态寄存器再读数据寄存器,或者数值被映射到其他寄存器。解决方法是读连续的寄存器段,打印全部字节后对照手册确认含义。
坑三:RDS 数据寄存器全是 0xFF。现象是轮询到数据就绪标志,但读出来的字节全是 0xFF。原因是 RDS 功能没有被真正使能,或者信号的副载波解调失败,天线信号太弱。解决方法是重新检查初始化序列里 RDS 使能位,靠近一个强台并换一根更长天线再试。
坑四:PS 字符串缺字符、乱序。现象是电台名显示出来只有两三个字符,或者字符位置错乱。原因是段地址判断逻辑写错了,可能是 A/B 标志没过滤,也可能是段地址取位位置不对。解决方法是打印每一组的段地址和对应字符做对照,确认 raw 数组字节序后再解析。
坑五:频率偏了半个台。现象是输入 101.1MHz,却收听到 101.45MHz 的台。原因是步进换算错误,初始化里配置的是 50kHz,代码里却按 100kHz 算。解决方法是把qn8035_calc_freq_reg的步进常量和初始化序列里的寄存器配置保持一致,打印换算后的寄存器值做验证。
6. 二次开发改造:从固定频率到自动搜台、RDS 事件上抛
6.1 自动搜台算法:用 RDS 的 PI 码过滤“伪强台”
固定频率调台只能验证驱动,真正做成产品还得自动搜台。常见做法是从频段低端扫到高端,每个频率点调台并等待锁定,锁定后读取 RSSI 和 PI 码,两个条件都满足才存台。只靠 RSSI 会把镜频和杂散信号存进来,加上 PI 码有效性判断能过滤大部分伪台。
void qn8035_scan(qn8035_dev_t *dev) { for (uint16_t f = 760; f <= 1080; f += 5) { /* 0.1MHz 为单位,步进 0.5MHz */ if (qn8035_tune_and_wait(dev, f) == 0) { uint16_t pi = rds_get_pi(dev); if (pi != 0xFFFF && qn8035_read_rssi(dev) > 40) { save_station(f, pi); } } } }这里的步进值得说一句:如果初始化配置的是 50kHz 步进,扫描步长设成 5(即 0.5MHz)太粗,会漏掉间隔近的台;设成 1 又太慢,扫完整段要几百次调台。我一般先用 0.5MHz 粗扫,找到有效频段后再精扫一次,体验和效率比较平衡。
6.2 把 RDS 数据格式化成 JSON,对接 UI 或 IoT 平台
驱动层解析出的 PS、RT、PI 都是结构体字段,对上层 UI 来说直接读结构体绑定即可,但如果要对接 MQTT、HTTP 或者前端渲染,最好统一转换成 JSON 字符串。常见的处理是在解析回调里用 snprintf 拼 JSON,转义工作处理一下引号和反斜杠就好。
void rds_to_json(char *buf, int len, const rds_ps_state_t *ps) { snprintf(buf, len, "{\"pi\":0x%04x,\"ps\":\"%.8s\",\"rt\":\"%.64s\"}", ps->pi, ps->ps, ps->rt); }要接入上层事件系统,可以设一个函数指针回调,每当 PS 完整或者 RT 更新时触发一次,上层拿到 JSON 直接刷新屏幕或者上报服务器。这就把底层收音机和业务逻辑彻底解耦了,我在做某跨平台系统时就是这么处理的,底层的每次纠错不需要上层感知。
6.3 改造后的验证方法:抓一份真实 RDS 原始数据回放
改完解析逻辑,最怕的是什么?怕一次只改一个字符,结果下一个台解析出错,把没改的地方也带崩了。从那以后我每次动 RDS 解析代码,都会强制走一遍 PC 回放流程:先在板子上把某台的真实 RDS 原始流抓成文件,然后修改解析逻辑后用rds_replay工具重新解析同一份文件,对比输出的 PS 和 RT 是否一致。这一步能在不碰硬件的情况下快速暴露位序移位、段地址取错这类问题,比反复烧板省事太多。希望帮到你,这套流程也推荐你直接内化成自己的习惯。
本文还有配套的精品资源,点击获取