STM32:IIC
1 IIC
1.1 iic定义
iic全称inter integrated circuit,集成电路总线;为串行通信接口协议;通过SCL、SDA 2线进行板间通讯;
1.2 iic速度
iic标准规定在iic协议在快速模式下传输速率最高可达400Kbps,在高速模式下最高3.4Mbps;
1.3 iic用途
iic协议主要用在eeprom存储器,音频解码器,数字电位器,rtc时钟之类的协议通讯上;
1.4 eeprom
以at24c02为例,2Kbits >> 2^11bits >> 2^8bytes;存储容量虽然用byte为单位更直观但是实际使用单位是bit;
eeprom的写擦除也是通过写0xff来擦除的;at24cxx规定了写操作都以byte为单位操作;
2 IIC的数据帧
iic的数据都是在SCL为高时才有效,SDA的上升沿和下降沿判断停止和开始,SDA稳定时是传输数据;
上面这段话的意思是iic不像uart和spi那样上升沿采样,下降沿改变;iic的上升沿和下降沿都会采样,数据只能在低电平时改变;
2.1 start and stop conditions ack_respond
每byte都会跟一bit的ack_respond,为了防止数据错误需要判断ack;
根据代码注意一下SDA释放的时候是在bit 0 SCL拉低之后再释放成高电平的,读取的时候是在ack_respond下降沿之前,读完再拉的下降沿,
响应ack由master拉高,slave拉低;
2.2 write byte
2.3 write bytes
2.4 read bytes
以上数据帧为后文iic程序的数据帧;
SCL为高时,SDA数据有效;SCL为低时,SDA数据无效;
SCL为高时,SDA数据变化为start、stop信号;SCL为高时,SDA数据稳定为有效数据;
SCL边沿跳变时,SDA数据无效,SDA要是也变化大约会有bug;
网上一堆博客说硬件iic没有软件iic稳定,可能就是因为iic的数据帧不是边沿有效而是周期有效的原因,具体等我改天把硬件iic整理了就知道啦;
以上iic数据帧格式来自at24cxx数据手册,主要就是查看那十来个数据帧图,在此仅以写数据帧举例,图太多显得乱,具体看数据手册时序图;
写1byte之后都会跟1bit的ack响应位,通过对ack是否为0来判断是否接受成功;具体看数据手册时序图;
读byte之后也会跟1bit的ack或no_ack,但是只有SCL时序而SDA不返回ack响应;具体看数据手册时序图;
3 软件模拟iic时序代码
void i2c_Stop(void) { // 当SCL高电平时,SDA出现一个上跳沿表示I2C总线停止信号 EEPROM_I2C_SDA_0(); EEPROM_I2C_SCL_1(); i2c_Delay(); EEPROM_I2C_SDA_1(); i2c_Delay();//cae add } /* bug 如果把上面的代码换成下面的代码,虽然不会报错,但是写数据会始终写不进; void i2c_Stop(void) { // 当SCL高电平时,SDA出现一个上跳沿表示I2C总线停止信号 EEPROM_I2C_SCL_1(); EEPROM_I2C_SDA_0(); i2c_Delay(); EEPROM_I2C_SDA_1(); i2c_Delay();//cae stop signal add }*/ //stop之前基本都是ack_respond信号0,这里是个上升沿SCL,所以SCL上升沿的数据变化会影响数据传输; //至于start信号,虽然里面也有上升沿SCL,但是修改了几次都没影响读写结果; //觉得这个iic的SCL时序不规律,想着能否把SCL变成占空比50%的方波,在SCL的上升沿或下降沿修改SDA的数据然后读取响应; //实践了一下,SCL的两个边沿修改SDA数据都不行哦;
at24cxx_gpio_iic.c
#include "at24cxx_gpio_iic.h" #include "stm32f10x.h" #include "usart.h" /** ****************************************************************************** * @file * @version V1.0 * @date 2013-xx-xx * @brief i2c EEPROM(AT24C02)应用函数bsp ****************************************************************************** * @attention * * 实验平台:野火 F103-霸道 STM32 开发板 * 论坛 :http://www.firebbs.cn * 淘宝 :https://fire-stm32.taobao.com * ****************************************************************************** */ static void i2c_CfgGpio(void); //这个注释的方法挺好的,这样的话这个函数就只能本.h文件内调用了,增加了代码的稳定性; /* ********************************************************************************************************* * 函 数 名: i2c_Delay * 功能说明: I2C总线位延迟,最快400KHz * 形 参:无 * 返 回 值: 无 ********************************************************************************************************* */ static void i2c_Delay(void) { uint8_t i; /* 下面的时间是通过逻辑分析仪测试得到的。 工作条件:CPU主频72MHz ,MDK编译环境,1级优化 循环次数为10时,SCL频率 = 205KHz 循环次数为7时,SCL频率 = 347KHz, SCL高电平时间1.5us,SCL低电平时间2.87us 循环次数为5时,SCL频率 = 421KHz, SCL高电平时间1.25us,SCL低电平时间2.375us */ for (i = 0; i < 5; i++); } /* ********************************************************************************************************* * 函 数 名: i2c_Start * 功能说明: CPU发起I2C总线启动信号 * 形 参:无 * 返 回 值: 无 ********************************************************************************************************* */ void i2c_Start(void) { /* 当SCL高电平时,SDA出现一个下跳沿表示I2C总线启动信号 */ EEPROM_I2C_SCL_1(); EEPROM_I2C_SDA_1(); i2c_Delay(); EEPROM_I2C_SDA_0(); i2c_Delay(); //EEPROM_I2C_SCL_0();cae 这个函数放到send_byte里了; //i2c_Delay(); } /* ********************************************************************************************************* * 函 数 名: i2c_Start * 功能说明: CPU发起I2C总线停止信号 * 形 参:无 * 返 回 值: 无 ********************************************************************************************************* */ void i2c_Stop(void) { // 当SCL高电平时,SDA出现一个上跳沿表示I2C总线停止信号 EEPROM_I2C_SDA_0(); EEPROM_I2C_SCL_1(); i2c_Delay(); EEPROM_I2C_SDA_1(); i2c_Delay();//cae add } /* ********************************************************************************************************* * 函 数 名: i2c_SendByte * 功能说明: CPU向I2C总线设备发送8bit数据 * 形 参:_ucByte : 等待发送的字节 * 返 回 值: 无 ********************************************************************************************************* */ void i2c_SendByte(uint8_t _ucByte) { uint8_t i; // 先发送字节的高位bit7 EEPROM_I2C_SCL_0();//cae start add instead delete for (i = 0; i < 8; i++) { if (_ucByte & 0x80) { EEPROM_I2C_SDA_1(); } else { EEPROM_I2C_SDA_0(); } i2c_Delay(); EEPROM_I2C_SCL_1(); i2c_Delay(); EEPROM_I2C_SCL_0(); if (i == 7) { EEPROM_I2C_SDA_1(); // 释放总线 } _ucByte <<= 1; // 左移一个bit i2c_Delay(); } } /* ********************************************************************************************************* * 函 数 名: i2c_ReadByte * 功能说明: CPU从I2C总线设备读取8bit数据 * 形 参:无 * 返 回 值: 读到的数据 ********************************************************************************************************* */ uint8_t i2c_ReadByte(void) { uint8_t i; uint8_t value; /* 读到第1个bit为数据的bit7 */ value = 0; for (i = 0; i < 8; i++) { value <<= 1; EEPROM_I2C_SCL_1(); i2c_Delay(); if (EEPROM_I2C_SDA_READ()) { value++; } EEPROM_I2C_SCL_0(); i2c_Delay(); } return value; } /* ********************************************************************************************************* * 函 数 名: i2c_WaitAck * 功能说明: CPU产生一个时钟,并读取器件的ACK应答信号 * 形 参:无 * 返 回 值: 返回0表示正确应答,1表示无器件响应 ********************************************************************************************************* */ uint8_t i2c_WaitAck(void) { uint8_t re; EEPROM_I2C_SDA_1(); // CPU释放SDA总线 i2c_Delay(); EEPROM_I2C_SCL_1(); // CPU驱动SCL = 1, 此时器件会返回ACK应答 i2c_Delay(); if (EEPROM_I2C_SDA_READ()) // CPU读取SDA口线状态 { re = 1; } else { re = 0; } EEPROM_I2C_SCL_0(); i2c_Delay(); return re; } /* ********************************************************************************************************* * 函 数 名: i2c_Ack * 功能说明: CPU产生一个ACK信号 * 形 参:无 * 返 回 值: 无 ********************************************************************************************************* */ void i2c_Ack(void) { EEPROM_I2C_SDA_0(); /* CPU驱动SDA = 0 */ i2c_Delay(); EEPROM_I2C_SCL_1(); /* CPU产生1个时钟 */ i2c_Delay(); EEPROM_I2C_SCL_0(); i2c_Delay(); EEPROM_I2C_SDA_1(); /* CPU释放SDA总线 */ } /* ********************************************************************************************************* * 函 数 名: i2c_NAck * 功能说明: CPU产生1个NACK信号 * 形 参:无 * 返 回 值: 无 ********************************************************************************************************* */ void i2c_NAck(void) { EEPROM_I2C_SDA_1(); /* CPU驱动SDA = 1 */ i2c_Delay(); EEPROM_I2C_SCL_1(); /* CPU产生1个时钟 */ i2c_Delay(); EEPROM_I2C_SCL_0(); i2c_Delay(); } /* ********************************************************************************************************* * 函 数 名: i2c_CfgGpio * 功能说明: 配置I2C总线的GPIO,采用模拟IO的方式实现 * 形 参:无 * 返 回 值: 无 ********************************************************************************************************* */ static void i2c_CfgGpio(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(EEPROM_RCC_I2C_PORT, ENABLE); /* 打开GPIO时钟 */ GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SCL_PIN | EEPROM_I2C_SDA_PIN; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; /* 开漏输出 */ GPIO_Init(EEPROM_GPIO_PORT_I2C, &GPIO_InitStructure); /* 给一个停止信号, 复位I2C总线上的所有设备到待机模式 */ i2c_Stop(); } /* ********************************************************************************************************* * 函 数 名: i2c_CheckDevice * 功能说明: 检测I2C总线设备,CPU向发送设备地址,然后读取设备应答来判断该设备是否存在 * 形 参:_Address:设备的I2C总线地址 * 返 回 值: 返回值 0 表示正确, 返回1表示未探测到 ********************************************************************************************************* */ /* 应用说明: 在访问I2C设备前,请先调用 i2c_CheckDevice() 检测I2C设备是否正常,该函数会配置GPIO */ uint8_t i2c_CheckDevice(uint8_t _Address) { uint8_t ucAck; i2c_CfgGpio(); /* 配置GPIO */ i2c_Start(); /* 发送启动信号 */ /* 发送设备地址+读写控制bit(0 = w, 1 = r) bit7 先传 */ i2c_SendByte(_Address | EEPROM_I2C_WR); ucAck = i2c_WaitAck(); /* 检测设备的ACK应答 */ i2c_Stop(); /* 发送停止信号 */ return ucAck; } /* ********************************************************************************************************* * 函 数 名: ee_CheckOk * 功能说明: 判断串行EERPOM是否正常 * 形 参:无 * 返 回 值: 1 表示正常, 0 表示不正常 ********************************************************************************************************* */ uint8_t ee_CheckOk(void) { if (i2c_CheckDevice(EEPROM_DEV_ADDR) == 0) { return 1; } else { /* 失败后,切记发送I2C总线停止信号 */ i2c_Stop(); printf("发送地址,ACK为1; \r\n"); return 0; } } /* ********************************************************************************************************* * 函 数 名: ee_ReadBytes * 功能说明: 从串行EEPROM指定地址处开始读取若干数据 * 形 参:_usAddress : 起始地址 * _usSize : 数据长度,单位为字节 * _pReadBuf : 存放读到的数据的缓冲区指针 * 返 回 值: 0 表示失败,1表示成功 ********************************************************************************************************* */ uint8_t ee_ReadBytes(uint8_t *_pReadBuf, uint16_t _usAddress, uint16_t _usSize) { uint16_t i; /* 采用串行EEPROM随即读取指令序列,连续读取若干字节 */ /* 第1步:发起I2C总线启动信号 */ i2c_Start(); /* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */ i2c_SendByte(EEPROM_DEV_ADDR | EEPROM_I2C_WR); /* 此处是写指令 */ /* 第3步:等待ACK */ if (i2c_WaitAck() != 0) { goto cmd_fail; /* EEPROM器件无应答 */ } /* 第4步:发送字节地址,24C02只有256字节,因此1个字节就够了,如果是24C04以上,那么此处需要连发多个地址 */ i2c_SendByte((uint8_t)_usAddress); /* 第5步:等待ACK */ if (i2c_WaitAck() != 0) { goto cmd_fail; /* EEPROM器件无应答 */ } /* 第6步:重新启动I2C总线。前面的代码的目的向EEPROM传送地址,下面开始读取数据 */ i2c_Start(); /* 第7步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */ i2c_SendByte(EEPROM_DEV_ADDR | EEPROM_I2C_RD); /* 此处是读指令 */ /* 第8步:发送ACK */ if (i2c_WaitAck() != 0) { goto cmd_fail; /* EEPROM器件无应答 */ } /* 第9步:循环读取数据 */ for (i = 0; i < _usSize; i++) { _pReadBuf[i] = i2c_ReadByte(); /* 读1个字节 */ /* 每读完1个字节后,需要发送Ack, 最后一个字节不需要Ack,发Nack */ if (i != _usSize - 1) { i2c_Ack(); /* 中间字节读完后,CPU产生ACK信号(驱动SDA = 0) */ } else { i2c_NAck(); /* 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1) */ } } /* 发送I2C总线停止信号 */ i2c_Stop(); return 1; /* 执行成功 */ cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */ /* 发送I2C总线停止信号 */ i2c_Stop(); return 0; } /* ********************************************************************************************************* * 函 数 名: ee_WriteBytes * 功能说明: 向串行EEPROM指定地址写入若干数据,采用页写操作提高写入效率 * 形 参:_usAddress : 起始地址 * _usSize : 数据长度,单位为字节 * _pWriteBuf : 存放读到的数据的缓冲区指针 * 返 回 值: 0 表示失败,1表示成功 ********************************************************************************************************* */ uint8_t ee_WriteBytes(uint8_t *_pWriteBuf, uint16_t _usAddress, uint16_t _usSize) { uint16_t i,m; uint16_t usAddr; /* 写串行EEPROM不像读操作可以连续读取很多字节,每次写操作只能在同一个page。 对于24xx02,page size = 8 简单的处理方法为:按字节写操作模式,没写1个字节,都发送地址 为了提高连续写的效率: 本函数采用page wirte操作。 */ usAddr = _usAddress; for (i = 0; i < _usSize; i++) { /* 当发送第1个字节或是页面首地址时,需要重新发起启动信号和地址 */ if ((i == 0) || (usAddr & (EEPROM_PAGE_SIZE - 1)) == 0) { /* 第0步:发停止信号,启动内部写操作 */ i2c_Stop(); /* 通过检查器件应答的方式,判断内部写操作是否完成, 一般小于 10ms CLK频率为200KHz时,查询次数为30次左右 */ for (m = 0; m < 1000; m++) { /* 第1步:发起I2C总线启动信号 */ i2c_Start(); /* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */ i2c_SendByte(EEPROM_DEV_ADDR | EEPROM_I2C_WR); /* 此处是写指令 */ /* 第3步:发送一个时钟,判断器件是否正确应答 */ if (i2c_WaitAck() == 0) { break; } } if (m == 1000) { goto cmd_fail; /* EEPROM器件写超时 */ } /* 第4步:发送字节地址,24C02只有256字节,因此1个字节就够了,如果是24C04以上,那么此处需要连发多个地址 */ i2c_SendByte((uint8_t)usAddr); /* 第5步:等待ACK */ if (i2c_WaitAck() != 0) { goto cmd_fail; /* EEPROM器件无应答 */ } } /* 第6步:开始写入数据 */ i2c_SendByte(_pWriteBuf[i]); /* 第7步:发送ACK */ if (i2c_WaitAck() != 0) { goto cmd_fail; /* EEPROM器件无应答 */ } usAddr++; /* 地址增1 */ } /* 命令执行成功,发送I2C总线停止信号 */ i2c_Stop(); return 1; cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */ /* 发送I2C总线停止信号 */ i2c_Stop(); printf("eeprom写无应答 \r\n"); return 0; } void ee_Erase(void) { uint16_t i; uint8_t buf[EEPROM_SIZE]; /* 填充缓冲区 */ for (i = 0; i < EEPROM_SIZE; i++) { buf[i] = 0xFF; } /* 写EEPROM, 起始地址 = 0,数据长度为 256 */ if (ee_WriteBytes(buf, 0, EEPROM_SIZE) == 0) { printf("擦除eeprom出错!\r\n"); return; } else { printf("擦除eeprom成功!\r\n"); } } /*--------------------------------------------------------------------------------------------------*/ static void ee_Delay(__IO uint32_t nCount) //简单的延时函数 { for(; nCount != 0; nCount--); } /* * eeprom AT24C02 读写测试 * 正常返回1,异常返回0 */ uint8_t ee_Test(void) { uint16_t i; uint8_t write_buf[EEPROM_SIZE]; uint8_t read_buf[EEPROM_SIZE]; /*-----------------------------------------------------------------------------------*/ if (ee_CheckOk() == 0) { /* 没有检测到EEPROM */ printf("没有检测到串行EEPROM!\r\n"); return 0; } /*------------------------------------------------------------------------------------*/ /* 填充测试缓冲区 */ for (i = 0; i < EEPROM_SIZE; i++) { write_buf[i] = i; } /*------------------------------------------------------------------------------------*/ if (ee_WriteBytes(write_buf, 0, EEPROM_SIZE) == 0) { printf("写eeprom出错!\r\n"); return 0; } else { printf("写eeprom成功!\r\n"); } /*写完之后需要适当的延时再去读,不然会出错*/ ee_Delay(0x0FFFFF); /*-----------------------------------------------------------------------------------*/ if (ee_ReadBytes(read_buf, 0, EEPROM_SIZE) == 0) { printf("读eeprom出错!\r\n"); return 0; } else { printf("读eeprom成功,数据如下:\r\n"); } /*-----------------------------------------------------------------------------------*/ for (i = 0; i < EEPROM_SIZE; i++) { if(read_buf[i] != write_buf[i]) { printf("0x%02X ", read_buf[i]); printf("错误:EEPROM读出与写入的数据不一致"); return 0; } printf(" %02X", read_buf[i]); if ((i & 15) == 15) { printf("\r\n"); } } printf("eeprom读写测试成功\r\n"); return 1; } /*********************************************END OF FILE**********************/
at24cxx_gpio._iich
#ifndef AT24CXX_GPIO_IIC_H #define AT24CXX_GPIO_IIC_H #include "stm32f10x.h" #include <inttypes.h> #define EEPROM_I2C_WR 0 /* 写控制bit */ #define EEPROM_I2C_RD 1 /* 读控制bit */ /* 定义I2C总线连接的GPIO端口, 用户只需要修改下面4行代码即可任意改变SCL和SDA的引脚 */ #define EEPROM_GPIO_PORT_I2C GPIOB /* GPIO端口 */ #define EEPROM_RCC_I2C_PORT RCC_APB2Periph_GPIOB /* GPIO端口时钟 */ #define EEPROM_I2C_SCL_PIN GPIO_Pin_6 /* 连接到SCL时钟线的GPIO */ #define EEPROM_I2C_SDA_PIN GPIO_Pin_7 /* 连接到SDA数据线的GPIO */ /* 定义读写SCL和SDA的宏,已增加代码的可移植性和可阅读性 */ #if 1 /* 条件编译: 1 选择GPIO的库函数实现IO读写 */ #define EEPROM_I2C_SCL_1() GPIO_SetBits(EEPROM_GPIO_PORT_I2C, EEPROM_I2C_SCL_PIN) /* SCL = 1 */ #define EEPROM_I2C_SCL_0() GPIO_ResetBits(EEPROM_GPIO_PORT_I2C, EEPROM_I2C_SCL_PIN) /* SCL = 0 */ #define EEPROM_I2C_SDA_1() GPIO_SetBits(EEPROM_GPIO_PORT_I2C, EEPROM_I2C_SDA_PIN) /* SDA = 1 */ #define EEPROM_I2C_SDA_0() GPIO_ResetBits(EEPROM_GPIO_PORT_I2C, EEPROM_I2C_SDA_PIN) /* SDA = 0 */ #define EEPROM_I2C_SDA_READ() GPIO_ReadInputDataBit(EEPROM_GPIO_PORT_I2C, EEPROM_I2C_SDA_PIN) /* 读SDA口线状态 */ #else /* 这个分支选择直接寄存器操作实现IO读写 */ /* 注意:如下写法,在IAR最高级别优化时,会被编译器错误优化 */ #define EEPROM_I2C_SCL_1() EEPROM_GPIO_PORT_I2C->BSRR = EEPROM_I2C_SCL_PIN /* SCL = 1 */ #define EEPROM_I2C_SCL_0() EEPROM_GPIO_PORT_I2C->BRR = EEPROM_I2C_SCL_PIN /* SCL = 0 */ #define EEPROM_I2C_SDA_1() EEPROM_GPIO_PORT_I2C->BSRR = EEPROM_I2C_SDA_PIN /* SDA = 1 */ #define EEPROM_I2C_SDA_0() EEPROM_GPIO_PORT_I2C->BRR = EEPROM_I2C_SDA_PIN /* SDA = 0 */ #define EEPROM_I2C_SDA_READ() ((EEPROM_GPIO_PORT_I2C->IDR & EEPROM_I2C_SDA_PIN) != 0) /* 读SDA口线状态 */ #endif void i2c_Start(void); void i2c_Stop(void); void i2c_SendByte(uint8_t _ucByte); uint8_t i2c_ReadByte(void); uint8_t i2c_WaitAck(void); void i2c_Ack(void); void i2c_NAck(void); uint8_t i2c_CheckDevice(uint8_t _Address); /* * AT24C02 2kb = 2048bit = 2048/8 B = 256 B * 32 pages of 8 bytes each * * Device Address * 1 0 1 0 A2 A1 A0 R/W * 1 0 1 0 0 0 0 0 = 0XA0 * 1 0 1 0 0 0 0 1 = 0XA1 */ /* AT24C01/02每页有8个字节 * AT24C04/08A/16A每页有16个字节 */ #define EEPROM_DEV_ADDR 0xA0 /* 24xx02的设备地址 */ #define EEPROM_PAGE_SIZE 8 /* 24xx02的页面大小 */ #define EEPROM_SIZE 256 /* 24xx02总容量 */ uint8_t ee_CheckOk(void); uint8_t ee_ReadBytes(uint8_t *_pReadBuf, uint16_t _usAddress, uint16_t _usSize); uint8_t ee_WriteBytes(uint8_t *_pWriteBuf, uint16_t _usAddress, uint16_t _usSize); void ee_Erase(void); uint8_t ee_Test(void); #endif
4 IIC的寄存器
5 标准库函数
6 小结
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了