【原创】Kinetis K系列使用DMA实现I2C读取
0赞最近辗转出差中,一直也没抽出空来写上几篇(虽然俺的“素材库”已经积累不少了),今天又坐上高铁了,顿时满血复活,赶紧趁此机会码上几篇(啥?不只一篇?,嘿嘿,必须的,欠的债早晚要还的…),我都快成为高铁上写作专业户了,每次坐上高铁都是文思泉涌啊,压都压不住啊有木有(咳咳,主要是闲的没事干)!
好了,闲扯了一会儿预热了一下,下面开整正题吧。以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]); }
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写的有点多,一步步走下来还是比较顺畅的,大家可以好好参考学习一下,有问题博客下面留言交流,好了,正好高铁也要下站了,刚刚好啊,收拾收拾东西准备撤了,未完待续~