Microchip 就此撰写了应用笔记:
应用笔记与 ASM 一起使用,但可以轻松移植到 C。
Microchip 的免费 C18 和 XC8 编译器具有 I2C 功能。您可以在编译器库文档的第 2.4 节中阅读有关它们的更多信息。以下是一些快速入门信息:
配置
您已经拥有 Microchip 的 C18 或 XC8 编译器。它们都具有内置的 I2C 功能。要使用它们,您需要包括i2c.h
:
#include i2c.h
如果你想看看源代码,你可以在这里找到:
- C18 标头:
installation_path
/v
x.xx
/h/i2c.h
- C18 来源:
installation_path
/v
x.xx
/src/pmc_common/i2c/
- XC8 标头:
installation_path
/v
x.xx
/include/plib/i2c.h
- XC8 源码:
installation_path
/v
x.xx
/sources/pic18/plib/i2c/
在文档中,您可以找到/i2c/
函数所在文件夹中的哪个文件。
打开连接
如果您熟悉 Microchip 的 MSSP 模块,您就会知道它们首先必须被初始化。OpenI2C
您可以使用该函数在 MSSP 端口上打开 I2C 连接。这是它的定义方式:
void OpenI2C (unsigned char sync_mode, unsigned char slew);
使用sync_mode
,您可以选择设备是主设备还是从设备,如果是从设备,它应该使用 10 位还是 7 位地址。大多数时候,使用 7 位,尤其是在小型应用程序中。的选项sync_mode
是:
SLAVE_7
- 从模式,7 位地址
SLAVE_10
- 从模式,10 位地址
MASTER
- 大师模式
使用slew
,您可以选择设备是否应使用摆率。有关它的更多信息:I2C 的压摆率是多少?
两个 MSSP 模块
具有两个 MSSP 模块的设备有一些特别之处,例如PIC18F46K22。它们有两组函数,一组用于模块 1,一组用于模块 2。例如OpenI2C()
,它们具有OpenI2C1()
和,而不是openI2C2()
。
好的,所以您已经完成了所有设置并打开了连接。现在让我们做一些例子:
例子
大师写例子
如果您熟悉 I2C 协议,您就会知道典型的主写序列如下所示:
Master : START | ADDR+W | | DATA | | DATA | | ... | DATA | | STOP
Slave : | | ACK | | ACK | | ACK | ... | | ACK |
首先,我们发送一个 START 条件。考虑这个拿起电话。然后,带有写入位的地址 - 拨打号码。此时,具有发送地址的从机知道他正在被调用。他发送一个确认(“你好”)。现在,主设备可以发送数据了——他开始说话了。他发送任意数量的字节。在每个字节之后,从机应确认接收到的数据(“是的,我听到了”)。当主设备完成通话时,他以 STOP 条件挂断。
在 C 中,主控写入序列对于主控来说如下所示:
IdleI2C(); // Wait until the bus is idle
StartI2C(); // Send START condition
IdleI2C(); // Wait for the end of the START condition
WriteI2C( slave_address & 0xfe ); // Send address with R/W cleared for write
IdleI2C(); // Wait for ACK
WriteI2C( data[0] ); // Write first byte of data
IdleI2C(); // Wait for ACK
// ...
WriteI2C( data[n] ); // Write nth byte of data
IdleI2C(); // Wait for ACK
StopI2C(); // Hang up, send STOP condition
主读示例
主读序列与写序列略有不同:
Master : START | ADDR+R | | | ACK | | ACK | ... | | NACK | STOP
Slave : | | ACK | DATA | | DATA | | ... | DATA | |
主机再次发起呼叫并拨打号码。不过,他现在想得到信息。从机首先接听电话,然后开始通话(发送数据)。主机确认每个字节,直到他有足够的信息。然后他发送一个 Not-ACK 并以 STOP 条件挂断。
在 C 语言中,主要部分如下所示:
IdleI2C(); // Wait until the bus is idle
StartI2C(); // Send START condition
IdleI2C(); // Wait for the end of the START condition
WriteI2C( slave_address | 0x01 ); // Send address with R/W set for read
IdleI2C(); // Wait for ACK
data[0] = ReadI2C(); // Read first byte of data
AckI2C(); // Send ACK
// ...
data[n] = ReadI2C(); // Read nth byte of data
NotAckI2C(); // Send NACK
StopI2C(); // Hang up, send STOP condition
从机代码
对于从站,最好使用中断服务程序或 ISR。您可以将微控制器设置为在调用您的地址时接收中断。这样您就不必经常检查公共汽车。
首先,让我们设置中断的基础知识。您必须启用中断并添加 ISR。重要的是 PIC18 有两个级别的中断:高和低。我们将 I2C 设置为高优先级中断,因为回复 I2C 调用非常重要。我们要做的是:
- 写入 SSP ISR,当中断是 SSP 中断(而不是另一个中断)时
- 写一个通用的高优先级 ISR,当中断是高优先级时。该函数必须检查触发了哪种中断,并调用正确的子 ISR(例如,SSP ISR)
GOTO
在高优先级中断向量上向通用 ISR添加一条指令。我们不能将通用 ISR 直接放在向量上,因为在很多情况下它太大了。
这是一个代码示例:
// Function prototypes for the high priority ISRs
void highPriorityISR(void);
// Function prototype for the SSP ISR
void SSPISR(void);
// This is the code for at the high priority vector
#pragma code high_vector=0x08
void interrupt_at_high_vector(void) { _asm GOTO highPriorityISR _endasm }
#pragma code
// The actual high priority ISR
#pragma interrupt highPriorityISR
void highPriorityISR() {
if (PIR1bits.SSPIF) { // Check for SSP interrupt
SSPISR(); // It is an SSP interrupt, call the SSP ISR
PIR1bits.SSPIF = 0; // Clear the interrupt flag
}
return;
}
// This is the actual SSP ISR
void SSPISR(void) {
// We'll add code later on
}
接下来要做的是在芯片初始化时启用高优先级中断。这可以通过一些简单的寄存器操作来完成:
RCONbits.IPEN = 1; // Enable interrupt priorities
INTCON &= 0x3f; // Globally enable interrupts
PIE1bits.SSPIE = 1; // Enable SSP interrupt
IPR1bits.SSPIP = 1; // Set SSP interrupt priority to high
现在,我们有中断工作。如果你正在实施这个,我现在就检查一下。当发生 SSP 中断时,编写基本SSPISR()
代码以开始闪烁 LED。
好的,所以你的中断工作了。现在让我们为该SSPISR()
函数编写一些真实的代码。但首先是一些理论。我们区分了五种不同的 I2C 中断类型:
- 主机写入,最后一个字节是地址
- 主机写入,最后一个字节是数据
- 主机读取,最后一个字节是地址
- 主机读取,最后一个字节是数据
- NACK:传输结束
您可以通过检查SSPSTAT
寄存器中的位来检查您处于什么状态。该寄存器在 I2C 模式下如下(未使用或不相关位省略):
- 位 5:D/NOT A:数据/非地址:如果最后一个字节是数据则置位,如果最后一个字节是地址则清零
- 位 4: P:停止位:如果最后出现 STOP 条件则设置(没有活动操作)
- 位 3: S:开始位:如果最后出现 START 条件(存在活动操作)则设置
- 位 2:R/NOT W:读/不写:如果操作是主读则设置,如果操作是主写则清零
- Bit 0: BF: Buffer Full: 如果 SSPBUFF 寄存器中有数据则置位,否则清零
有了这些数据,很容易看出如何查看 I2C 模块处于什么状态:
State | Operation | Last byte | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 0
------+-----------+-----------+-------+-------+-------+-------+-------
1 | M write | address | 0 | 0 | 1 | 0 | 1
2 | M write | data | 1 | 0 | 1 | 0 | 1
3 | M read | address | 0 | 0 | 1 | 1 | 0
4 | M read | data | 1 | 0 | 1 | 1 | 0
5 | none | - | ? | ? | ? | ? | ?
在软件中,最好使用状态 5 作为默认值,这是在其他状态的要求不满足时假定的。这样,当您不知道发生了什么时,您就不会回复,因为从机不会响应 NACK。
无论如何,让我们看一下代码:
void SSPISR(void) {
unsigned char temp, data;
temp = SSPSTAT & 0x2d;
if ((temp ^ 0x09) == 0x00) { // 1: write operation, last byte was address
data = ReadI2C();
// Do something with data, or just return
} else if ((temp ^ 0x29) == 0x00) { // 2: write operation, last byte was data
data = ReadI2C();
// Do something with data, or just return
} else if ((temp ^ 0x0c) == 0x00) { // 3: read operation, last byte was address
// Do something, then write something to I2C
WriteI2C(0x00);
} else if ((temp ^ 0x2c) == 0x00) { // 4: read operation, last byte was data
// Do something, then write something to I2C
WriteI2C(0x00);
} else { // 5: slave logic reset by NACK from master
// Don't do anything, clear a buffer, reset, whatever
}
}
您可以看到如何使用位掩码检查SSPSTAT
寄存器(首先与,0x2d
以便我们只有有用的位),以查看我们有什么中断类型。
找出响应中断时必须发送或执行的操作是您的工作:这取决于您的应用程序。
参考
再次,我想提一下 Microchip 写的关于 I2C 的应用笔记:
有编译器库的文档:编译器库文档
自行设置时,请查看 (M)SSP 部分的芯片数据表以了解 I2C 通信。我使用PIC18F46K22作为主控部分,使用PIC18F4620作为从属部分。