单片机用DMA实现不阻塞发送的printf函数(DMA,乒乓缓冲)
前言
在单片机里面通常为了调试方便,会把printf函数的输出重定向到串口中,具体的方法就是改写fputc函数或者_write函数(不同单片机有所不同),使其用串口来发送字符。而在我使用的单片机(沁恒微ch32v303)中,实现如下:
__attribute__((used)) int _write(int fd, char *buf, int size)
{
int i;
for(i = 0; i < size; i++)
{
while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
USART_SendData(USART1, *buf++);
}
return size;
}
这种实现方法虽然功能没问题,但是存在一些缺点:
- 发送是阻塞的。会占用cpu,并且波特率越低,占用时间越长。
那么有没有方法解决这个问题呢?
众所周知,DMA的传输是不占用CPU的,但是他有个特点就是:
- 每次DMA传输都是要指定数据长度。
而我们printf函数每次发送的都是一个变长的数据。
那么只要我们实现了DMA的变长数据发送,我们就能够使用DMA来实现无阻塞的printf函数
接下来一步一步实现这个功能。
功能构思
- 首先我们需要定义一个DMA发送函数(printfDMA_Send),这个函数的主要功能就是:启动DMA传送,把指定的发送buff的指定长度数据传输到串口发送数据寄存器。
- 重新实现一个自己的printf函数,该函数的主要功能就是往上述的发送buff中填入数据。
最终使用的效果大致如下:
//其他代码
printfDMA("test dma printf\r\n"); //往发送buff填入数据
//其他代码
printfDMA("test num printf:%d", 123) //网发送buff填入数据
//其他代码
/* 放到业务代码的最后 */
printfDMA_Send() //把填入发送buff的数据一次性通过DMA发送到串口数据寄存器
具体实现
DMA外设初始化配置
void ch32v30x_module_InitDMA(ch32v30x_module *self)
{
#ifndef HARDWARE_DISABLE
DMA_InitTypeDef DMA_InitStructure = {0};
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
DMA_DeInit(DMA1_Channel2);
DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)(&USART3->DATAR);
DMA_InitStructure.DMA_MemoryBaseAddr = (u32)(DebugBuff[0]);
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStructure.DMA_BufferSize = UART_TX_BUFF_SIZE;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel2, &DMA_InitStructure);
DMA_Cmd(DMA1_Channel2, ENABLE);
USART_DMACmd(USART3, USART_DMAReq_Tx, ENABLE);
#endif
}
定义用于DMA传输的发送数据buff
#define UART_TX_BUFF_SIZE 80
char DebugBuff[2][UART_TX_BUFF_SIZE] = {0};
char DebugIndex = 0;
char DebugTmpBuff[UART_TX_BUFF_SIZE] = {0};
UART_TX_BUFF_SIZE:这里定义了发送数据buff的长度,这个决定了一次每次DMA传输前一共能填入的字符数量,数据量越大该值越大。又或者使用小一点的数据长度,但调用DBprintfDMA_Send()的次数频繁一点。
char DebugBuff[2][UART_TX_BUFF_SIZE]:定义了一个二维数组用作乒乓缓冲,“DebugIndex”是指示当前使用的缓冲,每次发送完就切换另一个缓冲buff。使用乒乓缓冲的目的是:当DMA传输的过程中,会不断读取数据缓冲区,此时如果我们有新的数据来了如果仍填在这个缓冲区,那就会覆盖掉还未发送出去的旧数据。因此所谓“乒乓缓冲”的意义就是:一个缓冲区作为发送缓冲区,另一个缓冲区作为新数据输入的缓冲器。当一次发送完成后,两者交换,从而是的发送和新数据输入不会冲突
char DebugTmpBuff[UART_TX_BUFF_SIZE]:用于支持格式化打印的临时buff
printfDMA函数实现
#define printfDMA(...) \
sprintf(DebugTmpBuff, __VA_ARGS__); \
if (strlen(DebugBuff[DebugIndex]) + strlen(DebugTmpBuff) <= UART_TX_BUFF_SIZE - 1) \
{ \
strcat(DebugBuff[DebugIndex], DebugTmpBuff); \
}
主要功能:把格式化字符填入到当前DebugIndex指示的缓冲区,并做数据长度检查
加入一个临时缓冲区DebugTmpBuff的目的是为了使用sprintf做格式化数据转换
printfDMA_Send函数实现
#define printfDMA_Send() \
DMA_Cmd(DMA1_Channel2, DISABLE); /* 关闭DMA通道 */\
DMA1_Channel2->MADDR = (u32)DebugBuff[DebugIndex]; /* DMA源地址设置 */\
DMA1_Channel2->CNTR = strlen(DebugBuff[DebugIndex]); /* DMA发送数据长度设置 */\
DMA_Cmd(DMA1_Channel2, ENABLE); /* 启动DMA通道 */\
DebugIndex = (DebugIndex + 1) & 0x1; /* 对2取余,切换缓冲区 */\
DebugBuff[DebugIndex][0] = '\0'; /* 清空新缓冲区。字符串首字符设为0,strlen函数就会当成长度为0的字符串 */
主要功能:把当前DebugIndex指示的乒乓缓冲区中的数据通过DMA发送出去。
效果
在M4内核带FPU的144M的mcu上做测试(stm32F4、沁恒微ch32v303等)做测试,printfDMA_Send()占用3us左右。printfDMA()函数占用的时间主要取决于要打印的字符长度(不在取决于波特率)。整体时间占用比文中开头的阻塞式打印快了很多,适合在任务周期比较短,但是要求打印大量信息的场合中使用。
待改进
1. printfDMA()中的sprintf()函数执行效率低。
如果在不需要格式化打印,只需打印纯字符串的情况下,去掉sprintf效率会提高很多。目前仍未想到有其他更好的方法可以替代sprintf()的功能。
2. 占用空间比较大
需要提前开辟一段buff来支持DMA传输,对于RAM资源比较紧张的场合并不适用