在STM32上使用U8g2图形库并配合DMA发送显示数据(LL库)
izilzty 发布于 阅读:6840
本文将以STM32F103C8T6和GP1287VFD屏为例,说明如何使用DMA来传输U8g2的显示数据。
在U8g2内有一块显示缓冲区,大小和屏幕相同,所有绘制操作均是在此缓冲区上进行的,最后使用SendBuffer();
将缓冲区的内容发送到屏幕。
如果想要使用DMA来发送数据,那肯定不能直接将内部的缓冲区发出去,一是因为所有操作都是在缓冲区进行的,如果在发送过程中数据被修改,会造成显示混乱。二是对于大部分屏幕来说,内部缓冲区的布局和屏幕所需要的布局是不相同的,不能直接发送。
我们可以将内部的缓冲区内容复制出来,经过整理,然后再使用DMA统一发送到屏幕,这样可以同时解决上面所说的两个问题。但是这样也有缺点,那就是要再额外占用一块内存才可以,属于用空间换时间的操作了。
在开始前,请先确定使用基础工程可以点亮屏幕。关于基础工程的创建,请参考前一篇文章:https://www.izilzty.com/?post=14 。确定功能正常后,我们将在此基础工程上进行修改。
最后附有创建好的测试工程,如有需要请自行下载。文章所有步骤都有操作验证一遍,但是不排除遗漏的可能,如果按照文章建立工程无法点亮屏幕,但使用后面所附的示例工程可以点亮的话请留言说明,我会再次检查。
1、修改CubeMX创建工程
-
使用CubeMX打开基础工程的ioc工程文件。
-
在左侧
DMA1
选项卡内点击Add
添加一个通道,DMA Request
选择SPI1_TX
,其他选项保持默认即可。 -
在上方
Project Manager
选项卡的Advanced Setting
内点击所有HAL
字符,并在下拉框内修改为LL
以使用LL库。 -
点击右上方
GENERATE CODE
生成代码。
2、添加DMA传输代码
-
定义一个宏函数,用来交换字节顺序(按屏幕需求)。
/* USER CODE BEGIN PD */ #define SWAP8(a) ((((a)&0x80) >> 7) | (((a)&0x40) >> 5) | (((a)&0x20) >> 3) | (((a)&0x10) >> 1) | (((a)&0x08) << 1) | (((a)&0x04) << 3) | (((a)&0x02) << 5) | (((a)&0x01) << 7)) /* USER CODE END PD */
-
定义一个全局数组,里面会存放从U8g2复制出来的DMA显示数据。
/* USER CODE BEGIN PV */ static uint8_t dma_memory[1792]; /* USER CODE END PV */
-
在main函数前的自定义代码区域增加DMA函数:
/* USER CODE BEGIN 0 */ void u8g2_WaitDMA(u8g2_t *u8g2) { /* 必须等待DMA和SPI全部传输和完成才可以设置CS引脚或处理DMA内存数据,否则会显示错乱 */ if (LL_DMA_IsEnabledChannel(DMA1, LL_DMA_CHANNEL_3) == SET) { while (LL_DMA_IsActiveFlag_TC3(DMA1) == RESET) { __NOP(); } } while (LL_SPI_IsActiveFlag_BSY(SPI1) == SET) { __NOP(); } u8x8_byte_EndTransfer(&u8g2->u8x8); /* 使用U8g2拉高CS引脚 */ LL_SPI_DisableDMAReq_TX(SPI1); LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_3); LL_DMA_ClearFlag_TC3(DMA1); } void u8g2_SendBufferDMA(u8g2_t *u8g2) { uint16_t i, j; uint8_t *dest; uint8_t *src; u8g2_WaitDMA(u8g2); dest = dma_memory; /* DMA显存,里面存储整理好的显示数据,可以直接发送给屏幕 */ /* 整理并复制显示数据 */ for (i = 0; i < 256; i += 1) /* 屏幕共256列,循环256次 */ { src = u8g2->tile_buf_ptr + i; /* 移动u8g2显存指针到下一列 */ for (j = 0; j < 7; j++) /* u8g2显存一列56像素,分为7个Tile,需要循环7次 */ { *dest = SWAP8(*(src + (j * 256))); /* 复制一列 */ dest += 1; } } u8x8_cad_StartTransfer(&u8g2->u8x8); /* 使用U8g2拉低CS引脚 */ /* 代替U8g2向屏幕发送数据,参数和顺序和U8g2内相同 */ u8x8_cad_SendCmd(&u8g2->u8x8, SWAP8(0x0F0)); /* 开始写入显示数据,直到下次拉高片选前一直有效 */ u8x8_cad_SendArg(&u8g2->u8x8, SWAP8(0)); /* 起始X位置 */ u8x8_cad_SendArg(&u8g2->u8x8, SWAP8(0 + 4)); /* 起始Y位置,要加上屏幕本身的偏移 */ u8x8_cad_SendArg(&u8g2->u8x8, SWAP8(0x037)); /* Y方向每56像素自动折返,实际只显示50像素 */ LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_3, sizeof(dma_memory)); LL_DMA_ConfigAddresses(DMA1, LL_DMA_CHANNEL_3, (uint32_t)&dma_memory, LL_SPI_DMA_GetRegAddr(SPI1), LL_DMA_DIRECTION_MEMORY_TO_PERIPH); LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_3); LL_SPI_EnableDMAReq_TX(SPI1); } /* USER CODE END 0 */
3、使用DMA传输在屏幕上显示“HelloWorld”
- 在while函数内的自定义代码区域增加绘制代码:
u8g2_ClearBuffer(&u8g2); u8g2_SetFont(&u8g2, u8g2_font_ncenB08_tr); u8g2_DrawStr(&u8g2, 0, 10, "Hello World!"); u8g2_SendBufferDMA(&u8g2); LL_mDelay(1000); /* USER CODE END WHILE */
4、U8g2显存说明
-
U8g2显存每个块(Tile)为8x8像素,扫描方向为从上向下扫描,当扫描完一行8x8的块后从第二行起始位置继续扫描,以256x50分辨率的屏幕为例,数据结构如下:
方块内的数字为Tile的排列顺序 |8px| x: 256 +------------------------+ --- | 00 | 01 | 02 | .. | 31 | 8px y: 56 |------------------------| --- | 32 | 33 | 34 | .. | 63 | |------------------------| | ... |
需要注意的是,不管是什么型号的屏幕,U8g2显存内的数据结构始终是相同的。
-
整理后适合GP1287的DMA显示数据数据结构如下:
方块内的数字为扫描顺序 | 1px | x: 256 +-----------------------+ --- | | | | | y: 56 | 000 | 001 | ... | 255 | 56px | | | | | +-----------------------+ --- dma_memory [0] [1] .. [7] y0~7 y8~15 .. y47~55
整理后的数据就适合GP1287使用了,可以直接一次发完,中间无需停顿。
5、其他注意事项
- 在调用
u8g2_SendBufferDMA();
后如果需要调用其他非DMA功能,例如u8g2_SetContrast();
,则需要先调用u8g2_WaitDMA();
等待当前DMA传输完成,否则数据会混乱。而u8g2_SendBufferDMA();
内部会等待DMA传输完成,所以可以连续调用。
END、示例工程下载
-
STM32F103C8T6_U8G2_DMA_BENC
为跑分测试,STM32F103C8T6_U8G2_DMA_DEMO
为文章中的工程