该LCD分辨率为240 * 320. 与单片机连接需要接以下引脚
- GND: 地线
- 3.3V: 3.3V供电
- BL:背光信号,可以使用PWM调节背光亮度
- SCK: SPI时钟信号
- RST: LCD Reset引脚
- CS: SPI 片选引脚,如果只接一个SPI设备,该引脚可以直接接地
- A0:命令/数据选择引脚
- SDI: SPI数据输入引脚,MOSI
- SDO: SPI数据输出引脚,MISO,如果不读取液晶屏的数据,这个引脚可以不接
我们采用STM32F103RB单片机驱动该屏幕,连接好GND和3.3v后,其他引脚接线如下:
-
将BL引脚接到TIM2_CH2引脚 PA1,在STM32CubeMX中设置TIM2 Channel2为PWM Generation。根据我们希望的亮度值,设置Prescalar与ARR(详情见此文:xxxxx)
-
在STM32CubeMX中将SPI2设置为Full Duplex Master(如果不打算读取液晶屏的数据,设成Transmit Only Master也是可以的。)。将PB13脚接到LCD SCK脚,PB15脚接到LCD SDI脚。
-
将PB0接到LCD RST脚, PB1接到LCD A0脚,PB2接到LCD CS脚。并在STM32CubeMX中将这三个引脚的模式都设为Output Push Pull (推挽输出)。输出level High,最大输出速度 High。并将三个引脚分别命名为LCD_RST, LCD_A0, LCD_CS
完成以上设置于接线之后,我们就可以生成代码并实现相关驱动的代码了,部分关键代码如下所示:
#define LCD_ORIENTATION_LANDSCAPE 0x28
#define LCD_ORIENTATION_PORTRAIT 0x48
#define LCD_BASE_WIDTH 240
#define LCD_BASE_HEIGHT 320
#define LCD_ORIENTATION LCD_ORIENTATION_PORTRAIT
//默认是竖屏,所以宽度240,高度320,当横屏时,需要交换设置
#define LCD_WIDTH (LCD_ORIENTATION == LCD_ORIENTATION_PORTRAIT ? LCD_BASE_WIDTH : LCD_BASE_HEIGHT)
#define LCD_HEIGHT (LCD_ORIENTATION == LCD_ORIENTATION_PORTRAIT ? LCD_BASE_HEIGHT : LCD_BASE_WIDTH)
#define LCD_MODE_DATA() HAL_GPIO_WritePin(LCD_A0_GPIO_Port, LCD_A0_Pin, GPIO_PIN_SET)
#define LCD_MODE_CMD() HAL_GPIO_WritePin(LCD_A0_GPIO_Port, LCD_A0_Pin, GPIO_PIN_RESET)
#define LCD_CHIP_SELECT() HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_RESET)
#define LCD_CHIP_UNSELECT() HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_SET)
// 通过拉低LCD_RST引脚实现LCD硬复位
void LCD_Hard_Reset() {
HAL_GPIO_WritePin(LCD_RST_GPIO_Port, LCD_RST_Pin, GPIO_PIN_RESET);
HAL_Delay(20);
HAL_GPIO_WritePin(LCD_RST_GPIO_Port, LCD_RST_Pin, GPIO_PIN_SET);
HAL_Delay(120);
}
// 发送一个字节的命令
void LCD_Send_Cmd(uint8_t cmd) {
LCD_CHIP_SELECT();
LCD_MODE_CMD();
HAL_SPI_Transmit(&SPI_HANDLE, &cmd, 1, 1000);
LCD_CHIP_UNSELECT();
}
// 发送len个字节的数据
void LCD_Send_Data(uint8_t *data, size_t len) {
LCD_CHIP_SELECT();
LCD_MODE_DATA();
HAL_SPI_Transmit(&SPI_HANDLE, data, len, 1000);
LCD_CHIP_UNSELECT();
}
// 设置屏幕方向,横屏传入0x28,竖屏传入0x48
void LCD_Set_Oritention(uint8_t ori) {
LCD_Send_Cmd(0x36);
LCD_Send_Data(&ori, 1);
}
// 设置操作区的大小
void LCD_Set_Window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) {
LCD_Send_Cmd(0x2a);
LCD_Send_Data((uint8_t[]){x0 >> 8, x0 & 0xff, x1 >> 8, x1 & 0xff}, 4);
LCD_Send_Cmd(0x2b);
LCD_Send_Data((uint8_t[]){y0 >> 8, y0 & 0xff, y1 >> 8, y1 & 0xff}, 4);
}
// 用color颜色填充整个屏幕,这里不需要每个点指定位置,只要发送足够的数据,芯片会自动根据从上到下,从左到右的顺序绘制操作区的每个点
void LCD_Clear(uint16_t color) {
LCD_Set_Window(0, 0, LCD_WIDTH - 1, LCD_HEIGHT - 1);
LCD_Send_Cmd(0x2c);
uint8_t color_data[2];
color_data[0] = color >> 8; // 高字节
color_data[1] = color & 0xFF; // 低字节
for (int i = 0; i < LCD_WIDTH * LCD_HEIGHT; i++) {
LCD_Send_Data(color_data, 2);
}
}
// LCD初始化
void LCD_Init() {
LCD_Send_Cmd(0x3A);
LCD_Send_Data((uint8_t[]){0x55}, 1); // 设置成RGB565格式,默认为RGB666
LCD_Send_Cmd(0xB1); // 设置帧率
LCD_Send_Data((uint8_t[]){0x00, 0x15}, 2); // 默认为0x1B 70Hz, 会有肉眼可见的闪烁,可以设高一点,这里设成了0x15, 90Hz
LCD_Set_Oritention(LCD_ORIENTATION); // 根据需要设置横屏或竖屏
LCD_Clear(DEFAULT_BACKGROUND_COLOR); //清屏
// 退出睡眠模式
LCD_Send_Cmd(0x11);
HAL_Delay(10); // 手册要求至少delay 10ms
// 开启显示
LCD_Send_Cmd(0x29);
}
完成以上方法之后,在main.c中,调用
LCD_Reset();
LCD_Init();
我们就可以在屏幕上看到按照DEFAULT_BACKGROUND_COLOR颜色点亮的屏幕了。
其他显示文字、图像、或者线条的方法,就是通过发送0x2c命令填充像素。如:
// 画一个点,现将操作区设置为x, y坐标点,然后发送0x2c命令,跟上一个2字节的颜色数据,就能在屏幕的x,y坐标处显示出一个点
void LCD_Draw_Pixel(uint16_t x, uint16_t y, uint16_t color) {
LCD_Set_Window(x, y, x, y);
LCD_Send_Cmd(0x2c);
uint8_t color_data[] = {color >> 8, color & 0xFF};
LCD_Send_Data(color_data, 2);
}
// 画一条线,从(x0, y0)到(x1, y1)两点间画一条直线。利用了之前画点的函数
void LCD_Draw_Line(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) {
uint16_t start_x, start_y, end_x, end_y;
if (x0 < x1) {
start_x = x0;
end_x = x1;
} else {
start_x = x1;
end_x = x0;
}
if (y0 < y1) {
start_y = y0;
end_y = y1;
} else {
start_y = y1;
end_y = y0;
}
for (uint16_t i = start_x; i <= end_x; i++) {
for (uint16_t j = start_y; j <= end_y; j++) {
LCD_Draw_Pixel(i, j, color);
}
}
}
完整项目工程在这里:https://github.com/sqlxx/stm32-workshop/ (lcd_driver分支)