STM32标准库实现IIC连续读取MPU6050+DMA转运数据

一、前言

  之前在学习使用STM32F103C8T6来实现读取陀螺仪MPU6050的测量数据的时候,采用了逐个读取寄存器数据,最后合并成完整数据的方法,这样会比较耗时并且会有数据不准确的问题。所以就想使用IIC连续读取寄存器数据+DMA快速转运的方法来实现实时数据的读取,先说结论,最后数据的一致性提高了,并且同等条件下读取速度快了三倍。但在这个过程中由于对硬件的不了解遇到了一些问题,上网查阅资料也找不到解决方法,最后经过自己的摸索终于发现了问题所在并且解决,因为觉得这个过程很有意思,所以记录下来,也供遇到了相似困惑的朋友们参考。

二、问题描述

 2.1 硬件接线图

硬件接线如下,把读取到的数据显示在OLED屏幕上。

2.2 问题代码

下面是MPU6050的初始化代码MPU6050_Init(),具体作用是初始化了I2C2,并且对MPU6050进行参数的配置。

void MPU6050_Init(void)
{
	/*使能I2C2,GPIO时钟*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	
	/*初始化I2C引脚*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;//复用开漏,复用是把GPIO控制权交给硬件外设,开漏是I2C协议要求
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStructure);
	
	/*配置I2C2初始化结构体参数*/
	I2C_InitTypeDef I2C_InitStructure;
	I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;//接收一个字节后是否给应答
	I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;//指定stm32作为从机,可以响应几位的地址
	I2C_InitStructure.I2C_ClockSpeed = 40000;//范围是0~400khz
	I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;//调节时钟的占空比,在100khz以下,占空比是1:1,100Khz以上,占空比就是2:1或者16:9
	I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; //设置模式
	I2C_InitStructure.I2C_OwnAddress1 = 0x00; //指定stm32的自身从机地址
	I2C_Init(I2C2, &I2C_InitStructure);

   /*使能I2C2*/
	I2C_Cmd(I2C2, ENABLE);
	
	/*配置MPU6050config寄存器*/
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01);   //解除睡眠,选择陀螺仪时钟
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00);   //6个轴均不待机
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09);   //采样率分频,决定了数据输出的快慢,值越小越快,这里是10分频
	MPU6050_WriteReg(MPU6050_CONFIG,0x06);       //配置寄存器,滤波参数给最大
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x18);  //陀螺仪选择最大量程
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x18); //加速度计选择最大量程

}

 接着函数MPU6050_DMAInit()作用是初始化DMA1,从I2C2的数据寄存器转运14个数据单元到我自定义的结构体变量内,并且是循环模式。

/*如果要使用DMA转运I2C收到的数据,则使用这个函数进行DMA初始化*/
void MPU6050_DMAInit(void)
{
	/*开启DMA1时钟*/
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

	/*配置DMA1初始化结构体参数*/
	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&I2C2->DR; //外设站点的起始地址,也就是I2C2的数据寄存器地址
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设站点的数据宽度
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //不自增
	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t) &(MPU6050SixData.AccX); //存储器站点的起始地址
	DMA_InitStructure.DMA_MemoryDataSize = DMA_PeripheralDataSize_Byte; //存储器站点的数据宽度
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //是否自增
	DMA_InitStructure.DMA_BufferSize = 14; //缓存区大小,也就是传输计数器
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //传输方向
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //选择硬件触发还是软件触发
	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //传输模式,也就是是否使用自动重装
	DMA_InitStructure.DMA_Priority = DMA_Priority_High; //设置软件优先级,比硬件优先级高
	DMA_Init(DMA1_Channel5,&DMA_InitStructure);

	/*使能DMA中断*/
  DMA_ITConfig(DMA1_Channel5, DMA_IT_TC, ENABLE);

  /*配置NVIC*/
  NVIC_InitTypeDef NVIC_InitStructure;
  NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel5_IRQn;
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);
		
	/*开启DMA使能*/
	DMA_Cmd(DMA1_Channel5,ENABLE);
}

 最后调用这个函数MPU6050_DMA_GetData(),连续读取MPU6050,并且使能I2C2的DMA转运接收到的数据到结构体。

void MPU6050_DMA_GetData(void)
{
	static uint8_t DMA_Init_Flag = 0;
	if(DMA_Init_Flag == 0)
	{
		MPU6050_DMAInit();
		DMA_Init_Flag = 1;
	}

	/*开启I2C2的DMA转运*/
	I2C_DMACmd(I2C2, ENABLE);
	
	/*-------以下是I2C指定地址读的标准流程-----------*/
	
	/*发送起始波形,等待EV5事件:STM32已成为主机*/
	I2C_GenerateSTART(I2C2,ENABLE);
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT); 
	
	/*发送从机地址,等待EV6事件:代表地址和写位发送完成,发送模式已选择*/
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);//
	
	/*发送寄存器地址,等待Ev8事件:代表数据字节正在发送,可以把下一个要发送的数据写入到DR寄存器*/
	I2C_SendData(I2C2,MPU6050_ACCEL_XOUT_H);
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING);
	
	/*重复生成起始条件,等待EV5事件:STM32已成为主机*/
	I2C_GenerateSTART(I2C2,ENABLE);
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT); 
	
	/*发送从机地址,等待EV6事件:代表地址和读位发送完成,接收模式已选择*/
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);
	
	/*-------下面开始接收从机发来的数据------*/
	for(uint8_t i =0;i<14;i++)
	{
		/*等待Ev7事件,说明接收到了一个字节,会自动触发DMA转运	*/
		MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_RECEIVED);  
		I2C_ReceiveData(I2C2);
		if(i == 12)
		{
				/*接收到倒数第二个数据,要在接收最后一位数据前设置好停止位和非应答*/
				I2C_GenerateSTOP(I2C2,ENABLE);
				I2C_AcknowledgeConfig(I2C2,DISABLE);
				
		}
	}
	/*因为默认是给从机应答的,所以最后恢复一下,也是方便指定地址收多个字节*/
	 I2C_AcknowledgeConfig(I2C2,ENABLE);
	
	/*关闭I2C的DMA转运*/
	 I2C_DMACmd(I2C2, DISABLE);
}

 main函数

int main(void)
{
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	OLED_Init();
	MPU6050_Init();
	
	while(1)
	{
	
		MPU6050_DMA_GetData();
		OLED_ShowSignedNum(2,1,MPU6050SixData.AccX,5);
		OLED_ShowSignedNum(3,1,MPU6050SixData.AccY,5);
		OLED_ShowSignedNum(4,1,MPU6050SixData.AccZ,5);
		
		OLED_ShowSignedNum(2,8,MPU6050SixData.GyroX,5);
		OLED_ShowSignedNum(3,8,MPU6050SixData.GyroY,5);
		OLED_ShowSignedNum(4,8,MPU6050SixData.GyroZ,5);
	}

}

结果是读取显示出来的数据完全是乱的,没有如同我预想的那样正常读取显示。

三、解决过程

  一开始怀疑是DMA和IIC的配置问题,但检查过后并不是。因为DMA是第一次使用,所以有可能是DMA转运出了问题,所以我打开了DMA的转运完成中断,在中断函数里面添加了一个静态变量,用来指示进入了多少次中断,结果发现每调用一次MPU6050_DMA_GetData()函数,都会进入13次中断,说明DMA转运完成了13次(其实这里我对DMA转运完成的理解出现了偏差,所以导致即使我发现了这个问题,也还是没能定位到实际原因) ,而我要转运的数据是14个,是不是说明少转运了一个?

  为了查看实际通讯波形,我用了逻辑分析仪,得到了如下波形图,发现IIC接收到的数据不止14个,而是远超14个。这让我百思不得其解,明明我的代码里面在接收到第13个数据的时候就会提前配置好非应答和停止位,为什么停止位会这么久才出现呢?

接收数据的代码如下,按照我的预想,I2C2每接收到一个数据,就会申请一次DMA中断,那应该会进14次中断,而我通过打印变量的方式也证明了这个循环确实是执行了14次,另外为什么串口会接收到这么多额外数据,延迟了这么久才发送非应答和停止位呢?

/*-------下面开始接收从机发来的数据------*/
	for(uint8_t i =0;i<14;i++)
	{
		/*等待Ev7事件,说明接收到了一个字节,会自动触发DMA转运	*/
		MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_RECEIVED);  
	
		if(i == 12)
		{
				/*接收到倒数第二个数据,要在接收最后一位数据前设置好停止位和非应答*/
				I2C_GenerateSTOP(I2C2,ENABLE);
				I2C_AcknowledgeConfig(I2C2,DISABLE);
				
		}
	}
	/*因为默认是给从机应答的,所以最后恢复一下,也是方便指定地址收多个字节*/
	 I2C_AcknowledgeConfig(I2C2,ENABLE);

  经过思考和询问AI,我突然想到我使用的是硬件IIC,并且DMA转运速度极快,有没有可能DMA转运的速度太快了,所以IIC接收数据的速度也极快。因为一旦DMA把数据转运走,IIC立马就会接收下一个数据,而这一切都是硬件自动执行,我软件根本控制不了。这个循环看似可以在接收14个数据后立马停止接收,实际上在它循环的这段时间里DMA已经转运超过14个数据了。

  那为什么DMA会转运超过14个数据呢,我明明设置的是每14个是一个循环,如果我只调用一次MPU6050_DMA_GetData()函数,那么不会出现这种情况才对。突然我想到,DMA的转运完成,到底是指转运一个数据完成,还是把14个数据都转运了才是转运完成?我之前一直以为是前者,但如果是后者,那这一切都可以解释了。经过查阅,确实是后者。

  因为DMA的转运速度太快了,在转运完14个数据后,我的那个for循环可能才刚开始,等到完成14次循环,DMA都转运了13轮了,所以才会多接收了这么多数据后才出现停止位。我这时候才明白DMA可以减轻CPU负担的原因,它是独立于CPU而自己运作的,它并不会像软件那样顺序执行。

  所以解决方法就是给一段合适的延迟时间,在这段时间里DMA就会自动转运14个数据了,并且在DMA的转运完成中断里加上发出停止位和非应答位的代码,这样DMA转运完成后就能让I2C停止接收数据。代码如下:

void MPU6050_DMA_GetData(void)
{
	static uint8_t DMA_Init_Flag = 0;
	if(DMA_Init_Flag == 0)
	{
		MPU6050_DMAInit();
		DMA_Init_Flag = 1;
	}

	/*开启I2C2的DMA转运*/
	I2C_DMACmd(I2C2, ENABLE);
	
	/*-------以下是I2C指定地址读的标准流程-----------*/
	
	/*发送起始波形,等待EV5事件:STM32已成为主机*/
	I2C_GenerateSTART(I2C2,ENABLE);
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT); 
	
	/*发送从机地址,等待EV6事件:代表地址和写位发送完成,发送模式已选择*/
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);//
	
	/*发送寄存器地址,等待Ev8事件:代表数据字节正在发送,可以把下一个要发送的数据写入到DR寄存器*/
	I2C_SendData(I2C2,MPU6050_ACCEL_XOUT_H);
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING);
	
	/*重复生成起始条件,等待EV5事件:STM32已成为主机*/
	I2C_GenerateSTART(I2C2,ENABLE);
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT); 
	
	/*发送从机地址,等待EV6事件:代表地址和读位发送完成,接收模式已选择*/
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);
	
	/*-------下面开始接收从机发来的数据------*/
	 Delay_ms(4);
	 I2C_AcknowledgeConfig(I2C2,ENABLE);
	
	/*关闭I2C的DMA转运*/
	 I2C_DMACmd(I2C2, DISABLE);
}
// DMA中断服务函数
void DMA1_Channel5_IRQHandler(void)
{
    if (DMA_GetITStatus(DMA1_IT_TC5) == SET)
    {
				/*设置好停止位和非应答*/
				I2C_GenerateSTOP(I2C2,ENABLE);
				I2C_AcknowledgeConfig(I2C2,DISABLE);
        // 清除DMA中断标志
        DMA_ClearITPendingBit(DMA1_IT_TC5);
    }
		
}

四、总结

  一旦开启DMA循环转运,DMA就会尽快把转运任务完成,比如我想DMA转运14个数据,DMA一旦接收到DMA请求,就会立马转运14次,而不是我想象中的I2C接收到一个数据就发起一个DMA请求,DMA就只转运一个数据。这一切都是硬件自动完成的,所以并不是根据程序语句那样一条条执行,我现在才体会到硬件是如何解放cpu的,放在那里就自己动了!我应该在DMA中断里面配置停止位。

  在I2C开始读取数据后,调用一段时间的延时,在这段延时里DMA会搬运完成14个数据,并且在中断里面配置生成I2C的停止位,这样就完成了一次数据读取搬运。延时时间取决于I2C的通讯速度和MPU6050的采样速度,我目前的I2C速率是40KHZ,MPU6050采样选择是10分频(值越小越快),延时选择了4ms,再少就可能接收不过来了。

  • 13
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值