jicheng0622

【原创】Kinetis K系列使用DMA实现I2C读取

0
阅读(2996) 评论(4)

    最近辗转出差中,一直也没抽出空来写上几篇(虽然俺的“素材库”已经积累不少了),今天又坐上高铁了,顿时满血复活,赶紧趁此机会码上几篇(啥?不只一篇?,嘿嘿,必须的,欠的债早晚要还的…),我都快成为高铁上写作专业户了,每次坐上高铁都是文思泉涌啊,压都压不住啊有木有(咳咳,主要是闲的没事干)!

    好了,闲扯了一会儿预热了一下,下面开整正题吧。以DMA为主题的文章我之前写过两篇,一篇是DMA+UART,一篇是DMA+ADC,将这些外设模块加上DMA一下子功能上灵活和强大了很多(个人来讲,俺一直觉着DMA是一个高大上的东西,用好了非常方便),今儿个就继续为DMA加点料,把I2C通信这位大兄弟也拉入伙吧。前面那哥俩(UART和ADC)还好理解,可能提到I2C+DMA的话一些人不由得会脑子里有一个问号,I2C通信协议不同于UART等简单的通信,读写数据之前有比较繁琐的从机地址、寄存器地址发送,读写位控制,ACK/NACK等信号的握手,最后才是读写纯数据,这些复杂的操作如果都使用DMA去做,那么问题就来了(最近这句话貌似很火),一个是DMA能不能做,二是即使能做岂不是相当复杂和浪费很多DMA Channel资源吗。呵呵,如果我们有实际应用场景的话就会明白了,实际上使用DMA读取I2C数据只需要在读取纯数据阶段打开就行了,前面的发送从机地址、寄存器地址等操作还是使用传统的手动方式,待前面命令都发出去之后等待I2C从机发送数据流过来时再打开DMA,下次读取时重复上述步骤即可。这样虽然还是保留了前面的手动发送命令的方式,但是后面如果需要从I2C从机读取多字节数据时(单字节就不用了,呵呵)使用DMA传输的确可以释放不少CPU的loading的(CPU不需要死等数据的返回了,可以抽出空去干其他事,这个事就交给DMA去做了),最后稍稍提一句,据说我司的下一代产品的I2C模块可以把前面传统的手动方式都可以用DMA干了,小心脏扑通一下有木有,相当强大吧,哈哈,拭目以待。

    那说到这,我有必要提一下这种方式的具体应用场合(考验大家发散思维的时候到了,呵呵),在常用的I2C接口IC中,MEMS的sensor是其中一员,比如我们常用的三轴加速度传感器、陀螺仪和磁感器,或者他们融合在一块的9轴Sensor,他们的X、Y、Z轴的数据一般都是存放在连续的寄存器地址上的(这为使用DMA提供了基本条件),而且如果分辨率在8位以上的话,每个轴都有至少2个字节数据,那如果使用传统的等待方式的话,这至少6个字节的数据读取需要花费相对比较长的时间(I2C本身的通信速度就有点慢),这种情况下可想而知如果使用DMA会节省多少时间资源。当然还有,现在是可穿戴设备大行其道的时代,我们以其中一个必不可少的计步sensor为例(实际上使用的是加速度传感器,但是需要FIFO深度比较深以节省唤醒功耗,具体为啥这里就不细说了),其内部的数据FIFO深度一般都是十几个甚至几十个字节,这样的话每当MCU被唤醒读取这几十个字节数据做计步算法的时候就可以想象DMA带来的巨大好处了。至于更多的应用场合,我就不多说了,靠大家自己去想象了!

    一不小心啰嗦了不少,对工程师来说,说的再好也不如直接上代码来的简单,呵呵,简单粗暴就是俺们这伙人的风格。俺在这方面也不是抠门的人(俺比较讨厌在这方面藏着掖着),下面源代码呈上:

1. 首先是Kinetis的eDMA初始化部分,我们需要先把I2C触发源分配到DMA相应的Channel上,然后配置好DMA的源地址和目的地址,以及相关传输属性等操作,具体每一步可以直接参考代码中的注释部分,具体eDMA初始化配置如下:

void eDMA_Init(void) 
{ 
  SIM_SCGC6 |= SIM_SCGC6_DMAMUX_MASK; 
  
  DMAMUX_CHCFG0 =  DMAMUX_CHCFG_ENBL_MASK              /* 使能DMA通道 */     
                   | DMAMUX_CHCFG_SOURCE(19);  /* 指定DMA触发源为I2C,这个可以在Reference Mannual第三章节中找到 */ 
  
  SIM_SCGC7 |= SIM_SCGC7_DMA_MASK; 
  
  DMA_BASE_PTR->TCD[0].SADDR = (unsigned int)&I2C1_D;                  /* 分配DMA源地址 */ 
  DMA_BASE_PTR->TCD[0].DADDR = (unsigned int)&result;                  /* 分配DMA目标地址 */ 
  DMA_BASE_PTR->TCD[0].NBYTES_MLNO = 1;                 /* 每次minor loop传送1个字节 */ 
  DMA_BASE_PTR->TCD[0].ATTR  =(0 
                                     |DMA_ATTR_SMOD(0)        /* Source modulo feature disabled */ 
                                     | DMA_ATTR_SSIZE(0)      /* Source size, 8位传送 */ 
                                     | DMA_ATTR_DMOD(0)       /* Destination modulo feature disabled */ 
                                     | DMA_ATTR_DSIZE(0)      /* Destination size, 8位传送 */ 
                                    ); 
  DMA_BASE_PTR->TCD[0].SOFF  = 0x0000;                  /* 每次操作完源地址,源地址不增加 */ 
  DMA_BASE_PTR->TCD[0].DOFF  = 0x0001;                  /* 每次操作完目标地址,目标地址增加1  */ 
  DMA_BASE_PTR->TCD[0].SLAST = 0x00;                    /* DMA完成一次输出之后即major_loop衰减完之后不更改源地址 */ 
  DMA_BASE_PTR->TCD[0].DLAST_SGA = 0x00;                /* DMA完成一次输出之后即major_loop衰减完之后不更改目标地址 */ 
  DMA_BASE_PTR->TCD[0].CITER_ELINKNO = DMA_CITER_ELINKNO_CITER(6-1); /* 1个major loop, 即一次传输量=major_loop*minor_loop,最大为2^15=32767 */ 
  DMA_BASE_PTR->TCD[0].BITER_ELINKNO = DMA_CITER_ELINKNO_CITER(6-1); /* BITER应该等于CITER */ 
  
  DMA_BASE_PTR->TCD[0].CSR = 0;                         /* 先清零CSR,之后再设置 */ 
  
  DMA_INT |= 1<<0;                                      /* 先清零DMA相应通道的传输完成中断,与TCD_CSR_INTMAJOR或者TCD_CSR_INTHALF搭配 */ 
  DMA_BASE_PTR->TCD[0].CSR |= DMA_CSR_INTMAJOR_MASK;   /* 打开DMA major_loop完成中断 */ 
  DMA_BASE_PTR->TCD[0].CSR &= ~DMA_CSR_DREQ_MASK;      /* major_loop递减为0时不关闭ERQ功能,准备下一次DMA Major loop传输 */ 
  
  /* DMA_ERQ寄存器很重要,置位相应的位即开启DMA工作 */ 
  //DMA_ERQ |= 1 << 0;                                   /* 打开相应通道的DMA请求*/ 
  
  enable_irq(INT_DMA0-16); 
}

2. I2C配置部分,由于前面的发送地址信息等操作还是传统的手动方式,所以I2C初始化部分就不需要改了,我们只需要修改读取多字节数据的API函数即可,具体操作如下:

void I2CReadMultiRegisters(unsigned char u8RegisterAddress, unsigned char bytes) 
{ 
    unsigned char n = bytes; 
    int i;
    /* Send Slave Address */ 
    IIC_StartTransmission(SlaveID, MWSR); 
    i2c_Wait();
    /* Write Register Address */ 
    I2C1_D = u8RegisterAddress; 
    i2c_Wait();
    /* Do a repeated start */ 
    I2C1_C1 |= I2C_C1_RSTA_MASK;
    /* Send Slave Address */ 
    I2C1_D = (ACCEL_I2C_ADDRESS << 1) | 0x01;    //read address 
    i2c_Wait();
    /* Put in Rx Mode */ 
    I2C1_C1 &= (~I2C_C1_TX_MASK);
    /* Ensure TXAK bit is 0 */ 
    I2C1_C1 &= ~I2C_C1_TXAK_MASK;
    /* Dummy read */ 
    result[0] = I2C1_D; 
        
        I2C1_C1 |= I2C_C1_IICIE_MASK | I2C_C1_DMAEN_MASK; /* 在发送完地址信息之后打开DMA功能,Added by JiCheng */ 
        DMA_ERQ |= 1 << 0; //打开DMA 
}

3. 然后就是DMA接收完成中断了,这里面有一个地方需要注意一下,这里我以我司三轴加速度传感器为例,其分辨率为14位即每轴占用2个字节数据,这样的话应该是6个字节的数据,但是我这里在重新配置DMA接收长度时(即接收几个字节后产生DMA中断)写的是6-1即5(回到第一步中也是),这个在我们手中的I2C章节里特别标注了如下,即I2C在使用DMA读取数据时,其最后一个字节数据需要回到手动接收已提供NACK和STOP指令如下图,还有就是我们在DMA中断的最后需要把I2C的DMA先关掉,因为下次读取还要手动去发送地址信息呢,具体代码如下:

void DMA_ISR(void) 
{ 
    DMA_INT |= 1<<0; 
    
    DMA_BASE_PTR->TCD[0].DADDR = (unsigned int)&result;                  /* 分配DMA目标地址 */ 
    DMA_BASE_PTR->TCD[0].CITER_ELINKNO = DMA_CITER_ELINKNO_CITER(6-1); /* 1个major loop, 即一次传输量=major_loop*minor_loop,最大为2^15=32767 */ 
    DMA_BASE_PTR->TCD[0].BITER_ELINKNO = DMA_CITER_ELINKNO_CITER(6-1); /* BITER应该等于CITER */ 
    DMA_ERQ &= ~(1 << 0); 
    I2C1_C1 &=~ (I2C_C1_IICIE_MASK | I2C_C1_DMAEN_MASK); /* Added by JiCheng */ 
    /* Turn off ACK since this is  last read */ 
    I2C1_C1 |= I2C_C1_TXAK_MASK; 
         
    i2c_Wait();
        
    /* Send stop */ 
    i2c_Stop(); 
                
       result[5] = I2C1_D; 
    
    printf("%d  %d  %d  %d  %d  %d\n\r", result[0], result[1], result[2], result[3], result[4], result[5]); 
    
}

clip_image002

4. 最后我们简单调用一下I2C读取多字节API函数,即可以实现高大上的I2C+DMA读取功能了。

void main(void) 
{ 
    printf("Kinetis %s I2C Demo\n", TWR_STRING);
    /* Initialize I2C */ 
    init_I2C();
        eDMA_Init(); 
        
    /* Configure accelemoter sensor */ 
    I2CWriteRegister(0x2A, 0x01);
    printf("   X     Y     Z\n");
    while (1) { 
        /* Look at status of accelerometer */ 
        I2CReadMultiRegisters(0x01, 6);
        /* Delay for 250ms */ 
        time_delay_ms(100); 
        printf("\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"); 
    } 
}

好了,这样就结束了,呼,这篇文章写的有点high写的有点多,一步步走下来还是比较顺畅的,大家可以好好参考学习一下,有问题博客下面留言交流,好了,正好高铁也要下站了,刚刚好啊,收拾收拾东西准备撤了,未完待续~

  1. @wangrui449966   

    博主,您好,有个问题我想问一下


     DMA_BASE_PTR->TCD[0].DADDR = (unsigned int)&result;                  /* 分配DMA目标地址 */

    这一句里面的result是不是应该不用加取地址符?在您的代码里,可以看出,result是一个数组名,这里的取地址符是不是应该去掉呢?

    这个是C语言的问题,加不加‘&’都可以。

  2. 博主,您好,有个问题我想问一下


     DMA_BASE_PTR->TCD[0].DADDR = (unsigned int)&result;                  /* 分配DMA目标地址 */

    这一句里面的result是不是应该不用加取地址符?在您的代码里,可以看出,result是一个数组名,这里的取地址符是不是应该去掉呢?

  3. 学习         

  4. 今天特意注册了,支持你!