stm32专题十七:深度解析 stm32 硬件iic (i2c)

最新更新时间:2021-10-22来源: eefocus关键字:stm32  硬件iic 手机看文章 扫描二维码
随时随地手机看文章

首先是配置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字节对齐,否则会出错,而且极难被发现。

关键字:stm32  硬件iic 编辑:什么鱼 引用地址:http://news.eeworld.com.cn/mcu/ic552248.html

上一篇:stm32专题十七:AT24C02
下一篇: stm32专题十四:存储器介绍

推荐阅读

STM32处理器输入捕获分析
/比较模式寄存器TIMx_CCMR1(当然还有CCMR2,两个寄存器配置CH1~4的输入输出)捕获作为输入,因此我们只用图片中寄存器的后下部分;CC1S[1:0] : (1)决定定时为输出(比较)或输入(捕获)模式;(2)决定IC1信号源的选择(后边详细介绍);IC1PSC[1:0]:选择对IC1的分频模式(但是这个分频和定时器TIMx_PSC寄存器的分频并不太一样,但功能是一样的)IC1F[3:0]:输入捕获1滤波器(详细功能可以参考STM32手册,这里这个功能不作为细说,本博文也用不到)(2)捕获/比较使能寄存器TIMx_CCER1使能寄存器中除了对输入使能的控制之外,还有对输入输出极性的控制;(此时为输入模式下)CCxP
发表于 2021-11-29
<font color='red'>STM32</font>处理器输入捕获分析
STM32定时器输出比较(PWM)
前言:1.本博文基于ARM Cortex-M3内核的STM32F103ZET6处理器芯片和标准3.5.0库函数;2.不介绍PWM的基础概念,但是需要知道一点的是,PWM是输出比较的一种特例;3.如有不足指出,还望前辈多多指教;4.要想学会这个知识点,必须要掌握下面这位博友写的博客里的几个概念,不然后来很有可能会懵逼;http://blog.sina.com.cn/s/blog_3ba262a10101esd1.htmlⅠ 定时器和PWM(1)8个定时器中,除了TIM6和TIM7,其他定时器都可以产生PWM输出;(2)高级定时器TIM1和TIM8可以同时输出7路PWM(CH1~7,共7个通道),通用定时器同时可产生4个PWM输出(CH
发表于 2021-11-29
<font color='red'>STM32</font>定时器输出比较(PWM)
STM32处理器定时器分析(实现一个定时中断)
前言:1.本博文基于STM32F103ZET6芯片和ST官方提供的3.5.0库函数;2.学习过51单片机学习STM32定时器会容易理解一些,但是两个定时器的实力有很大的距离;3.定时器是难点也是重点;本博文以通用定时器TIM2~TIM5来说明;例程是TIM36.定时器结构稍复杂,寄存器较多,本博文根据一个简单的定时器中断实验展开,所列出的相关寄存器是专门针对本次实验来说的,其他不相关定时器不再列出;一 STM32定时器的分类1.高级定时器: TIM1和TIM82.通用定时器: TIM2,3,4,5;3.基本定时器: TIM6和TIM7;二 通用定时器的功能(1)16位向上,向下,向上/向下自动装载寄存器(TIMx_CNT)(向上
发表于 2021-11-29
<font color='red'>STM32</font>处理器定时器分析(实现一个定时中断)
STM32的USART分析
串口框图工程建立后,需要在工程中添加stm32f10x_usart.h和stm32f10x_usart.c文件;串口设置的一般步骤①串口时钟使能,GPIO时钟使能②串口复位③GPIO端口模式设置④串口参数初始化⑤开启中断并初始化NVIC(需要开启中断时开启)⑥使能串口⑦编写中断处理函数串口设置的相关函数以及解释1.串口使能RCC_APB2PeriphClockCmd(RCC_APB2Periph_USARTx,ENABLE);2.串口复位解释一下复位:当外设出现异常的时候通过复位设置实现该外设的复位,然后重新设置这个外设达到让其重新工作的目的。一般在系统刚开始的时候,都会执行复位该外设的操作。下面是复位所需的函数;void USAR
发表于 2021-11-29
<font color='red'>STM32</font>的USART分析
STM32处理器 RTC分析
前言:1.博客基于ARM Cortex-M3内核的STM32F103ZET6和标准3.5.0库;2.如有不足之处,还请多多指教一 RTC是什么?1. 从结构上讲就是一个独立的定时器;2. 从功能上来说就是为系统提供系统掉电不复位的日历时间;RTC分为两个完全能独立的部分:1. APB1接口;2. RTC核心;功能:(1)APB1总线连接APB1接口并负责驱动APB1接口,接口内部包含一组16位寄存器,可以通过APB1总线对其进行读写操作。(2)RTC核心由RTC20位预分频模块和32位可编程计数器模块组成;Ⅰ RTC预分频模块包含一个20位的可编程分频器RTC_DIV。预分频模块为32位计数器模块提供时基单元,这个很重要;预分频器内
发表于 2021-11-29
<font color='red'>STM32</font>处理器 RTC分析
STM32 Systick分析
为了两种情况:使用UCOSⅡ和不使用UCOSⅡ的情况程序开始之前先解释一下:下面的延时函数代码支持UCOSⅡ下使用,它可以和UCOSⅡ共用systick定时器;在UCOSⅡ下系统不许有一个节拍,而这个节拍必须是固定的,不能被打断,否则就不准了;举个例子:比如每5ms一个节拍,那么直接由OS_TICK_PER_SEC = 200即可,stm32下systick提供这节拍;由上可知:UCOSⅡ下的systick不能随意被更改;此时如果要sistick做delay_us和delay_ms的延时;这事有一个时钟摘取的办法(以delay_us(50)为例):刚进入函数之后,先计算出需要延时的这段时间内systick需要建多少数,假设系统时钟
发表于 2021-11-29
<font color='red'>STM32</font> Systick分析
小广播
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2021 EEWORLD.com.cn, Inc. All rights reserved