工程可见Github<传送门>
关于24C02的基础知识,之前有过很详细的一篇文章,这里就不再赘述,直接上菜吧。
以下用到的位带区及位带别名区的相关知识可参考这里。
用eeprom
记录上电次数这样一个实例,来巩固下eeprom
。
一、主要代码
main.c
/*******************************************************************************
* 文件名:main.c
* 描 述:
* 作 者:CLAY
* 版本号:v1.0.0
* 日 期: 2019年1月25日
* 备 注:EEPROM记录开机次数,LCD显示开机次数
*
*******************************************************************************
*/
#include "config.h"
#include "led.h"
#include "key.h"
#include "timer.h"
#include "beep.h"
#include "lcd.h"
#include "stdio.h"
#include "usart.h"
#include "i2c.h"
#include "eeprom.h"
int main(void)
{
u8 cnt; //程序启动次数
u8 chk; //启动次数校验字节
u8 str1[25];
u8 i;
u8 str[25];
u8 temp = 30;
float AO = 3.845;
STM3210B_LCD_Init();
LCD_Clear(Blue);
LEDInit();
KeyInit();
BeepInit();
TIM2Init(2000, 72);//定时2ms
USART2Init(9600);
I2CInit();
cnt = E2ReadByte(0x00);
chk = E2ReadByte(0x01);
if((cnt^chk) != 0xFF)//两个字节不是反码,归0重新计数
{
cnt = 0;
}
if(cnt < 250)
{
cnt ++;
}
LCD_ClearLine(Line8);
sprintf((char*)str1," cnt = %d ",cnt);
LCD_DisplayStringLine(Line8, str1);
E2WriteByte(0x00, cnt);
E2WriteByte(0x01, ~cnt);
LCD_DisplayStringLine(Line1,(u8*) "qwertyuioplkjhgfdsazxcvb");
sprintf((char*)str,"temp=%d A0=%.1f ",temp, AO);
LCD_DisplayStringLine(Line2,str);
while(1)
{
KeyDriver();
if(RxdOverFlag)
{
RxdOverFlag = 0;
LCD_ClearLine(Line5);
LCD_DisplayStringLine(Line5, RxdBuf);
USART2_SendByte(RxdBuf);
for(i=0; i<50; i++) RxdBuf[i] = 0;//清空串口接收缓冲区
USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);//开启串口接收中断,处理下一帧数据
}
}
}
void KeyAction(int code)
{
if(code == 1)//按下B1,切换灯状态,蜂鸣器鸣叫0.1s
{
GPIOC->ODR ^= (1<<8);//PC8不断取反
GPIOD->ODR |= (1<<2);//PD2置1,使能573锁存器
GPIOD->ODR &= ~(1<<2);//PD2清0,关闭573锁存器
Beep(100);
}
else if(code == 2)
{
Beep(-1);
}
else if(code == 3)
{
Beep(0);
}
}
i2c.c
#include "i2c.h"
void I2CInit(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //使能GPIOB时钟
GPIO_SetBits(GPIOB, GPIO_Pin_6|GPIO_Pin_7); //SCL和SDA初始输出高电平(先设置引脚电平可以避免IO初始化过程中可能产生的毛刺)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7; //选择SCL和SDA引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; //选择开漏输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz; //输出速率10MHz
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
/* 产生总线起始信号 */
void I2CStart(void)
{
I2C_SDA_OUT = 1; //首先确保SDA、SCL都是高电平
I2C_SCL_OUT = 1;
delay_us(5);
I2C_SDA_OUT = 0; //先拉低SDA
delay_us(5);
I2C_SCL_OUT = 0; //再拉低SCL
}
/* 产生总线停止信号 */
void I2CStop(void)
{
I2C_SCL_OUT = 0; //首先确保SDA、SCL都是低电平
I2C_SDA_OUT = 0;
delay_us(5);
I2C_SCL_OUT = 1; //先拉高SCL
delay_us(5);
I2C_SDA_OUT = 1; //再拉高SDA
delay_us(5);
}
/* I2C总线写操作,dat-待写入字节,返回值-从机应答位的值 */
u8 I2CWrite(u8 dat)
{
int i;
u8 ack; //用于暂存应答位的值
for (i=0; i<8; i++) //循环将8bit数据输出到总线上
{
I2C_SDA_OUT = (dat&0x80) ? 1 : 0; //将最高位的值输出到SDA上
delay_us(5);
I2C_SCL_OUT = 1; //拉高SCL
delay_us(5);
I2C_SCL_OUT = 0; //再拉低SCL,完成一个位周期
dat <<= 1; //左移将次高位变为最高位,实现高位在先低位在后的发送顺序
}
I2C_SDA_OUT = 1; //8位数据发送完后,主机释放SDA,以检测从机应答
delay_us(5);
I2C_SCL_OUT = 1; //拉高SCL
ack = I2C_SDA_IN; //读取此时的SDA值,即为从机的应答值
delay_us(5);
I2C_SCL_OUT = 0; //再拉低SCL完成应答位,并保持住总线
delay_us(5);
return (!ack); //应答值取反以符合通常的逻辑:
//0=不存在或忙或写入失败,1=存在且空闲或写入成功
}
/* I2C总线读取8位数据,返回值-读到的字节 */
u8 I2CRead(void)
{
int i;
u8 dat = 0; //数据接收变量赋初值0
I2C_SDA_OUT = 1; //首先确保主机释放SDA
for (i=0; i<8; i++) //循环将总线上的8bit数据读入dat中
{
delay_us(5);
I2C_SCL_OUT = 1; //拉高SCL
dat <<= 1; //左移将己读到的位向高位移动,实现高位在先低位在后的接收顺序
if(I2C_SDA_IN != 0) //读取SDA的值到dat最低位上
{
dat |= 0x01; //SDA为1时设置dat最低位为1,SDA为0时无操作,即仍为初始值的0
}
delay_us(5);
I2C_SCL_OUT = 0; //再拉低SCL,以使从机发送出下一位
}
return dat;
}
/* I2C总线读操作,并发送非应答信号,返回值-读到的字节 */
u8 I2CReadNAK(void)
{
u8 dat;
dat = I2CRead(); //读取8位数据
I2C_SDA_OUT = 1; //8位数据读取完后,拉高SDA,发送非应答信号
delay_us(5);
I2C_SCL_OUT = 1; //拉高SCL
delay_us(5);
I2C_SCL_OUT = 0; //再拉低SCL完成非应答位,并保持住总线
delay_us(5);
return dat;
}
/* I2C总线读操作,并发送应答信号,返回值-读到的字节 */
u8 I2CReadACK(void)
{
u8 dat;
dat = I2CRead(); //读取8位数据
I2C_SDA_OUT = 0; //8位数据读取完后,拉低SDA,发送应答信号
delay_us(5);
I2C_SCL_OUT = 1; //拉高SCL
delay_us(5);
I2C_SCL_OUT = 0; //再拉低SCL完成应答位,并保持住总线
delay_us(5);
return dat;
}
i2c.h
#ifndef _I2C_H
#define _I2C_H
#include "config.h"
#define I2C_SCL_OUT PB_OUT(6)
#define I2C_SDA_OUT PB_OUT(7)
#define I2C_SDA_IN PB_IN(7)
void I2CInit(void);
void I2CStart(void);
void I2CStop(void);
u8 I2CReadNAK(void);
u8 I2CReadACK(void);
u8 I2CWrite(u8 dat);
#endif
eeprom.c
#include "i2c.h"
#include "eeprom.h"
/* 读取EEPROM中的一个字节,addr-字节地址 */
u8 E2ReadByte(u8 addr)
{
u8 dat;
do { //用寻址操作查询当前是否可进行读写
I2CStart();
if (I2CWrite(0x50<<1)) //寻址器件,应答则跳出循环,否则继续查询
{
break;
}
I2CStop();
} while(1);
I2CWrite(addr); //写入存储地址
I2CStart(); //发送重复启动信号
I2CWrite((0x50<<1)|0x01); //寻址器件,后续为读操作
dat = I2CReadNAK(); //读取一个字节数据
I2CStop();
return dat;
}
/* 向EEPROM中写入一个字节,addr-字节地址 */
void E2WriteByte(u8 addr, u8 dat)
{
do { //用寻址操作查询当前是否可进行读写
I2CStart();
if (I2CWrite(0x50<<1)) //寻址器件,应答则跳出循环,否则继续查询
{
break;
}
I2CStop();
} while(1);
I2CWrite(addr); //写入存储地址
I2CWrite(dat); //写入一个字节数据
I2CStop();
}
eeprom.h
#ifndef _EEPROM_H
#define _EEPROM_H
#include "config.h"
u8 E2ReadByte(u8 addr);
void E2WriteByte(u8 addr, u8 dat);
#endif
config.c
#include "config.h"
/* 1/4微秒延时函数(含函数调用及返回时间共计耗时约1/4微妙@72MHz主频) */
void delay_qus(void)
{
__ASM ("nop");
__ASM ("nop");
__ASM ("nop");
__ASM ("nop");
__ASM ("nop");
__ASM ("nop");
__ASM ("nop");
__ASM ("nop");
}
/* 微秒延时函数,us-延时时间 */
void delay_us(u16 us)
{
while (us--)
{
delay_qus();
delay_qus();
delay_qus();
delay_qus();
}
}
/* 毫秒延时函数,ms-延时时间 */
void delay_ms(u16 ms)
{
while (ms--)
{
delay_us(1000);
}
}
config.h
#ifndef _CONFIG_H
#define _CONFIG_H
#include "stm32f10x.h"
//位带宏定义
#define BITBAND(addr, bitnum) ((addr&0xF0000000) + 0x2000000 + ((addr&0xFFFFF)<<5) + (bitnum<<2))
#define MEM_ADDR(addr) *((volatile unsigned long *)(addr))
#define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum))
//IO口地址位带映射
#define GPIOA_ODR_Addr (GPIOA_BASE+12) //GPIOA输出数据寄存器地址0x4001080C
#define GPIOB_ODR_Addr (GPIOB_BASE+12) //GPIOB输出数据寄存器地址0x40010C0C
#define GPIOC_ODR_Addr (GPIOC_BASE+12) //GPIOC输出数据寄存器地址0x4001100C
#define GPIOD_ODR_Addr (GPIOD_BASE+12) //GPIOD输出数据寄存器地址0x4001140C
#define GPIOE_ODR_Addr (GPIOE_BASE+12) //GPIOE输出数据寄存器地址0x4001180C
#define GPIOF_ODR_Addr (GPIOF_BASE+12) //GPIOF输出数据寄存器地址0x40011A0C
#define GPIOG_ODR_Addr (GPIOG_BASE+12) //GPIOG输出数据寄存器地址0x40011E0C
#define GPIOA_IDR_Addr (GPIOA_BASE+8) //GPIOA输入数据寄存器地址0x40010808
#define GPIOB_IDR_Addr (GPIOB_BASE+8) //GPIOB输入数据寄存器地址0x40010C08
#define GPIOC_IDR_Addr (GPIOC_BASE+8) //GPIOC输入数据寄存器地址0x40011008
#define GPIOD_IDR_Addr (GPIOD_BASE+8) //GPIOD输入数据寄存器地址0x40011408
#define GPIOE_IDR_Addr (GPIOE_BASE+8) //GPIOE输入数据寄存器地址0x40011808
#define GPIOF_IDR_Addr (GPIOF_BASE+8) //GPIOF输入数据寄存器地址0x40011A08
#define GPIOG_IDR_Addr (GPIOG_BASE+8) //GPIOG输入数据寄存器地址0x40011E08
//单个IO口位带操作
#define PA_OUT(n) BIT_ADDR(GPIOA_ODR_Addr,n) //PAx输出
#define PA_IN(n) BIT_ADDR(GPIOA_IDR_Addr,n) //PAx输入
#define PB_OUT(n) BIT_ADDR(GPIOB_ODR_Addr,n) //PBx输出
#define PB_IN(n) BIT_ADDR(GPIOB_IDR_Addr,n) //PBx输入
#define PC_OUT(n) BIT_ADDR(GPIOC_ODR_Addr,n) //PCx输出
#define PC_IN(n) BIT_ADDR(GPIOC_IDR_Addr,n) //PCx输入
#define PD_OUT(n) BIT_ADDR(GPIOD_ODR_Addr,n) //PDx输出
#define PD_IN(n) BIT_ADDR(GPIOD_IDR_Addr,n) //PDx输入
#define PE_OUT(n) BIT_ADDR(GPIOE_ODR_Addr,n) //PEx输出
#define PE_IN(n) BIT_ADDR(GPIOE_IDR_Addr,n) //PEx输入
#define PF_OUT(n) BIT_ADDR(GPIOF_ODR_Addr,n) //PFx输出
#define PF_IN(n) BIT_ADDR(GPIOF_IDR_Addr,n) //PFx输入
#define PG_OUT(n) BIT_ADDR(GPIOG_ODR_Addr,n) //PGx输出
#define PG_IN(n) BIT_ADDR(GPIOG_IDR_Addr,n) //PGx输入
void delay_us(u16 us);
void delay_ms(u16 ms);
#endif
二、程序讲解
特别注意程序校验的那一点的算法,原数与取反后的数进行异或等于0xFF,说明次数正确,否则就清零开机次数。
三、注意事项
1、为了方便I2C.c
的管脚操作,在config.h
中加入了位带操作
2、I2C
中的延时采用的是config.c
中的__ASM ("nop");
延时方法
3、I2C中先设置引脚输出,再初始化。
GPIO_SetBits(GPIOB, GPIO_Pin_6|GPIO_Pin_7); //SCL和SDA初始输出高电平(先设置引脚电平可以避免IO初始化过程中可能产生的毛刺)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7; //选择SCL和SDA引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; //选择开漏输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz; //输出速率10MHz
GPIO_Init(GPIOB, &GPIO_InitStructure);
因为你想啊,上电后为浮空输入,然后又有上拉电阻存在,自然变成高电平。到了I2C初始化这一点,因为数据输出寄存器复位默认值为0,如果先初始化再设置引脚输出电平,就会先输出低(初始化为开漏输出),然后再设置引脚输出电平高,自然有了 高 -> 低 -> 高的状态,当然会产生毛刺,所以这里先设置引脚电平,再初始化。
其实不这样,事也不大。