上一篇《I2C协议详解》
我们了解了I2C的操作流程,这一篇,我们就使用I2C,来对EEPROM进行操作吧。
我们做两种选择:
1.时序由IO口模拟高低电平,需要了解协议并按照协议操作相应的IO口。
2.时序由硬件自行产生,不需要人工干预;
由硬件产生的I2C时序,我们借助Stm32Cube配置实现便可,我们这一篇,抛开Stm32Cube,手撕代码,根据I2C的时序,一步步地实现I2C对EEPROM的读写吧。
我们分为几个步骤来对EEPROM进行操作:
1. 发动"看硬件原理图"技能,确定I2C连线;
2. 发动"乾坤大拷贝"技能,配置对应的IO口;
3. 发动"手撕代码"技能,写出相应时序;
1. 发动"看硬件原理图"技能,确定I2C连线;
第9页,找到EEPROM那一块,EEPROM用的是AT24C02,只有2K Bits,不是很大。AT24C02由ATMEL公司生产,其命名规则为 AT24Cxx,xx可以=02,04,32,64,128,256,512,1M 等等,xx也表示容量,单位是 Bits,注意,是位,不是字节,要换成字节要除以8,所以,AT24C02只有 256 个字节。别问我怎么知道的,AT24C02的规格书告诉我的,要记得看原理图,看规格书喔~~~
再找呀找呀找朋友,找到 SCL连到PB6,SDA连到PB9。咋找?搜索呗,搜引脚 I2C1_SCL就行了。
确定了I2C连线,SCL=PB6,SDA=PB9,接下来,我们就
2. 发动"乾坤大拷贝"技能,配置对应的IO口;
先新建两个文件,io_i2c.c/io_i2c.h,我们就在这里面写i2c时序。
并且把文件添加进工程项目里参与编译。
配置IO口,详见《STM32CubeMx 创建第一个工程》,把那段代码拷贝过来,改一下配置就行了。
void IOI2C_GpioInit(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
__HAL_RCC_GPIOB_CLK_ENABLE();
/*Configure GPIO pin Output Level */
// PB6 = SCL/PB9 = SDA
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6|GPIO_PIN_9, GPIO_PIN_SET);
/*Configure GPIO pin : PC7 */
GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // 记不记得i2c要上拉?
GPIO_InitStruct.Pull = GPIO_PULLUP; //
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
记得在初始化时调用一下它喔:
MX_GPIO_Init();
MX_DMA_Init();
MX_UART4_Init();
MX_TIM1_Init();
MX_TIM2_Init();
/* USER CODE BEGIN 2 */
USR_UartInit();
IOI2C_GpioInit(); // 我在这,快看这里~
/* USER CODE END 2 */
printf("Start System: Io I2C.\r\n");
3. 发动"手撕代码"技能,写出相应时序,I2C时序,请参照《I2C协议详解》。
看图,会有高电平持续一段时间,低电平持续一段时间的操作,这个持续一段时间,就是延时,关于延时,详细参照《STM32精准延时》。
这个持续多长时间怎么算呢?
根据I2C时钟频率,可以算出周期,再根据周期,算出持续多长时间。
For Example:
100kHz的频率,也就是1秒钟内,有100,000个时钟周期。
那1个时钟周期,也就是 1/100,000秒 = 1,000/100,000毫秒 = 1,000,000/100,000微秒,根据小学数学,算出,100kHz频率的时钟周期是 10微秒,一高一低一周期,那么,延时 = 5us。
在《STM32精准延时》篇中,我们做了个us级的精准延时,用上:
#define I2C_Delay USER_Delay1us(5)
如果要其它延时呢?
delay = 1,000,000 / freq / 2 = 500,000 / freq
算出来delay与时钟频率的关系:
1us = 500kHz,2us = 250kHz,3us = 166.67kHz,4us = 125kHz,5us = 100kHz,
6us = 83.33kHz,7us = 71.4kHz,8us = 62.5kHz,9us = 55.56kHz,10us = 50kHz。
选一个,但要符合芯片的最高支持速率喔。图:400k
知道延时,也知道SCL对应的IO口pb6,那么,要在SCL上生成一个周期为 10us 的方波,怎么写程序呢?
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); // 高电平
USER_Delay1us(5); // 持续5us
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); // 低电平
USER_Delay1us(5); // 持续5us
我们还可以把IO口拉高拉低写成宏定义:
#define SwI2cSetScl() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET)
#define SwI2cClrScl() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET)
#define SwI2cCheckScl() HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_6)
#define SwI2cSetSda() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_SET)
#define SwI2cClrSda() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_RESET)
#define SwI2cCheckSda() HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_9)
这样的好处是,如果后面,你不想用 pb6 和 pb9 作为 scl 和 sda,你想换两根io口,只需要改初始化和这些宏定义就行了。
现在,可以开始写时序了。
a、START: SCL为高电平,SDA从高电平跳变至低电平,表示开始发送数据:
SwI2cSetSda();
SwI2cSetScl();
I2C_Delay;
SwI2cClrSda();
I2C_Delay;
SwI2cClrScl();
I2C_Delay;
b、TRANSMIT: START之后,就可以发送数据了,数据在SCL高电平有效,发送8-Bit数据(1BYTE),
i2c传输数据,是从高位最先传输的,比如,0xaa = 10101010b,先传 bit7=1,接下来再传bit6=0……最后传bit0=0:
uint8_t bit_sel;
bit_sel = 0x80;
while (bit_sel) {
if (bit_sel & data)
SwI2cSetSda();
else
SwI2cClrSda();
I2C_Delay;
SwI2cSetScl();
I2C_Delay;
SwI2cClrScl();
bit_sel >>= 1;
}
data&0x80,data&0x40,data&0x20,data&0x10, data&0x08,data&0x04,data&0x02,data&0x01,
这一系列的操作,就把data从bit7取到bit0,按照要求,设置 sda 高低电平,记得,scl一个周期取一次sda值,高电平有效。
3、ACK:在第9个SCL周期,次设备将SDA拉低,主设备检测SDA为低电平,表示收到数据;如果在一个时钟周期内都没有ACK,说明从设备有问题,主设备必须发STOP,表示传输结束。
// ack
SwI2cSetSda(); // 释放sda线,等待从设备把sda接低
SwI2cSetScl();
I2C_Delay;
ack = SwI2cCheckSda(); // 检测 sda 电平
SwI2cClrScl();
I2C_Delay;
if(ack) // 从设备在一个时钟周期内没有响应。
{
// 发送 stop 信号,传输结束
debug_msg("io_i2c not ack. \r\n"); // 打印
return I2CSW_BUSY; // 返回错误信息
}
如果继续有数据要发,重复b、c步骤,如果没有,发STOP。
d、STOP:SCL为高电平,SDA从低电平跳变至高电平,表示发送数据结束;
SwI2cClrSda();
SwI2cSetScl();
I2C_Delay;
SwI2cSetSda();
I2C_Delay;
好啦,I2C时序就这样了,我们接着就用 I2C时序,来对 AT24C02 进行操作。
首先,需要一个设备地址,因为I2C总线上允许挂多个设备,设备地址,用于识别哪个设备。
看原理图+AT24C02 datasheet
A2/A1/A0引脚接地,为000,最低位(LSB)为低电平是写,高电平为读。
所以,地址为 0xA0(写),0xA1(读)。
根据datasheet描述,AT24C02的操作一般有 字节写、页写、随机读、连续读。
字节写和页写:
字节写:就是写一个字节。
START -> 设备地址 -> 寄存器地址(写eeprom哪个存储单元) -> 数据 -> STOP
页写:就是写一页。
START -> 设备地址 -> 寄存器地址(起始) -> 一连串数据 -> STOP
页写中的一连串数据,到底是多大的串呢?最大8BYTE,别问我怎么知道,DATASHEET。
根据描述,页写,其实跟字节写很相似,只不过是,寄存器地址的低3位,会自动增加。
页写存储单元 0x10,8个字节数据,
0x10 = 0001 0000b,寄存器低3位自动增加就是:
0001 0000b = 数据1,0001 0001b = 数据2,……,0001 0111b = 数据8
页写存储单元 0x12,8个字节数据呢?
0x12 = 0001 0010b,寄存器低3位自动增加就是:
0001 0010b = 数据1,0001 0011b = 数据2,……,0001 0111b = 数据5
0001 0000b = 数据6,0001 0001b = 数据7
结合上面的文字描述,存储单元地址到0001 0111b后,回滚到0001 0000b了,这是需要注意的地方,所以,页写,要注意处理页的边界问题。
写的代码如下:
1、存储单元地址传入*pMemAddr和memAddrLen,因为AT24C32/64/128/256,它的存储单元地址是16位的,这样这个函数也用在其它EEPROM上,而不需要再写一个函数了。
2、这里暂未对页写的页边界进行处理,因为其它EEPROM的页不是8个字节,页的处理,留在EEPROM写的时候做。
3、如果需要写一个字节,这个函数的最后一个参数 len 写1就行了。
uint8_t IOI2C_WriteBlock(uint8_t devAddr, uint8_t *pMemAddr, uint8_t memAddrLen, const uint8_t *pData, uint16_t len)
{
uint8_t ctl;
uint8_t startLimit = I2C_TIME_OUT;
//Validate input parameter
if (pData == NULL)
return FALSE;
if (len == 0)
return FALSE;
// I2C Start, Wait while device is busy
ctl = devAddr | I2C_WR;
while (IOI2C_Start(ctl) != IOI2C_OK) {
startLimit--;
if(!startLimit) {
debug_msg("IOI2C_WriteBlock start error.\r\n");
return IOI2C_BUSY;
}
}
// send slave write sub-address
while (memAddrLen--) {
if (IOI2C_TransmitByte(*pMemAddr++) != IOI2C_OK) {
debug_msg("IOI2C_WriteBlock write mem address error.\r\n");
return IOI2C_BUSY;
}
}
// write data from buffer into slave device
while(len--)
{
if(IOI2C_TransmitByte(*pData++) != IOI2C_OK) // Data address
{
debug_msg("IOI2C_WriteBlock write data(%d) error.\r\n", len);
return IOI2C_BUSY;
}
}
IOI2C_Stop(); // end stop operation
return IOI2C_OK;
}
随机读和连续读:
随机读,就是随便读个字节,读哪就是哪。
START->设备地址->存储单元地址->START->设备地址->读->STOP
连续读,就是随便读n个字节,读哪块算哪块,就是在随便读的基础上不停地读,直到读够了再发STOP
START->设备地址->存储单元地址->START->设备地址->读->读->读->……->读->STOP
直接撕代码:
1、存储单元地址传入*pMemAddr和memAddrLen,因为AT24C32/64/128/256,它的存储单元地址是16位的,这样这个函数也用在其它EEPROM上,而不需要再写一个函数了。
2、既然叫随便读,那就不存在着页边界的问题了。
3、如果需要读一个字节,这个函数的最后一个参数 len 写1就行了。
4、每读完一个字节,第9个SCL,需要主设备向从设备发ACK,告诉从设备收到,下一个。
uint8_t IOI2C_ReadBlock(uint8_t devAddr, uint8_t *pMemAddr, uint8_t memAddrLen, uint8_t *pData, uint16_t len)
{
uint8_t ctl;
uint8_t startLimit = I2C_TIME_OUT;
//Validate input parameter
if (pData == NULL)
return FALSE;
if (len == 0)
return FALSE;
// I2C Start, Wait while device is busy
ctl = devAddr | I2C_WR;
while (IOI2C_Start(ctl) != IOI2C_OK) {
startLimit--;
if(!startLimit) {
debug_msg("IOI2C_ReadBlock start error.\r\n");
return IOI2C_BUSY;
}
}
// send slave read sub-address
while (memAddrLen--) {
if (IOI2C_TransmitByte(*pMemAddr++) != IOI2C_OK) {
debug_msg("IOI2C_ReadBlock write mem address error.\r\n");
return IOI2C_BUSY;
}
}
// Repeat start for read operation
ctl = devAddr | I2C_RD;
if (IOI2C_Start(ctl) != IOI2C_OK)
{
debug_msg("IOI2C_ReadBlock Repeat Start Error.\r\n");
return IOI2C_BUSY;
}
//Read data from slave device into buffer
while(--len)
{
*pData++ = IOI2C_GetByte(); // Read data into buffer
IOI2C_SendAck(); // send ack to slave
}
*pData = IOI2C_GetByte(); // Read last data byte
IOI2C_Stop(); // end stop operation
return IOI2C_OK;
}
其实写完这两个函数,基本上也差不多了,在应用中调用这两个函数,便可对EEPROM进行读写,不过为了程序可读性和可移植性,我们再来写个at24c02.c和at24c02.h,将 at24c32 的读写逻辑封装一下,在这个封装里面,我们会处理"页写"的回滚逻辑,为了保证可靠性,我们会多读或者多写几次。
套路:新建文件 -> 加入编译器,往上看,新建 io_i2c.c 里有步骤。
开始继续手撕代码,我们就实现以下5个函数:
uint8_t AT24C02_ReadByte(uint8_t memAddr)
{
uint8_t data, count=0;
uint8_t result = IOI2C_BUSY;
do {
result = I2C_READBUF(&memAddr, &data, 1);
if(++count > 1)
printf("AT24C04_ReadByte error,count = %d \r\n",count);
} while ((count < CHECK_LIMIT) && (IOI2C_OK != result));
return data;
}
uint8_t AT24C02_WriteByte(uint8_t memAddr, uint8_t data)
{
uint8_t count=0;
uint8_t result = IOI2C_BUSY;
do {
result = I2C_WRITEBUF(&memAddr, &data, 1);
if(++count > 1)
printf("AT24C04_WriteByte error,count = %d \r\n",count);
} while ((count < CHECK_LIMIT) && (IOI2C_OK != result));
return result;
}
uint8_t AT24C02_ReadBuffer(uint8_t memAddr, uint8_t *buff, uint16_t len)
{
uint8_t count=0;
uint8_t result = IOI2C_BUSY;
do {
result = I2C_READBUF(&memAddr, buff, len);
if(++count > 1)
printf("AT24C04_ReadBuffer error,count = %d \r\n",count);
} while ((count < CHECK_LIMIT) && (IOI2C_OK != result));
return result;
}
uint8_t AT24C02_WriteBuffer(uint8_t memAddr, uint8_t *buff, uint16_t len)
{
uint8_t dataCnt, count=0;
uint8_t result = IOI2C_BUSY;
uint8_t pagesize;
// Write buffer of data
while (len) {
// Number of bytes available in current page.
pagesize = PAGE_SIZE - (memAddr % PAGE_SIZE);
// Is current page has enough byte for the buffer.
if (len > pagesize)
{
// Open new page?
if (pagesize == PAGE_SIZE) // No
{
// CASE3: The rest of the buffer is more than a page size.
dataCnt = PAGE_SIZE; // Yes
}
else
{
// CASE2: Current page has not enough byte for the buffer,
// write some data in this page and the rest in other
// page(s).
dataCnt = pagesize; // No
}
}
else
{
// CASE1: Current page has enough byte for the buffer.
// CASE4: Finished up the rest of the buffer.
dataCnt = len; // Yes
}
// Write to EEPROM and make sure success
count = 0;
do {
result = I2C_WRITEBUF(&memAddr, buff, dataCnt);
if(++count > 1)
printf("AT24C04_WriteBuffer error,count = %d \r\n",count);
} while ((count < CHECK_LIMIT) && (IOI2C_OK != result));
buff += dataCnt; // Adjust pointer to B_Count
memAddr += dataCnt; // Adjust register address
len -= dataCnt; // Adjust buffer count
delay_ms(10);
}
return result;
}
void AT24C02_Debug(uint8_t func, uint16_t pa1, uint16_t pa2, uint16_t pa3)
{
uint16_t cnt;
uint8_t temp[256];
printf("AT24C02_Debug: func(%d), pa1(%d), pa2(%d), pa3(%d).\r\n",
func, pa1, pa2, pa3);
switch (func) {
case 0: {
printf("func0: read at24c02 byte: addr(0x%x) data(0x%x).\r\n",
pa1, AT24C02_ReadByte((uint8_t)pa1) );
}
break;
case 1: {
memset(temp, 0, 256);
AT24C02_ReadBuffer((uint8_t)pa1, temp, pa2);
printf("func1: read buffer: addr(0x%x) len(%d).", pa1, pa2);
for (cnt = 0; cnt < (uint8_t)pa2; cnt++) {
if (cnt % 16 == 0)
printf("\r\n");
printf("0x%x, ", temp[cnt]);
}
printf("\r\n");
}
break;
case 2: {
printf("func2: at24c02 write: addr(0x%x) data(0x%x).\r\n", pa1, pa2);
AT24C02_WriteByte((uint8_t)pa1, (uint8_t)pa2);
}
break;
case 3: {
for (cnt = 0; cnt < 256; cnt++)
temp[cnt] = cnt;
printf("func3: write buffer: addr(0x%x) len(%d).\r\n", pa1, pa2);
AT24C02_WriteBuffer((uint8_t)pa1, temp, pa2);
}
break;
default:
printf("func0: read at24c04 byte.\r\n");
printf("func1: read at24c04 buffer.\r\n");
printf("func2: write at24c04 byte.\r\n");
printf("func3: write at24c04 buffer.\r\n");
break;
}
}
好了,在Debug函数里,调用
static void DebugCmdProceed(void)
{
switch(debArray[0])
{
case DEBCMD_TEST:
printf("DEBCMD_TEST: Parm1: %d / parm2: %d \r\n", debArray[1], debArray[2]);
break;
case DEBCMD_EEPROM:
AT24C02_Debug((uint8_t)debArray[1], debArray[2], debArray[3], debArray[4]);
break;
}
}
编译、烧录、运行、连上串口调试助手、执行:
1.、写字节
@cmd 0x1 2 0 0xac
@cmd 0x1 2 1 0xac
@cmd 0x1 2 2 0xac
2、读字节
@cmd 0x1 0 0
@cmd 0x1 0 1
@cmd 0x1 0 2
看,读回的就是写进去的吧?
3.、写一串,就往0x13存储单元写14个字节吧。
@cmd 0x1 3 0x13 14
4、读一串,就从0x12读16个字节吧。
@cmd 0x1 1 0x12 16
看0x13起,14个字节,就是写入的。
整个工程及代码呢,请上百度网盘上下载:
链接:https://pan.baidu.com/s/19usUcgZPX8cCRTKt_NPcfg
密码:07on
文件夹:\Stm32CubeMx\Code\IoI2c.rar
本章自己手撕的代码主要是 io_i2c.c/io_i2c.h 和 at24c02.c/at24c02.h
问题:
上面讲到使用延时,编写I2C的时序,根据计算得出,延时和时钟频率的关系是:
1us = 500kHz,2us = 250kHz,3us = 166.67kHz,4us = 125kHz,5us = 100kHz,
6us = 83.33kHz,7us = 71.4kHz,8us = 62.5kHz,9us = 55.56kHz,10us = 50kHz。
那么,如果,我想要用 400kHz 呢?300kHz呢?懵了吧?
下一篇,我带你进入,硬件I2C的世界,不管你用 400k,300k都没有问题。
上一篇:《I2C协议详解》
下一篇:《STM32 使用硬件I2C接口读写EEPROM》
回目录:《目录》