本帖最后由 [鑫森淼焱垚] 于 2024-5-30 11:56 编辑
#申请原创#
背景
DMA 介绍
SPI1 TX DMA 配置
何时使用 DMA
代码实现
配置 DMA 和 SPI
启动传输等待结束
恢复 SPI1 配置
结果1--失败
Debug
1. 对比外设寄存器
2. 经过上面的尝试,怀疑 SPI1 没有把数据发出去。
3. 查看SPI寄存器发现 OVR** 状态置位了
破案
结果2--成功
对比测试
后续
|
背景
上一篇文章 LVGL 提速,仅仅是在刷屏时逐个打点替换为刷数组,速度肉眼可见的提升了,但是刷屏速度还需要再提一提 -- SPI + DMA。
DMA 介绍
DMA 在无须 CPU 干预的情况下,可实现外设与存储器或存储器与存储器之间数据的高速传输,从而节省 CPU 资源来做其他操作。
APM32F411 一共有两个 DMA 控制器,共16个数据流。每个数据流对应8个通道,但每一个数据流同一时刻只能使用一个通道。每个数据流可以设置优先级,仲裁器可根据数据流的优先级协调各个数据流对应的DMA请求的优先级。
若外设或存储器需要使用 DMA 传输数据,就必须先发送 DMA 请求,等待 DMA 同意之后才开始数据传输。
两个 DMA 一共有16个数据流,每个数据流都连接着不同的外设通道,每个数据流都有5个事件标志(DMA半传输、DMA传输完成、DMA传输出错、DMA FIFO 错误、直接模式错误),5个事件标志的逻辑或称为一个单独的中断请求,且都支持软件出发。
多个外设请求同一个数据流是,需要配置对应寄存器,开启或关闭每个外设的请求,以保证一个数据流仅能开启一个外设请求。
SPI1 TX DMA 配置
这里只关心 APM32F411V SPI1 TX 方向。 SPI1_TX 这里选择 DMA2 数据流3 通道3,参考 DMA2 的请求映射表:
何时使用 DMA
参见LVGL刷屏函数 disp_flush() --> LCD_DrawBitmap(),后面这个函数可以分解为两步:
设置 GRAM 区域,SPI 发送的命令和数据都是8位宽度;
发送像素数据,连续发送的像素数据都是16位宽度。
仅当有大量数据需要发送到屏幕上时才需要使用 DMA + SPI TX。所以 LCD_DrawBitmap() 函数就可以拆解成两个动作:
- 对应函数 LCD_Address_Set(),设置 GRAM 坐标,SPI1 数据位宽都是8位;
- 配置 DMA + SPI1 TX,发送大量的像素数据,注意需要修改 SPI1 数据位宽为16位;
代码实现
下面讲讲 LCD_WR_DATA16_Array() 的实现,即 DMA + SPI TX 配置,启动 DMA 等待传输完成。
配置 DMA 和 SPI
- 函数 LCD_WR_DATA16_Array() 中,先配置 SPI1,然后配置 DMA 。
- 改变 SPI1 数据位宽为16位。注意必须是 SPIEN = 0 时才能改变数据帧长度,从 SPI_CTRL1 寄存器中可以找到相关信息(此外 SPI1 配置为 MSB 发送顺序,这里无需再次配置);
- 配置 DMA,首先使能 DMA2 时钟,选择通道3,设置外设地址为 SPI1->DATA 寄存器地址,源地址为内存地址,即像素数组起始地址;
- 配置外设和源的数据位宽为16位,和上面的对应;确保配置的是 DMA2_STREAM3;
启动传输等待结束
还是在 LCD_WR_DATA16_Array() 函数中,
- 启动传输,即使能 SPI1 TX 的 DMA 请求;
- 等待 SPI1 传输完成;【Debug时通过 Keil MDK 查看 DMA2 STREAM3 寄存器,NDATA 寄存器确实动了,从0x960变成了0;而且 SPI1 状态寄存器也变了】
- 清除 DMA 和 SPI 标志位;
恢复 SPI1 配置
还是在 LCD_WR_DATA16_Array() 函数中:
- 关闭 DMA2 对应的数据流;
- 关闭 DMA2 时钟;
- 关闭 SPI 的 TX DMA 请求,并恢复数据位为8位;
结果1--失败
【屏幕黑屏,一个像素点也没有点亮。】
Debug
为什么一直黑屏呢?进行了如下几个尝试试图定位问题。
1. 对比外设寄存器
通过 Keil MDK 看了 DMA2 和 SPI1 寄存器,该配置的寄存器都配置了,当 SPI1 使能 TX DMA 请求时,可以看到 DMA NDATA 寄存器也动了,从 0x960 变成了0,表示 DMA 把数据从内存搬运到 SPI1 完成了。
2. 经过上面的尝试,怀疑 SPI1 没有把数据发出去。
是不是 SPI 速率和 DMA 速率不匹配?尝试修改 SPI 时钟分频系数,从2分频到256分频都试过,还是黑屏。
3. 查看SPI寄存器发现 OVR** 状态置位了
说明只发送没有接收。为了解决这个问题,新增加了一个 DMA2 STREAM2 CHAN3,专门接收 SPI1 RX 数据。
在调试状态下,有以下两个发现:
DMA RX 配置 NDATA = 0xFFFF,当 SPI 传输完成,这个数据变少了,变成了 0xF6A0。实际接收的数据个数 = 0xFFFF - 0xF6A0 + 1 = 0x960。和发送的数据个数对应上了。
按照上面的配置,接收缓冲区固定为一个 u16 dummy_rx_data,初始值为0,最后变成了 0xFFFF,说明 DMA RX 搬运成功了。
有了以上两点,为什么屏幕还没有点亮呢?
我对比了 SDK 中所有 和 DMA 相关的示例,只有 ADC_DMA, DMA_ADC, DMA_FMCToRAM,TMR_TMR8DMA,USART_TwoBoardsDMA,发现示例中 DMA 用法并没有特别之处,甚至连 DMA 中断都没有用到,都是轮询等待 DMA 传输结束。
破案
零点还在写文档,入睡得到1点半。
早上睁开眼,灵-光一闪,是不是DMA+SPI 传输启动前少了一个步骤,LCD_CS 信号没有拉低,传输结束拉高??? 带着这个疑问,打开电脑,修改代码,最终版如下:
DMA + SPI 函数 LCD_WR_DATA16_Array() 实现
结果2--成功
屏幕点亮了,LVGL刷图成功,内流满面。被自己气哭了。要是早点上逻辑分析仪测一下管脚信号,当天就能解决问题。
对比测试
明显看出 DMA 方式还是有优势的,CPU占用率低一些。
方式
| 帧率
| CPU占用率
| 轮询发数组
| 33 FPS 偶尔掉一下
| 20%~64%
| DMA
| 33 FPS 稳定
| 11%~30%
| 视频地址:
https://www.bilibili.com/video/BV1naTNeDER7/?vd_source=8f2bbf56b70c541bec2ea0b9f102ebee
后续
当前代码还是可以优化的。上了 RTOS,那么不应该死等 DMA 传输完成,而应该在 DMA 传输完成中断中告知 LVGL 刷新完成,这样还能降低一些 CPU 占用率,提高效率。
|