STM32F767+STM32CubeMX I2C通信读写EEPROM数据(采用轮询、DMA、中断三种方式)

2023-05-16

STM32F767+STM32CubeMX I2C通信读写EEPROM数据(采用轮询、DMA、中断三种方式)

摘要-前言

作为一名STM32的初学者,在学习过程中会遇到很多问题,解决过程中会看到很多博主发过的文章,每次都是零零总总的学习各个大牛的经验。但时间久了就会忘记其中的一些关键点,所以才有了把自己解决问题的过程记录下来的想法,日后回忆起也很方便。
前人们做过很多STM32 I2C通信的努力,但大多都是基于STM32F0、F1、F4这些系列的板子,而众所周知不同系列之间还是有不同的,这就导致初学者学习STM32时,会遇到很多困难。另外 I2C通信很多人采取的是软件模拟实现,对硬件并不看好。但是毕竟这么多年过去了,HAL库及CubeMX的出现,能够很大程度上解决I2C宕机的问题。所以本文除了讲解CubeMX I2C通信以外,也顺便做了实验来验证I2C的实际效果。

硬件设施:正点原子STM32F676阿波罗开发板
IDE:KEIL5
STM32CubeMX:5.4.0
STM32CubeMX Firmware Package Name and Version:STM32Cube FW_F7 V1.15.0
Keil STM32F767芯片包:Keil.STM32F7xx_DFP.2.12.0
EEPROM:24C02
I2C2-SCL:PH4
I2C2-SDA:PH5

轮询方式(普通方式)读写EEPROM(24C02)

配置RCC

在这里插入图片描述选择HSE的Crystal/Ceramic Resonator其余默认

配置I2C2

这里需要注意一下,CubeMX默认的I2C2不是PH4和PH5,是PF0和PF1。如果直接点击左侧选项中的I2C2,就自动成了默认PF0和PF1。正点原子开发板资料中虽然写了24C02连接在I2C2上,但是粗心的我并没有注意引脚号,在配置工程时,选择了默认,这一个小小的问题,浪费了我两天时间。

配置时,在右侧的芯片上找到PH4与PH5,左键引脚,选择I2C2_SCL和I2C2-SDA,此时两个引脚会变成黄色。
在这里插入图片描述
在这里插入图片描述
这个时候再去选中左侧的I2C2,就会定位到PH4和PH5了。
在这里插入图片描述
接下来再看下面的配置参数:
在这里插入图片描述
选择了标准模式,那么频率对应100KHZ。Rise Time、Fall Time、Coefficient of Digital Filter 实际上是要遵循一套非常复杂的时序计算方法的,也和对应的外设有关系,在设置前要阅读相关的外设资料此处暂时不展开。Timing由软件自动计算好,这也是CubeMX方便之处。

配置USART1

配置串口的目的,是为了能够把从EEPROM读出来的数据“打印”在串口调试助手上,方便检验。
在这里插入图片描述选择USART1,在选择Asynchronous,其余默认即可。

配置时钟树

在这里插入图片描述直接把红色地方拉满即可。

工程管理

在这里插入图片描述

从串口打印汉字、英文、数字等可读性信息

配置完后,打开工程,在main.c中添加如下代码,对prinf函数进行重定位。

#include <stdio.h>
/* USER CODE BEGIN PFP */
#ifdef __GNUC__  
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)  
#else  
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)  
#endif /* __GNUC__ */  
PUTCHAR_PROTOTYPE
{
	HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0x0001);
	return ch;
}
/* USER CODE END PFP */

勾选微库
在这里插入图片描述然后就可以开开心心用Printf函数了。

I2C轮询方式的代码

/* USER CODE BEGIN PD */
#define ADDR_24LCxx_Write 0xA0
#define ADDR_24LCxx_Read 0xA1
#define BufferSize 0X100
/* USER CODE END PD */

24C02的写地址是0XA0,读地址为0XA1,总存储空间为256字节。

/* USER CODE BEGIN PV */
uint8_t WriteBuffer[BufferSize], ReadBuffer[BufferSize];
uint16_t i,j;
uint16_t recv;//保存I2C读写函数的返回值,方便DEBUG
/* USER CODE END PV */

查阅24C02的资料可知,该EEPROM总存储量256字节,按照页来存储,每页8个字节。因此每次写入只可以写8个字节,写满的话总共要分32次执行。参考资料同时指出,每次写入需要等待5ms之后才能进行下次写入。因此下面的程序我分了32次执行,每次存储8字节。
使用函数HAL_I2C_Mem_Write进行写入操作,该函数在用户手册中定义如下:
在这里插入图片描述从描述中可以看出,这种传递方式是阻塞的,形象的说,CPU等在这里Timeout ms啥都不敢,就等写入或者读取数据,只有读写结束后程序才往下进行。这种方式令人很不舒服。尤其是在严格控制节拍的地方。

/* USER CODE BEGIN 2 */
  printf("Write data in EEPROM\r\n");
  for (i = 0; i < 256; i++)
	  WriteBuffer[i] = i;
  for (j = 0; j < 32; j++)
  {
	  if ((recv = HAL_I2C_Mem_Write(&hi2c2, ADDR_24LCxx_Write, 8 * j, I2C_MEMADD_SIZE_8BIT, WriteBuffer + 8 * j, 8)) == HAL_OK)
	  {
		  printf("\r\n EEPROM 24C02 Write Test OK \r\n");
		  HAL_Delay(5);
	  }
	  else
	  {
		  HAL_Delay(5);
		  printf("\r\n EEPROM 24C02 Write Test False \r\n");
		  printf("\r\n recv = %d \r\n",recv);
	  }
  }
   /* USER CODE END 2 */

在主函数中添加:使用HAL_I2C_Mem_Read函数进行一次性读取操作。参数意义和写函数一样。读取完后,打印出读到的结果。

  if ((recv = HAL_I2C_Mem_Read(&hi2c2, ADDR_24LCxx_Read, 0, I2C_MEMADD_SIZE_8BIT, ReadBuffer, BufferSize)) == HAL_OK)
					  {
						  printf("This is Data in EEPROM!!\r\n");
						  for (i = 0; i < 256; i++)
							  printf("%d", ReadBuffer[i]);
						  printf("\r\nGOT Finished!!\r\n");
					  }
					  else
					  {
						  printf("Failed to get data\r\n");
						  printf("recv =  %d !\r\n", recv);
					  }

I2C轮询方式的实验结果

下载到单片机中,连接好串口线,打开串口调试助手,选好端口等参数,打开串口,给单片机上电,助手中出现以下内容:在这里插入图片描述
成功写入32次,然后读取到结果。

用DMA的方式,实现I2C通信

DMA方式实现I2C通信是非阻塞的,DMA控制器化身成为数据的搬运工,专门负责管理数据在内存外设之间传递。CPU把数据扔给DMA后,就撒手不管了,继续该干嘛干嘛。DMA则接盘数据,进行搬运工作。大大节约CPU的运行效率。

CubeMX配置I2C-DMA

在刚才的基础上,只需要修改两个地方:
在这里插入图片描述Add接受和发送的DMA Request,参数默认就行了。
接下来就是关键的地方,一定要勾选I2C2 event interrupt,否则你只能进行一次不超过 256 Bytes 的 DMA,之后就得手动去清空标志位,再手动启动一次 DMA 发送。

参考这位大佬的原话:使用硬件 I2C + DMA 操作液晶屏 (STM32)

在这里插入图片描述
勾选 I2C2事件中断

代码修改-DMA方式实现I2C通信

重新生成代码,在新的代码中,只需要换一下函数。
写入操作:

(recv = HAL_I2C_Mem_Write_DMA(&hi2c2, ADDR_24LCxx_Write, 8 * j, I2C_MEMADD_SIZE_8BIT, WriteBuffer + 8 * j, 8)) == HAL_OK

读取操作:

(recv = HAL_I2C_Mem_Read_DMA(&hi2c2, ADDR_24LCxx_Read, 0, I2C_MEMADD_SIZE_8BIT, ReadBuffer, BufferSize)) == HAL_OK

HAL_I2C_Mem_Write_DMA函数和HAL_I2C_Mem_Read_DMA函数的参数含义和之前轮询方式的一致。只是少了Timeout这一项,因为CPU从此再也不需要等待了。

实验结果-DMA方式实现I2C通信

在这里插入图片描述成功写入32次,然后读取到EEPROM中的数据。

用中断的方式,实现I2C通信

中断方式实现I2C通信也是非阻塞的,每次执行完一些特定事件之后,CPU会自动调取对应的中断回调函数,STM32的I2C定义的回调函数有:
void HAL_I2C_EV_IRQHandler (I2C_HandleTypeDef * hi2c);
void HAL_I2C_ER_IRQHandler (I2C_HandleTypeDef * hi2c);
void HAL_I2C_MasterTxCpltCallback (I2C_HandleTypeDef *hi2c);
void HAL_I2C_MasterRxCpltCallback (I2C_HandleTypeDef * hi2c);
void HAL_I2C_SlaveTxCpltCallback (I2C_HandleTypeDef * hi2c);
void HAL_I2C_SlaveRxCpltCallback (I2C_HandleTypeDef * hi2c);
void HAL_I2C_AddrCallback (I2C_HandleTypeDef * hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode);
void HAL_I2C_ListenCpltCallback (I2C_HandleTypeDef * hi2c);
void HAL_I2C_MemTxCpltCallback (I2C_HandleTypeDef * hi2c);
void HAL_I2C_MemRxCpltCallback (I2C_HandleTypeDef * hi2c);
void HAL_I2C_ErrorCallback (I2C_HandleTypeDef * hi2c);
void HAL_I2C_AbortCpltCallback (I2C_HandleTypeDef * hi2c);
除了前两个函数已经被HAL库实现,其余的都为弱函数,用户可以根据自己的需求,重新定义声明这些函数。每一种回调函数的执行时间和条件都很清楚的说明在STM32F7的用户手册412页到415页的IO操作中。
这是STM32F7用户手册的下载地址
查阅P412 Polling mode IO MEM operation 和 Interrupt mode IO MEM operation可知,HAL_I2C_Mem_Write/Read_系列的函数,是对内存读写。执行HAL_I2C_Mem_Write/Read_IT函数后,HAL_I2C_MemTx/RxCpltCallback() 函数被调用。其实仔细读一下会发现,DMA实现方法也能使HAL_I2C_MemTx/RxCpltCallback() 函数被调用。

CubeMX配置I2C-IT

啥都不用改,沿用DMA的配置!!!!

代码修改-中断方式实现I2C通信

首先需要用中断的方式需要重新写回调函数。在程序前面添加函数声明:

/* USER CODE BEGIN PFP */
void HAL_I2C_MemTxCpltCallback(I2C_HandleTypeDef *hi2c);
void HAL_I2C_MemRxCpltCallback(I2C_HandleTypeDef *hi2c);
/* USER CODE END PFP */

为了能够证明程序执行了回调函数中的内容,在回调函数中计数,因此全局变量中增添两个变量:

uint16_t check_TX;
uint16_t check_RX;

后面需要重新定义回调函数:

/* USER CODE BEGIN 4 */
void HAL_I2C_MemTxCpltCallback(I2C_HandleTypeDef *hi2c)
{
	check_TX++;
}

void HAL_I2C_MemRxCpltCallback(I2C_HandleTypeDef *hi2c)
{
	check_RX++;
}
/* USER CODE END 4 */

备注:本来打算是要在回调函数中使用printf函数打印出信息的,但是失败了,我认为可能是因为printf函数是一种很费时占资源的操作,在CPU高速调用回调函数时,这种费时操作并不能成功。但是对变量的运算时很简便的,轻松可以完成。所以我采用了计数,而非打印出信息。

最后读写完毕后,在主程序内打印出check_TX和check_RX:
至于读写函数,可以依旧沿用DMA方式,也可以改成中断形式HAL_I2C_Mem_Write/Read_IT,并不影响结果,因为用户手册已经表示,两种方式都能触发HAL_I2C_MemTxCpltCallback和HAL_I2C_MemRxCpltCallback。

if ((recv = HAL_I2C_Mem_Read_IT(&hi2c2, ADDR_24LCxx_Read, 0, I2C_MEMADD_SIZE_8BIT, ReadBuffer, BufferSize)) == HAL_OK)
					  {
						  printf("This is Data in EEPROM!!\r\n");
						  for (i = 0; i < 256; i++)
							  printf("%d", ReadBuffer[i]);
						  printf("\r\nGOT Finished!!\r\n");
						  printf("\r\ncheck_TX = %d\r\n",check_TX);
						  printf("\r\ncheck_RX = %d\r\n",check_RX);
					  }
					  else
					  {
						  printf("Failed to get data\r\n");
						  printf("recv =  %d !\r\n", recv);
					  }

实验结果-中断方式实现I2C通信,打印进入回调函数中的次数

实验结果如下:
可以看出写入了32次,读取了1次。

硬件I2C的极限测试

在网上看到很多人都在说意法半导体为了避免飞利浦的专利问题,STM32系列芯片的硬件I2C通信存在BUG,但这么多年过去了,STM32的HAL库出好几代了,CubeMX更是方便至极,我觉得这么牛皮的公司,应该能解决这些问题吧。所以最后,为了验证I2C的可靠性,做一个极限测试。继续沿用之前的代码,稍作修改,采用DMA的方式进行测试。I2C配置上,直接把速度拉满400KHZ(24C02最大支持到400KHZ)。在程序中,先写入数据。然后疯狂无限读取。看看会不会宕机。

代码修改-每ms读取一次I2C

因为需要1ms读取一次,现在stm32f7xx_it.c文件中声明一个变量Flag_1msChanged;

/* USER CODE BEGIN PV */
extern unsigned char Flag_1msChanged;
/* USER CODE END PV */

在void SysTick_Handler(void)函数中加一句:

void SysTick_Handler(void)
{
  /* USER CODE BEGIN SysTick_IRQn 0 */

  /* USER CODE END SysTick_IRQn 0 */
  HAL_IncTick();
  /* USER CODE BEGIN SysTick_IRQn 1 */
  Flag_1msChanged = 1;//每1ms实现一次标志位改变
  /* USER CODE END SysTick_IRQn 1 */
}

回到main.c文件,加入如下变量:

/* USER CODE BEGIN PV */
unsigned char Flag_1msChanged = 0;
unsigned char Flag_10msChanged = 0;
unsigned char Flag_100msChanged = 0;
unsigned char Flag_500msChanged = 0;
unsigned char Flag_1000msChanged = 0;
unsigned char Counter_1ms = 0;
unsigned char Counter_10ms = 0;
unsigned char Counter_100ms = 0;
unsigned char Counter_1000ms = 0;
unsigned char Counter_200ms = 0;
/* USER CODE END PV */

while(1)函数加入以下逻辑,就能够实现精确地定时操作。

 while (1)
  {
	  if (Flag_1msChanged == 1)
	  {
		  Flag_1msChanged = 0;
		  Counter_1ms++;
		  if (Counter_1ms >= 10)
		  {
			  Counter_1ms = 0;
			  Flag_10msChanged = 1;
			  Counter_10ms++;
			  if (Counter_10ms >= 10)
			  {
				  Counter_10ms = 0;
				  Flag_100msChanged = 1;
				  Counter_100ms++;
				  if (Counter_100ms >= (10 * 1))//1s
				  {
					  Counter_100ms = 0;
				  }

在1ms的区块内增加如下代码:

 while (1)
  {
	  if (Flag_1msChanged == 1)
	  {
		  Flag_1msChanged = 0;
		  Counter_1ms++;
//每1ms执行一次的代码 
 if ((recv = HAL_I2C_Mem_Read_IT(&hi2c2, ADDR_24LCxx_Read, 0, I2C_MEMADD_SIZE_8BIT, ReadBuffer, BufferSize))       
                                                                                                  == HAL_OK)
			  {
				  printf("\r\nS\r\n");
			  }
			  else
			  {
				  printf("F\r\n");
				  printf("recv =  %d !\r\n", recv);
			  }
//成功读取打印S  失败打印F并返回错误代码
		  if (Counter_1ms >= 10)
		  {
			  Counter_1ms = 0;
			  Flag_10msChanged = 1;
			  Counter_10ms++;
			  if (Counter_10ms >= 10)
			  {
				  Counter_10ms = 0;
				  Flag_100msChanged = 1;
				  Counter_100ms++;
				  if (Counter_100ms >= (10 * 1))//1s
				  {
					  Counter_100ms = 0;
				  }
			  }
}

实验结果

连续跑了一个多小时没有任何问题,没出现前人发现的宕机之类的问题,也可能是测试代码太过于简单。但我个人认为,稳定性没什么问题的。可以放心使用。下图为测试结果,可以看到已经接收到了两百万余次成功。(确实跑了一个多小时,但是次数没够 这里没有去深究 只是怀疑printf是一种耗时操作,每次循环printf可能都大于1ms)
在这里插入图片描述

总结

这些内容也只是STM32的I2C通信的皮毛,在整理资料的过程中仍然发现还有很多未知。但是个人觉得学习东西浅尝辄止就足够了,基本能够应付简单的I2C通信问题。在实际的项目中,若发现当前的知识并不足以解决问题,那么以问题为导向去学习,效率会更高。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

STM32F767+STM32CubeMX I2C通信读写EEPROM数据(采用轮询、DMA、中断三种方式) 的相关文章

  • STM32设置为I2C从机模式(HAL库版本)

    STM32设置为I2C从机模式 HAL库版本 目录 STM32设置为I2C从机模式 HAL库版本 前言 1 硬件连接 2 软件编程 2 1 步骤分解 2 2 测试用例 3 运行测试 3 1 I2C连续写入 3 2 I2C连续读取 3 3 I
  • STM32CubeMX之RTC电子钟

    STM32CubeMX之RTC电子钟 1 简介 实时时钟是一个独立的定时器 RTC模块拥有一组连续计数的计数器 在相应软件配置下 可提供时钟日历的功能 修改计数器的值可以重新设置系统当前的时间和日期 2 特性 可编程的预分频系数 分频系数最
  • STM32F103 UART4串口使用DMA接收不定长数据和DMA中断发送

    一 前言 使用DMA通信的好处是 不占用单片机资源 不像普通串口中断 发送一个字节触发一次中断 发送100个字节触发100次中断 接收一个字节触发一次中断 接收200个字节触发200次中断 数据接收完毕触发一次DMA中断 发送数据完毕触发一
  • EEPROM芯片(24c02)使用详解(I2C通信时序分析、操作源码分析、原理图分析)

    1 前言 1 本文主要是通过24c02芯片来讲解I2C接口的EEPROM操作方法 包含底层时序和读写的代码 2 大部分代码是EEPROM芯片通用的 但是其中关于某些时间的要求 是和具体芯片相关的 和主控芯片和外设芯片都有关系 需要具体分析
  • STM32F1应用DMA——串口收发不定长数据

    STM32F1应用DMA 串口收发不定长数据 使用STM32自带DMA传输数据 可以减轻CPU负担 只需设置一些参数即可发送想要发送的数据 以下是STM32F1系列芯片测试过的部分代码 可实现DMA串口收发数据 下图来自STM32官网的手册
  • STM32设置为I2C从机模式

    STM32设置为I2C从机模式 目录 STM32设置为I2C从机模式 前言 1 硬件连接 2 软件编程 3 运行测试 3 1 I2C连续写入 3 2 I2C连续读取 3 3 I2C单次读写测试 4 总结 前言 STM32的I2C作为主机的情
  • STM32设置为I2C从机

    硬件平台 STM32F401 编辑器 keil 5 18 操作系统 win7 一 I2C协议 在传输数据的时候 SDA线必须在时钟的高电平周期保持稳定 SDA的高或低电平状态只有在SCL 线的时钟信号是低电平时才能改变 起始和停止条件 SC
  • STM32CubeMX配置GPIO外部中断

    前言 用PA0来检测按键的输入信号 当按键按下时会由低电平变为高电平 1 配置RCC时钟 将RCC的High Speed Clock HSE 配置为Crystal Ceramic Resonator 将主频设置为72MHz 2 配置GPIO
  • CORE-ESP32C3

    目录 参考博文 源于网友oled eink aht10项目 源代码修改及复现说明 主要修改 显示效果 编辑硬件准备 软件版本 日志及soc下载工具 软件使用 接线说明 天气显示屏 硬件接线 温度采集 日期温度显示屏 正常初始化LOG 示例代
  • 使用STM32CubeMX和STM32CubeIDE的常见问题和注意事项

    STM32CubeMX和STM32CubeIDE是ST公司的STM32Cube生态系统中最重要和最常用的2个软件 使用这2个免费软件可以高效地进行STM32系统的开发 CubeMX用于对一个STM32器件进行可视化的配置 然后生成CubeI
  • 《STM32单片机开发应用教程(HAL库版)—基于国信长天嵌入式竞赛实训平台(CT117E-M4)》第四章4.8 TIM---PWM输出实验

    写在前面 STM32单片机开发应用教程 HAL库版 基于国信长天嵌入式竞赛实训平台 CT117E M4 第四章4 8 TIM PWM输出实验 讲解TIM 定时与PWM输出的STM32CubeMX配置和程序设计方法 官方例程下载 https
  • STM32CubeMX安装、使用、配置

    1 在官网下载应用 https www st com 并安装java环境所需软件jre 8u271 windows x64 exe 2 使用cube新建项目 打开file gt new prj 3 Pinout Configuration配
  • 数据缓存如何路由本例中的对象?

    考虑图示的数据缓存架构 ASCII 艺术如下 CPU core A CPU core B Devices Cache A1 Cache B1 with DMA Cache 2 RAM
  • VS Code 有没有办法导入 Makefile 项目?

    正如标题所说 我可以从现有的 Makefile 自动填充 c cpp properties json 吗 Edit 对于其他尝试导入 makefile 的人 我找到了一组脚本 它们完全可以实现我想要实现的目标 即通过 VS Code 管理
  • docker 容器内的 I2C

    我正在尝试在 docker 容器内的树莓派上使用 i2c 引脚 我使用 RUN 安装所有模块 但是当我使用 CMD 运行我的 python 程序时 我收到一条错误消息 Trackback most recent call last file
  • 在 U-Boot 中使用 I2C 读取多个字节

    我的 Freescale p1022tw 板的 I2C 驱动程序有问题 U Boot 的控制台上有一个从 I2C 设备读取的命令 i2c md chip address 0 1 2 of objects 当我从 id 为 0x60 地址为
  • 启用 DMA 的 UART Tx 模式

    我已经为 UART 在传输模式下编写了一个简单的设备驱动程序 并启用了 DMA 和中断 我使用的硬件是 omap 4460 pandaboard 其中加载了 Linux 3 4 下面我分享一下相关部分的代码 在开放阶段 dma map io
  • 如何从cdev获取设备

    我正在编写一个内核模块 它将分配一些一致的内存并返回相应的虚拟和物理地址 我正在将模块注册为cdev 分配空间dma alloc coherent 我想使用 mmap 它dma common mmap dma common mmap 需要一
  • 是否有通用 I2C 命令来查看设备是否仍然存在于总线上?

    是否有通用的 I2C 命令来查看设备在初始化一次后是否仍然存在于总线上 例如 OLED 显示器 我问这个的原因是为了避免主程序由于库代码中存在无限循环而冻结 当设备断开连接时 例如 Wire 库 在 MCU 启动时 我想检查设备是否可用 并
  • glBufferSubData什么时候返回? [复制]

    这个问题在这里已经有答案了 我想将一个非常大的内存块的内容传输到足够大的 GPU 缓冲区 然后立即更改 CPU 上的内存内容 伪代码是这样的 glBindBuffer very large buffer glBufferSubData ve

随机推荐