UART不是老古董,是被低估的通信基石
上个月帮朋友排查一台设备,现象很诡异:两台板子明明用的是同一款芯片、同一份程序,A板发数据B板能收到,B板发数据A板就乱码。折腾了半小时,最后发现B板用的是内部RC振荡器,频率比A板偏了大概2.3%,在115200波特率下就这么点偏差,直接让通信崩溃。这种问题,十有八九都会在UART上翻车。
UART,全称是通用异步收发器(Universal Asynchronous Receiver/Transmitter),你在任意一块STM32、ESP32、Arduino开发板上都能找到它,而且往往不止一组。它出现得比绝大多数读者都早,速度又慢,协议也简单,但今天几乎所有嵌入式设备、GPS模块、蓝牙模块、WiFi模块、工业仪表都还靠着它通信。我打算从UART的帧结构、电平标准、波特率误差、16550寄存器标准一路聊到STM32 HAL库的实际工程配置,把那些文档里不会明说的坑都翻出来。
1. 为什么半个世纪过去了,我们还在用UART
1.1 两根线就能完成的异步全双工
通信的本质就是两端商量好规则,然后按规则传递信息。UART把规则简化到了极致:一根发送线TX,一根接收线RX,然后双方共地,就完事了。
不需要时钟线。这在今天看来稀松平常,但设身处地想一下——如果你要在两块板子之间传数据,总共只给你三根线,其中一根还是地线,能传同步时钟吗?做不到。UART在只有数据线的情况下,靠着"双方提前约定好速率"这个笨办法,完成了数据的可靠传输。发送方按约定速率把每个bit依次放在线上,接收方按同样速率去采样,这就是"异步"两个字的核心含义:各自用自己的时钟,不额外同步时钟信号。
也正因为如此,UART天然是全双工的。TX和RX互不干扰,发的同时就能收。你在调试串口时,连着USB转串口工具,一边看日志一边下发命令,这个体验的底层就是全双工。
1.2 和SPI、I2C放在一起比,UART差在哪又赢在哪
很多教程喜欢把UART、SPI、I2C放在一起对比,我直接给一张实际工程中常用的选型表:
| 特性 | UART | SPI | I2C |
|---|---|---|---|
| 需要时钟线 | 不需要 | 需要(SCLK) | 需要(SCL) |
| 数据线数量 | 2(TX/RX) | 最少3(MOSI/MISO/SCLK) | 2(SDA/SCL) |
| 全双工/半双工 | 全双工 | 全双工 | 半双工 |
| 多设备连接 | 点对点为主 | 一主多从靠片选 | 一主多从靠地址 |
| 典型速率 | 9600bps~几Mbps | 几十Mbps | 100k/400k/1Mbps |
| 是否带从设备地址 | 不带 | 不带 | 带7位/10位地址 |
SPI快,但接线多、没有流控和校验;I2C省线,但地址冲突、时钟拉伸问题偶尔让人头疼。UART的优势很朴素:接线最少、实现最简单、任何单片机的任何引脚几乎都能软模拟一个出来,而且它是为数不多能让"两个完全不相关的设备"直接对话的协议——电脑和单片机之间通信,主要就是靠UART翻成USB虚拟串口。
当年PC上有RS-232串口,后来的单片机开发板、路由器、工业控制器也都带着UART接口。这种"什么设备都有UART"的生态,让它在设备间互通这个场景上几乎没有对手。现在你去看ESP32这类模组,板载的串口不仅要跑AT指令,还要下载固件,全靠UART撑着。
2. 拆开一帧UART数据:每一位都是妥协出来的工程智慧
2.1 从空闲电平到停止位:一位一位拆给你看
UART通信的空闲状态是固定的:TTL电平下,空闲时总线保持高电平。别小看这个"默认高电平",接收方正是靠着它来判断"什么时候是真正的数据开始"。
一帧UART数据长这样:
- 空闲:高电平
- 起始位:1个bit,拉低
- 数据位:5~9个bit,默认8个,从最低位(LSB)开始发
- 校验位:可选,1个bit
- 停止位:1~2个bit,回到高电平
接收方什么时候知道"话开始说了"?就是检测到线上出现下降沿的那一刻。因为空闲是高电平,起始位是低电平,这个由高变低的跳变在示波器上会非常醒目,信号再乱,下降沿总不会认错。这也是为什么起始位必须是一个完整的bit时间而不是半个——它要足够宽,让接收方来得及反应。
数据位为什么从LSB开始发?纯粹是历史原因,早期UART芯片从最低位开始处理最简单,这个习惯一直留到今天。你抓波形数bit时,记得第一位对应的是bit0,不是bit7,否则对应关系全错。
停止位的作用很多人理解成"帧结尾",实际更准确的说法是"回到空闲电平的缓冲时间"。接收方需要这段时间来确认这帧结束了、准备接收下一帧的起始位。因为下一帧的起始位又是下降沿,如果上一帧的停止位太短,前后两帧连在一起,下降沿之间隔得太近,接收方很容易漏掉新帧。
2.2 接收方的"半位采样"才是异步通信的定盘星
之前说UART不传时钟,那接收方到底怎么保证采样到的是正确的电平?答案是一个叫"半位采样"的技巧。
接收方内部有一个比波特率快16倍(部分芯片是8倍)的采样时钟。检测到起始位的下降沿后,接收方先等半个bit时间,走到起始位的正中间,然后确认这里确实是低电平——如果是,说明这是个可信的起始位;如果等了半位发现电平不对,那刚才那个下降沿多半是干扰,直接丢弃。
确认起始位之后,接收方每过一个完整的bit时间就采一次样,采的位置都在每个bit的正中心。这个动作背后的道理很简单:一个bit从开始跳变到稳定需要一个过程,边缘处的电平最容易受噪声影响,而正中间是最稳定、最不会出错的地方。所以UART接收对"每位内部的抖动"并不敏感,它怕的是累积性的偏差——你每个bit都偏一点,越到后面偏差越大,最终采到错误的位置上。这就引出了波特率误差的问题,后面专门讲。
2.3 8N1到底代表什么,为什么它是默认
串口调试工具里的"115200 8N1",拆开就是:波特率115200,8位数据位,无校验(None),1位停止位。
校验位的存在意义是提供最简单的检错能力。偶校验要求一帧里所有数据位加上校验位,"1"的总个数为偶数;奇校验则要求为奇数。比如数据是0x55(二进制01010101,有4个1),偶校验的校验位就是0,奇校验就是1。接收方收到后自己数一遍,对不上就知道这帧传输中某一位被翻转了。
但校验位只能检错,不能纠错,而且对偶数个bit翻转(比如两个bit同时被干扰翻转)完全无能为力。所以现代通信帧一般都在UART之上再用CRC做校验,UART这一层的校验位反而经常省掉。8N1成为了事实默认,因为8位数据正好是一个字节,不带校验又省了一位开销,简单直接。
有些人会疑惑"9位数据"是什么场景。实际上把9位数据模式中的第9位当作地址/数据标志位来用,可以实现多机通信:一台主机发地址帧,所有从机都收到,但只有地址匹配的那个从机才接收接下来的数据帧。老式485总线组网在单片机层面常常这么干。
3. 波特率不是一拍脑袋定的:误差怎么算、什么时候会翻车
3.1 波特率发生器的分频原理
波特率就是每秒传多少个bit。UART外设内部通常有一个波特率发生器,本质就是一个分频器:把外设时钟(比如72MHz、36MHz)除以一个分频系数,得到波特率时钟。常见计算套路是:
分频系数 = 外设时钟 / (16 × 目标波特率)
因为接收端要16倍过采样,所以波特率时钟得是目标波特率的16倍。拿STM32F1系列举例,USART1挂在APB2上,时钟72MHz;USART2/3挂在APB1上,时钟36MHz。如果我想让USART2跑115200:
36000000 / (16 × 115200) ≈ 19.53
问题来了:分频寄存器只能写有限位数的整数和小数。STM32的分频器带4位小数精度,所以19.53可以近似表示成19 + 8/16 = 19.5。按这个分频结果实际产生的波特率是:
36000000 / (16 × 19.5) ≈ 115384.6
和目标的偏差约0.16%,完全没毛病。
3.2 误差预算:理论上限与工程经验值
很多工程师觉得"波特率差不多就行",但UART对误差是有一个明确预算的。你可以这样理解:一帧11位(1起始+8数据+1停止),接收方在每个bit正中间采样。如果发送方时钟比接收方快,那发送方的一个bit实际比接收方想象的要短,采样点会在实际bit的后半段移动;反之前半段移动。数据位越多,误差累积越大。
理论极限大约在半个bit时间除以帧长度。11位的帧,半bit就是5.5%,看起来还有空间,但实际工作中受晶体容差、温度漂移、信号边沿斜率影响,工程上通常要求双方总误差控制在±2%以内。这还没完,如果一端用了自动波特率检测(Auto Baud Rate),另一端时钟偏偏精确得过分,有时候反而会因为首帧的边沿噪声导致识别失败。
我自己在项目上习惯留一个更大的安全垫:低速(9600~115200)要求误差不超过±1.5%,高速(1Mbps以上)尽量不超过±1%。晶振选型上,普通无源晶振通常有±20~50ppm的精度,足够;但如果用单片机内部RC振荡器,温漂可能到±1%甚至±2%以上,低温或高温环境下一旦突破容限,通信就是时好时坏,连示波器都未必能一眼看出原因。
3.3 高波特率下的时钟源选择和"首字节丢失"之谜
有人为了追求速度把波特率调到3Mbps、6Mbps,这时候必须确认两件事:外设时钟够不够高,分频误差能不能压下来。3Mbps下,16倍过采样意味着采样时钟要到48MHz,如果APB时钟才36MHz,分频值会小于2,很多芯片的UART外设直接不支持。高速场景优先用USART1这类挂在高速总线上的外设,并且用16倍过采样改成8倍过采样,代价是抗干扰能力稍降。
另一个跟波特率相关的高频故障是"首字节丢失"。现象是设备刚上电,上位机发的第一个命令总没反应,第二个命令开始正常。这往往不是波特率的锅,而是双方上电时序不同步:单片机UART还没初始化完成,上位机的USB转串口已经发数据了,自然丢。更隐蔽的情况是USB转串口芯片自己也有初始化时间,Windows枚举串口要一两秒,你的上位机如果枚举完成后立刻发数据,对端此时可能还没跑起来。
解决方法没有银弹,但可以让双方在建立连接时先握个手:从机启动后主动发一帧"就绪"标志,主机收到后再开始业务通信。这个习惯能替你省掉很多"莫名其妙的第一个字节"问题。
4. UART的三种皮肤:TTL、RS-232、RS-485与USB适配
4.1 TTL电平:板内通信的默认工业标准
单片机直接引出的UART叫TTL电平,逻辑1对应3.3V(或者老一点的5V),逻辑0对应0V。它便宜、简单,两根线直接就能连,但抗干扰能力很弱,传输距离一般也就几十厘米到一两米,而且要求通信双方必须共地。
在开发板上,板载USB转串口芯片和主控之间的连接就是TTL UART。你插上一根USB线,电脑上出现"COM3",本质上是CH340这类芯片在你电脑的USB协议和单片机的UART之间做翻译,电脑那边看到的是一个串口设备,单片机这边看到的还是普通的UART引脚。
4.2 RS-232:负逻辑、负电压与DB9
RS-232是UART的老牌"皮肤",标准RS-232电平规定:
- 逻辑1对应-3V~-15V
- 逻辑0对应+3V~+15V
注意它是反逻辑的,而且用负电压表示空闲,这点和TTL完全不同。用RS-232做长线传输比TTL略强,因为它把摆幅拉高了十几伏,噪声容忍度更大。PC老式主板上那个DB9公头就是RS-232接口。
单片机没有负电源,所以要接RS-232就必须搭配电平转换芯片。MAX232是这一行最经典的芯片,内部自带电荷泵,用几个电容就能把3.3V或5V抬出正负十几伏的电压。到今天我仍然在某些工业仪表和设备调试口上看到RS-232的身影,比如老式条码枪、某些医院的医疗设备串口。
4.3 RS-485:差分信号如何撑起1200米总线
如果把RS-232比作普通双行道,RS-485就是高速公路加护栏。它用两条线A和B的差分电压表示逻辑:
- A比B高200mV以上:逻辑1
- B比A高200mV以上:逻辑0
因为靠的是两条线上的电压差而不是单线电平,外界干扰同时叠加在两根线上,差分会自动抵消,所以RS-485抗干扰能力远强于RS-232,传输距离可以到1200米,还支持一条总线上挂几十个节点。工业自动化、楼宇控制、光伏逆变器通信里满地都是RS-485。
RS-485芯片的DE/RE方向控制引脚是经典坑。半双工意味着你发的时候不能收,收的时候不能发。如果程序里发送完立刻切换方向,最后一两个字节常常被截断,因为UART的移位寄存器里可能还压着没发完的数据。正确的做法是发送完成后等待发送移位寄存器空置(HAL里可以用__HAL_UART_GET_FLAG查UART_FLAG_TC),再拉低DE引脚。自动收发芯片(比如MAX13487)能省掉这个烦恼,但它牺牲了对发送时序的精细控制,具体取舍看项目。
4.4 USB转UART芯片的选型与驱动坑
开发阶段最常用的就是USB转UART芯片。市场上主流三派:
- CH340/CH341:国产,便宜,下载慢,但新手入门必备,绝大多数开发板标配
- CP2102/CP2104:Silicon Labs,稳定,驱动生态好
- FT232R/FT231X:FTDI,老牌,工业级应用多,支持配置EEPROM、CBUS引脚当GPIO用
这三类芯片的核心功能一样,但如果你要在产品上量产,建议优先选FTDI或CP210x,稳定性和驱动兼容性更好;CH340胜在成本低,但假货和劣质晶振带来的问题也见得多。
驱动安装的坑集中在两点。一是买到了假FT232R,Windows装完驱动后设备管理器显示"Unknown device"或设备带感叹号,这个问题在低价USB转串口线上特别普遍。二是不管你用的是CH340还是CP210x,都尽量从芯片原厂或大代理商官网下驱动,不要用Windows自动更新拉到的几年前的旧版本,尤其不要混装多个厂家的VCP驱动,否则有时会出现"串口号能识别但打不开"的怪问题。
选型表放这里:
| 芯片 | 典型价格 | 最大波特率 | 驱动兼容性 | 典型场景 |
|---|---|---|---|---|
| CH340 | 极低 | 2Mbps | Windows自带/需装官方 | 学习板、低成本USB串口 |
| CP2102 | 中低 | 2Mbps | 良好,免驱体验好 | 开发板、调试线 |
| FT232R | 中高 | 3Mbps | 优秀,有EEPROM可配 | 工业调试、量产设计 |
| FT231X | 中高 | 3Mbps | 优秀,体积更小 | 空间受限的便携设备 |
5. 16550标准为什么至今还在支配串口世界
5.1 从8250到16550:16字节FIFO的诞生
上世纪80年代,IBM PC上的串口用的是National Semiconductor的8250 UART芯片。它没有FIFO,每收到一个字节就触发一次中断,CPU得赶紧把数据取走,否则下一个字节到了就会覆盖,这在当时Windows不太流行、DOS时代低速场景下还算够用。
到了16550,芯片加入了16字节的发送/接收FIFO,CPU可以攒一批中断、做一批搬运,一下子解放了系统。16550 DMA模式下还可以配合内存直接搬数据,效率大幅提升。后来的16550A、16750修补了不少bug,但寄存器接口基本兼容,成了"PC串口事实标准"。
5.2 一串寄存器搞定一个串口
16550的寄存器布局极其直白,8个端口地址搞定一切:
| 偏移 | DLAB=0读 | DLAB=0写 | DLAB=1 |
|---|---|---|---|
| 0 | 接收缓冲RBR | 发送保持THR | 分频低字节DLL |
| 1 | 中断使能IER | 中断使能IER | 分频高字节DLH |
| 2 | 中断标识IIR | FIFO控制FCR | — |
| 3 | 线路控制LCR | 线路控制LCR | 线路控制LCR |
| 5 | 线路状态LSR | — | — |
配置一个16550跑115200 8N1,核心代码就这么几行:
#define DLL 0x0 #define DLH 0x1 #define IER 0x1 #define FCR 0x2 #define LCR 0x3 #define LSR 0x5 void uart16550_init(uint32_t base, uint32_t clock, uint32_t baud) { uint16_t divisor = clock / (16 * baud); /* 分频值 */ /* 打开DLAB锁存才能写分频寄存器 */ write_reg(base + LCR, 0x80); write_reg(base + DLL, divisor & 0xFF); write_reg(base + DLH, (divisor >> 8) & 0xFF); /* 8N1:8位数据、无校验、1位停止位 */ write_reg(base + LCR, 0x03); /* 使能并清空FIFO */ write_reg(base + FCR, 0x07); /* 使能接收数据可用中断 */ write_reg(base + IER, 0x01); }那个DLAB位(LCR的最高位)是个很有意思的设计:它让同一个地址在"配置分频"和"收发数据"两种模式下有不同的含义,相当于一组寄存器当两组用,这就是早年芯片省地址空间的做法。理解了DLAB,你就理解了为什么串口驱动里有那么多"先写LCR再写DLL/DLH"的古怪顺序。
5.3 这套标准在今天的影响力
今天的PC早就没有板载串口了,但你去翻Linux内核的drivers/tty/serial/8250目录,会发现这棵30多年的老树依然枝繁叶茂。几乎所有x86平台的串口控制器、很多SoC内置的UART、虚拟机模拟出来的串口、甚至PCIe转串口卡,通通兼容16550寄存器接口。FPGA里想做UART,很多人也是照抄一份16550的寄存器设计,因为这样可以直接用操作系统现成的驱动。
这意味着,你学会16550的寄存器操作,就相当于掌握了一类驱动源码的阅读基础。以后在Linux下看setserial、stty这类工具的输出,或者做驱动移植时看到UART_LSR_DR、UART_LCR_WLEN8这些宏,再看我上面这个表格,会非常轻松。
6. STM32 HAL库的不定时炸弹:UART配置与调通的完整路径
6.1 初始化配置里那些"看起来能用但埋着雷"的参数
STM32工程里初始化UART,大部分人就是打开CubeMX,选个波特率,生成代码,完事。但有几个配置项,生成时稍不留神,后面调试就头疼。
先看一个典型的HAL库UART初始化:
UART_HandleTypeDef huart1; huart1.Instance = USART1; huart1.Init.BaudRate = 115200; huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_TX_RX; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart1.Init.OverSampling = UART_OVERSAMPLING_16; HAL_UART_Init(&huart1);这里面最容易踩的雷是WordLength和Parity的联动关系。在STM32的HAL库里,当使能了校验位,WordLength = UART_WORDLENGTH_8B意味着实际传输是"8位数据+1位校验",即线上总位数是9位;而UART_WORDLENGTH_9B加校验则是"9位数据+1位校验",相当于10位。很多人设置成"8位数据、偶校验",以为线上跑的还是8位,结果和对端对不上,抓波形才发现多了一位。简单记:你配置的WordLength不包含校验位,校验位是额外加在后面的,如果对端设备用的是"8位数据+校验"的帧格式,在STM32里要选8B并打开Parity。
另一个容易忽略的是OverSampling。16倍过采样是默认,如果只是低速通信完全够;当波特率很高,分频器无法精确16倍分频时,切到8倍过采样能把误差压低一截,代价是采样位置少了一半,抗干扰能力变差。高波特率长距离传输时我宁可把速率降下来用16倍过采样,也不推荐用8倍硬扛。
6.2 阻塞、中断与DMA:三种收发机制的取舍
HAL库的UART收发有三种模式,很多人一开始都只用阻塞式:
HAL_UART_Transmit(&huart1, sendBuf, len, 1000); HAL_UART_Receive(&huart1, recvBuf, len, 1000);阻塞式优点是逻辑极简单,一个字节一个字节搬完才返回。缺点也明显:如果对端一直不回数据,HAL_UART_Receive会超时卡住主循环,甭想看门狗会不会先抗议。
中断式收发用回调函数通知结果,适合低速、数据量小的场景:
HAL_UART_Transmit_IT(&huart1, sendBuf, len); HAL_UART_Receive_IT(&huart1, recvBuf, len); /* 完成回调 */ void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { /* 收到len字节,在这里处理 */ }中断式的问题是:如果你想一直收数据,每次收完都得重新调用一次HAL_UART_Receive_IT,少调一次就断一次接收,这种"手脚架"代码几乎每本教程都会犯"只调一次"的毛病。
DMA式适合大数据量中高速传输,CPU几乎不参与搬运。它的坑在另一个方向:DMA一次要搬完指定长度才算完,如果对端只发来10个字节,你配置DMA接收256字节,它就永远不产生完成回调。这时候就要靠下一节的IDLE中断来兜底。
6.3 不定长数据接收:IDLE中断加DMA的经典组合
工程上用的最多的不定长接收方案,是DMA接收加空闲中断(IDLE)。IDLE是UART外设本身的标志位:当接收线上出现一个完整字节时间以上的空闲电平,硬件就认为"这一帧结束了",触发中断。
核心思路是:DMA负责往缓冲区持续搬运字节,IDLE中断负责告诉CPU"对方说完了,赶紧看缓冲区里有多少数据"。代码骨架大致是这样:
static uint8_t rxBuf[256]; /* 启动DMA接收,并打开IDLE中断 */ HAL_UART_Receive_DMA(&huart1, rxBuf, sizeof(rxBuf)); __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); void USART1_IRQHandler(void) { HAL_UART_IRQHandler(&huart1); if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(&huart1); HAL_UART_DMAStop(&huart1); /* 暂停DMA,拿到当前写到哪了 */ uint16_t len = sizeof(rxBuf) - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx); /* 这里处理len个有效字节 */ HAL_UART_Receive_DMA(&huart1, rxBuf, sizeof(rxBuf)); /* 重新装填 */ } }不同HAL版本里__HAL_UART_CLEAR_IDLEFLAG的位置和写法可能有变化,我建议以你手上的stm32xx_hal_uart.h为准。这个方案写好了很省心,但它不是万能的——缓冲区如果溢出了,DMA计数器会怎么表现,不同芯片略有差异。工程上更稳妥的进阶做法是用环形队列配合DMA接收完成中断和IDLE双重判断,这样溢出也能检测到。
6.4 没有逻辑分析仪怎么排查串口故障
手头有逻辑分析仪自然方便,但很多现场调试场景根本掏不出额外设备。我把这些年处理过的串口故障整理成一个排查顺序,照着做大概率能定位:
| 现象 | 优先怀疑 | 验证手段 |
|---|---|---|
| 完全无数据 | TX/RX接反、没共地 | 万用表量TX脚空闲电平,应为高;对地应有电压 |
| 乱码 | 波特率不一致、对端时钟不准确 | 降低波特率到9600试,正常则多半是误差问题 |
| 首字节丢失 | 对端上电时序、USB转串口枚举延迟 | 让从机启动后主动发就绪帧,等握手完成再发数据 |
| 只能发不能收 | RX引脚被复用成别的外设、电平不匹配 | 检查GPIO复用配置,量RX引脚是否能被对端拉低 |
| 单字节能通,多字节丢 | 中断里处理时间过长、FIFO被打满 | 中断里只置标志,数据搬运放主循环 |
有一个很廉价的验证方法:把TX和RX直接短接,形成自环测试。程序里发什么,自己就收到什么。如果自环都不过,问题一定在本机配置;如果自环通但对端不通,再排查接线和电平行列。这个办法在单片机、电脑串口、USB转串口上通吃,值得养成习惯。
最后说点实在的
UART是一个值得花时间彻底搞明白的协议,因为它承载的是"在没有时钟线的情况下,双方如何靠约定和耐性完成通信"这一课。你看懂UART的起始位、停止位、半位采样,再去看CAN的同步段、看I2C的时钟拉伸,会觉得很多设计思路似曾相识。
我个人调试时有个固定习惯:任何串口问题,第一步永远是抓波形,而不是改代码。把示波器探头接到TX引脚,看空闲电平对不对、看第一帧的下降沿在哪、数一下bit宽度跟理论波特率差多少,很多故障当场就能定性。如果没有示波器,用逻辑分析仪也能数出波特率偏差,成本还低。
如果这个系列继续写下去,下一篇我想聊聊SPI——时钟极性和相位那四个模式,几乎每个新人都要在这上面挂一次,到时候咱们把时序彻底掰开揉碎说清楚。