把两个设备连在一起以后,真正存在于物理世界的只有一件事:线上的电平在变。
什么电压算 1、什么电压算 0,一位占多长时间,一串比特从哪开始算一个字节,两个字节之间怎么区分------这些没有一条是自然规律,全部是人事先约好的。 所谓电平标准,说白了就是"什么电压算 1、什么电压算 0"的那张对照表:它是人为规定的,不是电压自己长出来的规矩。协议两个字听起来专业,本质就是"我们说好这样"。
这里不接新的硬件,只把这件事讲透:为什么同一个字节在不同约定下能是三种东西、约定到底约定了哪几层、以及为什么现代通信是一摞约定叠起来的结果。
同一个字节,三种意思
拿 0x41 这一个字节举例。
在十六进制模式的串口助手里,它显示成 41;把同一个窗口切到文本模式,它显示成 A。字节一个都没变,变的是你用什么约定去解释它:一个是"原始数据",一个是"按某种编码读出来"。
我们这条链路上,同一个字节的位置每换一层,意思也跟着换:
text
线上: 一位一位的电平 ------ 只有 0 和 1,没有别的
串口帧: 起始位 + 8 位数据 + 停止位 ------ 一个字节被装进一帧,低位先行
应用帧: AA 55 01 01 02 00 01 05 0D ------ 其中一个字节是"命令字"
业务层: 02 表示前进 ------ 到这一层才有"意思"
再换一条链:173,486 这几个字符送进 VOFA+,FireWater 约定把逗号当分隔、把 \r\n 当结尾,于是它变成两条曲线上的两个数。同样的字符如果按"文本日志"去读,它就只是一句话。
还有一层更容易被忽略的例子:I2C 上起始条件之后的第一个字节不是数据,而是"7 位地址 + 一位读写方向"。那个字节在协议里是地址,在设备内部是"谁在跟我说话"。
字节本身没有意思。 意思来自双方共同的约定;约定不变,换一个解释它的地方,它就换一个身份。这也是为什么同一份代码里,0x41 可以是数字、可以是字符 'A'、也可以是某个设备的命令------看它在哪个位置上被谁解释。
约定都约定了哪几层
把这一路见过的约定排成一张表,脉络就清楚了:
| 约定什么 | 具体内容 | 我们见过的例子 |
|---|---|---|
| 电平 | 什么电压算 1、什么算 0 | TTL 用 3.3V 或 5V 表示 1、0V 表示 0;RS232 用负电压表示 1;RS485 用两根线的压差表示 |
| 位与帧 | 一位占多久、一帧怎么摆 | 115200 8N1:波特率、8 位数据、无校验、1 位停止;低位先行 |
| 边界 | 一串字节从哪开始、到哪结束 | 文本命令靠 \r\n;AA 55 当帧头、末尾一个 0D 当帧尾 |
| 完整性 | 数据错了怎么发现 | 校验位、SUM8、CRC;发现之后是丢弃还是重来 |
| 含义 | 哪个字节是命令、哪个是参数 | 命令字 02 是前进、06 是设速度;内容里那一个字节是百分比 |
| 对话规则 | 谁先说话、要不要应答、等多久算超时 | I2C 由主机产生时钟、第 9 拍看应答;我们那条链回一条应答帧 |
| 文本约定 | 怎么把数据写成一行字 | FireWater 用逗号分隔、\r\n 结尾;控件命令写成 [slider,1,120] 这样 |
这七行没有哪一行是物理定律,都是当时做协议时选出来的:选了 TTL 而不是 RS232,选了 115200 而不是 9600,选了 AA 55 而不是别的帧头。选哪个都能跑,只要两边一样。
完整性这一层:错了怎么发现
上面那张表里,"完整性"那一格只写了三个词:校验位、SUM8、CRC。三个词背后是同一件事的三种做法,代价和本事差得很远。这一节把它们拆开。
先分清一件容易混的事:校验位和校验和不是一回事。
校验位是硬件干的:跟着一个字节走,就一位,接在数据位后面。发送方算好填上,接收方再算一遍,对不上就把这个字节标成"收到过、但不可信"。
校验和是软件干的:跟着一整块数据走,可以是一个字节、两个字节、四个字节,算完放在帧里某个约定好的位置上。
一个是链路层替你做的,一个是你自己在协议里安排的。混着说,就会以为"串口开了校验,就不用校验和了"。
| 做法 | 谁算的 | 跟着什么走 | 占几位 | 能查什么 | 查不出什么 |
|---|---|---|---|---|---|
| 奇校验 (偶校验) | 串口硬件 | 一个字节 | 1 位 | 奇数个位翻转 | 偶数个位翻转 |
SUM8 |
软件 | 一整块 | 1 字节 | 单字节错、字段错位、少一个字节 | 一加一减、字节互换位置 |
SUM16 |
软件 | 一整块 | 2 字节 | 同上,碰巧对上的概率低得多 | 同样挡不住换位 |
| XOR | 软件 | 一整块 | 1 字节 | 某一位翻过一次 | 同一位翻两次、两个字节同位互抵 |
| CRC16 | 软件或硬件外设 | 一整块 | 2 字节 | 绝大多数随机错、一定长度内的突发错 | 专门构造出来的错 |
| CRC32 | 软件或硬件外设 | 一整块 | 4 字节 | 同上,覆盖面更宽 | 同上 |
| Fletcher / 加权和 | 软件 | 一整块 | 2 字节 | 多了"换位"这一类 | 加权本身也能被抵消 |
| 汉明码 | 软件或逻辑 | 一个定长块 | 多出 3 位 | 错一位,而且能改回来 | 错两位 |
下面一种一种看。
奇偶校验:一位,只够查出"有位翻了"
把数据位里 1 的个数数一遍。奇校验要求连同校验位一起,1 的总数是奇数;偶校验要求是偶数。发送方按这个规矩填校验位,接收方再数一遍。
拿 0x41 举例,二进制是 0100 0001,里面有两个 1。
- 偶校验:两个 1 已经是偶数,校验位填
0; - 奇校验:要凑成奇数,校验位填
1。
它便宜到几乎不要钱------就一位。代价是它只认识"奇偶",不认识"位置"。
一位翻了,奇偶性就变,查得出。两位同时翻,奇偶性不变,查不出。四位也一样。所以奇偶校验只能查出奇数个位翻转,偶数个一律放过。 线上一旦开始出错,常常是成片翻,这时候它基本是瞎的。
STM32 上还有一个具体的坑。
Uart_Init() 里那一行 uart.USART_Parity = USART_Parity_No;,多数人照着抄,不知道另外两个选项填进去会发生什么。参考手册写得很直接:使能校验位以后,校验位插在字长最高位的位置上------8 位字长就是第 8 位,9 位字长就是第 9 位。
也就是说,USART_WordLength_8b 配上 USART_Parity_Even,数据位只剩 7 位,第 8 位被校验位占了。要真的"8 个数据位 + 1 个校验位",得写 USART_WordLength_9b,那个 9 是连校验位一起数的。
这也解释了我们这套帧为什么用 No:帧头是 0xAA,十进制 170,7 个数据位装不下。
SUM8:把字节当数字加起来
把要保护的那几个字节当成一串无符号整数,全加起来,只留最低 8 位。
text
0x01 + 0x01 + 0x02 = 0x04
它挡得住三类现场问题:少了一个字节;某一个字节被干扰改掉;字段整体错位(错位以后各字节的位置全变了,和几乎不可能还一样)。
加法的漏洞来自它的好处:它不认位置。
两个字节互换,和不变。一个字节加一、另一个减一,和也不变------0x28 + 0x01 和 0x27 + 0x02 都是 0x29。所以干扰要是恰好成对地一个加一个减,SUM8 一点反应都没有。
SUM16:一样的加法,宽一点
同样的加法,只是留 16 位,结果占两个字节。
text
0x00FF + 0x0005 + 0x0002 = 0x0104
好处只体现在"碰巧对上"的概率上:8 位是 1/256,16 位是 1/65536。它能挡的类别和 SUM8 一样,挡不住的也一样------换位、成对加减,照样过去。
代价有两个:帧里多一个字节;还有一个必须约定清楚的事------两个字节谁在前。发送方按高字节在前装进去、接收方按低字节在前读,校验永远对不上,而且现象很难看。
XOR:对"位"敏感一点
把每个字节按位异或,最后得到一个字节。
text
0x41 ^ 0x55 = 0x14
异或和加法的区别在进位:加法会把低位的进位带到高位,异或不做这件事,每一位各算各的。两个相同的值异或回 0,这是它最常用的性质。
它比加法敏感的地方在"单个位":某一个字节里有一位翻了,异或的结果一定变。加法在这种情况下也可能变,但当翻转刚好被进位吃掉时就不一定。所以对付"偶发的单个位翻转",异或比累加和可靠一点。
它的窟窿也很清楚:同一位上翻两次,异或的结果回到原样;两个字节分别在相同的位上各翻一次,也是互相抵消。它同样不认位置------顺序换了结果不变。
很多按行传输的文本协议在行尾放两个十六进制字符表示这个值,就是因为它几行代码就能算完,写出来也只有四个字符。
CRC:不是加法,是除法
校验和家族里真正换了一层的,是 CRC。
它不再把数据当"一堆要相加的数",而是当一个二进制多项式 :一串比特,从高到低,第几位上有 1,就代表多项式的哪一次项存在。1011 就是 x³ + x + 1。
然后约定一个生成多项式 ------比如 1001,也就是 x³ + 1。做法是:在数据后面补上若干个 0(补的个数等于生成多项式的阶数,这里是 3),然后做模 2 除法。模 2 的意思是不借位,减法和加法一样都是异或。除到最后剩下的余数,就是校验值。
数据 1011、生成多项式 1001,一位一位算:
text
被除数 1 0 1 1 0 0 0
除数 1 0 0 1
余数寄存器 r 每次左移一位、把下一位移进来;
只要 r 的最高位是 1,就 r = r ^ 除数。
开始 r = 0000
移入 1 → r = 0001 最高位 0,不动
移入 0 → r = 0010 最高位 0,不动
移入 1 → r = 0101 最高位 0,不动
移入 1 → r = 1011 最高位 1 → r = 1011 ^ 1001 = 0010
移入 0 → r = 0100 最高位 0,不动
移入 0 → r = 1000 最高位 1 → r = 1000 ^ 1001 = 0001
移入 0 → r = 0010 最高位 0,不动
余数 = 010
送出去的是 1011 010:原来的 4 位数据,后面接 3 位余数。
为什么它比加法强:
- 单个位翻转,余数一定变。 只要生成多项式的常数项是 1(最低位是 1),
x的任何次幂都除不尽它,余数就不可能还是原来那个。 - 一段连续的位翻转能查出来。 突发错的长度只要不超过生成多项式的阶数,基本全能抓住。
- 生成多项式带
x + 1这个因子时,所有奇数个位错误都能查出来。 常用那几个多项式都带这个因子。 - 不同的生成多项式擅长抓不同形状的错误,所以才会并存那么多个标准版本。
代价是算得慢:要么一位一位移,要么事先把表算好、用 256 项的表换速度。两种算法结果完全一样,只是拿内存换时间。
参数也不止多项式一个。工程上说 CRC16,通常还要交代四件事:生成多项式、初值、输入字节是否按位反转、最后是否再异或一个常数。同样叫 CRC16,CCITT-FALSE 是 0x1021 + 0xFFFF + 都不反转 + 不再异或,MODBUS 是 0x8005 + 0xFFFF + 都反转 + 不再异或。参数不一样,同样一串字节算出来的两个字节完全不同------这也是"照抄一段 CRC 代码却对不上"的最常见原因。
CRC32 是同一套东西加宽:多项式 0x04C11DB7,初值全 1,最后再异或全 1。压缩包里每个文件、以太网每一帧的末尾都挂着它。宽度换来的还是概率:32 位碰巧对上是 1/4294967296,日常干扰基本到不了这个量级。
汉明码:从"发现"到"改正"
前面所有办法都只能发现 错误。发现之后能做的只有丢掉、重来,或者带着错继续跑。汉明码多做了一步:定位到哪一位,并且改回来。
思路是给数据块的每一位编号,再让几个校验位分别覆盖"编号里某一个二进制位是 1"的那些位置。编号从 1 到 7,三个校验位分别盯住"编号最低位是 1""中间位是 1""最高位是 1"的位置:
text
位置 1 2 3 4 5 6 7
内容 p1 p2 d1 p4 d2 d3 d4
p1 覆盖 1、3、5、7
p2 覆盖 2、3、6、7
p4 覆盖 4、5、6、7
四个数据位 1011,三个校验位按组异或:
text
p1 = d1 ^ d2 ^ d4 = 1 ^ 0 ^ 1 = 0
p2 = d1 ^ d3 ^ d4 = 1 ^ 1 ^ 1 = 1
p4 = d2 ^ d3 ^ d4 = 0 ^ 1 ^ 1 = 0
送出去的是 0 1 1 0 0 1 1。
接收方把三组各自再算一遍,得到一个三位的"症状值"。全 0 就是没错;不全 0,那个二进制数就是出错位的位置。
比如第 5 位(d2)在传输中从 0 翻成了 1,收到的是 0 1 1 0 1 1 1:
text
s1 = p1 ^ d1 ^ d2 ^ d4 = 0 ^ 1 ^ 1 ^ 1 = 1
s2 = p2 ^ d1 ^ d3 ^ d4 = 1 ^ 1 ^ 1 ^ 1 = 0
s4 = p4 ^ d2 ^ d3 ^ d4 = 0 ^ 1 ^ 1 ^ 1 = 1
症状值 = s4 s2 s1 = 101 = 5
第 5 位,对上了,翻回来就行。
它解决的是"一位错"。两位错时症状值同样不是 0,但指的位置是错的------距离为 3 的码,纠一位和查两位只能选一个。实际用在内存上的 ECC 会再加一位总校验位,才做得到"纠一位、报两位"。
链路通信里一般不用它,原因很实际:
- 代价高。 4 位数据配 3 位校验,多出 75% 的位;串口线上每一位都要花时间,这个比例换不起。
- 要求固定分块。 我们的帧是不定长的,内容字段从 0 到 16 个字节都有,凑不成固定块。
- 救不了成片错。 线上一旦开始出错,常常连着坏好几个字节,一次只纠一位的码没有意义。
它真正在的地方是另一个场景:内存的 ECC、卫星链路、早期的磁盘和磁带------那些地方重传的代价远高于多存几个校验位。
落回我们这套帧
AA 55 ... SUM8 0D 选了 SUM8,理由和当初选它的过程一样朴素:一个字节、几行代码、软件算得动,而且现场最常见的三类错------少一个字节、某个字节被改、字段整体错位------它都能挡住。
它挡不住的两类前面都点到了:一加一减成对出现、两个字节互换位置。第二类特别要留意,因为它不是"碰巧",而是加法本身的性质------只要协议里有两个字段会被整体对调,SUM8 就永远看不见,XOR 也一样(异或同样满足交换律)。
真要挡住换位,最便宜的办法是给每个字节加位置权重:第 1 个字节乘 1、第 2 个乘 2,依此类推再累加。这就是 Fletcher 校验和的思路,它对"顺序"敏感,代价是多一次乘加、结果还得宽出一点。再往上一步就是 CRC,不认位置这件事它是从根上解决的。
什么时候该往上换?051 给过判据,这里补全:线路变长(多了一段线、换成无线)、干扰变强(电机和串口走在一起、电源不干净)、报文变重要(一条命令错了会让车撞墙)、数据错了之后没人能重来(固件、配置)------满足任何一条,就该从 SUM8 换到 CRC16;固件这种错一个字节就变砖的,直接上 CRC32。
最后一句留在这里:这些手段都只是让错误大概率暴露出来,不是防止有人故意改。 有人想动你的数据,他会连着校验一起改------那是加密和签名那一层的事,不是这一节的题目。
没有"对错",只有"一不一致"
约定最特别的地方在这里:它不产生"对"和"错",只产生"一致"和"不一致"。
两边约定不一样时,现象是这几种:
- 波特率不一致:收到的不是"某个字节错了",而是整行不可读------接收方在错误的时间点采样,每一位都被切错;
- 帧格式不一致(数据位、校验位、停止位不同):同样不可读,或者偶尔对、偶尔错;
- 地址不一致:设备一声不吭,连错误都收不到,因为根本没人应答;
- 应用格式不一致 :收到了、也读出来了,但认不出是什么意思------
LED ON收到了一句Unknown command。
注意最后一种:前面三层全对,链路是通的,问题出在"这句话在我这儿不算命令"。这就是为什么排查必须按层来:先看物理(线、供电、共地),再看参数(波特率、校验位、停止位),再看边界(帧头、长度、校验),最后才是含义(命令对不对)。跳着查,就会在一个完全没问题的层次上浪费一下午。
一致性还有一个副作用:约定一旦改了,另一边必须知道。 所以正经协议会留出余地------长度字段让格式以后能变长,版本字段让新旧设备能认出彼此。我们自己那套帧把版本和长度都留在了帧里,不是为了现在用,而是为了以后改的时候,旧的程序不至于照着错误的格式去解释新的数据。
那为什么大多数时候直接用别人定好的
既然约定可以自己定,为什么我们还在用 115200 8N1、还在用 I2C、还在用蓝牙?
因为标准协议是别人替你约好、并且已经跟全世界约好了的那一份。 你遵守它,就等于瞬间跟所有现成设备通了话:I2C 让一块屏幕和一颗陀螺仪能挂在一起,蓝牙让手机能连上小车,USB 让插上去就能被认出来。代价是你要按它的规矩办------I2C 要开漏加上拉,蓝牙要先配对,USB 有一整套枚举流程。
私有协议的好处正好相反:自己定的最贴合自己的需求。 虽然不是同一层,不过意思大差不差,我们那套 AA 55 ... SUM8 0D 就是为了这几辆小车设计的:短、能验错、能重同步、自己一眼看得懂。代价也很清楚:只有你懂,出问题没人替你查,而且必须自己把文档写下来。
判据其实很短:有现成的,先用现成的;真要自己定,就把帧格式、字段含义和错误处理写清楚。 一份没写下来的私有协议,等于没有协议------过半年连自己都要重新猜一遍。前面那篇讲 I2C 的时候有一句话可以搬过来:一个没有来历的数字,就是下一次"神秘"的来源。
谁把约定写成了标准
约定谁都能定,但能被叫作"标准",是因为它走完了另一段路:有人把它写下来、公开发布,并且让很多互不相识的人按它做。
回头看这些名字的出身,都是具体的公司或组织:I2C 出自飞利浦,SPI 出自摩托罗拉;CAN 这类总线也一样,先是一家公司为了解决自己车上的问题定下来的规则,后来被更多人采用,才变成行业里共享的约定。串口那一套(波特率、起始位、停止位)更早,早到我们今天用的 8N1 还带着当年电传打字机的痕迹。
这里有个很实际的差别:
- 有正式文档的标准:你遇到问题可以去查规范,别人已经替你踩过坑;代价是规范往往写得很厚,读起来不轻松;
- 事实标准:用的人多了就成了约定,文档可能散落在十几份应用笔记里;
- 私有协议:文档就是你自己的笔记。写得好,它就是一份能用的规范;写不好,它就只是一个"当初能跑"的程序。
所以"自己定一套"从来不是不行,而是你要顺手把标准该有的那部分工作也做了。
协议和实现是两件事
同一个约定,可以有很多种实现方式------这一点在前面已经撞见过一次。
I2C 是同一份协议,但实现有两套:前面那套软件 I2C 是 CPU 一位一位拨出来的,硬件 I2C 是外设自己产生时钟与起始终止。协议没变,实现换了。 于是程序里能看见的东西也变了:一套能让你数出每一拍,另一套把每一拍藏起来。
我们的帧协议同样如此。AA 55 ... SUM8 0D 这套约定在单片机上是一段状态机,在电脑上可以是一个几十行的脚本,在手机上可以是某个 App 里的一串十六进制。只要两边按同一份约定解释字节,用什么语言、什么平台、什么芯片都不重要。
分辨这两件事很有用:
- 换实现(软件 I2C 换成硬件 I2C、F103 换成 F407、C 换成 Python):协议不用动,两边照旧通话;
- 换协议 (
AA 55帧换成别的格式、\r\n换成分号结尾):两边都得改,因为约定本身变了。
我以前也把这两件事混在一起,改协议时只改过一边,换芯片时还白白重写过一遍协议。
计算机网络:约定叠起来的样子
"协议"这个词被广为人知,是因为计算机网络。而计算机网络之所以看起来复杂,是因为它不只一个协议,而是一摞。
随便打开一个网页,这件事在底下大概是这样的:Wi-Fi 负责无线电怎么传帧,IP 负责把包送到哪台机器,TCP 负责顺序和丢了重发,HTTP 负责说明这次请求是什么意思。四层,四种约定,各管一件事。
它们之间有一条规矩,要单独记下来:每一层只跟对面的同一层说话。 HTTP 不关心下面走的是 Wi-Fi 还是网线,Wi-Fi 也不知道上面传的是网页还是一封邮件。正因为这样,家里路由器从 Wi-Fi 5 换成 Wi-Fi 6,你的浏览器不需要改一行设置------底下那层的约定换掉了,上面那层照旧。
我们这条链路其实是同一个形状,只是短一些:
text
电平与时序 串口的 8N1
链路 蓝牙 SPP(或者一根 USB 线)
应用帧 AA 55 ... SUM8 0D
业务 前进、停止、差速
上一回换模块的时候已经验证过这件事了:把 CH340 换成蓝牙,帧协议一个字没改,业务层一个字没改------因为变的是"链路"那一层,上面两层根本不知道底下发生了什么。分层不是学术名词,它是"换东西的时候不用推倒重来"这件事的实现方式。
顺带回答一个常被问的问题:为什么网络协议要分这么多层,而不是定一个大的? 因为每一层面对的麻烦不一样:无线电要处理干扰和重传,IP 要处理寻址和路由,TCP 要处理顺序和丢包,应用层要处理"这次请求是什么意思"。把它们塞进一层,改任何一处都要动全身;分开以后,换 Wi-Fi 不影响浏览器,这就是那一摞约定存在的理由。
人类也在用协议
把两个国家的人放在一起,就是两套约定碰面。中文和英语之间没有谁对谁错,只有"能不能互通"------这和两台参数不一致的设备一模一样:两边都是好的,就是听不懂对方。
人类试过的办法,和设备之间那几种办法一一对应:
- 一方学另一方。 设备 A 改成和设备 B 一样的约定:改波特率、改帧格式、改地址;
- 约定一个中间层。 国际上用一门共同的语言当中间层,各方说自己的母语,但要先翻成那门中间语言再传出去。网络里也是这样:底下走 Wi-Fi 还是网线无所谓,上面都走同一套 IP;
- 中间站一个翻译。 翻译不改变你要说的内容,只改变表达方式。CH340 和蓝牙模块干的就是这件事------一边是 USB 或无线电,一边是串口,中间那个人只管把话转过去,不替你做决定。
翻译还有一件事要单独记住:它一定会丢东西。 双关语、语气的软硬、称"您"还是"你",换一门语言常常没法原样保留;两种协议之间转换也一样------字段没地方放就得丢,精度不够就得截,单位不同就得换算。所以要紧的东西不能指望在"翻译"那一步靠默契,必须在约定里显式写清楚:放在哪、单位是什么、范围到哪里。
人类语言里的第三种约定是行业术语 :tHD;STA、失步、重同步这些词,圈内一句话说明白,圈外一句都听不懂。它和私有协议是一类东西:为了精确和效率牺牲了通用性,代价是新人得额外学一套。
把这一层对上号,很多事就顺了:
- 学一门专业,很大一部分是学它的约定(哪个词指哪个东西、哪个数字代表什么状态);
- 两个人沟通出问题,往往不是谁逻辑错了,而是约定不一致:各自对某个词的理解不同;
- 想让别人听懂你的东西,最省力的做法不是反复解释,而是把约定先写下来------这正是协议文档在做的事。
所以"协议"不是机器的专利。只要有两方要合作,就得先约好怎么读懂对方,机器只是把这件事做得更严格:它不会猜,你说什么它就照什么执行。
三条实用落点
"两边参数明明一样了,为什么还是不通?" 因为要一致的层次不止一层。按顺序对一遍:电平对得上吗(3.3V 设备接了 5V 上拉吗)、参数一致吗(波特率、校验位、停止位)、边界一致吗(一边按 \r\n 分行,另一边按 AA 55 找帧头吗)、含义一致吗(命令字表是不是同一张)。四层里任意一层不一致,现象都可能长得差不多。
"我自己定的小协议,要不要写文档?" 要。至少写三样:帧长什么样(帧头、长度、校验、帧尾)、每个字段的含义、出错怎么办(丢弃、重来、还是回一条应答)。写的对象不是别人,是半年后的自己。
"能不能不用协议,直接发一串字节?" 那一串字节本身就是协议,只是没写下来而已。你省掉的从来不是协议,而是文档和自检------只要两边都按同一套解释去读它,约定就已经存在了。
最后橘猫说
线路上真正存在的只有电平在变,其余全是人为约定。所以协议不产生对错,只产生一致与不一致------不一致的时候没有报错,只有乱码和沉默,只能按物理、参数、边界、含义一层一层往回找。协议和实现要分开算:同一份 I2C,CPU 一位一位拨和让外设自己产生时钟都是它,换实现协议不动,换协议两边都得改。剩下的都是同一件事的小尺寸版本:Wi-Fi 到 HTTP 是各管一件事的叠加,CH340 和蓝牙模块是翻译,翻译一定丢东西,要紧的字段得写进约定里。
最后蜘蛛说
方言也是通信协议