一、使用CPU搬运数据到显存
" H2 h- i8 K2 I& f- W在上一篇文章中讲述了如何配置 LTDC 驱动 RGB 屏幕:
, c: F# _: l7 L- m( I: [* }) {, t( b4 X6 f' j( \; o* U2 D' y- ? \
* w" {4 Y% w* V/ }0 H) WSTM32CubeMX | 41-使用LTDC驱动TFT-LCD屏幕(RGB屏)。
+ [+ H) F, ?/ @, I4 |
( D/ P9 r1 B9 \$ T e8 P! f. I" e
本节中我们接着上一节的实验,讲述如何使用 DMA2D 实现打点、画线、填充等函数,只需要单层全屏即可,修改LTDC层配置如下:
X$ Z. d6 r7 Y* B1 T
: X! v$ ]$ q0 r* c. _7 k0 Q& W% r9 [$ u# ?' Z
! Y, C( m4 [! I& {. E& n5 T6 G1. 编写lcd驱动头文件0 C5 h2 R( i m, ~$ s+ C
创建lcd_rgb_ltdc_drv.h文件,存放关于操作LCD屏幕的一些宏定义配置和函数定义:3 H) @+ b* k, {1 H: J* O- t
- <font face="微软雅黑" size="3">#ifndef _LCD_RGB_LTDC_DRV_H_% A: P5 O, D6 z! t7 Q- W
- #define _LCD_RGB_LTDC_DRV_H_
: f; H; X( a8 z* H. l, g
; \' l' c& P+ d% m- #include "ltdc.h"% _. \( s$ Q; \: e/ \
- 1 n; a" o, r" A1 K
- /**8 f% \6 z; i6 r, k& M
- * @brief Windows size on lcd.
- s. C: s0 V1 ^6 O; I7 h t1 D$ Q - */
: ~. F' Y4 V1 `! E1 h& {- I' k - #define LCD_WIDTH 1024
0 E0 x6 ^( M: O; v# u - #define LCD_HEIGHT 600
0 B' c7 N$ }4 {
, E7 H& W! K! A1 u7 ]% G! s. L ?- /**& G9 h! h+ H! j: y7 A* `4 W' j6 k
- * @brief Backlight control pin of lcd.
4 Q* ?/ ]& w, M) }" i2 h - */
) }' C* y- D2 h, H" z: v! S - #define LCD_BL_GPIO_PORT GPIOB M9 f$ N9 E$ d3 f+ a* `
- #define LCD_BL_GPIO_PIN GPIO_PIN_5
' d" e- b3 r: ~' a p! g* `
7 |6 V# P3 a( `4 E# i- /**
" n. q2 H$ ]" j5 c6 @) J - * @brief start address of lcd framebuffer.( g7 r! m: n* }
- */7 S0 |7 _& `, d0 J+ P& n2 Q; c! J
- #define LCD_FRAME_BUFFER 0xc0000000
! f+ \$ Y! }6 E! A4 j$ k* ?* ~
% k4 b& ~5 c4 A- I* o6 k- /**
$ b3 d" h& E/ H9 L* A) c - * @brief color1 `$ e7 X6 R. ], k& u8 z; b
- * @note rgb565 " l$ M( Y1 S: R) w1 l
- */6 K# F2 T; s& @ t5 _
- #define BLACK 0x0000
% I: k9 t( `6 m7 q( M) D! z) h - #define BLUE 0x001F! C% R6 p( T/ ^% J4 Y' R
- #define GREEN 0x07E0
8 s2 Z, E' R$ D4 @' R! N - #define GBLUE 0X07FF9 {$ }7 j/ T' P! v0 N4 Z+ y q
- #define GRAY 0X84304 k: Q- h( d( C u. k
- #define BROWN 0XBC405 d! H: k! j! D4 ?& ^' ]
- #define RED 0xF800+ m$ P. A* E" E
- #define PINK 0XF81F
: I5 L7 Z9 N! G( H# U2 _" h - #define BRRED 0XFC07
$ H9 }; G. `4 f1 _6 z5 r1 \. E - #define YELLOW 0xFFE0' o7 Q8 z6 R& ~
- #define WHITE 0xFFFF
! i r- C* V r( v1 e8 }% q1 _4 ] - 3 S K1 D7 t$ t, H/ Y$ B5 J$ c
- /**, H a; _; N: i: `4 ?
- * @brief Control the lcd backlight./ B" O" C2 g, Z4 h1 Y
- * @param[in] brightness the value of lcd backlight.( ?: ?9 ]! B1 ^9 c: p0 W
- * @return None
8 b3 w3 x* {' J* `& J - */6 O8 h. d9 o: ^: {: j) q/ b2 u! X
- void lcd_backlight_control(uint8_t bightness);
, q# o% M/ k/ x3 S. c* f$ E
: f1 A- |# n" E4 p! F e- /*** P/ Y1 J/ ?9 M4 ~
- * @brief LCD initialization.
0 j0 q1 I; x- m* q; v - * @param None
/ E/ k4 y) g$ ]- W - * @return None: q. Z. L1 K5 Y# ~
- *// f1 n/ F0 L% |' B% k
- void lcd_init(void);6 ]: w0 u! s0 E$ P: `9 R
8 Y2 K! i( \6 B- /**( o! w7 W0 h j# U2 h
- * @brief Clear lcd.
* h$ H& p8 X/ X4 Q, C! h! j - * @param[in] color rgb565.
% M8 `6 x% b9 `0 y3 C - * @return None f% z& [2 \: S6 U# c" b0 D" d
- */
3 f8 C/ e3 W2 ~: V0 K. f" n) `2 Y' E - void lcd_clear(uint16_t color);
; F0 k; \$ s. U/ A+ e, A T: J
1 f/ p4 c* f9 ^1 y- #endif /* _LCD_RGB_LTDC_DRV_H_ */</font>
复制代码 ) B. _& W' i' ^/ s
) Z# g0 ~. Q; O A6 n5 i
2. lcd驱动实现
! O! \* f, W0 ?0 c' ]. O创建lcd_rgb_ltdc_drv.c文件,存放关于操作LCD屏幕的函数实现。% ? R& \1 Y0 j( D% v1 v0 u
$ i& N9 h `* Z. E+ L9 o" }7 i$ f5 M. K$ O# a
首先是背光控制实现,应该使用pwm实现背光调节,本文中为了方便直接使用GPIO控制:
1 v+ V. f7 X, v- @: \$ e/ B- <font face="微软雅黑" size="3">void lcd_backlight_control(uint8_t bightness)
, j/ R1 M/ g/ q - {
5 g+ n" L M; \; g4 m" b( d - // todo: use pwm to control backlight
: Q( ?$ u8 Q1 ]
3 E: f+ E2 P2 O% h3 \: E9 ?- if (bightness) { . C$ l& d* U2 i0 ]2 S
- // turn on the backlight
/ ?# O( |, O! w7 M+ X: v: ]/ W - HAL_GPIO_WritePin(LCD_BL_GPIO_PORT, LCD_BL_GPIO_PIN, GPIO_PIN_SET);
, K# Q1 t5 }0 v3 \! G - } else { 3 C, J4 f: R+ i) `" Q* m. E
- // turn off the backlight
7 X, g6 G+ ^4 f e. p3 P$ R6 s - HAL_GPIO_WritePin(LCD_BL_GPIO_PORT, LCD_BL_GPIO_PIN, GPIO_PIN_RESET);' ]# ?" i1 D; U) f8 D! b9 e
- }8 a( e; D# H6 c7 q- M" E5 ]( h
- }</font>
复制代码 接着实现lcd清屏函数,使用CPU(循环)搬运数据到显存中:$ F* i1 J e4 v: P
- <font face="微软雅黑" size="3">void lcd_clear(uint16_t color)
' o b. w6 }* o# A. T% C, f1 ^$ t) V - {5 V( Z9 B, }* z/ r
- uint16_t *ptr = (uint16_t*)LCD_FRAME_BUFFER;0 |7 T+ v' B& }' G
- uint32_t i = 0;6 ]) p \ [9 e1 M6 I- h6 o
# J$ U7 W! e4 N4 w* w- while (i++ < LCD_WIDTH*LCD_HEIGHT) {
' Z8 y! ]2 t$ ?4 r& _ o$ @ - *(ptr+i) = color;
7 h9 z" u3 ?, Y8 h, \* Y - }
2 w; c# y7 f: O m4 P3 t) z. ^ - }</font>
复制代码 最后实现lcd初始化函数,先刷显存,然后开背光,防止设备上电时屏幕闪烁:7 |+ Q: b% k0 }6 E
- <font face="微软雅黑" size="3">void lcd_init()
' k* K" K, Z0 ]9 @( Y+ G - {5 |5 ^' E! y E, X4 {
- lcd_clear(BLACK);5 d4 B! [# ~, S4 H
- lcd_backlight_control(255);
4 {; ]; h$ h5 Y6 o8 I- [* A7 P - }</font>
复制代码 8 ]# T. A) L1 X4 M/ S$ L7 _
2 P2 K- K4 Q- @+ Y+ Z
3. lcd单次清屏时间测试
. b9 F- ~5 K6 W3 p% E在main.c中包含驱动头文件:- [5 E$ J# M h* V
- <font face="微软雅黑" size="3">#include "lcd_rgb_ltdc_drv.h"</font>
复制代码 在main函数中的sdram初始化函数之后,添加lcd初始化函数,并使用HAL库自带的systick时间戳测量一次清屏的时间:& u5 u$ p# r5 P9 L& M r
- <font face="微软雅黑" size="3">/* USER CODE BEGIN 2 */5 \0 X# W; e: y! ^' f! Z3 q4 O, F5 R
- printf("sdram test by mculover666\r\n");/ n! [$ E3 u! Q* N" X
- SDRAM_Init();" R/ m1 k# ? C
- printf("sdram init success\r\n");
: S: i A, `# G! g - 3 A, @7 P- G4 e, n' I
- lcd_init();5 g9 O9 n& u/ q( H$ ~. `) ~9 z$ P
- 0 d9 U/ h- T0 W( Z+ N
- start_time = HAL_GetTick();
& j; h# G5 J9 ]5 d j$ n - lcd_clear(PINK);2 m) y) F0 J9 w! z. N' C
- end_time = HAL_GetTick();
% U" Y& b8 I6 n( B0 ^ - 6 N1 v' c% l2 D
- printf("lcd clear spend time:%ld ms\r\n", end_time - start_time);8 T. p' R5 E/ W! x6 l8 T0 c
- /* USER CODE END 2 */</font>
复制代码 编译、运行,在串口助手可以看到使用CPU搬运数据到显存中,在-Og优化等级下单次清屏需要 155 ms左右,在-O0优化等级下单次清屏需要321ms左右:' a4 R& ]) |9 e2 \) Z5 x. y9 n
! g' \' V/ g% c1 a! A6 o7 O8 z% b& {
6 ^) V- L: F( U1 c! X$ F( ^3 I U; t, z' s
二、使用DMA2D加速显存数据搬运8 q# V, c k! m
1. DMA2D
& X$ `" [# W% f* ]在STM32中,DMA2D外设专门用来给LCD显示加速,有LTDC外设的型号中,通常也会配套有DMA2D。; ~# K' j3 s/ N# Y5 C+ Z; e
5 p1 Y' x) z& S
J$ v* ^$ s5 D9 cDMA2D外设主要提供了两个功能:
( P$ u$ S2 e2 e4 L0 N0 G/ `7 q: C4 o
5 q/ d3 e' T" _$ }3 dDMA数据搬运:常用从寄存器到存储器、存储器到存储器两种模式,快速高效,并且不占用cpu资源;
6 ?; y$ g L3 Y3 ]. e8 H2 ~/ }' }2D图形加速:支持快速格式转换和混合;' ]) b: e; O n7 l3 m
本文中主要使用到DMA2D外设的数据搬运功能,使用起来也是比较简单。) Y U' s4 @0 [" W' {$ l; g
8 }, G% Q. \' @' b1 X8 h
0 T% J/ v, f! z/ ?5 m" p: `2. 开启DMA2D
3 M7 x" d/ n8 H( y" \1 o
& T/ Q, e! H) }2 z
重新生成工程,cubemx会自动生成并调用dma2d初始化函数,完成dma2d外设时钟使能以及dma2d传输模式配置。
) L. n* P" K5 { @
9 P3 u0 x2 m5 P N: b- c% `6 R7 u1 y" o c' J9 t
3. 使用DMA2D实现lcd清屏函数
; x: p* e9 Q; e7 e& m0 Y0 ]在lcd驱动头文件中添加一个宏定义,用于控制是否使能DMA2D:
4 v- n7 o; q) i" O: E! j- <font face="微软雅黑" size="3">/**
( M `* G% ?6 @2 y% m/ ~ - * @brief whether use dma2d to transfer data to lcd framebuffer.
) h3 Y q/ T8 i) ? - */
/ _4 e. u& R2 F3 z+ f% b - #define USE_DMA2D_EN 1</font>
复制代码 接着对DMA2D传输操作进行封装,编写一个DMA2D传输函数:+ u0 e$ I& z3 R7 Y/ n
- <font face="微软雅黑" size="3">static void dma2d_transfer_data_r2m(uint32_t *addr, uint32_t xSize, uint32_t ySize, uint32_t offsetLine, uint16_t color)
3 t1 y. H1 L+ }4 n - {
7 M7 v6 [& N- u& x# ^7 w$ i - DMA2D->CR = DMA2D_R2M; // dma2d mode: register to memory.' \6 u& q- R0 |. a) [" u4 @
- DMA2D->OPFCCR = DMA2D_OUTPUT_RGB565;
$ k; |4 H$ }* e+ H - ( e5 H3 L9 K: R; m; e
- DMA2D->OCOLR = color;
6 l( O$ F, C) {2 ? - DMA2D->OMAR = (uint32_t)addr;
" t5 }7 c' k& z, m& k - DMA2D->OOR = offsetLine;
% q6 R9 Z ?! N- k - DMA2D->NLR = (uint32_t)(xSize << 16) | (uint16_t)ySize;
! r( Z+ j$ u% g+ }9 I) X -
; Y y9 N) O4 Y - DMA2D->CR |= DMA2D_CR_START;0 s2 R/ w7 |# w5 r; q
- while (DMA2D->CR & DMA2D_CR_START);
" A+ G3 Y" {2 K - }</font>
复制代码 利用此DMA2D传输函数,重新添加清屏函数的实现:
6 ^3 f2 }4 {& i, w- <font face="微软雅黑" size="3">void lcd_clear(uint16_t color)6 e2 v( r# u) p. v0 R1 S9 S7 e3 l- h
- {
) Q7 }* C, W3 j' k0 Y - #if USE_DMA2D_EN
, `; a9 n6 F; \( B/ s - dma2d_transfer_data_r2m((uint32_t *)LCD_FRAME_BUFFER, LCD_WIDTH, LCD_HEIGHT, 0, color);: D/ r' M% A& g& t! M
- #else3 b. O! K1 d5 ?) K [, w" \/ K) H
- uint16_t *ptr = (uint16_t*)LCD_FRAME_BUFFER;) O, C. ~# {9 }/ @6 v( E; x) R
- uint32_t i = 0;
1 A. m- i1 T+ M; H$ C - 6 p6 A* ?7 r/ Z1 @5 Z/ ?7 R
- while (i++ < LCD_WIDTH*LCD_HEIGHT) {
& x6 _' B$ D$ H6 s; \* r7 U5 g - *(ptr+i) = color;* c4 l: j( y9 A* z
- }
0 z( r2 b) K( ? - #endif /* USE_DMA2D_EN */3 l$ S' [- L @) d% b+ f9 k
- }</font>
复制代码 上层测试代码不变,确保控制dma2d的宏使能:
0 h# B6 S& {2 I& [3 T- <font face="微软雅黑" size="3">
+ ~3 ]0 G% u% _" B; w+ g0 Y - #define USE_DMA2D_EN 1</font>
复制代码 编译、下载,在串口助手中查看清屏一次所需时间:: _6 x3 M5 y2 f7 j% }4 g: [4 E
6 G2 j$ C; N7 }$ }( f( M) T
可以看到,刷屏一次只需31ms即可,并且在使用dma2d传输数据的情况下,数据传输时间和编译优化等级无关。. x& n) i+ ?% I/ E
( F0 m/ Q0 Z1 O6 J3 A/ J! ~0 E. C6 d( |. p+ O6 }: G+ k
三、LCD基本功能实现
& m1 B p/ D- j7 ^/ i0 `, ]+ `2 VLCD基本功能包括打点、读点、画线、绘图等函数。) \9 h# Q. {8 A) P4 _( s* x) r
+ L) n- v- `! A# G- P6 s1 |9 `/ f/ k/ s7 B, q: L9 h0 ]5 @/ X
1. 打点函数3 Y- Y6 C. A" f! u4 R6 |
打点函数的核心是计算当前用户给出的坐标位置在显存中的位置,两种实现如下:9 F2 x& q) V& W3 ]! |5 G. \
" f- b: U6 m/ V* x3 e0 M
- <font face="微软雅黑" size="3">void lcd_draw_point(uint16_t x, uint16_t y, uint16_t color)
; f: i- G* Y$ A1 d/ j - {
; S. O m8 f9 E3 ] - uint32_t pos;( ?5 o& L5 T- d1 t3 Y& @9 }
- uint16_t *ptr;
7 W1 f# g4 `2 {2 A! b/ S" R; S3 V - ) \2 D% V( }) p9 R9 n& }8 T- j
- // check position.3 Z6 g% \, R/ }. Y) h- L
- if (x > LCD_WIDTH || y > LCD_HEIGHT) {
' w/ v) n8 s% a# y+ v - return;/ C1 W1 @' S/ a4 U# h0 T- H
- }' ~4 c4 |/ O0 m$ n" X) T- T, ~
- 4 l3 h, ~! e0 x% X
- // calculate the position offset in framebuffer.9 R/ S3 |4 ]) c$ [& g
- pos = x + y*LCD_WIDTH;
8 Z$ c, D5 R. O) |* K1 Y5 K - ptr = (uint16_t*)LCD_FRAME_BUFFER;
+ D* X4 H2 q, g - " q/ A1 P+ a+ [6 S" Q+ y; Q! N
- // modify the framebuffer.
4 t. t( A5 V* Y6 h2 Y& R& D - #if USE_DMA2D_EN7 H( a! S7 I/ D
- dma2d_transfer_data_r2m((uint32_t *)(ptr+pos), 1, 1, 0, color);
$ R y7 W; c8 z& x; r - #else3 p& @) B/ z0 p* |& Q
- *(ptr+pos) = color;. F7 F; T. W; X! g T5 ?
- #endif /* USE_DMA2D_EN */0 F! @& Q4 n' L1 B! _) ]
- }</font>
复制代码 2. 读点函数实现
# L- d4 I4 f1 ~读点函数实现的核心也是计算出用户给出的坐标位置在显存中的位置:
3 q; b2 ~0 s) P* C7 U
& l5 h( m5 p7 S# C- o( ]* \- <font face="微软雅黑" size="3">uint16_t lcd_read_point(uint16_t x, uint16_t y)" r6 s, }& \- z8 ?
- {% w/ i% M. T& Q% ~, j
- uint32_t pos;
7 N6 e3 e: k# G& C. z- Z' V - uint16_t *ptr, data;
+ H) R- N# {8 H
2 |4 k! |( D" W1 i" U ?- // check position.+ G4 I2 F& x. W/ A! S
- if (x > LCD_WIDTH || y > LCD_HEIGHT) {
( a% u$ Y7 @4 Z! n' G - return 0;
4 I. G5 s3 Q& ^8 z- m - }9 v t: |$ |4 `4 z( W5 e: ]
- | V2 F2 Z* h) x4 y8 O
- // calculate the position offset in framebuffer.
% G: ?, y2 [3 F2 ]; k) E8 L7 e - pos = x + y*LCD_WIDTH; ^ i; X+ j+ H4 \8 M! Q) Y
- ptr = (uint16_t*)LCD_FRAME_BUFFER;( T8 o3 C5 ~8 E. W7 p
6 U( I) W9 T7 X, M4 l- // read the framebuffer.
+ t4 Y0 Z# U; Z1 K# ` - data = *(ptr+pos);
% A' K. R0 t5 z+ h
3 J+ d9 L5 Q+ B6 N7 C/ o ]- return data;
& L8 c6 r4 z* J/ Q% k - }</font>
复制代码 3. 画线、画矩形、画圆! B k3 R$ F" Y+ X9 b* \
这三个功能都是基于打点函数,使用 Bresenham 算法,代码篇幅过多,如有兴趣可直接查看本篇源码。
% b/ O" v1 T' @* [- A7 p# g3 u t* p! ]$ q h# m; G+ h4 V
" x* H9 ?5 Y; U' f3 Q4. 测试
/ v+ E1 l5 q2 g) D/ F在main函数中添加测试代码:3 \/ I9 [# Z# Y7 W4 z5 {
8 D& a a: C- e# [2 v+ W
- <font face="微软雅黑" size="3">lcd_draw_line(0, 0, 1024, 600, GREEN);1 q# e# S9 h# l7 v' ^$ _4 R( y
- lcd_draw_line(0, 300, 1024, 300, RED);! X6 [) }4 K) Z: u V" Z
- lcd_draw_line(512, 0, 512, 600, BLUE);; o2 |5 {# q& m4 V3 N% B
- lcd_draw_line(1024, 0, 0, 600, YELLOW);! L* T9 `3 L+ w0 x- r* ?4 B- b' m
8 q3 S4 L$ B& h- b) J- lcd_draw_rect(256, 150, 1024-256, 600-150, PINK);</font>
复制代码 编译,下载,结果如下图:6 t, Q) W+ j+ V
4 ~% s Q- @8 J7 M1 F# Z% n& o8 J
$ y: B: a' Z( o8 b5 g t$ z
S" H/ E5 `) f6 c8 c0 x4 D7 u |
大佬,源码可以分享一下吗?