day19:stm32向eeprom写入和读取单个字节(本节内容继承day18)
实验:在day18章节中已经写好了I2C的底层函数,这一节调用这些函数实现STM32对EEPROM的写入单个字节和读取单个字节的操作
工程结构:

程序清单:
【1】bsp_usart.h
#ifndef __BSP_USART_H__ #define __BSP_USART_H__ #include "stm32f10x.h" #include "stdio.h" // ----------------------- 串口1-USART1 // 使用哪个串口(串口1..5) #define DEBUG_USARTx USART1 // APB2串口的同步时钟 #define DEBUG_USART_CLK RCC_APB2Periph_USART1 // APB2系统时钟(因为串口USART1是挂载到APB2总线上的,所以要打开APB2总线的时钟) #define DEBUG_USART_APBxClkCmd RCC_APB2PeriphClockCmd // 串口通信的波特率 #define DEBUG_USART_BAUDRATE 19200 // ----------------------- USART GPIO 引脚宏定义 // GPIO引脚 #define DEBUG_USART_GPIO_CLK (RCC_APB2Periph_GPIOA) // APB2系统时钟(因为串口USART1是挂载到APB2总线上的,所以要打开APB2总线的时钟) #define DEBUG_USART_GPIO_APBxClkCmd RCC_APB2PeriphClockCmd // GPIO引脚,发送接PA9,接收接PA10 #define DEBUG_USART_TX_GPIO_PORT GPIOA #define DEBUG_USART_TX_GPIO_PIN GPIO_Pin_9 #define DEBUG_USART_RX_GPIO_PORT GPIOA #define DEBUG_USART_RX_GPIO_PIN GPIO_Pin_10 #define DEBUG_USART_IRQ USART1_IRQn #define DEBUG_USART_IRQHandler USART1_IRQHandler /* 串口调试配置函数:配置串口的相关参数,使能串口 */ void DEBUG_USART_Config(void); /* 发送一个字节 */ void Usart_SendByte(USART_TypeDef* pUSARTx, uint8_t ch); /* 发送字符串 */ void Usart_SendString(USART_TypeDef* pUSARTx, char* str); /* 串口中断配置函数 */ static void NVIC_Configuration(void); #endif /* __BSP_USART_H__ */
【2】bsp_usart.c
#include "./usart/bsp_usart.h"
/* 串口中断配置函数 */
static void NVIC_Configuration(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
/* 嵌套向量中断控制器组选择 */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
/* 配置USART为中断源 */
NVIC_InitStructure.NVIC_IRQChannel = DEBUG_USART_IRQ;
/* 抢断优先级*/
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
/* 子优先级 */
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
/* 使能中断 */
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
/* 初始化配置NVIC */
NVIC_Init(&NVIC_InitStructure);
}
/* 串口调试配置函数:配置串口的相关参数,使能串口 */
void DEBUG_USART_Config(void)
{
/* 结构体变量声明 */
GPIO_InitTypeDef GPIO_InitStructure; // GPIO
USART_InitTypeDef USART_InitStructure; // USART
/* ------------ 第一步:初始化GPIO */
// 打开串口GPIO的时钟
DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE);
// 将USART Tx的GPIO配置为推挽复用模式
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN; // 引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 速率
GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure); // 初始化结构体
// 将USART Rx的GPIO配置为浮空输入模式
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);
/* ------------ 第二步:配置串口的初始化结构体 */
// 打开串口外设的时钟
DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE);
/* 配置串口的工作参数 */
// 波特率
USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;
// 针数据字长
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
// 停止位
USART_InitStructure.USART_StopBits = USART_StopBits_1;
// 校验位
USART_InitStructure.USART_Parity = USART_Parity_No ;
// 硬件流控制
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
// 工作模式,收发一起
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
// 完成串口的初始化配置
USART_Init(DEBUG_USARTx, &USART_InitStructure);
/* -------------------------------------------------------- */
// 串口中断优先级配置
NVIC_Configuration();
// 使能串口接收中断
USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, ENABLE);
/* -------------------------------------------------------- */
/* ------------ 第三步:使能串口 */
USART_Cmd(DEBUG_USARTx, ENABLE);
}
/* 发送一个字节 */
void Usart_SendByte(USART_TypeDef* pUSARTx, uint8_t ch)
{
/* 发送一个字节数据到USART */
USART_SendData(pUSARTx, ch);
/* 等待发送数据寄存器为空 */
while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
}
/* 发送字符串 */
void Usart_SendString(USART_TypeDef* pUSARTx, char* str)
{
unsigned int k=0;
do
{
Usart_SendByte(pUSARTx, *(str + k));
k++;
} while(*(str + k)!='\0');
/* 等待发送完成 */
while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TC) == RESET);
}
/* 重定向c库函数printf到串口,重定向后可使用printf函数 */
int fputc(int ch, FILE *f)
{
/* 发送一个字节数据到串口 */
USART_SendData(DEBUG_USARTx, (uint8_t) ch);
/* 等待发送完毕 */
while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_TXE) == RESET);
return (ch);
}
/* 重定向c库函数scanf到串口,重写向后可使用scanf、getchar等函数 */
int fgetc(FILE *f)
{
/* 等待串口输入数据 */
while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_RXNE) == RESET);
return (int)USART_ReceiveData(DEBUG_USARTx);
}
【3】bsp_i2c_gpio.h
#ifndef __BSP_I2C_H__ #define __BSP_I2C_H__ #include "stm32f10x.h" /* I2C的GPIO端口定义:SCL接PA2,SDA接PA3 */ #define I2C_SCL_GPIO_PORT GPIOA // A端口 #define I2C_SCL_GPIO_PIN GPIO_Pin_2 // 引脚2 #define I2C_SCL_GPIO_CLK RCC_APB2Periph_GPIOA // 时钟:PA2挂在到APB2总线上 #define I2C_SDA_GPIO_PORT GPIOA // A端口 #define I2C_SDA_GPIO_PIN GPIO_Pin_3 // 引脚3 #define I2C_SDA_GPIO_CLK RCC_APB2Periph_GPIOA // 时钟:PA3挂在到APB2总线上 /* EEPROM的引脚高低电平设置 */ #define EEPROM_I2C_SCL_1() GPIO_SetBits(I2C_SCL_GPIO_PORT, I2C_SCL_GPIO_PIN) // 设置SCL引脚为高电平 #define EEPROM_I2C_SCL_0() GPIO_ResetBits(I2C_SCL_GPIO_PORT, I2C_SCL_GPIO_PIN) // 设置SCL引脚为低电平 #define EEPROM_I2C_SDA_1() GPIO_SetBits(I2C_SDA_GPIO_PORT, I2C_SDA_GPIO_PIN) // 设置SDA引脚为高电平 #define EEPROM_I2C_SDA_0() GPIO_ResetBits(I2C_SDA_GPIO_PORT, I2C_SDA_GPIO_PIN) // 设置SDA引脚为低电平 /* STM32读取EEPROM设备的数据 */ #define EEPROM_I2C_SDA_READ() GPIO_ReadInputDataBit(I2C_SDA_GPIO_PORT, I2C_SDA_GPIO_PIN) /* I2C的GPIO端口初始化 */ void I2C_GPIO_CONFIG(void); /* I2C产生起始信号 */ void I2C_START(void); /* I2C产生结束信号 */ void I2C_STOP(void); /* 产生应答信号 */ void I2C_ASK(void); /* STM32读EEPROM的数据时,EEPROM产生非应答信号 */ void I2C_NO_ASK(void); /* 等待EEPROM的应答信号:应答置0,非应答置1 */ uint8_t I2C_WAIT_ASK(void); /* STM32写一个字节数据到EEPROM */ void I2C_WRITE_BYTE(uint8_t data); /* STM32读EEPROM的一个字节 */ uint8_t I2C_READ_BYTE(void); #endif /* __BSP_I2C_H__ */
【4】bsp_i2c_gpio.c
#include "./i2c/bsp_i2c_gpio.h"
/* 延迟时间 */
static void i2c_Delay(void)
{
uint8_t i;
/*
下面的时间是通过"逻辑分析仪"测试得到的。
工作条件:CPU主频72MHz ,MDK编译环境,1级优化
循环次数为10时,SCL频率 = 205KHz
循环次数为7时,SCL频率 = 347KHz, SCL高电平时间1.5us,SCL低电平时间2.87us
循环次数为5时,SCL频率 = 421KHz, SCL高电平时间1.25us,SCL低电平时间2.375us
*/
for (i = 0; i < 10; i++);
}
/* I2C的GPIO端口初始化 */
void I2C_GPIO_CONFIG(void)
{
/* GPIO结构体 */
GPIO_InitTypeDef GPIO_InitStructure;
/* 打开时钟 */
RCC_APB2PeriphClockCmd(I2C_SCL_GPIO_CLK | I2C_SDA_GPIO_CLK, ENABLE);
/* 实例化SCL结构体 */
GPIO_InitStructure.GPIO_Pin = I2C_SCL_GPIO_PIN; // SCL引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏输出模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // I2C一般最大为400k/s,50MHZ足够了
/* 初始化SCL */
GPIO_Init(I2C_SCL_GPIO_PORT, &GPIO_InitStructure);
/* 实例化SDA结构体 */
GPIO_InitStructure.GPIO_Pin = I2C_SDA_GPIO_PIN;
/* 初始化SDA */
GPIO_Init(I2C_SDA_GPIO_PORT, &GPIO_InitStructure);
}
/* I2C产生起始信号:根据信号图来写 */
void I2C_START(void)
{
EEPROM_I2C_SDA_1();
EEPROM_I2C_SCL_1();
i2c_Delay();
EEPROM_I2C_SDA_0();
i2c_Delay();
EEPROM_I2C_SCL_0();
i2c_Delay();
}
/* I2C产生结束信号:根据时序图来写 */
void I2C_STOP(void)
{
EEPROM_I2C_SDA_0();
EEPROM_I2C_SCL_1();
i2c_Delay();
EEPROM_I2C_SDA_1();
i2c_Delay();
}
/* STM32读EEPROM的数据时,EEPROM产生应答信号 */
void I2C_ASK(void)
{
EEPROM_I2C_SDA_0();
EEPROM_I2C_SCL_1();
i2c_Delay();
EEPROM_I2C_SCL_0();
i2c_Delay();
/*
释放SDA总线控制权,如果不置1,即拉成高阻态,则SDA为低电平0,
因为SDA线为低电平比高电平优先,所以这条线一直都是低电平,
那么其他EEPROM设备就无法接入SCL线
*/
EEPROM_I2C_SDA_1();
i2c_Delay();
}
/* STM32读EEPROM的数据时,EEPROM产生非应答信号 */
void I2C_NO_ASK(void)
{
EEPROM_I2C_SDA_1();
EEPROM_I2C_SCL_1();
i2c_Delay();
EEPROM_I2C_SCL_0();
i2c_Delay();
}
/* 等待EEPROM的应答信号:应答置0,非应答置1 */
uint8_t I2C_WAIT_ASK(void)
{
uint8_t reply;
// 释放SDA线的控制权
EEPROM_I2C_SDA_1();
EEPROM_I2C_SCL_1();
i2c_Delay();
// 判断是应答信号还是非应答信号:看时序图
if(EEPROM_I2C_SDA_READ()==1) // 非应答
{
reply = 1;
}
else // 应答
{
reply = 0;
}
// SCL拉低
EEPROM_I2C_SCL_0();
i2c_Delay();
// 返回信号
return reply;
}
/* STM32写一个字节数据到EEPROM */
void I2C_WRITE_BYTE(uint8_t data)
{
uint8_t i;
// 每次发送一位,循环8次,将一个字节发送完,先发送最高位,后发送低位
for(i=0; i<8; i++)
{
/*
比如:data = 0101 0010,0x80 = 1000 0000,则:
第一次循环:data & 0x80 = 0000 0000,为0,执行:EEPROM_I2C_SDA_0();发送0
data <<= 1;之后,data = 1010 0100
第二次循环:data & 0x80 = 1000 0000,为1,执行:EEPROM_I2C_SDA_1();发送1
data <<= 1;之后,data = 0100 1000
第三次循环:data & 0x80 = 0000 0000,为0,执行:EEPROM_I2C_SDA_0();发送0
data <<= 1;之后,data = 1001 0000
第四次循环:data & 0x80 = 1000 0000,为1,执行:EEPROM_I2C_SDA_1();发送1
data <<= 1;之后,data = 0010 0000
第五次循环:data & 0x80 = 0000 0000,为0,执行:EEPROM_I2C_SDA_0();发送0
data <<= 1;之后,data = 0100 0000
第六次循环:data & 0x80 = 0000 0000,为0,执行:EEPROM_I2C_SDA_0();发送0
data <<= 1;之后,data = 1000 0000
第七次循环:data & 0x80 = 1000 0000,为1,执行:EEPROM_I2C_SDA_1();发送1
data <<= 1;之后,data = 0000 0000
第八次循环:data & 0x80 = 0000 0000,为0,执行:EEPROM_I2C_SDA_0();发送0
经过八次循环之后,发送的数据为:0101 0010,就是一开始要发送的数据data的值
*/
if(data & 0x80)
{
EEPROM_I2C_SDA_1();
}
else
{
EEPROM_I2C_SDA_0();
}
// 每次都延迟,相当于事先将数据准备好,再产生时钟时序,这样数据发送稳定一点
i2c_Delay();
// 每次循环,SCL都产生一个0/1方波,表示在一个时钟周期内发送数据
EEPROM_I2C_SCL_1();
i2c_Delay();
EEPROM_I2C_SCL_0();
i2c_Delay();
// 将刚刚发送的位移掉
data <<= 1;
// 判断如果发送完数据了,就要释放SDA线的控制权
if(i == 7)
{
EEPROM_I2C_SDA_1();
}
}
}
/* STM32读EEPROM的一个字节 */
uint8_t I2C_READ_BYTE(void)
{
uint8_t i;
uint8_t temp = 0;
// 每次发送一位,循环8次,将一个字节发送完,先发送最高位,后发送低位
for(i=0; i<8; i++)
{
// 在每次开始之前先将最后一位腾出位置
temp <<= 1;
// 时钟开始
EEPROM_I2C_SCL_1();
i2c_Delay();
/* 在时钟为高电平时开始读取数据 */
/* 分析过程和写过程类似 */
if(EEPROM_I2C_SDA_READ() == 1)
{
temp += 1;
}
// 每次都延迟,相当于事先将数据准备好,再产生时钟时序,这样数据发送稳定一点
i2c_Delay();
// 时钟结束
EEPROM_I2C_SCL_0();
i2c_Delay();
}
// 返回读取的数据
return temp;
}
【5】bsp_i2c_eeprom.h
#ifndef __BSP_I2C_EEPROM_H__ #define __BSP_I2C_EEPROM_H__ #include "stm32f10x.h" /* EEPROM的读写方向位 */ #define EEPROM_WRITE_ADDR 0xA0 #define EERPOM_READ_ADDR 0xA1 /* 检测EEPROM是否正常工作 */ uint8_t EEPROM_CHECK_DEVICE(uint8_t addr); /* 向EEPROM写入一个字节数据 */ uint8_t EEPROM_WRITE_BYTE(uint8_t w_addr, uint8_t data); /* 从EEPROM读取一个字节数据 */ uint8_t EEPROM_READ_BYTE(uint8_t r_addr, uint8_t *data); #endif /* __BSP_I2C_EEPROM_H__ */
【6】bsp_i2c_eeprom.c
#include "./i2c/bsp_i2c_eeprom.h"
#include "./i2c/bsp_i2c_gpio.h"
/*
检测EEPROM是否正常工作
addr:EEPROM的设备地址
返回1:未检测到EEPROM
返回0:检测到EEPROM
*/
uint8_t EEPROM_CHECK_DEVICE(uint8_t addr)
{
// 响应结果的返回值
uint8_t result;
// 产生起始信号
I2C_START();
// 发送EEPROM的设备地址
I2C_WRITE_BYTE(addr);
// 判断EEPROM是否响应
if(I2C_WAIT_ASK())
{
result = 1; // 没有响应
}
else
{
result = 0; // 响应
}
// 不响应
I2C_NO_ASK();
// 产生结束信号
I2C_STOP();
return result;
}
/*
等待EEPROM内部时序完成
返回1:超时
返回0:完成
*/
uint8_t EEPROM_WAIT_STANDPY(void)
{
uint16_t cycle = 0;
while(EEPROM_CHECK_DEVICE(EEPROM_WRITE_ADDR))
{
cycle++;
if(cycle > 10000)
{
return 1;
}
}
return 0;
}
/*
向EEPROM写入一个字节数据
w_addr:EEPROM的存储单元格地址
data:要写入EEPROM的数据
返回1:成功
返回0:失败
*/
uint8_t EEPROM_WRITE_BYTE(uint8_t w_addr, uint8_t data)
{
// 在开始之前先延迟,给EEPROM足够的反应时间
if(EEPROM_WAIT_STANDPY())
{
// 如果超时了,就直接结束
goto w_fail;
}
// 产生起始信号
I2C_START();
// 发送EEPROM的设备地址
I2C_WRITE_BYTE(EEPROM_WRITE_ADDR);
// 判断EEPROM是否响应
if(I2C_WAIT_ASK())
{
goto w_fail; // 没有响应
}
else
{
// 发送要写入的存储单元格地址
I2C_WRITE_BYTE(w_addr);
// 发送完存储单元格地址,判断EEPROM是否响应
if(I2C_WAIT_ASK()) // 没有响应
{
goto w_fail;
}
else // 响应
{
// 发送要写入EEPROM的数据
I2C_WRITE_BYTE(data);
// 发送完数据继续检测是否响应
if(I2C_WAIT_ASK()) // 没有响应
{
// 写入失败执行
goto w_fail;
}
else
{
}
}
}
// 产生结束信号
I2C_STOP();
// 在写完数据之后,给EEPROM足够的反应时间
if(EEPROM_WAIT_STANDPY())
{
// 如果超时了,就直接结束
goto w_fail;
}
return 1;
// 写入失败执行
w_fail:
I2C_STOP();
return 0;
}
/*
从EEPROM读取一个字节数据(看单片机资料的时序图来写程序)
r_addr:EEPROM的存储单元格地址
*data:从EEPROM读取到的数据放到一个指针里面,在调用处直接获取指针地址就可以把数据拿出来
返回1:成功
返回0:失败
*/
uint8_t EEPROM_READ_BYTE(uint8_t r_addr, uint8_t *data)
{
// 在开始之前先延迟,给EEPROM足够的反应时间
if(EEPROM_WAIT_STANDPY())
{
// 如果超时了,就直接结束
goto r_fail;
}
// 产生起始信号
I2C_START();
// 第一次:发送EEPROM的设备地址,写方向的设备地址(开始由STM32主动寻址)
I2C_WRITE_BYTE(EEPROM_WRITE_ADDR);
// 判断EEPROM是否响应
if(I2C_WAIT_ASK())
{
goto r_fail; // 没有响应
}
else
{
// 发送要读取的存储单元格地址
I2C_WRITE_BYTE(r_addr);
// 发送完存储单元格地址,判断EEPROM是否响应
if(I2C_WAIT_ASK()) // 没有响应
{
goto r_fail;
}
else // 响应
{
// 第二次发送起始信号
I2C_START();
// 第二次:发送第二次的设备地址,读方向(第二次由EEPROM主动发数据到STM32)
I2C_WRITE_BYTE(EERPOM_READ_ADDR);
// 发送完数据继续检测是否响应
if(I2C_WAIT_ASK()) // 没有响应
{
// 写入失败执行
goto r_fail;
}
else
{
// 如果EEPROM有响应,就读取数据
*data = I2C_READ_BYTE();
}
}
}
// 产生一个NOASK信号再结束,让EEPROM只返回一个字节数据,才不会影响后面的流程
I2C_NO_ASK();
// 产生结束信号
I2C_STOP();
/* 读取没有内部时序,所以不需要延迟
if(EEPROM_WAIT_STANDPY())
{
// 如果超时了,就直接结束
goto r_fail;
}
*/
return 1;
// 写入失败执行
r_fail:
I2C_STOP();
return 0;
}
【7】main.c
/*
I2C的初始化工程:
包括:端口与引脚的定义、起始信号、终止信号、应答与非应答、读写输入函数等等I2C通信的支持函数
*/
#include "stm32f10x.h"
#include "./usart/bsp_usart.h"
#include "./i2c/bsp_i2c_gpio.h"
#include "./i2c/bsp_i2c_eeprom.h"
int main(void)
{
/* 从EEPROM读取到的数据放入临时变量中 */
uint8_t data;
uint8_t addr = 1; // 单元格地址
uint8_t w_data = 12; // 要写入EEPROM的数据
/* USB转串口初始化 */
DEBUG_USART_Config();
/* I2C初始化 */
I2C_GPIO_CONFIG();
/* 检测EEPROM是否正常 */
printf("\r\n ========== 欢迎使用秉火STM32F103RCT6系列开发板 ========== \r\n");
if(EEPROM_CHECK_DEVICE(EEPROM_WRITE_ADDR) == 0)
{
printf("\r\n 检测到EEPROM \r\n");
}
else
{
printf("\r\n 未检测到EEPROM \r\n");
}
/* 向EEPROM写入一个字节数据 */
if(EEPROM_WRITE_BYTE(addr, w_data) == 1)
{
printf("\r\n 写入到EEPROM的存储单元地址【 %d 】中的数据是:【 %d 】 \r\n", addr, w_data);
}
else
{
printf("\r\n 单个字节写入失败 \r\n");
}
/* 从EEPROM读取一个字节数据 */
if(EEPROM_READ_BYTE(addr, &data) == 1)
{
printf("\r\n 从EEPROM的存储单元格【 %d 】中读取到的数据:【 %d 】\r\n", addr, data);
}
else
{
printf("\r\n 从EEPROM读取一个字节失败 \r\n");
}
}
实验测试:
用USB连接板子和电脑,编译程序生成 .hex文件,打开mcuisp.exe,将.hex文件烧录到板子中,打开 秉火串口调试助手V1.0.exe,设置好参数:
参数需和程序中设置的参数一致,否则会出现乱码或者通信失败。

打开串口,按板子上的RESET键,出现的结果:

修改main.c:

重新编译烧录,测试:

以上实现了STM32向EEPROM写入和读取单个字节的操作。

浙公网安备 33010602011771号