一、原理图
—— —— —— —— —— —— —— —— 软件IIC—— —— —— —— —— —— —— ——
二、 CubeMX配置
Step1.打开 STM32CubeMX,点击“New Project”,选择芯片型号,STM32F103VETx。
Step2.选择时钟源,并配置时钟树。选择Crystal/Ceramic Resonator,并配置系统时钟为72M。
Step3.配置SYS,我们这里选择的是Serial Wire。(正常情况配置不配置不影响,debug可以使用。但是你不可以把这两个引脚用于其他复用功能,如果用于其他复用功能,debug就不起作用了。)
Step4.串口配置(主要为了在串口调试助手显示读写数据),因为没有用到中断和DMA所以我们就不过多讲解。
Step5.配置软件IIC所需要的的GPIO口,需要注意的是这边要设置成开漏输出模式,上拉模式。这里选用PB6,PB7作为SCL,SDA,因为是软件IIC,所以这里的选择接口是不受限制的。
到这里关于软件IIC参数配置基本已经完成,只需要根据之前文章《STM32Cube HAL:GPIO输入/输出(一)》Step4-Step8,设置相关工程参数和生成代码。
三、添加功能代码
1、我们等会会向串口调试助手发送数据,进行实验结果的验证。 发送数据我们采用printf函数,所有需要重定向c库函数printf到串口。注意使用时需要在keil设置中勾选微库(use mircolib),同时需要添加头文件#include <stdio.h>。重定向代码如下(usart.c)
- //重定向c库函数printf到串口DEBUG_USART,重定向后可使用printf函数
- int fputc(int ch, FILE *f)
- {
- /* 发送一个字节数据到串口DEBUG_USART */
- HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000);
-
- return (ch);
- }
- //重定向c库函数scanf到串口DEBUG_USART,重写向后可使用scanf、getchar等函数
- int fgetc(FILE *f)
- {
- int ch;
- HAL_UART_Receive(&huart1, (uint8_t *)&ch, 1, 1000);
- return (ch);
- }
复制代码
2、为了把IIC和EEPROM的相关接口区分开,方便以后移植到其他IIC设备。在cubemx自动生成的gpio.c/gpio.h IIC的相关接口,同时新建了i2c_ee.c/i2c_ee.c中写读写EEPROM的接口。
(1)IIC相关接口的书写。
首先是在gpio.h中,对SCL/SDA相关IO口的写0/1和读取进行宏定义,提高代码可读性。以及等会涉及到读写位进行宏定义。最后就是函数的声明。
- #define I2C_SCL_1() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET);
- #define I2C_SCL_0() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET);
- #define I2C_SDA_1() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET);
- #define I2C_SDA_0() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET);
- #define I2C_SDA_READ() HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_7)
- #define I2C_WR 0 //写控制bit
- #define I2C_RD 1 //读控制bit
- void MX_GPIO_Init(void);
- void i2c_Start(void);
- void i2c_Stop(void);
- void i2c_SendByte(uint8_t HS_Byte);
- uint8_t i2c_ReadByte(void);
- uint8_t i2c_WaitAck(void);
- void i2c_Ack(void);
- void i2c_NAck(void);
- uint8_t i2c_CheckDevice(uint8_t _Address);
复制代码
然后就是在gpio.c中,把我们刚才声明的函数进行完善。
(2)使用刚才写好的IIC的接口编写对EEPROM的读写、擦除等程序。
还是一样在i2c_ee.h中,进行相关代码宏定义,提高代码可读性。还有就是函数的声明。
- * AT24C02 2kb = 2048bit = 2048/8 B = 256 B
- * 32 pages of 8 bytes each
- *
- * Device Address
- * 1 0 1 0 A2 A1 A0 R/W
- * 1 0 1 0 0 0 0 0 = 0XA0
- * 1 0 1 0 0 0 0 1 = 0XA1
- */
- /* AT24C01/02每页有8个字节
- * AT24C04/08A/16A每页有16个字节
- */
-
- #define EEPROM_DEV_ADDR 0xA0 // 24xx02的设备地址
- #define EEPROM_PAGE_SIZE 8 // 24xx02的页面大小
- #define EEPROM_SIZE 256 // 24xx02总容量
- uint8_t ee_CheckOk(void);
- uint8_t ee_ReadBytes(uint8_t *_pReadBuf, uint16_t _usAddress, uint16_t _usSize);
- uint8_t ee_WriteBytes(uint8_t *_pWriteBuf, uint16_t _usAddress, uint16_t _usSize);
- void ee_Erase(void);
- uint8_t ee_Test(void);
复制代码
在i2c_ee.c完善相关函数
- /* 判断串行EERPOM是否正常 */
- uint8_t ee_CheckOk(void)
- {
- if(i2c_CheckDevice(EEPROM_DEV_ADDR))
- {
- i2c_Stop();//失败后,切记发送I2C总线停止信号
- return 0;//设备异常
- }
- else
- {
- return 1;//设备正常
- }
- }
- /*从串行EEPROM指定地址处开始读取若干数据*/
- uint8_t ee_ReadBytes(uint8_t *_pReadBuf, uint16_t _usAddress, uint16_t _usSize)
- {
- uint16_t i;
- i2c_Start();//第1步:启动I2C总线
-
- i2c_SendByte(EEPROM_DEV_ADDR|I2C_WR);//第2步:发送地址+写指令
- if(i2c_WaitAck()!=0)//第3步:判断设备是否响应
- {
- goto res_error;
- }
- i2c_SendByte(_usAddress);//第4步:发送字节地址
- if(i2c_WaitAck()!=0)//第5步:判断设备是否响应
- {
- goto res_error;
- }
-
- i2c_Start();//第6步:重启I2C总线,开始读数据
-
- i2c_SendByte(EEPROM_DEV_ADDR|I2C_RD);//第7步:发送地址+读指令
- if(i2c_WaitAck()!=0)//第8步:判断设备是否响应
- {
- goto res_error;
- }
-
- for(i=0;i<_usSize;i++)
- {
- _pReadBuf=i2c_ReadByte();//第9步:开始读取数据
- if(i!=_usSize-1)//第10步:在接收_usSize-1个字节前都发送应答信号,最后一个字节发送非应答信号
- {
- i2c_Ack();
- }
- else
- {
- i2c_NAck();
- }
- }
-
- i2c_Stop();//第11步:停止I2C总线
- return 1;//读取成功
- res_error:
- printf("\r\n响应超时\r\n");
- return 0;//读取失败
- }
- /*向串行EEPROM指定地址写入若干数据,采用页写操作提高写入效率*/
- uint8_t ee_WriteBytes(uint8_t *_pWriteBuf, uint16_t _usAddress, uint16_t _usSize)
- {
- uint16_t i,m,Addr;
- Addr=_usAddress;
- for(i=0;i<_usSize;i++)
- {
-
- if((i==0)||(Addr&(EEPROM_PAGE_SIZE - 1))==0)//当发送第1个字节或是页面首地址时,需要重新发起启动信号和地址
- {
- i2c_Stop();//发停止信号,启动内部写操作
- for(m=0;m<1000;m++)//通过检查器件应答的方式,判断内部写操作是否完成, 一般小于 10ms,CLK频率为200KHz时,查询次数为30次左右
- {
- i2c_Start();//第1步:发起I2C总线启动信号
-
- i2c_SendByte(EEPROM_DEV_ADDR|I2C_WR);//第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读
-
- if(i2c_WaitAck()==0)//第3步:判断设备是否响应
- {
- break;
- }
- }
- if(m==1000)//响应超时
- {
- goto res_error;
- }
-
- i2c_SendByte(Addr);// 第4步:发送字节地址,24C02只有256字节,因此1个字节就够了,如果是24C04以上,那么此处需要连发多个地址
- if(i2c_WaitAck()!=0)//第5步:判断设备是否响应
- {
- goto res_error;
- }
- }
-
- <i> i2c_SendByte(_pWriteBuf);//第6步:开始写入数据
- if(i2c_WaitAck()!=0)//第7步:判断设备是否响应
- {
- goto res_error;
- }
- Addr++;
- }
-
- i2c_Stop();//第8步:停止I2C总线
- return 1;//写入成功
-
- res_error:
- printf("\r\n响应超时\r\n");
- return 0;//写入失败
- }
- /* 擦除EEPROM */
- void ee_Erase(void)
- {
- uint16_t i;
- uint8_t buf[EEPROM_SIZE];
-
- for (i = 0; i < EEPROM_SIZE; i++)// 填充缓冲区
- {
- buf = 0xFF;
- }
-
- if (ee_WriteBytes(buf, 0, EEPROM_SIZE) == 0)//写EEPROM, 起始地址 = 0,数据长度为 256
- {
- printf("擦除eeprom出错!\r\n");
- }
- else
- {
- printf("擦除eeprom成功!\r\n");
- }
- }
- /* 测试程序 */
- uint8_t ee_Test(void)
- {
- uint16_t i;
- uint8_t write_buf[EEPROM_SIZE];
- uint8_t read_buf[EEPROM_SIZE];
- if (ee_CheckOk() == 0)// 没有检测到EEPROM
- {
- printf("没有检测到串行EEPROM!\r\n");
- return 0;
- }
- for (i = 0; i < EEPROM_SIZE; i++)//填充测试缓冲区
- {
- write_buf = i;
- }
-
- if (ee_WriteBytes(write_buf, 0, EEPROM_SIZE) == 0)//向EEPROM写入数据
- {
- printf("写eeprom出错!\r\n");
- return 0;
- }
- else
- {
- printf("写eeprom成功!\r\n");
- }
- HAL_Delay(1);//写完之后需要适当的延时再去读,不然会出错
- if (ee_ReadBytes(read_buf, 0, EEPROM_SIZE) == 0)//从EEPROM读取数据
- {
- printf("读eeprom出错!\r\n");
- return 0;
- }
- else
- {
- printf("读eeprom成功,数据如下:\r\n");
- }
- for (i = 0; i < EEPROM_SIZE; i++)//比较读取和写入的数据是否一致
- {
- if(read_buf != write_buf)
- {
- printf("0x%02X ", read_buf);
- printf("错误:EEPROM读出与写入的数据不一致");
- return 0;
- }
- printf(" %02X", read_buf);
-
- if ((i & 15) == 15)
- {
- printf("\r\n");
- }
- }
- printf("eeprom读写测试成功\r\n");
- return 1;
- }</i>
复制代码
3、在主函数中调用i2c_ee.c中的测试函数即可。
关于软件IIC有几点需要注意的:
1、我们传输速率的设置:硬件IIC,标准模式传输速率为 100kbit/s ,快速模式为 400kbit/s ,高速模式下可达 3.4Mbit/s,但目前大多 I2C 设备尚不支持高速模式。这里使用软件IIC的设置SCL频率200kbit/s左右,大家可以根据需要设置gpio.c中i2c_Delay(void)的延时时间来修改SCL频率。
2、在i2c_ee.c 写ee_ReadBytes、ee_WriteBytes,可以发现步骤有一点区别,ee_WriteBytes在多了一个for循环来重复判断设备内的写操作是否完成,而不是直接采用判断一次。虽然后者的写法在实验结果上也没有出错,但是为了代码严谨性,避免不必要的错误,选用前者。
3、在写入数据到EEPROM的时候,根据数据的大小,如果需要读写的话,需要进行一定的延时,否则会错。
—— —— —— —— —— —— —— ——硬件IIC—— —— —— —— —— —— —— ——
二、 CubeMX配置
step1-step4与上面软件IIC模式一致
step5.IIC外设配置,这里选用的I2C1,因为使用的主机模式,所以从机参数默认即可。
到这里关于硬件IIC参数配置基本已经完成,只需要根据之前文章《STM32Cube HAL:GPIO输入/输出(一)》Step4-Step8,设置相关工程参数和生成代码。
三、添加功能代码
1、我们等会会向串口调试助手发送数据,进行实验结果的验证。 发送数据我们采用printf函数,所有需要重定向c库函数printf到串口。注意使用时需要在keil设置中勾选微库(use mircolib),同时需要添加头文件#include <stdio.h>。(代码同上: 软件IIC)
2、在i2c.h文件中添加相关的宏定义以及函数声明。
- #define EEPROM_ADDRESS 0xA0 // 24xx02的设备地址
- #define EEPROM_PAGESIZE 8 // 24xx02的页面大小
- #define DATA_Size 7//256
- #define EEP_Firstpage 0x02 // 0x00
- void MX_I2C1_Init(void);
- uint8_t I2C_Test(void);
- void I2C_EE_BufferWrite(uint8_t* pBuffer, uint8_t WriteAddr, uint16_t NumByteToWrite);
- uint32_t I2C_EE_ByteWrite(uint8_t* pBuffer, uint8_t WriteAddr);
- uint32_t I2C_EE_PageWrite(uint8_t* pBuffer, uint8_t WriteAddr, uint8_t NumByteToWrite);
- uint32_t I2C_EE_BufferRead(uint8_t* pBuffer, uint8_t ReadAddr, uint16_t NumByteToRead);
复制代码
3、在i2c.c实现EEPROM的读写函数,以及用于测试的测试函数。
- /*将缓冲区中的数据写到I2C EEPROM中*/
- void I2C_EE_BufferWrite(uint8_t* pBuffer, uint8_t WriteAddr, uint16_t NumByteToWrite)
- {
- uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0;
- Addr = WriteAddr % EEPROM_PAGESIZE;//判断写入的首地址是否与EEPROM页的首地址对齐,0为对齐
- count = EEPROM_PAGESIZE - Addr;//计算从写入的首地址需要写多少数据才能填满当前页
- NumOfPage = NumByteToWrite / EEPROM_PAGESIZE;//计算写入数据需要写几个完整页(地址对齐的情况)
- NumOfSingle = NumByteToWrite % EEPROM_PAGESIZE;//计算写完完整页剩下的数据个数(地址对齐的情况)
- if(Addr == 0) //判断写入的首地址是否与页地址对齐
- {
- if(NumOfPage == 0) //如果页对齐,判断数据是否不满一页
- {
- I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);//如果不满一页,直接写入数据
- }
- else //在数据满一页的情况下,通过地址自增方式,循环写入数据(页写入的形式)
- {
- while(NumOfPage--)//循环写入数据:先写入完整页
- {
- I2C_EE_PageWrite(pBuffer, WriteAddr, EEPROM_PAGESIZE);
- WriteAddr += EEPROM_PAGESIZE;
- pBuffer += EEPROM_PAGESIZE;
- }
- if(NumOfSingle!=0)//循环写入数据:再写入不满一页的数据
- {
- I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
- }
- }
- }
- else
- {
- if(NumOfPage== 0) //如果页不对齐,判断数据是否不满一页
- {
- if(NumOfSingle<=count)//如果不满一页,判断数据是否跨页
- {
- I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);//如果不跨页,直接写入数据
- }
- else
- {
- I2C_EE_PageWrite(pBuffer, WriteAddr, count);//如果跨页,先写首页数据,再写次页数据
- I2C_EE_PageWrite(pBuffer+count, WriteAddr+count, NumOfSingle-count);
- }
- }
- else
- {
- /*如果数据满一页,对数据进行分离*/
- NumByteToWrite -= count;//扣除第一页数据个数
- NumOfPage = NumByteToWrite / EEPROM_PAGESIZE;//计算写入数据需要写几个完整页
- NumOfSingle = NumByteToWrite % EEPROM_PAGESIZE; //计算写完完整页剩下的数据个数
- if(count != 0)
- {
- I2C_EE_PageWrite(pBuffer, WriteAddr, count);//写入首页数据
- WriteAddr += count;//写地址自增
- pBuffer += count;//缓冲区指针自增
- }
- while(NumOfPage--)//依次写入完整页的数据
- {
- I2C_EE_PageWrite(pBuffer, WriteAddr, EEPROM_PAGESIZE);
- WriteAddr += EEPROM_PAGESIZE;//写地址自增
- pBuffer += EEPROM_PAGESIZE; //缓冲区指针自增
- }
- if(NumOfSingle != 0)//判断最后一页的数据是否是填满完整一页的
- {
- I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle); //写入最后一页的数据
- }
- }
- }
- }
- /*写一个字节到I2C EEPROM中*/
- uint32_t I2C_EE_ByteWrite(uint8_t* pBuffer, uint8_t WriteAddr)
- {
- HAL_StatusTypeDef status = HAL_OK;
- status = HAL_I2C_Mem_Write(&hi2c1, EEPROM_ADDRESS, (uint16_t)WriteAddr, I2C_MEMADD_SIZE_8BIT, pBuffer, 1, 100);
- /* 检测IIC外设是否传输完成,并准备进行下一次通信 */
- if(status != HAL_OK)
- {
- /* Execute user timeout callback */
- }
- while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY)
- {
-
- }
- /* 检测目标设备是否准备好进行通信(即目标设备是否在进行读写操作)*/
- while (HAL_I2C_IsDeviceReady(&hi2c1, EEPROM_ADDRESS, 300, 300) == HAL_TIMEOUT);
- // while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY)
- // {
- //
- // }
- return status;
- }
- /*在EEPROM的一个写循环中可以写多个字节,但一次写入
- 的字节数不能超过EEPROM页的大小,AT24C02每页有8个字节*/
- uint32_t I2C_EE_PageWrite(uint8_t* pBuffer, uint8_t WriteAddr, uint8_t NumByteToWrite)
- {
- HAL_StatusTypeDef status = HAL_OK;
- status=HAL_I2C_Mem_Write(&hi2c1, EEPROM_ADDRESS,WriteAddr, I2C_MEMADD_SIZE_8BIT, (uint8_t*)(pBuffer),NumByteToWrite, 100);
- /* 检测IIC外设是否传输完成,并准备进行下一次通信 */
- while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY)
- {
-
- }
- /* 检测目标设备是否准备好进行通信(即目标设备是否在进行读写操作)*/
- while (HAL_I2C_IsDeviceReady(&hi2c1, EEPROM_ADDRESS, 300, 300) == HAL_TIMEOUT);
- // while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY)
- // {
- //
- // }
- return status;
- }
- /*从EEPROM里面读取一块数据 */
- uint32_t I2C_EE_BufferRead(uint8_t* pBuffer, uint8_t ReadAddr, uint16_t NumByteToRead)
- {
- HAL_StatusTypeDef status = HAL_OK;
-
- status=HAL_I2C_Mem_Read(&hi2c1,EEPROM_ADDRESS,ReadAddr, I2C_MEMADD_SIZE_8BIT, (uint8_t *)pBuffer, NumByteToRead,1000);
- return status;
- }
- uint8_t I2C_Test(void)
- {
- uint16_t i;
- printf("写入的数据");
- for ( i=0; i<DATA_Size; i++ ) //填充缓冲
- {
- I2c_Buf_Write =i;
- printf("0x%02X ", I2c_Buf_Write);
- if(i%16 == 15)
- printf("\n\r");
- }
- //将I2c_Buf_Write中顺序递增的数据写入EERPOM中
- I2C_EE_BufferWrite( I2c_Buf_Write, EEP_Firstpage, DATA_Size);
- printf("读出的数据");
- //将EEPROM读出数据顺序保持到I2c_Buf_Read中
- I2C_EE_BufferRead(I2c_Buf_Read, EEP_Firstpage, DATA_Size);
- //将I2c_Buf_Read中的数据通过串口打印
- for (i=0; i<DATA_Size; i++)
- {
- if(I2c_Buf_Read != I2c_Buf_Write)
- {
- printf("0x%02X ", I2c_Buf_Read);
- printf("错误:I2C EEPROM写入与读出的数据不一致");
- return 0;
- }
- printf("0x%02X ", I2c_Buf_Read);
- if(i%16 == 15)
- printf("\n\r");
- }
- printf("I2C(AT24C02)读写测试成功");
- return 1;
- }
复制代码
4、在主函数中添加测试代码验证功能是否正常。
以上程序主要参考野火的硬件IIC程序,但是在页写入程序中添加了一个判断,用于判断在地址不对齐,且数据不满一页的情况下,数据是否跨页。数据不跨页直接写入数据,跨页分两次写入数据。代码如下:
- if(NumOfPage== 0) //如果页不对齐,判断数据是否不满一页
- {
- if(NumOfSingle<=count)//如果不满一页,判断数据是否跨页
- {
- I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);//如果不跨页,直接写入数据
- }
- else
- {
- I2C_EE_PageWrite(pBuffer, WriteAddr, count);//如果跨页,先写首页数据,再写次页数据
- I2C_EE_PageWrite(pBuffer+count, WriteAddr+count, NumOfSingle-count);
- }
- }
复制代码
同时需要注意的EEPROM的几个关键特性:(AT24C02)
在写数据的过程中,每成功写入一个字节,E2PROM存储空间的地址就会自动加1,当加到0xFF后,再写一个字节,地址就会溢出又变成0x00。
还有需要有一个页的概念,页写入的时候,写入的数据有跨页现象,需要进行数据分离处理,否则数据会重复覆盖当前页(发送的内存地址所在页),而不会到下一页,这也是我们代码中进行页写入的时候提前处理数据的原因。
写数据的时候需要注意,E2PROM是先写到缓冲区,然后再“搬运到”到掉电非易失区。所以这个过程需要一定的时间,AT24C02这个过程是不超过5ms。
这里总结软件IIC和硬件IIC的优缺点:
1、软件IIC可灵活选用引脚;硬件IIC引脚固定。
2、软件IIC需要通过代码模拟时序以及读写;硬件IIC只需调用库函数。
3、硬件IIC效率更高,可以使用DMA ,中断。
4、软件IIC代码相对较多,但是流程更清晰;硬件IIC由于使用很多封装函数,处理的过程不够直观,不易理解。
|