在单片机中,最基础的一个驱动就是串口,本文就以NXP中串口eDMA的收发为例,通过分析源代码来理解eDMA的执行过程。
uart_edma_transfer.c
第一步肯定是初始化串口的引脚和时钟,还有波特率、奇偶校验位等参数。
/* 引脚和时钟初始化函数:可以用MCUXpresso生成 */
BOARD_InitBootPins();
BOARD_InitBootClocks();
/* 获取默认配置并初始化串口 */
UART_GetDefaultConfig(&uartConfig);
uartConfig.enableTx = uartConfig.enableRx = true;
UART_Init(UART0, &uartConfig, CLOCK_GetFreq(UART0_CLK_SRC));
前面没有介绍DMAMUX,顾名思义,这个模块的功能就是将DMA的某个通道映射到某一源上,从而形成一一对应的关系。
再来看看代码
/* 初始化DMAMUX,实际上是使能DMAMUX时钟 */
DMAMUX_Init(DMAMUX0);
/* Set channel for UART */
DMAMUX_SetSource(DMAMUX0, UART_TX_DMA_CHANNEL, UART_TX_DMA_REQUEST);
DMAMUX_SetSource(DMAMUX0, UART_RX_DMA_CHANNEL, UART_RX_DMA_REQUEST);
DMAMUX_EnableChannel(DMAMUX0, UART_TX_DMA_CHANNEL);
DMAMUX_EnableChannel(DMAMUX0, UART_RX_DMA_CHANNEL);
DMAMUX0
UART_TX_DMA_REQUEST
和UART_RX_DMA_REQUEST
则是设置DMAMUX
寄存器中的源字段的宏定义,将串口0发送和接收的源对应的值设置到寄存器中即可上面两个函数实际上都是设置DMAMUX->CHCFG
寄存器。
uart_edma_handle_t g_uartEdmaHandle;
edma_handle_t g_uartTxEdmaHandle;
edma_handle_t g_uartRxEdmaHandle;
/* 默认配置:关闭通道循环仲裁,关闭通道连接模式,关闭Debug模式,打开出错时停止EDMA工作的功能 */
EDMA_GetDefaultConfig(&config);
/* 使能DMA时钟,清除DMA中断,中断请求,错误标志,根据上面的默认配置设置DMA->CR寄存器 */
EDMA_Init(DMA0, &config);
/* 初始化EDMA的发送和接收Handle */
EDMA_CreateHandle(&g_uartTxEdmaHandle, DMA0, UART_TX_DMA_CHANNEL);
EDMA_CreateHandle(&g_uartRxEdmaHandle, DMA0, UART_RX_DMA_CHANNEL);
/* 用发送和接收Handle初始化为一个总的EDMA Handle */
UART_TransferCreateHandleEDMA(UART0, &g_uartEdmaHandle, UART_UserCallback, NULL, &g_uartTxEdmaHandle, &g_uartRxEdmaHandle);
(1)EDMA_CreateHandle
函数:
typedef struct _edma_handle
{
edma_callback callback; /*!< Callback function for major count exhausted. */
void *userData; /*!< Callback function parameter. */
DMA_Type *base; /*!< eDMA peripheral base address. */
edma_tcd_t *tcdPool; /*!< Pointer to memory stored TCDs. */
uint8_t channel; /*!< eDMA channel number. */
volatile int8_t header; /*!< The first TCD index. Should point to the next TCD to be loaded into the eDMA engine. */
volatile int8_t tail; /*!< The last TCD index. Should point to the next TCD to be stored into the memory pool. */
volatile int8_t tcdUsed; /*!< The number of used TCD slots. Should reflect the number of TCDs can be used/loaded in
the memory. */
volatile int8_t tcdSize; /*!< The total number of TCD slots in the queue. */
uint8_t flags; /*!< The status of the current channel. */
} edma_handle_t;
-----
void EDMA_CreateHandle(edma_handle_t *handle, DMA_Type *base, uint32_t channel)
{
uint32_t edmaInstance;
uint32_t channelIndex;
edma_tcd_t *tcdRegs;
(void)memset(handle, 0, sizeof(*handle));
/* 在K64中恒为DMA0 */
handle->base = base;
/* DMA通道,取值范围为0~15 */
handle->channel = (uint8_t)channel;
/* 获取DMA的索引,DMAx则返回x,这里仅有DMA0,返回0 */
edmaInstance = EDMA_GetInstance(base);
/* 由于仅有一个DMA,故EDMA_GetInstanceOffset偏移返回0,故channelIndex=channel */
channelIndex = (EDMA_GetInstanceOffset(edmaInstance) * (uint32_t)FSL_FEATURE_EDMA_MODULE_CHANNEL) + channel;
/* fsl_edma.c中的全局静态变量,每个通道对应一个edma_handle_t */
s_EDMAHandle[channelIndex] = handle;
/* 使能通道x的DMA中断DMAx_IRQn */
(void)EnableIRQ(s_edmaIRQNumber[edmaInstance][channel]);
/* 复位通道对应的TCD内存 */
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;
}
(2)UART_TransferCreateHandleEDMA
函数
typedef void (*uart_edma_transfer_callback_t)(UART_Type *base,uart_edma_handle_t *handle,
status_t status, void *userData);
typedef struct _uart_edma_handle uart_edma_handle_t;
struct _uart_edma_handle
{
uart_edma_transfer_callback_t callback; /*!< Callback function. */
void *userData; /*!< UART callback function parameter.*/
size_t rxDataSizeAll; /*!< Size of the data to receive. */
size_t txDataSizeAll; /*!< Size of the data to send out. */
edma_handle_t *txEdmaHandle; /*!< The eDMA TX channel used. */
edma_handle_t *rxEdmaHandle; /*!< The eDMA RX channel used. */
uint8_t nbytes; /*!< eDMA minor byte transfer count initially configured. */
volatile uint8_t txState; /*!< TX transfer state. */
volatile uint8_t rxState; /*!< RX transfer state */
};
-----
void UART_TransferCreateHandleEDMA(UART_Type *base,
uart_edma_handle_t *handle,
uart_edma_transfer_callback_t callback,
void *userData,
edma_handle_t *txEdmaHandle,
edma_handle_t *rxEdmaHandle)
{
/* 获得串口的索引,UARTx则返回x,这里传入UART0返回0 */
uint32_t instance = UART_GetInstance(base);
/* 在fsl_uart_edma.c中的全局静态变量,保存每个串口的基地址和EDMA Handle */
s_edmaPrivateHandle[instance].base = base;
s_edmaPrivateHandle[instance].handle = handle;
/* 清零该结构体 */
(void)memset(handle, 0, sizeof(*handle));
/* 设置EDMA的发送和接收状态为idle */
handle->rxState = (uint8_t)kUART_RxIdle;
handle->txState = (uint8_t)kUART_TxIdle;
/* 保存前面初始化的DMA发送和接收Handle */
handle->rxEdmaHandle = rxEdmaHandle;
handle->txEdmaHandle = txEdmaHandle;
/* 回调函数:在eDMA发送完成和接收满时调用,具体代码参考fsl_uart_edma.c */
handle->callback = callback;
/* 回调函数参数 */
handle->userData = userData;
/* 如果打开了DMA接收通道和串口的FIFO,应FIFO的Watermark为1,避免收数据不及时进不了中断 */
if (rxEdmaHandle != NULL)
{
base->RWFIFO = 1U;
}
/* 在fsl_uart.c中的全局静态变量,保存每个串口的EDMA Handle,作为中断回调函数中的参数 */
s_uartHandle[instance] = handle;
/* 所有串口的中断处理函数,UARTx_RX_TX_DriverIRQHandler中调用 */
s_uartIsr = UART_TransferEdmaHandleIRQ;
/* 关闭串口的所有中断:空闲,overrun等标志位 */
UART_DisableInterrupts(base, (uint32_t)kUART_AllInterruptsEnable);
/* 使能串口的总中断,具体产生什么中断打开对应的标志位 */
(void)EnableIRQ(s_uartIRQ[instance]);
/* 设置TX EDMA Handle中的回调函数为SDK中的UART_SendEDMACallback,参数为总EDMA Handle */
if (txEdmaHandle != NULL)
{
EDMA_SetCallback(handle->txEdmaHandle, UART_SendEDMACallback, &s_edmaPrivateHandle[instance]);
}
/* 设置RX EDMA Handle中的回调函数为SDK中的UART_ReceiveEDMACallback,参数为数为总EDMA Handle */
if (rxEdmaHandle != NULL)
{
EDMA_SetCallback(handle->rxEdmaHandle, UART_ReceiveEDMACallback, &s_edmaPrivateHandle[instance]);
}
}
UART_TransferEdmaHandleIRQ
来说,SDK中仅打开了发送完成中断,所以这个函数就是处理发送完成再调用设置的callback
,如果还要打开空闲、overrun等中断,还需要修改此函数UART_SendEDMACallback
和UART_ReceiveEDMACallback
后续分析在完成上面的初始化工作后,就可以调用UART_SendEDMA
向串口发送数据了
uart_transfer_t xfer;
/* 数据地址和长度填充到xfer结构体中 */
xfer.data = "12345678";
xfer.dataSize = 8;
UART_SendEDMA(UART0, &g_uartEdmaHandle, &xfer);
所以我们就来看看UART_SendEDMA
中做了什么:
status_t UART_SendEDMA(UART_Type *base, uart_edma_handle_t *handle, uart_transfer_t *xfer)
{
edma_transfer_config_t xferConfig;
status_t status;
/* 如果之前的发送还没结束则直接返回 */
if ((uint8_t)kUART_TxBusy == handle->txState)
{
status = kStatus_UART_TxBusy;
}
else
{
handle->txState = (uint8_t)kUART_TxBusy;
handle->txDataSizeAll = xfer->dataSize;
/* 设置传输参数结构体 */
EDMA_PrepareTransfer(&xferConfig, xfer->data, sizeof(uint8_t), (uint32_t *)UART_GetDataRegisterAddress(base), sizeof(uint8_t), sizeof(uint8_t), xfer->dataSize, kEDMA_MemoryToPeripheral);
/* 保存eDMA的次循环传输计数到UART handle */
handle->nbytes = 1U;
/* 提交传输 */
(void)EDMA_SubmitTransfer(handle->txEdmaHandle, &xferConfig);
EDMA_StartTransfer(handle->txEdmaHandle);
/* 使能UART->C2的TIE位,即使能DMA传输 */
UART_EnableTxDMA(base, true);
status = kStatus_Success;
}
return status;
}
(1)EDMA_PrepareTransfer
:填写edma_transfer_config_t
结构体
void EDMA_PrepareTransfer(edma_transfer_config_t *config,
void *srcAddr,
uint32_t srcWidth,
void *destAddr,
uint32_t destWidth,
uint32_t bytesEachRequest,
uint32_t transferBytes,
edma_transfer_type_t type)
{
int16_t srcOffset = 0, destOffset = 0;
/* 根据传输的四种情况填写偏移量 */
switch (type)
{
case kEDMA_MemoryToMemory:
destOffset = (int16_t)destWidth;
srcOffset = (int16_t)srcWidth;
break;
case kEDMA_MemoryToPeripheral:
destOffset = 0;
srcOffset = (int16_t)srcWidth;
break;
case kEDMA_PeripheralToMemory:
destOffset = (int16_t)destWidth;
srcOffset = 0;
break;
case kEDMA_PeripheralToPeripheral:
destOffset = 0;
srcOffset = 0;
break;
default:
assert(false);
break;
}
EDMA_PrepareTransferConfig(config, srcAddr, srcWidth, srcOffset, destAddr, destWidth, destOffset, bytesEachRequest, transferBytes);
}
-----
void EDMA_PrepareTransferConfig(edma_transfer_config_t *config,
void *srcAddr,
uint32_t srcWidth,
int16_t srcOffset,
void *destAddr,
uint32_t destWidth,
int16_t destOffset,
uint32_t bytesEachRequest,
uint32_t transferBytes)
{
/* Initializes the configure structure to zero. */
(void)memset(config, 0, sizeof(*config));
/* 即UART_GetDataRegisterAddress(base),UART->D数据寄存器地址 */
config->destAddr = (uint32_t)(uint32_t *)destAddr;
/* 用户传输的数据的地址 */
config->srcAddr = (uint32_t)(uint32_t *)srcAddr;
/* 即sizeof(uint8_t)=1 */
config->minorLoopBytes = bytesEachRequest;
/* 主循环计数 = 传输的数据大小/1 */
config->majorLoopCounts = transferBytes / bytesEachRequest;
/* srcWidth=destWidth=sizeof(uint8_t),
* 获取TCD中对应传输长度所对应的宏,宏的值对应TCD寄存器设置的内容,这里为kEDMA_TransferSize1Bytes
*/
config->srcTransferSize = EDMA_TransferWidthMapping(srcWidth);
/* 同上 */
config->destTransferSize = EDMA_TransferWidthMapping(destWidth);
/* 对于MemoryToPeripheral来说,destoffset为0 */
config->destOffset = destOffset;
/* 对于MemoryToPeripheral来说,这里传输数据为按字节传输(minorLoopBytes),故为1 */
config->srcOffset = srcOffset;
}
(2)EDMA_SubmitTransfer
:根据EDMA_PrepareTransfer
中填写的edma_transfer_config_t
结构体,设置DMA通道对应的TCD
status_t EDMA_SubmitTransfer(edma_handle_t *handle, const edma_transfer_config_t *config)
{
edma_tcd_t *tcdRegs = (edma_tcd_t *)(uint32_t)&handle->base->TCD[handle->channel];
/* 对于串口eDMA的例子中没有使用道tcdPool,故在这里省略else部分的代码 */
if (handle->tcdPool == NULL)
{
/* 判断eDMA是否正在执行:(1)TCD->CSR.ACTIVE位=1
* (2)TCD->CSR.ACTIVE位!=1 但 BITER!=CITER,表示主循环还没结束
*/
if (((handle->base->TCD[handle->channel].CSR & DMA_CSR_ACTIVE_MASK) != 0U) ||
(((handle->base->TCD[handle->channel].CITER_ELINKNO & DMA_CITER_ELINKNO_CITER_MASK) !=
(handle->base->TCD[handle->channel].BITER_ELINKNO & DMA_BITER_ELINKNO_BITER_MASK))))
{
return kStatus_EDMA_Busy;
}
else
{
/* 见下面分析 */
EDMA_SetTransferConfig(handle->base, handle->channel, config, NULL);
/* 使能TCD的DREQ,即主循环结束后由硬件清除DMA_ERQ寄存器中对应通道的DMA请求位 */
handle->base->TCD[handle->channel].CSR |= DMA_CSR_DREQ_MASK;
/* 当主循环结束后,产生一个中断 */
handle->base->TCD[handle->channel].CSR |= DMA_CSR_INTMAJOR_MASK;
return kStatus_Success;
}
}else{
/* 需要使用多个TCD,如scatter/gather模式,需要调用EDMA_InstallTCDMemory初始化tcdPool */
...
}
}
-----
void EDMA_SetTransferConfig(DMA_Type *base, uint32_t channel, const edma_transfer_config_t *config, edma_tcd_t *nextTcd)
{
EDMA_TcdSetTransferConfig((edma_tcd_t *)(uint32_t)&base->TCD[channel], config, nextTcd);
}
/* 根据之前的config设置TCD相关寄存器的值,具体值的内容见EDMA_PrepareTransferConfig函数注释 */
void EDMA_TcdSetTransferConfig(edma_tcd_t *tcd, const edma_transfer_config_t *config, edma_tcd_t *nextTcd)
{
/* 源地址SADDR */
tcd->SADDR = config->srcAddr;
/* 目标地址DADDR */
tcd->DADDR = config->destAddr;
/* 设置tcd->ATTR中的源数据传输大小SSIZE[10:8]和目标数据传输大小[2:0] */
tcd->ATTR = DMA_ATTR_SSIZE(config->srcTransferSize) | DMA_ATTR_DSIZE(config->destTransferSize);
/* 设置源地址的偏移,这里为1Byte */
tcd->SOFF = (uint16_t)config->srcOffset;
/* 目标地址的偏移,这里为0 */
tcd->DOFF = (uint16_t)config->destOffset;
/* 次循环计数值,这里为1;DMA->CR.EMLM默认为0,即没有打开在每次次循环结束后对源/目标地址的偏移增加 */
tcd->NBYTES = config->minorLoopBytes;
/* 设置DMA通道的主循环计数;tcd->CITER.ELINK默认为0,即不打开通道链接,故该值可以设置16位 */
tcd->CITER = (uint16_t)config->majorLoopCounts;
/* 同上:在每次主循环结束后,重新赋值给CITER */
tcd->BITER = (uint16_t)config->majorLoopCounts;
/* 使能scatter/gather特性:这里没有使用 */
if (nextTcd != NULL)
{
/* 主循环结束后,加到目标地址的补码 */
tcd->DLAST_SGA = (uint32_t)nextTcd;
/* 打开ESG(scatter/gather)特性,在该模式下需要清除DMA请求位 */
tcd->CSR = (tcd->CSR | (uint16_t)DMA_CSR_ESG_MASK) & ~(uint16_t)DMA_CSR_DREQ_MASK;
}
}
(3)EDMA_StartTransfer
void EDMA_StartTransfer(edma_handle_t *handle)
{
if (handle->tcdPool == NULL)
{
/* 设置DMA->SERQ[3:0]的SERQ位使能对应DMA通道的请求,在SERQ设置的内容会同步到ERQ寄存器中 */
handle->base->SERQ = DMA_SERQ_SERQ(handle->channel);
}
else /* Use the TCD queue. */
{
/* 没有使用到,后续在其他例子中进行分析 */
...
}
}
假设我们将一个uint8_t buffer[256]
数组中的内容调用UART_SendEDMA
请求发送了,硬件还需要一定时间来发送这些数据。在发送完成之前,如果你修改buffer
中的数据,会导致发送的数据出现错误。所以我们需要知道eDMA的数据什么时候发送完毕。
在UART_TransferCreateHandleEDMA
中,有一行EDMA_SetCallback(handle->txEdmaHandle, UART_SendEDMACallback, &s_edmaPrivateHandle[instance]);
设置了一个回调函数UART_SendEDMACallback
,下面来看一下这个函数:
static void UART_SendEDMACallback(edma_handle_t *handle, void *param, bool transferDone, uint32_t tcds)
{
uart_edma_private_handle_t *uartPrivateHandle = (uart_edma_private_handle_t *)param;
/* Avoid the warning for unused variables. */
handle = handle;
tcds = tcds;
if (transferDone)
{
/* 失能DMA传输 */
UART_EnableTxDMA(uartPrivateHandle->base, false);
/* 关闭eDMA传输 */
EDMA_AbortTransfer(handle);
/* 使能串口发送完成中断 */
UART_EnableInterrupts(uartPrivateHandle->base, (uint32_t)kUART_TransmissionCompleteInterruptEnable);
}
}
很明显这个函数就是在eDMA执行完后,失能DMA传输再停止eDMA传输,最后再打开串口的发送完成中断。也就是说在执行到这个函数的时候,eDMA已经完成了数据的处理,接下来就是等待串口这边的响应。但还是眼见为实,我们想知道UART_SendEDMACallback
是在什么时候被调用的,调用流程如下:
DMAx_DriverIRQHandler
EDMA_HandleIRQ
if (handle->tcdPool == NULL)
{
if (handle->callback != NULL)
{
(handle->callback)(handle, handle->userData, transfer_done, 0);
}
}
现在我们就恍然大悟,前面我们设置TCD的CSR
寄存器时,打开了主循环传输结束中断,这样就会进入到上面的处理函数中。
现在我们就是等待串口这边传输结束,那么串口这边又是怎么处理的呢?流程如下:
UART0_RX_TX_DriverIRQHandler
UART0_DriverIRQHandler
s_uartIsr(UART0, s_uartHandle[0]);
前面在UART_TransferCreateHandleEDMA
函数中设置了s_uartIsr
为UART_TransferEdmaHandleIRQ
,现在来看一下这个函数:
void UART_TransferEdmaHandleIRQ(UART_Type *base, void *uartEdmaHandle)
{
uart_edma_handle_t *handle = (uart_edma_handle_t *)uartEdmaHandle;
/* 设置EDMA结构体的状态位idle */
handle->txState = (uint8_t)kUART_TxIdle;
/* 关闭发送完成中断(在每次主循环完成中断中打开) */
UART_DisableInterrupts(base, (uint32_t)kUART_TransmissionCompleteInterruptEnable);
if (handle->callback != NULL)
{
handle->callback(base, handle, kStatus_UART_TxIdle, handle->userData);
}
}
其中callback就是前面UART_TransferCreateHandleEDMA
中传的第三个参数UART_TransferCreateHandleEDMA
,在本例程中,该函数如下:
void UART_UserCallback(UART_Type *base, uart_edma_handle_t *handle, status_t status, void *userData)
{
userData = userData;
if (kStatus_UART_TxIdle == status)
{
txOnGoing = false;
}
/* 接收中断:稍后分析 */
if (kStatus_UART_RxIdle == status)
{
rxOnGoing = false;
}
}
所以我们只要在发送前将txOnGoing
设置为true
,在调用UART_SendEDMA
后阻塞判断这个变量直到其为false
即表示发送完成。当然如果有操作系统的话,就在中断中释放一个信号量即可。
status_t UART_ReceiveEDMA(UART_Type *base, uart_edma_handle_t *handle, uart_transfer_t *xfer)
{
edma_transfer_config_t xferConfig;
status_t status;
/* If previous RX not finished. */
if ((uint8_t)kUART_RxBusy == handle->rxState)
{
status = kStatus_UART_RxBusy;
}
else
{
handle->rxState = (uint8_t)kUART_RxBusy;
handle->rxDataSizeAll = xfer->dataSize;
/* Prepare transfer. */
EDMA_PrepareTransfer(&xferConfig, (uint32_t *)UART_GetDataRegisterAddress(base), sizeof(uint8_t), xfer->data, sizeof(uint8_t), sizeof(uint8_t), xfer->dataSize, kEDMA_PeripheralToMemory);
/* Store the initially configured eDMA minor byte transfer count into the UART handle */
handle->nbytes = 1U;
/* Submit transfer. */
(void)EDMA_SubmitTransfer(handle->rxEdmaHandle, &xferConfig);
EDMA_StartTransfer(handle->rxEdmaHandle);
/* Enable UART RX EDMA. */
UART_EnableRxDMA(base, true);
status = kStatus_Success;
}
return status;
}
可以发现,UART_ReceiveEDMA
的内容和UART_SendEDMA
如出一辙,只不过传输方向变为kEDMA_PeripheralToMemory
,源地址为UART->D
,目标地址为用户设置的缓冲区,最后设置TCD由硬件完成这些操作。
同样的,我们需要知道串口的数据什么时候来,什么时候我们可以去设置的buffer
中取数据。和发送完成中断一样,在UART_TransferCreateHandleEDMA
中,有一行EDMA_SetCallback(handle->rxEdmaHandle, UART_ReceiveEDMACallback, &s_edmaPrivateHandle[instance]);
设置了一个回调函数UART_ReceiveEDMACallback
,下面来看一下这个函数:
static void UART_ReceiveEDMACallback(edma_handle_t *handle, void *param, bool transferDone, uint32_t tcds)
{
uart_edma_private_handle_t *uartPrivateHandle = (uart_edma_private_handle_t *)param;
/* Avoid warning for unused parameters. */
handle = handle;
tcds = tcds;
if (transferDone)
{
/* 关闭eDMA传输 */
UART_TransferAbortReceiveEDMA(uartPrivateHandle->base, uartPrivateHandle->handle);
if (uartPrivateHandle->handle->callback != NULL)
{
/* 即UART_UserCallback */
uartPrivateHandle->handle->callback(uartPrivateHandle->base, uartPrivateHandle->handle, kStatus_UART_RxIdle,
uartPrivateHandle->handle->userData);
}
}
}
同样地,在eDMA传输结束后,会调用该回调函数,最终调用的回调函数也是UART_UserCallback
,当检测到rxOnGoing==false
时,即有数据待处理。该函数的调用时机同样是:DMAx_DriverIRQHandler
->EDMA_HandleIRQ
。
那么什么时候会调用这个接收完成中断呢,代码中也没有开启串口的中断,我们注意到UART_ReceiveEDMA
的xfer
中有设置接收数据的大小,当eDMA接收到指定的这么多数据后,主循环计数为0,则会调用上面的回调函数。
在上一篇文章介绍完TCD寄存器后,本文通过对eDMA串口的分析,大致了解了eDMA中对于TCD的配置,算是入门了。eDMA还有很多特性没有使用,后续将通过实际例程的代码分析其他的特性。