本实验目的:用MCU的GPIO口模拟I2C时序对存储器EEPROM进行数据读出和写入
所用开发板:STM32F103ZET6
所用库函数:STM32 HAL 库
开发环境— :Keil u5.27
参考文章— :I2C时序参考
时序可以阅读大佬所写的I2C介绍,我这里不在过多讲解。 I2C时序参考
- 起始信号:
/**
* @brief I2C起始信号
**/
void I2C_start()
{
_SDA(1); //拉高SDA
delay_us(10); //SDA持续时间大于4.7us
_SCL(1); //拉高SCL
delay_us(10);
_SDA(0); //拉低SDA
delay_us(10); //大于4us后SCL拉低
_SCL(0); //拉低SCL,起始信号已发出,准备发送数据(如果不拉低的话,SDA跳变就是起始信号或者停止信号,就不是数据信号了)
}
- 停止信号:
/**
* @brief I2C停止信号
**/
void I2C_stop()
{
_SDA(0); //拉低SDA
delay_us(10); //SDA持续时间大于4.7us
_SCL(1); //拉高SCL
delay_us(10);
_SDA(1); //拉高SDA产生应答信号
delay_us(10);
}
- MCU产生应答信号:
/**
* @brief 产生应答
**/
void ack_R()
{
_SCL(0); //确保SDA跳变不是产生开始信号和停止信号
delay_us(10);
_SDA(0); //拉低SDA产生应答
_SCL(1); //拉高SCL
delay_us(10);
_SCL(0); //拉低SCL
}
- MCU产生非应答信号:
/**
* @brief 产生非应答
**/
void ack_N()
{
_SCL(0); //确保SDA跳变不是产生开始信号和停止信号
delay_us(10);
_SDA(1); //拉高SDA产生非应答
_SCL(1); //拉高SCL
delay_us(10);
_SCL(0); //拉低SCL
}
}
- MCU等待EEPROM返回应答信号:
/**
* @brief 等待应答
* @retval 1:非应答 0:应答
**/
uint8_t ACK_wait()
{
uint16_t count; //等待应答计数
uint8_t ack;
_SDA(1); //主机拉高SDA,使得SDA引脚可以作为输入
delay_us(10);
_SCL(1); //主机拉高SCL,等待应答信号产生。
delay_us(10);
while(I2C_READ_SDA==1) //等待应答产生
{
count++;
if(count>2000)
{
lcd_show_string(0,0,100,50,16,(char*)"ack is nack",BLACK);
I2C_stop(); //通讯异常,立即停止
ack = 1; //从机产生了非应答信号
}
}
lcd_show_string(0,0,100,50,16,(char*)"ack is ack",BLACK);
_SCL(0); //拉低SCL,等待应答结束,开始发送下一字节数据
return ack;
}
- MCU向EEPROM写一字节数据:
/**
* @brief 主机向从机写一个字节数据
* @param data:发送的字节数据
**/
void I2C_write_byte(uint8_t data)
{
uint8_t i;
for(i=0;i<8;i++)
{
_SCL(0); //拉低SCL开始发送数据
if(data&0x80) //发送数据的最高位
{
_SDA(1);
}
else
{
_SDA(0);
}
data<<=1; //次高位变成高位
delay_us(10);
_SCL(1); //拉高SCL将数据发送出去
delay_us(10);
}
_SCL(0); //拉低SCL准备接受应答信号
}
- MCU向EEPROM读一字节数据:
/**
* @brief 主机向从机读字节数据
* @param ack 1:产生非应答 0:产生应答
* @retval 主机读取的数据
**/
uint8_t I2C_read_byte(uint8_t ack)
{
uint8_t i,res ;
for(i=0;i<8;i++)
{
_SDA(1); //主机拉高SDA,使得SDA引脚可以作为输入
_SCL(0); //拉低SCL
delay_us(10);
_SCL(1); //拉高SCL读取一位数据
res<<=1;
if(I2C_READ_SDA==1){res++;}//如果读取的SDA为1,则接受的数据+1
delay_us(10);
}
if(ack)
{
ack_N();
}
else
{
ack_R();
}
return res;
}
- I2C.c文件:
/**
****************************************************************************************************
* @file I2C.c
* @author TGU_LINBO
* @version V1.0
* @date 2023-02-01
* @brief 软件模拟I2C程序
* @GPIO SCL:PB6 SDA: PB7
****************************************************************************************************
**/
#include "I2C.h"
/**
* @brief I2C初始化函数
**/
void I2C_init()
{
GPIO_InitTypeDef gpio_init_struct;
C02_GPIO_CLK_ENABLE(); //GPIOB时钟使能
gpio_init_struct.Pin = C02_SCL; //SCL引脚
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(C02_GPIO_PORT, &gpio_init_struct); //初始化SCL引脚
gpio_init_struct.Pin =C02_SDA; //SDA引脚
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_OD; //开漏输出
HAL_GPIO_Init(C02_GPIO_PORT, &gpio_init_struct); //初始化SDA引脚
/*****空闲状态*****/
_SCL(0);
_SDA(0);
delay_ms(10);
}
/**
* @brief I2C起始信号
**/
void I2C_start()
{
_SDA(1); //拉高SDA
delay_us(10); //SDA持续时间大于4.7us
_SCL(1); //拉高SCL
delay_us(10);
_SDA(0); //拉低SDA
delay_us(10); //大于4us后SCL拉低
_SCL(0); //拉低SCL,起始信号已发出,准备发送数据(如果不拉低的话,SDA跳变就是起始信号或者停止信号,就不是数据信号了)
}
/**
* @brief I2C停止信号
**/
void I2C_stop()
{
_SDA(0); //拉低SDA
delay_us(10); //SDA持续时间大于4.7us
_SCL(1); //拉高SCL
delay_us(10);
_SDA(1); //拉高SDA产生应答信号
delay_us(10);
}
/**
* @brief 等待应答
* @retval 1:非应答 0:应答
**/
uint8_t ACK_wait()
{
uint16_t count; //等待应答计数
uint8_t ack;
_SDA(1); //主机拉高SDA,使得SDA引脚可以作为输入
delay_us(10);
_SCL(1); //主机拉高SCL,等待应答信号产生。
delay_us(10);
while(I2C_READ_SDA==1) //等待应答产生
{
count++;
if(count>2000)
{
lcd_show_string(0,0,100,50,16,(char*)"ack is nack",BLACK);
I2C_stop(); //通讯异常,立即停止
ack = 1; //从机产生了非应答信号
}
}
lcd_show_string(0,0,100,50,16,(char*)"ack is ack",BLACK);
_SCL(0); //拉低SCL,等待应答结束,开始发送下一字节数据
return ack;
}
/**
* @brief 产生非应答
**/
void ack_N()
{
_SCL(0); //确保SDA跳变不是产生开始信号和停止信号
delay_us(10);
_SDA(1); //拉高SDA产生非应答
_SCL(1); //拉高SCL
delay_us(10);
_SCL(0); //拉低SCL
}
/**
* @brief 产生应答
**/
void ack_R()
{
_SCL(0); //确保SDA跳变不是产生开始信号和停止信号
delay_us(10);
_SDA(0); //拉低SDA产生应答
_SCL(1); //拉高SCL
delay_us(10);
_SCL(0); //拉低SCL
}
/**
* @brief 主机向从机写一个字节数据
* @param data:发送的字节数据
**/
void I2C_write_byte(uint8_t data)
{
uint8_t i;
for(i=0;i<8;i++)
{
_SCL(0); //拉低SCL开始发送数据
if(data&0x80) //发送数据的最高位
{
_SDA(1);
}
else
{
_SDA(0);
}
data<<=1; //次高位变成高位
delay_us(10);
_SCL(1); //拉高SCL将数据发送出去
delay_us(10);
}
_SCL(0); //拉低SCL准备接受应答信号
}
/**
* @brief 主机向从机读字节数据
* @param ack 1:产生非应答 0:产生应答
* @retval 主机读取的数据
**/
uint8_t I2C_read_byte(uint8_t ack)
{
uint8_t i,res ;
for(i=0;i<8;i++)
{
_SDA(1); //主机拉高SDA,使得SDA引脚可以作为输入
_SCL(0); //拉低SCL
delay_us(10);
_SCL(1); //拉高SCL读取一位数据
res<<=1;
if(I2C_READ_SDA==1){res++;}//如果读取的SDA为1,则接受的数据+1
delay_us(10);
}
if(ack)
{
ack_N();
}
else
{
ack_R();
}
return res;
}
- I2C.h文件:
/**
****************************************************************************************************
* @file I2C.h
* @author TGU_LINBO
* @version V1.0
* @date 2023-02-01
****************************************************************************************************
**/
#ifndef __I2C_H
#define __I2C_H
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LCD/lcd.h"
/********引脚宏定义********/
#define C02_SCL GPIO_PIN_6 //24C02 SCL引脚
#define C02_SDA GPIO_PIN_7 //24C02 SDA引脚
#define C02_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0) /* 所在IO口时钟使能 */
#define C02_GPIO_PORT GPIOB
/* 24C02端口定义 */
#define _SCL(x) do{ x ? \
HAL_GPIO_WritePin(C02_GPIO_PORT,C02_SCL,GPIO_PIN_SET) : \
HAL_GPIO_WritePin(C02_GPIO_PORT,C02_SCL,GPIO_PIN_RESET); \
}while(0) /* SCL翻转 */
#define _SDA(x) do{ x ? \
HAL_GPIO_WritePin(C02_GPIO_PORT,C02_SDA,GPIO_PIN_SET) : \
HAL_GPIO_WritePin(C02_GPIO_PORT,C02_SDA,GPIO_PIN_RESET); \
}while(0) /* SDA翻转 */
#define I2C_READ_SDA HAL_GPIO_ReadPin(C02_GPIO_PORT,C02_SDA) /* 读取SDA */
/********函数声明********/
void I2C_init(void); //I2C初始化
void I2C_start(void); //起始信号
void I2C_stop(void); //停止信号
uint8_t ACK_wait(void); //主机等待应答
void ack_N(void); //主机产生非应答
void ack_R(void); //主机产生应答
void I2C_write_byte(uint8_t data);//主机向从机写一个字节数据
uint8_t I2C_read_byte(uint8_t ack);//主机向从机读取一个字节数据
#endif
EEPROM 24C02 阅读大佬的文章即可AT24C02参考
- 24C02.c:
/**
****************************************************************************************************
* @file 24c02.c
* @author TGU_LINBO
* @version V1.0
* @date 2023-02-01
* @brief AT24C02 I2C 读写程序
* @GPIO SCL:PB6 SDA: PB7
****************************************************************************************************
**/
#include "24c02.h"
#include "I2C.h"
#include <string.h>
/**
* @brief 主机向24c02选中地址写入一字节数据
* @param add:写入的地址 data 写入的数据
**/
void write_byte(uint8_t add,uint8_t data)
{
I2C_start(); //发送开始信号
I2C_write_byte(0XA0); //发送24C02的从设备地址,为写操作
ACK_wait(); //等待应答
I2C_write_byte(add); //发送要写入的地址
ACK_wait();
I2C_write_byte(data); //发送要写入的数据
ACK_wait();
I2C_stop(); //发送停止信号
delay_ms(10); //注意: EEPROM 写入比较慢,必须等到10ms后再写下一个字节
}
/**
* @brief 主机向24c02选中地址读入一字节数据
* @param add:需要读出的地址 *buf 写入的数据存入buf数组中
* @retval 读取的数据
**/
uint8_t read_byte(uint8_t add)
{
uint8_t res;
I2C_start(); //发送开始信号
I2C_write_byte(0XA0); //发送24C02的从设备地址,为写操作
ACK_wait(); //等待应答
I2C_write_byte(add); //发送要读取的地址
ACK_wait();
I2C_start(); //发送开始信号
I2C_write_byte(0XA1); //发送24C02的从设备地址,为读操作
ACK_wait();
res=I2C_read_byte(0);//开始读取
I2C_stop(); //发送停止信号
return res;
delay_ms(10); //注意: EEPROM 读取比较慢,必须等到10ms后再写下一个字节
}
/**
* @brief 主机向24c02选中地址开始写入一连串数据
* @param add:写入的起始地址 buf:写入的数据
**/
void write_byte_con(uint8_t add,uint8_t *buf)
{
uint8_t i,count,addr;
addr=add;
count=sizeof(buf); //计算写入的数组长度
for(i=0;i<15;i++)
{
write_byte(addr+i,buf[i]);
delay_ms(10);
}
}
/**
* @brief 主机向24c02选中地址开始读取一连串数据
* @param add:开始读取的起始地址 buf:读取的数据存放数组
* @param data_len:需要读取的数据长度
**/
void read_byte_con(uint8_t add,uint8_t *buf,uint8_t data_len)
{
I2C_start(); //发送开始信号
I2C_write_byte(0XA0); //发送24C02的从设备地址,为写操作
ACK_wait(); //等待应答
I2C_write_byte(add); //发送要读取的地址
ACK_wait();
I2C_start(); //发送开始信号
I2C_write_byte(0XA1); //发送24C02的从设备地址,为读操作
ACK_wait();
for(int i=0;i<data_len;i++)
{
buf[i]=I2C_read_byte(0);//开始读取
delay_ms(10); //注意: EEPROM 读取比较慢,必须等到10ms后再写下一个字节
}
I2C_read_byte(1); //发送一个非应答信号告诉从机不在读取数据了
I2C_stop(); //发送停止信号
delay_ms(10);
}
- 24C02.h:
/**
****************************************************************************************************
* @file 24c02.h
* @author TGU_LINBO
* @version V1.0
* @date 2023-02-01
* @brief AT24C02 I2C 读写程序
* @GPIO SCL:PB6 SDA: PB7
****************************************************************************************************
**/
#ifndef __24C02_H
#define __24C02_H
#include "I2C.h"
/********函数声明********/
void write_byte(uint8_t add,uint8_t data); //主机向24C02指定地址写入数据
uint8_t read_byte(uint8_t add); //主机向24C02指定地址读取数据
void write_byte_con(uint8_t add,uint8_t *buf);//主机向24c02选中地址开始写入一连串数据
void read_byte_con(uint8_t add,uint8_t *buf,uint8_t data_len); //主机向24c02选中地址开始读取一连串数据
#endif
这里我将通过STM32F103ZET6将数据存储在EEPROM中,然后再从EEPROM中读取出来。
- main.c:
/**
****************************************************************************************************
* @file main.c
* @author TGU_LINBO
* @version V1.0
* @date 2023-02-01
* @brief 软件模拟I2C程序读写2C402存储器程序
* @GPIO SCL:PB6 SDA: PB7
****************************************************************************************************
**/
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "I2C.h"
#include "24C02.h"
#include "key.h"
int main(void)
{
uint8_t x= 0;
uint8_t lcd_id[12];
uint8_t addres = 0x1f; /* 写入的地址 */
uint8_t Data[] = {'I','L','O','V','E','Y','O','U','B','A','B','Y','Y','E','S'}; /* 写入的数据 */
uint8_t R_Data[]={0}; /* 初始化HAL库 */
uint8_t key;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
delay_init(72); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
I2C_init(); /* 初始化I2C */
key_init(); /* 初始化按键 */
g_point_color = BLACK;
sprintf((char *)lcd_id, "LCD ID:%04X", lcddev.id); /* 将LCD ID打印到lcd_id数组 */
lcd_clear(WHITE);
/****多字节读写字节数据测试****/
printf("写入的数据为:\r\n");
write_byte_con(addres,Data);
for(int p=0;p<sizeof(Data);p++)
{
printf("%c ",Data[p]);
}
printf("\r\n");
printf("读入的数据为:\r\n");
read_byte_con(addres,R_Data,sizeof(Data));
for(int p=0;p<sizeof(Data);p++)
{
printf("%c ",R_Data[p]);
}
printf("\r\n");
while (1)
{
}
}
下载到单片机后,单片机通过串口助手打印实验信息
代码整个工程在我的GitHub上本实验工程代码下载免费