小熊派gd32f303学习之旅(8)— 使用软件模拟I2C读写EEPROM
一、前言
IIC是一种非常常用的低速通信总线,广泛被用于连接微控制器及其外围设备。
关于IIC的介绍可以参考:IIC通信协议详解
这里我们用软件模拟 IIC来读写AT24C02这个EEPROM。芯片AT24C02的总容量是 256 个字节,该芯片通过 IIC 总线与外部连接。
如下所示,是E53_SC1拓展板上AT24C02的硬件连接图,不过这里有一个很大的问题,就是通过其地址设置引脚的接线我们得到其7位地址为0x51,可实际上,三个地址设置引脚都接到了地,即其7位地址为0x50
;不知道是不是因为改版的原因,所以在使用的时候最好从实际硬件去确认一下其器件地址。
接下来我们查看小熊派的原理图,可以看到其使用的IIC引脚
二、编写软件模拟I2C驱动程序
接下来我们就要编写软件模拟I2C的驱动程序了,首先,将PB6和PB7引脚初始化,
/* 描述:软件模拟IIC引脚初始化
* 参数:无
* 返回值:无*/
void Soft_I2C_Init(void)
{
rcu_periph_clock_enable(RCU_GPIOB); /* 使能GPIOB时钟 */
/* 配置 IIC_SCL --> PB6 引脚为推挽输出 */
gpio_init(GPIOB, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_6);
/* 配置 IIC_SDA --> PB7 引脚为推挽输出 */
gpio_init(GPIOB, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_7);
gpio_bit_set(GPIOB, GPIO_PIN_6);
gpio_bit_set(GPIOB, GPIO_PIN_7);
}
因为模拟IIC通信里需要用到us延时,所以,编写一个us延时函数
/* 描述:us级延时函数
* 参数nus:需要延时的us数
* 返回值:无*/
void delay_us(uint32_t nus)
{
uint32_t ticks;
uint32_t told, tnow, tcnt = 0;
uint32_t reload = SysTick->LOAD; /* 滴答定时器的重装载值 */
ticks = nus * 120; /* 需要的节拍数 */
told = SysTick->VAL; /* 刚进入时的计数器值 */
while(1)
{
tnow = SysTick->VAL;
if(tnow != told)
{
if(tnow < told)tcnt += told - tnow;
else tcnt += reload - tnow + told;
if(tcnt >= ticks)break; /* 时间超过/等于要延迟的时间,则退出. */
told = tnow;
}
}
}
然后就是模拟IIC通信的函数
/* 描述:启动I2C总线,即发送I2C起始条件.
* 参数: 无
* 返回值:无 */
void IIC_Start(void)
{
SDA_OUT();
IIC_SDA(1);
IIC_SCL(1);
delay_us(4);
IIC_SDA(0);
delay_us(4);
IIC_SCL(0);
}
/* 描述:结束I2C总线,即发送I2C结束条件.
* 参数: 无
* 返回值:无 */
void IIC_Stop(void)
{
SDA_OUT();
IIC_SCL(0);
IIC_SDA(0);
delay_us(4);
IIC_SCL(1);
delay_us(4);
IIC_SDA(1);
delay_us(4);
}
/* 描述:发送应答 ACK
* 参数: 无
* 返回值:无 */
void IIC_ACK(void)
{
SDA_OUT();
IIC_SCL(0);
delay_us(2);
IIC_SDA(0);
delay_us(2);
IIC_SCL(1);
delay_us(2);
IIC_SCL(0);
delay_us(1);
}
/* 描述:发送非应答 NACK
* 参数: 无
* 返回值:无 */
void IIC_NACK(void)
{
SDA_OUT();
IIC_SCL(0);
delay_us(2);
IIC_SDA(1);
delay_us(2);
IIC_SCL(1);
delay_us(2);
IIC_SCL(0);
delay_us(1);
}
/* 描述:等待ACK
* 参数: 无
* 返回值:等待应答返回0,没有等待到应答返回1 */
uint8_t IIC_wait_ACK(void)
{
uint8_t t = 200;
SDA_OUT();
IIC_SDA(1);
delay_us(1);
IIC_SCL(0);
delay_us(1);
SDA_IN(); /* 数据发送完后释放数据线,准备接收应答位 */
delay_us(1);
while(READ_SDA) /* 等待IIC应答*/
{
t--;
delay_us(1);
if(t==0)
{
IIC_SCL(0);
return 1;
}
delay_us(1);
}
delay_us(1);
IIC_SCL(1);
delay_us(1);
IIC_SCL(0);
delay_us(1);
return 0;
}
/* 描述:一个字节数据发送函数
* 参数: 无
* 返回值:无 */
void IIC_SendByte(uint8_t byte)
{
uint8_t BitCnt;
SDA_OUT();
IIC_SCL(0);
for(BitCnt=0;BitCnt<8;BitCnt++) /* 要传送的数据长度为8位 */
{
if(byte&0x80) IIC_SDA(1); /* 判断发送位 */
else IIC_SDA(0);
byte<<=1;
delay_us(2);
IIC_SCL(1);
delay_us(2);
IIC_SCL(0);
delay_us(2);
}
}
/* 描述:一个字节数据接收函数
* 参数: 无
* 返回值:接收到的字节数据 */
uint8_t IIC_RcvByte(void)
{
uint8_t retc;
uint8_t BitCnt;
retc=0;
SDA_IN(); /* 设置数据线为输入方式 */
delay_us(1);
for(BitCnt=0;BitCnt<8;BitCnt++)
{
IIC_SCL(0); /* 设置时钟线为低,准备接收数据位 */
delay_us(2);
IIC_SCL(1); /* 设置时钟线为高使数据线上数据有效 */
retc=retc<<1;
if(READ_SDA) retc |=1; /* 读数据位,接收的数据位放入retc中 */
delay_us(1);
}
IIC_SCL(0);
return(retc);
}
三、编写AT24C02控制函数
通过查看AT24C02的数据手册可以看到其读写时序如下
然后编写AT24C02的读取和写入函数
/* 描述:AT24C02初始化
* 参数:无
* 返回值:无*/
void at24c02_init(void)
{
Soft_I2C_Init();
}
/* 描述:在AT24C02指定地址读出一个字节的数据
* 参数:ReadAddr: 需要读出数据的地址
* ReadByte: 读出的数据值的存放指针
* 返回值:0:读取成功 其他:读取错误*/
uint8_t AT24C02_Read_Byte(uint16_t ReadAddr, uint8_t *ReadByte)
{
uint8_t err = 0;
IIC_Start();
IIC_SendByte((AT24C02_Addr<<1) | WRITE_BIT); /* 发送器件地址,写数据 */
err |= IIC_wait_ACK();
IIC_SendByte(ReadAddr); /* 发送要读出数据的地址 */
err |= (IIC_wait_ACK() << 1);
IIC_Start();
IIC_SendByte((AT24C02_Addr<<1) | READ_BIT); /* 进入接收模式 */
err |= (IIC_wait_ACK() << 2);
*ReadByte = IIC_RcvByte();
IIC_NACK(); /* 发送非应答 NACK */
IIC_Stop(); /*产生一个停止条件*/
return err;
}
/* 描述:在AT24C02指定地址写入一个字节的数据
* 参数:WriteAddr: 需要写入数据的地址
* WriteByte: 要写入的数据
* 返回值:0:写入成功 其他:写入错误*/
uint8_t AT24C02_Write_Byte(uint16_t WriteAddr,uint16_t WriteByte)
{
uint8_t err = 0;
IIC_Start();
IIC_SendByte((AT24C02_Addr<<1) | WRITE_BIT); /* 发送器件地址,写数据 */
err |= IIC_wait_ACK();
IIC_SendByte(WriteAddr); /* 发送要写入数据的地址 */
err |= (IIC_wait_ACK() << 1);
IIC_SendByte(WriteByte); /* 写入数据 */
err |= (IIC_wait_ACK() << 2);
IIC_Stop();//产生一个停止条件
return err;
}
四、编写主函数
int main(void)
{
uint8_t buff;
uint8_t err;
/* 配置系统时钟 */
systick_config();
/* 初始化LED */
// led_init();
/* 初始化USART0 */
uart0_init(115200);
/* 初始化AT24C02 */
at24c02_init();
/* 通过串口打印 Hello world! */
u0_printf("Hello world! ");
u0_printf("I am William. \r\n");
err = AT24C02_Write_Byte(0x0a, 0xa5);
if(err == 0)
printf("Write 0xa5 to addr 0x0a ok \r\n");
else
{
printf("Write 0xa5 to addr 0x0a err \r\n");
printf("err num : 0x%x \r\n",err);
}
if(AT24C02_Read_Byte(0x0a, &buff) == 0)
printf("Read data: 0x%x from addr 0x0a ok \r\n", buff);
else
printf("Read data from addr 0x0a err \r\n");
while(1)
{
if(UART0_RX_STAT > 0)
{
UART0_RX_STAT = 0;
u0_printf("RECEIVE %d data:%s \r\n", UART0_RX_NUM, UART0_RX_BUF);
}
delay_1ms(10);
}
}
五、功能验证
编译链接烧录到小熊派开发板,然后观察串口输出情况,可以看到读取和写入都成功了
六、附录
完整代码我存放在码云,可以查看:https://gitee.com/william_william/BearPi-GD32F303RGT6.git
上一篇:小熊派gd32f303学习之旅(7)—使用PWM实现LED呼吸灯
下一篇:小熊派gd32f303学习之旅(9)— 使用硬件I2C读写EEPROM