BUG分享|DMA发送数据时,会被莫名打断,或者发送乱码。

发布时间 2023-12-29 14:31:26作者: 起振电容

引言

在驱动ST7789屏幕时,使用了SPI+DMA进行图像刷新。在执行清屏操作时,使用配置DMA内存到外设,内存地址不变,发送的内存是一个16位的RGB565像素值变量,可以指定清屏填充的颜色。
单片机:STM32F411CEU6
库函数:标准库

现象

清屏代码如下:

/* 清屏函数 输入参数填充矩形的左上角坐标和右下角坐标以及颜色(RGB565)*/
void vST7789V3_Fill(u8 xsta,u8 ysta,u8 xend,u8 yend,u16 color)
{
	u16 num;
	num=(xend-xsta)*(yend-ysta);
	vST7789V3_Address_Set(xsta,ysta,xend-1,yend-1);// 设置显示范围
	/* 配置DMA */
	SPI_DMA_Fill_Config((u32)&color,num);

	/* 启动DMA */
	SPI_DMA_Enable();
}

/* DMA配置函数*/
/*
	SPI1_DMA1传输参数配置
	用于色块填充
	内存->外设 内存地址不变 为16位RGB565色度值
输入参数cmar:色块值地址
输入参数cmar:数据长度(填充的像素数量)
*/
void SPI_DMA_Fill_Config(u32 cmar,u16 cndtr)
{
	DMA_InitTypeDef DMA_InitStructure;

	SPI_DataSizeConfig(SPI1, SPI_DataSize_16b);		//设置SPI发送数据宽度16位 注:传输完成需要改成8位宽
	DMA1_MEM_LEN=cndtr;

	// DMA配置
	/*SPI1发送功能的DMA在DMA2通道2数据流2中,在STM32F411参考手册170页*/
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2,ENABLE);//DMA2时钟使能

	DMA_DeInit(DMA2_Stream2);
	while (DMA_GetCmdStatus(DMA2_Stream2) != DISABLE){}//等待 DMA可配置

	/* 配置 DMA Stream */
	DMA_InitStructure.DMA_Channel = DMA_Channel_2;  			//通道选择
	DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&SPI1->DR;  //DMA外设SPI基地址
	DMA_InitStructure.DMA_Memory0BaseAddr = cmar;				//DMA 存储器0地址
	DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;		//存储器到外设模式
	DMA_InitStructure.DMA_BufferSize = cndtr;					//数据传输量
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设非增量模式
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Disable;		//存储器非增量模式
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;	//外设数据长度:16位
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; 		//存储器数据长度:16位
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;								// 使用普通模式
	DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;						//中等优先级
	DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;						//FIFO模式禁止
	DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;				//FIFO 阈值
	DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single; 				//存储器突发单次传输
	DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; 		//外设突发单次传输
	DMA_Init(DMA2_Stream2, &DMA_InitStructure);									//初始化DMA Stream
}

使用时,直接调用如vST7789V3_Fill(0,0,240,135,0x0000);开启DMA传输,将屏幕填充黑色。DMA传输完成大概10ms。

/* 代码段1 */
while(1)
{
	vST7789V3_Fill(0,0,240,135,0xffff);
	while(1);
}

代码段1运行,一切正常,屏幕全白。当在调用填充函数之后,添加其他功能,如串口打印时,屏幕就乱了。

/* 代码段2 */
while(1)
{
	vST7789V3_Fill(0,0,240,135,0xffff);
	printf("hello world");
	while(1);
}

代码段2运行效果:
image
可以看到,在显示了3个像素点之后,数据就开始不对了。填充整屏时间大概10ms,添加延时函数试一下。

/* 代码段3 */
while(1)
{
	vST7789V3_Fill(0,0,240,135,0xffff);
	delay_ms(5);
	printf("hello world");
	while(1);
}

代码段3运行效果:
image
加入了延时5ms之后,数据在延时时间内是正常的,延时结束,调用串口打印时,出问题了。

分析

正常来说,DMA传输时,是不占用CPU的,CPU可以干其他事情,互不干扰。但是目前现象是,串口发送造成了DMA发送错误。后续更换了其他函数,都会影响屏幕的显示。必须使用延时函数,在DMA发送期间,CPU只去计数,其他事情都不能干(试过控制LED闪烁,也是可以的)。
考虑半天,对比两个显示结果,发现端倪。LCD屏幕重新上电后会显示之前的画面,如果有新的显示数据写入,会覆盖在上一个画面之上。代码段2的结果屏幕大部分是蓝色,而运行代码段3时,白色画面覆盖了半个屏幕,如果在中途中断了,后续没有覆盖的部分应该也是蓝色,可是这里是绿色。说明DMA实际上是没有被打断的,后续的绿色也是DMA写入的数据。
那么问题到这里明朗了些,检查DMA发送的内存数据。这里由于是色块填充函数,所以只需要输入颜色的数据,DMA自动的只发送一个固定的16位数据。检查这个16位数据,发现了问题所在。由于这个函数是从SPI循环发送改成了DMA发送,所以没有注意到这个bug。

/* 清屏函数 输入参数填充矩形的左上角坐标和右下角坐标以及颜色(RGB565)*/
void vST7789V3_Fill(u8 xsta,u8 ysta,u8 xend,u8 yend,u16 color)
{
	u16 num;
	num=(xend-xsta)*(yend-ysta);
	vST7789V3_Address_Set(xsta,ysta,xend-1,yend-1);// 设置显示范围
	/* 配置DMA */
	SPI_DMA_Fill_Config((u32)&color,num);

	/* 启动DMA */
	SPI_DMA_Enable();
}

问题出在这个函数里,输出参数中color是16位颜色数据,而SPI_DMA_Fill_Config(u32 cmar,u16 cndtr)中,cmar是DMA搬运数据的内存地址。这里将输出参数color直接取地址传入DMA配置参数,当启动了DMA之后,这个函数就消灭了,随之申请在栈空间的变量color也会被自动释放掉。这时,&color这个指针就是野指针了,这块内存如果被后续的函数申请,会被改变内容。如果在DMA传输的过程中被改变了,当然DMA发送的内容也会改变。这里的致命错误就是将栈空间的地址给了DMA。

解决方案

修改填充函数,将16位的色彩参数改成色彩的地址,在外部定义全局色彩变量,这里的变量是存储在静态存储区,不会消失。每次传入色彩变量的地址即可。

const u8 LCD_black = 0x0000;
const u8 LCD_white = 0xffff;
void vST7789V3_Fill(u8 xsta,u8 ysta,u8 xend,u8 yend,u8* colorAddr)
{
	u16 num;
	num=(xend-xsta)*(yend-ysta);
	vST7789V3_Address_Set(xsta,ysta,xend-1,yend-1);//设置显示范围
	SPI_DMA_Fill_Config((u32)colorAddr,num);
	SPI_DMA_Enable();
}

但是,上述方法只适合于固定预设好的色彩值,且如果色彩太多,需要定义的全局变量也会增多。我们的问题就是函数的输入参数会在函数结束时消失,那我们不让他消失就可以了,另一种方案如下:

//}
void vST7789V3_Fill(u8 xsta,u8 ysta,u8 xend,u8 yend,u16 color)
{
	u16 num;
	/* 申请静态局部变量 函数结束时不会被释放 */
	static u16 save_color;
	save_color = color;
	num=(xend-xsta)*(yend-ysta);
	vST7789V3_Address_Set(xsta,ysta,xend-1,yend-1);//设置显示范围
	SPI_DMA_Fill_Config((u32)&save_color,num);
	SPI_DMA_Enable();
}

这里申请了一个静态局部变量,普通的局部变量是存储在栈空间的,函数结束会释放。添加了static的修饰,变量将存储在静态存储区,即使函数结束,变量以及存储的内容依然存在。这样就可以保证DMA在传输时,该变量不会改变。