STM32学习笔记---IIC与AT24C02

一、什么是IIC

1、I2C总线概念

I2C总线是由Philips公司开发的一种双向二线制同步串行半双工总线。它只需要两根线(SCL、SDA)即可在连接于总线上的器件之间传送信息。

2、主机与从机的关系

主器件用于启动总线传送数据,并产生时钟以开放传送的器件,此时任何被寻址的器件均被认为是从器件。在总线上主和从、发和收的关系不是恒定的,而取决于此时数据传送方向。如果主机要发送数据给从器件,则主机首先寻址从器件,然后主动发送数据至从器件,最后由主机终止数据传送;如果主机要接收从器件的数据,首先由主器件寻址从器件.然后主机接收从器件发送的数据,最后由主机终止接收过程。在这种情况下.主机负责产生定时时钟和终止数据传送。

3、主机的作用

3.1 波特率的选择

3.2 数据通信的方向

X:数据传输的方向 0:主机发送数据 1:主机接收数据

3.3 数据的发送和接收

写数据:开始--->发送从机地址+写(0)--->主发从收--->结束

读数据:开始--->发送从机地址+读(1)--->从发主收--->结束

注意:无论是开始还是结束都是由主机决定的

4、通信配置方式

        配置IIC通信的方式一般有两种,一种是配置通信控制器中的寄存器,利用IIC通信控制器驱动IO口产生通信协议时序,这种方式的优点就是稳定;;另一种就是IO口模拟通信时序,主要根据时序图利用IO口输出高低电平模仿通信时序,从而可以达到模拟通信时序的效果,并且这种方式的优点就是移植性好。由于考虑到后续程序的移植,所以IIC通信一般会使用IO口模拟时序的配置方式。两种方式具体的区别如下:

而此篇使用的是IO口模拟通信时序来配置IIC通信,为了更好的移植。 

二、如何配置IIC

1、IO口模拟通信时序

如何看时序图

规则:

因为一个通信协议规则是由时钟线和数据线一起体现出来的,即规则应该是时钟线和数据线结合看。

时钟线的某个电平状态态此时数据线是什么状态?

        规则①: 时钟线为低电平,数据线呈现出交叉状态

                        时钟线为低电平,可以写数据

                        想要写入数据,就要先把时钟线拉低

        规则②:时钟线为高电平,数据线呈现出平行状态

                        时钟线为高电平,可以读数据

                        想要读数据,就要先把时钟线拉高

看时序图方法:

        ①分清时钟线和数据线

        ②数据线数据段---------平行和交叉

        ③将时钟线与数据线结合--数据段规则         

        时钟线什么电平的时候可以写数据,时钟线什么电平的时候可以读数据

        ④看空闲状态

        ⑤看其他(从左到右看)

2、IO口模拟IIC时序

2.1  数据线的开漏类型

时钟线:是芯片提供时钟源,所以时钟线管脚为通用输出(推挽)

数据线:既能输入,又能输出,配置双向的,所以数据线配置为开漏输出

IIC的数据线为什么要配置开漏? 怎么输出?如何切换到输入?

首先,因为在输入时,输出禁止;而推挽输出既可以输出高电平又可以输出低电平,无法在输入时禁止输出;而开漏输出只能输出低电平,如果想输出高电平时,可以借助IIC的外接上拉电阻输出高电平;如果想切换到输入,就让输出数据寄存器ODR置1,使得输出禁止,这时就可以输入了。

开漏输出的优势:

  1. 防止短路
  2. 实现线与逻辑

外接上拉电阻与内置上拉电阻的区别

一、结构区别

内置上拉电阻:这是芯片本身自带的电阻,一般在输入端口内部连接。其电阻值固定,常见的有1KΩ、10KΩ等,具体值取决于芯片的设计。

外接上拉电阻:需要通过外部引脚连接到芯片上。其电阻值和型号可以根据实际需要选择,具有更高的灵活性。

二、使用区别

内置上拉电阻

优点:可以直接在硬件设计中使用,无需连接外部元器件,节省引脚资源和PCB面积,提高设计的灵活性。同时,简化了电路设计,降低了开发难度,适用于大多数应用场合。

缺点:电阻值固定,无法根据电路要求进行变化。在一些特殊场合下,如高精度模拟信号处理,内置上拉电阻的参数可能会产生影响。

外接上拉电阻

优点:电阻值可以根据实际需求进行选取,提高了电路的灵活性和适用性。尤其是在需要高精度信号处理的场合下,外接上拉电阻更具优势。

缺点:需要占用更多的引脚资源和PCB面积。

2.2 IIC的时序图

时序帧: 空闲段+起始段+数据段+应答段+停止段

空闲:

时钟线为高电平,数据线为高电平

起始信号:

时钟线为高电平,数据线为下降沿

数据段:

时钟线为低电平,数据线平行(可写)

时钟线为高电平,数据线交叉(可读)

停止信号:

时钟线为高电平,数据线为上升沿

应答:

IIC通信每传输一个字节(8位)就要有一个应答/不应答信号产生

主机是如何接收应答的?

①当主机作为发送数据端:---------主机检测应答/不应答信号

从机有IIC硬件电路,所以接收数据/检测应答--->从机自动完成

②当主机作为接收数据端:---------主机发送应答/不应答信号

从机有IIC硬件电路,所以发送数据/检测应答--->从机自动完成

 

应答/不应答信号总结:

①主机发送数据端

  1. 主机发送数据  //写程序
  2. 从机接收数据  //自动
  3. 从机发送应答信号/不应答信号 //自动
  4. 主机检测发送应答信号/不应答信号 //写程序

所以:检测应答/不应答函数

②主机接收数据端

  1. 从机发送数据  //自动
  2. 主机接收数据  //写程序
  3. 主机发送应答信号/不应答信号 //写程序
  4. 从机检测发送应答信号/不应答信号 //自动

所以:发送应答/不应答函数

 数据帧格式:

一帧数据帧的格式--->起始位+数据位+应答位+停止位 

总结:

空闲信号:

        IIC所用iO口初始化函数

起始信号:

        IIC起始信号函数

数据传输段:

        IIC发送一个字节函数

        IIC接收一个字节函数

应答段:

        发送应答/不应答信号函数

        检测应答/不应答函数

停止信号:

        IIC停止信号函数

3、程序设计

IIC标准时序图:

AT24Cxx时序图:

①起始信号函数:

时钟线拉低//为了改变数据线

数据线拉高//为下降沿做准备

时钟线拉高//起始信号的条件

延时5ms

数据线拉低//产生起始信号

延时5ms

时钟线拉低//安全作用

/*
函数名: iic_star
函数功能:IIC起始信号函数
返回值:void
形参:void
函数说明: 
				  
*/
void iic_star(void)
{
	//时钟线拉低   //为了可以改变数据线
	IIC_SCL_L;
	//数据线拉高   //为下降沿做准备
	IIC_SDA_OUT_H;
	
	//时钟线拉高   //起始信号的条件
	IIC_SCL_H;
	timer6_delay_us(5);
	//数据线拉低   //产生起始信号
	IIC_SDA_OUT_L;
	timer6_delay_us(5);
	
	//时钟线拉低 //安全作用
	IIC_SCL_L;
}

②停止信号函数:

时钟线拉低//为了改变数据线

数据线拉低//为上升沿做准备

时钟线拉高//停止信号的条件

延时5ms

数据线拉高//产生停止信号

延时5ms

/*
函数名: iic_stop
函数功能:IIC停止信号函数
返回值:void
形参:void
函数说明: 
				  
*/
void iic_stop(void)
{
	//时钟线拉低   //为了可以改变数据线
	IIC_SCL_L;
	//数据线拉低   //为上升沿做准备
	IIC_SDA_OUT_L;
	
	//时钟线拉高   //停止信号的条件
	IIC_SCL_H;
	timer6_delay_us(5);
	//数据线拉高   //产生停止信号
	IIC_SDA_OUT_H;
	timer6_delay_us(5);
}

③发送应答/不应答函数:

时钟线拉低//为了改变数据线

延时3ms

数据线拉低/拉高

延时2ms

时钟线拉高//主机帮助从机拉高时钟线,从机可以读取此位数据位

延时5ms

时钟线拉低 //安全作用

/*
函数名: iic_send_ack
函数功能:IIC发送应答/不应答函数
返回值:void
形参:u8 ack
函数说明: 传入1  主机停止接收数据
		   传入0  主机继续接收数据
*/
void iic_send_ack(u8 ack)
{
		//时钟线拉低   		//为了可以改变数据线
		IIC_SCL_L;
	  timer6_delay_us(3);
		//不应答--数据线拉高  应答数据线拉低
	  ack ? (IIC_SDA_OUT_H) : (IIC_SDA_OUT_L); 
		timer6_delay_us(2);
	  //时钟线拉高   		//产生应答\不应答信号的条件
		IIC_SCL_H;
		timer6_delay_us(5);
		//时钟线拉低			 //安全作用
		IIC_SCL_L;
		
}

接收应答\不应答函数:

 /*切换到输入模式*/

时钟线拉低//为了改变数据线

数据线拉高//为了关闭输出通道

/*读取应答信号*/

时钟线拉低//主机帮助从机拉低时钟线,从机自动发送应答/不应答信号

延时5ms

时钟线拉高//主机读取数据线

延时5ms

读取数据线   //读取数据不需要时间

时钟线拉低  //安全作用

返回应答

/*
函数名: iic_rec_ack
函数功能:IIC检测应答\不应答函数
返回值:u8
形参:void
函数说明: 
	        返回0   主机接收到从机的应答
			返回1   主机接收到从机的不应答
*/
u8 iic_rec_ack(void)
{
	  u8 ack;
	/*将数据线切换到输入*/
	//时钟线拉低   		//为了可以改变数据线
		IIC_SCL_L;
	//数据线拉高      //为了关闭输出通道
	  IIC_SDA_OUT_H;
	
	/*读取应答信号*/
	//时钟线拉低      //主机帮助从机拉低时钟线,从机自动发送应答/不应答信号
		IIC_SCL_L;
		timer6_delay_us(5);
	//时钟线拉高
		IIC_SCL_H;
	  timer6_delay_us(5);
	//读取数据线---读取数据不需要时间
		if(IIC_SDA_IN)
		{
			ack = 1;//不应答
		}
		else 
		{
			ack = 0;//应答
		}
	//时钟线拉低			 //安全作用
		IIC_SCL_L;
	
	  return ack;
	
}

⑤发送一个字节函数: 

时钟线拉低//为了改变数据线

延时3ms

/*循环一位一位的发送*/

时钟线拉低//为了可以改变数据线

延时3ms

根据要发送的数据的对应为是0还是1,决定时钟线拉高还是拉低

延时2ms

时钟线拉高//主机帮助从机拉高时钟线,从机可以读取此位数据位

延时5ms

下一位数据位

时钟线拉低  //安全作用

/*
函数名: iic_send_byte
函数功能:IIC发送一字节函数
返回值:void
形参:u8 data
函数说明: 		   

*/
void iic_send_byte(u8 data)
{
	  u8 i;
	
	//发一位接一位
	 for(i = 0;i < 8; i++)
	{
		//时钟线拉低   		//为了可以改变数据线
		IIC_SCL_L;
		timer6_delay_us(3);
		if(data & 0x80)
		{
			//数据线拉高
			IIC_SDA_OUT_H;
		}
		else
		{
			//数据线拉低
			IIC_SDA_OUT_L;
		}
		timer6_delay_us(2);
		//时钟线拉高
		IIC_SCL_H;//主机帮助从机拉高时钟线,从机可以读取此位数据位
		timer6_delay_us(5);
		data = data << 1;//下一位数据
	}
		//时钟线拉低			 //安全作用
		IIC_SCL_L;
	
}

⑥接收一个字节函数:

 /*切换到输入模式*/

时钟线拉低//为了改变数据线

数据线拉高//为了关闭输出通道

 /*读取数据*/

//循环-->一位一位的读取

时钟线拉低///主机帮助从机拉低时钟线,从机自动数据

延时5ms

时钟线拉高//主机读取此位数据

延时5ms

左移一位数据

读取数据线

时钟线拉低//安全作用

返回数据

/*
函数名: iic_rec_byte
函数功能:IIC接收一字节函数
返回值:u8 
形参:void
函数说明: 		

*/
u8 iic_rec_byte(void)
{
	u8 i;
	u8 data;
	/*将数据线切换到输入*/
	//时钟线拉低   		//为了可以改变数据线
	IIC_SCL_L;
	//数据线拉高      //为了关闭输出通道
	IIC_SDA_OUT_H;
	
	/*读取数据*/
	for(i = 0;i < 8;i++)
	{
		//时钟线拉低    //主机帮助从机拉低时钟线,从机改变数据线
		IIC_SCL_L;
		timer6_delay_us(5);
		//时钟线拉高    //主机读取此位数据位
		IIC_SCL_H;
		timer6_delay_us(5);
		data = data << 1;//低位补0
		//读取数据线
		if(IIC_SDA_IN)
		{
			data |= 0x01;
		}
		
	}
	//时钟线拉低			 //安全作用
		IIC_SCL_L;
	
		return data;
	
	
	
}

三、具体使用IIC

使用IIC通信是基于EEPROM的存储器上的,故需要了解什么是AT24C02

EEPROM(Electrically Erasable Programmable Read-Only Memory)是指带电可擦可编程只读存储器,它是一种非易失性存储器,能够在系统断电后仍然保留数据。

1、AT24C02

1.1 内存分类

RAM与ROM是计算机中常见的存储器类型。RAM(Random Access Memory)是一种临时存储器,用于存储计算机正在运行的程序和数据。它具有快速的读写速度和随机访问的特点。相比之下,ROM(Read-Only Memory)是一种只读存储器,用于存储固定的程序指令和数据。ROM中的数据在计算机断电时不会丢失,因此被称为非易失性存储器。ROM可分为MASKROM(掩模只读存储器)、PROM(可编程只读存储器)、EPROM(可擦除可编程只读存储器)、EEPROM、FLASH。而最常用的ROM是EEPROM和FLASHEEPROM是一种电可擦除的可编程ROM,相对于EPROM,它不需要紫外线擦除,它是利用电压脉冲进行擦除和编程数据;而FLASH是一种基于EEPROM技术的存储器,它是通过块擦除方式进行修改,相比EEPROM更具灵活性和可编程 

1.2 什么是AT24C02

AT24C02就属于EEPROM中的一种存储芯片

AT24C02特点

①at24c02是一种EEPROM存储芯片

②工作电压范围 1.8~5.5v (工作电压范围宽)

③支持低功耗模式(待机)

④内置256*8bit(共2k)存储空间

内部存储格式为8字节一页,共32页,构成256字节空间绝对地址0~255

⑥硬件数据写保护

把数据写进去,可以通过写保护管脚接入相应的电平时候,只能读不能写

⑦内部写周期最大5ms

当写数据时候,他先将数据存在存储器中,把相应地址空间的数据清除,然后才会把数据写到对应地址中。要给5ms的时间(写一次所需的时间,最大写入8个字节也就是一页)

⑧可按字节写,也可以按页写

按字节写:对应的找到地址空间,写入数据

按页写  :一页8字节,可以自动写下一个字节但是不能自动换页

起始地址编号  和  内容

读是没有限定的可以从0地址空间读到255地址空间(自动地址递增)

⑨可擦写100万次。(注意:跟读没关系)

1.3 内部结构框图

数据传输到数据缓冲器里,需要5ms的时间再写入到存储器中 

1.4 引脚说明

Vcc      : 电源

GND     : 地

A0,A1,A2 : 器件地址位

此器件地址: 1 0 1 0 A2 A1 A0 R/W 前四位固定,后三位自由设置,最后一位为读写控制位

                        (1:主机读AT24的数据   0:主机写数据到AT24)

根据原理图可知:A2 A1 A0 接地  

所以器件地址为: 1 0 1 0 0 0 0 R/W      写:10100000(0xa0)     读:10100001(0xa1)   

                                                                 0xa0:写模式                   0xa1:读模式

SDA      : IIC通信的数据线

SCL      : IIC通信的时钟线

WP       : 写保护管脚,接地可以正常读写,高电平只能读

2、基于IIC基础协议对AT24C02进行读写操作

2.1 读写位

接下来STM32要对存储芯片的读写操作

写: 0                 10100000      0xa0

读: 1                 10100001      0xa1

                                                                                

2.2 单字节写入 

字地址:器件内部存储空间地址(0~255)

/***************************************
*函数名			:at24c02_write_byte
*函数功能		:对at24c02某个空间存储一个字节数据
*函数参数		:u8  inner_addr     要存储的内部地址
				  u8  data           要存储的数据
*函数返回值	:u8
*函数描述		:阶段性错误返回标志
				AT24C02_NO_ERR  0;   //应答  无错误
				AT24C02_ERR1   1;   //无应答
				AT24C02_ERR2   2;   //无应答
			    AT24C02_ERR3   3;   //无应答
****************************************/
u8 at24c02_write_byte(u8 inner_addr,u8 data)
{
	u8 ack;
	iic_star();
	iic_send_byte(ATC24C02_ADDR_W);
	ack = iic_rec_ack();
	if(ack == 1)
	{
		iic_stop();
		return AT24C02_ERR1;
	}
	
	iic_send_byte(inner_addr);
	ack = iic_rec_ack();
	if(ack == 1)
	{
		iic_stop();
		return AT24C02_ERR2;
	}
	
	iic_send_byte(data);
	ack = iic_rec_ack();
	if(ack == 1)
	{
		iic_stop();
		return AT24C02_ERR3;
	}
	
	iic_stop();
	
	//写周期
	timer6_delay_ms(5);
	
	return AT24C02_NO_ERR;
}
 2.3 单字节读取

 

/***************************************
*函数名			:at24c02_read_byte
*函数功能		:从at24c02某个空间读一个字节数据
*函数参数		:u8  inner_addr     要读取的内部地址
				  u8 *data           读取到的内容放的地址
*函数返回值	:u8
*函数描述		:阶段性错误返回标志
				AT24C02_NO_ERR  0;   //应答  无错误
				AT24C02_ERR1    1;   //无应答
				AT24C02_ERR2    2;   //无应答
				AT24C02_ERR3    3;   //无应答
****************************************/
u8 at24c02_read_byte(u8 inner_addr,u8 *data)
{
	u8 ack;
	iic_star();
	iic_send_byte(ATC24C02_ADDR_W);
	ack = iic_rec_ack();
	if(ack == 1)
	{
		iic_stop();
		return AT24C02_ERR1;
	}
	
	iic_send_byte(inner_addr);
	ack = iic_rec_ack();
	if(ack == 1)
	{
		iic_stop();
		return AT24C02_ERR2;
	}
	
	iic_star();
	iic_send_byte(ATC24C02_ADDR_R);
	ack = iic_rec_ack();
	if(ack == 1)
	{
		iic_stop();
		return AT24C02_ERR1;
	}
	
	*data = iic_rec_byte();
	
	iic_send_ack(1);//停止接收应答
	iic_stop();
	
	return AT24C02_NO_ERR;
	
}
 2.4 页写

为什么不用IIC发送一字节函数连续写入数据?

首先IIC发送一字节函数每发一次字节有5ms的写入周期,如果连续写入多个数据后则会造成写周期时间累积,导致效率低;所以可以用页写进行操作,页写是每页写完才会进行一次写周期,并且页内是可以自动写下一个字节但是不能自动换页。

分析:

需要根据写入的起始地址和写入的字节数判断是否跨页

Inner/8==(inner + num_byte-1)/8  不跨页

/***************************************
函数名    :at24c02_write_page
函数功能  :对at24c02的存储空间页写操作       
函数参数  :u8 inner_addr    //起始空间地址
           u8 num_byte     //要写的字节数
		   u8 *str         //要写内容的起始地址
函数返回值:每次发送都会加测响应,通过返回值判断具体哪次发送出问题
			宏定义错误标志:
			AT24C02_NO_ERR 0;   //应答  无错误
			AT24C02_ERR1   1;   //无应答
			AT24C02_ERR2   2;   //无应答
			AT24C02_ERR3   3;   //无应答
			AT24C02_OVER   4;   //跨页操作错误返回
****************************************/
u8 at24c02_write_page(u8 inner_addr,u8 num_byte,u8 *str)
{
	u8 ack;
	if(inner_addr/8 != (inner_addr+num_byte-1)/8 )//跨页
	{
		return AT24C02_OVER;
	}
	/*跨页操作*/
	iic_star();
	iic_send_byte(ATC24C02_ADDR_W);
	ack = iic_rec_ack();
	if(ack == 1)
	{
		iic_stop();
		return AT24C02_ERR1;
	}
	
	iic_send_byte(inner_addr);
	ack = iic_rec_ack();
	if(ack == 1)
	{
		iic_stop();
		return AT24C02_ERR2;
	}
	//循环发送
	while(num_byte)//abc
	{
		iic_send_byte(*str);
		ack = iic_rec_ack();
		if(ack == 1)
		{
			return AT24C02_ERR3;
		}
		num_byte--;
		str++;
	}
	
	iic_stop();
	
	//写周期
	timer6_delay_ms(5);
	
	return AT24C02_NO_ERR;
	
}
 2.5 连续读

 

/***************************************
函数名  :at24c02_read_bytes
函数功能:从at24c02的某个地址空间开始连续读多个字节
函数参数:u8  inner_addr     //器件内部存储空间地址  0~255 
			 u16  num_byte      //要读多少个字节
			 u8   *data          //将读到的数据写入此地址	  
        
返回值  :每次发送都会加测响应,通过返回值判断具体哪次发送出问题
		宏定义错误标志:
		AT24C02_NO_ERR 0;   //应答  无错误
		AT24C02_ERR1   1;   //无应答
		AT24C02_ERR2   2;   //无应答
		AT24C02_ERR3   3;   //无应答
		AT24C02_OVER   4;   //跨页操作错误返回
****************************************/
u8 at24c02_read_bytes(u8 inner_addr,u16 num_byte,u8 *data)
{
	u8 ack;
	iic_star();
	iic_send_byte(ATC24C02_ADDR_W);
	ack = iic_rec_ack();
	if(ack == 1)
	{
		iic_stop();
		return AT24C02_ERR1;
	}
	
	iic_send_byte(inner_addr);
	ack = iic_rec_ack();
	if(ack == 1)
	{
		iic_stop();
		return AT24C02_ERR2;
	}
	
	iic_star();
	iic_send_byte(ATC24C02_ADDR_R);
	ack = iic_rec_ack();
	if(ack == 1)
	{
		iic_stop();
		return AT24C02_ERR1;
	}
	
	/*连续读操作*/
	while(num_byte)
	{
		*data = iic_rec_byte();
		if(num_byte == 1)
		{
			iic_send_ack(1);//停止接收应答
		}
		else
		{
			iic_send_ack(0);
		}
		num_byte--;
		data++;
	}
	
	iic_stop();
	
	return AT24C02_NO_ERR;
	
}
2.6 连续页写 

说明:

从任意地址位置,连续写多字节,不要考虑跨页限制

分析:

不能跨页的原因:

页内地址自动递增,页之间的地址是不能自动递增的, 7号地址不能自动递增到8号地址

跨页的关键:如何切换到下一页

思路:

/************************************************
*函数名			:at24c02_write_bytes
*函数功能		:对at20c02的存储空间开始连续写 可跨页
*函数参数		:u8  inner_addr      起始地址
				  u16 num_byte        要读的字节数
                  u8  *str            要写的数据的首地址
*函数返回值	:u8
*函数描述		:
*************************************************/
void at24c02_write_bytes(u8 inner_addr,u16 num_byte,u8 *str)
{
	u8 less_byte;
	
	while(1)
	{
		//计算本页还剩多少空间可写 less_byte = 8 - inner_addr % 8   
		less_byte = 8 - inner_addr % 8;
		//如果要写的内容不需要跨页 less_byte >= num_byte
		if(less_byte >= num_byte)
		{
			//调用发送页写函数(inner_addr,num_byte,str)
			at24c02_write_page(inner_addr,num_byte,str);
			break;
		}
		//如果要写的内容需要跨页 less_byte < num_byte
		else 
		{
			//调用发送页写函数把本页剩下的空间写完(inner_addr,less_byte,str)
			at24c02_write_page(inner_addr,less_byte,str);
			//计算出剩下多少个字节:num_byte = num_byte - less_byte
			num_byte = num_byte - less_byte;
			//下一页的首地址: inner_addr = inner_addr + less_byte
			inner_addr = inner_addr + less_byte;
			//剩余要写的内容数据的地址:str = str + less_byte
			str = str + less_byte;
		}
		
	}
	
	
	
}

3、具体使用

需求1:往AT24C02中存储结构体,并且读出

分析:

存储一本书的信息

typedef struct book
{
	
	u8 name[15];
	u8 writer[15];
	u8 number[15];
	u32 hot;
  u32 sc;
	float price;
}BK;
 

int main(void)
{
	
	u8 start_val;
	u8 set_flag = 0;
	BK send_book = {"西游记","吴承恩","W201955",0,30,55.5};
	BK rec_book;
	NVIC_SetPriorityGrouping(5); //设置优先级分组
	Usart1_init(115200);//串口初始化
	
	at24c02_init();
	
	at24c02_write_bytes(0,sizeof(send_book),(u8 *)&send_book);
	at24c02_read_bytes(0,sizeof(rec_book),(u8 *)&rec_book);
	

		
	while(1)
	{
		printf("name:%s writer:%s number:%s hot:%d sc:%d price:%.1f\r\n",rec_book.name,rec_book.writer,rec_book.number,rec_book.hot,rec_book.sc,rec_book.price);
		timer11_delay_ms(200);
	}
	
}

需求2:打印开机次数

分析:

获取上一次的开机次数(从AT24C02中读出来)

读出来的数据+1,再写到AT24C02中(下次用)

根据需求怎么分析?

首先根据分析可得,先读取上一次开机次数->次数+1->打印开机次数->往AT24C02中存储本次的开机次数;

发现问题:第一次开机之前就没存过开机次数

解决问题:分情况第一次开机与不是第一次开机

又发现问题:如何判断第一次开机

解决问题:一个空间作为第一次开机标志位

具体如下:

                                                                        比喻

每次开机都去读取某个空间-------->每次都去”到此一游”留名处

如果读到不是0xff(自己定),则是第一次开机------>无”到此一游”则证明没来过

并且将0xff写入到空间内,以便下次开机判断是否为第一次开机------>写上”到此一游”,下次再来则证明来过了

    at24c02_init();
	/**********************
	10号是开机标志位 11号是开机次数
	**********************/
	//一开机就读取10号空间,以便于判断是否为第一次开机
	at24c02_read_byte(10,&open_flag);
	
	//第一次开机
	if(open_flag != 0xff)
	{
		open_cnt = 1;
		printf("开机次数:%d\r\n",open_cnt);
		//往AT24C02中存储开机次数
		at24c02_write_byte(11,open_cnt);
		//往AT24C02中存储开机标志位
		open_flag = 0xff;
		at24c02_write_byte(10,open_flag);
	}
	
	//不是第一次开机
	else
	{
		//获取上一次开机的次数
		at24c02_read_byte(11,&open_cnt);
		//打印
		open_cnt++;
		printf("开机次数:%d\r\n",open_cnt);
		//往AT24C02中存储开机次数
		at24c02_write_byte(11,open_cnt);

	}
	

需求3:存储流水灯速度,按键调控流水灯,调控的速度要存储到AT24C02中

优化:

①按按键1存储数据

KEY1在没有开流水灯的时候,起到开启作用--->显示初始速度或者上次存储的速度

开流水灯后,起到保存当前速度作用--->确定保存速度并且显示

②调整到某个速度后,3s不做任何操作,速度自动保存

按下按键开始计时     

如何做到按下按键开始计时?

利用定时中断-->按按键并且到时间 才保存速度(一次)

    at24c02_init();
	/**********************
	12号空间是开机标志位 
	13号空间作为存储流水灯的速度
	**********************/
	//一开机就读取12号空间,以便于判断是否为第一次开机
	at24c02_read_byte(12,&open_flag);
	
	//第一次开机
	if(open_flag != 0xff)
	{
		//赋值流水灯速度
		speed = 5;
		//往AT24C02中存储流水灯速度
		at24c02_write_byte(13,speed);
		
		//往AT24C02中存储开机标志位
		open_flag = 0xff;
		at24c02_write_byte(12,open_flag);
	}
	
	//不是第一次开机
	else
	{
		//获取上一次开机流水灯的速度
		at24c02_read_byte(13,&speed);
	}
	
	printf("当前流水灯速度:%d\r\n",speed);
		
	while(1)
	{
		  keynum = Key_scan();
			switch(keynum)
			{
				case 1:timer7_flag = 0;at24c02_write_byte(13,speed);printf("当前流水灯速度:%d\r\n",speed);led_flag = 1;break;//按键1打开流水灯
				case 2:led_flag = 0;LED_OFF;break;//按键2关闭流水灯
				case 3:if(led_flag){Tim7_cnt[8] = 0;timer7_flag = 1;speed -= 1;if(speed < 1)speed = 5;}break;//按键3加速
				case 4:if(led_flag){Tim7_cnt[8] = 0;timer7_flag = 1;speed += 1;if(speed > 10)speed = 5;}break;//按键4减速
			}
			if(led_flag == 1)
			{
				LED_flash(speed);
			}


	}
/*
函数名:TIM7_IRQHandler
函数功能:基本定时器中断服务函数
返回值:void
形参:void
函数说明:

*/
u16 Tim7_cnt[10];
u8 RGB_flag = 0;
u8 speed;
u8 timer7_flag = 0;
void TIM7_IRQHandler(void)
{
	//清除中断标志位
	TIM7->SR &= ~(1<<0);
	//紧急事件
	Tim7_cnt[8]++;
	
	if(Tim7_cnt[8] >= 3000)
	{
		if(timer7_flag == 1)
		{
			//标志位清零
			timer7_flag = 0;
			//获取流水灯速度
			at24c02_write_byte(13,speed);
			printf("当前流水灯速度:%d\r\n",speed);
			Beep_ON;
		}
		Tim7_cnt[8] = 0;
	}
	if(Tim7_cnt[8] >= 300)
		Beep_OFF;
	
	
	
}

③利用结构体来管理数据

typedef struct ctrval

{

        u8 open_flag; //第一次开机标志

        u8 speed;     //存储流水灯速度

        u16 open_cont;//存储开机次数

}CTRVAL_t;

at24c02_init();
	/**************************************
	设置参数存放地址:1
	***************************************/
	//读取开机标志空间
	at24c02_read_bytes(1,sizeof(ctr_data),(u8 *)&ctr_data);
	
	//第一次开机
	if(ctr_data.open_flag != 0xff)
	{
		//赋值开机标志位
		ctr_data.open_flag = 0xff;
		
		//赋值开机次数
		ctr_data.open_cont = 1;
		printf("第%d次开机\r\n",ctr_data.open_cont);
		
		//赋值流水灯速度
		ctr_data.speed = 5;
		printf("当前的速度:%d\r\n",ctr_data.speed);
		
		//往AT24C02中储存结构体数据
		at24c02_write_bytes(1,sizeof(ctr_data),(u8 *)&ctr_data);
	}
	
	//不是第一次开机
	else
	{
		//开机次数自增
		ctr_data.open_cont++;
		printf("第%d次开机\r\n",ctr_data.open_cont);
		
		//往AT24C02中储存结构体数据
		at24c02_write_bytes(1,sizeof(ctr_data),(u8 *)&ctr_data);
	}
	
	printf("当前流水灯速度:%d\r\n",ctr_data.speed);
		
	while(1)
	{
		  keynum = Key_scan();
			switch(keynum)
			{
				case 1:timer7_flag = 0;at24c02_write_bytes(1,sizeof(ctr_data),(u8 *)&ctr_data);printf("当前流水灯速度:%d\r\n",ctr_data.speed);led_flag = 1;break;//按键1打开流水灯
				case 2:led_flag = 0;LED_OFF;break;//按键2关闭流水灯
				case 3:if(led_flag){Tim7_cnt[8] = 0;timer7_flag = 1;ctr_data.speed -= 1;if(ctr_data.speed < 1)ctr_data.speed = 5;}break;//按键3加速
				case 4:if(led_flag){Tim7_cnt[8] = 0;timer7_flag = 1;ctr_data.speed += 1;if(ctr_data.speed > 10)ctr_data.speed = 5;}break;//按键4减速
			}
			if(led_flag == 1)
			{
				LED_flash(ctr_data.speed);
			}


	}
	
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值