目录
需求3:存储流水灯速度,按键调控流水灯,调控的速度要存储到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,使得输出禁止,这时就可以输入了。
开漏输出的优势:
- 防止短路
- 实现线与逻辑
外接上拉电阻与内置上拉电阻的区别:
一、结构区别
内置上拉电阻:这是芯片本身自带的电阻,一般在输入端口内部连接。其电阻值固定,常见的有1KΩ、10KΩ等,具体值取决于芯片的设计。
外接上拉电阻:需要通过外部引脚连接到芯片上。其电阻值和型号可以根据实际需要选择,具有更高的灵活性。
二、使用区别
内置上拉电阻:
优点:可以直接在硬件设计中使用,无需连接外部元器件,节省引脚资源和PCB面积,提高设计的灵活性。同时,简化了电路设计,降低了开发难度,适用于大多数应用场合。
缺点:电阻值固定,无法根据电路要求进行变化。在一些特殊场合下,如高精度模拟信号处理,内置上拉电阻的参数可能会产生影响。
外接上拉电阻:
优点:电阻值可以根据实际需求进行选取,提高了电路的灵活性和适用性。尤其是在需要高精度信号处理的场合下,外接上拉电阻更具优势。
缺点:需要占用更多的引脚资源和PCB面积。
2.2 IIC的时序图
时序帧: 空闲段+起始段+数据段+应答段+停止段
空闲:
时钟线为高电平,数据线为高电平
起始信号:
时钟线为高电平,数据线为下降沿
数据段:
时钟线为低电平,数据线交叉(可读)
时钟线为高电平,数据线平行(可写)
停止信号:
时钟线为高电平,数据线为上升沿
应答:
IIC通信每传输一个字节(8位)就要有一个应答/不应答信号产生
主机是如何接收应答的?
①当主机作为发送数据端:---------主机检测应答/不应答信号
从机有IIC硬件电路,所以接收数据/检测应答--->从机自动完成
②当主机作为接收数据端:---------主机发送应答/不应答信号
从机有IIC硬件电路,所以发送数据/检测应答--->从机自动完成
应答/不应答信号总结:
①主机发送数据端
- 主机发送数据 //写程序
- 从机接收数据 //自动
- 从机发送应答信号/不应答信号 //自动
- 主机检测发送应答信号/不应答信号 //写程序
所以:检测应答/不应答函数
②主机接收数据端
- 从机发送数据 //自动
- 主机接收数据 //写程序
- 主机发送应答信号/不应答信号 //写程序
- 从机检测发送应答信号/不应答信号 //自动
所以:发送应答/不应答函数
数据帧格式:
一帧数据帧的格式--->起始位+数据位+应答位+停止位
总结:
空闲信号:
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和FLASH;EEPROM是一种带电可擦除的可编程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);
}
}
}