STM32 I2C读写EEPROM(POLLING模式)
本工程运行于野火MINI开发板,主控型号为STM32F103RC,读写对象为AT24C02 2Kbit容量的EEPROM
STM32的硬核I2C十分复杂,而且网上有说有缺陷,这就有意思了,值得一探究竟
I2C通信中各设备有主从之分,那么此处先从简单的主模式下手,做一个简单的读写EEPROM实验
从AT24C02的规格书中看到,对它的操作有以下几种
写操作
BYTE WRITE
PAGE WRITE
ACKNOWLEDGE POLLING
读操作
CURRENT ADDRESS READ
RANDOM READ
SEQUENTIAL READ
简单梳理一下,POLLING操作适用于写完之后等待存储芯片搬运数据完成。PAGE WRITE和RANDOM READ既是基本的单字节读写。为了提高效率,写操作支持8字节的页写,完整的页写需要地址边缘对齐,不然写进去的内容会从一页的末端回滚到页首,覆盖原来的数据。而顺序读即完成一字节读后回应ACK即可继续读下一个地址的数据,直到所有数据读取完成回应NACK即可,读的回滚是达到器件末地址后从首地址继续
初始化I2C和GPIO
void mini_i2c_init(void) { // structure define I2C_InitTypeDef i2c_struct; GPIO_InitTypeDef gpio_struct; // clock enable USER_I2C_RCC_CMD(USER_I2C_RCC, ENABLE); GPIO_RCC_CMD(USER_I2C_GPIO_RCC, ENABLE); // GPIO initialization gpio_struct.GPIO_Mode = GPIO_Mode_AF_OD; gpio_struct.GPIO_Speed = GPIO_Speed_50MHz; gpio_struct.GPIO_Pin = USER_I2C_SCL | USER_I2C_SDA; GPIO_Init(USER_I2C_GPIO, &gpio_struct); // I2C initialization i2c_struct.I2C_Mode = I2C_Mode_I2C; i2c_struct.I2C_DutyCycle = I2C_DutyCycle_2; i2c_struct.I2C_OwnAddress1 = USER_I2C_OWN_ADDR; i2c_struct.I2C_Ack = I2C_Ack_Enable; i2c_struct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; i2c_struct.I2C_ClockSpeed = USER_I2C_SPEED; I2C_Init(USER_I2C, &i2c_struct); I2C_Cmd(USER_I2C, ENABLE); }
为了方便调试,配置按钮并开启外部中断
void mini_pb_config(void) { // struct define GPIO_InitTypeDef gpio_struct; EXTI_InitTypeDef exti_struct; NVIC_InitTypeDef nvic_struct; // enable clock // enable AFIO!!! GPIO_RCC_CMD(AFIO_RCC, ENABLE); GPIO_RCC_CMD(PB_K1_RCC, ENABLE); GPIO_RCC_CMD(PB_K2_RCC, ENABLE); // GPIO initialization gpio_struct.GPIO_Mode = GPIO_Mode_IN_FLOATING; gpio_struct.GPIO_Pin = PB_K1; GPIO_Init(PB_K1_GPIO, &gpio_struct); GPIO_EXTILineConfig(PB_K1_PORTSOURCE, PB_K1_PINSOURCE); gpio_struct.GPIO_Mode = GPIO_Mode_IN_FLOATING; gpio_struct.GPIO_Pin = PB_K2; GPIO_Init(PB_K2_GPIO, &gpio_struct); GPIO_EXTILineConfig(PB_K2_PORTSOURCE, PB_K2_PINSOURCE); // EXTI initialization exti_struct.EXTI_Mode = EXTI_Mode_Interrupt; exti_struct.EXTI_Trigger = EXTI_Trigger_Rising; exti_struct.EXTI_Line = PB_K1_EXTILINE; exti_struct.EXTI_LineCmd = ENABLE; EXTI_Init(&exti_struct); exti_struct.EXTI_Mode = EXTI_Mode_Interrupt; exti_struct.EXTI_Trigger = EXTI_Trigger_Rising; exti_struct.EXTI_Line = PB_K2_EXTILINE; exti_struct.EXTI_LineCmd = ENABLE; EXTI_Init(&exti_struct); // NVIC initialization nvic_struct.NVIC_IRQChannel = PB_K1_IRQ; nvic_struct.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&nvic_struct); nvic_struct.NVIC_IRQChannel = PB_K2_IRQ; nvic_struct.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&nvic_struct); }
然后可以在中断服务函数调用I2C读写操作
void PB_K1_IRQ_HANDLER(void) { if(EXTI_GetITStatus(PB_K1_EXTILINE) != RESET) { EXTI_ClearITPendingBit(PB_K1_EXTILINE); mini_i2c_sequential_read(0x00, i2c_rx_data, 8); LED_D4_TOGGLE; } } void PB_K2_IRQ_HANDLER(void) { if(EXTI_GetITStatus(PB_K2_EXTILINE) != RESET) { EXTI_ClearITPendingBit(PB_K2_EXTILINE); mini_i2c_page_write(0x00, page_data); LED_D5_TOGGLE; } } void DEBUG_UART_IRQ_HANDLER(void) { if(USART_GetITStatus(DEBUG_UART, USART_IT_RXNE) != RESET) { USART_ClearITPendingBit(DEBUG_UART, USART_IT_RXNE); uint16_t data = USART_ReceiveData(DEBUG_UART); USART_SendData(DEBUG_UART, data); } }
字节写功能的实现最为简单,依次调用开始,设备地址,字地址,数据操作的库函数,并且在每次操作完检查对应的事件或是标志位
void mini_i2c_byte_write(uint8_t word_addr, uint8_t data) { // start I2C_GenerateSTART(USER_I2C, ENABLE); while(I2C_CheckEvent(USER_I2C, I2C_EVENT_MASTER_MODE_SELECT) == RESET); // device address I2C_Send7bitAddress(USER_I2C, USER_I2C_DEVICE_ADDR, I2C_Direction_Transmitter); while(I2C_CheckEvent(USER_I2C, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) == RESET); // EEPROM address I2C_SendData(USER_I2C, word_addr); while(I2C_CheckEvent(USER_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED) == RESET); I2C_SendData(USER_I2C, data); while(I2C_GetFlagStatus(USER_I2C, I2C_FLAG_BTF) == RESET); I2C_GenerateSTOP(USER_I2C, ENABLE); }
页写操作即将原来的写一次数据改为写八次数据,并在最后一次写数据之后检查EV8_2
// AT24C02 accept 8-byte page write void mini_i2c_page_write(uint8_t word_addr, uint8_t *page_data) { // S I2C_GenerateSTART(USER_I2C, ENABLE); // EV5: SB=1, cleared by reading SR1 register followed by writing DR register with Address while(I2C_CheckEvent(USER_I2C, I2C_EVENT_MASTER_MODE_SELECT) == RESET); // Address I2C_Send7bitAddress(USER_I2C, USER_I2C_DEVICE_ADDR, I2C_Direction_Transmitter); // EV6: ADDR=1, cleared by reading SR1 register followed by reading SR2 while(I2C_CheckEvent(USER_I2C, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) == RESET); // word address I2C_SendData(USER_I2C, word_addr); for(uint8_t i=0;i<8;i++) { // EV8: TxE=1, shift register not empty, data regiter empty, cleared by writing DR register while(I2C_CheckEvent(USER_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTING) == RESET); I2C_SendData(USER_I2C, page_data[i]); } // EV8_2: TxE=1, BTF=1, Program Stop request. TxE and BTF are cleared by hardware by the Stop condition while(I2C_CheckEvent(USER_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED) == RESET); I2C_GenerateSTOP(USER_I2C, ENABLE); }
相对之下,读操作则复杂的多,且有两种操作方式,这里使用的是方式二,关于每种方式的使用场景,参考手册中有如下说明
Method 2: This method is for the case when the I2C is used with interrupts that do not have the highest priority in the application or when the I2C is used with polling
本实验使用的就是POLLING的方式,所用用方式二,麻烦之处是对应读一个字节,两个字节和三个字节及以上还需要不同处理
void mini_i2c_sequential_read(uint8_t word_addr, uint8_t *data, uint32_t nbyte) { // start I2C_GenerateSTART(USER_I2C, ENABLE); while(I2C_CheckEvent(USER_I2C, I2C_EVENT_MASTER_MODE_SELECT) == RESET); // device address I2C_Send7bitAddress(USER_I2C, USER_I2C_DEVICE_ADDR, I2C_Direction_Transmitter); while(I2C_CheckEvent(USER_I2C, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) == RESET); // register address I2C_SendData(USER_I2C, word_addr); while(I2C_GetFlagStatus(USER_I2C, I2C_FLAG_BTF) == RESET); // restart I2C_GenerateSTART(USER_I2C, ENABLE); while(I2C_CheckEvent(USER_I2C, I2C_EVENT_MASTER_MODE_SELECT) == RESET); // device address I2C_Send7bitAddress(USER_I2C, USER_I2C_DEVICE_ADDR, I2C_Direction_Receiver); while(I2C_GetFlagStatus(USER_I2C, I2C_FLAG_ADDR) == RESET); if(nbyte == 1) { // EV6_3 I2C_AcknowledgeConfig(USER_I2C, DISABLE); USER_I2C->SR2; I2C_GenerateSTOP(USER_I2C, ENABLE); // EV7 while(I2C_GetFlagStatus(USER_I2C, I2C_FLAG_RXNE) == RESET); *data = I2C_ReceiveData(USER_I2C); I2C_AcknowledgeConfig(USER_I2C, ENABLE); } else if(nbyte == 2) { // set pos flag I2C_NACKPositionConfig(USER_I2C, I2C_NACKPosition_Next); // EV6 USER_I2C->SR2; // EV6_1 I2C_AcknowledgeConfig(USER_I2C, DISABLE); // EV7_3 while(I2C_GetFlagStatus(USER_I2C, I2C_FLAG_BTF) == RESET); I2C_GenerateSTOP(USER_I2C, ENABLE); *data = I2C_ReceiveData(USER_I2C); *(data+1) = I2C_ReceiveData(USER_I2C); I2C_AcknowledgeConfig(USER_I2C, ENABLE); } else { // EV6 USER_I2C->SR2; for(uint32_t i=0;i<nbyte-3;i++) { // EV7, using BTF instea of RXNE, refering Discovering STM32 Microcontroller while(I2C_GetFlagStatus(USER_I2C, I2C_FLAG_BTF) == RESET); *(data+i) = I2C_ReceiveData(USER_I2C); } // EV7_2 while(I2C_GetFlagStatus(USER_I2C, I2C_FLAG_BTF) == RESET); I2C_AcknowledgeConfig(USER_I2C, DISABLE); *(data+nbyte-3) = I2C_ReceiveData(USER_I2C); I2C_GenerateSTOP(USER_I2C, ENABLE); *(data+nbyte-2) = I2C_ReceiveData(USER_I2C); // EV7 while(I2C_GetFlagStatus(USER_I2C, I2C_FLAG_RXNE) == RESET); *(data+nbyte-1) = I2C_ReceiveData(USER_I2C); I2C_AcknowledgeConfig(USER_I2C, ENABLE); } }
代码中有少量注释,可参照参考手册
读写变量定义如下
uint8_t page_data[8] = {0x08, 0x07, 0x01, 0x06, 0x02, 0x05, 0x03, 0x04}; uint8_t i2c_rx_data[8];
写入截图
读取截图
且可以连续操作,功能验证OK
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?