零、目录
一、硬件和参考对象
二、STC32G 的 LCM
三、简要的初始化流程
四、代码的移植和修改
五、引脚接线
六、运行结果
七、总结和注意事项
STC32G 系列单片机内部集成了一个 LCM 接口控制器,可驱动 I8080 接口和 M6800 接口,支持 8位 和 16位 数据宽度。
本文将使用到 STC 官方提供的库函数来控制 LCM,对于 LCM 的初始化,将使用库文件: STC32G_LCM.H;LCM 的功能引脚切换,将使用库文件:STC32G_Switch().H。
STC32G 的库函数可以在 STC32G 官网下载。对于 LCM,STC32G_LCM.H 的主要内容如下:
/* 引脚定义 */
sbit LCD_RS = P4^5; // 数据/命令切换
sbit LCD_RD = P4^4; // 读控制
sbit LCD_WR = P4^2; // 写控制
sbit LCD_CS = P4^0; // 片选
sbit LCD_RESET = P4^3; // 复位
sbit LCD_BK = P4^1; // 背光控制
/* 数据/指令触发宏定义 */
#define LCM_WRITE_CMD() LCMIFCR = ((LCMIFCR & ~0x07) | 0x84)
#define LCM_WRITE_DAT() LCMIFCR = ((LCMIFCR & ~0x07) | 0x85)
#define LCM_READ_CMD() LCMIFCR = ((LCMIFCR & ~0x07) | 0x86)
#define LCM_READ_DAT() LCMIFCR = ((LCMIFCR & ~0x07) | 0x87)
/* 接口类型和数据宽度类型 */
#define MODE_I8080 0 //I8080模式
#define MODE_M6800 1 //M6800模式
#define BIT_WIDE_8 0 //8位数据宽度
#define BIT_WIDE_16 2 //16位数据宽度
/* 初始化结构体 */
typedef struct
{
u8 LCM_Enable; //LCM接口使能 ENABLE,DISABLE
u8 LCM_Mode; //LCM接口模式 MODE_I8080,MODE_M6800
u8 LCM_Bit_Wide; //LCM数据宽度 BIT_WIDE_8,BIT_WIDE_16
u8 LCM_Setup_Time; //LCM通信数据建立时间 0~7
u8 LCM_Hold_Time; //LCM通信数据保持时间 0~3
} LCM_InitTypeDef;
/* 接口函数 */
void LCM_Inilize(LCM_InitTypeDef *LCM);
LCD_RS、LCD_RD、LCD_WR 的引脚定义受到 LCM 寄存器的控制,具体设置可参考官方手册,如下图所示。本文使用 LCMIFCPS[1:0] = 00 时的功能引脚。
LCD_CS、LCD_RESET、LCD_BK 可任意定义,主要用于 LCD 的片选、复位和背光控制。
触发数据和指令的宏定义用于触发 LCM 模块将数据寄存器中的数据通过端口发送给 LCD,仅用于触发,数据需要在触发前写入数据寄存器。
接口类型和数据宽度类型将在 LCM 的初始化结构体里被使用,本次使用的 LCD 的类型是 I8080 接口 和 16位数据宽度。16位 LCM 接口数据脚选择参考官方手册,如下图所示。本文使用的是 LCMIFDPS[1:0] = 11 时的引脚。
LCM 结构体公有 5个成员函数,STC 在注释中给出了可设置的范围。结构体配置完成后通过函数 LCM_Inilize() 对 LCM 进行初始化。
功能引脚切换使用 STC32G_Switch() 中提供的宏定义:
#define LCM_CTRL_SW(Pin) LCMIFCFG2 = (LCMIFCFG2 & ~0x60) | (Pin << 5)
#define LCM_DATA_SW(Pin) LCMIFCFG = (LCMIFCFG & ~0x0C) | (Pin << 2)
STM32 | STC32G | |
GPIO | 都是对需要的 GPIO 进行初始化 | |
驱动 LCD 的外设 |
采用 FSMC 控制 | 采用 LCM 控制 |
液晶屏配置 | 对于液晶屏的配置函数,只要基本的读写函数能够实现,基本不需要做修改 | |
读写函数 | 基于 FSMC 的读写触发 | 基于 LCM 的读写触发 |
LCD 控制引脚 | 通过 FSMC ,自动控制的对应的引脚实现控制 | 需要用户来手动控制引脚的高低电平 |
提前准备好需要的库函数和驱动程序,本文的中断函数将使用 STC32G 库函数自带 ISR 函数。具体使用到的有关文件请参考下表:
STC32G 库函数 | STM32 参考程序 |
STC32G_Delay STC32G_GPIO STC32G_LCM STC32G_NVIC STC32G_SWITCH STC32G_UART |
bsp_ili9341_lcd font |
创建 KEIL C251 项目,先将 STC32G 的文件导入到项目中,并实现通过串口发送 Hello World 的程序,以此确保 STC32G 的库函数移植无误。
接下来添加 STM32 的液晶屏驱动程序和字模 到工程项目中。在 bsp_ili9341_lcd.h 中删除有关 FSMC 的所有定义,并针对 bsp_ili9341_lcd.c 的需要,补充基本的类型定义,并包含下表所需要的头文件:
头文件 | 作用 |
STC32G_GPIO | 对引脚进行初始化 |
STC32G_LCM | 提供对 LCM 的控制 |
STC32G_NVIC | 中断嵌套配置,用于配置外设中断 |
STC32G_Delay | 延时函数 |
STC32G_Switch | 提供功能引脚切换 |
接下来对需要修改的函数进行重定义。
需要重定义的函数 | 函数功能 |
ILI9341_Write_Cmd() | 向 ILI9341 写指令 |
ILI9341_Write_Data() | 向 ILI9341 写数据 |
ILI9341_Read_Data() | 向 ILI9341 读数据 |
ILI9341_Delay() |
延时函数 |
ILI9341_GPIO_Config() |
GPIO 初始化 |
ILI9341_FSMC_Config() |
LCM 初始化 |
ILI9341_Init() |
初始化外设并对液晶屏进行基本配置 |
ILI9341_BackLed_Control() |
背光控制 |
ILI9341_Rst() |
复位控制 |
首先对 ILI9341_Write_Cmd()、ILI9341_Write_Data()、ILI9341_Read_Data() 和 ILI9341_Delay() 进行重定义,如下:
/**
* @brief 向ILI9341写入命令
* @param usCmd :要写入的命令(表寄存器地址)
* @retval 无
*/
void ILI9341_Write_Cmd(uint16_t usCmd)
{
//* ( __IO uint16_t * ) ( FSMC_Addr_ILI9341_CMD ) = usCmd;
LCD_RS = 0;
LCMIFDATL = usCmd & 0x00ff;
LCMIFDATH = (usCmd >> 8) & 0x00ff;
LCM_WRITE_CMD();
}
/**
* @brief 向ILI9341写入数据
* @param usData :要写入的数据
* @retval 无
*/
void ILI9341_Write_Data(uint16_t usData)
{
//* ( __IO uint16_t * ) ( FSMC_Addr_ILI9341_DATA ) = usData;
LCD_RS = 0;
LCMIFDATL = usData & 0x00ff;
LCMIFDATH = (usData >> 8) & 0x00ff;
LCM_WRITE_DAT();
}
/**
* @brief 从ILI9341读取数据
* @param 无
* @retval 读取到的数据
*/
uint16_t ILI9341_Read_Data(void)
{
// return ( * ( __IO uint16_t * ) ( FSMC_Addr_ILI9341_DATA ) );
LCD_RS = 1;
LCM_READ_DAT();
return (LCMIFDATH << 8) | LCMIFDATL;
}
/**
* @brief 用于 ILI9341 简单延时函数
* @param nCount :延时计数值
* @retval 无
*/
static void ILI9341_Delay(uint16_t ms)
{
delay_ms(ms);
}
重定义 GPIO 初始化函数,具体的初始化端口依据实际用到的端口来决定。
/**
* @brief 初始化ILI9341的IO引脚
* @param 无
* @retval 无
*/
static void ILI9341_GPIO_Config(void)
{
GPIO_InitTypeDef gpio;
// 选用 P7 P6 作为数据/指令发送信号线,16线制
gpio.Mode = GPIO_PullUp;
gpio.Pin = GPIO_Pin_All;
GPIO_Inilize(GPIO_P6, &gpio);
GPIO_Inilize(GPIO_P7, &gpio);
// LCD 控制信号线配置
gpio.Pin = GPIO_Pin_All;
GPIO_Inilize(GPIO_P4, &gpio);
}
重定义 LCM 初始化函数,此处函数名没有修改。
/**
* @brief LCD FSMC 模式配置
* @param 无
* @retval 无
*/
static void ILI9341_FSMC_Config(void)
{
LCM_InitTypeDef lcm;
lcm.LCM_Bit_Wide = BIT_WIDE_16;
lcm.LCM_Enable = ENABLE;
lcm.LCM_Hold_Time = 1;
lcm.LCM_Setup_Time = 1;
lcm.LCM_Mode = MODE_I8080;
LCM_Inilize(&lcm);
NVIC_LCM_Init(ENABLE, Priority_1);
LCM_CTRL_SW(LCM_CTRL_P45_P44_P42);
LCM_DATA_SW(LCM_D16_P6_P7);
}
重定义 ILI9341 复位函数。
/**
* @brief ILI9341G 软件复位
* @param 无
* @retval 无
*/
void ILI9341_Rst(void)
{
// digitalL( GPIOE,GPIO_PIN_1); //低电平复位
LCD_RESET = 0;
ILI9341_Delay(50);
// digitalH( GPIOE,GPIO_PIN_1);
LCD_RESET = 1;
ILI9341_Delay(50);
}
重定义 ILI9341 背光控制函数。
/**
* @brief ILI9341G背光LED控制
* @param enumState :决定是否使能背光LED
* 该参数为以下值之一:
* @arg ENABLE :使能背光LED
* @arg DISABLE :禁用背光LED
* @retval 无
*/
void ILI9341_BackLed_Control(FunctionalState enumState)
{
if (enumState)
{
// digitalL( GPIOD, GPIO_PIN_12);
LCD_BK = 0; // 用的 PNP 型三极管做开关,因此低电平点亮
}
else
{
// digitalH( GPIOD, GPIO_PIN_12);
LCD_BK = 1;
}
}
在 ILI9341 初始化函数中补上 LCD 片选控制。
/**
* @brief ILI9341初始化函数,如果要用到lcd,一定要调用这个函数
* @param 无
* @retval 无
*/
void ILI9341_Init(void)
{
ILI9341_GPIO_Config();
ILI9341_FSMC_Config();
LCD_CS = 0;
ILI9341_BackLed_Control(ENABLE); // 点亮LCD背光灯
ILI9341_Rst();
ILI9341_REG_Config();
// 设置默认扫描方向,其中 6 模式为大部分液晶例程的默认显示方向
ILI9341_GramScan(LCD_SCAN_MODE);
ILI9341_Clear(0, 0, ILI9341_LESS_PIXEL, ILI9341_MORE_PIXEL);
}
将 bsp_ili9341_lcd.h 包含到 main.c 中。编写测试程序。其中 gpio_init() 和 uart_init() 是针对串口输出做的初始化。
void main(void)
{
__STC32G_DRIVER_INIT();
gpio_init();
uart_init();
ILI9341_Init();
LCD_SetTextColor(BLACK);
ILI9341_DrawRectangle(0, 0, ILI9341_LESS_PIXEL, ILI9341_MORE_PIXEL, 1);
LCD_SetFont(&Font8x16);
LCD_SetColors(YELLOW, BLACK);
EA = 1;
while(1)
{
printf("Hello world\r\n");
LCD_SetColors(YELLOW, BLACK);
ILI9341_DispStringLine_EN(LINE(0), "Hello LCM I8080!");
delay_ms(1000);
// 画线
LCD_ClearLine(LINE(4)); /* 清除单行文字 */
LCD_SetColors(YELLOW, BLACK);
ILI9341_DispStringLine_EN(LINE(4), "Draw line:");
LCD_SetColors(RED, BLACK);
ILI9341_DrawLine(50, 170, 210, 230);
ILI9341_DrawLine(50, 200, 210, 240);
LCD_SetColors(GREEN, BLACK);
ILI9341_DrawLine(100, 170, 200, 230);
ILI9341_DrawLine(200, 200, 220, 240);
LCD_SetColors(BLUE, BLACK);
ILI9341_DrawLine(110, 170, 110, 230);
ILI9341_DrawLine(130, 200, 220, 240);
delay_ms(2500);
ILI9341_Clear(0, 16 * 8, LCD_X_LENGTH, LCD_Y_LENGTH - 16 * 8); /* 清屏,显示全黑 */
// 画矩形
LCD_ClearLine(LINE(4)); /* 清除单行文字 */
LCD_SetColors(GREEN, BLACK);
ILI9341_DispStringLine_EN(LINE(4), "Draw Rectangle:");
LCD_SetColors(RED, BLACK);
ILI9341_DrawRectangle(50, 200, 100, 30, 1);
LCD_SetColors(GREEN, BLACK);
ILI9341_DrawRectangle(160, 200, 20, 40, 0);
LCD_SetColors(BLUE, BLACK);
ILI9341_DrawRectangle(170, 200, 50, 20, 1);
delay_ms(2500);
ILI9341_Clear(0, 16 * 8, LCD_X_LENGTH, LCD_Y_LENGTH - 16 * 8); /* 清屏,显示全黑 */
// 画圆
LCD_ClearLine(LINE(4)); /* 清除单行文字 */
LCD_SetColors(GREEN, BLACK);
ILI9341_DispStringLine_EN(LINE(4), "Draw Circle:");
LCD_SetColors(RED, BLACK);
ILI9341_DrawCircle(100, 200, 20, 0);
LCD_SetColors(GREEN, BLACK);
ILI9341_DrawCircle(100, 200, 10, 1);
LCD_SetColors(BLUE, BLACK);
ILI9341_DrawCircle(140, 200, 20, 0);
delay_ms(2500);
ILI9341_Clear(0, 0, LCD_X_LENGTH, LCD_Y_LENGTH); /* 清屏,显示全黑 */
}
}
编译项目,若无报错则说明文件移植没有语法错误。
已经接线对照表如下。
实物接线图如下。
烧录后,液晶屏正常点亮并显示测试程序:首先显示字符串 “Hello LCM I8080”,然后显示字符串“Draw Line”,绘制测试直线,之后显示字符串“Draw Rectangle”,绘制测试矩形,最后显示字符串“Draw Circle”,绘制测试圆形。运行结果如下图所示。
1. BUG 汇总
问题 | 可能的原因 | 解决方法 |
程序卡死 | 未开启外设或中断 | 1. 使能外设 2. 开启中断,STC32G 的库函数开启外设中断的方式是使用 NVIC 提供的接口 |
未定义正确的中断函数或未清空标志位 | 本文使用 STC32G 库函数提供的默认中断函数,可用于参考 | |
使用 printf 时未正确指定串口 | 确定 printf 使用的串口是连接到电脑端的串口 | |
未正确初始化相关外设 | 确认 GPIO、LCM、UART 等外设初始化正确 | |
串口有输出,屏幕没有点亮 | LCB_BK 接线不正确或电平值不正确 | 检查 LCB_BK 电平值是否正确,或检查引脚是否接线正确 |
可能是因为片选引脚 LCB_CS 电平不对 | LCB_CS 处于低电平时 LCD 被选中 | |
GPIO 未正确初始化 | 检查 GPIO 的引脚配置是否正确 | |
屏幕点亮,没有显示字符串和图像 | 接线错误或未正确指定功能引脚 | 检查接线是否正常,检查是否进行了正确的功能引脚切换 |
驱动程序未正确重定义 | 检查需要重定义的函数是否都正确重定义 | |
数据建立时间和数据保存时间数值不正确 | 初始化 LCM 时,依据实际情况赋值进行测试,笔者的两个值都为 1 | |
清屏函数仅清屏部分区域 | ILI9341_FillColor() 的传参不正确 |
ILI9341_FillColor() 第一个参数是 uint32_t 类型,调用该函数时,需要将参数进行强制类型转换到 uint32_t,建议将 bsp_ili9341_lcd.c 中所有用到 ILI9341_FillColor() 的地方都手动补上强制类型转换的语句。 |
屏幕输出字符串像素点不完整 | 数据建立时间和数据保存时间数值不正确 | 初始化 LCM 时,依据实际情况赋值进行测试,笔者的两个值都为 1 |
填充区块时出现白屏闪烁问题 | 数据建立时间和数据保存时间数值不正确 | 初始化 LCM 时,依据实际情况赋值进行测试,笔者的两个值都为 1 |
调用了 ILI9341_FillColor() | 该问题目前暂未解决,除了黑色填充外,其他颜色进行填充时都会导致白色屏闪的情况,推测可能和 STC32G 的执行速度有关系。 |
2. LCM 使用感受
STC32G 提供的 LCM 对驱动 TFTLCD 提供了便利的帮助,官方库函数的提供让使用这一外设变得更加简单。STC8A8K64D4 也具有 LCM 功能,通用官方也提供了相应的库函数,而 STC8A8K64D4 的时钟频率能调节到 45MHz,比 STC32G 在 ISP 上能调节的范围更广,也许用起来会比 STC32G 更好吧(可能)。
可惜适配 51 的 GUI 太少了,虽然 STC 完成了 FreeRTOS 的移植(据说可以尝试 uC/GUI,没试过不清楚)。