4、I2C通信协议:I2C总线原理、NXP NFC芯片I2C时序要求、I2C地址配置、Linux下I2C设备驱动框架
好,咱们进入第四讲。这一章聊的是I2C通信协议。说实话,在NFC芯片的嵌入式集成中,I2C是最常用的接口之一。为什么?因为它引脚少、布线简单,而且NXP的NFC芯片基本都支持。我最早接触NFC芯片时,第一个坑就踩在I2C时序上——芯片死活不响应,折腾了两天才发现是上拉电阻选错了。嗯,咱们今天就把这些细节掰开揉碎讲清楚。
4.1 I2C总线原理:两根线怎么干活?
I2C总线,说白了就是两根线:SCL(时钟线)和SDA(数据线)。所有设备都挂在这两根线上,通过地址来区分谁跟谁说话。你想想看,一个主设备(比如MCU)带着一堆从设备(比如NFC芯片、传感器、EEPROM),大家共用两根线,怎么不打架?
核心机制就三点:
- 开漏输出 + 上拉电阻:SCL和SDA都是开漏结构。平时靠上拉电阻拉到高电平。谁要发数据,就把线拉低。这设计的好处是——多个设备可以同时拉低总线,不会短路。
- 仲裁机制:如果两个主设备同时想控制总线,谁先拉低SDA谁就赢。输的那个自动退出。我在项目中遇到过两个MCU抢总线的情况,最后发现是地址冲突,改一下地址就好了。
- 起始条件和停止条件:SCL高电平时,SDA从高变低是起始信号;SCL高电平时,SDA从低变高是停止信号。这两个信号由主设备产生,其他设备只能乖乖听着。
数据传输时,每发8位数据,从设备要回一个ACK(应答位)。如果从设备忙不过来,可以拉低SCL让主设备等一等——这叫时钟拉伸。NXP的NFC芯片就支持这个功能,我建议你在驱动里一定要处理时钟拉伸,否则高速通信时容易丢数据。
4.2 NXP NFC芯片I2C时序要求
不同NXP NFC芯片的I2C时序参数略有差异,但核心要求是一致的。我以PN7150为例,给你列个表:
| 参数 | 符号 | 最小值 | 最大值 | 说明 |
|---|---|---|---|---|
| SCL时钟频率 | f_SCL | - | 400 kHz | 标准模式100kHz,快速模式400kHz |
| 起始条件保持时间 | t_HD;STA | 0.6 μs | - | SCL高电平后SDA拉低 |
| 数据保持时间 | t_HD;DAT | 0 ns | 0.9 μs | SCL低电平时SDA变化 |
| SCL低电平时间 | t_LOW | 1.3 μs | - | 快速模式下要求 |
| SCL高电平时间 | t_HIGH | 0.6 μs | - | 快速模式下要求 |
| 上拉电阻 | R_p | 1 kΩ | 10 kΩ | 取决于总线电容和速度 |
这里有个关键点:上拉电阻的选择直接影响通信可靠性。我曾经在一个项目里用了4.7kΩ的上拉电阻,总线电容大约200pF,400kHz通信时波形已经有点圆了。换成2.2kΩ后,信号干净多了。我的经验是——如果总线长度超过10cm,或者挂的设备超过3个,上拉电阻选小一点,比如2.2kΩ。
4.3 I2C地址配置
NXP NFC芯片的I2C地址通常是7位地址,加上读写位组成8位。以PN7150为例,它的7位地址是0x28。写操作时发送0x50(0x28左移一位,最低位为0),读操作时发送0x51(最低位为1)。
但有些芯片支持地址配置引脚,比如PN532就有两个地址引脚(AD0和AD1),可以配置成4种不同地址。你想想看,如果总线上挂多个NFC芯片,这个功能就很有用了。
地址配置的注意事项:
- 地址不能冲突:总线上每个设备的地址必须唯一。我见过有人把两个NFC芯片的地址引脚都接地,结果两个芯片地址一样,通信时数据全乱套了。
- 地址与读写位:发送地址时,最低位是读写标志。0表示写,1表示读。很多新手容易搞混——发送地址后没收到ACK,检查一下是不是读写位搞反了。
- 10位地址:NXP NFC芯片基本都用7位地址,但有些高端芯片支持10位地址。如果你用10位地址,发送方式不同——先发11110xx(xx是地址高两位),再发剩下的8位。我个人建议,能用7位就别用10位,省事。
4.4 Linux下I2C设备驱动框架
在Linux下写NFC芯片的I2C驱动,其实有现成的框架可以用。你不需要从零开始写,Linux内核已经帮你封装好了I2C核心层。你只需要实现几个回调函数就行。
驱动框架的核心结构体是struct i2c_driver:
#include <linux/i2c.h>
#include <linux/module.h>
static const struct i2c_device_id pn7150_id[] = {
{ "pn7150", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, pn7150_id);
static int pn7150_probe(struct i2c_client *client)
{
// 初始化硬件、注册中断、创建设备文件等
printk(KERN_INFO "PN7150 found at address 0x%02x\n", client->addr);
return 0;
}
static int pn7150_remove(struct i2c_client *client)
{
// 清理资源
return 0;
}
static struct i2c_driver pn7150_driver = {
.driver = {
.name = "pn7150",
.of_match_table = of_match_ptr(pn7150_of_match),
},
.probe = pn7150_probe,
.remove = pn7150_remove,
.id_table = pn7150_id,
};
module_i2c_driver(pn7150_driver);
这里有几个关键点:
- 设备树匹配:现在主流做法是用设备树(Device Tree)来描述硬件。在设备树里声明I2C从设备,驱动通过
of_match_table来匹配。我建议你养成写设备树的习惯,这样硬件改动时只需要改设备树,不用改驱动代码。 - probe函数:当内核发现匹配的设备时,调用probe函数。在这里做初始化——设置GPIO、注册中断、分配缓冲区等。我习惯在probe里先读一下芯片的ID寄存器,确认通信正常,再继续初始化。
- 数据传输:用
i2c_master_send()和i2c_master_recv()来收发数据。如果需要组合操作(先发地址再读数据),用i2c_transfer()。
i2cdetect工具扫描总线上的设备。如果扫描不到你的NFC芯片,先检查硬件连接——上拉电阻、电源、地址引脚。我曾经花了一下午调试驱动,最后发现是芯片没上电。嗯,先检查硬件,再怀疑软件。
驱动写好后,编译进内核或者作为模块加载。模块加载时,内核会自动调用probe函数。卸载时调用remove函数。整个过程很清晰,对吧?
最后说一句:Linux下的I2C驱动框架已经很成熟了,你不需要自己实现时序控制。内核的I2C核心层会处理时钟拉伸、ACK检查、重试等细节。你只需要关注芯片特有的操作——比如NFC芯片的寄存器读写、固件下载、中断处理等。这样开发效率高很多。
好,这一章就到这里。下一章咱们讲NFC芯片的寄存器配置和初始化流程,到时候会用到今天讲的I2C知识。有什么问题,欢迎随时交流。