通过寄存器上的LTDC ...启动STM32上的显示

问候! 最近,一个项目需要启动具有LVDS界面的显示器。 为了实现该任务,选择了STM32F746控制器,因为 我已经与他进行了很多合作,并且他具有LTDC模块,该模块可让您直接使用显示器而无需控制器。 在这种情况下,控制器已经在微控制器内部实现。 另外,最后一个论点是,我已经在这块石头上进行了STM32F746-Disco调试,这意味着我可以开始进行项目工作,而无需等待电路板,组件等来找我。

今天,我将告诉您如何使用寄存器(CMSIS)运行LTDC模块。 HAL和其他图书馆出于宗教原因不喜欢也不使用,但这也很有趣。 您将看到在寄存器上提高复杂的外设就像常规SPI一样简单。 有意思吗 那我们走吧!



1.关于LTDC的一些知识


该外围模块实质上是一个控制器,通常位于显示器的侧面,例如SSD1963等。 如果我们看一下LTDC的结构,我们会发现实际上这是一个24位并行总线+一个硬件图形加速器+ RAM中的数据数组,实际上是一个显示缓冲区(帧缓冲区)。



在输出端,我们有一个普通的并行总线,其中包含24位颜色(RGB模型的每种颜色8位),同步线,在线显示/离线显示和像素时钟。 实际上,后者是一个时钟信号,通过该时钟信号将像素加载到显示器中,也就是说,如果我们具有9.5 MHz的频率,则在1秒内可以加载950万像素。 从理论上讲,当然,在实践中,由于时间安排和其他因素,这个数字稍微适中。

有关LTDC的详细介绍,建议您阅读一些文档:

  1. 概述F4中LTDC的功能,在我们的F7中,所有功能都是相同的
  2. 应用笔记4861。“ STM32 MCU上的LCD-TFT显示控制器(LTDC)”

2.我们需要做什么?


ST微控制器之所以获得普及是有充分理由的,任何电子组件的最重要要求是文档,一切都很好。 该站点肯定很糟糕,但是我将保留所有文档的链接。 制造商使我们免于遭受自行车的折磨和发明,因此,在参考手册RM0385中的第520页给出了黑白步骤,我们需要做的是:



实际上,您不必执行上述操作的一半:不需要启动它,或者默认情况下已经配置好了它。 对于最低限度的开始,它使我们能够绘制像素,显示图片,图形,文本等,足以执行以下操作:

  • 启用LTDC时钟
  • 设置时钟系统和数据输出频率(像素时钟)
  • 配置I / O端口(GPIO)以与LTDC一起使用
  • 为我们的显示模型设置时间
  • 调整信号的极性。 默认情况下已经完成
  • 指定显示器的背景色。 我们还未见到他,您可以将其保留为“零”
  • 设置特定图层的显示可见区域的实际大小
  • 选择颜色格式:ARGB8888,RGB 888,RGB565等。
  • 指定将用作帧缓冲区的数组的地址
  • 指示一行中的数据量(长宽)
  • 指示行数(显示高度)
  • 包括我们正在使用的图层
  • 启用LTDC模块

吓人的 我很害怕,但是事实证明,在所有程序中,它工作了20分钟。 有一个任务,这个计划是计划好的,只是为了实现它而已。

3.设置时钟系统


我们需要发送时钟信号到LTDC模块的第一项,这是通过写入RCC寄存器来完成的:

RCC->APB2ENR |= RCC_APB2ENR_LTDCEN; 

接下来,您需要将外部石英(HSE)的时钟频率配置为最大216 MHz。 第一步是打开石英谐振器的时钟源,并等待就绪标志:

 RCC->CR |= RCC_CR_HSEON; while (!(RCC->CR & RCC_CR_HSERDY)); 

现在将控制器闪存的延迟设置为 她不知道如何在核心频率工作。 与其他数据一样,其值取自参考手册:

 FLASH->ACR |= FLASH_ACR_LATENCY_5WS; 

现在,为了获得所需的频率,我将输入的25 MHz除以25,得到1 MHz。 接下来,在PLL中,我乘以432,因为 将来会有一个最小值为/ 2的分频器,您需要对其应用两倍的频率。 之后,我们将PLL输入连接到我们的石英谐振器(HSE):

 RCC->PLLCFGR |= RCC_PLLCFGR_PLLM_0 | RCC_PLLCFGR_PLLM_3 | RCC_PLLCFGR_PLLM_4; RCC->PLLCFGR |= RCC_PLLCFGR_PLLN_4 | RCC_PLLCFGR_PLLN_5 | RCC_PLLCFGR_PLLN_7 | RCC_PLLCFGR_PLLN_8; RCC->PLLCFGR |= RCC_PLLCFGR_PLLSRC; 

现在启用PLL并等待就绪标志:

 RCC->CR |= RCC_CR_PLLON; while((RCC->CR & RCC_CR_PLLRDY) == 0){} 

我们将PLL的输出分配为系统频率的源,并等待就绪标志:

 RCC->CFGR |= RCC_CFGR_SW_PLL; while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_1) {} 

这将结束常规时钟设置,然后我们继续设置显示器的时钟频率(PLLSAI)(像素时钟)。 根据数据手册,用于PLLSAI的信号在分频器/ 25之后获取,即在输入处为1 MHz。 我们需要获得大约9.5 MHz的频率,为此,我们将1 MHz的频率乘以192,然后使用两个分频器分别乘以5和4,得到所需的值PLLSAI = 1 MHz * 192/5/4 = 9.6 MHz:

 RCC->PLLSAICFGR |= RCC_PLLSAICFGR_PLLSAIN_6 | RCC_PLLSAICFGR_PLLSAIN_7; RCC->PLLSAICFGR |= RCC_PLLSAICFGR_PLLSAIR_0 | RCC_PLLSAICFGR_PLLSAIR_2; RCC->DCKCFGR1 |= RCC_DCKCFGR1_PLLSAIDIVR_0; RCC->DCKCFGR1 &= ~RCC_DCKCFGR1_PLLSAIDIVR_1; 

最后一步,我们启用PLLSAI进行显示,并等待准备工作标志:

 RCC->CR |= RCC_CR_PLLSAION; while ((RCC->CR & RCC_CR_PLLSAIRDY) == 0) {} 

这样就完成了时钟系统的基本设置,为避免忘记然后不受影响,让我们在所有输入/输出端口(GPIO)上启用时钟。 我们没有电池电源,至少用于调试,所以我们不保存:

 RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN; RCC->AHB1ENR |= RCC_AHB1ENR_GPIOCEN; RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN; RCC->AHB1ENR |= RCC_AHB1ENR_GPIOEEN; RCC->AHB1ENR |= RCC_AHB1ENR_GPIOFEN; RCC->AHB1ENR |= RCC_AHB1ENR_GPIOGEN; RCC->AHB1ENR |= RCC_AHB1ENR_GPIOHEN; RCC->AHB1ENR |= RCC_AHB1ENR_GPIOJEN; RCC->AHB1ENR |= RCC_AHB1ENR_GPIOKEN; 

4.配置I / O端口(GPIO)


设置gpio非常简单-我们将LTDC总线的所有支路都配置为高频率的备用输出。 为此,在第201页的参考手册中,有以下提示:



该表指示需要设置寄存器中的哪些位以获得必要的设置。 值得注意的是,所有大括号都被禁用。 在哪里寻找包括哪些替代功能? 为此,请转到控制器数据表中的第76页,并查看下表:



如您所见,表的逻辑是最简单的:我们找到了所需的功能,在我们的例子LTDC B0中,然后我们查看了它在哪个GPIO上(例如PE4),在顶部我们看到了将用于配置的替代功能的编号(与我们一起使用AF14)。 要将我们的输出配置为带有替代函数LTDC B0的推挽输出,我们需要编写以下代码:

 GPIOE->MODER &= ~GPIO_MODER_MODER4; GPIOE->MODER |= GPIO_MODER_MODER4_1; GPIOE->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR4_1; GPIOE->AFR[0] &= ~GPIO_AFRL_AFRL4_0; GPIOE->AFR[0] |= GPIO_AFRL_AFRL4_1 | GPIO_AFRL_AFRL4_2 | GPIO_AFRL_AFRL4_3; 

我举了一个PE4引脚的示例,它对应于LTDC总线上的B0引脚,也就是蓝色的零位。 对于所有其他结论,设置是相同的,只有两个结论值得特别注意,其中一个准备就绪,其中包括一个显示器,另一个结论是其背光。 它们被配置为普通推挽输出,每个人都可以使用它来使LED闪烁。 设置如下所示:

 GPIOK->MODER &= ~GPIO_MODER_MODER3; GPIOK->MODER |= GPIO_MODER_MODER3_0; 

此设置用于PK3输出,它可以打开或关闭背光。 顺便说一句,您也可以按它以平滑地调节亮度。 对于包含显示器(DISP)的PI12,一切都相同。 默认情况下,这两个引脚的速度较低,因为 不需要执行某些高频操作。

您可以在调试板的电路板上或自己设备的电路图上查看所有其他I / O端口。

5.时间及其设置


从物理角度来看,时间是普通的延迟。 我认为您在使用类似于ILI9341的SPI / I2C控制器的显示器上查看代码示例时,已经反复观察到延迟(1)类型的各种变态。 那里需要一个延迟,以便控制器,例如,有时间接受命令,执行命令,然后对数据做一些事情。 对于LTDC,一切都差不多,只是我们不会拐杖,为什么不拐杖-我们的微控制器本身能够在硬件中配置必要的时序。 为什么在没有控制器的显示器上需要它们? 是的,基本是,在填充第一行像素后,转到下一行并返回其开头。 这归因于显示器的生产技术,因此每种特定的显示器型号都有自己的时序。

要找出我们需要的值,请访问ST网站,并查看STM32F746-Disco调试板示意图 。 在那里,我们可以看到该显示器是RK043FN48H-CT672B,并且例如可以在此处找到其文档。 我们最感兴趣的是第7.3.1节第13页的表:



这些是我们设置时需要的价值观。 此外,在文档中还有许多有趣的内容,例如总线上的信号图等,例如,如果您想将显示内容提升到FPGA或CPLD,可能需要这些。

进入设置。 首先,为了不让我牢记这些值,我将以定义的形式排列它们:

 #define DISPLAY_HSYNC ((uint16_t)30) #define DISPLAY_HBP ((uint16_t)13) #define DISPLAY_HFP ((uint16_t)32) #define DISPLAY_VSYNC ((uint16_t)10) #define DISPLAY_VBP ((uint16_t)2) #define DISPLAY_VFP ((uint16_t)2) 

有一个有趣的功能。 定时脉冲宽度 (称为DISPLAY_HSYNC)在表中仅针对5 MHz的像素时钟频率具有值,而对于9和12 MHz则没有。 需要为您的显示选择该时序,我得到的值为30,而在ST的示例中则不同。 第一次开始时,如果设置有误,图像将向左或向右移动。 如果在右边,则减少计时;在左边,则增加计时。 实际上,它会影响可见区域的起源,我们将在后面看到。 请记住,AN4861第24页的以下图片将有助于理解整个段落:



这里小的抽象很方便。 我们有2个显示区域:可见和常规。 可见区域的尺寸声明的分辨率为480 x 272像素,总区域为可见区域+我们的计时,每边有3个。 值得一提的是(不再是抽象的)一个系统刻度是1像素,因此总面积为480像素+ HSYNC + HBP + HFP。

还值得认识到的是,计时越少越好-显示屏更新速度更快,帧频也会略有增加。 因此,在第一次运行后,请对时序进行试验,并在保持稳定性的同时尽可能减少时序。

为了设定时间,我为自己准备了一个小“备忘单”,供将来在项目中使用,它还可以帮助您了解具体的数字以及在何处编写:

 /* *************************** Timings for TFT display********************************** * * HSW = (DISPLAY_HSYNC - 1) * VSH = (DISPLAY_VSYNC - 1) * AHBP = (DISPLAY_HSYNC + DISPLAY_HBP - 1) * AVBP = (DISPLAY_VSYNC + DISPLAY_VBP - 1) * AAW = (DISPLAY_HEIGHT + DISPLAY_VSYNC + DISPLAY_VBP - 1) * AAH = (DISPLAY_WIDTH + DISPLAY_HSYNC + DISPLAY_HBP - 1) * TOTALW = (DISPLAY_HEIGHT + DISPLAY_VSYNC + DISPLAY_VBP + DISPLAY_VFP - 1) * TOTALH = (DISPLAY_WIDTH + DISPLAY_HSYNC + DISPLAY_HBP + DISPLAY_HFP - 1) * */ 

这个“备忘单”来自哪里?首先,您在之前的几段中看到了类似的“公式”。 其次,转到我们的AN4861的第56页:



没错,我希望您能在本备忘单出现之前了解这些时间安排的物理意义,并且我相信您自己也可以进行整理。 没什么复杂的,RM和AN的图片有助于直观地了解时序对图像形成过程的影响。

现在是时候编写设置这些时间的代码了。 在“备忘单”中指示要写入的寄存器的位,例如TOTALH,并且在符号之后等于给出输出一定数量的公式。 好吗 然后我们写:

 LTDC->SSCR |= ((DISPLAY_HSYNC - 1) << 16 | (DISPLAY_VSYNC - 1)); LTDC->BPCR |= ((DISPLAY_HSYNC+DISPLAY_HBP-1) << 16 | (DISPLAY_VSYNC+DISPLAY_VBP-1)); LTDC->AWCR |= ((DISPLAY_WIDTH + DISPLAY_HSYNC + DISPLAY_HBP - 1) << 16 | (DISPLAY_HEIGHT + DISPLAY_VSYNC + DISPLAY_VBP - 1)); LTDC->TWCR |= ((DISPLAY_WIDTH + DISPLAY_HSYNC + DISPLAY_HBP + DISPLAY_HFP -1)<< 16 |(DISPLAY_HEIGHT + DISPLAY_VSYNC + DISPLAY_VBP + DISPLAY_VFP - 1)); 

这就是时间! 在此部分中,您只能配置背景颜色。 默认情况下,我将其设置为黑色,因此将其写为零。 如果要更改背景层(背景)的颜色,则可以同样地写入任何值,例如0xFFFFFFFF并用白色填充所有内容:

 LTDC->BCCR = 0; 

参考手册中有一个很棒的插图,可以清楚地说明我们实际上分为3层:背景,第1层和第2层。背景层是“ castated”的,只能填充一种特定的颜色,但是它也非常有用未来的GUI设计。 另外,此插图清楚地说明了图层的优先级,这意味着仅当其余图层为空或透明时,我们才会在背景上看到填充颜色。

作为示例,我将显示项目的页面之一,其中在模板的实现过程中,背景填充了一种颜色,并且控制器没有重绘整个页面,而是仅重绘了单个扇区,这使得许多其他任务可以接收约50-60 fps:



6. LTDC设置的最后一部分


LTDC设置分为两部分:第一部分对于整个LTDC模块是通用的,位于LTDC寄存器组中 ,第二部分在两层之一中进行配置,并且位于LTDC_Layer1LTDC_Layer2组中

我们在上一段中进行了常规设置,其中包括设置时间,背景层。 现在我们继续设置图层,我们的列表需要该图层可见区域的实际大小,以4个坐标(x0,y0,x1,y2)的形式描述,这使我们可以获得矩形的尺寸。 可见层的大小可能小于显示器的分辨率,没有人愿意将每100像素的层大小设为100。 要调整可见区域的大小,请编写以下代码:

 LTDC_Layer2->WHPCR |= (((DISPLAY_WIDTH + DISPLAY_HBP + DISPLAY_HSYNC - 1) << 16) | (DISPLAY_HBP + DISPLAY_HSYNC)); LTDC_Layer2->WVPCR |= (((DISPLAY_HEIGHT + DISPLAY_VSYNC + DISPLAY_VBP - 1) << 16) |(DISPLAY_VSYNC + DISPLAY_VBP)); 

如您所见,一切都与时机相同。 可见区域的起点(x0,y0)由两个时序之和组成:HSYNC + HBP和VSYNC + VBP。 为了计算端点(x1,y1)的坐标,只需将以像素为单位的宽度和高度添加到值数据中。

现在,您需要配置接收到的数据的格式。 使用ARGB8888格式可获得最高的质量,但与此同时,我们可以获得最大的占用内存量。 一个像素占用32位或4字节,这意味着整个屏幕占用4 * 480 * 272 = 522,240字节,即不是我们最薄弱的控制器的闪存的一半。 不用担心-通过QSPI连接外部SDRAM和闪存可以解决内存问题,并且对此格式没有限制,我们以高品质感到高兴。 如果要节省空间或显示器不支持24位格式,则可以使用更合适的模型,例如RGB565。 对于显示器和相机而言,这是一种非常流行的格式,最重要的是,使用它时,1像素仅占用5 + 6 + 5 = 16位或2字节。 因此,该层占用的内存量将减少2倍。 默认情况下,控制器已经配置了ARGB8888格式,如下所示:

 LTDC_Layer2->PFCR = 0; 

如果需要与ARGB8888不同的格式,请转到参考手册中的第533和534页,然后从下面的列表中选择所需的格式:



现在创建一个数组并将其地址传递给LTDC,它将变为帧缓冲区,并将成为我们层的“反射”。 例如,您需要用白色填充第一行的第一个像素,为此,您只需要将颜色值(0xFFFFFFFF)写入此数组的第一个元素即可。 是否需要填充第二行中的第一个像素? 然后,我们还将颜色值写入带有数字(480 + 1)的元素中。 480-换行,然后在所需的行中添加数字。

此设置如下所示:

 #define DISPLAY_WIDTH ((uint16_t)480) #define DISPLAY_HEIGHT ((uint16_t)272) const uint32_t imageLayer2[DISPLAY_WIDTH * DISPLAY_HEIGHT]; LTDC_Layer2->CFBAR = (uint32_t)imageLayer2; 

以一种好的方式,在配置LTDC之后,您还需要配置SDRAM以删除const修饰符并获取RAM中的帧缓冲区,因为 即使对于4字节的一层,MK自己的RAM还是不够的。 尽管这不会影响测试外围设备的正确配置。

接下来,您需要指定alpha层的值,即Layer2层的透明度,为此,我们编写一个介于0到255之间的值,其中0是完全透明的层,255是完全不透明的,即100%可见:

 LTDC_Layer2->CACR = 255; 

根据我们的计划,现在有必要以字节为单位记录可见显示区域的大小,为此,我们将相应的值写入寄存器:

 LTDC_Layer2->CFBLR |= (((PIXEL_SIZE * DISPLAY_WIDTH) << 16) | (PIXEL_SIZE * DISPLAY_WIDTH + 3)); LTDC_Layer2->CFBLNR |= DISPLAY_HEIGHT; 

剩下的最后两个步骤,即包含第2层和LTDC外设模块本身。 为此,写入相应的位:

 LTDC_Layer2->CR |= LTDC_LxCR_LEN; LTDC->GCR |= LTDC_GCR_LTDCEN; 

这样就完成了我们模块的配置,您可以使用我们的显示器!

7.关于与LTDC合作的一些知识


现在,与显示器的所有工作仅归结为将数据写入imageLayer2数组,它的大小为480 x 272个元素,这完全符合我们的分辨率,并暗示了一个简单的事实- 数组元素1 =显示器上的1像素

举例来说,我将图片写入了在LCD Image Converter中转换为阵列的图像 ,但实际上,您的任务不太可能仅限于此。 有两种方法:使用现成的GUI并自己编写。 对于诸如文本输出,制图之类的相对简单的任务,我建议您编写自己的GUI,这将花费一些时间,并使您对它的工作有充分的了解。 当任务艰巨而又无暇开发自己的GUI时,建议您注意现成的解决方案,例如uGFX等。

文本,线条和其他元素的符号本质上是像素阵列,因此要实现它们,您需要自己实现逻辑,但是您应该从最基本的功能-“像素输出”开始。 它应该包含3个参数:沿X的坐标,沿Y的坐标,以及相应地绘制给定像素的颜色。 它可能看起来像这样:

 typedef enum ColorDisplay { RED = 0xFFFF0000, GREEN = 0xFF00FF00, BLUE = 0xFF0000FF, BLACK = 0xFF000000, WHITE = 0xFFFFFFFF } Color; void SetPixel (uint16_t setX, uint16_t setY, Color Color) { uint32_t numBuffer = ((setY - 1) * DISPLAY_WIDTH) + setX; imageLayer2[numBuffer] = Color; } 

将坐标带入函数后,我们将其重新计算为与给定坐标对应的数组编号,然后将接收到的颜色写入到接收到的元素中。 基于此功能,您已经可以实现用于显示几何图形,文本和其他GUI“好东西”的功能。 我认为这个想法是可以理解的,但是如何实现它则由您自己决定。

总结


如您所见,即使在寄存器上复杂的外设(CMSIS)的实现也不是一件容易的事,您只需要了解它在内部的工作原理即可。 当然,现在不了解发生了什么就开发固件很流行,但是如果您打算成为一名工程师而不是...这将是死胡同。

如果将结果代码与HAL或SPL中的解决方案进行比较,您会发现写入寄存器的代码更加紧凑。 在需要的地方添加一些注释并将其包装在函数中,我们的可读性至少与HAL / SPL相同,并且,如果您还记得参考手册文档进行了注册,那么使用CMSIS更为方便。

1)在TrueSTUDIO中带有源代码的项目可以在这里下载

2)对于那些更喜欢GitHub的人

3) 在此处下载用于将图像转换为LCD Image Converter代码的实用程序

Source: https://habr.com/ru/post/zh-CN412753/


All Articles