IIC 简介
IIC(Inter-Integrated Circuit)总线是一种由 PHILIPS 公司开发的两线式串行总线,用于连接微 控制器以及其外围设备。它是由数据线 SDA 和时钟线 SCL 构成的串行总线,可发送和接收数 据,在 CPU 与被控 IC 之间、IC 与 IC 之间进行双向传送。
IIC 总线有如下特点:
①总线由数据线 SDA 和时钟线 SCL 构成的串行总线,数据线用来传输数据,时钟线用来 同步数据收发。
②总线上每一个器件都有一个唯一的地址识别,所以我们只需要知道器件的地址,根据时 序就可以实现微控制器与器件之间的通信。
③数据线 SDA 和时钟线 SCL 都是双向线路,都通过一个电流源或上拉电阻连接到正的电 压,所以当总线空闲的时候,这两条线路都是高电平。
④总线上数据的传输速率在标准模式下可达 100kbit/s 在快速模式下可达 400kbit/s 在高速 模式下可达 3.4Mbit/s。
⑤总线支持设备连接。在使用 IIC 通信总线时,可以有多个具备 IIC 通信能力的设备挂载 在上面,同时支持多个主机和多个从机,连接到总线的接口数量只由总线电容 400pF 的限制决 定。
IIC 总线挂载多个器件的示意图,如下图所示:下面来学习 IIC 总线协议,IIC 总线时序图如下所示:
为了便于大家更好的了解 IIC 协议,我们从起始信号、停止信号、应答信号、数据有效性、 数据传输以及空闲状态等 6 个方面讲解。
① 起始信号
当 SCL 为高电平期间,SDA 由高到低的跳变,起始信号是一种电平跳变时序信号,而不是 一个电平信号。该信号由主机发出,在起始信号产生后,总线就处于被占用状态,准备数据传 输。
② 停止信号
当 SCL 为高电平期间,SDA 由低到高的跳变;停止信号也是一种电平跳变时序信号,而不 是一个电平信号。该信号由主机发出,在停止信号发出后,总线就处于空闲状态。
③ 应答信号
发送器每发送一个字节,就在时钟脉冲 9 期间释放数据线,由接收器反馈一个应答信号。 应答信号为低电平时,规定为有效应答位(ACK 简称应答位),表示接收器已经成功地接收了 该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成 功。
观察上图就可以发现,有效应答的要求是从机在第 9 个时钟脉冲之前的低电平期间 将 SDA 线拉低,并且确保在该时钟的高电平期间为稳定的低电平。如果接收器是主机,则在它 收到最后一个字节后,发送一个 NACK 信号,以通知被控发送器结束数据发送,并释放 SDA 线,以便主机接收器发送一个停止信号。
④ 数据有效性
IIC 总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在 时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。数据在 SCL 的上 升沿到来之前就需准备好。并在下降沿到来之前必须稳定。
⑤ 数据传输
在 IIC 总线上传送的每一位数据都有一个时钟脉冲相对应(或同步控制),即在 SCL 串行 时钟的配合下,在 SDA 上逐位地串行传送每一位数据。数据位的传输是边沿触发。
⑥ 空闲状态
IIC 总线的 SDA 和 SCL 两条信号线同时处于高电平时,规定为总线的空闲状态。此时各个 器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉 高。
了解前面的知识后,下面介绍一下 IIC 的基本的读写通讯过程,包括主机写数据到从机即 写操作,主机到从机读取数据即读操作。下面先看一下写操作通讯过程图,见图 34.1.3.2 所示:
主机首先在 IIC 总线上发送起始信号,那么这时总线上的从机都会等待接收由主机发出的 数据。主机接着发送从机地址+0(写操作)组成的 8bit 数据,所有从机接收到该 8bit 数据后,自行检验是否是自己的设备的地址,假如是自己的设备地址,那么从机就会发出应答信号。主机 在总线上接收到有应答信号后,才能继续向从机发送数据。注意:IIC 总线上传送的数据信号 是广义的,既包括地址信号,又包括真正的数据信号。
接着讲解一下 IIC 总线的读操作过程,先看一下读操作通讯过程图,见图 34.1.3.3 所示。
主机向从机读取数据的操作,一开始的操作与写操作有点相似,观察两个图也可以发现, 都是由主机发出起始信号,接着发送从机地址+1(读操作)组成的 8bit 数据,从机接收到数据验 证是否是自身的地址。 那么在验证是自己的设备地址后,从机就会发出应答信号,并向主机返 回 8bit 数据,发送完之后从机就会等待主机的应答信号。假如主机一直返回应答信号,那么从 机可以一直发送数据,也就是图中的(n byte + 应答信号)情况,直到主机发出非应答信号,从 机才会停止发送数据。
AT24C02 简介
24C02 是一个 2K bit 的串行 EEPROM 存储器,内部含有 256 个字节。在 24C02 里面还有 一个 8 字节的页写缓冲器。该设备的通信方式 IIC,通过其 SCL 和 SDA 与其他设备通信。
上图的 WP 引脚是写保护引脚,接高电平只读,接地允许读和写,我们的板子设计是把该 引脚接地。每一个设备都有自己的设备地址,24C02 也不例外,但是 24C02 的设备地址是包括 不可编程部分和可编程部分,可编程部分是根据上图的硬件引脚 A0、A1 和 A2 所决定。设备 地址最后一位用于设置数据的传输方向,即读操作/写操作,0 是写操作,1 是读操作。根据我们的板子设计,A0、A1 和 A2 均接地处理,所以 24C02 设备的读操作地址为:0xA1; 写操作地址为:0xA0。
上图展示的主机向 24C02 写操作时序图,主机在 IIC 总线发送第 1 个字节的数据为 24C02 的设备地址 0xA0,用于寻找总线上找到 24C02,在获得 24C02 的应答信号之后,继续发送第 2 个字节数据,该字节数据是 24C02 的内存地址,再等到 24C02 的应答信号,主机继续发送 第 3 字节数据,这里的数据即是写入在第 2 字节内存地址的数据。主机完成写操作后,可以发 出停止信号,终止数据传输。
24C02 读取数据的过程是一个复合的时序,其中包含写时序和读时序。先看第一个通信过 程,这里是写时序,起始信号产生后,主机发送 24C02 设备地址 0xA0,获取从机应答信号后, 接着发送需要读取的内存地址;在读时序中,起始信号产生后,主机发送 24C02 设备地址 0xA1, 获取从机应答信号后,接着从机返回刚刚在写时序中内存地址的数据,以字节为单位传输在总 线上,假如主机获取数据后返回的是应答信号,那么从机会一直传输数据,当主机发出的是非 应答信号并以停止信号发出为结束,从机就会结束传输。
目前大部分 MCU 都带有 IIC 总线接口,STM32F1 也不例外。但是这里我们不使用 STM32F1 的硬件 IIC 来读写 24C02,而是通过软件模拟。ST 为了规避飞利浦 IIC 专利问题,将 STM32 的 硬件 IIC 设计的比较复杂,而且稳定性不怎么好,所以这里我们不推荐使用。有兴趣的读者可 以自行研究 STM32F1 的硬件 IIC 的使用。
用软件模拟 IIC,最大的好处就是方便移植,同一个代码兼容所有 MCU,任何一个单片机只要 有 IO 口,就可以很快的移植过去,而且不需要特定的 IO 口。而硬件 IIC,则换一款 MCU,基 本上就得重新移植,这也是我们推荐使用软件模拟 IIC 的另外一个原因。
代码
#include "./BSP/IIC/iic.h"
#include "./SYSTEM/delay/delay.h"/* 初始化IIC */
void iic_init(void)
{GPIO_InitTypeDef gpio_init_struct = {0};__HAL_RCC_GPIOB_CLK_ENABLE();/* SCL,SDA引脚时钟使能 */gpio_init_struct.Pin = GPIO_PIN_6;gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;/* 推挽输出 */gpio_init_struct.Pull = GPIO_PULLUP;/* 上拉 */gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;/* 高速 */HAL_GPIO_Init(GPIOB, &gpio_init_struct);/* SCL */gpio_init_struct.Pin = GPIO_PIN_7;gpio_init_struct.Mode = GPIO_MODE_OUTPUT_OD;/* 开漏输出 */HAL_GPIO_Init(GPIOB, &gpio_init_struct);/* SDA *//* SDA引脚模式设置,开漏输出,上拉, 这样就不用再设置IO方向了, 开漏输出的时候(=1), 也可以读取外部信号的高低电平 */iic_stop();/* 停止总线上所有设备 */
}/* IIC延时函数,用于控制IIC读写速度 */
static void iic_delay(void)
{delay_us(2);/* 2us的延时, 读写速度在250Khz以内 */
}/* 产生IIC起始信号 */
void iic_start(void)
{IIC_SCL(1);IIC_SDA(1);iic_delay();IIC_SDA(0);/* START信号: 当SCL为高时, SDA从高变成低, 表示起始信号 */iic_delay();IIC_SCL(0);/* 钳住I2C总线,准备发送或接收数据 */iic_delay();
}/* 产生IIC停止信号 */
void iic_stop(void)
{IIC_SDA(0);/* STOP信号: 当SCL为高时, SDA从低变成高, 表示停止信号 */iic_delay();IIC_SCL(1);iic_delay();IIC_SDA(1);/* 发送I2C总线结束信号 */iic_delay();
}/*** @brief 等待应答信号到来* @param 无* @retval 1,接收应答失败* 0,接收应答成功*/
uint8_t iic_wait_ack(void)
{uint8_t waittime = 0;uint8_t rack = 0;IIC_SDA(1);/* 主机释放SDA线(此时外部器件可以拉低SDA线) */iic_delay();IIC_SCL(1);/* SCL=1, 此时从机可以返回ACK */iic_delay();while(IIC_READ_SDA)/* 等待应答 */{waittime++;if(waittime > 250){iic_stop();rack = 1;break;}}IIC_SCL(0);/* SCL=0, 结束ACK检查 */iic_delay();return rack;
}/* 产生ACK应答 */
void iic_ack(void)
{IIC_SDA(0);/* SCL 0 -> 1 时 SDA = 0,表示应答 */iic_delay();IIC_SCL(1);/* 产生一个时钟 */iic_delay();IIC_SCL(0);iic_delay();IIC_SDA(1);/* 主机释放SDA线 */iic_delay();
}/* 不产生ACK应答 */
void iic_nack(void)
{IIC_SDA(1);/* SCL 0 -> 1 时 SDA = 1,表示不应答 */iic_delay();IIC_SCL(1);/* 产生一个时钟 */iic_delay();IIC_SCL(0);iic_delay();
}/*IIC发送一个字节*/
void iic_send_byte(uint8_t data)
{uint8_t t;for(t = 0; t < 8; t++){IIC_SDA((data & 0x80) >> 7);/* 高位先发送 */iic_delay();IIC_SCL(1);iic_delay();IIC_SCL(0);data <<= 1;/* 左移1位,用于下一次发送 */}IIC_SDA(1);/* 发送完成, 主机释放SDA线 */
}/*** @brief IIC读取一个字节* @param ack: ack=1时,发送ack; ack=0时,发送nack* @retval 接收到的数据*/
uint8_t iic_read_byte(uint8_t ack)
{uint8_t i, receive = 0;for(i = 0; i < 8; i++)/* 接收1个字节数据 */{receive <<= 1;/* 高位先输出,所以先收到的数据位要左移 */IIC_SCL(1);iic_delay();if(IIC_READ_SDA){receive ++;}IIC_SCL(0);iic_delay();}if(!ack){iic_nack();/* 发送nACK */}else{iic_ack();/* 发送ACK */}return receive;
}
在 iic_init 函数中主要工作就是对于 GPIO 的初始化,用于 iic 通信,不过这里需要注意的 一点是 SDA 线的 GPIO 模式使用开漏模式,同时需要注意:STM32F103 必须要外接上拉电阻!
在这里首先定义一个 iic_delay 函数,目的就是控制 IIC 的读写速度,通过示波器检测读写 速度在 250KHz 内,所以一秒钟传送 500Kb 数据,换算一下即一个 bit 位需要 2us,在这个延时 时间内可以让器件获得一个稳定性的数据采集。
iic_start 函数中,通过调用 myiic.h 中通过宏定义好的可以输出高低电平的 SCL 和 SDA 来 模拟 iic 总线中起始信号的发送,在 SCL 时钟线为高电平的时候,SDA 数据线从高电平状态转 化到低电平状态,最后拉低时钟线,准备发送或者接收数据。
iic_stop 函数中,也是按着模拟 iic 总线中停止信号的逻辑,在 SCL 时钟线为高电平的时候, SDA 数据线从低电平状态转化到高电平状态。
在 iic 的发送函数 iic_send_byte 中,我们把需要发送的数据作为形参,形参大小为 1 个字 节。在 iic 总线传输中,一个时钟信号就发送一个 bit,所以该函数需要循环八次,模拟八个时 钟信号,才能把形参的 8 个位数据都发送出去。这里使用的是形参 data 和 0x80 与运算的方式, 判断其最高位的逻辑值,假如为 1 即需要控制 SDA 输出高电平,否则为 0 控制 SDA 输出低电 平。
通过上图就可以很清楚了解数据传输时的细节,经过第一步的 SDA 高低电平的确定后,接 着需要延时,确保 SDA 输出的电平稳定,在 SCL 保持高电平期间,SDA 线上的数据是有效的, 此过程也是需要延时,使得从设备能够采集到有效的电平。然后准备下一位的数据,所以这里 需要的是把 data 左移一位,等待下一个时钟的到来,从设备进行读取。把上述的操作重复 8 次 就可以把 data 的 8 个位数据发送完毕,循环结束后,把 SDA 线拉高,等待接收从设备发送过 来的应答信号。
iic_read_byte 函数具体实现的方式跟 iic_send_byte 函数有所不同。首先可以明确的是时钟 信号是通过主机发出的,而且接收到的数据大小为 1 字节,但是 IIC 传输的单位是 bit,所以就 需要执行 8 次循环,才能把一字节数据接收完整。
具体实现过程:首先需要一个变量 receive 存放接收到的数据,在每一次循环开始前都需要 对 receive 进行左移 1 位操作,那么 receive 的 bit0 位每一次赋值前都是空的,用来存放最新接 收到的数据位,然后在 SCL 线进行高低电平切换时输出 IIC 时钟,在 SCL 高电平期间加入延 时,确保有足够的时间能让数据发送并进行处理,使用宏定义 IIC_READ_SDA 就可以判断读取 到的高低电平,假如 SDA 为高电平,那么 receive++即在 bit0 置 1,否则不做处理即保持原来的 0 状态。当 SCL 线拉低后,需要加入延时,便于从机切换 SDA 线输出数据。在 8 次循环结束 后,我们就获得了 8bit 数据,把它作为返回值返回,然而按照时序图,作为主机就需要发送应 答或者非应答信号,去回复从机。
首先先讲解一下 iic_wait_ack 函数,该函数主要用在写时序中,当启动起始信号,发送完 8bit 数据到从机时,我们就需要等待以及处理接收从机发送过来的响应信号或者非响应信号, 一般就是在 iic_send_byte 函数后面调用。
具体实现:首先先释放 SDA,把电平拉高,延时等待从机操作 SDA 线,然后主机拉高时 钟线并延时,确保有充足的时间让主机接收到从机发出的 SDA 信号,这里使用的是 IIC_READ_SDA 宏定义去读取,根据 IIC 协议,主机读取 SDA 的值为低电平,就表示“应答信 号”;读到 SDA 的值为高电平,就表示“非应答信号”。在这个等待读取的过程中加入了超时判 断,假如超过这个时间没有接收到数据,那么主机直接发出停止信号,跳出循环,返回等于 1的变量。在正常等待到应答信号后,主机会把 SCL 时钟线拉低并延时,返回是否接收到应答信 号。
当主机作为作为接收端时,调用 iic_read_byte 函数之后,按照 iic 通信协议,需要给从机返 回应答或者是非应答信号,这里就是用到了 iic_ack 和 iic_nack 函数。
具体实现:从上面的说明已经知道了 SDA 为低电平即应答信号,高电平即非应答信号,那 么还是老规矩,首先先根据返回“应答”或者“非应答”两种情况拉低或者拉高 SDA,并延时 等待 SDA 电平稳定,然后主机拉高 SCL 线并延时,确保从机能有足够时间去接收 SDA 线上的 电平信号。然后主机拉低时钟线并延时,完成这一位数据的传送。最后把 SDA 拉高,呈高阻态, 方便后续通信用到。
#include "./BSP/24CXX/24cxx.h"
#include "./BSP/IIC/iic.h"
#include "./SYSTEM/delay/delay.h"/* 初始化AT24C02 */
void at24c02_init(void)
{iic_init();
}void at24c02_write_one_byte(uint8_t addr, uint8_t data)
{/* 1. 发送起始信号 */iic_start();/* 2. 发送通讯地址(写操作地址) */iic_send_byte(0xa0);/* 3. 等待应答信号 */iic_wait_ack();/* 4. 发送内存地址 */iic_send_byte(addr);/* 5. 等待应答信号 */iic_wait_ack();/* 6. 发送写入数据 */iic_send_byte(data);/* 7. 等待应答信号*/iic_wait_ack();/* 8. 发送停止信号 */iic_stop();/* 9. 等待EEPROM写入完成 */delay_ms(10);
}uint8_t at24c02_read_one_byte(uint8_t addr)
{uint8_t rec = 0;/* 1. 发送起始信号 */iic_start();/* 2. 发送通讯地址(写操作地址) */iic_send_byte(0xa0);/* 3. 等待应答信号 */iic_wait_ack();/* 4. 发送内存地址 */iic_send_byte(addr);/* 5. 等待应答信号 */iic_wait_ack();/* 6. 发送起始信号 */iic_start();/* 7. 发送通讯地址(读操作地址) */iic_send_byte(0xa1);/* 8. 等待应答信号 */iic_wait_ack();/* 9. 等待接收数据 */rec = iic_read_byte(0);/* 10. 发送停止信号 */iic_stop();return rec;
}
下面先看一下 at24cxx_write_one_byte 函数,实现在 AT24Cxx 芯片指定地址写入一个 数据 。
该函数的操作流程跟前面已经分析过的 24C02 单字节写时序一样,首先调用 iic_start 函数 产生起始信号,然后调用 iic_send_byte 函数发送第 1 个字节数据设备地址,等待 24Cxx 设备返 回应答信号;收到应答信号后,继续发送第 2 个 1 字节数据内存地址 addr;等待接收应答后, 最后发送第 3 个字节数据写入内存地址的数据 data,24Cxx 设备接收完数据,返回应答信号, 主机调用 iic_stop 函数产生停止信号终止数据传输,最终需要延时 10ms,等待 eeprom 写入完 毕。
接下来看一下 at24cxx_read_one_byte 函数。
这里的函数的实现跟前面第 34.1.4 小节 24C02 数据传输中的读时序一致,主机首先调用 iic_start 函数产生起始信号,然后调用 iic_send_byte 函数发送第 1 个字节数据设备写地址,使用 iic_wait_ack 函数等待 24Cxx 设备返回应答信号;收到应答信号后,继续发送第 2 个 1 字节数 据内存地址 addr;等待接收应答后,重新调用 iic_start 函数产生起始信号,这一次的设备方向 改变了,调用 iic_send_byte 函数发送设备读地址,然后使用 iic_wait_ack 函数去等待设备返回 应答信号,同时使用 iic_read_byte 去读取从从机发出来的数据。由于 iic_read_byte 函数的形参 是 0,所以在获取完 1 个字节的数据后,主机发送非应答信号,停止数据传输,最终调用 iic_stop 函数产生停止信号,返回从从机 addr 中读取到的数据。
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/KEY/key.h"
#include "./BSP/24CXX/24cxx.h"int main(void)
{uint8_t t = 0;uint8_t key = 0;uint8_t receive1 = 0, receive2 = 0;HAL_Init(); /* 初始化 HAL 库 */sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */delay_init(72); /* 延时初始化 */usart_init(115200); /* 传口初始化 */led_init(); /* LED初始化 */lcd_init(); /* LCD初始化 */key_init(); /* KEY初始化 */at24c02_init(); /* AT24C02初始化 */lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);lcd_show_string(30, 70, 200, 16, 16, "IIC TEST", RED);lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);lcd_show_string(30, 110, 200, 16, 16, "KEY0:Write KEY1:Read", RED); /* 显示提示信息 */lcd_show_string(30, 130, 200, 16, 16, "24C02 Ready!", RED);while(1){key = key_scan(0);if(key == KEY0_PRES){at24c02_write_one_byte(10, 66);lcd_show_string(30, 150, 200, 16, 16, "Data1 Write Finished!", BLUE); /* 提示传送完成 */at24c02_write_one_byte(100, 124);lcd_show_string(30, 170, 200, 16, 16, "Data2 Write Finished!", BLUE); /* 提示传送完成 */}if(key == KEY1_PRES){receive1 = at24c02_read_one_byte(10);lcd_show_string(30, 150, 200, 16, 16, "The Data1 Readed Is:", BLUE); /* 提示传送完成 */lcd_show_xnum(30 + 20 * 8, 150, receive1, 3, 16, 0, BLUE); /* 显示读取的值 */receive2 = at24c02_read_one_byte(100);lcd_show_string(30, 170, 200, 16, 16, "The Data2 Readed Is:", BLUE); /* 提示传送完成 */lcd_show_xnum(30 + 20 * 8, 170, receive2, 3, 16, 0, BLUE); /* 显示读取的值 */}t++;if(t == 20){t = 0;LED1_TOGGLE();}delay_ms(10);}
}