STM32F103使用GPIO模拟I2C操作AT24C02
最近找到一块0.96寸OLED屏幕,使用的是I2C通信协议。
但是自己使用GPIO模拟I2C通信总是不成功,猜测应该是自己写的I2C协议代码的问题。
最后移植了别人用STM32硬件I2C操作OLED屏幕的代码,完成后发现用指头触碰OLED引脚的时候会导致STM32卡死,我相信这是之前总提到STM32硬件I2C会跑飞的现象(之后用GPIO模拟I2C通信时不会出现)。
使用GPIO模拟I2C
阅读了之前学习AT24C02写的模拟I2C代码,发现当时写的这破玩意根本不能看(当然现在这个代码可能过几个月也觉得是一坨poop),因此重新写了使用GPIO模拟I2C的代码,使用AT24C02进行测试,并且使用时只需要修改iic_gpio.h中相关宏定义即可切换I2C时钟、端口和引脚。
#define IIC3_SCL GPIO_Pin_2
#define IIC3_SDA GPIO_Pin_3
#define IIC3_CLK RCC_APB2Periph_GPIOA
#define IIC3_PORT GPIOA
由于我的单片机有两个硬件I2C,因此命名为了IIC3。
为了提高模拟IIC的速度,使用直接操作寄存器的方式代替库函数。
#define IIC3_SCL_SET (GPIOA->ODR |= GPIO_Pin_2)
#define IIC3_SCL_CLR (GPIOA->ODR &= ~(GPIO_Pin_2))
#define IIC3_SDA_SET (GPIOA->ODR |= GPIO_Pin_3)
#define IIC3_SDA_CLR (GPIOA->ODR &= ~(GPIO_Pin_3))
#define IIC3_SDA_GET (GPIOA->IDR &= GPIO_Pin_3)
使用时需初始化IIC3,函数为IIC3_Init();
void IIC3_Init(void)
{
IIC3_RCC_Config(); // 配置GPIO时钟
IIC3_GPIO_Config(); // 配置GPIO的模式,I2C需要配置为开漏输出
}
其他函数为I2C的相关操作。
void IIC3_Start(void);
void IIC3_Stop(void);
void IIC3_SendAck(void);
void IIC3_SendNAck(void);
unsigned char IIC3_GetAck(void); //返回值为 ACK or NACK 在头文件中已经定义
void IIC3_WriteByte(unsigned char byte);
unsigned char IIC3_ReadByte(void);
简化延时操作,没有使用systick或者tim定时,而是使用软件延时,系统时钟为72MHz。
void IIC3_Delay(uint16_t ntime);
使用I2C通讯的器件时,只需要在其BSP中包含iic_gpio.h,我这里使用的是绝对路径,没在Options中配置头文件路径。
#include "./Board Support Package/eeprom/iic_gpio.h"
通过I2C协议操作AT24C02
重新写了AT24C02的驱动(应该是叫驱动吧?),将Atmel公司手册中相关读写操作都实现了一下,并且实现了对整数,浮点数,双精度浮点数的操作(这一部分代码的很没有艺术感,之后抽空要优化一下),自己测试了一下感觉没什么问题,但是我觉得还是有点不可靠。
此外 ,如果有幸哪位技术大佬看到的话,还恳请大佬批评指正,自己C语言确实一般,刚刚看完C Peimer Plus,实在感觉刚刚入门,尤其是指针方面的操作,如果有好的建议还请赐教。
使用前可以检查一下AT24C02是否能够通信,其实就是写器件地址,看有没有ACK应答信号。
我使用的板子AT24C02地址引脚全接地了,因此按照手册中的描述地址应该是(0x50 << 1)。
/* EEPROM(AT24C02) Address Define */
#define EEPROM_ADDR (0x50 << 1)
#define EEPROM_WRITEMASK 0xFE
#define EEPROM_READMASK 0x01
AT24C02手册中给出的相关读写操作。
void EEPROM_WriteByte(uint8_t addr, uint8_t data);
void EEPROM_WritePage(uint8_t addr, const uint8_t arr[]);
uint8_t EEPROM_CurrentReadByte(void);
uint8_t EEPROM_RandomReadByte(uint8_t addr);
void EEPROM_SequentialReadByte(uint8_t addr, uint8_t arr[]);
int float double 类型的读写操作。
void EEPROM_intWrite(uint8_t addr, int intdata);
void EEPROM_floatWrite(uint8_t addr, float floatdata);
void EEPROM_doubleWrite(uint8_t addr, double doubledata);
int EEPROM_intRead(uint8_t addr);
float EEPROM_floatRead(uint8_t addr);
double EEPROM_doubleRead(uint8_t addr);
向AT24C02中全部的地址写入同一个数据,可以用来擦除AT24C02。
void EEPROM_FillWrite(uint8_t data);
AT24C02写入时需要延时一定时间,等待AT24C02将数据写入单元,这个延时很重要,必须添加。
void EEPROM_WriteDelay(uint16_t ntime);
测试效果
通过串口打印到串口调试助手。测试代码在task.c中。
void EEPROM_Test(void)
{
uint8_t arr[8] = {
00, 11, 22, 33, 44, 55, 66, 77
};
uint8_t copy[8];
uint8_t i, j;
EEPROM_FillWrite(0x00);
printf( "EEPROM Checking...\n");
if(EEPROM_CHECK() == EEPROM_READY)
printf("EEPROM Ready\n");
else
printf("EEPROM NOT Ready, Please Check It\n\n");
printf("*******************************************\n");
printf("EEPROM Ranrom Write\\Read Test...");
EEPROM_WriteByte(0x54,5);
EEPROM_WriteByte(0x67,2);
printf("\nEEPROM Random Addr[0x54] = %d", EEPROM_RandomReadByte(0x54));
printf("\nEEPROM Random Addr[0x67] = %d", EEPROM_RandomReadByte(0x67));
printf("\n*******************************************\n");
printf("EEPROM Current Read Test...");
printf("\nEEPROM Current Addr[0x68] = %d", EEPROM_CurrentReadByte());
printf("\nEEPROM Random Addr[0x68] = %d", EEPROM_RandomReadByte(0x68));
printf("\n*******************************************\n");
printf("EEPROM intWrite Test...\n");
EEPROM_intWrite(0xB0, 0x11223344);
printf("intdata = %X(%d)", EEPROM_intRead(0xB0), EEPROM_intRead(0xB0));
printf("\n*******************************************\n");
printf("EEPROM floatWrite Test...");
EEPROM_floatWrite(0xC0, -254.987654321);
printf("\n floatdata = %f", EEPROM_floatRead(0xC0));
printf("\n*******************************************\n");
printf("EEPROM doubleWrite Test...");
EEPROM_doubleWrite(0xD0, -123456789.987654321);
printf("\n doubledata = %f", EEPROM_doubleRead(0xD0));
printf("\n*******************************************\n");
printf("EEPROM Pages Write\\Read Test...");
EEPROM_WritePage(0x00, arr);
EEPROM_WritePage(0x08, arr);
for(i = 0; i < 32; i++)
{
EEPROM_SequentialReadByte(i*8, copy);
for(j = 0; j < 8; j++)
{
printf("\n%d:Addr[0x%02x] = %X(%d)", i*8 + j, i*8 + j, copy[j], copy[j]);
}
}
printf("\n*******************************************");
}
效果如下:
学习中遇到的一些问题和经验
## IIC通信
- 连接总线的设备要有独立地址,这个地址可以是 7 位或者 10 位,8位地址时加入了 R\W| 位,为读地址和写地址,才能正确返回数据。
- 总线通过上拉电阻接到电源。当I2C设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。
- 具有三种传输模式:标准模式传输速率为100kbit/s ,快速模式为400kbit/s ,高速模式下可达 3.4Mbit/s,但目前大多I2C设备尚不支持高速模式。
- 连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制。
- 仲裁机制:逻辑 0 比逻辑 1 优先。(?)
- 值得注意的是:推挽输出对应->需要切换输入输出模式;开漏输出对应->不需要切换输入输出模式。输入部分施密特触发器是否打开(更像是一个三态门)决定。
### EEPROM
- Byte Write 时序:起始信号,先发送设备地址+0(LSB),接收应答,再发送写入单元格地址,接收应答,发送写入的数据,接收应答,停止信号。
- Page Write 时序:起始信号,先发送设备地址+0(LSB),接收应答,再发送写入起始单元格地址,接收应答,发送写入的第一个数据,接收应答.....发送写入的第8\16个数据,接收应答,停止信号。
- 2kB 大小一页为 8 个字节,当写入数据大于 8 时,会覆盖最后一个地址的数据。
- Current Address Read:起始信号,先发送设备地址+1(LSB),接收应答,接收读取的数据,发送非应答,停止信号。
- Random Read:[起始信号,先发送设备地址+0(LSB),接收应答,再发送读取单元格地址,接收应答,]发送起始信号,发送设备地址+1(LSB),接收应答,接收读取的单元格的数据,发送非应答,停止信号。
- [Dummy_Write] + Current Address Read
- Sequential Read:[起始信号,先发送设备地址+0(LSB),接收应答,再发送读取单元格地址,接收应答,]发送起始信号,发送设备地址+1(LSB),接收应答,接收读取的单元格的数据,[发送应答]......接收读取的单元格的数据,发送非应答,停止信号。
- sequential:按次序的。随机读取结束发送应答信号,而不是非应答信号。
-
- 注意:操作 SDA 后要释放总线
- 注意:EEPROM 写入时速度缓慢,必须延时等待写入完成。