【器件型号】
单片机采用STM32F429IG,运行频率为180MHz,外部晶振HSE的频率为25MHz。
开发板采用外部32MB的SDRAM内存作显存。显示屏分辨率为800×480,颜色格式为RGB565,每个像素占2个字节(显示半透明位图时,位图每像素占3个字节)。显存占用内存的大小为800×480×2=768000字节=750KB。
7寸LCD液晶屏与单片机的连接电路如下图所示。这个液晶屏通过LTDC接口和I²C接口与单片机连接。LTDC用于传输图像数据,I²C用于传输触摸信息。图中的RESET、T_PEN、T_MISO和T_CS引脚为空引脚。
名称 | 描述 | I/O口 |
---|---|---|
LCD_BL | 液晶屏显示开关(高电平开显示) | PB5 |
LCD_DE | LTDC数据使能信号(低电平有效) | PF10 |
LCD_VSYNC | LTDC垂直同步信号 | PI9 |
LCD_HSYNC | LTDC水平同步信号 | PI10 |
LCD_CLK | LTDC时钟信号 | PG7 |
LCD_R3~7 | LTDC红色分量值(5位) | PH9~12, PG6 |
LCD_G2~7 | LTDC绿色分量值(6位) | PH13~15, PI0~2 |
LCD_B3~7 | LTDC蓝色分量值(5位) | PG11, PI4~7 |
T_SCK | I²C总线时钟 | PH6 |
T_MOSI | I²C总线数据 | PI3 |
SDRAM与单片机的连接电路如下图所示。这些线全部为FMC的SDRAM信号线。
【测试程序】
2020年7月31日新版程序下载地址:https://pan.baidu.com/s/1jxoHQsvztWcXmkB-AHL2Pw(提取码:znvw)
#include <GUI.h>
#include <stdio.h>
#include <stm32f4xx.h>
#include "common.h"
#include "images.h"
#include "ARKLCD7.h"
#include "W9825G6KH.h"
static void display_image(void)
{
GUI_BITMAP bmp;
bmp.XSize = IMAGE_EXCLAMATION_WIDTH; // 宽度
bmp.YSize = IMAGE_EXCLAMATION_HEIGHT; // 高度
bmp.BytesPerLine = 3 * bmp.XSize; // 每行字节数
bmp.BitsPerPixel = 24; // 每个像素点字节数
bmp.pData = image_exclamation; // 位图数据
bmp.pPal = NULL; // 调色板
bmp.pMethods = GUI_DRAW_BMPAM565; // 数据格式: ARGB565
GUI_DrawBitmap(&bmp, (SCREEN_WIDTH - IMAGE_EXCLAMATION_WIDTH) / 2, 30);
}
int main(void)
{
char str[60];
int i, ret;
ARKLCD7_TouchInfo touch;
GUI_MEMDEV_Handle memdev;
GUI_RECT rect;
HAL_Init();
clock_init();
usart_init(115200);
printf("STM32F429IG ARKLCD7\n");
printf("SystemCoreClock=%u\n", SystemCoreClock);
W9825G6KH_Init(); // 初始化SDRAM内存
ARKLCD7_Init(); // 初始化液晶屏
GUI_Init();
GUI_SetBkColor(GUI_MAGENTA); // 背景设为浅紫色
GUI_Clear(); // 清屏
//GUI_InvertRect(10, 10, 100, 100); // 反色
// 打开缓冲功能, 绘图先在内存里面完成, 绘制好了之后再一次性显示出来, 避免闪烁
memdev = GUI_MEMDEV_Create(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
GUI_MEMDEV_Select(memdev);
display_image(); // 绘制位图
GUI_MEMDEV_CopyToLCD(memdev); // 图像绘制完成后, 调用这个函数显示出来
GUI_SetFont(GUI_FONT_32B_ASCII); // 设置字体
GUI_SetTextMode(GUI_TM_TRANS); // 文字背景透明
while (1)
{
ret = ARKLCD7_ReadTouchInfo(&touch); // 读取触摸信息
if (ret == 0)
{
// 显示系统时间
GUI_SetColor(GUI_RED);
rect.x0 = 0;
rect.y0 = 200;
rect.x1 = SCREEN_WIDTH;
rect.y1 = 235;
snprintf(str, sizeof(str), "Ticks: %u", HAL_GetTick());
GUI_ClearRectEx(&rect);
GUI_DispStringInRect(str, &rect, GUI_TA_HCENTER | GUI_TA_VCENTER);
//GUI_InvertRect(rect.x0, rect.y0, rect.x1, rect.y1);
// 显示触摸点的个数
rect.y0 += 35;
rect.y1 += 35;
snprintf(str, sizeof(str), "Point count: %d", touch.count);
GUI_ClearRectEx(&rect);
GUI_DispStringInRect(str, &rect, GUI_TA_HCENTER | GUI_TA_VCENTER);
// 显示触摸点的坐标
GUI_SetColor(GUI_BLUE);
for (i = 0; i < 10; i++)
{
if (i % 2 == 0)
{
rect.x0 = 100;
rect.x1 = SCREEN_WIDTH / 2 - 1;
rect.y0 += 35;
rect.y1 += 35;
}
else
{
rect.x0 = SCREEN_WIDTH / 2 + 50;
rect.x1 = SCREEN_WIDTH - 1;
}
GUI_ClearRectEx(&rect);
if (i < touch.count)
{
snprintf(str, sizeof(str), "Point %d: (%u, %u)", i + 1, touch.points[i].x, touch.points[i].y);
GUI_DispStringInRect(str, &rect, GUI_TA_VCENTER);
}
}
GUI_MEMDEV_CopyToLCD(memdev);
}
HAL_Delay(50);
}
}
其中用到的半透明图像保存在单片机的内部Flash中,大小约为60KB:
#ifndef __IMAGES_H
#define __IMAGES_H
#define IMAGE_EXCLAMATION_WIDTH 154
#define IMAGE_EXCLAMATION_HEIGHT 134
extern const unsigned char image_exclamation[61908];
#endif
【程序运行效果】
背景色为浅紫色,上方显示一张半透明图片,下方显示毫秒计数器的当前值(ticks),以及触摸点的个数和坐标。
注意:把电源拔掉后重新上电,会发现屏幕上只有一个感叹号,没有文字显示,直到手按下触摸屏后才恢复正常。这是因为刚上电没有任何触摸时,寄存器读出来的点的个数是255,所有点的坐标都是(65535, 65535),因此ARKLCD7_ReadTouchInfo返回-1。只要按下了触摸屏,就会恢复正常。
旧版程序只能显示前5个触控点,新版程序可以显示所有10个触控点。
【液晶屏驱动程序】
LCD显示屏的初始化由ARKLCD7.c完成。初始化液晶屏调用的是ARKLCD7_Init函数,这个函数会先初始化液晶屏的GPIO口,然后配置好LTDC。
ARKLCD7这个液晶屏的LTDC_R7、LTDC_G7和LTDC_B7(PG6、PI2和PI7)引脚上接了外部上下拉电阻,用来标识这个液晶屏的分辨率(具体看正点原子的液晶屏手册)。本项目使用的液晶屏的分辨率为800×480,颜色为16位RGB565,每个像素占用两个字节,所以需要的显存是800×480×2字节,即750KB。
HAL_RCCEx_PeriphCLKConfig配置的是LTDC的频率,也就是显示器的刷新率。使用外部SDRAM作显存时,时钟频率不能太高,否则显示器无法显示图像。还要注意不能超过SDRAM的最大读写速率。考虑了这些因素后,程序中将刷新率设置为30MHz。计算方式为PLLSAIN÷PLLSAIR÷PLLSAIDivR=360÷6÷2=30。最开始设置的频率是33MHz(也就是正点原子例程上面的频率),后来发现往SDRAM上通过memcpy放置大尺寸图像时,液晶屏抖动很厉害,刷新率降低到后30MHz问题就解决了。值得注意的是,板子上还接了一个NAND Flash存储器,这个存储器的读取速度能达到6MB/s。但是由于NAND Flash和SDRAM共用了FMC的总线,所以用SDRAM作显存时,NAND Flash的读取速度不能太快,否则会因为总线占用时间过长,LTDC没有足够的机会读取SDRAM,导致液晶屏图像剧烈抖动。读取NAND Flash时,要注意每一条指令发出去后都要delay延时一下,delay函数中的for循环至少要循环1000次,这样液晶屏就不会发生抖动。
防止液晶屏抖动,还要注意尽量减少SDRAM的并发读写操作。例如,在FreeRTOS系统环境下,当一个任务在调用GUI_DrawBitmap函数往SDRAM上绘制位图,另一个任务在用FATFS的f_read函数将W25Q256存储器中存储的文件加载到SDRAM上。此时加上LTDC的液晶屏显示(LTDC每时每刻都在不停地读取SDRAM,并将显存中的数据发给液晶屏显示),就有3个并发的SDRAM操作。这时液晶屏图像就会发生抖动。要解决这个问题,可以设法减少SDRAM并发操作的个数。调用FreeRTOS系统的xSemaphoreCreateRecursiveMutex函数建立一个互斥量。进行GUI_DrawBitmap操作前先用xSemaphoreTakeRecursive锁住这个互斥量,完了之后用xSemaphoreGiveRecursive解锁。FATFS底层的diskio读取数据块的函数也加上这个互斥锁。这样同时的并发操作数只有2个,就能解决液晶屏抖动的问题。
void ARKLCD7_Init(void)
{
uint8_t screen_id = 0;
GPIO_InitTypeDef gpio;
LTDC_LayerCfgTypeDef layer;
RCC_PeriphCLKInitTypeDef clk = {0};
__HAL_RCC_CRC_CLK_ENABLE(); // STemWin GUI_Init前必须打开CRC
__HAL_RCC_DMA2D_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOF_CLK_ENABLE();
__HAL_RCC_GPIOG_CLK_ENABLE();
__HAL_RCC_GPIOH_CLK_ENABLE();
__HAL_RCC_GPIOI_CLK_ENABLE();
/* 先读取ID */
gpio.Mode = GPIO_MODE_INPUT;
gpio.Pin = GPIO_PIN_6; // LTDC_R7
gpio.Pull = GPIO_PULLUP;
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOG, &gpio);
gpio.Pin = GPIO_PIN_2 | GPIO_PIN_7; // LTDC_G7, LTDC_B7
HAL_GPIO_Init(GPIOI, &gpio);
if (HAL_GPIO_ReadPin(GPIOG, GPIO_PIN_6) == GPIO_PIN_SET)
screen_id |= 1;
if (HAL_GPIO_ReadPin(GPIOI, GPIO_PIN_2) == GPIO_PIN_SET)
screen_id |= 2;
if (HAL_GPIO_ReadPin(GPIOI, GPIO_PIN_7) == GPIO_PIN_SET)
screen_id |= 4;
printf("Screen ID: %u\n", screen_id);
/* 初始化Screen引脚 */
// PB5: 显示控制 (LCD_BL)
gpio.Mode = GPIO_MODE_OUTPUT_PP;
gpio.Pin = GPIO_PIN_5;
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &gpio);
// PH6: SCL, PI3: SDA
SCL_1;
SDA_1;
gpio.Mode = GPIO_MODE_OUTPUT_OD;
gpio.Pin = GPIO_PIN_6;
gpio.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOH, &gpio);
gpio.Pin = GPIO_PIN_3;
HAL_GPIO_Init(GPIOI, &gpio);
// PF10: LTDC_DE
gpio.Alternate = GPIO_AF14_LTDC;
gpio.Mode = GPIO_MODE_AF_PP;
gpio.Pin = GPIO_PIN_10;
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
HAL_GPIO_Init(GPIOF, &gpio);
// PG6: LTDC_R7, PG7: LTDC_CLK, PG11: LTDC_B3
gpio.Pin = GPIO_PIN_6 | GPIO_PIN_7 | GPIO_PIN_11;
HAL_GPIO_Init(GPIOG, &gpio);
// PH9~12: LTDC_R3~6, PH13~15: LTDC_G2~4
gpio.Pin = GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15;
HAL_GPIO_Init(GPIOH, &gpio);
// PI0~2: LTDC_G5~7, PI4~7: LTDC_B4~7, PI9: LTDC_VSYNC, PI10: LTDC_HSYNC
gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7 | GPIO_PIN_9 | GPIO_PIN_10;
HAL_GPIO_Init(GPIOI, &gpio);
/* 初始化LTDC */
// 使用外部SRAM作显存时, 时钟频率不能太高
clk.PeriphClockSelection = RCC_PERIPHCLK_LTDC;
clk.PLLSAI.PLLSAIN = 360;
clk.PLLSAI.PLLSAIR = 6;
clk.PLLSAIDivR = RCC_PLLSAIDIVR_2;
HAL_RCCEx_PeriphCLKConfig(&clk);
__HAL_RCC_LTDC_CLK_ENABLE();
HAL_NVIC_EnableIRQ(LTDC_IRQn);
hltdc.Instance = LTDC;
hltdc.Init.HSPolarity = LTDC_HSPOLARITY_AL;
hltdc.Init.VSPolarity = LTDC_VSPOLARITY_AL;
hltdc.Init.DEPolarity = LTDC_DEPOLARITY_AL;
hltdc.Init.PCPolarity = LTDC_PCPOLARITY_IPC;
hltdc.Init.HorizontalSync = SCREEN_HSW - 1;
hltdc.Init.VerticalSync = SCREEN_VSW - 1;
hltdc.Init.AccumulatedHBP = SCREEN_HSW + SCREEN_HBP - 1;
hltdc.Init.AccumulatedVBP = SCREEN_VSW + SCREEN_VBP - 1;
hltdc.Init.AccumulatedActiveW = SCREEN_HSW + SCREEN_HBP + SCREEN_WIDTH - 1;
hltdc.Init.AccumulatedActiveH = SCREEN_VSW + SCREEN_VBP + SCREEN_HEIGHT - 1;
hltdc.Init.TotalWidth = SCREEN_HSW + SCREEN_HBP + SCREEN_WIDTH + SCREEN_HFP - 1;
hltdc.Init.TotalHeigh = SCREEN_VSW + SCREEN_VBP + SCREEN_HEIGHT + SCREEN_VFP - 1;
hltdc.Init.Backcolor.Red = 0;
hltdc.Init.Backcolor.Green = 0;
hltdc.Init.Backcolor.Blue = 0;
HAL_LTDC_Init(&hltdc);
layer.WindowX0 = 0;
layer.WindowX1 = SCREEN_WIDTH;
layer.WindowY0 = 0;
layer.WindowY1 = SCREEN_HEIGHT;
layer.PixelFormat = LTDC_PIXEL_FORMAT_RGB565;
layer.Alpha = 255;
layer.Alpha0 = 0;
layer.BlendingFactor1 = LTDC_BLENDING_FACTOR1_PAxCA;
layer.BlendingFactor2 = LTDC_BLENDING_FACTOR2_PAxCA;
layer.FBStartAdress = (uint32_t)screen_buffer[0];
layer.ImageWidth = SCREEN_WIDTH;
layer.ImageHeight = SCREEN_HEIGHT;
layer.Backcolor.Red = 0;
layer.Backcolor.Green = 0;
layer.Backcolor.Blue = 0;
HAL_LTDC_ConfigLayer(&hltdc, &layer, LTDC_LAYER_1);
hdma2d.Instance = DMA2D;
}
配置完成后,三维数组screen_buffer就是液晶屏所显示的内容,格式是screen_buffer[缓冲区编号][y坐标][x坐标]。将PB5引脚拉高,打开液晶屏显示。
ARKLCD7_Enable函数通过LCD_BL(PB5)引脚控制液晶屏是否显示图像。设为高电平时开显示。但是要注意,LTDC至少要传输图像150ms后才能开显示,否则开显示瞬间会出现一道白线(正点原子本身的例程就有这个bug) 。
如果显存screen_buffer是由malloc分配而来的,那么一定要执行memset清空整个显存,使整个屏幕变为黑色!
开机时首次绘制图像需要花一定的时间,这能保证背光打开后图像绘制完成前屏幕显示全黑,而不是其他乱七八糟的图像。
开机过程:
(1) 背光关闭状态下初始化并开启LTDC
(2) 延时150ms,开背光,屏幕显示全黑
(3) 开始绘制图像,图像绘制完成后显示出来
void ARKLCD7_Enable(int enabled)
{
if (enabled)
{
// 开显示前必须确保LTDC已经打开并开始传输图像
printf("Screen is enabled!\n");
HAL_Delay(150); // 延时, 让LTDC传输一段时间图像但不显示, 这样可以防止开显示屏的瞬间出现白线
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_SET); // 开显示
}
else
{
printf("Screen is disabled!\n");
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_RESET); // 关显示
}
}
开发板使用的SDRAM是W9825G6KH,容量为32MB。SDRAM的初始化操作由W9825G6KH.c完成。首先系统启动时会在main函数中调用W9825G6KH_Init函数,这个函数主要是初始化SDRAM的GPIO端口(HAL_GPIO_Init),配置好STM32的FMC(HAL_SDRAM_Init),然后发送SDRAM初始化命令(HAL_SDRAM_SendCommand)。初始化完毕后,可通过0xc0000000~0xc1ffffff地址访问SDRAM内存。
SDRAM内存的初始化程序如下:
#include <stm32f4xx.h>
#include "W9825G6KH.h"
SDRAM_HandleTypeDef hsdram;
void W9825G6KH_Init(void)
{
FMC_SDRAM_CommandTypeDef cmd;
FMC_SDRAM_TimingTypeDef timing;
GPIO_InitTypeDef gpio;
__HAL_RCC_FMC_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOD_CLK_ENABLE();
__HAL_RCC_GPIOE_CLK_ENABLE();
__HAL_RCC_GPIOF_CLK_ENABLE();
__HAL_RCC_GPIOG_CLK_ENABLE();
/* 初始化SDRAM引脚 */
// PC0: FMC_SDNWE, PC2: FMC_SDNE0, PC3: FMC_SDCKE0
gpio.Alternate = GPIO_AF12_FMC;
gpio.Mode = GPIO_MODE_AF_PP;
gpio.Pin = GPIO_PIN_0 | GPIO_PIN_2 | GPIO_PIN_3;
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
HAL_GPIO_Init(GPIOC, &gpio);
// PD0~1: FMC_D2~3, PD8~10: FMC_D13~15, PD14~15: FMC_D0~1
gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_14 | GPIO_PIN_15;
HAL_GPIO_Init(GPIOD, &gpio);
// PE0~1: FMC_NBL0~1, PE7~15: FMC_D4~12
gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15;
HAL_GPIO_Init(GPIOE, &gpio);
// PF0~5: FMC_A0~5, PF11: FMC_SDNRAS, PF12~15: FMC_A6~9
gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15;
HAL_GPIO_Init(GPIOF, &gpio);
// PG0~2: FMC_A10~12, PG4~5: FMC_BA0~1, PG8: FMC_SDCLK, PG15: FMC_SDNCAS
gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_8 | GPIO_PIN_15;
HAL_GPIO_Init(GPIOG, &gpio);
/* 初始化SDRAM */
hsdram.Instance = FMC_Bank5_6;
hsdram.Init.SDBank = FMC_SDRAM_BANK1;
hsdram.Init.ColumnBitsNumber = FMC_SDRAM_COLUMN_BITS_NUM_9;
hsdram.Init.RowBitsNumber = FMC_SDRAM_ROW_BITS_NUM_13;
hsdram.Init.MemoryDataWidth = FMC_SDRAM_MEM_BUS_WIDTH_16;
hsdram.Init.InternalBankNumber = FMC_SDRAM_INTERN_BANKS_NUM_4;
hsdram.Init.CASLatency = FMC_SDRAM_CAS_LATENCY_3;
hsdram.Init.WriteProtection = FMC_SDRAM_WRITE_PROTECTION_DISABLE;
hsdram.Init.SDClockPeriod = FMC_SDRAM_CLOCK_PERIOD_2;
hsdram.Init.ReadBurst = FMC_SDRAM_RBURST_ENABLE;
hsdram.Init.ReadPipeDelay = FMC_SDRAM_RPIPE_DELAY_0;
timing.LoadToActiveDelay = 1;
timing.ExitSelfRefreshDelay = 1;
timing.SelfRefreshTime = 6;
timing.RowCycleDelay = 4;
timing.WriteRecoveryTime = 1;
timing.RPDelay = 1;
timing.RCDDelay = 1;
HAL_SDRAM_Init(&hsdram, &timing);
cmd.CommandMode = FMC_SDRAM_CMD_CLK_ENABLE;
cmd.CommandTarget = FMC_SDRAM_CMD_TARGET_BANK1;
cmd.AutoRefreshNumber = 1; // 避免assert_failed
cmd.ModeRegisterDefinition = 0;
HAL_SDRAM_SendCommand(&hsdram, &cmd, HAL_MAX_DELAY);
HAL_Delay(1);
// Precharge All
cmd.CommandMode = FMC_SDRAM_CMD_PALL;
HAL_SDRAM_SendCommand(&hsdram, &cmd, HAL_MAX_DELAY);
cmd.CommandMode = FMC_SDRAM_CMD_AUTOREFRESH_MODE;
cmd.AutoRefreshNumber = 8;
HAL_SDRAM_SendCommand(&hsdram, &cmd, HAL_MAX_DELAY);
// Load Mode Register
// CAS latency: 3, Write mode: burst read and single write
cmd.CommandMode = FMC_SDRAM_CMD_LOAD_MODE;
cmd.ModeRegisterDefinition = 0x230; // 参阅10.4 Mode Register Set Cycle
HAL_SDRAM_SendCommand(&hsdram, &cmd, HAL_MAX_DELAY);
HAL_SDRAM_ProgramRefreshRate(&hsdram, 683);
}
液晶屏的I²C接口专门用来传输触控信息,设备地址为0x70。由于正点原子的板子的触控引脚没有接到STM32的I²C引脚上,因此只能GPIO模拟I²C。ARKLCD7_ReadTouchInfo就是通过模拟I²C读取触控信息的函数,读出来的寄存器值是保存到ARKLCD7_TouchInfo结构体里面的。其中count成员代表当前一共有多少个触控点,points数组保存了每个触控点的坐标信息。比如,第一个触控点的X坐标是info.points[0].x & ARKLCD7_X,Y坐标是info.points[0].y & ARKLCD7_Y。其中ARKLCD7_X和ARKLCD7_Y都等于0xfff。之所以要&0xfff,是因为保存x、y坐标的寄存器里面还保存了坐标以外的其他信息。
通过I²C读取触控信息的代码如下:
int ARKLCD7_ReadTouchInfo(ARKLCD7_TouchInfo *info)
{
int i, ret;
ret = ARKLCD7_Read(0, info, sizeof(ARKLCD7_TouchInfo));
if (ret != sizeof(ARKLCD7_TouchInfo))
return -1;
if (info->count > ARKLCD7_MAX_POINTS)
return -1;
for (i = 0; i < info->count; i++)
{
info->points[i].x = htons(info->points[i].x) & ARKLCD7_X;
info->points[i].y = htons(info->points[i].y) & ARKLCD7_Y;
}
return 0;
}
【STemWin移植】
液晶屏采用STemWin图形库来绘制图形,如绘制直线、矩形、圆等图形。STemWin是不开放源代码的库,所以只有编译好的库文件,没有源代码。STemWin可在ST官网HAL库文件包中的STM32Cube_FW_F4_V1.24.0/Middlewares/ST/STemWin文件夹中找到。裸机环境下采用的库文件是STemWin_CM4_wc16.a。如果有操作系统,应该选择STemWin_CM4_OS_wc16.a。添加到Keil工程后,要将文件属性选择为Library file,而不是汇编语言的源文件。
将inc文件夹全部复制出来。Config文件夹中只需要复制GUIConf.c/h、LCDConf_Lin.c/h。最后将GUI_X.c也添加到工程中。
这些文件中,只有GUIConf.c和LCDConf_Lin.c需要修改。(若想要使用GUI_Delay延时函数则还需要修改GUI_X.c)
在GUIConf.c中,将aMemory修改到外部SDRAM内存里面(地址0xc0000000)。这块内存用于图形库的图形处理,分配的大小由GUI_NUMBYTES宏决定。将数组的类型定义为uint32_t而不是uint8_t,可以保证数组的首地址能够被4整除。
static U32 aMemory[GUI_NUMBYTES / 4] __attribute__((at(0xc0000000)));
LCDConf_Lin.c用于将STemWin库和LCD液晶屏绑定起来,里面最重要的语句是LCD_SetVRAMAddrEx(0, (void *)VRAM_ADDR),其中VRAM_ADDR就是screen_buffer,也就是显存的地址。设定了这个地址之后,所有的图形操作都是往这个显存里面写。以下还有一些其他的参数。
屏幕分辨率:
#define XSIZE_PHYS 800
#define YSIZE_PHYS 480
颜色格式:RGB565
#define COLOR_CONVERSION GUICC_M565
驱动类型:
#define DISPLAY_DRIVER GUIDRV_LIN_16
显存地址:
#define VRAM_ADDR screen_buffer
重写一些图形操作函数(可利用DMA2D硬件加速):
//
// Set custom functions for several operations to optimize native processes
//
LCD_SetDevFunc(0, LCD_DEVFUNC_COPYBUFFER, (ARKLCD7_Function)ARKLCD7_CopyBuffer);
LCD_SetDevFunc(0, LCD_DEVFUNC_COPYRECT, (ARKLCD7_Function)ARKLCD7_CopyRect);
//LCD_SetDevFunc(0, LCD_DEVFUNC_FILLRECT, (ARKLCD7_Function)ARKLCD7_FillRect);
//LCD_SetDevFunc(0, LCD_DEVFUNC_DRAWBMP_8BPP, (void(*)(void))CUSTOM_LCD_DrawBitmap8bpp);
//LCD_SetDevFunc(0, LCD_DEVFUNC_DRAWBMP_16BPP, (void(*)(void))CUSTOM_LCD_DrawBitmap16bpp);
GUI_MEMDEV_SetDrawMemdev16bppFunc(ARKLCD7_CopyRectFromMemdev);
还有就是LCD_X_DisplayDriver函数里面,和我们的驱动程序绑定。
GUI_Init函数初始化图形库,执行这个函数前必须要调用__HAL_RCC_CRC_CLK_ENABLE函数打开STM32自带的CRC功能,否则程序会卡死。打开CRC是在ARKLCD7_Init里面进行的。之后就可以用图形库提供的各种API来绘制图形。
同时绘制多个图形时,为了防止看到绘制的中间过程产生的屏幕闪烁,可以在内存中创建一个GUI_MEMDEV,用GUI_MEMDEV_Select选中,在内存中绘制好所有的内容后,再调用GUI_MEMDEV_CopyToLCD一次性显示出来。GUI_MEMDEV还可以用于保存位图的绘制结果,比如位图的缩放或旋转结果,要用的时候直接显示出来,不用再绘制一遍。
MEMDEV能够消除多个图像绘制步骤中产生的屏幕闪烁,但是不能消除拖动图片时屏幕上产生的撕裂现象(tearing effect)。MULTIBUF(多缓冲)方式可以解决这个问题。MULTIBUF需要配合LTDC的垂直同步中断,将画好的内容从内存转移到液晶屏上的操作必须在此中断产生的瞬间完成。图像绘制好了要显示时,调用ARKLCD7_ShowBuffer函数,函数中用HAL_LTDC_ProgramLineEvent开中断,中断产生后,在HAL_LTDC_LineEventCallback函数中用HAL_LTDC_SetAddress直接将显存的地址修改到绘制好的内存地址上,避免复制操作,然后调用GUI_MULTIBUF_Confirm函数通知STemWin图像已完成显示。
【2020年7月24日更新】
旧版本程序中关于GUI_MULTIBUF的问题:
(1) ARKLCD7_ShowBuffer可以利用LTDC自带的垂直同步功能更新显示地址。代码如下:
void ARKLCD7_ShowBuffer(int index)
{
screen_pending_index = index;
HAL_LTDC_SetAddress_NoReload(&hltdc, (uint32_t)screen_buffer[screen_pending_index], LTDC_LAYER_1);
HAL_LTDC_Reload(&hltdc, LTDC_RELOAD_VERTICAL_BLANKING);
while (screen_pending_index != -1);
}
void LTDC_IRQHandler(void)
{
HAL_LTDC_IRQHandler(&hltdc);
}
void HAL_LTDC_ReloadEventCallback(LTDC_HandleTypeDef *hltdc)
{
if (screen_pending_index != -1)
{
GUI_MULTIBUF_Confirm(screen_pending_index);
screen_pending_index = -1;
}
}
程序中还增加了ARKLCD7_WaitForVerticalBlanking函数可用于等待垂直同步信号到来。
(2) 之前LCDConf_Lin.c中定义的LCD_DEVFUNC_COPYRECT和LCD_DEVFUNC_FILLRECT函数实现不正确。
第一个参数是图层号,不是缓冲区号!原来的代码错把图层号当做了缓冲区号,从而在MULTIBUF模式下GUI_Clear和GUI_FillRect等函数无法被GUI_MULTIBUF_Begin和GUI_MULTIBUF_End保护起来。
遗憾的是,STemWin的MULTIBUF并没有提供获取当前缓冲区号的API函数,而执不执行GUI_MULTIBUF_Begin,当前缓冲区号的值是不一样的。比如:
GUI_FillRect(0, 0, 100, 100); // 当前缓冲区号为0
GUI_MULTIBUF_Begin();
GUI_MULTIBUF_End();
但是
GUI_MULTIBUF_Begin();
GUI_FillRect(0, 0, 100, 100); // 当前缓冲区号为1
GUI_MULTIBUF_End();
那在MULTIBUF模式下我们怎么才能知道当前到底该往哪个缓冲区写数据呢?
在LCDConf_Lin.c的LCD_X_Config函数中,我们绑定了3个自定义函数:
typedef void (*ARKLCD7_Function)(void);
LCD_SetDevFunc(0, LCD_DEVFUNC_COPYBUFFER, (ARKLCD7_Function)ARKLCD7_CopyBuffer);
LCD_SetDevFunc(0, LCD_DEVFUNC_COPYRECT, (ARKLCD7_Function)ARKLCD7_CopyRect);
LCD_SetDevFunc(0, LCD_DEVFUNC_FILLRECT, (ARKLCD7_Function)ARKLCD7_FillRect);
这里ARKLCD7_CopyBuffer就是关键所在。我们在MULTIBUF模式下使用GUI_MULTIBUF_Begin和GUI_MULTIBUF_End这两个函数,都会调用我们自定义的ARKLCD7_CopyBuffer函数,此函数的第三个参数dest正是当前缓冲区号的值。
MULTIBUF的工作原理是,调用GUI_MULTIBUF_Begin函数时会将显示器上显示的内容复制到绘图用的缓冲区中,绘制完了之后又调用GUI_MULTIBUF_End函数将绘制好的图像内容复制回屏幕显示出来。缓冲区间复制图像正是调用的我们的ARKLCD7_CopyBuffer函数!
所以解决方案就出来了,我们定义一个名为screen_paint_index的全局变量,每次CopyBuffer的时候,都将dest参数的值保存到全局变量中:
void ARKLCD7_CopyBuffer(int layer, int src, int dest)
{
screen_paint_index = dest; // GUI_MULTIBUF_Begin和GUI_MULTIBUF_End均会触发CopyBuffer函数调用, 可以根据dest参数记录当前绘图使用的缓冲区编号
memcpy(screen_buffer[dest], screen_buffer[src], SCREEN_WIDTH * SCREEN_HEIGHT * sizeof(uint16_t));
}
这样我们就知道CopyRect和FillRect该往哪个缓冲区写数据了:
void ARKLCD7_CopyRect(int layer, int x0, int y0, int x1, int y1, int xsize, int ysize)
{
//...
HAL_DMA2D_Start(&hdma2d, (uint32_t)&screen_buffer[screen_paint_index][y0][x0], (uint32_t)&screen_buffer[screen_paint_index][y1][x1], xsize, ysize);
//...
}
void ARKLCD7_FillRect(int layer, int x0, int y0, int x1, int y1, uint32_t color)
{
//...
HAL_DMA2D_Start(&hdma2d, color, (uint32_t)&screen_buffer[screen_paint_index][y0][x0], width, height);
//...
}
在LCD_X_DisplayDriver函数中,我们还通过LCD_X_SHOWBUFFER绑定了ARKLCD7_ShowBuffer函数,该函数会在调用GUI_MULTIBUF_End时执行,用于切换屏幕显示的缓冲区。
程序里面还定义了一个screen_display_index变量,在ARKLCD7_ShowBuffer的时候赋值,表示当前屏幕上显示的是哪个缓冲区。有了这个变量,我们就可以直接操作screen_buffer数组更新屏幕上的显示内容:
screen_buffer[screen_display_index][y坐标][x坐标] = RGB565颜色值;
如果想通过malloc动态分配screen_buffer缓冲区,我们可以将screen_buffer定义成一个指向二维数组的指针:
uint16_t (*screen_buffer)[SCREEN_HEIGHT][SCREEN_WIDTH];
然后:
size = SCREEN_BUFFER_NUM * SCREEN_HEIGHT * SCREEN_WIDTH * sizeof(uint16_t);
screen_buffer = pvPortMalloc(size);
memset(screen_buffer, 0, size); // 为了防止闪屏, 开机时必须为黑屏
其中pvPortMalloc是FreeRTOS系统里面的动态内存分配函数,可以配置为在外部SDRAM内存(0xc0000000)上分配内存。
这样的话我们就动态分配了一个三维数组出来,还是可以使用screen_buffer[缓冲区号][y坐标][x坐标]这样的语法来访问屏幕上的每个像素。
另外,STemWin里面绘图的函数(如GUI_DrawBitmap)只能绘制整张图片,没有提供绘制图片指定区域的功能。
如果想要截取图片的一部分然后显示到屏幕上,应该配合使用GUI_SetClipRect函数。
下面的示例代码从bmp的(x0,y0)处截取width×height大小的图片,然后显示到屏幕的(x,y)坐标上。
/* 截取一部分位图并显示 */
void copy_part_of_bitmap(int x, int y, const GUI_BITMAP *bmp, int x0, int y0, int width, int height)
{
GUI_RECT rect;
rect.x0 = x;
rect.y0 = y;
rect.x1 = x + width - 1;
rect.y1 = y + height - 1;
GUI_SetClipRect(&rect);
GUI_DrawBitmap(bmp, x - x0, y - y0);
GUI_SetClipRect(NULL);
}
其中的GUI_DrawBitmap函数也可以换成其他的图片显示函数,比如GUI_PNG_Draw函数。
【2020年7月31日更新】
重定义LCD_DEVFUNC_FILLRECT函数将导致正常模式以及MULTIBUF模式下GUI_InvertRect反色函数无法正常工作,MEMDEV模式不受影响。
解决方案是将LCD_SetDevFunc(0, LCD_DEVFUNC_FILLRECT, (ARKLCD7_Function)ARKLCD7_FillRect)这行代码注释掉。