历史上的今天
返回首页

历史上的今天

今天是:2024年10月22日(星期二)

正在发生

2021年10月22日 | stm32专题十七:深度解析 stm32 硬件iic (i2c)

2021-10-22 来源:eefocus

首先是配置I2C的GPIO,然后配置I2C参数。就是常规配置,按流程来写不会错。


/**

 * @brief EEPROM IIC 配置

 */

void I2C_EE_config(void)

{

  GPIO_InitTypeDef GPIO_InitStruct;

I2C_InitTypeDef I2C_InitStruct;

// 开启I2C GPIO时钟

EPROM_I2C_GPIO_APBxClkCmd(EEPROM_I2C_SCL_GPIO_CLK | EEPROM_I2C_SDA_GPIO_CLK, ENABLE);

// 开启I2C 外设时钟

EEPROM_I2C_APBxClkCmd(EEPROM_I2C_CLK, ENABLE);

 

  // I2C的引脚配置为复用开漏输出

GPIO_InitStruct.GPIO_Pin = EPROM_I2C_SCL_GPIO_PIN;

GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD;

GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;

  GPIO_Init(EPROM_I2C_SCL_GPIO_PORT, &GPIO_InitStruct);

GPIO_InitStruct.GPIO_Pin = EPROM_I2C_SDA_GPIO_PIN;

  GPIO_Init(EPROM_I2C_SDA_GPIO_PORT, &GPIO_InitStruct);

// 配置I2C参数(时钟速度、模式、占空比、自身地址、应答使能、7位设备地址)

I2C_InitStruct.I2C_ClockSpeed = EEPROM_I2C_BAUDRATE;

I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;

I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;

I2C_InitStruct.I2C_OwnAddress1 = STM32_I2C_OWN_ADDR;

I2C_InitStruct.I2C_Ack = I2C_Ack_Enable;

I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;

// 初始化

I2C_Init(EEPROM_I2C, &I2C_InitStruct);

// 使能串口

I2C_Cmd(EEPROM_I2C, ENABLE);

}

EEPROM写入一个字节的代码,这里唯一值得注意的是,在检测到EV8事件时的状态。此时移位寄存器正在发送数据,而数据寄存器DR为空。发送data会被暂存到DR,直到移位寄存器发送完,再由DR转移到移位寄存器。


// 向EEPROM写入一个字节(因为EEPROM设备地址固定为0XA0)

void EEPROM_Byte_Write(uint8_t addr, uint8_t data)

{

// 产生起始信号

I2C_GenerateSTART(EEPROM_I2C, ENABLE);

 

while(I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT) == ERROR);

// EV5事件被检测到,发送设备地址(直接填入EEPROM的设备地址:EEPROM_ADDR)

I2C_Send7bitAddress(EEPROM_I2C, EEPROM_ADDR, I2C_Direction_Transmitter);

while(I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) == ERROR);

// EV6事件被检测到,发送要操作的存储单元地址

I2C_SendData(EEPROM_I2C, addr);

while(I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTING) == ERROR);

// EV8事件被检测到(此时移位寄存器正在发送数据,而数据寄存器DR为空)

// (发送data会被暂存到DR,直到移位寄存器发送完,再由DR转移到移位寄存器)

I2C_SendData(EEPROM_I2C, data);

// DR不再填入新的数据,移位寄存器发送完最后一个字节,结束

while(I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED) == ERROR);

// 数据传输完成,产生结束信号

I2C_GenerateSTOP(EEPROM_I2C, ENABLE);

}


然后是读取n个字节的代码。核心思想有几个。第一,按照EEPROM的random read时序,并检测对应的事件;第二,在读入最后一个字节之前,要把应答使能设置为disable,通知EEPROM不要再发数据;第三,接收数据指针要不断递增,防止覆盖;第四,读操作完成后,重新开启应答使能。


// 从EEPROM读取多个字节

void EEPROM_Read(uint8_t addr, uint8_t *data, uint8_t numByteToRead)

{

// 产生起始信号

I2C_GenerateSTART(EEPROM_I2C, ENABLE);

 

while(I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT) == ERROR);

// EV5事件被检测到,发送设备地址(直接填入EEPROM的设备地址:EEPROM_ADDR)

I2C_Send7bitAddress(EEPROM_I2C, EEPROM_ADDR, I2C_Direction_Transmitter);

while(I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) == ERROR);

// EV6事件被检测到,发送要操作的存储单元地址

I2C_SendData(EEPROM_I2C, addr);

while(I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTING) == ERROR);

// 第二次产生起始信号

I2C_GenerateSTART(EEPROM_I2C, ENABLE);

 

while(I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT) == ERROR);

// EV5事件被检测到,发送设备地址,这里方向要选为接收

I2C_Send7bitAddress(EEPROM_I2C, EEPROM_ADDR, I2C_Direction_Receiver);

while(I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) == ERROR);

// 判断是否为最后一个字节

while (numByteToRead) 

{

if (numByteToRead == 1)

{

// 最后一个字节,产生非应答

I2C_AcknowledgeConfig(EEPROM_I2C, DISABLE);

}

while(I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_RECEIVED) == ERROR);

// EV7事件被检测到,即数据寄存器有新的有效数据

*data = I2C_ReceiveData(EEPROM_I2C);

data++; // 指针指向下一个字节

numByteToRead--;

}

// 数据传输完成,产生结束信号

I2C_GenerateSTOP(EEPROM_I2C, ENABLE);

// 重新配置Ack使能,以便下次通讯

I2C_AcknowledgeConfig(EEPROM_I2C, ENABLE);

}


接下来,就是硬件i2c要注意的几个地方。事实上,直接按如下代码操作,程序会直接卡死:


int main(void)

{

uint8_t readData[10] = {0};

USART_config();

I2C_EE_config();

printf("这是一个IIC通讯实验n");

// 写入一个字节

EEPROM_Byte_Write(11, 0X55);

// 读取数据

EEPROM_Read(11, readData, 1);

printf("n接收到的数据为: %#X", readData[0]);

while (1)

{

}

}

为什么?


因为我们是写完立刻读取,而没有缓冲时间。实际上,之前我在EEPROM那篇博客中,有强调一个问题。EEPROM需要写入时间,在这期间内,不响应外部发送的数据,因此,不会产生应答。而在我们的读取函数中,(实际上是发送起始信号后,再发送设备地址然后等待响应),由于EEPROM还在写入,不会响应读取函数中的应答,所以程序会在这里卡死掉while()死循环。

https://blog.csdn.net/dingyc_ee/article/details/100042665


程序卡死的地方:


// 从EEPROM读取多个字节

void EEPROM_Read(uint8_t addr, uint8_t *data, uint8_t numByteToRead)

{

// 产生起始信号

I2C_GenerateSTART(EEPROM_I2C, ENABLE);

 

while(I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT) == ERROR);

// EV5事件被检测到,发送设备地址(直接填入EEPROM的设备地址:EEPROM_ADDR)

I2C_Send7bitAddress(EEPROM_I2C, EEPROM_ADDR, I2C_Direction_Transmitter);

    // 程序会一直卡死在接下来这个while中

while(I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) == ERROR);


解决的方案,就是上面提到的确认轮询。通过不断的产生起始 + 设备地址信号,并检测EEPROM的应答,来判断EEPROM是否已经写入完成,如果应答,则表示写入完成,产生结束条件。这里还有几个操蛋的地方,事件检测函数直接不能用(程序中注释的代码,一用就会出错,我也不知道为什么,好像说是因为这个函数会检测好几个标志位),只能检测响应的标志位。确认轮询的代码如下:


// 确认轮询,等待EEPROM完成写入时序的操作(不断产生:起始 + 设备地址)

void EEPROM_WaitForWriteEnd(void)

{

do 

{

// 产生起始信号

I2C_GenerateSTART(EEPROM_I2C, ENABLE);

// 这个函数会出错 while(I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT) == ERROR);

while(I2C_GetFlagStatus(EEPROM_I2C, I2C_FLAG_SB) == RESET);

// EV5事件被检测到,发送设备地址(直接填入EEPROM的设备地址:EEPROM_ADDR)

I2C_Send7bitAddress(EEPROM_I2C, EEPROM_ADDR, I2C_Direction_Transmitter);

}

// 如果EEPROM不响应,则一直产生起始信号,直到响应为止

// 这个函数会出错 while(I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) == ERROR);

while (I2C_GetFlagStatus(EEPROM_I2C, I2C_FLAG_ADDR) == RESET);

// 如果检测到EEPROM的响应,认为内部写入时序完成,产生结束信号

I2C_GenerateSTOP(EEPROM_I2C, ENABLE);

}


然后在主函数中进行调用,结果正确:


int main(void)

{

uint8_t readData[10] = {0};

USART_config();

I2C_EE_config();

printf("这是一个IIC通讯实验n");

// 写入一个字节

EEPROM_Byte_Write(11, 0X55);

// 确认轮询操作,等待EEPROM写入完成

EEPROM_WaitForWriteEnd();

// 读取数据

EEPROM_Read(11, readData, 1);

printf("n接收到的数据为: %#X", readData[0]);

while (1)

{

}

}


串口输出数据如下:

测试读两个数据:


int main(void)

{

uint8_t readData[10] = {0};

USART_config();

I2C_EE_config();

printf("这是一个IIC通讯实验n");

// 写入一个字节

EEPROM_Byte_Write(11, 0X55);

// 确认轮询操作,等待EEPROM写入完成

EEPROM_WaitForWriteEnd();

EEPROM_Byte_Write(12, 0X56);

EEPROM_WaitForWriteEnd();

// 读取数据

EEPROM_Read(11, readData, 2);

printf("接收到的数据为: %#X, %#Xn", readData[0], readData[1]);

while (1);

}


结果如下,说明多字节读取函数正确。

接下来实现页写入的函数(每次不能超过8字节,否则会卷回来到本页起始,覆盖掉之前数据)


// 向EEPROM写入多个字节(页写入),最大不能超过8字节

void EEPROM_Page_Write(uint8_t addr, uint8_t *data, uint8_t numByteToRead)

{

// 产生起始信号

I2C_GenerateSTART(EEPROM_I2C, ENABLE);

 

while(I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT) == ERROR);

// EV5事件被检测到,发送设备地址(直接填入EEPROM的设备地址:EEPROM_ADDR)

I2C_Send7bitAddress(EEPROM_I2C, EEPROM_ADDR, I2C_Direction_Transmitter);

while(I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) == ERROR);

// EV6事件被检测到,发送要操作的存储单元地址

I2C_SendData(EEPROM_I2C, addr);

while(I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTING) == ERROR);

while (numByteToRead)

{

// EV8事件被检测到

I2C_SendData(EEPROM_I2C, *data);

// DR不再填入新的数据,移位寄存器发送完最后一个字节,结束

while(I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED) == ERROR);

data++; // 指针指向下一个字节

numByteToRead--;

}

// 数据传输完成,产生结束信号

I2C_GenerateSTOP(EEPROM_I2C, ENABLE);

}


测试代码:


int main(void)

{

uint8_t i = 0;

uint8_t readData[8] = {0};

uint8_t writeData[8] = {1, 2, 3, 4, 5, 6, 7, 8};

USART_config();

I2C_EE_config();

printf("这是一个IIC通讯实验n");

EEPROM_Page_Write(16, writeData, 8);

 

// 确认轮询操作,等待EEPROM写入完成

EEPROM_WaitForWriteEnd();

 

EEPROM_Read(16, readData, 8);

for (i = 0; i < 8; i++)

{

printf("%4d", readData[i]);

}

printf("n");

while (1);

}


测试结果:

接下来,演示的是一些很不好的操作,如果写入的数据超过一页,诗句是怎么被覆盖掉的?我们尝试向一页中写入10个数据,然后读取这一页(8)个数据,看看会有什么现象:


测试代码:


int main(void)

{

uint8_t i = 0;

uint8_t readData[8] = {0};

uint8_t writeData[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

USART_config();

I2C_EE_config();

printf("这是一个IIC通讯实验n");

EEPROM_Page_Write(16, writeData, 10);

 

// 确认轮询操作,等待EEPROM写入完成

EEPROM_WaitForWriteEnd();

 

EEPROM_Read(16, readData, 8);

for (i = 0; i < 8; i++)

{

printf("%4d", readData[i]);

}

printf("n");

while (1);

}


接下来就是见证奇迹的时刻,最后的9 10两个数据,把之前的1 2给覆盖掉了

还有一个问题,关于地址对齐。AT24C02每页为8字节,所以每页的起始地址为0 8 16...,如果我们以对齐的地址来写入,则数据和地址都是正确的。但是,如果我们错开地址,会如何?接下来,从地址2开始连续写入8字节,然后读取8字节,观察结果


测试代码:


int main(void)

{

uint8_t i = 0;

uint8_t readData[8] = {0};

uint8_t writeData[8] = {1, 2, 3, 4, 5, 6, 7, 8};

USART_config();

I2C_EE_config();

printf("这是一个IIC通讯实验n");

EEPROM_Page_Write(2, writeData, 8);

 

// 确认轮询操作,等待EEPROM写入完成

EEPROM_WaitForWriteEnd();

 

EEPROM_Read(2, readData, 8);

for (i = 0; i < 8; i++)

{

printf("%4d", readData[i]);

}

printf("n");

while (1);

}


测试结果:

可以看到,前面6个数据是正确的,而后2个数据是错误的。分析:后面两个数据,由于反卷,直接写到了第一页的开始两个字节,而我们读取到的数据,则是第二页的前2个数据,验证一下:


测试代码:


int main(void)

{

uint8_t i = 0;

uint8_t readData[8] = {0};

uint8_t writeData[8] = {1, 2, 3, 4, 5, 6, 7, 8};

USART_config();

I2C_EE_config();

 

printf("这是一个IIC通讯实验n");

 

// 读取第一页的前2个数据

EEPROM_Read(0, readData, 2);

// 读取第二页的前2个数据

EEPROM_Read(8, (readData + 2), 2);

printf("读取第一页的前2个数据n");

for (i = 0; i < 2; i++)

{

printf("%4d", readData[i]);

}

printf("n");

printf("读取第二页的前2个数据n");

for (i = 2; i < 4; i++)

{

printf("%4d", readData[i]);

}

printf("n");

while (1);

}


测试结果:

与设想的一致。7 8由于反卷,写入到了第一页的前两个字节。而我们读数据时,地址是直接递增的,所以在读取完前6个后,按顺序(不会反卷)读取第二页的数据,因此会导致出错。


那么,我们在使用EEPROM进行页写入时,一定要注意地址8字节对齐,否则会出错,而且极难被发现。

推荐阅读

史海拾趣

FRIWO公司的发展小趣事

FRIWO公司自成立以来,始终在技术领域保持领先地位。从便携式收录机问世之初,FRIWO就凭借其先进的技术在该领域崭露头角。随后,随着技术的不断进步,FRIWO迅速将业务扩展到Atari计算机、摄影机等新兴领域,并成功成为这些领域的电源解决方案提供商。近年来,FRIWO更是专注于移动电话充电器市场,凭借其卓越的技术实力和创新能力,成为了世界上移动电话充电器的最主要供应商。这一系列的成功,彰显了FRIWO在电子行业中的技术领先地位和多元化发展战略。

Dynachip Corp公司的发展小趣事

Dynachip Corp始终将创新作为公司发展的核心动力。他们不断投入研发资源,探索新的技术方向和应用场景。同时,他们还积极关注行业动态和市场趋势,及时调整产品策略和市场布局。这种持续创新的精神使Dynachip Corp能够保持在行业中的领先地位,并为未来的发展奠定了坚实的基础。展望未来,Dynachip Corp将继续秉承创新、品质、合作的理念,致力于成为全球领先的半导体企业。

DELTA公司的发展小趣事

为了更好地服务全球客户,Delta公司积极推进全球布局战略。公司在全球范围内设立了多个研发中心和生产基地,如中国大陆、中国台湾、美国、泰国、日本、墨西哥、印度、巴西以及欧洲等地。这些研发中心和生产基地不仅为Delta提供了强大的技术支持和生产能力,还使其能够更快速地响应市场需求,提供更贴近客户的服务。

AIRPAX公司的发展小趣事

近年来,随着数字化技术的快速发展,AIRPAX也开始积极探索数字化转型之路。公司加大了对智能化、自动化生产线的投入,提高了生产效率和质量。同时,AIRPAX还积极利用大数据、云计算等先进技术,对客户需求和市场趋势进行深入分析,以更好地满足客户需求并推动创新发展。

这些故事只是AIRPAX发展历程中的一部分,它们展示了AIRPAX如何在技术、市场、环保和创新等多个方面取得了显著成就。然而,随着电子行业的不断发展和竞争的加剧,AIRPAX仍需继续努力,以保持其在电气保护领域的领先地位。

DCX-CHOL Enterprises公司的发展小趣事

随着公司规模的扩大和市场竞争的加剧,DCX-CHOL Enterprises意识到品质管理的重要性。公司投入大量资源提升生产线自动化水平,引进先进的品质检测设备,并建立了严格的质量管理体系。这些措施有效地提高了产品的品质稳定性和可靠性,赢得了客户的信赖。同时,公司还注重员工培训和技能提升,培养了一支高素质、专业化的技术和管理团队。

Hama公司的发展小趣事

H&D Wireless公司成立于2009年,总部位于瑞典斯德哥尔摩。成立初期,公司专注于物联网技术的研发,致力于提供智能家居Wi-Fi组件和无线多媒体解决方案。2016年,H&D Wireless宣布获得了一笔300万美元的风险投资,投资方包括Blasieholmen Investment Group及旗下网络内的40名瑞典和欧洲企业家与私人投资家。这笔资金为公司后续的全球业务扩张和物联网云服务平台的发布奠定了坚实基础。

问答坊 | AI 解惑

TMS320LF240x_DSP应用程序设计教程

TMS320LF240x_DSP应用程序设计教程…

查看全部问答>

S3C2410管脚复用超级基础问题

新手提问,请勿取笑,呵呵。 S3C2410在管脚复用的时候有这样一段代码: //set GPG1 as EINT9 for CS8900A value = INREG32(&pOalPortRegs->GPGCON); OUTREG32(&pOalPortRegs->GPGCON,(value & ~(3…

查看全部问答>

Wince中使用指定名称无线网络

问题是这样的: 现在我用C#做了一个wince的项目,在手持机设备上运行的, 设备上就运行我的这个程序,什么网络,数据库连接,在程序一运行,全部加载完毕 就是让客户有傻瓜式的感觉,现在功能基本上都实现的, 但是如果周围有其余的无线网络,手持机就会 ...…

查看全部问答>

有个产品使用了一段时间后无法启动了,帮分析分析

产品最初正常工作,内核和应用程序都好的。过了一段时间后启动异常。 步骤是开机Bootloader启动,启动时加载了LOGO界面填充了液晶的显示缓冲区,后来就停在这里了。 我可以按照原来的方式烧内核,更换了画面也可以显示。 重烧了BOOTLOADER后也没 ...…

查看全部问答>

STTT系列热电偶温度传感器

STTT系列热电偶温度传感器   STTT系列热电偶温度传感器采用不锈钢外壳封装,内部填充导热材料和密封材料灌封而成,尺寸小巧,适用于仪器仪表,精密恒温设备等温度的测量。   综述   温度是表征物体冷热程度的物理量。温度只能通过物体 ...…

查看全部问答>

请问各位我用的是2812,但是定义了2维数组后,就会提示数据溢出,请问为什么?谢谢

我定义了 int        a[8][16]; int        b[8][16]; 但是在使用中只能用到a[2][16];b[2][16];如果大于就会编译通不过,提示h0溢出, 但是我查看变量时,每个变量又都会有地址,没有重叠,请问为什 ...…

查看全部问答>

LM3S系列的芯片对于芯片有固化驱动库和没有固化驱动库的区别

LM3S系列的芯片对于芯片有固化驱动库和没有固化驱动库的区别 今天在选型的时候发现,有一个区别,对于有Stellarisware in rom 和没有Stellarisware in rom的LM3S型号的芯片在使用驱动库函数的时候有什么区别吗,固化和没固化有什么优势吗?…

查看全部问答>

运放

本帖最后由 dontium 于 2015-1-23 13:12 编辑   嗨  大家好  能介绍基本简单的运放书籍吗?  谢了 …

查看全部问答>

beaglebone心得八:有人来了

我淘了红外释电模块今天,接上了。由于就三根线。所以很容易就接上了。 模块5V接SYS_5V 模块输出接P1_7 模块地接GND 当有人接近时,模块输了高电平。其实那人就是我。只是我得动才行。我不动它也不亮。原来是这样。 唉。我原来以为一直高电平 ...…

查看全部问答>