引言
在驱动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
运行效果:
可以看到,在显示了3个像素点之后,数据就开始不对了。填充整屏时间大概10ms,添加延时函数试一下。
/* 代码段3 */
while(1)
{
vST7789V3_Fill(0,0,240,135,0xffff);
delay_ms(5);
printf("hello world");
while(1);
}
代码段3
运行效果:
加入了延时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在传输时,该变量不会改变。