1)实验平台:正点原子stm32f103战舰开发板V4
2)平台购买地址:https://detail.tmall.com/item.htm?id=609294757420
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/thread-340252-1-1.html#
本章,我们将介绍如何使用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 硬件设计
图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驱动。
/* 引脚 定义 */
#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通信接口的,可以尝试去驱动它们,这样才能学以致用。