一、前言
之前在学习使用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,再少就可能接收不过来了。