目录
一、硬件概况
本实验采用的开发板是NXP官方发布的i.MX RT1020评估板,该评估板的官方介绍链接如下,需要详细了解的小伙伴可点击查看。
i.MX RT1020 Evaluation Kit | NXP Semiconductorshttps://www.nxp.com/design/development-boards/i-mx-evaluation-and-development-boards/i-mx-rt1020-evaluation-kit:MIMXRT1020-EVK 首先摆在我们面前的问题就是i.MX RT1020评估板上的CPU(也就是MIMXRT1021DAG5A,这个在上面的链接中可以找到)有几个I2C接口,以及我们可以用哪个I2C接口进行通信?这是我们就需要去翻看芯片手册了,这里我找到了芯片的data sheet。小伙伴们可以通过下面链接进行下载。
i.MX RT1020处理器一共有两个型号MIMXRT1021DAG5A和MIMXRT1021DAF5A,官方评估板采用的是前者,而MIMXRT1021DAG5A中包含4个I2C接口,这点可以在data sheet中看到,截图如下
接下来我们需要在评估板中找到一个I2C接口进行通信。这里我们可以在官方公布的评估板设计文件中找到答案。这些设计文件可以在评估板的官方介绍链接中进行下载。打开其中的原理图pdf文件。这里我采用的是J18连接器的5和6两个引脚的I2C接口完成实验,如下图所示。
我们到目前已经知道了MIMXRT1021DAG5A包含四个I2C接口,那么J18的5和6引脚的I2C接口是4个中的哪个呢?
这里我们需要参考一个很重要的官方文档-i.MX RT1020 Processor Reference Manual,该文档大家可以在下载data sheet的链接中进行下载。
在这篇文档的第10章(External Signals and Pin Multiplexing),我们可以看到如下内容。
从原理图中我们可以看到J18的5和6连接到了CPU的GPIO_AD_B1_15和GPIO_AD_B1_14,而这两个引脚正好是LPI2C1的SDA和SCL引脚。
二、芯片内部的I2C模块基本工作机制
做I2C实验,需要先了解芯片内部的I2C模块,这里参考的手册是NXP官方的i.MX RT1020 Processor Reference Manual手册,下载链接如下
Search | NXP Semiconductorshttps://www.nxp.com/search?keyword=i.MX%20RT1020&start=0&category=documents 接下来通过手册,我们大体了解下I2C模块基本情况
通过上面截图我们知道I2C模块即可以作为主机,也可以作为从机,并且支持多种模式,包括标准、快速等等,同时还支持DMA操作(以后我们会讲到用DMA做I2C通信)。
上图是I2C模块示意图,由于我们的芯片是做主机与其他I2C设备进行通信的,所以这里我们重点关注作为主机的情况。从图中我们至少可以看到不管是接收还是发送都是通过FIFO实现的。由此我们大概能够猜到,如果我们要发送数据,会将发送数据写入FIFO中,接下来I2C模块内部逻辑会把数据从FIFO中读出,然后通过I2C总线发送出去;如果接收数据,内部逻辑会把收到的数据写入RX FIFO中,我们只需要从FIFO中读取数据即可。
通过上面截图,我们知道发送和接收FIFO都是四字大小的(在IMXRT1020中,一个字是四个字节;这里的FIFO大小是可以配置的,不过默认是四字)。发送FIFO不仅用来发送数据,也用来发送指令,大家可能有疑惑,什么是指令?其实上面的截图也说的很清楚,就是START和STOP。我们都知道要进行I2C通信,首先需要发送START起始位,那么怎么发送起始位呢?就是通过往transmit FIFO中写入一个指令,内部逻辑读到这个指令后,就会在I2C总线上产生一个起始位。那如何区分是数据还是指令呢?这个我们后面会给出答案。
通过上面截图我们知道在I2C总线空闲,transmit FIFO非空时主机会发起I2C通信。
通过以上内容我们大体知道了1020实现I2C的套路,核心就是,就是读写FIFO,那么问题来了,我们如何实现读写FIFO的操作呢?
这里有两个很重要的寄存器:MTDR和MRDR。我们先看MTDR
上图是MTDR寄存器各个区域定义,看到它,关于如何区分指令与数据的问题就有答案了。如果我们要发送起始位,那么CMD区域就写入100/101,DATA区域就是从设备地址;如果我们要写入数据,那么CMD区域就写入000,DATA区域就是我们要发送的数据。也就是说MTDR寄存器是transmit FIFO的访问接口,我们通过该寄存器向FIFO中写入数据!
上图是MRDR寄存器各个区域定义,其中DATA区域就是从I2C总线上读到的数据,也就是说我们通过该寄存器从RX FIFO中读取数据!
经过上面的分析,我们对1020内部的I2C模块有了基本了解,下一篇我会分享用轮询方式实现I2C通信的代码分析(基于官方examples)。
三、官方example代码分析(轮询)
官方SDK中提供的I2C例程还挺全的,如下图
可以看到,例程中既有作为主机的,也有作为从机的,这里我们只看作为主机的例程。而作为主机的例程又分别采用三种不同方式实现了I2C通信:轮询、中断、DMA+中断三种方式。我们首先分析其中最简单的实现方式:轮询。也就是如下图所示的例程。另外两种实现方式的代码放在后面分析。
/*Clock setting for LPI2C*/
CLOCK_SetMux(kCLOCK_Lpi2cMux, LPI2C_CLOCK_SOURCE_SELECT);
CLOCK_SetDiv(kCLOCK_Lpi2cDiv, LPI2C_CLOCK_SOURCE_DIVIDER);
上面的代码是对lpi2c模块的时钟进行配置,我们具体看下CLOCK_SetMux函数定义,如下
static inline void CLOCK_SetMux(clock_mux_t mux, uint32_t value)
{
uint32_t busyShift;
busyShift = CCM_TUPLE_BUSY_SHIFT(mux);
CCM_TUPLE_REG(CCM, mux) = (CCM_TUPLE_REG(CCM, mux) & (~CCM_TUPLE_MASK(mux))) |
(((uint32_t)((value) << CCM_TUPLE_SHIFT(mux))) & CCM_TUPLE_MASK(mux));
assert(busyShift <= CCM_NO_BUSY_WAIT);
/* Clock switch need Handshake? */
if (CCM_NO_BUSY_WAIT != busyShift)
{
/* Wait until CCM internal handshake finish. */
while ((CCM->CDHIPR & (1UL << busyShift)) != 0UL)
{
}
}
}
上面的代码核心的内容就是对一个寄存器进行配置,那么CCM_TUPLE_REG(CCM, mux)究竟指的是哪个寄存器呢?根据宏定义我们知道它展开后是下面这句,
(*((volatile uint32_t *)(((uint32_t)(((CCM_Type *)(0x400FC000u)))) + (((uint32_t)mux) & 0xFFU))))
也就是说这个寄存器地址是0x400FC000基础上加上mux形参的低八位!而mux就是kCLOCK_Lpi2cMux,通过查看它的定义,我们知道mux的低八位是0x38,也就是所我们要配置的寄存器地址是0x400FC038,查看参考手册,该寄存器定义如下
这个寄存器有两个区域需要配置,一个区域控制i2c模块输入时钟的分频系数(24-19);一个区域控制i2c模块输入时钟的来源(18)。而CLOCK_SetMux就是用来配置输入时钟的来源,CLOCK_SetDiv配置输入时钟的分频系数!这点大家也可以通过SDK中对这两个函数的注释中得到答案,如下
这里设计到了1020内部一个很重要的模块,时钟控制器模块(CCM),该模块为许多其他的片内模块提供时钟,手册描述如下
关于这个模块我们不详细说,只看一个手册上的时钟树的截图,如下
这是时钟树I2C部分截图,图中CSCDR2正是我们上面说的地址为0x400FC038的寄存器!该寄存器两个有效区域也在图中用选择器和分频图标体现,这也说明我们上面的分析是正确的!
LPI2C_MasterGetDefaultConfig(&masterConfig);
/* Change the default baudrate configuration */
masterConfig.baudRate_Hz = LPI2C_BAUDRATE;
/* Initialize the LPI2C master peripheral */
LPI2C_MasterInit(EXAMPLE_I2C_MASTER, &masterConfig, LPI2C_MASTER_CLOCK_FREQUENCY);
继续往下我们看到上面代码,这里SDK中定义了一个lpi2c_master_config_t结构体,该结构体中包含了I2C配置的各个项,这里我们不逐个分析。LPI2C_MasterGetDefaultConfig函数会对该结构体变量进行赋值(当然是按照主机模式进行初始化),接下来配置I2C时钟,也就是SCL时钟,接下来调用LPI2C_MasterInit,对I2C模块进行配置,这里我们看下该函数的三个参数。
EXAMPLE_I2C_MASTER是I2C1的基地址,我们知道1020一共有三个I2C,而我们用的是I2C1,如何在配置时正确配置到我们想要的I2C接口呢?就是通过这个参数实现的。
masterConfig中包含了I2C模块的各个配置项,LPI2C_MasterInit就是通过该参数进行配置的。
LPI2C_MASTER_CLOCK_FREQUENCY是一个宏,根据SDK对该函数的注释,我们知道该参数用来指定I2C模块的functional clock的时钟频率的,
我们再看下时钟树的截图,LPI2C_CLK_ROOT是由一个pll3_sw_clk的时钟八分频,再经过CSCDR2设置的分频系数分频生成的,而PLL3还有一个名称,如下
就是USB1PLL。我们再来看下LPI2C_MASTER_CLOCK_FREQUENCY宏定义如下,
/* Get frequency of lpi2c clock */
#define LPI2C_CLOCK_FREQUENCY ((CLOCK_GetFreq(kCLOCK_Usb1PllClk) / 8) / (LPI2C_CLOCK_SOURCE_DIVIDER + 1U))
#define LPI2C_MASTER_CLOCK_FREQUENCY LPI2C_CLOCK_FREQUENCY
LPI2C_MASTER_CLOCK_FREQUENCY其实说的就是LPI2C_CLK_ROOT的频率,而其定义和时钟树画的是一致的。
以上是I2C的配置阶段,接下来可以进行I2C通信了。
LPI2C_MasterStart(EXAMPLE_I2C_MASTER, LPI2C_MASTER_SLAVE_ADDR_7BIT, kLPI2C_Write)
查看LPI2C_MasterStart函数注释我们知道,该函数实现了发送起始位和从机地址的功能,查看该函数的具体定义我们可以看到该函数也是通过配置MTDR寄存器实现功能的,这和我们之前说的理论部分是一致的。
LPI2C_MasterGetFifoCounts(EXAMPLE_I2C_MASTER, NULL, &txCount);
while (txCount)
{
LPI2C_MasterGetFifoCounts(EXAMPLE_I2C_MASTER, NULL, &txCount);
}
LPI2C_MasterGetFifoCounts能够查看I2C模块中FIFO中字的个数(函数注释),这里我们只看发送的FIFO,所以rxCount位置填NULL即可。这段代码的意思也就很清楚了,刚刚我们向FIFO中写入了一个字,就是发送起始位和从机地址,然后在这里我们通过while来看这个字是否发送出去了,如果发送出去了,FIFO应该为空,继续向下运行;否则等待。
if (LPI2C_MasterGetStatusFlags(EXAMPLE_I2C_MASTER) & kLPI2C_MasterNackDetectFlag)
{
return kStatus_LPI2C_Nak;
}
上面的代码就是看从机是否给出ACK应答,如果有应答,继续向下运行。
reVal = LPI2C_MasterSend(EXAMPLE_I2C_MASTER, &deviceAddress, 1);
if (reVal != kStatus_Success)
{
if (reVal == kStatus_LPI2C_Nak)
{
LPI2C_MasterStop(EXAMPLE_I2C_MASTER);
}
return -1;
}
reVal = LPI2C_MasterSend(EXAMPLE_I2C_MASTER, g_master_txBuff, LPI2C_DATA_LENGTH);
if (reVal != kStatus_Success)
{
if (reVal == kStatus_LPI2C_Nak)
{
LPI2C_MasterStop(EXAMPLE_I2C_MASTER);
}
return -1;
}
接下来的代码,调用LPI2C_MasterSend函数进行数据发送,该函数的最后一个形参是发送的字节数,这里的官方例程应该假设访问的I2C设备是以I2C作为接口的EEPROM,所以需要先给出一个写入首地址,然后是写入的数据。注意该函数是非阻塞的,查看该函数定义可以看到,该函数实际是只要FIFO中有空间,就将数据写入到里面,全部写入完成就返回。并不是等要发送的数据都发送完再返回!也就是说,该函数执行完了,只说明要发送的数据进入FIFO了,但不一定发送出去了。
reVal = LPI2C_MasterStop(EXAMPLE_I2C_MASTER);
if (reVal != kStatus_Success)
{
return -1;
}
LPI2C_MasterStop是发送停止位的函数,需要说明的是该函数只有在停止位发送出去后才会返回(函数注释),所以只要该函数返回就代表着之前写入FIFO的数据都已经发送出去了!至此,我们已经向EEPROM设备中写入了一些数据,接下来我们再把这些数据通过I2C总线读出。
reVal = LPI2C_MasterRepeatedStart(EXAMPLE_I2C_MASTER, LPI2C_MASTER_SLAVE_ADDR_7BIT, kLPI2C_Read);
if (reVal != kStatus_Success)
{
return -1;
}
在读操作中,发送完起始位、器件地址和读出首地址(这些和写操作类似,在此不赘述)后,需要再发送一个起始位,然后开始读数据,LPI2C_MasterRepeatedStart就是实现该功能的。
reVal = LPI2C_MasterReceive(EXAMPLE_I2C_MASTER, g_master_rxBuff, LPI2C_DATA_LENGTH - 1);
if (reVal != kStatus_Success)
{
if (reVal == kStatus_LPI2C_Nak)
{
LPI2C_MasterStop(EXAMPLE_I2C_MASTER);
}
return -1;
}
LPI2C_MasterReceive就是实现读取数据的功能,查看该函数定义我们可以看到,它其实就是通过读取MRDR寄存器实现读功能的,这和上面说的可以对上,另外在定义中能够看到,该函数在监测FIFO非空的时候,才会将数据放入用户传入的形参buffer中,所以该函数执行完,就代表着数据已经读取完成了!
四、I2C中断机制
由上,我们知道1020内部有一个中断控制器,缩写NVIC,它会收集各个中断源,然后通过一个接口给的cortex-M7内核(1020内部的处理器)。通过Table 4-2我们可以看到我们要用的I2C1的中断,如下图所示
NVIC给I2C1分配了一个中断号,28,也就是所有I2C1中断都用此中断号触发中断,那么I2C1中有多少个中断呢?
Table 42-8中列除了I2C中所有的中断,上面截图只截取了一部分,这里我们重点关注两个,TDF和RDF。看下图
TDF置1代表TXFIFO中字的数量小于等于MFCR[TXWATER],而MFCR[TXWATER]也就是TXFIFO的watermark,也就是说,当TXFIFO中的数据小于等于watermark时,TDF拉高,进而触发中断;当RXFIFO中的数据大于watermark时,RDF拉高,进而触发中断。
五、官方example代码分析(中断)
这节我们分析的代码是采用中断方式实现I2C通信的官方例程,如下图所示
该工程中有些函数在轮询代码中分析过,这里就不赘述了,我们看下之前没有分析过的函数。
void LPI2C_MasterTransferCreateHandle(LPI2C_Type *base,
lpi2c_master_handle_t *handle,
lpi2c_master_transfer_callback_t callback,
void *userData)
{
uint32_t instance;
assert(NULL != handle);
/* Clear out the handle. */
(void)memset(handle, 0, sizeof(*handle));
/* Look up instance number */
instance = LPI2C_GetInstance(base);
/* Save base and instance. */
handle->completionCallback = callback;
handle->userData = userData;
/* Save this handle for IRQ use. */
s_lpi2cMasterHandle[instance] = handle;
/* Set irq handler. */
s_lpi2cMasterIsr = LPI2C_MasterTransferHandleIRQ;
/* Clear internal IRQ enables and enable NVIC IRQ. */
LPI2C_MasterDisableInterrupts(base, (uint32_t)kLPI2C_MasterIrqFlags);
/* Enable NVIC IRQ, this only enables the IRQ directly connected to the NVIC.
In some cases the LPI2C IRQ is configured through INTMUX, user needs to enable
INTMUX IRQ in application code. */
(void)EnableIRQ(kLpi2cIrqs[instance]);
}
上面函数初始化了一个lpi2c_master_handle_t类型的变量,该变量在收发数据时需要用到,同时,将我们用户自己定义的中断回调函数接入系统中。这个中断系统总体工作流程是这样的,首先在startup_mimxrt1021.c中有一个名叫g_pfnVectors的数组,该数组中的成员都是各个中断源的中断处理函数,当某一个中断源产生中断时,对应的中断处理函数就会被调用,假设I2C1产生中断时,该数组中的名为LPI2C1_IRQHandler的函数就会被调用,我们可以看下该函数的定义如下,
WEAK void LPI2C1_IRQHandler(void)
{ LPI2C1_DriverIRQHandler();
}
该函数又会调用LPI2C1_DriverIRQHandler函数,它又会调用LPI2C_CommonIRQHandler函数,如下,
void LPI2C1_DriverIRQHandler(void)
{
LPI2C_CommonIRQHandler(LPI2C1, 1U);
}
我们看下LPI2C_CommonIRQHandler函数定义如下,
static void LPI2C_CommonIRQHandler(LPI2C_Type *base, uint32_t instance)
{
/* Check for master IRQ. */
if ((0U != (base->MCR & LPI2C_MCR_MEN_MASK)) && (NULL != s_lpi2cMasterIsr))
{
/* Master mode. */
s_lpi2cMasterIsr(base, s_lpi2cMasterHandle[instance]);
}
/* Check for slave IRQ. */
if ((0U != (base->SCR & LPI2C_SCR_SEN_MASK)) && (NULL != s_lpi2cSlaveIsr))
{
/* Slave mode. */
s_lpi2cSlaveIsr(base, s_lpi2cSlaveHandle[instance]);
}
SDK_ISR_EXIT_BARRIER;
}
由于我们这里用master模式,所以s_lpi2cMasterIsr会被调用,而查看s_lpi2cMasterIsr定义我们会发现,它其实是一个函数指针,并且在LPI2C_MasterTransferCreateHandle函数(上面有该函数定义)中被赋值的,如下
s_lpi2cMasterIsr = LPI2C_MasterTransferHandleIRQ;
其实执行的是指针指向的函数,我们再看下LPI2C_MasterTransferHandleIRQ定义,这里不全粘代码了,我们重点看下其中有一句如下,
handle->completionCallback(base, handle, result, handle->userData);
这里的handle其实就是s_lpi2cMasterHandle[instance],而它的赋值也是在LPI2C_MasterTransferCreateHandle函数中,如下,
/* Save base and instance. */
handle->completionCallback = callback;
handle->userData = userData;
/* Save this handle for IRQ use. */
s_lpi2cMasterHandle[instance] = handle;
completionCallback不就是我们传入的回调函数嘛,所以绕了这么大一圈,总结就是,当中断源产生中断,最终会调用到我们用户指定的回调函数!
LPI2C_MasterTransferCreateHandle函数还有一个点需要我们注意,就是该函数调用LPI2C_MasterDisableInterrupts函数,会禁止一些中断,但是中断一旦禁止了,就不会产生中断了,那这个例程岂不是有问题吗?当然不是,因为在后面又会开启中断。这里禁止的中断中包括了kLPI2C_MasterTxReadyFlag和kLPI2C_MasterRxReadyFlag,这两个对应之前说的TDF和RDF。
接下来有一个lpi2c_master_transfer_t类型的变量masterXfer,该变量中的成员是进行I2C通信的相关信息,包括传输方向、设备地址、读或写的起始地址、要读写的字节个数等等。对这些内容赋值完成后,调用LPI2C_MasterTransferNonBlocking函数开始进行I2C通信,该函数是非阻塞的,也就是说,该函数并不会等通信结束后才会返回,那么我们如何判断数据传输是否结束了呢?
根据例程,接下来会判断一个变量,叫做g_MasterCompletionFlag,该变量为true,代表传输结束,而它只有在中断的回调函数中才被赋值为true,如下,
static void lpi2c_master_callback(LPI2C_Type *base, lpi2c_master_handle_t *handle, status_t status, void *userData)
{
if (status == kStatus_LPI2C_Nak)
{
g_MasterNackFlag = true;
}
else
{
g_MasterCompletionFlag = true;
/* Display failure information when status is not success. */
if (status != kStatus_Success)
{
PRINTF("Error occured during transfer!");
}
}
}
这里的逻辑在哪里?为什么进入了中断回调代表传输结束?我们知道,I2C通信是通过读写FIFO实现的,而上面的分析又没有向FIFO中写入数据,这是怎么回事?中断不是在初始化的时候禁能了吗?什么时候打开的?接下来,我们一点点揭晓答案。
LPI2C_MasterTransferNonBlocking最后会调用LPI2C_MasterEnableInterrupts函数,将之前禁能的函数重新使能。这时,由于我们并没有往TXFIFO中写入数据,所以该FIFO为空,并且在初始化阶段我们并没有修改MFCR寄存器数值,所以该寄存器保持默认值,全0,该寄存器的TXWATER域代表FIFO的水位线,当FIFO中的数据小于等于TXWATER时,TDF标志会有效进而触发中断。
而在当前阶段不正好满足这个条件吗?所以LPI2C_MasterTransferNonBlocking调用后会产生中断,上面对中断流程的分析我们知道LPI2C_MasterTransferHandleIRQ会被调用,该函数中会调用LPI2C_RunTransferStateMachine。在LPI2C_RunTransferStateMachine中我们可以看到MTDR被写入数值,等所有数据都发送完成,会跳转到中断回调中。所以进入了中断回调就代表着数据发送完成。后面的读操作和写操作类似,不再进行分析。
六、I2C的DMA实现方式
DMA通俗的理解,可以把它理解成通道,这个通道一边是源地址,一边是目的地址,它会把数据从源地址搬到目的地址。官方解释如下,
从上面可以看到,1020芯片采用的是第二代的DMA控制器,其中包含32个通道。我们知道1020芯片包括多种外设,可是DMA通道只有32个,这该怎么分呢?这时就需要DMAMUX登场了!
它会把需要进行DMA操作的外设连接到DMA通道上,如下图,
一共有128个外设可以使用DMA通道,我们的I2C也在其中。
DMA模块由两部分组成:DMA引擎和用于存储传输控制描述符的存储空间构成,如下图所示。
那么什么是DMA引擎呢?官方解释如下
总结就是,具体执行数据搬运的单元就是DMA引擎。
什么又是传输控制描述符呢?传输控制描述符英文是transfer control descriptor,缩写是TCD。每个DMA通道都有一个32字节的TCD用来定义数据传输操作,比如源地址、目的地址、传输字节数量等等。而这些TCD都在DMA模块中存储,需要进行数据传输时,就把相应的TCD读出。
现在我们对DMA有了一个大概的了解,接下来我们通过手册上的两个例子具体看看TCD该如何配置 。
这个例子中我们只进行一次DMA传输,从0X1000地址处传输16个字节数据到0X2000处,并且源地址处的存储空间接口是8位的,目的地址处的存储空间接口是32位的。针对这个情况对TCD进行如下配置
我们先看CITER和BITER。这里我们需要先清楚minor loop的概念,minor loop是DMA通道进行了一次数据搬运操作。那这次搬运了多少个字节呢?这由NBYTES决定,如下,
这个例子中我们只进行一次数据搬运,并且搬运16个字节数据,所以这里的NBYTES设置成16。
还有一个major loop的概念,major loop是由一个或者多个minor loop构成,而具体包含几个minor loop,是由BITER决定的,如下,
BITER用来定义主循环迭代次数,并且该数值与CITER相等,当一个minor loop结束后,CITER的数值会减一,当CITER为0时,代表所有minor loop已经执行完,此时可以选择是否产生一个中断。
我们这个例子中,只进行一次传输,所以主循环迭代次数设置为1,也即是BITER和CITER都为1。SADDR和DADDR分别是源和目的地址,这里分别是0X1000和0X2000。
SOFF和DOFF代表源或目的地址每次地址自增的数量,这里源地址处的存储空间接口是8位的,目的地址处的存储空间接口是32位的,所以,每次地址自增分别是1和4,所以SOFF为1,DOFF为4。
SLAST和DLASTSGA代表当主迭代计数完成后源或目的地址调整的量, 这个例子中这两个数值都为-16,也就是传输完成后,源和目的地址都回归到传输前的位置。
接下来这个例子是会进行两次minor loop,每次由一个外设发起请求,每次传输16字节,一共传输32字节。所以这里CITER和BITER都为2;传输完成,我们希望源和目的地址回归到传输前的位置,所以 SLAST和DLASTSGA均为32。
七、官方example代码分析(DMA)
这节我们分析的代码是采用DMA方式实现I2C通信的官方例程。
之前分析过的代码我们略过,直接跳到如下部分,
#if defined(FSL_FEATURE_SOC_DMAMUX_COUNT) && FSL_FEATURE_SOC_DMAMUX_COUNT
/* DMAMUX init */
DMAMUX_Init(EXAMPLE_LPI2C_MASTER_DMA_MUX);
DMAMUX_SetSource(EXAMPLE_LPI2C_MASTER_DMA_MUX, LPI2C_TRANSMIT_DMA_CHANNEL,
LPI2CMASTER_TRANSMIT_EDMA_REQUEST_SOURCE);
DMAMUX_EnableChannel(EXAMPLE_LPI2C_MASTER_DMA_MUX, LPI2C_TRANSMIT_DMA_CHANNEL);
DMAMUX_SetSource(EXAMPLE_LPI2C_MASTER_DMA_MUX, LPI2C_RECEIVE_DMA_CHANNEL, LPI2CMASTER_RECEIVE_EDMA_REQUEST_SOURCE);
DMAMUX_EnableChannel(EXAMPLE_LPI2C_MASTER_DMA_MUX, LPI2C_RECEIVE_DMA_CHANNEL);
#endif
这段代码很明显是配置DMAMUX,这里将LPI2C_TRANSMIT_DMA_CHANNEL和LPI2C_RECEIVE_DMA_CHANNEL两个通道都连接到LPI2C1外设,显然,其中一个用于发送,一个用于接收。
接下来我们直接跳到EDMA_CreateHandle函数,在该函数的定义中,会将对应DMA通道的中断打开,如下,
/* Enable NVIC interrupt */
(void)EnableIRQ(s_edmaIRQNumber[edmaInstance][channel]);
并且会把TCD区域全部置0。
tcdRegs = (edma_tcd_t *)(uint32_t)&handle->base->TCD[handle->channel];
tcdRegs->SADDR = 0;
tcdRegs->SOFF = 0;
tcdRegs->ATTR = 0;
tcdRegs->NBYTES = 0;
tcdRegs->SLAST = 0;
tcdRegs->DADDR = 0;
tcdRegs->DOFF = 0;
tcdRegs->CITER = 0;
tcdRegs->DLAST_SGA = 0;
tcdRegs->CSR = 0;
tcdRegs->BITER = 0;
void LPI2C_MasterCreateEDMAHandle(LPI2C_Type *base,
lpi2c_master_edma_handle_t *handle,
edma_handle_t *rxDmaHandle,
edma_handle_t *txDmaHandle,
lpi2c_master_edma_transfer_callback_t callback,
void *userData)
{
assert(handle != NULL);
assert(rxDmaHandle != NULL);
assert(txDmaHandle != NULL);
/* Look up instance number */
uint32_t instance = LPI2C_GetInstance(base);
/* Clear out the handle. */
(void)memset(handle, 0, sizeof(*handle));
/* Set up the handle. For combined rx/tx DMA requests, the tx channel handle is set to the rx handle */
/* in order to make the transfer API code simpler. */
handle->base = base;
handle->completionCallback = callback;
handle->userData = userData;
handle->rx = rxDmaHandle;
handle->tx = (FSL_FEATURE_LPI2C_HAS_SEPARATE_DMA_RX_TX_REQn(base) > 0) ? txDmaHandle : rxDmaHandle;
/* Save the handle in global variables to support the double weak mechanism. */
s_lpi2cMasterHandle[instance] = handle;
/* Set LPI2C_MasterTransferEdmaHandleIRQ as LPI2C DMA IRQ handler */
s_lpi2cMasterIsr = LPI2C_MasterTransferEdmaHandleIRQ;
/* Enable interrupt in NVIC. */
(void)EnableIRQ(kLpi2cIrqs[instance]);
/* Set DMA channel completion callbacks. */
EDMA_SetCallback(handle->rx, LPI2C_MasterEDMACallback, handle);
if (FSL_FEATURE_LPI2C_HAS_SEPARATE_DMA_RX_TX_REQn(base) != 0)
{
EDMA_SetCallback(handle->tx, LPI2C_MasterEDMACallback, handle);
}
}
在LPI2C_MasterCreateEDMAHandle中,我们会看到在中断代码中分析到的s_lpi2cMasterIsr函数指针,该指针指向LPI2C_MasterTransferEdmaHandleIRQ,也就是说LPI2C_MasterTransferEdmaHandleIRQ在I2C中断发生时会被调用,并且会将LPI2C_MasterCreateEDMAHandle中的handle作为形参传入,而handle->completionCallback又会在LPI2C_MasterTransferEdmaHandleIRQ中被调用,如下,
static void LPI2C_MasterTransferEdmaHandleIRQ(LPI2C_Type *base, void *lpi2cMasterEdmaHandle)
{
assert(lpi2cMasterEdmaHandle != NULL);
lpi2c_master_edma_handle_t *handle = (lpi2c_master_edma_handle_t *)lpi2cMasterEdmaHandle;
uint32_t status = LPI2C_MasterGetStatusFlags(base);
status_t result = kStatus_Success;
/* Terminate DMA transfers. */
EDMA_AbortTransfer(handle->rx);
if (FSL_FEATURE_LPI2C_HAS_SEPARATE_DMA_RX_TX_REQn(base) != 0)
{
EDMA_AbortTransfer(handle->tx);
}
/* Done with this transaction. */
handle->isBusy = false;
/* Disable LPI2C interrupts. */
LPI2C_MasterDisableInterrupts(base, (uint32_t)kLPI2C_MasterIrqFlags);
/* Check error status */
if (0U != (status & (uint32_t)kLPI2C_MasterPinLowTimeoutFlag))
{
result = kStatus_LPI2C_PinLowTimeout;
}
else if (0U != (status & (uint32_t)kLPI2C_MasterArbitrationLostFlag))
{
result = kStatus_LPI2C_ArbitrationLost;
}
else if (0U != (status & (uint32_t)kLPI2C_MasterNackDetectFlag))
{
result = kStatus_LPI2C_Nak;
}
else if (0U != (status & (uint32_t)kLPI2C_MasterFifoErrFlag))
{
result = kStatus_LPI2C_FifoError;
}
else
{
; /* Intentional empty */
}
/* Clear error status. */
(void)LPI2C_MasterCheckAndClearError(base, status);
/* Send stop flag if needed */
if (0U == (handle->transfer.flags & (uint32_t)kLPI2C_TransferNoStopFlag))
{
status = LPI2C_MasterGetStatusFlags(base);
/* If bus is still busy and the master has not generate stop flag */
if ((status & ((uint32_t)kLPI2C_MasterBusBusyFlag | (uint32_t)kLPI2C_MasterStopDetectFlag)) ==
(uint32_t)kLPI2C_MasterBusBusyFlag)
{
/* Send a stop command to finalize the transfer. */
handle->base->MTDR = (uint32_t)kStopCmd;
}
}
/* Invoke callback. */
if (handle->completionCallback != NULL)
{
handle->completionCallback(base, handle, result, handle->userData);
}
}
等等,我们这里是通过DMA实现I2C通信,为什么又用上了I2C中断,要用也应该是DMA的中断啊?这个问题我们讲解到后面代码会有答案。不仅如此,LPI2C_MasterCreateEDMAHandle中又调用EnableIRQ使能了I2C中断。
在LPI2C_MasterCreateEDMAHandle的最后通过EDMA_SetCallback,设置DMA的中断回调为LPI2C_MasterEDMACallback。这里我们看下DMA中断函数调用的链路。首先在中断向量表中我们以表中DMA0_DMA16_IRQHandler中断处理函数为例,该函数调用DMA0_DMA16_DriverIRQHandler,
WEAK void DMA0_DMA16_IRQHandler(void)
{ DMA0_DMA16_DriverIRQHandler();
}
它又会调用EDMA_HandleIRQ,并且以s_EDMAHandle数组成员作为形参,该数组在EDMA_CreateHandle函数中被赋值,
s_EDMAHandle[channelIndex] = handle;
在EDMA_HandleIRQ中会调用s_EDMAHandle数组成员中的callback执行回调,
(handle->callback)(handle, handle->userData, transfer_done, 0);
而回调函数就是在EDMA_SetCallback中指定的,也就是LPI2C_MasterEDMACallback函数!在LPI2C_MasterEDMACallback函数中,最终会调用传入LPI2C_MasterCreateEDMAHandle函数中的回调函数!
所以我们可以得出结论,I2C中断和DMA中断调用的回调函数其实都是我们用户指定的同一个函数!
在LPI2C_MasterTransferEDMA函数中我们以写入数据为例进行分析,I2C写入数据之前需要先写入诸如设备地址等指令,然后写入数据。所以在该函数中要生成一些指令用于写操作,如下,
/* Generate commands to send. */
uint32_t commandCount = LPI2C_GenerateCommands(handle);
然后根据我们的写入的信息,配置TCD。这里配置TCD通过EDMA_TcdSetTransferConfig函数实现的,这里需要说明的是,我们写操作是通过向MTDR写入数据实现的,所以我们的目的地址就是该寄存器的地址,并且不需要自增,所以tcd->DOFF应该设置为0,也即是,
transferConfig.destOffset = 0;
另外将minor Loop设置为1,major Loop设置为要写入的字节数,这样做主要是MTDR寄存器每次只能接收一个字节的数据。但我们目前配置的TCD只能作为第二个要发送的TCD,因为在发送数据之前要先发送一些指令!
if (commandCount != 0U)
{
/* Create a software TCD, which will be chained after the commands. */
EDMA_TcdReset(tcd);
EDMA_TcdSetTransferConfig(tcd, &transferConfig, NULL);
EDMA_TcdEnableInterrupts(tcd, (uint32_t)kEDMA_MajorInterruptEnable);
linkTcd = tcd;
}
所以EDMA_TcdSetTransferConfig函数配置的TCD会被暂存在linkTcd中。在LPI2C_MasterTransferEDMA函数的底部,会把发送指令的TCD放入DMA控制器的TCD区域,然后会通过DLAST_SGA存储linkTcd的地址。
if (commandCount != 0U)
{
transferConfig.srcAddr = (uint32_t)handle->commandBuffer;
transferConfig.destAddr = (uint32_t)LPI2C_MasterGetTxFifoAddress(base);
transferConfig.srcTransferSize = kEDMA_TransferSize2Bytes;
transferConfig.destTransferSize = kEDMA_TransferSize2Bytes;
transferConfig.srcOffset = (int16_t)sizeof(uint16_t);
transferConfig.destOffset = 0;
transferConfig.minorLoopBytes = sizeof(uint16_t); /* TODO optimize to fill fifo */
transferConfig.majorLoopCounts = commandCount;
EDMA_SetTransferConfig(handle->tx->base, handle->tx->channel, &transferConfig, linkTcd);
}
在最后使能中断,这里也包括I2C中断,不过大家可以看到,这里使能的I2C中断都是发生I2C错误的时候才能触发的中断,所以正常的发送和接收是不会触发I2C中断的,所以在不出错误的情况下,我们用户定义的回调只会在DMA传输完成才会被调用。
/* Enable DMA in both directions. This actually kicks of the transfer. */
LPI2C_MasterEnableDMA(base, true, true);
/* Enable all LPI2C master interrupts */
LPI2C_MasterEnableInterrupts(base,
(uint32_t)kLPI2C_MasterArbitrationLostFlag | (uint32_t)kLPI2C_MasterNackDetectFlag |
(uint32_t)kLPI2C_MasterPinLowTimeoutFlag | (uint32_t)kLPI2C_MasterFifoErrFlag);