本帖最后由 y369369 于 2024-9-30 15:21 编辑
LPC800系列微控制器,自其问世以来便以其高性能和灵活性赢得了众多开发者的青睐。历经数代技术革新,LPC800携全新特性强势回归,旨在重新定义嵌入式应用的可能性。 今天,首先为大家带来的是关于集成电路芯片间通信总线的详细介绍,这一核心技术对于理解LPC800的工作原理及其优势至关重要。敬请期待接下来的的系列文章,与我们一起探索LPC800的过去、现在与未来。 I2C又写做IIC或I2C,英文发音是:I-squared-C,中文通常读为“Iái-方fāngC-xī”。 I2C是英文Inter-Integrated Circuit的缩写,是飞利浦半导体(现恩智浦半导体)在上世纪80年代初,开发的一个简单的双线总线,用于电路板上控制器芯片与设备芯片之低速数据通信,也常用于一个系统中跨电路板的芯片间通信。下图是I2C总线的图标。
I2C总线已经成为一个事实的工业标准,目前有成千上万的芯片实现了I2C接口。另外,还有很多控制系统也是基于I2C开发的,例如常用于可充电电池管理的SMBus(System Management Bus)、用于电源管理的PMBus(Power Management Bus)、用于电脑显卡与显示器之间通信的DDC(Display Data Channel)等。 I2C是一种总线协议,具有以下特点: ▲只使用二根信号线,一个信号线是SDA(Serial Data Line 串行数据线),另一个信号线是SCL(Serial Clock Line串行时钟线)。 ▲设备之间的通信始终是主从的关系,只有主设备才能发起数据传输。每个连接到I2C总线的设备都有一个唯一的地址,主设备使用从设备地址确定通信对象。 ▲总线上允许多个主设备的存在,当两个或多个主设备同时要发起数据传输时,通过冲突检测和仲裁能够防止数据被破坏或丢失。 ▲数据传输是串行的、始终是以每8个比特位为单位,传输速率有多种模式: 标准模式:100 kbit/s 快速模式:400 kbit/s 快速模式+:1 Mbit/s 高速模式:3.4 Mbit/s 超高速模式:5 Mbit/s,此种模式的数据传输是单向的,通常用于微处理器与LED控制器的通信,或与那些不需要回传数据的设备的通信。 !!!注意!!!传输速率是指时钟速率,而不是有效的数据传输率。 由于协议的额外开销,实际数据传输率会比时钟速率低。 一般为保险起见,应以时钟速率的1/3或更低来计算实际数据传输率。 与SPI相比,I2C总线具有较少的信号线,可以在不增加信号线的情况下,挂接很多设备,挂接设备的数目只受设备地址数目和最大总线电容的限制。 一个典型的I2C总线配置如下图,这里有两个MCU作为主设备,还有四个从设备。
图1.I2C总线配置示意图 一、I2C模块特性 LPC800系列的每一个型号都内置了至少一个I2C硬件模块。软件对所有型号中每个I2C模块的操作都是一样的,用户可以很方便地把对应的I2C驱动代码,应用在任一LPC800产品上、任一的I2C模块上,做到一次编写多次使用。 下面按照用户手册的顺序列出了I2C模块的主要功能和一个简要的说明: 图2. I2C模块主要功能一览图
二、I2C模块的内部构成 2.1寄存器组一览 每个I2C模块的操作寄存器组都是相同的,除了寄存器组的起始地址不同,每个寄存器相对起始地址的偏移量都是相同的,这非常方便用户使用同一组I2C的操作代码,操作不同的I2C模块,同时也方便用户在LPC800的不同型号之间移植代码。 下表是寄存器组一览,共18个寄存器,每个寄存器占据4个字节的地址空间。这些寄存器按照功能可以划分为四组,表中以双分割线隔离: ■涉及所有功能的功能配置寄存器组,包含CFG、STAT、INTENSET、INTENCLR、TIMEOUT、CLKDIV和INTSTAT,共7个寄存器。 ■控制主机功能的寄存器,包含MSTCTL、MSTTIME和MSTDAT,共3个寄存器。 ■控制从机功能的寄存器,包含SLVCTL、SLVDAT、SLVADR0~ SLVADR3和SLVQUAL0,共7个寄存器。 ■监测功能的寄存器,只有一个MONRXDAT。 最后一列“有效位”用阴影标出了对应的有效控制/数据位,白底的位是保留位。“有效位”这一列可以让读者有一个直观的印象和参考,在编写程序时,有哪些寄存器和多少控制位需要考虑,做到心中有数。 图3. I2C模块寄存器一览图 注(1):LPC81x没有DMA模块,不包含DMA控制位 2.2 模块框图 接下来,让我们看下I2C模块的功能框图:
图4. I2C模块的功能框图 从这个框图也可以明显地看出,主机、从机和监测功能分别是三个独立的模块,它们可以互不干扰地工作。对于这三个独立模块,我们可以理解在每个独立模块中都有一个移位寄存器,分别单独负责主机功能、从机功能和监测功能的数据发送或接收。特别是对于只有一套移位寄存器的产品,是不能实现监测功能的。 利用这一点,可以实现单一I2C模块的自发自收,或自我监测以确保主从功能正常工作;尤其是自我监测功能可以有效地用于软件的调试,下面会用一个例程说明如何使用监测功能进行调试。
三、I2C主机收发操作步骤-轮询方式 3.1 初始化:I2C模块主机收发 (轮询方式) 对I2C模块的初始化,分为几个步骤: ▲打开I2C模块的时钟,并同时打开开关矩阵的时钟; ▲为I2C的外部信号线分配引脚; LPC82x的I2C0引脚是固定的,不能移动;其它I2C模块的引脚可以移动; LPC81x只有I2C0,其引脚可以移动。如果使用默认引脚时,即P0_10和P0_11,则速率可达1Mbit/s;如果使用其它引脚,则速率可达400 kbit/s; ▲配置I2C的时钟分频器。这一步产生的时钟频率仅用于I2C模块内部,还不是最终SCL信号线的频率。在上面框图中,该信号以I2C_PCLK表示; ▲配置SCL信号线高低电平的长度,这个长度的基准就是前一步时钟分配器输出的信号(I2C_PCLK); 下面是对应的初始化函数代码,要求得到的SCL速率为100kHz: 代码片段1. I2C0主机初始化函数 - 01 void I2C0_Master_Init(void)
- 02 {
- 03 // Enable clocks to I2C0, SWM
- 04 LPC_SYSCON->SYSAHBCLKCTRL |= (I2C0 | SWM);
- 05
- 06 #if TARGET_BOARD == 824
- 07 // For LPC824, I2C0_SDA and I2C0_SCL are fixed pin.
- 08 LPC_SWM->PINENABLE0 &= ~(I2C0_SCL|I2C0_SDA);
- 09 #endif
- 10
- 11 #if TARGET_BOARD == 812
- 12 // On the LPC812, I2C0_SDA and I2C0_SCL are movable.
- 13 ConfigSWM(I2C0_SCL, P0_10);
- 14 ConfigSWM(I2C0_SDA, P0_11);
- 15 #endif
- 16
- 17 // Give I2C0 a reset
- 18 LPC_SYSCON->PRESETCTRL &= (I2C0_RST_N);
- 19 LPC_SYSCON->PRESETCTRL |= ~(I2C0_RST_N);
- 20
- 21 // Set clock divider to get 100kHz bit rate.
- 22 LPC_I2C0->DIV = (75 - 1);
- 23
- 24 // WARNING: default value is 0x77
- 25 LPC_I2C0->MSTTIME = 0x0;
- 26
- 27 // Configure the I2C0 CFG register
- 28 LPC_I2C0->CFG = CFG_MSTENA;
- 29 }
复制代码
3.2 主机发送(轮询方式) 下图是I2C主机发送的状态图,以及对应的软硬件操作: 图5. I2C主机发送状态图
上图以I2C主机发送的状态变化图为主线,在各处相关的节点,标注了应该执行的操作。 下面直接以一个写入EEPROM的程序代码,详细说明这些操作。下图是写入EEPROM的操作流程图: 图6.写入EEPROM操作流程图
首先先看图6的第一个操作,发出START位的函数,该函数执行图5中的B操作: 代码片段2. I2C0作为主机发送器时输出START位的函数 - 00 // 等待主机状态为空闲,然后发送START和从机地址,读/写位为”写”;
- // 反复发送START和从机地址,直到从机ACK为止
- 01 void I2C_Start_Tx(LPC_I2C_TypeDef* pI2C, unsigned char slave_addr)
- 02 { uint32_t mststate;
- 03
- 04 I2C_Wait_Master(pI2C, I2C_STAT_MSTST_IDLE);
- 05 do {
- 06 pI2C->MSTDAT = (slave_addr<<1) | 0;
- 07 pI2C->MSTCTL = CTL_MSTSTART;
- 08 while(!(pI2C->STAT & STAT_MSTPEND));
- 09 mststate = pI2C->STAT & MASTER_STATE_MASK;
- 10 } while (mststate != I2C_STAT_MSTST_TX);
- 11 }
复制代码
函数I2C_Start_Tx()返回时,意味着硬件状态已经到达位置D,软件可以开始发送数据字节了。 函数I2C_Start_Tx(),通过调用以下的函数,等待主机状态。后面的代码中还会经常使用这个函数。 代码片段3. I2C等待主机状态函数 - 00 // 等待主机状态为以下条件:STAT_MSTIDLE(空闲)、STAT_MSTRX(接收就绪)、STAT_MSTTX(发送就绪)、
- // STAT_MSTNACKADDR(从机NACK地址)或STAT_MSTNACKTX(从机NACK数据)
- 01 void I2C_Wait_Master(LPC_I2C_TypeDef* pI2C, uint32_t state)
- 02 { uint32_t mstate;
- 03 while(!(pI2C->STAT & STAT_MSTPEND)); // 等待主机就绪
- 04 mstate = pI2C->STAT & MASTER_STATE_MASK;
- 05 if (mstate != state) { // 如果主机状态不是期待的状态,则点亮红灯
- 06 LED_Rotate();
- 07 while(1); // 这里是个死循环,操作者需要检查哪里出了问题
- 08 }
- 09 }
复制代码函数I2C_Wait_Master ()只是简单地等待主机功能就绪,然后查看主机状态是否为期待的状态,如果有所差池,则循环点亮红灯进入一个死循环,操作者在看到红灯后就知道发生错误,可以停下调试器查找原因。 写入EEPROM的程序代码,需要用到这些变量定义: - #define RXBUF_SIZE 128 // 与EEPROM进行数据交换的缓冲区大小
- 定义数据缓冲区
- unsigned char txbuf[RXBUF_SIZE]; // 与EEPROM进行数据交换的缓冲区
- uint32_t txbuf_ptr; // 操作txbuf的索引变量
- uint32_t txbuf_len; // 控制txbuf中有效数据的数量
复制代码上述变量定义段不一定和下面的代码在同一个地方。 接下来这个程序片段,完整地实现了写入EEPROM一组数据的功能: 代码片段4. I2C0写入24C02 EEPROM函数 - 00 // 初始化I2C0并从txbuf[]缓冲区中,发送txbuf_len个字节至EEPROM中
- 01 I2C0_Master_Init();
- 02
- 03 txbuf_ptr = 0;
- 04 while (txbuf_ptr < txbuf_len) {
- 05 I2C_Start_Tx(LPC_I2C0, I2C_EEPROM_ADDR);
- 06 I2C_Send_Byte(LPC_I2C0, txbuf_ptr);
- 07
- 08 unsigned char eepage_ptr;
- 09 for (eepage_ptr = 0; eepage_ptr < EEPAGE_SIZE; eepage_ptr++) {
- 10 I2C_Send_Byte(LPC_I2C0, txbuf[txbuf_ptr]);
- 11 txbuf_ptr++;
- 12 if (txbuf_ptr >= txbuf_len)
- 13 break;
- 14 }
- 15 I2C_Stop(LPC_I2C0);
- 16 }
复制代码
程序执行步骤说明如下: 1. 初始化I2C0,并使能主机功能——语句01; 2. 检测缓冲区指针是否到达结尾——语句04; 3. 发送START、从机地址和读写位——语句05; 4. 发送EEPROM的起始地址——语句06; 5. 循环写入EEPROM一页的数据(最多8个字节)——语句09~14; 6. 在循环中判断缓冲区指针是否到达结尾——语句12; 7. 发送STOP——语句15; 8. 返回步骤2,继续下一页的操作。
下面是函数I2C_Send_Byte()的代码: 代码片段5. I2C输出一个字节的函数 - 00 // 发出一个字节,并等待发送结束
- 01 void I2C_Send_Byte(LPC_I2C_TypeDef* pI2C, unsigned char databyte)
- 02 {
- 03 pI2C->MSTDAT = databyte;
- 04 pI2C->MSTCTL = CTL_MSTCONTINUE;
- 05 I2C_Wait_Master(pI2C, I2C_STAT_MSTST_TX);
- 06 }
复制代码发送一个字节的操作很直接:把输出字节写入MSTDAT寄存器,并设置MSTCTL.MSTCONTINUE=”1”,随后硬件就会输出对应的SCL和SDA信号,然后等待EEPROM的ACK或NACK。见图5的E操作。 最后是函数I2C_Stop()的代码: 代码片段6. I2C输出STOP的函数 - 00 // 发出STOP,并等待发送结束
- 01 void I2C_Stop(LPC_I2C_TypeDef* pI2C)
- 02 {
- 03 pI2C->MSTCTL = CTL_MSTSTOP;
- 04 while(!(pI2C->STAT & STAT_MSTPEND));
- 05 }
复制代码
下面两张图片是示波器的截图,清楚地显示了完整的STOP位,和因为上述错误的复位导致不成功的STOP。图中上行信号是SDA,下行显示的信号是SCL。
图7.正确的STOP输出(上图)和被终止的STOP输出(下图) 下图中也可以看到,最后一个SCL低也被截断变得不完整:上图显示最后一个SCL低持续大约27μs,但下图中最后一个SCL低只持续了大约9μs,同时SDA信号也变得紊乱。 综合一下上述操作过程,可以看出了LPC800的I2C模块功能十分强大,在初始化好I2C模块后,软件只需做到以下三个步骤即可完成整个I2C的发送流程: 写一个数据寄存器:MSTDAT寄存器 检测两个状态域:STAT状态寄存器的MSTSTATE 域(空闲和发送就绪状态),和MSTPENDING位 三个控制位:主机控制寄存器的MSTSTART、MSTCONTINUE和MSTSTOP位 还要始终记住一件事,每次设置任一个主机控制寄存器的控制位后,进入下一个操作前,都要确认主机已经就绪(状态寄存器的MSTPENDING位),即前一步操作结束,否则很可能出现意想不到的问题。
3.3 主机接收(轮询方式) 下图是I2C主机接收的状态图,以及对应的软硬件操作: 图8. I2C主机接收状态图 上图以I2C主机接收的状态变化图为主线,在各处相关的节点,标注了应该执行的操作。 下面以前述写入EEPROM的程序为基础,说明如何把写入的数据读出来。下图是读出EEPROM数据的操作流程:
图9. 从EEPROM读出N字节数据的流程图
图9的上半部分是一个写操作(读写位为”0”),用于输出EEPROM读出区域的首地址,这个过程的程序与代码片段4的05~06行相同。图9的下半部分是一个读操作(读写位为”1”),用于从EEPROM中读出数据。 下面来看发出读操作START位的函数,该函数执行图8中的B操作: 代码片段7. I2C0作为主机接收器时输出START位的函数 - 00 // 等待主机状态为Pending,然后发送START和从机地址,读/写位为”读”;
- // 反复发送START和从机地址,直到从机ACK为止
- 01 void I2C_Start_Rx(LPC_I2C_TypeDef* pI2C, unsigned char slave_addr)
- 02 {
- 03 uint32_t mststate;
- 04 while(!(pI2C->STAT & STAT_MSTPEND)); // Wait for master pending
- 05 do {
- 06 pI2C->MSTDAT = (slave_addr<<1) | 1;
- 07 pI2C->MSTCTL = CTL_MSTSTART;
- 08
- 09 while(!(pI2C->STAT & STAT_MSTPEND));
- 10
- 11 mststate = pI2C->STAT & MASTER_STATE_MASK;
- 12 } while (mststate != I2C_STAT_MSTST_RX);
- 13 }
复制代码
函数I2C_Start_Rx()返回时,意味着硬件状态已经到达位置E,即硬件已经发出了START、7位从机地址、读写位,并且得到从机ACK响应,第一个8位数据也已经移入了数据寄存器。
这个函数与前面I2C主机发送的START函数在流程上差不多,但也有几处明显的不同: 函数I2C_Start_Rx()首先要等待I2C模块进入Pending状态(04行),而不是等待Idle状态。因为我们现在需要发送图69的第二个START, I2C总线不是处于Idle状态。 读写位=”1”(06行),而不是”0”。 循环 (05~12行) 的条件是“接收就绪”,而不是“发送就绪”。
图8的F操作需要特别留意一下,当软件读出接收到的数据后,在置位MSTCTL.MSTCONTINUE=“1”之前主机不会发出ACK,如果处理时间过长,则有可能造成从机超时,尽管一般的I2C设备没有超时控制,我们还是应该尽量把对数据的处理放到所有I2C操作结束以后进行,尽快置位MSTCTL.MSTCONTINUE=“1”,向从机发出ACK。 缩短所有I2C的操作,减少从START至STOP之间的时间间隔,可以尽早关闭I2C模块和让外部I2C设备进入睡眠状态,有利于减小总体功耗;在多主系统中,也可以避免某个主机过长霸占总线的情况。
下面是实现图8和图9状态流程图的代码: 代码片段8. I2C0读出24C02 EEPROM 00 // 从EEPROM读出数据至rxbuf[RXBUF_SIZE]01 rxbuf_ptr = 0; // 复位读缓存指针02 rxbuf_len = txbuf_len; // 读出数据段长度03 04 I2C_Start_Tx(LPC_I2C0, I2C_EEPROM_ADDR);05 I2C_Send_Byte(LPC_I2C0, 0)06 07 I2C_Start_Rx(LPC_I2C0, I2C_EEPROM_ADDR); 08 rxbuf[rxbuf_ptr++] = LPC_I2C0->MSTDAT;09 10 while (rxbuf_ptr < rxbuf_len) {11 LPC_I2C0->MSTCTL = CTL_MSTCONTINUE; 12 I2C_Wait_Master(LPC_I2C0, I2C_STAT_MSTST_RX); 13 rxbuf[rxbuf_ptr++] = LPC_I2C0->MSTDAT; 14 }15 I2C_Stop(LPC_I2C0); 3.4 I2C主机功能的三个控制位 这里需要特别说明的是MSTCTL中三个控制位,在I2C主机接收时的作用与作为主机发送时的作用有所不同: ▲MSTSTART位:基本功能是发送START、7位从机地址和读写位,接受从机回送的ACK或NACK状态: 对于读写位为“写”时,当从机回送ACK或NACK后,硬件设置MSTPENDING位,软件开始进行状态判断和下一步操作; 对于读写位为“读”时,当从机回送ACK后,主机继续发送SCL时钟并按位移入从机送出的数据,完成全部8位数据后,硬件设置MSTPENDING位,软件开始进行状态判断和下一步操作。注意,此时主机不会发送ACK,也不会发送NACK; ▲MSTCONTINUE位:基本功能是继续之前已经开始的“读”或“写”操作: 对于写操作,主机继续发送SCL和一个字节的数据,当从机回送ACK或NACK后,硬件设置MSTPENDING位,软件开始进行状态判断和下一步操作; 对于读操作,主机首先回送一个ACK给从机,然后主机继续发送SCL时钟并按位移入从机送出的数据,随后硬件设置MSTPENDING位,软件开始进行状态判断和下一步操作; ▲MSTSTOP位:基本功能是发出STOP位: 对于写操作,主机发出STOP位,然后硬件设置MSTPENDING位,主机进入IDLE状态; 对于读操作,主机首先回送NACK给从机,然后再发出STOP位,随后是硬件设置MSTPENDING位并进入IDLE状态; 综合上述说明,下面的状态图中标示了各控制位的作用域和各状态位变化的情况: 图10.主机发送流程控制位、状态位和外部信号的时序关系图 图11.主机接收流程控制位、状态位和外部信号的时序关系图 3.5 轮询方式读写EEPROM的完整代码 前面分段介绍了主机功能初始化和读/写EEPROM的代码段,下面把所有主程序代码集中在一起,方便读者查看。相关调用函数的代码,请参考前面部分。 代码片段9. I2C0作为主机读写EEPROM - 00 const unsigned char the_prompt[] = "Type any key to continue.\n\r";
- 01 // 定义EEPROM的一些常量
- 02 #define I2C_EEPROM_ADDR 0x50 // EEPROM 24C02 的7位I2C地址
- 03 #define EEPROM_SIZE 255 // EEPROM存储区的大小(字节)
- 04 #define EEPAGE_SIZE 8 // EEPROM存储区每页的大小(字节)
- 05
- 06 unsigned char eeprom_string[] = "1234567890ABCD\0"; // 测试数据
- 07 #define TEST_SIZE 12 // 操作字节数目
- 08 int main(void) {
- 09 setup_debug_uart(); // Configure the debug UART
- 10
- 11 while(1) {
- 12 // Display promote string on UART screen
- 13 PutTerminalString(LPC_USART0, (uint8_t *)the_prompt);
- 14
- 15 uart_handshake = false;
- 16 while (!uart_handshake);
- 17
- 18 memcpy(txbuf, eeprom_string, TEST_SIZE);
- 19 txbuf_len = TEST_SIZE;
- 20
- 21 // 将txbuf[TXBUF_SIZE]的数据写入EEPROM,总共TEST_SIZE个字节
- 22 // Initialize I2C0 as master
- 23 I2C0_Master_Init();
- 24 I2C0_Enable_Monitor();
- 25
- 26 txbuf_ptr = 0;
- 27 while (txbuf_ptr < txbuf_len) {
- 28 I2C_Start_Tx(LPC_I2C0, I2C_EEPROM_ADDR);
- 29 I2C_Send_Byte(LPC_I2C0, txbuf_ptr);
- 30
- 31 unsigned char eepage_ptr;
- 32 for (eepage_ptr = 0; eepage_ptr < EEPAGE_SIZE; eepage_ptr++) {
- 33 I2C_Send_Byte(LPC_I2C0, txbuf[txbuf_ptr]);
- 34 txbuf_ptr++;
- 35 if (txbuf_ptr >= txbuf_len)
- 36 break;
- 37 }
- 38 I2C_Stop(LPC_I2C0);
- 39 }
- 40
- 41 // Reading data from EEPROM to rxbuf[RXBUF_SIZE]
- 42 rxbuf_ptr = 0; // Reset the pointer of receiver
- 43 rxbuf_len = txbuf_len; // Get back all writing bytes
- 44
- 45 I2C_Start_Tx(LPC_I2C0, I2C_EEPROM_ADDR); // Send a START condition
- 46 I2C_Send_Byte(LPC_I2C0, 0); // Send the memory address of EEPROM
- 47
- 48 I2C_Start_Rx (LPC_I2C0, I2C_EEPROM_ADDR); // Send a START condition as receiver
- 49 rxbuf[rxbuf_ptr++] = LPC_I2C0->MSTDAT; // Get the first received byte
- 50
- 51 while (rxbuf_ptr < rxbuf_len) {
- 52 LPC_I2C0->MSTCTL = CTL_MSTCONTINUE; // Continue the transaction
- 53 I2C_Wait_Master(LPC_I2C0, I2C_STAT_MSTST_RX);// Wait for the data to be received
- 54 rxbuf[rxbuf_ptr++] = LPC_I2C0->MSTDAT; // Get one received byte
- 55 }
- 56 I2C_Stop(LPC_I2C0);
- 57 } // end of while 1
- 58 } // end of main
复制代码转载自: 恩智浦MCU加油站 如有侵权请联系删除
|