一、半双工为什么要“换挡”,换挡要花多久
RS485 的物理层是一对差分线,所有节点都挂在这对线上。它天生是半双工:
同一时刻只能有一个节点在驱动总线,其他节点只能听。为了让一个节点既能说又能听,
收发器上一般有两个控制引脚:DE(驱动使能,高有效)和 /RE(接收使能,低有效)。
实际电路里最常见的接法是把它们并在一起,用一根 GPIO 控制:
拉高是“只发”,拉低是“只收”。这根引脚我习惯叫方向脚。
方向脚的两行代码看起来太简单了,以至于很容易被当成“不用想”的事。但我后来发现, 真正要算的是换挡要花多久。这段时间不是零,它由好几段拼起来:
| 时间片段 | 由什么决定 | 我实际操作时怎么留余量 |
|---|---|---|
| 软件到引脚 | 写寄存器到引脚真正翻转之间的延迟,包含中断响应、总线仲裁、GPIO 翻转速率与引脚负载电容 | 按最坏情况估,不按“空循环一条指令”估;引脚下拉/上拉电阻越大越慢 |
| 收发器使能建立 | 收发器内部的驱动器上电与接收器失能时间,数据手册里会给出上限值 | 取手册给出的最大值,再留一个量级的余量;不用“典型值” |
| 差分电平建立 | 线缆电容、终端电阻、总线上的节点数、驱动器输出摆率 | 线越长、节点越多,这段越明显;长总线上把余量再放大一点 |
| 对端识别 | 对端收发器的接收阈值与它自己的滤波;有的收发器带故障保护与斜率限制 | 不假设对端“立刻听懂”,协议上留出帧间静默时间 |
这四段里,前两段是我能控制的,后两段是现场条件决定的。所以我的做法是: 把前两段算准,把后两段用协议上的静默时间兜住。 这也是为什么很多 485 通信问题的现象是“换线、换波特率、换终端电阻都没用”—— 因为它根本不是线路问题,而是换挡时间没算够。方向脚相关的坑, 我在《串口通信的帧同步》里也提过一次, 那篇讲的是方向和帧边界的耦合,这一篇专门讲方向本身怎么配。
二、做法一:软件手动切换(GPIO 控制 DE/RE)
最朴素也最常用的做法:把方向脚当普通 GPIO,在收发路径上手动切换。 说它简单,是因为它只有两个动作;说它容易错,是因为这两个动作出现在六个位置上, 少一个都不行。我现在的固定顺序是这样:
- 上电先设为接收态。方向脚在 MCU 复位后是输入(高阻),此时收发器的状态由外围偏置决定。
所以我一般在外围把
/RE偏置成“默认允许接收”,让上电瞬间谁也不会误驱动总线; 软件初始化时再显式把方向脚配成输出并输出低电平。 - 先配方向脚,再初始化串口。反过来的话,串口一使能就可能立刻接收(接收态是对的,问题不大), 但如果初始化流程里恰好有一次发送(比如上电打印),方向脚还没配好,那一帧就会丢在总线上。
- 发送前:先拉高方向脚,再等使能建立时间。这一步的关键是先把收发器叫醒,再让数据上总线。 很多人的写法是先调发送函数再切方向,或者切完立刻写数据,这两种都会让第一个字节吃亏。
- 把数据交给串口。用中断还是 DMA 都行,这一步与方向无关,只影响 CPU 占用。
- 等最后一个字节真正发完。这一步最容易被写错,下一节单独讲。
- 拉低方向脚,再等接收恢复。接收器从失能到能正确识别起始位同样需要时间; 如果拉低之后立刻允许解析,紧接着到来的应答帧头部可能被吞掉几个位。
为了不在这六步里漏项,我把方向切换封装成一对宏(或一对函数), 所有收发路径都必须走它们,不在别处直接写 GPIO:
/* 按个人理解重写的最小片段:只固定顺序,延时值按收发器数据手册取值 */
#define RS485_DIR_PORT (RS485_DIR_GPIO)
#define RS485_DIR_PIN (RS485_DIR_PIN_N)
/* 切到发送:先使能驱动器,等使能建立,再允许写数据 */
#define RS485_DIR_TX() do { \
gpio_set(RS485_DIR_PORT, RS485_DIR_PIN); /* 1. 先叫醒收发器 */\
delay_ticks(RS485_TX_SETTLE_TICKS); /* 2. 等使能建立 */\
} while (0)
/* 切到接收:必须在本帧最后一个字节真正发完之后调用 */
#define RS485_DIR_RX() do { \
gpio_clr(RS485_DIR_PORT, RS485_DIR_PIN); /* 1. 先放开总线 */\
delay_ticks(RS485_RX_RECOVER_TICKS); /* 2. 等接收恢复 */\
} while (0)两个宏里的延时都用了符号常量,不写死数字,原因是它跟收发器型号和现场线缆长度都有关。 取值方法我写在第 4 节,这里只要记住一件事:两个方向都要等,等的理由不一样。
| 步骤 | 做什么 | 漏掉或顺序反了的后果 |
|---|---|---|
| 1 | 上电默认接收(含外围偏置) | 上电瞬间总线被误驱动,或收到一串来历不明的字节 |
| 2 | 先配方向脚,再开串口 | 初始化阶段的那一帧丢失在总线上 |
| 3 | 拉高后等使能建立 | 第一个字节的起始位被削,表现为首字节错、后续字节正确 |
| 4 | 把数据交给串口 | 与方向无关,但用 DMA 时要记得核对第 5 步 |
| 5 | 等发送完成(不是“等发送寄存器空”) | 最后一个字节被截断,表现为偶尔校验失败、重发就好 |
| 6 | 拉低后等接收恢复 | 对端的应答头部被吞掉,表现为“下一帧莫名其妙解析失败” |
三、这里最容易错的一步:什么时候才能拉低
上面六步里,第 5 步是唯一一个“写错了也能跑通大半”的地方,所以它最难被发现。 串口的发送侧一般会对外提供两个状态:
TXE(发送数据寄存器空):表示“发送寄存器空了,你可以把下一个字节写进来”;TC(传输完成):表示“发送移位寄存器也空了,最后一个停止位已经完整地送出去了”。
差别就在这里:TXE 置起来的时候,最后一个字节还在移位寄存器里往外走。
如果看到 TXE 就拉低方向脚,收发器的驱动器会在最后一个字节的中途关断,
最后一个字节(有时是最后两个字节)就被削掉一部分。现象非常有特点:
帧长度看起来对、前面的数据全对、就是校验过不去,而且重发一次往往又好了——
因为重发时对齐的时刻不同,被削掉多少位是随机的。
正确的判据是 TC。用中断实现时,这一点更要注意:
要挂在“传输完成”那个中断源上,而不是“发送寄存器空”那个中断源上。
很多芯片的串口只有一个发送中断入口,进中断后再靠标志位区分是 TXE 还是 TC;
库函数里那个名字叫“发送完成回调”的函数,实际往往是被 TXE 触发的——
我第一次用时就栽在这里,回调里切方向,结果最后一个字节常年被削。
/* 错误示范:等“发送寄存器空”就切方向 —— 最后一个字节还在移位寄存器里 */
while (!uart_tx_register_empty()) { } /* TXE:只说明“可以写下一个字节” */
RS485_DIR_RX(); /* ← 最后一个字节被中途截断 */
/* 正确判据:等“传输完成”,此时停止位已经发完 */
while (!uart_tx_complete()) { } /* TC:移位寄存器空且停止位已出 */
RS485_DIR_RX(); /* 现在放开总线才是安全的 */
还有两个相关的小坑。一是用 DMA 发送时:DMA 的“传输完成”只表示“所有字节都已经交给串口了”,
并不表示“最后一个字节已经上过总线”,所以 DMA 完成之后仍要补一次 TC 判断。
二是标志清除的顺序:在中断里切方向时,要先清掉 TC 标志再切,
否则切完方向、中断退出后标志还在,会立刻又进一次中断,形成“发一帧进两次”的怪现象。
四、两种延时的作用不同:切换建立延时与发送完成判定
讲到这里可以把两件事分清楚了。我一开始把它们混为一谈,以为“加一个延时就行”, 结果在某个项目里加了延时还是偶发丢字节——因为加错了地方。
| 对比项 | 切换建立延时 | 发送完成判定 |
|---|---|---|
| 要解决的问题 | 收发器还没准备好,数据就已经上总线 | 数据还没发完,收发器就被关掉 |
| 位置 | 方向脚切换之后、写数据之前 | 最后一个字节写完之后、方向脚切回之前 |
| 实现方式 | 固定等待(延时或空操作),长度按收发器手册取 | 查询或中断等待状态标志,不是“等一段时间” |
| 能不能用另一个替代 | 不能。等再久也不会让“没发完”变成“发完了” | 不能。等到标志位也不会让驱动器更快上电 |
| 漏掉的典型现象 | 第一个字节(尤其起始位)被削,首字节错 | 最后一个字节被截断,校验偶发失败 |
| 怎么验证 | 示波器看方向脚与 TX 上起始位的相对位置 | 示波器看方向脚与最后一个停止位的相对位置 |
所以这两个都不能省,也不能拿一个当另一个用。我的几个项目里,这两处的取值策略也不太一样:
- 液位采集类的小板:收发两侧各留了一段固定的等待时间,长度按收发器手册的使能时间向上取整到系统最小延时粒度。 这类小板通信不密集,固定等待最省事。
- 加注机主板:代码注释里明确写了一句“发送完毕后切换读取必须先延时”, 而且切到接收前的等待比切到发送前的等待更长——因为接收侧还要等总线上最后一个停止位被对方识别完。
- 压力变送器:发送函数把所有字节丢完之后固定等一段再切回接收,属于“用固定延时兜底”的写法; 代价是延时写死了,参数表里波特率可配,延时却没跟着波特率走。
- 扫码转接板:两个方向用了两个不同的等待值,发送方向等得比接收方向长, 因为它要在“总线忙仲裁”里判断什么时候可以抢总线,等待时间本身也是仲裁的一部分。
这些取值都不是随手写的,是按“最坏情况 + 余量”定的。而“最坏情况”里最容易忘的一项是 中断延迟:如果切方向之前刚好有个高优先级中断在跑,那几微秒就被吃掉了。 所以我的余量一般不是“刚好够”,而是“够得明显”。
还有一点容易被忽略:这些等待时间最终都建立在系统节拍上。 如果节拍本身不准、或者中断服务比预期慢,算好的等待时间就不成立。 把时基配准、把中断耗时量清楚的办法,我写在 《定时器与 PWM 配置基础:从时基到互补输出》里了。 而封装成宏的那些等待时间到底是不是它名字上写的那个长度,需要按注释反向核算一遍, 这一步我整理成了《定时器与单位换算的坑:为什么注释不能信》里的五步清单。
五、做法二:硬件自动方向(用 TX 信号驱动 DE)
第二类做法不在软件里切方向,而是让数据本身去控制方向。
经典的电路是这样的:串口的 TX 引脚平时是高电平(空闲态),
拿它驱动方向脚,中间加一级反相——TX 空闲时方向脚无效(接收态),
TX 上出现起始位的下降沿时方向脚有效(发送态),最后一个停止位让 TX 回到高电平,
方向脚随即失效。整个过程软件完全不参与,收发器跟着数据自己“换挡”。
它的好处很实在:
- 不占 GPIO。方向脚不需要 MCU 管,PCB 上把它留给别的功能。
- 固件零侵入。收发路径上不用成对调用宏,少一个漏调的机会。
- 切换快。延迟只有门电路(或三极管)的传播时间,比调度一个中断快得多。
但它有一个结构性的毛病:方向脚的触发信号就是起始位本身。 收发器从“接收态”切到“发送态”需要一段使能建立时间,而这段等待正好发生在起始位刚开始的时候。 等驱动器真正有力气驱动总线时,起始位可能已经过去一两个位时间了。 对端看到的就是一个没有起始位(或起始位太窄)的字节—— 于是第一个字节读错,后面所有字节都对。
这个失败指纹很有用:“永远只有第一个字节错”, 在我这里基本可以判定是硬件自动方向的建立时间问题,而不是线路或波特率问题。 常见的缓解办法有三个:
- 软件先发一个哑字节把方向“顶”起来,再发真实数据。牺牲一个字节的时间,换第一个真实字节的正确性,最简单有效。
- 让 TX 提前拉低:在真正发送前先手动把 TX 拉低一小会儿,触发方向切换,再交还给串口。
- 把首字节设计成固定帧头并允许它出错:不推荐。它把“通信可靠”建立在“对端能容忍错字节”上,很难讲清楚。
另外,很多这类电路会用一组 RC 把方向脚的释放时刻往后拖,
保证最后一个停止位真的发完再放开总线。R 和 C 要按波特率范围来选:
波特率低的时候位时间长,RC 要更大;波特率高的时候 RC 太大又会拖住总线不放手。
这也是它最难受的一点——一套 RC 很难同时覆盖很宽的波特率范围。
六、做法三:外设级自动方向(串口的 RS485 模式)
第三类做法介于两者之间:方向脚仍然接到 MCU 的引脚上, 但控制它的不是软件,也不是外部电路,而是串口外设自己。 不少 MCU 的 USART 带一个 RS485 模式:使能之后,硬件在发送开始时自动把方向脚置为有效, 在最后一个停止位发完之后自动撤销,软件只需要像平时那样写数据。
这类外设通常提供三样可配置的东西,配置顺序我一般这么走:
- 先按普通串口配好波特率、数据位、校验位、停止位。方向控制是加在串口之上的功能,底层没配对之前谈它没有意义。
- 打开 RS485 模式位,并选对极性。极性要跟收发器一致:
DE高有效就把极性配成高有效, 电路上如果加了一级反相,这里就必须反过来。 - 配好断言与撤销的保持时间。方向脚需要提前一段时间有效(等收发器使能)、 也需要滞后一段时间失效(等最后一个停止位出去)。这两个时间通常以“位时间的分数”为单位配置, 好处是能自动跟随波特率,不用软件改延时。
- 确认方向脚的复用位置。外设自动方向往往只能在固定的几个引脚上使用, PCB 画好之后才发现“这个 USART 的 DE 只能从另一个脚出来”是很常见的事,所以要在原理图阶段就查清楚。
- 最后再使能串口与方向输出。配置期间让方向输出保持关闭,避免配到一半就开始驱动总线。
它的限制同样明确,我在选型时会重点看这四条:
- 受型号限制。不是所有串口都有这个模式,往往同一颗芯片上只有部分 USART 支持; 如果一块板子上挂了好几路 485,很容易出现“一半能用、一半不能用”的尴尬局面。
- 保持时间的粒度是位时间的分数。低波特率下“多少分之一位”折算出来大得离谱, 高波特率下又可能不够用;配置时要按波特率范围的两端各算一次。
- 收发器可能比外设更慢。如果收发器手册给出的使能建立时间超过了外设能提供的最大保持时间, 硬件自动方向仍然不够用,还得靠软件在发送前“顶”一下——那就绕回做法一或做法二了。
- 调试时现象更像硬件问题。方向脚波形在示波器上很干净,软件里又“什么都没做”, 一旦出问题,很容易怀疑到收发器或线缆上,反而比手动方案更难查。
我自己的板子上没有用过这一类模式,方向脚都是普通 GPIO。原因见下一节,但这里要说明一点: 外设自动方向本身是好东西,尤其是波特率固定、芯片恰好支持、又想把 CPU 占用压到最低的场合。
七、三种做法的对照表
把三种做法放在同一张表里对比,选择就清楚了。我按自己最关心的五个维度来排:
| 维度 | 硬件自动(TX 驱动 DE) | 外设自动(串口 RS485 模式) | 软件手动(GPIO) |
|---|---|---|---|
| 切换延迟 | 最小,只有门电路传播时间;但使能建立时间被“吃掉”,首字节有风险 | 由硬件按位时间自动缩放,能跟随波特率;粒度受外设限制 | 由软件显式决定,可精确到系统最小节拍;取值偏保守时最慢 |
| 占用引脚 | 0 个 GPIO,但要多几个分立元件 | 0 个 GPIO,但要占用外设指定的复用引脚 | 1 个 GPIO |
| 对固件的侵入 | 几乎没有,收发路径不用改 | 只需初始化时配置,收发路径不用改 | 收发路径上要成对调用切换宏,漏一处就出问题 |
| 典型失败模式 | 第一个字节的起始位被削,首字节固定错 | 保持时间不够则末字节被截断;受型号与复用限制 | 忘切、切早、切晚三种,都能在代码审查里看出来 |
| 适合的场景 | 波特率固定、单机主发、对首字节不敏感、板上有空间加元件 | 芯片支持、波特率固定、批量产品、希望 CPU 占用最低 | 任何 MCU、宽波特率范围、多机总线、需要软件仲裁总线 |
| 调试难度 | 中等,要同时看 TX 与方向脚 | 较高,软件里“什么都没做”让人容易怀疑硬件 | 最低,现象有指纹,代码可静态审查 |
八、为什么我的项目都用软件手动
这一节如实说,不是“手动更好”,而是在我的项目约束下手动更划算。四条理由:
- 硬件自动方案要加元件。板子本来就小,多一个三极管或门电路,就多一处失效点、 多一个温漂变量、多一条“为什么这块板子行、那块板子不行”的排查路径。 而方向脚用 GPIO 是“本来就富余”的资源,不用白不用。
- 外设自动受型号限制。我手上几款 MCU,有的没有 RS485 模式,有的只在部分串口上有; 而一块控制板上可能同时挂四路 485(比如集控类的板子,一路接执行机构、一路接光源、 一路接断路器、一路留作调试)。如果一半自动一半手动,风格不统一,维护成本反而更高。
- 手动方案只要把两处延时写对就够稳。把方向切换封成一对宏, 所有收发路径都走它们、不在别处直接写 GPIO,那么“漏切”这件事在代码审查阶段就能查出来。 相比之下,硬件自动方案的问题要在示波器上才看得见。
- 总线上要做半双工仲裁。扫码转接板那块板子的核心逻辑就是“总线忙检测 + 超时退让 + 应答重发”, 这些判断都要求软件能在任意时刻主动决定“我现在放不放开总线”。 手动方案随时能把方向脚放开;而自动方案下想“提前放弃发送”,反而要跟外设较劲。
手动方案的代价也要说清楚:固定等待会浪费总线时间。 收发两侧各留一段等待,加起来可能相当于好几个字节的传输时间。 在波特率不高、报文又不长的场合完全可以接受;但如果协议做得很密(一问一答之间不留缝), 这些等待就会累积成“轮询周期跑不起来”。我在加注机主板上就是这么被教育的—— 最后把提问改成了“等到应答再发下一条”,让等待时间藏进本来就要等的间隙里。
九、多机总线上的额外约束
单机点对点时方向脚只影响自己;挂到多机总线上,它就变成了发言秩序的一部分。 下面几条是我在实现从站时反复提醒自己的:
- 从站不能抢答。 主站发完请求后要切换到接收态,这个切换本身要花时间。 从站如果“收到就立刻回”,主站还没切过来,回应的头几个字节就被丢掉了。 所以从站要留出静默时间,把主站的换挡时间算进去。我在实现里把方向切换的等待算在内, 在常见波特率范围内留出足够余量后,就没有再出现过“主站偶尔收不到应答”。
- 收完要尽快放开总线(或者干脆禁收)。 半双工下自己发出去的数据会回灌到自己的接收脚上, 如果一边处理一边还允许接收,这些回波会被当成新帧的开头,把帧同步状态全部搅乱。 有一类做法是“解析函数一进来就切到发送态”,等于收完立即禁收,处理完再切回; 代价是处理期间收不到新帧。这个取舍我写在帧同步那篇里了。
- 广播帧只执行、不应答。 广播地址的帧没有应答环节,从站必须留在接收态。 如果哪个从站“顺手回了一句”,总线上就会有多个节点同时驱动——轻则这一帧谁都读不懂, 重则收发器长时间对打发热。这是我认为最容易造成“整条总线突然全哑”的一类错误。
- 地址不匹配时不要有任何动作。 包括不要为了“确认收到”而切一下方向脚。 方向脚一旦切到发送就应该真的发出东西,否则宁可不动。
- 主站侧的轮询周期要含换挡时间。 如果按“一帧传输时间”排轮询, 跑起来会发现越来越紧,最后出现“上一帧还没发完就开始收”的现象。
| 总线上的情形 | 方向脚应有的状态 | 为什么 |
|---|---|---|
| 上电与空闲 | 接收态(含外围偏置) | 任何节点都不应该在没人说话时驱动总线 |
| 收到发给自己的请求 | 保持接收态,直到本帧收完 | 提前切换会把本帧尾巴截掉,也会丢掉紧跟其后的字节 |
| 准备应答(等待静默时间) | 已经切到发送态并等使能建立 | 静默时间本身也是给对端留的换挡时间 |
| 应答发送完毕 | 等发送完成标志后切回接收 | 早切会截断末字节,晚切会吞掉主站的下一帧 |
| 收到广播帧 | 全程保持接收态,只执行不应答 | 避免多节点同时驱动总线 |
十、一份方向控制自查清单
这份清单是我在交付前会逐条过一遍的,按“初始化 → 发送 → 接收 → 总线”的顺序排:
| # | 检查项 | 不通过的典型后果 |
|---|---|---|
| 1 | 上电默认态是不是接收?方向脚浮空时外围有没有偏置? | 上电瞬间误驱动总线,收到来历不明的数据 |
| 2 | 初始化顺序是不是“先配方向脚、再使能串口”? | 初始化阶段的一帧丢失 |
| 3 | 发送前是不是先使能、并等了使能建立时间? | 首字节起始位被削 |
| 4 | 发送完成的判据用的是传输完成标志,不是“发送寄存器空”? | 末字节被截断,校验偶发失败 |
| 5 | 用 DMA 发送时,DMA 完成之后有没有补一次传输完成判断? | DMA 一完成就切方向,同样截断末字节 |
| 6 | 切回接收之后有没有等接收恢复? | 对端的应答头部被吞掉 |
| 7 | 中断里切方向时,标志是不是先清后切?有没有重复进入? | 发一帧进两次中断,时序被自己打乱 |
| 8 | 方向切换是不是只有一对宏/函数,没有别处直接写 GPIO? | 新增收发路径时漏切方向 |
| 9 | 波特率改变时,两处等待时间是否跟着调整? | 高波特率下余量不足,低波特率下浪费总线时间 |
| 10 | 广播帧是不是只执行、不应答? | 多节点同时驱动总线 |
| 11 | 从站应答前是否留了静默时间? | 主站还没切到接收,应答头部就丢了 |
| 12 | 遇到“校验偶发失败”时,先看的是方向脚时序而不是线缆? | 换线换终端电阻白忙一场,问题还在 |
十一、几条我反复用到的经验
- 方向脚的问题几乎都是时序问题,不是器件问题。 换了收发器、换了线、降了波特率都没改善的通信故障,我现在第一件事就是同时看 TX 和方向脚。
- “等发送寄存器空”和“等发送完成”是两件事。 前者只是允许你写下一个字节,后者才说明最后一个字节上了总线。切方向的判据必须是后者。
- 两处等待不能互相替代。 切换建立延时解决“发不出去”,发送完成判定解决“发不完”。 我在同一个项目里见过两个都缺的写法,也见过只加了一个还以为万事大吉的写法。
- 把方向切换封成一对宏,不许别处直接写 GPIO。 这条纪律的价值在于:漏切会变成“编译期看得见的不一致”,而不是“现场偶发故障”。
- 上电默认态当成功能来设计。 “复位后方向脚是输入”不是设计, 外围偏置 + 软件显式初始化才是设计。
- 失败指纹比测量更快。 “只有第一个字节错”指向使能建立时间, “只有最后一个字节错”指向发送完成判定,“下一帧头部丢”指向接收恢复—— 这三种现象我基本不再需要用示波器就能猜个八九分,然后再去验证。
- 选做法看约束,不看先进程度。 硬件自动、外设自动、软件手动各有适用面; 我这几个项目选手动,是因为板小、元件贵、路数多、要仲裁,而不是因为手动“更好”。
参考资料与说明
- TIA/EIA-485 串行通信标准中关于差分总线、驱动器使能与接收器使能的定义,属于公开标准。
- 各厂商 MCU 参考手册中 USART 章节关于发送寄存器空(TXE)与传输完成(TC)标志的定义, 以及部分型号 RS485 模式(方向输出、断言与撤销保持时间)的说明,属于公开文档。
- RS485 收发器数据手册中的驱动器使能时间、接收器使能时间与失效保护特性,属于公开文档。
- 出于对个人项目实现细节的保护,本文只公开方法与思路;涉及核心实现的部分以步骤与字段说明代替代码,示例代码为按个人理解重写的通用片段。
- 文中提到的做法均来自我个人学习项目中的实际调试过程,不包含任何具体产品的初始化实现、私有帧格式或现场参数。
- 文中出现的芯片与器件类型仅用于说明技术方案,与相关厂商无隶属或授权关系。