【正点原子STM32连载】第三十五章 IIC实验 摘自【正点原子】STM32F103 战舰开发指南V1.2

news2024/12/26 11:08:18

1)实验平台:正点原子stm32f103战舰开发板V4
2)平台购买地址:https://detail.tmall.com/item.htm?id=609294757420
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/thread-340252-1-1.html#

第三十五章 IIC实验

本章,我们将介绍如何使用STM32F103的普通IO口模拟IIC时序,并实现和24C02之间的双向通信,并把结果显示在TFTLCD模块上。本章分为如下几个小节:
35.1 IIC及24C02介绍
35.2 硬件设计
35.3 程序设计
35.4 下载验证

35.1 IIC及24C02介绍
35.1.1 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总线挂载多个器件的示意图,如下图所示:
在这里插入图片描述

图35.1.1.1 IIC总线挂载多个器件
下面来学习IIC总线协议,IIC总线时序图如下所示:
在这里插入图片描述

图35.1.1.2 IIC总线时序图
为了便于大家更好的了解IIC协议,我们从起始信号、停止信号、应答信号、数据有效性、数据传输以及空闲状态等6个方面讲解,大家需要对应图35.1.1.2的标号来理解。
① 起始信号
当SCL为高电平期间,SDA由高到低的跳变。起始信号是一种电平跳变时序信号,而不是一个电平信号。该信号由主机发出,在起始信号产生后,总线就处于被占用状态,准备数据传输。
② 停止信号
当SCL为高电平期间,SDA由低到高的跳变。停止信号也是一种电平跳变时序信号,而不是一个电平信号。该信号由主机发出,在停止信号发出后,总线就处于空闲状态。
③ 应答信号
发送器每发送一个字节,就在时钟脉冲9期间释放数据线,由接收器反馈一个应答信号。 应答信号为低电平时,规定为有效应答位(ACK简称应答位),表示接收器已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。
观察上图标号③就可以发现,有效应答的要求是从机在第9个时钟脉冲之前的低电平期间将SDA线拉低,并且确保在该时钟的高电平期间为稳定的低电平。如果接收器是主机,则在它收到最后一个字节后,发送一个NACK信号,以通知被控发送器结束数据发送,并释放SDA线,以便主机接收器发送一个停止信号。
④ 数据有效性
IIC总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。数据在SCL的上升沿到来之前就需准备好。并在下降沿到来之前必须稳定。
⑤ 数据传输
在I2C总线上传送的每一位数据都有一个时钟脉冲相对应(或同步控制),即在SCL串行时钟的配合下,在SDA上逐位地串行传送每一位数据。数据位的传输是边沿触发。
⑥ 空闲状态
IIC总线的SDA和SCL两条信号线同时处于高电平时,规定为总线的空闲状态。此时各个器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。
了解前面的知识后,下面介绍一下IIC的基本的读写通讯过程,包括主机写数据到从机即写操作,主机到从机读取数据即读操作。下面先看一下写操作通讯过程图,见图35.1.3.2所示:
在这里插入图片描述

图35.1.3.2 写操作通讯过程图
主机首先在IIC总线上发送起始信号,那么这时总线上的从机都会等待接收由主机发出的数据。主机接着发送从机地址+0(写操作)组成的8bit数据,所有从机接收到该8bit数据后,自行检验是否是自己的设备的地址,假如是自己的设备地址,那么从机就会发出应答信号。主机在总线上接收到有应答信号后,才能继续向从机发送数据。注意:IIC总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。
接着讲解一下IIC总线的读操作过程,先看一下读操作通讯过程图,见图35.1.3.3所示。
在这里插入图片描述

图35.1.3.3 读操作通讯过程图
主机向从机读取数据的操作,一开始的操作与写操作有点相似,观察两个图也可以发现,都是由主机发出起始信号,接着发送从机地址+1(读操作)组成的8bit数据,从机接收到数据验证是否是自身的地址。 那么在验证是自己的设备地址后,从机就会发出应答信号,并向主机返回8bit数据,发送完之后从机就会等待主机的应答信号。假如主机一直返回应答信号,那么从机可以一直发送数据,也就是图中的(n byte + 应答信号)情况,直到主机发出非应答信号,从机才会停止发送数据。
24C02的数据传输时序是基于IIC总线传输时序,下面讲解一下24C02的数据传输时序。
35.1.2 24C02简介
24C02是一个2K bit的串行EEPROM存储器,内部含有256个字节。在24C02里面还有一个8字节的页写缓冲器。该设备的通信方式IIC,通过其SCL和SDA与其他设备通信,芯片的引脚图如图35.1.2.1所示。
在这里插入图片描述

图35.1.2.1 24C02引脚图
上图的WP引脚是写保护引脚,接高电平只读,接地允许读和写,我们的板子设计是把该引脚接地。每一个设备都有自己的设备地址,24C02也不例外,但是24C02的设备地址是包括不可编程部分和可编程部分,可编程部分是根据上图的硬件引脚A0、A1和A2所决定。设备地址最后一位用于设置数据的传输方向,即读操作/写操作,0是写操作,1是读操作,具体格式如下图35.1.2.2所示:
在这里插入图片描述

图35.1.2.2 24C02设备地址格式图
根据我们的板子设计,A0、A1和A2均接地处理,所以24C02设备的读操作地址为:0xA1;写操作地址为:0xA0。
在前面已经说过IIC总线的基本读写操作,那么我们就可以基于IIC总线的时序的上,理解24C02的数据传输时序。
下面把实验中到的数据传输时序讲解一下,分别是对24C02的写时序和读时序。24C02写时序图见图35.1.4.1所示。
在这里插入图片描述

图35.1.4.1 24C02写时序图
上图展示的主机向24C02写操作时序图,主机在IIC总线发送第1个字节的数据为24C02的设备地址0xA0,用于寻找总线上找到24C02,在获得24C02的应答信号之后,继续发送第2个字节数据,该字节数据是24C02的内存地址,再等到24C02的应答信号,主机继续发送第3字节数据,这里的数据即是写入在第2字节内存地址的数据。主机完成写操作后,可以发出停止信号,终止数据传输。
上面的写操作只能单字节写入到24C02,效率比较低,所以24C02有页写入时序,大大提高了写入效率,下面看一下24C02页写时序图,图35.1.4.2所示。
在这里插入图片描述

图35.1.4.2 24C02页写时序
在单字节写时序时,每次写入数据时都需要先写入设备的内存地址才能实现,在页写时序中,只需要告诉24C02第一个内存地址1,后面数据会按照顺序写入到内存地址2,内存地址3等,大大节省了通信时间,提高了时效性。因为24C02每次只能8bit数据,所以它的页大小也就是1字节。页写时序的操作方式跟上面的单字节写时序差不多,所以不作过多解释了。参考以上说明去理解页写时序。
说完两种写入方式之后,下面看一下图35.1.2.2关于24C02的读时序。
在这里插入图片描述

图35.1.2.2 24C02读时序图
24C02读取数据的过程是一个复合的时序,其中包含写时序和读时序。先看第一个通信过程,这里是写时序,起始信号产生后,主机发送24C02设备地址0xA0,获取从机应答信号后,接着发送需要读取的内存地址;在读时序中,起始信号产生后,主机发送24C02设备地址0xA1,获取从机应答信号后,接着从机返回刚刚在写时序中内存地址的数据,以字节为单位传输在总线上,假如主机获取数据后返回的是应答信号,那么从机会一直传输数据,当主机发出的是非应答信号并以停止信号发出为结束,从机就会结束传输。
目前大部分MCU都带有IIC总线接口,STM32F1也不例外。但是这里我们不使用STM32F1的硬件IIC来读写24C02,而是通过软件模拟。ST为了规避飞利浦IIC专利问题,将STM32的硬件IIC设计的比较复杂,而且稳定性不怎么好,所以这里我们不推荐使用。有兴趣的读者可以自行研究STM32F1的硬件IIC的使用。
用软件模拟IIC,最大的好处就是方便移植,同一个代码兼容所有MCU,任何一个单片机只要有IO口,就可以很快的移植过去,而且不需要特定的IO口。而硬件IIC,则换一款MCU,基本上就得重新移植,这也是我们推荐使用软件模拟IIC的另外一个原因。
35.2 硬件设计

  1. 例程功能
    每按下KEY1,MCU通过IIC总线向24C02写入数据,通过按下KEY0来控制24C02读取数据。同时在LCD上面显示相关信息。LED0闪烁用于提示程序正在运行。
  2. 硬件资源
    1)LED灯
    LED0 – PB5
    2)独立按键
    KEY0 – PE4
    KEY1 – PE3
    3)EEPROM AT24C02
    4)正点原子 2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
    5)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)( USMART使用)
  3. 原理图
    我们主要来看看24C02和开发板的连接,如下图所示:
    在这里插入图片描述

图35.2.1 IIC连接原理
24C02的SCL和SDA分别连接在STM32的PB6和PB7上。本实验通过软件模拟IIC信号建立起与24C02的通信,进行数据发送与接收,使用按键KEY0和KEY1去触发,LCD屏幕进行显示。
35.3 程序设计
IIC实验中使用的是软件模拟IIC,所以用到的是HAL中GPIO相关函数,前面也有介绍到,这里就不做展开了。下面介绍一下使用IIC传输数据的配置步骤:
使用IIC传输数据的配置步骤
1)使能IIC的SCL和SDA对应的GPIO时钟。
本实验中IIC使用的SCL和SDA分别是PB6和PB7,因此需要先使能GPIOB的时钟,代码如下:
__HAL_RCC_GPIOB_CLK_ENABLE(); /* 使能GPIOB时钟 */
2)设置对应GPIO工作模式(SCL推挽输出 SDA开漏输出)
SDA线的GPIO模式使用开漏输出模式(硬件已接外部上拉电阻,对于F4以上板子也可以用内部的上拉电阻),而SCL线的GPIO模式使用推挽输出模式,通过函数HAL_GPIO_Init设置实现。
3)参考IIC总线协议,编写信号函数(起始信号,停止信号,应答信号)
起始信号:SCL为高电平时,SDA由高电平向低电平跳变。
停止信号:SCL为高电平时,SDA由低电平向高电平跳变。
应答信号:接收到IC数据后,向IC发出特定的低电平脉冲表示已接收到数据。
4)编写IIC的读写函数
通过参考时序图,在一个时钟周期内发送1bit数据或者读取1bit数据。读写函数均以一字节数据进行操作。
有了读和写函数,我们就可以对外设进行驱动了。
35.3.1 程序流程图
在这里插入图片描述

图35.3.1.1 IIC实验程序流程图
35.3.2 程序解析
本实验中,我们通过GPIO来模拟IIC,所以不需要使用FWLIB分组下添加HAL库文件支持。实验工程中,我们新增了myiic.c存放iic底层驱动代码,24cxx.c文件存放24C02驱动。

  1. IIC底层驱动代码
    这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。IIC驱动源码包括两个文件:myiic.c和myiic.h。
    下面我们直接介绍IIC相关的程序,首先先介绍 myiic.h文件,其定义如下:
/* 引脚 定义 */
#define IIC_SCL_GPIO_PORT         	GPIOB
#define IIC_SCL_GPIO_PIN           	GPIO_PIN_6
#define IIC_SCL_GPIO_CLK_ENABLE()	do{ __HAL_RCC_GPIOB_CLK_ENABLE();}while(0)

#define IIC_SDA_GPIO_PORT        	GPIOB
#define IIC_SDA_GPIO_PIN            	GPIO_PIN_7
#define IIC_SDA_GPIO_CLK_ENABLE() 	do{ __HAL_RCC_GPIOB_CLK_ENABLE();}while(0)

/* IO操作 */
#define IIC_SCL(x)        do{ x ? \
          HAL_GPIO_WritePin(IIC_SCL_GPIO_PORT,IIC_SCL_GPIO_PIN, GPIO_PIN_SET):\
          HAL_GPIO_WritePin(IIC_SCL_GPIO_PORT,IIC_SCL_GPIO_PIN, GPIO_PIN_RESET);\
                             }while(0)       /* SCL */

#define IIC_SDA(x)        do{ x ? \
         HAL_GPIO_WritePin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN, GPIO_PIN_SET):\
         HAL_GPIO_WritePin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN, GPIO_PIN_RESET);\
                          }while(0)       /* SDA */
/* 读取SDA */
#define IIC_READ_SDA HAL_GPIO_ReadPin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN)

我们通过宏定义标识符的方式去定义SCL和SDA两个引脚,同时通过宏定义的方式定义了IIC_SCL() 和IIC_SDA()设置这两个管脚可以输出0或者1,主要还是通过HAL库的GPIO操作函数实现的。另外方便在iic操作函数中调用读取SDA管脚的数据,这里直接宏定义IIC_READ_SDA实现,在后面iic模拟信号实现中会频繁调用。
接下来我们看一下myiic.c代码中的初始化函数,代码如下:

/**
 * @brief      	初始化IIC
 * @param       	无
 * @retval    	无
 */
void iic_init(void)
{
    GPIO_InitTypeDef gpio_init_struct;

    IIC_SCL_GPIO_CLK_ENABLE();  		/* SCL引脚时钟使能 */
    IIC_SDA_GPIO_CLK_ENABLE();  		/* SDA引脚时钟使能 */

    gpio_init_struct.Pin = IIC_SCL_GPIO_PIN;
    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(IIC_SCL_GPIO_PORT, &gpio_init_struct);	/* SCL */

/* SDA引脚模式设置,开漏输出,上拉, 这样就不用再设置IO方向了, 开漏输出的时候(=1), 
也可以读取外部信号的高低电平 */
gpio_init_struct.Pin = IIC_SDA_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_OD;        		/* 开漏输出 */
HAL_GPIO_Init(IIC_SDA_GPIO_PORT, &gpio_init_struct);	/* SDA */
    iic_stop();     /* 停止总线上所有设备 */
}
在iic_init函数中主要工作就是对于GPIO的初始化,用于iic通信,不过这里需要注意的一点是SDA线的GPIO模式使用开漏模式,同时需要注意:STM32F103必须要外接上拉电阻!
接下来介绍在上面已经在文字上说明过的IIC模拟信号:起始信号、停止信号、应答信号,下面以代码方法实现,大家可以对着图去看代码,有利于理解。
/**
 * @brief     	IIC延时函数,用于控制IIC读写速度
 * @param      	无
 * @retval     	无
 */
static void iic_delay(void)
{
    delay_us(2);    /* 2us的延时, 读写速度在250Khz以内 */
}

/**
 * @brief     	产生IIC起始信号
 * @param      	无
 * @retval    	无
 */
void iic_start(void)
{
    IIC_SDA(1);
    IIC_SCL(1);
    iic_delay();
    IIC_SDA(0);     /* START信号: 当SCL为高时, SDA从高变成低, 表示起始信号 */
    iic_delay();
    IIC_SCL(0);     /* 钳住I2C总线,准备发送或接收数据 */
    iic_delay();
}

/**
 * @brief     	产生IIC停止信号
 * @param      	无
 * @retval     	无
 */
void iic_stop(void)
{
    IIC_SDA(0);     /* STOP信号: 当SCL为高时, SDA从低变成高, 表示停止信号 */
    iic_delay();
    IIC_SCL(1);
    iic_delay();
    IIC_SDA(1);     /* 发送I2C总线结束信号 */
    iic_delay();
}

在这里首先定义一个iic_delay函数,目的就是控制IIC的读写速度,通过示波器检测读写速度在250KHz内,所以一秒钟传送500Kb数据,换算一下即一个bit位需要2us,在这个延时时间内可以让器件获得一个稳定性的数据采集。
为了大家更加清晰了解代码实现的过程,下面单独把起始信号和停止信号从iic总线时序图中抽取出来,见图35.2.1.1。
在这里插入图片描述

图35.2.1.1 起始信号与停止信号图
iic_start函数中,通过调用myiic.h中通过宏定义好的可以输出高低电平的SCL和SDA来模拟iic总线中起始信号的发送,在SCL时钟线为高电平的时候,SDA数据线从高电平状态转化到低电平状态,最后拉低时钟线,准备发送或者接收数据。
iic_stop函数中,也是按着模拟iic总线中停止信号的逻辑,在SCL时钟线为高电平的时候,SDA数据线从低电平状态转化到高电平状态。
接下来讲解一下iic的发送函数,其定义如下:

/**
 * @brief    	IIC发送一个字节
 * @param       	data: 要发送的数据
 * @retval      	无
 */
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线 */
}

在iic的发送函数iic_send_byte中,我们把需要发送的数据作为形参,形参大小为1个字节。在iic总线传输中,一个时钟信号就发送一个bit,所以该函数需要循环八次,模拟八个时钟信号,才能把形参的8个位数据都发送出去。这里使用的是形参data和0x80与运算的方式,判断其最高位的逻辑值,假如为1即需要控制SDA输出高电平,否则为0控制SDA输出低电平。为了更好说明,数据发送的过程,单独拿出数据传输时序图,见图35.2.1.2。
在这里插入图片描述

图35.2.1.2数据传输时序图
通过上图就可以很清楚了解数据传输时的细节,经过第一步的SDA高低电平的确定后,接着需要延时,确保SDA输出的电平稳定,在SCL保持高电平期间,SDA线上的数据是有效的,此过程也是需要延时,使得从设备能够采集到有效的电平。然后准备下一位的数据,所以这里需要的是把data左移一位,等待下一个时钟的到来,从设备进行读取。把上述的操作重复8次就可以把data的8个位数据发送完毕,循环结束后,把SDA线拉高,等待接收从设备发送过来的应答信号。
接着讲解一下iic的读取函数iic_read_byte,它的定义如下:

/**
 * @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_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数据,把它作为返回值返回,然而按照时序图,作为主机就需要发送应答或者非应答信号,去回复从机。
上面提及到应答信号和非应答信号是在读时序中发生的,此外在写时序中也存在有一个信号响应,当发送完8bit数据后,这里是一个等待从机应答信号的操作,这里我们也定义了,下面看一下它们的定义:

/**
 * @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;
}

/**
 * @brief      	产生ACK应答
 * @param       	无
 * @retval      	无
 */
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();
}

/**
 * @brief    	不产生ACK应答
 * @param       	无
 * @retval      	无
 */
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_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拉高,呈高阻态,方便后续通信用到。
2. 24C02驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。24CXX驱动源码包括两个文件:24cxx.c和24cxx.h。
在上一小节已经对IIC协议中的需要用到的信号都用函数封装好了,那么现在就要定义符合24C02时序的函数。为了使代码功能更加健全,所以在24cxx.h中宏定义了不同容量大小的24C系列型号,具体定义如下:

#define AT24C01		127
#define AT24C02     	255
#define AT24C04     	511
#define AT24C08     	1023
#define AT24C16     	2047
#define AT24C32     	4095
#define AT24C64     	8191
#define AT24C128    	16383
#define AT24C256    	32767

/* 开发板使用的是24c02,所以定义EE_TYPE为AT24C02 */
#define EE_TYPE AT24C02
在24cxx.c文件中,读/写操作函数对于不同容量大小的24Cxx芯片都有相对应的代码块解决处理。下面先看一下at24cxx_write_one_byte函数,实现在AT24Cxx芯片指定地址写入一个数据,代码如下:

/**
 * @brief     	在AT24CXX指定地址写入一个数据
 * @param       	addr: 写入数据的目的地址
 * @param      	data: 要写入的数据
 * @retval     	无
 */
void at24cxx_write_one_byte(uint16_t addr, uint8_t data)
{
    /* 原理说明见:at24cxx_read_one_byte函数, 本函数完全类似 */
    iic_start();                		/* 发送起始信号 */


    if (EE_TYPE > AT24C16)      		/* 24C16以上的型号, 分2个字节发送地址 */
    {
        iic_send_byte(0XA0);			/* 发送写命令, IIC规定最低位是0, 表示写入 */
        iic_wait_ack();         		/* 每次发送完一个字节,都要等待ACK */
        iic_send_byte(addr >> 8);	/* 发送高字节地址 */
    }
    else 
    {   /* 发送器件 0XA0 + 高位a8/a9/a10地址,写数据 */
        iic_send_byte(0XA0 + ((addr >> 8) << 1));   
    }
    
    iic_wait_ack();             	/* 每次发送完一个字节,都要等待ACK */
    iic_send_byte(addr % 256);	/* 发送低位地址 */
iic_wait_ack();             	/* 等待ACK, 此时地址发送完成了 */
    
    /* 因为写数据的时候,不需要进入接收模式了,所以这里不用重新发送起始信号了 */
    iic_send_byte(data);   	/* 发送1字节 */
    iic_wait_ack();      	/* 等待ACK */
    iic_stop();            	/* 产生一个停止条件 */
    delay_ms(10);             /* 注意: EEPROM 写入比较慢,必须等到10ms后再写下一个字节 */
}

该函数的操作流程跟前面已经分析过的24C02单字节写时序一样,首先调用iic_start函数产生起始信号,然后调用iic_send_byte函数发送第1个字节数据设备地址,等待24Cxx设备返回应答信号;收到应答信号后,继续发送第2个1字节数据内存地址addr;等待接收应答后,最后发送第3个字节数据写入内存地址的数据data,24Cxx设备接收完数据,返回应答信号,主机调用iic_stop函数产生停止信号终止数据传输,最终需要延时10ms,等待eeprom写入完毕。
我们的函数兼容24Cxx系列多种容量,就在发送设备地址处做了处理,这里说一下为什么需要这样子设计。大家请看一下24Cxx芯片内存组织表,见表35.2.2.1所示。
芯片 页数 每页字节数 总的字节数 字节寻址地址线数量
AT24C01A 16 8 128 7
AT24C02 32 8 256 8
AT24C04 32 16 512 9
AT24C08A 64 16 1024 10
AT24C16A 128 16 2048 11
AT24C32 128 32 4096 12
AT24C64A 256 32 8192 13
表35.2.2.1 24Cxx芯片内存组织表
主机发送的设备地址和内存地址共同确定了要写入的地方,这里分析一下24C16的使用的是iic_send_byte(0XA0+((addr>>8)<<1))和iic_send_byte(addr % 256)确定写入位置,由于它内存大小一共2048字节,所以只需要定义11个寻址地址线,2048 = 2^11。主机下发读写命令的时候带了3位,后面再跟1个字节(8位)的地址,正好11位,就不需要再发后续的地址字节了。
而容量大于24C16的芯片,需要单独发送2个字节(甚至更多)的地址,如24C32,它的大小为4096,需要12个寻址地址线支持,4096 = 2^12。24C16是2个字节刚刚好,而它需要三个字节才能确定写入的位置。24C32芯片规定设备写地址0xA0/读地址0xA1,后面接着发送8位高地址,最后才发送8位低地址。与函数里面的操作是一致。
接下来看一下at24cxx_read_one_byte函数,其定义如下:

/**
 * @brief       	在AT24CXX指定地址读出一个数据
 * @param       	readaddr: 开始读数的地址
 * @retval      	读到的数据
 */
uint8_t at24cxx_read_one_byte(uint16_t addr)
{
    uint8_t temp = 0;
    iic_start();                /* 发送起始信号 */

    /* 根据不同的24CXX型号, 发送高位地址
     * 1, 24C16以上的型号, 分2个字节发送地址
     * 2, 24C16及以下的型号, 分1个低字节地址 + 占用器件地址的bit1~bit3位 用于表示高位地
址, 最多11位地址
     *    对于24C01/02, 其器件地址格式(8bit)为: 1  0  1  0  A2  A1  A0  R/W
     *    对于24C04,    其器件地址格式(8bit)为: 1  0  1  0  A2  A1  a8  R/W
     *    对于24C08,    其器件地址格式(8bit)为: 1  0  1  0  A2  a9  a8  R/W
     *    对于24C16,    其器件地址格式(8bit)为: 1  0  1  0  a10 a9  a8  R/W
     *    R/W      : 读/写控制位 0,表示写; 1,表示读;
     *    A0/A1/A2 : 对应器件的1,2,3引脚(只有24C01/02/04/8有这些脚)
     *    a8/a9/a10: 对应存储整列的高位地址, 11bit地址最多可以表示2048个位置,可以寻址
24C16及以内的型号
     */    
    if (EE_TYPE > AT24C16)    		/* 24C16以上的型号, 分2个字节发送地址 */
    {
        iic_send_byte(0XA0);    		/* 发送写命令, IIC规定最低位是0, 表示写入 */
        iic_wait_ack();          		/* 每次发送完一个字节,都要等待ACK */
        iic_send_byte(addr >> 8);	/* 发送高字节地址 */
    }
    else 
    {  /* 发送器件 0XA0 + 高位a8/a9/a10地址,写数据 */
        iic_send_byte(0XA0 + ((addr >> 8) << 1));   
    }
    
    iic_wait_ack();             		/* 每次发送完一个字节,都要等待ACK */
    iic_send_byte(addr % 256);  		/* 发送低位地址 */
    iic_wait_ack();             		/* 等待ACK, 此时地址发送完成了 */
    
    iic_start();                		/* 重新发送起始信号 */ 
    iic_send_byte(0XA1);        		/* 进入接收模式, IIC规定最低位是0, 表示读取 */
    iic_wait_ack();             		/* 每次发送完一个字节,都要等待ACK */
    temp = iic_read_byte(0);    		/* 接收一个字节数据 */
    iic_stop();                 		/* 产生一个停止条件 */
    return temp;
}

这里的函数的实现跟前面第35.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中读取到的数据。
为了方便检测24Cxx芯片是否正常工作,在这里也定义了一个检测函数,代码如下:

/**
 * @brief    	检查AT24CXX是否正常
 *   @note      	检测原理: 在器件的末地址写如0X55, 然后再读取, 如果读取值为0X55
 *              	则表示检测正常. 否则,则表示检测失败.
 * @param       	无
 * @retval      	检测结果
 *              	0: 检测成功
 *              	1: 检测失败
 */
uint8_t at24cxx_check(void)
{
    uint8_t temp;
    uint16_t addr = EE_TYPE;
    temp = at24cxx_read_one_byte(addr);		/* 避免每次开机都写AT24CXX */

    if (temp == 0X55)   						/* 读取数据正常 */
    {
        return 0;
    }
    else    									/* 排除第一次初始化的情况 */
    {
        at24cxx_write_one_byte(addr, 0X55);	/* 先写入数据 */
        temp = at24cxx_read_one_byte(255); 	/* 再读取数据 */
        if (temp == 0X55)return 0;
    }
    return 1;
}

学到这个地方相信大家,对于这个操作并不陌生了,在前面的RTC实验也有相似的操作,可以翻回去看看。这里利用的是EEPROM芯片掉电不丢失的特性,在第一次写入了某个值之后,再去读一下是否写入成功,这种方式去检测芯片是否正常工作。
此外方便多字节写入和读取,还定义了在指定地址读取指定个数的函数以及在指令地址写入指定个数的函数,代码如下:

/**
 * @brief     	在AT24CXX里面的指定地址开始读出指定个数的数据
 * @param       	addr    : 开始读出的地址 对24c02为0~255
 * @param       	pbuf    : 数据数组首地址
 * @param       	datalen : 要读出数据的个数
 * @retval      	无
 */
void at24cxx_read(uint16_t addr, uint8_t *pbuf, uint16_t datalen)
{
    while (datalen--)
    {
        *pbuf++ = at24cxx_read_one_byte(addr++);
    }
}

/**
 * @brief     	在AT24CXX里面的指定地址开始写入指定个数的数据
 * @param       	addr    : 开始写入的地址 对24c02为0~255
 * @param       	pbuf    : 数据数组首地址
 * @param       	datalen : 要写入数据的个数
 * @retval      	无
 */
void at24cxx_write(uint16_t addr, uint8_t *pbuf, uint16_t datalen)
{
    while (datalen--)
    {
        at24cxx_write_one_byte(addr, *pbuf);
        addr++;
        pbuf++;
    }
}
对于这两个函数都是调用前面的单字节操作函数去实现的,利用for循环,连续调用单字节操作函数去实现,这里就不多讲。
3. main.c代码
在main.c里面编写如下代码: 
const uint8_t g_text_buf[] = {"STM32 IIC TEST"};	/* 要写入到24c02的字符串数组 */
#define TEXT_SIZE       sizeof(g_text_buf)  			/* TEXT字符串长度 */

int main(void)
{
    uint8_t key;
    uint16_t i = 0;
    uint8_t datatemp[TEXT_SIZE];

    HAL_Init();                                	/* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9);    	/* 设置时钟, 72Mhz */
    delay_init(72);                            	/* 延时初始化 */
    usart_init(115200);                       	/* 串口初始化为115200 */
    usmart_dev.init(72);                      	/* 初始化USMART */
    led_init();                                 	/* 初始化LED */
    lcd_init();                                 	/* 初始化LCD */
    key_init();                                 	/* 初始化按键 */
    at24cxx_init();                            	/* 初始化24CXX */

    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, "KEY1:Write  KEY0:Read", RED);

    while (at24cxx_check())	/* 检测不到24c02 */
    {
        lcd_show_string(30, 130, 200, 16, 16, "24C02 Check Failed!", RED);
        delay_ms(500);
        lcd_show_string(30, 130, 200, 16, 16, "Please Check!      ", RED);
        delay_ms(500);
        LED0_TOGGLE();   		/* 红灯闪烁 */
    }

lcd_show_string(30, 130, 200, 16, 16, "24C02 Ready!", RED);

    while (1)
    {
        key = key_scan(0);

        if (key == KEY1_PRES)   /* KEY1按下,写入24C02 */
        {
            lcd_fill(0, 150, 239, 319, WHITE);  /* 清除半屏 */
            lcd_show_string(30, 150, 200, 16, 16, "Start Write 24C02....", BLUE);
            at24cxx_write(0, (uint8_t *)g_text_buf, TEXT_SIZE);
/* 提示传送完成 */
            lcd_show_string(30, 150, 200, 16, 16, "24C02 Write Finished!", BLUE);   
        }

        if (key == KEY0_PRES)   /* KEY0按下,读取字符串并显示 */
        {
            lcd_show_string(30, 150, 200, 16, 16, "Start Read 24C02.... ", BLUE);
            at24cxx_read(0, datatemp, TEXT_SIZE);
		   /* 提示传送完成 */
            lcd_show_string(30, 150, 200, 16, 16, "The Data Readed Is:  ", BLUE);   
/* 显示读到的字符串 */
            lcd_show_string(30, 170, 200, 16, 16, (char *)datatemp, BLUE); 
        }

        i++;

        if (i == 20)
        {
            LED0_TOGGLE();  /* 红灯闪烁 */
            i = 0;
        }

        delay_ms(10);
    }
}

main函数的流程大致是:在main函数外部定义要写入24C02的字符串数组g_text_buf。在完成系统级和用户级初始化工作后,检测24c02是否存在,然后通过KEY0去读取0地址存放数据并把数据显示在LCD上;另外还可以通过KEY1去0地址处写入g_text_buf数据并在LCD界面中显示传输中,完成后并显示“24C02 Write Finished!”。
35.4 下载验证
将程序下载到开发板后,可以看到LED0不停的闪烁,提示程序已经在运行了,先按下KEY1写入数据,然后再按KEY0读取数据,最终LCD显示的内容如图35.4.1所示:
在这里插入图片描述

图35.4.1 IIC实验程序运行效果图
假如大家需要验证24C02的自检函数,可以用跟杜邦线把PB6和PB7短接,重新上电看看是否能看到报错。
该实验还支持USMART,在这里我们可以方便测试24C02的读写功能,可以操作24C02的任意地址,不过在0~255这个范围,读写测试图如图35.4.2所示。
在这里插入图片描述

图35.4.2 24C02读写测试图
图中,我们首先调用at24cxx_read_one_byte函数在123地址处读取,获取的数据为0xFF。通过调用at24cxx_write_one_byte函数在123地址处写入0x12的值,然后继续调用at24cxx_read_one_byte函数在123地址处读取,获得0x12的值,表明实验成功。
至此,我们整个IIC实验就结束了,本章内容比较多,需要大家花多点时间去理解,一定要自己去用一下IIC通信协议。市面上很多器件都是具有IIC通信接口的,可以尝试去驱动它们,这样才能学以致用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/662075.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023年NPDP产品经理认证线上班,到这里

产品经理国际资格认证NPDP是新产品开发方面的认证&#xff0c;集理论、方法与实践为一体的全方位的知识体系&#xff0c;为公司组织层级进行规划、决策、执行提供良好的方法体系支撑。 【认证机构】 产品开发与管理协会&#xff08;PDMA&#xff09;成立于1979年&#xff0c;是…

docker 镜像制作 与 CI/CD

目录 镜像到底是什么&#xff1f; 使用docker创建镜像 步骤&#xff1a; 1、编辑Dockerfile&#xff08;Dockerfile是docker制作镜像的配方文件&#xff09; 2、编辑requirements.txt文件 3、编辑app.py文件&#xff0c;我们的程序文件 4、生成镜像文件 5、查看生成的镜…

这些软件,你知道几个呢?

软件分享一&#xff1a;情绪指压 情绪指压(MoodPress)是一款记录心情的应用&#xff0c;也是一款非常简单的减压游戏。可以根据自己现在的心情来决定指压的力度和时间&#xff08;压力越大或者时间越长越生气&#xff09;&#xff0c;适时释放&#xff0c;来判断和记录自己的心…

Oracle19c安装和远程访问设置

Oracle Database&#xff0c;又名Oracle RDBMS&#xff0c;或简称Oracle。是甲骨文公司的一款关系数据库管理系统。它是在数据库领域一直处于领先地位的产品。可以说Oracle数据库系统是世界上流行的关系数据库管理系统&#xff0c;系统可移植性好、使用方便、功能强&#xff0c…

cron表达式 字符含义 详细解释

cron表达式的组成 cron表达式是一个字符串&#xff0c;由6到7个字段组成&#xff0c;用空格分隔。 其中前6个字段是必须的&#xff0c;最后一个年是可选填的。 cron表达式的字段含义 字段允许值通用字符秒0-59* , - /分0-59时0-23日期1-31 ? L C W月1-12 JAN-DEC星期1-7 SU…

单片机原理及接口技术 - 第三版 张毅刚 习题答案

第 1 章 思考题及习题 1 参考答案 一、填空 1.除了单片机这一名称之外&#xff0c;单片机还可称为 微控制器 或 嵌入式控制器 2.单片机与普通微型计算机的不同之处在于其将 CPU、存储器、I/O 口三部分,通过内部总线连接一起&#xff0c;集成芯片上。 3. AT89S52 单片机工作频率…

高并发缓存实战RedisSon、性能优化

高并发缓存实战RedisSon、性能优化 分布式锁性能提升 1.数据冷热分离 对于经常访问的数据保留在redis缓存当中&#xff0c;不用带数据设置超时时间定期删除控制redis的大小 String productStr redisUtil.get(productCacheKey);if (!StringUtils.isEmpty(productStr)) {prod…

docker搭建nginx

一、安装Docker 1、安装&#xff1a; yum install docker 2、启动/停止/重启docker服务 service docker start service docker stop service docker restart 3、查看docker版本信息 docker version 4、查看所有docker镜像 docker images 二、安装Nginx 1、拉取Nginx镜像…

关于POL网络中的ODN部署方案,这些你都了解吗?

近年来&#xff0c;行业的智能化和信息化呈现加速发展趋势&#xff0c;高清视频会议、云服务、移动办公等应用几乎成为企业标配。与此同时&#xff0c;带宽的接入、升级和物联网融合等网络新要求也变得越来越迫切&#xff0c;网络架构升级成为企业解决网络难题的一个新选择。 …

Python基础(10)——Python条件语句

Python基础&#xff08;10&#xff09;——Python条件语句 文章目录 Python基础&#xff08;10&#xff09;——Python条件语句目标一. 了解条件语句二. if 语法2.1 语法2.2 快速体验 三. 实例&#xff1a;上网3.1 简单版3.2 进阶版 四. if...else...4.1 语法4.2 实用版&#x…

KSM01.2B-061C-35N-M1-HP2-SE-NN –D7-NN-FW

​ KSM01.2B-061C-35N-M1-HP2-SE-NN –D7-NN-FW KSM01.2B-061C-35N-M1-HP2-SE-NN –D7-NN-FW 集散控制的基本思想是集中管理&#xff0c;分散控制。即&#xff1a;将流程工业的自动控制过程与操作管理人员对自动控制过程的管理过程相对分离&#xff1b;流程工业的自动控制过程…

Dump寄存器使用、解析

前人种树&#xff0c;后人乘凉&#xff1b;创造不易&#xff0c;请勿迁移~ author daisy.skye的博客_CSDN博客-嵌入式,Qt,Linux领域博主 daisy.skye的博客_CSDN博客-嵌入式,Qt,Linux领域博主daisy.skye擅长嵌入式,Qt,Linux,等方面的知识https://blog.csdn.net/qq_40715266?t…

git的安装与配置教程-超详细版

一、git的安装 1、下载git git官网地址&#xff1a;https://git-scm.com/download/win/ 选择所需要的版本&#xff0c;进行下载。 2、下载完成之后&#xff0c;双击下载好的exe文件进行安装。 3、默认是C盘&#xff0c;推荐修改一下路径&#xff08;非中文并且没有空格&…

2021电工杯数学建模B题解题思路

目录 一、前言 二、问题背景 三、具体问题 四、解题思路 &#xff08;一&#xff09;整体思路 &#xff08;二&#xff09;问题一 &#xff08;三&#xff09;问题二 &#xff08;四&#xff09;问题三 &#xff08;五&#xff09;问题四 &#xff08;六&#xff09;…

浅谈kubernetes部署:UI部署

UI部署 镜像制作 登录私服 以阿里云docker私服举例 sudodockerlogin—usernameregistry.cn-beijing.aliyuncs.com 制作UI和静态页镜像 参考&#xff1a; 《前端镜像制作》 《openresty镜像制作》 修改yaml文件 vi/opt/kubernetes/ui.yaml 修改相应image值为您的镜像目录 部…

VTK学习之边缘检测(梯度算子)

参考博客&#xff1a;VTK修炼之道32&#xff1a;边缘检测_梯度算子_基于梯度的边缘检测算子_沈子恒的博客-CSDN博客 直接上源码&#xff1a; #include <vtkAutoInit.h> #include <vtkSmartPointer.h> #include <vtkJPEGReader.h> #include <vtkImageGra…

屏幕录制没有声音?快看看这2个方法!

案例&#xff1a;我今天尝试在电脑上进行屏幕录制&#xff0c;一开始一切都挺正常的。直到结束后&#xff0c;查看刚刚录制的视频发现没有声音。 【录屏只有画面没有声音&#xff0c;会影响视频的观感体验&#xff0c;甚至你根本不知道视频想表达的意思。那录屏的同时如何录制…

计算机基础--->网络(1)【分层模型、网络协议、HTTP等】

文章目录 网络分层模型OSI七层模型及其作用TCP/IP四层模型及作用为什么网络需要分层&#xff1f; 常见的网络协议应用层常见的协议传输层常见的协议网络层常见协议 从输入URL到页面展示的过程HTTP常见的状态码HTTP与HTTPS的区别HTTP是不保存状态的协议&#xff0c;如何保存用户…

华硕电脑怎么用U盘重装系统Win10?

华硕电脑怎么用U盘重装系统Win10&#xff1f;用户想用U盘给华硕电脑重装Win10系统&#xff0c;但不知道要怎么操作&#xff0c;这时候用户需要准备一台能够正常联网的华硕电脑&#xff0c;还有一个8G以上的U盘&#xff0c;最后根据小编分享的华硕电脑用U盘重装Win10系统教程操作…

MQTT Broker 规则引擎入门:快速指南

引言 规则引擎是一种能够根据输入数据按照预设规则进行决策或执行动作的软件系统。本文将向您介绍 EMQX MQTT Broker 的规则引擎功能&#xff0c;并阐述其在 MQTT 消息转换和数据集成方面的重要作用。同时&#xff0c;我们还将提供一份快速入门指南&#xff0c;通过实例帮助您…