关于STM32F4单片机,使用HAL库自带的SPI,驱动TFTLCD屏幕的资料网上好像不太多,正好最近我做了这项工作,把成果分享给大家。我的代码实现了这些功能:任意坐标画点,指定首尾坐标画线,画方框,指定区域显示彩图,显示16* 16或者12* 12的汉字、ASCII码,并附带ASCII码表与少量的汉字字库。
硬件设计
屏幕选择:使用了一款低成本十六位彩屏,只要十块钱。链接
厂家看到文章请联系我打广告费,哈哈。
虽然用这个屏幕的可能不多,但我了解到,只要其控制芯片是ST7735S,那么程序就应该差不多。不同的地方在于,厂家的封装与玻璃不太一样,玻璃有个伽马值不同,会导致颜色看上去不太一样。
屏幕的引脚信息
我的原理图设计:使用了STM32F405RG芯片的SPI1,屏幕没有MISO。
cubeMX中SPI的配置大致如下:
其实SPI的速度我选的是21MBITS/s,可能再快一点也行,没有测试。
其它引脚比较散,都是当做IO来用,CubeMX中的配置过程就不说了,汇总如下
名称 引脚 功能
LCD_RST PC5 屏幕复位
LCD_CD PB0 0数据1指令
SPI_MOSI PB5 数据线
SPI_CLK PB3 时钟线
LCD_CS PB1 片选,低电平有效
LCD_LED PB2 背光,高电平有效
发送数据与指令的基本函数
在引脚初始化以后,我定义了几个位带操作,方便操作引脚
#define LCD_RST PCout(5)
#define LCD_CD PBout(0)
#define LCD_CS PBout(1)
#define LCD_LED PBout(2)
不论是发送数据还是引脚,我都采用了HAL库提供的现成的SPI发送函数:
很多人在使用STM32的SPI时都用模拟SPI,说STM32的硬件SPI有问题,我暂时没有发现硬件SPI的问题。不过模拟SPI很容易讲清楚原理,按位发送数据,一般写法是这样的:
for(i=0;i<8;i++)
{
if(dat&0x80)
{
SDA=1;
如果你没有使用HAL库,可以把HAL_SPI_Transmit替换掉。
发送数据与指令的区别就在于LCD_CD引脚的电平状态,两个函数如下:
/**
* @brief 向LCD屏幕写一个字节的命令
* @param 命令内容,具体命令可以参照手册
* @retval None
*/
static void LCD_WriteCommand(uint8_t temp)
{
LCD_CD = 0;
LCD_CS = 0;
HAL_SPI_Transmit(&hspi1,&temp, 1, 0xffff);
LCD_CS = 1;
}
/**
* @brief 向LCD屏幕写一个字节的数据
* @param 数据
* @retval None
*/
static void LCD_WriteData(uint8_t temp)
{
LCD_CD = 1;
LCD_CS = 0;
HAL_SPI_Transmit(&hspi1,&temp, 1, 0xffff);
LCD_CS = 1;
}
可以看出来,除了LCD_CD引脚用于切换命令,也需要操作LCD_CS来选中屏幕。个人认为操作过多操作引脚会影响效率,而发送数据的函数应用的十分频繁,特别是对于我们选用的十六位屏幕,每个像素都需要十六位的数据,所以,我们经常用到的功能是发送个十六位的数据。代码可以这么写,调用两次发送8位数据的函数:
static void LCD_WD_U16(u16 temp)
{
LCD_WriteData(temp>>8);
LCD_WriteData(temp);
}
由于要操作两次IO,所以我稍微做了一点优化:
/**
* @brief 向LCD屏幕写两个字节的数据
* @param 16位的数据
* @note 此函数可以直接调用LCD_WriteData两次,但是IO的操作是多余的
* 由于每个图片的数据都是16位的,所以此函数很常用,因此稍作优化,减少操作IO
* @retval None
*/
static void LCD_WD_U16(u16 temp)
{
u8 tempBuf[2];
tempBuf[0] = temp>>8;
tempBuf[1] = temp;
LCD_CD = 1;
LCD_CS = 0;
HAL_SPI_Transmit(&hspi1,tempBuf, 2, 0xffff);
LCD_CS = 1;
}
同理写了一个函数,用于发送数组。彩图数组动辄都是上万位的,并且是连续发送数据,所以也不需要操作多次IO。
/**
* @brief 向LCD屏幕写一个数组的长度
* @param 数组地址与长度
* @note 此函数可以直接调用LCD_WriteData若干次,但是IO的操作是多余的
* 由于每个图片的数据都是16位的很长的数组,所以此函数很常用,因此稍作优化,减少操作IO,一个图片的数组值操作一次IO
* @retval None
*/
static void LCD_WD_buf(uint8_t *pData, uint16_t Size)
{
LCD_CD = 1;
LCD_CS = 0;
HAL_SPI_Transmit(&hspi1,pData, Size, 0xffff);
LCD_CS = 1;
}
初始化与定位
初始化代码太长,就不放了。其实初始化代码是厂家提供的,只不过原来是51程序,我移植了下。
屏幕的显示需要坐标系,定位操作其实就是发个特定的命令,表示设置x/y轴,在发送特定的数据,表示具体位置。操作思路在《ST7735S手册》中都有体现,例如设置列地址:
我们找到了设置列地址的命令,再把自己需要的坐标计算出来,假如全屏显示:
/**
* @brief 设置显示区域为全屏
* @param None
* @retval None
*/
static void Full_Screen(void)
{
LCD_WriteCommand(0x2A); //设置列地址
LCD_WriteData(0x00);
LCD_WriteData(0x02);
LCD_WriteData(0x00);
LCD_WriteData(0x81);
LCD_WriteCommand(0x2B); //设置行地址
LCD_WriteData(0x00);
LCD_WriteData(0x03);
LCD_WriteData(0x00);
LCD_WriteData(0x82);
LCD_WriteCommand(0x2C); //写内存
}
设置某个点的坐标:
/**
* @brief 设置某个点的坐标
* @param 点的横纵坐标
* @note 坐标的起点为(2,3)
* @retval None
*/
static void LCD_SetXY(u16 x,u16 y)
{
LCD_WriteCommand(0x2A); //设置横轴
LCD_WD_U16(x+2);
LCD_WriteCommand(0x2B); //设置纵轴
LCD_WD_U16(y+3);
LCD_WriteCommand(0x2C); //写内存
}
设置某个区域的坐标:
/**
* @brief 设置某个显示区域的坐标
* @param 区域左上角的坐标与右下角的坐标
* @note 坐标的起点为(2,3)
* @retval None
*/
static void LCD_SetArea(u16 x0, u16 y0,u16 x1, u16 y1)
{
LCD_WriteCommand(0x2A); //设置横轴
LCD_WD_U16(x0+2);
LCD_WD_U16(x1+2);
LCD_WriteCommand(0x2B); //设置纵轴
LCD_WD_U16(y0+3);
LCD_WD_U16(y1+3);
LCD_WriteCommand(0x2C); //写内存
}
颜色的确定
所谓十六位真彩色,意思就是每个像素的颜色由十六位决定。我们在初始化函数中设置的是这样分配的:
红色5位,绿色6位,蓝色5位
很容易想到白色的RGB值就是0xffff,黑色是0x0000。其它还有几个颜色的定义如下:
#define RED 0xf800
#define GREEN 0x07e0
#define BLUE 0x001f
#define YELLOW 0xffe0
#define WHITE 0xffff
#define BLACK 0x0000
#define PURPLE 0xf81f
一定要注意,高位在前。有很多取色工具可以帮我们算出某个颜色的RGB值。
画点、线、框
前边已经写了确定点坐标的方法,画点就十分简单了:
/**
* @brief 画一个点
* @param 点的横纵坐标,点的颜色
* @retval None
*/
void LCD_DrawPoint(u16 x,u16 y,u16 color)
{
LCD_SetXY(x,y);
LCD_WD_U16(color);
}
画线函数理论上来讲就是调用多次画点的函数。如果是横平竖直的线,那十分简单了。如果是斜线呢?那就需要考虑斜率了。由于像素是离散的,所以线上的点,我们只处理所谓的整数部分,代码比较复杂,主要是因为整型变量处理四舍五入的小数部分稍微有点吃力。
/**
* @brief 画一条线
* @param 线的起点与终点的横纵坐标,颜色
* @note 可以画斜线
* @retval None
*/
void LCD_DrawLine(u16 x0, u16 y0,u16 x1, u16 y1,u16 Color)
{
int dx, // x轴上的距离
dy, // y轴上的距离
dx2, // 计算坐标的临时变量
dy2,
x_inc, // inc表示点的“生长方向” x_inc>1代表从左向右
y_inc, // inc表示点的“生长方向” x_inc>1代表从上向下(左上角是坐标原点)
error, // 由于坐标点只有整数,是离散的不是连续的,需要变量用于四舍五入的计算
index;
LCD_SetXY(x0,y0);
dx = x1-x0;//计算x距离
dy = y1-y0;//计算y距离
if (dx>=0)
{
x_inc = 1;
}
else
{
x_inc = -1;
dx = -dx;
}
if (dy>=0)
{
y_inc = 1;
}
else
{
y_inc = -1;
dy = -dy;
}
dx2 = dx << 1; //相当于乘以2,如此一来,四舍五入的误差就变成了不到1舍,大于1入
dy2 = dy << 1;
if (dx > dy)//x距离大于y距离,那么对于每个x轴上只有一个点,每个y轴可能只有半个点
{
error = dy2 - dx;
for (index=0; index <= dx; index++)//要画的点数不会超过x距离
{
LCD_DrawPoint(x0,y0,Color);
if (error >= 0) //如果error>0 说明真实的y的误差>0.5了,实际上应该+1了
{
error-=dx2;
y0+=y_inc;//增加y坐标值
}
error+=dy2;
x0+=x_inc;//x坐标值每次画点后都递增1
}
}
else
{
error = dx2 - dy;
for (index=0; index <= dy; index++)
{
LCD_DrawPoint(x0,y0,Color);
if (error >= 0)
{
error-=dy2;
x0+=x_inc;
}
error+=dx2;
y0+=y_inc;
}
}
}
由于界面中,我们常常需要划一个方框,或者称之为“按钮”,所以我又封装了一个函数:
/**
* @brief 画一个方框,或者称之为按钮
* @pa
『本文转载自网络,版权归原作者所有,如有侵权请联系删除』