3. MCP协议数据链路层:帧结构、起始条件、停止条件、应答机制、数据有效性
各位好,我是老张。今天咱们聊聊MCP协议的数据链路层。说实话,这一层是MCP协议的「脊梁骨」——所有数据能不能可靠传输,全看它。
我在做逆向分析时,经常遇到有人把MCP帧结构搞混。尤其是起始条件和停止条件,稍不留神就解析错。嗯,今天咱们就把这块彻底掰扯清楚。
3.1 帧结构:数据怎么打包的?
MCP的帧结构,说白了就是一套「信封」规则。数据要发出去,得先装进信封里,贴上邮票,写上地址。MCP的帧就是这个信封。
一个标准的MCP数据帧长这样:
| 起始条件 | 从机地址(7bit) | R/W位 | 应答位 | 数据字节(8bit) | 应答位 | ... | 停止条件 |
我个人习惯把帧分成三部分来看:
- 头部:起始条件 + 从机地址 + R/W位
- 主体:数据字节 + 应答位(可重复多次)
- 尾部:停止条件
你想想看,这个结构其实很巧妙。头部告诉总线「谁要跟谁说话,是读还是写」,主体就是实际传输的数据,尾部告诉总线「话说完了,散会」。
关键点:MCP帧没有固定的长度。主体部分可以包含1个字节,也可以包含N个字节。这也是为什么逆向分析时,你得动态判断帧的结束位置。
3.2 起始条件:SCL高电平时SDA下降沿
起始条件,英文叫START condition。它的定义很简单:当SCL为高电平时,SDA从高电平跳变到低电平。
我刚开始学MCP时,总觉得这个定义太抽象。后来在逻辑分析仪上亲眼看到波形,才恍然大悟。说白了就是:
- SCL在高电平(1)
- SDA从1变成0
- 这个下降沿就是起始信号
为什么要在SCL高电平时变化?因为MCP规定,数据只能在SCL低电平时变化。SCL高电平时SDA变化,就是特殊的「命令信号」。起始条件和停止条件都是这么来的。
我的经验:在Ghidra中解析MCP帧时,我习惯先扫描SDA的下降沿,同时检查SCL是否为高电平。如果两者都满足,就标记为起始条件。这个逻辑写进插件里,准确率很高。
3.3 停止条件:SCL高电平时SDA上升沿
停止条件正好反过来:当SCL为高电平时,SDA从低电平跳变到高电平。
你可以把起始条件和停止条件看作一对「括号」:
- 起始条件 = 左括号 (
- 停止条件 = 右括号 )
所有数据必须在这对括号之间传输。没有起始条件,从机不知道「要开始收数据了」;没有停止条件,从机不知道「数据发完了」。
避坑指南:我曾经在分析一个I2C转MCP的桥接芯片时,发现数据总是解析错。查了两天才发现,芯片在发送停止条件时,SCL的保持时间不够长。MCP协议要求停止条件前SCL高电平至少保持4μs,但那个芯片只保持了2μs。嗯,从此以后我写插件时都会加一个时序检查。
3.4 应答机制:ACK与NACK
应答机制是MCP协议可靠性的核心。每次发送完一个字节(8位数据),接收方必须回复一个应答位。
应答位的规则很简单:
- ACK (0):接收方将SDA拉低,表示「收到了,继续发」
- NACK (1):接收方释放SDA(上拉电阻拉高),表示「别发了,我收够了」或「出错了」
应答位在第9个SCL时钟周期采样。前8个时钟是数据位,第9个是应答位。
| 场景 | 应答位 | 含义 |
|---|---|---|
| 主机发送地址后 | ACK | 从机存在且准备好通信 |
| 主机发送地址后 | NACK | 从机不存在或忙 |
| 主机发送数据后 | ACK | 从机成功接收 |
| 主机发送数据后 | NACK | 从机无法接收(缓冲区满等) |
| 主机接收数据后 | ACK | 主机还要继续读 |
| 主机接收数据后 | NACK | 主机不再读了(读操作结束) |
你发现没有?应答机制其实是一种「握手」。每次传输一个字节,都要确认一次。这比那种「一口气发完再确认」的协议要可靠得多。
3.5 数据有效性:SCL低电平时变化,高电平时采样
数据有效性是MCP协议最基础也最容易忽略的规则。它的核心就一句话:
数据在SCL低电平时变化,在SCL高电平时采样。
换句话说:
- SCL = 0 时,SDA可以随便变(准备下一个bit)
- SCL = 1 时,SDA必须稳定(主机/从机读取这个bit)
这个规则保证了数据在采样时刻是稳定的。如果SDA在SCL高电平时变化,就会被误判为起始条件或停止条件。
实战技巧:我在Ghidra插件中实现数据解析时,会先检查SCL的上升沿。每次SCL从0变1时,就采样SDA的电平,作为当前bit的值。这样写出来的代码逻辑清晰,而且不容易出错。
3.6 知识体系总览
说了这么多,咱们用一张图来总结MCP数据链路层的核心逻辑:
这张图把数据链路层的五个核心要素串起来了。帧结构是骨架,起始/停止条件是边界,应答机制是可靠性保障,数据有效性是底层规则。五者缺一不可。
3.7 实战:Ghidra中解析MCP帧
最后,我分享一段在Ghidra插件中解析MCP帧的核心逻辑。这段代码我用了好几年,改了好几版,现在拿出来给大家参考:
// MCP帧解析核心逻辑(伪代码)
function parseMCPFrame(byte[] rawData) {
int i = 0;
List<Frame> frames = new ArrayList<>();
while (i < rawData.length - 1) {
// 1. 检测起始条件:SCL高电平时SDA下降沿
if (isStartCondition(rawData, i)) {
i += 2; // 跳过起始条件占用的采样点
// 2. 读取7位从机地址 + R/W位
int address = readBits(rawData, i, 7);
int rwBit = readBit(rawData, i + 7);
i += 8;
// 3. 读取应答位(第9个时钟)
int ack = readBit(rawData, i);
i += 1;
if (ack != 0) {
// NACK:从机无应答,放弃此帧
continue;
}
// 4. 循环读取数据字节
List<Byte> dataBytes = new ArrayList<>();
while (i < rawData.length - 1) {
// 检测停止条件
if (isStopCondition(rawData, i)) {
break;
}
// 读取8位数据
byte data = (byte) readBits(rawData, i, 8);
i += 8;
// 读取应答位
int dataAck = readBit(rawData, i);
i += 1;
dataBytes.add(data);
// 如果收到NACK,停止读取
if (dataAck != 0) {
break;
}
}
// 5. 组装成帧对象
frames.add(new Frame(address, rwBit, dataBytes));
} else {
i++;
}
}
return frames;
}
使用建议:这段代码的核心思路是「状态机」——从起始条件开始,依次解析地址、应答、数据、停止条件。我在实际插件中,还会加入超时检测和错误重试机制。毕竟真实硬件环境比理想情况复杂得多。
好了,MCP数据链路层的内容就讲到这里。帧结构、起始条件、停止条件、应答机制、数据有效性——这五个概念你搞清楚了,MCP协议的底层通信逻辑就通了。下一层咱们会聊更具体的传输细节,到时候见。
公众号:蓝海资料掘金营,微信deep3321