从 PIC18 上的 I2C 开始

电器工程 微控制器 图片 沟通 C i2c
2022-01-27 21:56:20

对于一个项目,我想要三个 PIC(两个从设备 PIC18F4620,一个主设备 PIC18F46K22)通过 I2C 总线进行通信。稍后,可能会添加更多从设备(如 EEPROM、SRAM 等)。我正在使用 C18 编译器在 C 中为这些 PIC 编写代码。我在网上看了很多遍,但找不到处理 (M)SSP 外围设备的库。我已经阅读了 I2C 模式下 (M)SSP 外设上两个 PIC 的数据表,但无法找到如何连接总线。

所以我需要主库从库。

你有什么建议吗?你有这样的图书馆吗?它是否内置在编译器中,如果是,在哪里?网络上有什么好的教程吗?

3个回答

Microchip 就此撰写了应用笔记:

  • AN734关于实现 I2C 从机
  • AN735关于实现 I2C 主机
  • 还有一个更具理论意义的AN736用于设置环境监测网络协议,但本项目不需要它。

应用笔记与 ASM 一起使用,但可以轻松移植到 C。

Microchip 的免费 C18 和 XC8 编译器具有 I2C 功能。您可以在编译器库文档的第 2.4 节中阅读有关它们的更多信息。以下是一些快速入门信息:

配置

您已经拥有 Microchip 的 C18 或 XC8 编译器。它们都具有内置的 I2C 功能。要使用它们,您需要包括i2c.h

#include i2c.h

如果你想看看源代码,你可以在这里找到:

  • C18 标头:installation_path/vx.xx/h/i2c.h
  • C18 来源:installation_path/vx.xx/src/pmc_common/i2c/
  • XC8 标头:installation_path/vx.xx/include/plib/i2c.h
  • XC8 源码:installation_path/vx.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 中断类型:

  1. 主机写入,最后一个字节是地址
  2. 主机写入,最后一个字节是数据
  3. 主机读取,最后一个字节是地址
  4. 主机读取,最后一个字节是数据
  5. 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 的应用笔记:

  • AN734关于实现 I2C 从机
  • AN735关于实现 I2C 主机
  • AN736关于为环境监测建立网络协议

有编译器库的文档:编译器库文档

自行设置时,请查看 (M)SSP 部分的芯片数据表以了解 I2C 通信。我使用PIC18F46K22作为主控部分,使用PIC18F4620作为从属部分。

首先,我建议更改为 XC8 编译器,因为它是最新的。有可用的外围库,但我从来没有使用过太多。有关详细信息和文档,请访问 Microchips 网站。

好的,我在这里有一些非常古老的 I2C eeprom 通信的基本例程,我很久以前使用 PIC16F 和旧的 Microhip 中档编译器(它可能是高科技编译器),但我认为它们可能工作得很好用PIC18,因为我认为外设是一样的。无论如何,如果一切都不同,您会很快发现。
它们是与温度记录器项目一起使用的较大文件的一部分,因此我迅速删除了所有其他不相关的功能并保存为以下文件,所以我可能把它弄得一团糟,但希望你将能够了解需要什么(它甚至可能只是工作,你永远不知道;-))

您需要确保主头文件正确,并检查/更改引脚以确保它们是正确的 I2C 外围引脚,以及寄存器名称(如果它们来自 Hi-Tech 编译器,它的处理方式略有不同)关于寄存器命名约定。

I2C.c 文件:

#include "I2C.h"
#include "delay.h"
#include <pic.h>

#define _XTAL_FREQ 20000000


void write_ext_eeprom(unsigned int address, unsigned char data)
 {
    unsigned char a0 = ((address & 0x8000) >> 14);  
    unsigned char msb = (address >> 8);
    unsigned char lsb = (address & 0x00FF);


   i2c_start();
   i2c_write(0xa0 | a0);
   i2c_write(msb);
   i2c_write(lsb);
   i2c_write(data);
   i2c_stop();
   DelayMs(11);
}

/******************************************************************************************/

unsigned char read_ext_eeprom(unsigned int address)
{
   unsigned char a0 = ((address & 0x8000) >> 14);  
   unsigned char data;
   unsigned char msb = (address >> 8);
   unsigned char lsb = (address & 0x00FF);

   i2c_start();
   i2c_write(0xa0 | a0);
   i2c_write(msb);
   i2c_write(lsb);
   i2c_repStart();
   i2c_write(0xa1 | a0);
   data=i2c_read(0);
   i2c_stop();
   return(data);
}

void i2c_init()
{
 TRISC3=1;           // set SCL and SDA pins as inputs
 TRISC4=1;

 SSPCON = 0x38;      // set I2C master mode
 SSPCON2 = 0x00;

 //SSPADD = 9;          // 500kHz bus with 20MHz xtal 
 SSPADD = 49;           // 100kHz bus with 20Mhz xtal

 CKE=0;     // use I2C levels      worked also with '0'
 SMP=1;     // disable slew rate control  worked also with '0'

 PSPIF=0;      // clear SSPIF interrupt flag
 BCLIF=0;      // clear bus collision flag
}

/******************************************************************************************/

void i2c_waitForIdle()
{
 while (( SSPCON2 & 0x1F ) | RW ) {}; // wait for idle and not writing
}

/******************************************************************************************/

void i2c_start()
{
 i2c_waitForIdle();
 SEN=1;
}

/******************************************************************************************/

void i2c_repStart()
{
 i2c_waitForIdle();
 RSEN=1;
}

/******************************************************************************************/

void i2c_stop()
{
 i2c_waitForIdle();
 PEN=1;
}

/******************************************************************************************/

int i2c_read( unsigned char ack )
{
 unsigned char i2cReadData;

 i2c_waitForIdle();

 RCEN=1;

 i2c_waitForIdle();

 i2cReadData = SSPBUF;

 i2c_waitForIdle();

 if ( ack )
  {
  ACKDT=0;
  }
 else
  {
  ACKDT=1;
  }
  ACKEN=1;               // send acknowledge sequence

 return( i2cReadData );
}

/******************************************************************************************/

unsigned char i2c_write( unsigned char i2cWriteData )
{
 i2c_waitForIdle();
 SSPBUF = i2cWriteData;
//if(ACKSTAT)
{
//while(ACKSTAT);
}
 return ( ! ACKSTAT  ); // function returns '1' if transmission is acknowledged
}

I2C.h 头文件:

extern void i2c_init();
extern void i2c_waitForIdle();
extern void i2c_start();
extern void i2c_repStart();
extern void i2c_stop();
extern int i2c_read( unsigned char ack );
extern unsigned char i2c_write( unsigned char i2cWriteData );

XC8 和 XC16 编译器包括 I2C 库。

我遇到的问题是文档不是很好!如果您使用 Microchip 文档中的示例,那么您就不走运了。即使是 Microchip 支持也无法帮助您。我自己去过那里。

前段时间我使用 PIC24EP512GP 系列微控制器,但正如 Microchip 所记录的那样,该库对我不起作用。