你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

STM32经验分享 第16章 通信—模拟I2C

[复制链接]
STMCU小助手 发布时间:2022-8-30 19:35
16.1 关于I2C
16.1.1 I2C协议

I²C(Inter-Integrated Circuit),常读作“I方C”,它是一种多主从架构串行通信总线。在1980年由飞利浦公司设计,用于让主板、嵌入式系统或手机连接低速周边设备。如今在嵌入式领域是非常常见通信协议,常用于MPU/MCU与外部设备连接通信、数据传输。

I²C由两条线组成,一条双向串行数据线SDA,一条串行时钟线SCL。每个连接到总线的设备都有一个独立的地址,主机可以通过该地址来访问不同设备。因为I²C协议比较简单,常常用GPIO来模拟I²C时序,这种方法称为模拟I²C。如果使用MCU的I²C控制器,设置好I²C控制器, I²C控制器就自动实现协议时序,这种方式称为硬件I²C。因为I²C设备的速率比较低,通常两种方式都可以,模拟I²C方便移植,硬件I²C工作效率相对较高。

关于I²C协议,通过下面例子进行一个形象的比喻方便大家理解,如图 16.1.1 所示,老师(MCU)将球(数据)传给众多学生中的一个(众多外设设备中的一个)。
%YND%}Q`{_{YL6R25LM%N[C.png


图 16.1.1 I²C协议比喻



首先老师将球踢给某学生,即主机发送数据给从机,步骤如下:

1) 老师:开始了(start);

2) 老师:A!我要发球给你!(地址/方向);

3) 学生A:到!(回应);

4) 老师把球发出去(传输);

5) A收到球之后,应该告诉老师一声(回应);

6) 老师:结束(停止);



接着老师让学生把球传给自己,即从机发送数据给主机,步骤如下:

1) 老师:开始了(start);

2) 老师:B!把球发给我!(地址/方向);

3) 学生B:到!

4) B把球发给老师(传输);

5) 老师收到球之后,给B说一声,表示收到球了(回应);

6) 老师:结束(停止)。



从上面的例子可知,都是老师(主机)主导传球,按照规范的流程(通信协议),以保证传球的准确性,收发球的流程总结如下:

① 老师说开始了,表示开始信号(start);

② 老师提醒某个学生要发球,表示发送地址和方向(address/read/write);

③ 该学生回应老师(ack);

④ 老师发球/接球,表示数据的传输;

⑤ 收到球要回应:回应信号(ACK);

⑥ 老师说结束,表示IIC传输结束(P)。



以上就是I²C的传输协议,如果是软件模拟I²C,需要依次实现每个步骤。因此,还需要知道每一步的具体细节,比如什么时候的数据有效,开始信号怎么表示。



数据有效性

I²C由两条线组成,一条双向串行数据线SDA,一条串行时钟线SCL。SDA线上的数据必须在时钟的高电平周期保持稳定,数据线的高或低电平状态只有在 SCL 线的时钟信号是低电平时才能改变。换言之,SCL为高电平时表示有效数据,SDA为高电平表示“1”,低电平表示“0”;SCL为低电平时表示无效数据,此时SDA会进行电平切换,为下次数据表示做准备。数据有效性示意图如图 16.1.2 所示。

O1J[D%HRUEWX}~%LXAIL3U1.png


图 16.1.2 数据有效性



开始信号和结束信号

I²C起始信号(S):当SCL高电平时,SDA由高电平向低电平转换;

I²C停止信号(P):当SCL高电平时,SDA由低电平向高电平转换;

(R)Z}CEWQ3DL`85Y4}79ZL7.png


图 16.1.3 开始信号和结束信号



应答信号     
                     

I²C每次传输的8位数据,每次传输后需要从机反馈一个应答位,以确认从机是否正常接收了数据。当主机发送了8位数据后,会再产生一个时钟,此时主机放开SDA的控制,读取SDA电平,在上拉电阻的影响下,此时SDA默认为高,必须从机拉低,以确认收到数据。

C3L[O{`E4[39{JL~T6LNDES.png


图 16.1.4 数据传输格式和应答信号



完整传输流程  

I²C完整传输流程如下:

① SDA和SCL开始都为高,然后主机将SDA拉低,表示开始信号;

② 在接下来的8个时间周期里,主机控制SDA的高低,发送从机地址。其中第8位如果为0,表示接下来是写操作,即主机传输数据给从机;如果为1,表示接下来是读操作,即从机传输数据给主机;另外,数据传输是从最高位到最低位,因此传输方式为MSB(Most Significant Bit)。

③ 总线中对应从机地址的设备,发出应答信号;

④ 在接下来的8个时间周期里,如果是写操作,则主机控制SDA的高低;如果是读操作,则从机控制SDA的高低;

⑤ 每次传输完成,接收数据的设备,都发出应答信号;

⑥ 最后,在SCL为高时,主机由低拉高SDA,表示停止信号,整个传输结束;


6(2[DKU6YG]]34I8SPMHCUS.png


图 16.1.5 I2C传输时序



16.1.2 EEPROM介绍


EEPROM的全称是“电可擦除可编程只读存储器”,即Electrically Erasable Programmable Read-Only Memory。通常用于存放用户配置信息数据,比如在开发板首次运行时,需要屏幕校准,校准后的配置信息就可以保存在EEPROM里,开发板断电后配置信息不丢失,下次启动,开发板自动读取EEPROM的校准配置信息,就不需要重新校准。

EEPROM和Flash的本质上是一样的,Flash包括MCU内部的Flash和外部扩展的Flash,本开发板就有一个SPI接口的外部Flash(W25Q64),在后面SPI接口再讲解。从功能上,Flash通常存放运行代码,运行过程中不会修改,而EEPROM存放用户数据,可能会反复修改。从结构上,Flash按扇区操作,EEPROM通常按字节操作。两者区别这里不再过多赘述,读者理解EEPROM在嵌入式中扮演的角色即可。



结构组成


EEPROM类型众多,其中比较常见是AT24Cxx系列,从命名上看,AT24Cxx中xx的单位是K Bit,如AT24C08,其存储容量为8K Bit。本开发板上的EEPROM型号为AT24C02,其存储容量为2K Bit,2*1024=2048 Bit。

对于AT24C01/02,每页大小为8 Byte,对于AT24C04/08/16,每页大小为16 Byte。如图 16.1.6 所示,AT24C02由32页(Page)组成,每一页由8个字节(Byte)组成,每个Byte由8位(Bit)组成,Bit为最小存储单位,存放1个0或1。

3%EN%$T6C]@CBMA7CO_2G.png


图 16.1.6 AT24C02结构示意图



设备地址  


I²C设备都会有一个设备地址,不同容量的AT24C02,设备地址定义会有所差异,由芯片数据手册《AT24Cxx.pdf》可知,如图 16.1.7 所示。

ATQJQHJOXZTSM{E`D0)}P_I.png


图 16.1.7 AT24Cxx设备地址定义



AT24C02的容量为2K,对应上图中的第一行,高四位固定为“1010”,中间三位由A2、A1、A0引脚的电平决定,比如A2~0引脚全接地,则值为“000”,最后的最低位为读写位,0代表写命令,1代表读命令。

A2、A1、A0引脚电平需要由原理图决定,假设全接电源地,则如果需要向AT24C02写数据,则发送地址“1010 0000”,如果需要向AT24C02读数据,则发送地址“1010 0001”。

假设开发板有多个AT24C02挂在同一I²C总线上,通过这个规则,只需设计电路时,让A2、A1、A0引脚电平不同,即可区分两个AT24C02。

对于容量再大一点的AT24Cxx系列,比如AT24C04,器件地址由A2、A1引脚决定,数据空间有P0决定。比如对AT24C04的0~2K空间操作,则P0为0,对2K~4K空间操作,则P0为1。



写AT24Cxx  

AT24Cxx支持字节写模式和页写模式。字节写模式是一个地址一个数据的写;页写模式是连续写数据,一个地址多个数据的写,但是页写模式不能自动跨页,如果超出一页长度,超出的数据会覆盖原先写入的数据。

如图 16.1.8 所示,为AT24Cxx字节写模式的时序,在MCU发出开始信号(Start)后,发出8 Bit的设备地址信息(图中读写位为低电平,即写数据),待收到AT24Cxx应答信号后,再发出要写的数据地址,再次等待AT24Cxx应答,最后发出8 Bit数据写数据,待AT24Cxx应答后,发出停止信号(Stop),完成一次单字节写数据。

(U9)P5LJ$(8$H}A$VJP8]MI.png


图 16.1.8 AT24Cxx字节写模式时序

AT24C02容量为2K,因此数据地址范围为0x00~0xFF,即0~255,每个数据地址每次写1Byte,即8bit,也就刚好256*8=2048Bit。对于1K容量的产品,数据地址范围为0x00~0x7F,最高位不会用到,因此图中数据地址的最高位为“*”,意思是对于1K容量的产品,该位无需关心。

])0U)7VGW6RCHO}CE@BGDON.png


图 16.1.9 单字节写模流程图

图 16.1.10 为AT24Cxx的页写模式时序,与字节写模式的差异在于,不是只发送1Byte数据,而是任意多个。需要注意,该模式不能跨页写,遇到跨页时,需要重新发送完整的时序。

RZOEPVCBR2YI8[ARD52~E.png


图 16.1.10 AT24Cxx页写模式时序

值得一提的是,《AT24Cxx.pdf》里提到每次写完之后,再到下次写之前,需要间隔5ms时间,以确保上次写操作在芯片内部完成,如图 16.1.11 所示。

5J]FGYZLQ~S}%$E0}AVTNJG.png


图 16.1.11 AT24Cxx写间隔



读AT24Cxx

AT24Cxx支持当前地址读模式、随机地址读模式和顺序读模式。当前地址读模式就是在上一次读/写操作之后的最后位置,继续读出数据,比如上次读/写在地址n,接下来可以直接从n+1处读出数据;随机地址读模式是指定数据地址,然后读出数据;顺序读模式是连续读出多个数据。

在当前地址读模式下,无需发送数据地址,数据地址为上一次读/写操作之后的位置,时序如图 16.1.12 所示,注意在结尾,主机接收数据后,无需产生应答信号。

NFL_VKNSVYJ]JOD4WTU.png


图 16.1.12 AT24Cxx当前地址读模式

在随机地址读模式下,需要先发送设备地址,待读的数据地址,接着再重新发出开始信号,设备地址,读出数据,时序如图 16.1.13 所示。

GWA$ZUK`194_RL]5SU)]P40.png


图 16.1.13 AT24Cxx随机地址读模式



在顺序读模式下,需要先从当前地址读模式或随机地址读模式启动,随后便可连续读多个数据,时序如图 16.1.14 所示

_QE_AYTXQV@X7JO@6_4%N8X.png


图 16.1.14 AT24Cxx顺序读模式





16.2 硬件设计
如图 16.2.1 为开发板EEPROM部分的原理图,U6为AT24C02芯片,它的A0、A1、A2都接地,因此该设备地址为“1010 000X”,当读该设备时,X为1,写该设备时,X为0。

U4的7脚为写保护引脚(Write Protect,WP),当该引脚为高,则禁止写AT24C02,这里直接拉低WP,任何时候都可直接写AT24C02。

此外,I2C的两个脚SCL和SDA都进行了上拉处理,从而保证I2C总线空闲时,两根线都必须为高电平。如果没有上拉,在主机发送完数据后,放开SDA,此时SDA的电平状态不确定,可能为高,也可能为低,无法确定是从机拉低给出应答信号。

结合原理图可知,PB6作为了I2C1的SCL,PB7作为了I2C1的SDA。

0RV87Z7$Y8LWZ7F_{ICHENF.png


图 16.2.1 EEPROM模块原理图



16.3 软件设计
16.3.1 软件设计思路

实验目的:本实验通过GPIO模拟I2C总线时序,对EEPROM设备AT24C02进行读写操作。

1) 引脚初始化:GPIO端口时钟使能、GPIO引脚设置为输入/输出模式(PB6、PB7);

2) 封装I2C每个环节的时序函数:起始信号、响应信号、读写数据、停止信号;

3) 使用I2C协议函数,实现对AT24C02的读写;

4) 主函数,每按一次按键,写一次AT24C02,接着读出来验证是否和写的数据一致;

本实验配套代码位于“5_程序源码\8_通信—模拟I2C\”。



16.3.2 软件设计讲解

1) GPIO选择与接口定义

首先定义SCL和SDA引脚,引脚的高低电平宏定义,如代码段 16.3.1 所示。

代码段 16.3.1 模拟I2C引脚相关定义(driver_i2c.h)

  1. /************************* I2C 硬件相关定义 *************************/

  2. #define ACK                 (0)

  3. #define NACK                (1)



  4. #define SCL_PIN             GPIO_PIN_6

  5. #define SCL_PORT            GPIOB

  6. #define SCL_PIN_CLK_EN()    __HAL_RCC_GPIOB_CLK_ENABLE()



  7. #define SDA_PIN             GPIO_PIN_7

  8. #define SDA_PORT            GPIOB

  9. #define SDA_PIN_CLK_EN()    __HAL_RCC_GPIOB_CLK_ENABLE()



  10. #define SCL_H()             HAL_GPIO_WritePin(SCL_PORT, SCL_PIN, GPIO_PIN_SET)

  11. #define SCL_L()             HAL_GPIO_WritePin(SCL_PORT, SCL_PIN, GPIO_PIN_RESET)

  12. #define SCL_INPUT()         HAL_GPIO_ReadPin(SCL_PORT, SCL_PIN)



  13. #define SDA_H()             HAL_GPIO_WritePin(SDA_PORT, SDA_PIN, GPIO_PIN_SET)

  14. #define SDA_L()             HAL_GPIO_WritePin(SDA_PORT, SDA_PIN, GPIO_PIN_RESET)

  15. #define SDA_INPUT()         HAL_GPIO_ReadPin(SDA_PORT, SDA_PIN)
复制代码

接着将两个GPIO引脚初始化,使能引脚时钟,先默认设置为输出模式。SCL引脚为时钟信号,始终为输出模式,SDA引脚为数据引脚,可能输出或者输入,因此还需要编写函数实现输入、输出的切换,如代码段 16.3.2 所示。

代码段 16.3.2 I2C引脚初始化(driver_i2c.c)

  1. /*
  2. *  函数名:void I2C_Init(void)
  3. *  输入参数:
  4. *  输出参数:无
  5. *  返回值:无
  6. *  函数作用:初始化模拟I2C的引脚为输出状态且SCL/SDA都初始为高电平
  7. */
  8. void I2C_Init(void)
  9. {
  10.     GPIO_InitTypeDef GPIO_InitStruct = {0};

  11.     SCL_PIN_CLK_EN();
  12.     SDA_PIN_CLK_EN();

  13.     GPIO_InitStruct.Mode      = GPIO_MODE_OUTPUT_PP;
  14.     GPIO_InitStruct.Pull      = GPIO_NOPULL;
  15.     GPIO_InitStruct.Speed     = GPIO_SPEED_FREQ_HIGH;

  16.     GPIO_InitStruct.Pin       = SCL_PIN;
  17.     HAL_GPIO_Init(SCL_PORT, &GPIO_InitStruct);

  18.     GPIO_InitStruct.Pin       = SDA_PIN;
  19.     HAL_GPIO_Init(SDA_PORT, &GPIO_InitStruct);

  20.     SCL_H();
  21.     SDA_H();
  22. }

  23. /*
  24. *  函数名:static void I2C_SDA_OUT(void)
  25. *  输入参数:
  26. *  输出参数:无
  27. *  返回值:无
  28. *  函数作用:配置SDA引脚为输出
  29. */
  30. static void I2C_SDA_OUT(void)
  31. {
  32.     GPIO_InitTypeDef GPIO_InitStruct = {0};

  33.     GPIO_InitStruct.Mode      = GPIO_MODE_OUTPUT_PP;
  34.     GPIO_InitStruct.Pull      = GPIO_PULLUP;
  35.     GPIO_InitStruct.Speed     = GPIO_SPEED_FREQ_HIGH;

  36.     GPIO_InitStruct.Pin       = SDA_PIN;
  37.     HAL_GPIO_Init(SDA_PORT, &GPIO_InitStruct);
  38. }

  39. /*
  40. *  函数名:static void I2C_SDA_IN(void)
  41. *  输入参数:
  42. *  输出参数:无
  43. *  返回值:无
  44. *  函数作用:配置SDA引脚为输入
  45. */
  46. static void I2C_SDA_IN(void)
  47. {
  48.     GPIO_InitTypeDef GPIO_InitStruct = {0};

  49.     GPIO_InitStruct.Mode      = GPIO_MODE_INPUT;
  50.     GPIO_InitStruct.Speed     = GPIO_SPEED_FREQ_HIGH;

  51.     GPIO_InitStruct.Pin       = SDA_PIN;
  52.     HAL_GPIO_Init(SDA_PORT, &GPIO_InitStruct);
  53. }
复制代码


2) I2C时序函数

开始信号/结束信号

参考前面图 16.1.3 所示的开始信号和结束信号编写程序。对于开始信号,首先将SDA和SCL都拉高,随后SDA拉低,再SCL拉低。对于结束信号,首先拉低SDA,拉高SCL,再拉低SDA,代码如代码段 16.3.3 所示。

代码段 16.3.3 I2C开始信号和结束信号(driver_i2c.c)

  1. /*
  2. *  函数名:void I2C_Start(void)
  3. *  输入参数:
  4. *  输出参数:无
  5. *  返回值:无
  6. *  函数作用:I2C开始信号
  7. */
  8. void I2C_Start(void)
  9. {
  10.     I2C_SDA_OUT();

  11.     SCL_H();
  12.     I2C_Delay();

  13.     SDA_H();
  14.     I2C_Delay();

  15.     SDA_L();
  16.     I2C_Delay();

  17.     SCL_L();
  18.     I2C_Delay();
  19. }

  20. /*
  21. *  函数名:void I2C_Stop(void)
  22. *  输入参数:
  23. *  输出参数:无
  24. *  返回值:无
  25. *  函数作用:I2C停止信号
  26. */
  27. void I2C_Stop(void)
  28. {
  29.     I2C_SDA_OUT();

  30.     SDA_L();
  31.     I2C_Delay();

  32.     SCL_H();
  33.     I2C_Delay();

  34.     SDA_H();
  35.     I2C_Delay();
  36. }
复制代码

应答信号/非应答信号/等待应答信号

参考前面图 16.1.4 所示,编译应答信号,如代码段 16.3.4 所示。

代码段 16.3.4 应答/非应答/等待应答信号(driver_i2c.c)

  1. /*
  2. *  函数名:void I2C_ACK(void)
  3. *  输入参数:
  4. *  输出参数:无
  5. *  返回值:无
  6. *  函数作用:I2C发出应答信号
  7. */
  8. void I2C_ACK(void)
  9. {
  10.     I2C_SDA_OUT();

  11.     SCL_L();
  12.     I2C_Delay();

  13.     SDA_L();
  14.     I2C_Delay();

  15.     SCL_H();
  16.     I2C_Delay();

  17.     SCL_L();
  18.     I2C_Delay();
  19. }

  20. /*
  21. *  函数名:void I2C_NACK(void)
  22. *  输入参数:
  23. *  输出参数:无
  24. *  返回值:无
  25. *  函数作用:I2C发出非应答信号
  26. */
  27. void I2C_NACK(void)
  28. {
  29.     I2C_SDA_OUT();

  30.     SCL_L();
  31.     I2C_Delay();

  32.     SDA_H();
  33.     I2C_Delay();

  34.     SCL_H();
  35.     I2C_Delay();

  36.     SCL_L();
  37.     I2C_Delay();
  38. }

  39. /*
  40. *  函数名:uint8_t I2C_GetACK(void)
  41. *  输入参数:
  42. *  输出参数:无
  43. *  返回值:1无应答,0有应答
  44. *  函数作用:I2C等待从机的应答信号
  45. */
  46. uint8_t I2C_GetACK(void)
  47. {
  48.     uint8_t time = 0;
  49.     I2C_SDA_IN();

  50.     SCL_L();
  51.     I2C_Delay();

  52.     SDA_H();
  53.     I2C_Delay();

  54.     SCL_H();
  55.     I2C_Delay();

  56.     while(SDA_INPUT())
  57.     {
  58.         time++;
  59.         if(time>250)
  60.         {
  61.             SCL_L();
  62.             return 1;
  63.         }
  64.     }
  65.     SCL_L();

  66.     return 0;
  67. }
复制代码

8~23行:应答信号,在一个SDA时钟周期里,将SCL拉低;

32~47行:非应答信号,在一个SDA时钟周期里,将SCL拉高;

56~82行:等待应答信号,拉高SDA后放开SDA,读取SDA是否被拉低,如果拉低返回0,否则返回1;



发送/接收函数

最后还剩发送/接收函数,如代码段 16.3.5 所示。对于发送函数,控制SDA产生8个时钟周期,每个时钟周期里控制SDA高低电平发送1位数据。对于接收函数,控制SDA产生8个时钟周期,每个时钟周期里读取SDA高低电平接收1位数据。

代码段 16.3.5 发送/接收函数(driver_i2c.c)



  1. /*
  2. *  函数名:void I2C_SendByte(uint8_t data)
  3. *  输入参数:data->发送的数据
  4. *  输出参数:无
  5. *  返回值:无
  6. *  函数作用:I2C发送一个字节
  7. */
  8. void I2C_SendByte(uint8_t data)
  9. {
  10.     uint8_t cnt = 0;

  11.     I2C_SDA_OUT();

  12.     for(cnt=0; cnt<8; cnt++)
  13.     {
  14.         SCL_L();
  15.         I2C_Delay();

  16.         if(data & 0x80)
  17.         {
  18.             SDA_H();
  19.         }
  20.         else
  21.         {
  22.             SDA_L();
  23.         }
  24.         data = data<<1;
  25.         SCL_H();
  26.         I2C_Delay();
  27.     }

  28.     SCL_L();
  29.     I2C_Delay();
  30.     I2C_GetACK();
  31. }

  32. /*
  33. *  函数名:uint8_t I2C_ReadByte(uint8_t ack)
  34. *  输入参数:ack->发送的应答标志,1应答,0非应答
  35. *  输出参数:无
  36. *  返回值:返回读到的字节
  37. *  函数作用:I2C读出一个字节
  38. */
  39. uint8_t I2C_ReadByte(uint8_t ack)
  40. {
  41.     uint8_t cnt;
  42.     uint8_t data = 0xFF;

  43.     SCL_L();
  44.     I2C_Delay();

  45.     for(cnt=0; cnt<8; cnt++)
  46.     {
  47.         SCL_H();                 //SCL高(读取数据)
  48.         I2C_Delay();

  49.         data <<= 1;
  50.         if(SDA_INPUT())
  51.         {
  52.             data |= 0x01;        //SDA高(数据为1)
  53.         }
  54.         SCL_L();
  55.         I2C_Delay();
  56.     }
  57.     //发送应答信号,为低代表应答,高代表非应答
  58.     if(ack == 0)
  59.     {
  60.         I2C_ACK();
  61.     }
  62.     else
  63.     {
  64.         I2C_NACK();
  65.     }
  66.     return data;                 //返回数据
  67. }
复制代码

14~31行:循环8次,每次循环:

16行:先拉低SCL;

19~26行:将输入的数据data与0x08且运算,得到最高位的值,从而控制SDA输出对应的高、低电平;

27行:将data左移一位,得到次高位;

29行:拉高SCL,让SDA处于稳定期,从设备即可获取SDA的值;

35行:等待从设备的应答信号;

53~65行:循环8次,每次循环:

55行:先拉高SCL,此时认为从设备控制SDA电平,处于稳定期;

58行:将data左移1位,以确保收到数据按最高位在前存放;

59~62行:读取SDA电平,如果为高,保存到data当前最低位,否则data最低位默认为0;

63行:SCL拉低,此时从设备继续控制SDA电平变化

66~74行:根据传入的参数,决定是否发送应答信号;



整个I2C协议函数中,经常用到“ I2C_Delay()”来实现SCL时钟周期。对于AT24Cxx,由其芯片手册可知,时钟脉冲宽度(Clock Pulse Width)需要大于5us,也就是SCL如果刚变为高电平,需要等待至少5us才能变为低电平,因此定义“ I2C_Delay()”为5us以上即可。

  1. #define I2C_Delay()     us_timer_delay(5)  // Clock Pulse Width >5us
复制代码

这里的“us_timer_delay()”可以由定时器提供,也可以使用循环提供,前者精度更高,效果更好。定时器的介绍在后面章节,本章不作分析,延时函数的两者方式如代码段 16.3.6 所示。

代码段 16.3.6 延时函数的实现(driver_timer.c)

  1. #if 0
  2. /*
  3. *  函数名:void us_timer_delay(uint16_t t)
  4. *  输入参数:t-延时时间us
  5. *  输出参数:无
  6. *  返回值:无
  7. *  函数作用:定时器实现的延时函数,延时时间为t us,为了缩短时间,函数体使用寄存器操作,用户可对照手册查看每个寄存器每一位的意义
  8. */
  9. void us_timer_delay(uint16_t t)
  10. {
  11.     uint16_t counter = 0;
  12.     __HAL_TIM_SET_AUTORELOAD(&htim, t);
  13.     __HAL_TIM_SET_COUNTER(&htim, counter);
  14.     HAL_TIM_Base_Start(&htim);
  15.     while(counter != t)
  16.     {
  17.         counter = __HAL_TIM_GET_COUNTER(&htim);
  18.     }
  19.     HAL_TIM_Base_Stop(&htim);
  20. }
  21. #else
  22. /*
  23. *  函数名:void us_timer_delay(uint16_t t)
  24. *  输入参数:t-延时时间us
  25. *  输出参数:无
  26. *  返回值:无
  27. *  函数作用:延时粗略实现的延时函数,延时时间为t us
  28. */
  29. void us_timer_delay(uint16_t t)
  30. {
  31.     uint16_t counter = 0;

  32.     while(t--)
  33.     {
  34.         counter=10;

  35.         while(counter--) ;
  36.     }
  37. }
  38. #endif
复制代码


3) AT24C02读写函数

编写好I2C协议函数后,参考AT24C02手册编写读写数据函数,如代码段 16.3.7 所示。

代码段 16.3.7 读写AT24C02一字节数据(driver_eeprom.c)

  1. /*
  2. *  函数名:uint8_t EEPROM_WriteByte(uint16_t addr, uint8_t data)
  3. *  输入参数:addr -> 写一个字节的EEPROM初始地址
  4. *            data -> 要写的数据
  5. *  输出参数:无
  6. *  返回值:无
  7. *  函数作用:EEPROM写一个字节
  8. */
  9. void EEPROM_WriteByte(uint16_t addr, uint8_t data)
  10. {
  11.     /* 1. Start */
  12.     I2C_Start();

  13.     /* 2. Write Device Address */
  14.     I2C_SendByte( EEPROM_DEV_ADDR | EEPROM_WR );

  15.     /* 3. Data Address */
  16.     if(EEPROM_WORD_ADDR_SIZE==0x08)
  17.     {
  18.         I2C_SendByte( (uint8_t)(addr & 0x00FF) );
  19.     }
  20.     else
  21.     {
  22.         I2C_SendByte( (uint8_t)(addr>>8) );
  23.         I2C_SendByte( (uint8_t)(addr & 0x00FF) );
  24.     }

  25.     /* 4. Write a byte */
  26.     I2C_SendByte(data);

  27.     /* 5. Stop */
  28.     I2C_Stop();
  29. }

  30. /*
  31. *  函数名:uint8_t EEPROM_ReadByte(uint16_t addr, uint8_t *pdata)
  32. *  输入参数:addr -> 读一个字节的EEPROM初始地址
  33. *            data -> 要读的数据指针
  34. *  输出参数:无
  35. *  返回值:无
  36. *  函数作用:EEPROM读一个字节
  37. */
  38. void EEPROM_ReadByte(uint16_t addr, uint8_t *pdata)
  39. {
  40.     /* 1. Start */
  41.     I2C_Start();

  42.     /* 2. Write Device Address */
  43.     I2C_SendByte( EEPROM_DEV_ADDR | EEPROM_WR );

  44.     /* 3. Data Address */
  45.     if(EEPROM_WORD_ADDR_SIZE==0x08)
  46.     {
  47.         I2C_SendByte( (uint8_t)(addr & 0x00FF) );
  48.     }
  49.     else
  50.     {
  51.         I2C_SendByte( (uint8_t)(addr>>8) );
  52.         I2C_SendByte( (uint8_t)(addr & 0x00FF) );
  53.     }

  54.     /* 4. Start Again */
  55.     I2C_Start();

  56.     /* 5. Write Device Address Read */
  57.     I2C_SendByte( EEPROM_DEV_ADDR | EEPROM_RD );

  58.     /* 6.Read a byte */
  59.     *pdata = I2C_ReadByte(NACK);

  60.     /* 7. Stop */
  61.     I2C_Stop();
  62. }
复制代码

参加前面图 16.2.1 和图 16.1.13 所示的介绍时序,编写AT24C02一字节读写程序。

9~33行:写AT24C02一字节数据;

12行:发送I2C开始信号;

15行:发送AT24C02的设备地址,最后一位表示写操作;

18~26行:根据EEPROM型号,调用不同的数据地址长度设置函数(AT24C01/02为8位,AT24C04/08/16为16位);

29行:发送数据;

32行:发送I2C停止信号;

43~73行:读AT24C02一字节数据;

46行:发送I2C开始信号;

49行:发送AT24C02的设备地址,最后一位表示写操作(接下来要写数据地址);

52~60行:根据EEPROM型号,调用不同的数据地址长度设置函数(AT24C01/02为8位,AT24C04/08/16为16位);

63行:再次发送I2C开始信号;

66行:发送AT24C02的设备地址,最后一位表示读操作;

69行:读取AT24C02数据,且无需ACK;

72行:发送I2C停止信号;

实现了对AT24C02单字节的读写,还需要实现多字节的读写。多字节读写可以通过AT24Cxx的页写模式和顺序读模式,实现多个数据的连续读写。在页写模式时,需要程序上设置,不能跨页写,这里简单处理,直接多次调用前面的单次读写即可,如代码段 16.3.8 所示。

代码段 16.3.8 读写AT24C02多字节数据(driver_eeprom.c)

  1. /*
  2. *  函数名:void EEPROM_Write_NBytes(uint16_t addr, uint8_t *pdata, uint16_t sz)
  3. *  输入参数:addr -> 写一个字节的EEPROM初始地址
  4. *            data -> 要写的数据指针
  5. *            sz   -> 要写的字节个数
  6. *  输出参数:无
  7. *  返回值:无
  8. *  函数作用:EEPROM写N个字节
  9. */
  10. void EEPROM_Write_NBytes(uint16_t addr, uint8_t *pdata, uint16_t sz)
  11. {
  12.     uint16_t i = 0;

  13.     for(i=0; i<sz; i++)
  14.     {
  15.         EEPROM_WriteByte(addr, pdata<i>);
  16.         addr++;
  17.         HAL_Delay(10); // Write Cycle Time 5ms
  18.     }
  19. }

  20. /*
  21. *  函数名:void EEPROM_Read_NBytes(uint16_t addr, uint8_t *pdata, uint16_t sz)
  22. *  输入参数:addr -> 读一个字节的EEPROM初始地址
  23. *            data -> 要读的数据指针
  24. *            sz   -> 要读的字节个数
  25. *  输出参数:无
  26. *  返回值:无
  27. *  函数作用:EEPROM读N个字节
  28. */
  29. void EEPROM_Read_NBytes(uint16_t addr, uint8_t *pdata, uint16_t sz)
  30. {
  31.     uint16_t i = 0;

  32.     for(i=0; i<sz; i++)
  33.     {
  34.         EEPROM_ReadByte(addr, &pdata<i>);
  35.         addr++;
  36.     }
  37. }</i></i>
复制代码

需要注意的是,AT24Cxx每次写操作后,有一个写间隔,需要间隔5ms以上,因此在写多个字节时,每次写完都需要延时5ms以上。



4) 主函数控制逻辑

在主函数里,每按一下按键,调用“EEPROM_Write_Nbytes()”对AT24C02写一串数据,再调用“EEPROM_Read_Nbytes()”读出该数据,如代码段 16.3.9 所示。

代码段 16.3.9 主函数控制逻辑(main.c)

  1. // 初始化I2C
  2.    I2C_Init();

  3.    while(1)
  4.    {
  5.        if(key_flag) // 按键按下
  6.        {
  7.            key_flag = 0;

  8.            printf("\n\r");
  9.            printf("Start write and read eeprom.\n\r");

  10.            // 读写一串字符,并打印
  11.            EEPROM_Write_NBytes(0, tx_buffer, sizeof(tx_buffer)); // 写数据
  12.            HAL_Delay(1);

  13.            EEPROM_Read_NBytes(0, rx_buffer, sizeof(tx_buffer));  // 读数据
  14.            HAL_Delay(1);

  15.            printf("EEPROM Write: %s\n\r", tx_buffer);
  16.            printf("EEPROM Read : %s\n\r", rx_buffer);

  17.            memset((uint8_t*)rx_buffer, 0, sizeof(rx_buffer));   // 清空接收的数据
  18.        }
  19. }
复制代码


16.4 实验效果
本实验对应配套资料的“5_程序源码\8_通信—模拟I2C\”。打开工程后,编译,下载,按下按键KEY,即可看到串口如图 16.4.1 所示。

QFS63RP3__5)}U79`W0)EWH.png


图 16.4.1 模拟I2C读写AT24C02数据


作者:攻城狮子黄


收藏 评论0 发布时间:2022-8-30 19:35

举报

0个回答

所属标签

相似分享

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
st-img 微信公众号
st-img 手机版