关于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)
- 1
- 2
- 3
- 4
不论是发送数据还是引脚,我都采用了HAL库提供的现成的SPI发送函数:
HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout)
- 1
很多人在使用STM32的SPI时都用模拟SPI,说STM32的硬件SPI有问题,我暂时没有发现硬件SPI的问题。不过模拟SPI很容易讲清楚原理,按位发送数据,一般写法是这样的:
for(i=0;i<8;i++)
{
if(dat&0x80)
{
SDA=1;
- 1
- 2
- 3
- 4
- 5
如果你没有使用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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
可以看出来,除了LCD_CD引脚用于切换命令,也需要操作LCD_CS来选中屏幕。个人认为操作过多操作引脚会影响效率,而发送数据的函数应用的十分频繁,特别是对于我们选用的十六位屏幕,每个像素都需要十六位的数据,所以,我们经常用到的功能是发送个十六位的数据。代码可以这么写,调用两次发送8位数据的函数:
static void LCD_WD_U16(u16 temp)
{
LCD_WriteData(temp>>8);
LCD_WriteData(temp);
}
- 1
- 2
- 3
- 4
- 5
由于要操作两次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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
同理写了一个函数,用于发送数组。彩图数组动辄都是上万位的,并且是连续发送数据,所以也不需要操作多次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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
初始化与定位
初始化代码太长,就不放了。其实初始化代码是厂家提供的,只不过原来是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);
<span class="token function">LCD_WriteCommand</span><span class="token punctuation">(</span><span class="token number">0x2B</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//设置行地址</span>
<span class="token function">LCD_WriteData</span><span class="token punctuation">(</span><span class="token number">0x00</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">LCD_WriteData</span><span class="token punctuation">(</span><span class="token number">0x03</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">LCD_WriteData</span><span class="token punctuation">(</span><span class="token number">0x00</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">LCD_WriteData</span><span class="token punctuation">(</span><span class="token number">0x82</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">LCD_WriteCommand</span><span class="token punctuation">(</span><span class="token number">0x2C</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//写内存</span>
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
设置某个点的坐标:
/**
* @brief 设置某个点的坐标
* @param 点的横纵坐标
* @note 坐标的起点为(2,3)
* @retval None
*/
static void LCD_SetXY(u16 x,u16 y)
{
LCD_WriteCommand(0x2A); //设置横轴
LCD_WD_U16(x+2);
<span class="token function">LCD_WriteCommand</span><span class="token punctuation">(</span><span class="token number">0x2B</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//设置纵轴</span>
<span class="token function">LCD_WD_U16</span><span class="token punctuation">(</span>y<span class="token operator">+</span><span class="token number">3</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">LCD_WriteCommand</span><span class="token punctuation">(</span><span class="token number">0x2C</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//写内存</span>
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
设置某个区域的坐标:
/**
* @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);
<span class="token function">LCD_WriteCommand</span><span class="token punctuation">(</span><span class="token number">0x2B</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//设置纵轴</span>
<span class="token function">LCD_WD_U16</span><span class="token punctuation">(</span>y0<span class="token operator">+</span><span class="token number">3</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">LCD_WD_U16</span><span class="token punctuation">(</span>y1<span class="token operator">+</span><span class="token number">3</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">LCD_WriteCommand</span><span class="token punctuation">(</span><span class="token number">0x2C</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//写内存</span>
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
颜色的确定
所谓十六位真彩色,意思就是每个像素的颜色由十六位决定。我们在初始化函数中设置的是这样分配的:
红色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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
一定要注意,高位在前。有很多取色工具可以帮我们算出某个颜色的RGB值。
画点、线、框
前边已经写了确定点坐标的方法,画点就十分简单了:
/**
* @brief 画一个点
* @param 点的横纵坐标,点的颜色
* @retval None
*/
void LCD_DrawPoint(u16 x,u16 y,u16 color)
{
LCD_SetXY(x,y);
LCD_WD_U16(color);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
画线函数理论上来讲就是调用多次画点的函数。如果是横平竖直的线,那十分简单了。如果是斜线呢?那就需要考虑斜率了。由于像素是离散的,所以线上的点,我们只处理所谓的整数部分,代码比较复杂,主要是因为整型变量处理四舍五入的小数部分稍微有点吃力。
/**
* @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距离
<span class="token keyword">if</span> <span class="token punctuation">(</span>dx<span class="token operator">>=</span><span class="token number">0</span><span class="token punctuation">)</span>
<span class="token punctuation">{<!-- --></span>
x_inc <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">else</span>
<span class="token punctuation">{<!-- --></span>
x_inc <span class="token operator">=</span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">;</span>
dx <span class="token operator">=</span> <span class="token operator">-</span>dx<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>dy<span class="token operator">>=</span><span class="token number">0</span><span class="token punctuation">)</span>
<span class="token punctuation">{<!-- --></span>
y_inc <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">else</span>
<span class="token punctuation">{<!-- --></span>
y_inc <span class="token operator">=</span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">;</span>
dy <span class="token operator">=</span> <span class="token operator">-</span>dy<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
dx2 <span class="token operator">=</span> dx <span class="token operator"><<</span> <span class="token number">1</span><span class="token punctuation">;</span> <span class="token comment">//相当于乘以2,如此一来,四舍五入的误差就变成了不到1舍,大于1入</span>
dy2 <span class="token operator">=</span> dy <span class="token operator"><<</span> <span class="token number">1</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>dx <span class="token operator">></span> dy<span class="token punctuation">)</span><span class="token comment">//x距离大于y距离,那么对于每个x轴上只有一个点,每个y轴可能只有半个点</span>
<span class="token punctuation">{<!-- --></span>
error <span class="token operator">=</span> dy2 <span class="token operator">-</span> dx<span class="token punctuation">;</span>
<span class="token keyword">for</span> <span class="token punctuation">(</span>index<span class="token operator">=</span><span class="token number">0</span><span class="token punctuation">;</span> index <span class="token operator"><=</span> dx<span class="token punctuation">;</span> index<span class="token operator">++</span><span class="token punctuation">)</span><span class="token comment">//要画的点数不会超过x距离</span>
<span class="token punctuation">{<!-- --></span>
<span class="token function">LCD_DrawPoint</span><span class="token punctuation">(</span>x0<span class="token punctuation">,</span>y0<span class="token punctuation">,</span>Color<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>error <span class="token operator">>=</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token comment">//如果error>0 说明真实的y的误差>0.5了,实际上应该+1了</span>
<span class="token punctuation">{<!-- --></span>
error<span class="token operator">-</span><span class="token operator">=</span>dx2<span class="token punctuation">;</span>
y0<span class="token operator">+</span><span class="token operator">=</span>y_inc<span class="token punctuation">;</span><span class="token comment">//增加y坐标值</span>
<span class="token punctuation">}</span>
error<span class="token operator">+</span><span class="token operator">=</span>dy2<span class="token punctuation">;</span>
x0<span class="token operator">+</span><span class="token operator">=</span>x_inc<span class="token punctuation">;</span><span class="token comment">//x坐标值每次画点后都递增1</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token keyword">else</span>
<span class="token punctuation">{<!-- --></span>
error <span class="token operator">=</span> dx2 <span class="token operator">-</span> dy<span class="token punctuation">;</span>
<span class="token keyword">for</span> <span class="token punctuation">(</span>index<span class="token operator">=</span><span class="token number">0</span><span class="token punctuation">;</span> index <span class="token operator"><=</span> dy<span class="token punctuation">;</span> index<span class="token operator">++</span><span class="token punctuation">)</span>
<span class="token punctuation">{<!-- --></span>
<span class="token function">LCD_DrawPoint</span><span class="token punctuation">(</span>x0<span class="token punctuation">,</span>y0<span class="token punctuation">,</span>Color<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>error <span class="token operator">>=</span> <span class="token number">0</span><span class="token punctuation">)</span>
<span class="token punctuation">{<!-- --></span>
error<span class="token operator">-</span><span class="token operator">=</span>dy2<span class="token punctuation">;</span>
x0<span class="token operator">+</span><span class="token operator">=</span>x_inc<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
error<span class="token operator">+</span><span class="token operator">=</span>dx2<span class="token punctuation">;</span>
y0<span class="token operator">+</span><span class="token operator">=</span>y_inc<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
由于界面中,我们常常需要划一个方框,或者称之为“按钮”,所以我又封装了一个函数:
/**
* @brief 画一个方框,或者称之为按钮
* @param 方框左上角和右下角的点的坐标,颜色
* @note 右边和下边的线自带加粗效果 如需花在屏幕最边缘无加粗效果
* @retval None
*/
void LCD_DrawBTN(u16 x1,u16 y1,u16 x2,u16 y2,u16 Color)
{
LCD_DrawLine(x1, y1, x2,y1, Color); //H
LCD_DrawLine(x1, y1, x1,y2, Color); //V
<span class="token function">LCD_DrawLine</span><span class="token punctuation">(</span>x1<span class="token operator">+</span><span class="token number">1</span><span class="token punctuation">,</span>y2<span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">,</span>x2<span class="token punctuation">,</span>y2<span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">,</span> Color<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//H 加粗 多画一条线</span>
<span class="token function">LCD_DrawLine</span><span class="token punctuation">(</span>x1<span class="token punctuation">,</span> y2<span class="token punctuation">,</span> x2<span class="token punctuation">,</span>y2<span class="token punctuation">,</span> Color<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//H</span>
<span class="token function">LCD_DrawLine</span><span class="token punctuation">(</span>x2<span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">,</span>y1<span class="token operator">+</span><span class="token number">1</span><span class="token punctuation">,</span>x2<span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">,</span>y2<span class="token punctuation">,</span> Color<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//V</span>
LCD_DrawLine(x2 ,y1 ,x2,y2, Color); //V
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
显示纯色背景与图片
我们已经做到了全屏显示,那么纯色背景的显示就很简单了:
/**
* @brief 全屏显示纯色图片,可用作清屏
* @param 颜色
* @retval None
*/
void LCD_BG_Color(u16 color)
{
int i=16384;//128*128
Full_Screen();
while(i-->0)//不可使用无符号数据类型
LCD_WD_U16(color);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
一共有16384个像素,那就调用16384次画点的程序。只不过这里我稍作了一点优化,不用再管点的坐标了,因为已经设置了显示区域是全屏。
我专门写了一个函数,显示各个纯色图片,可以看出有没有那个像素点颜色显示的不全:
/**
* @brief LCD屏幕测试,依次显示纯色照片,可以看出屏幕的每一个像素点是否正常
* @param 每种颜色持续的时间,单位ms
* @retval None
*/
void LCD_Test(u16 delay_Time)
{
LCD_BG_Color(BLACK);
HAL_Delay(delay_Time);
LCD_BG_Color(RED);
HAL_Delay(delay_Time);
LCD_BG_Color(GREEN);
HAL_Delay(delay_Time);
LCD_BG_Color(BLUE);
HAL_Delay(delay_Time);
LCD_BG_Color(WHITE);
HAL_Delay(delay_Time);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
显示全屏幕的背景图片跟纯色背景思路类似,把数据的来源改为数组就好:
/**
* @brief 在LCD屏幕中显示一个全屏的背景图片
* @param 无
* @note 只适用于128*128的屏幕,为了效率把长度直接计算了出来
* 由于图片数组要放在ROM,所以用了const,因此要做强制类型转化(u8 *)
* 如果是双工,则收发需要同一个数组,数组不能放在ROM
* @retval None
*/
void LCD_BG_Image(void)
{
uint32_t i=32768; //Z144_HEIGHT*Z144_WIDTH*2;
Full_Screen();
LCD_WD_buf((u8 *)LCD_MOTOR,i);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
接下来就是指定区域显示图片了,函数也很简单:
/**
* @brief 按照指定的坐标显示图片
* @param 图片左上角的坐标与右下角的坐标,图片地址
* @note 图片大小与坐标必须对应
* 由于图片数组要放在ROM,所以用了const,因此要做强制类型转化(u8 *)
* 坐标范围[0,127]
* @retval None
*/
void LCD_Show_Image(u16 x0, u16 y0,u16 x1,u16 y1,const unsigned char *p)
{
int i = (x1-x0)*(y1-y0)*2;
LCD_SetArea(x0,y0,x1,y1);
LCD_WD_buf((u8 *)p,i);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
那么图片数组从哪里来?
取模得到。我去网上随便找了一张彩图,然后把尺寸编辑为100* 100像素。取模软件如下设置:
然后可以得到图片转化为的数组。
显示文字与字符
中文字符都采用GBK(或者说GB2312)编码,英文字符采用ASCII码。显示文字其实与显示图片的原理是一样一样的,思路在我另一篇博客中介绍过。
先说汉字的取模设置:
我使用了结构体储存汉字,结构体的每个元素都包含文字(或者称之为索引)和它对应的编码。因此得到的数组要稍加修改,增加双引号与汉字,例如北京:
//修改前
0x04,0x40,0x04,0x40,0x04,0x40,0x04,0x44,0x04,0x48,0x7C,0x50,0x04,0x60,0x04,0x40,
0x04,0x40,0x04,0x40,0x04,0x40,0x04,0x42,0x1C,0x42,0xE4,0x42,0x44,0x3E,0x04,0x00,/*"北",4*/
0x02,0x00,0x01,0x00,0xFF,0xFE,0x00,0x00,0x00,0x00,0x1F,0xF0,0x10,0x10,0x10,0x10,
0x10,0x10,0x1F,0xF0,0x01,0x00,0x11,0x10,0x11,0x08,0x21,0x04,0x45,0x04,0x02,0x00,/*"京",5*/
//修改后
"北",0x04,0x40,0x04,0x40,0x04,0x40,0x04,0x44,0x04,0x48,0x7C,0x50,0x04,0x60,0x04,0x40,
0x04,0x40,0x04,0x40,0x04,0x40,0x04,0x42,0x1C,0x42,0xE4,0x42,0x44,0x3E,0x04,0x00,/*"北",4*/
"京",0x02,0x00,0x01,0x00,0xFF,0xFE,0x00,0x00,0x00,0x00,0x1F,0xF0,0x10,0x10,0x10,0x10,
0x10,0x10,0x1F,0xF0,0x01,0x00,0x11,0x10,0x11,0x08,0x21,0x04,0x45,0x04,0x02,0x00,/*"京",5*/
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
显示的字符要先判断是中文的还是英文的。英文字符的值小于128,显示函数如下:
/**
* @brief 输出16*16的汉字或8*16的字符,函数可以自动识别是中文字符还是ASCII
* @param 第一个字符的坐标,汉字颜色,背景颜色,需要显示的字符串。背景颜色为0表示不画背景
* @retval None
*/
void LCD_DrawFont_GBK16(u16 x, u16 y, u16 fc, u16 bc, u8 *s)
{
unsigned char i,j;
unsigned short k,x0;
x0=x;
while(*s)
{
if((*s) < 128) //如果是ASCII码,那么编号小于128
{
k=*s;
if (k==13) //回车
{
x=x0;//记录本行第一个字符的位置
y+=16;
}
else
{
if (k>32) k-=32; else k=0; //ASCII前32个都不是字符,除了回车,对显示没有影响,所以字库不储存
for(i=0;i<16;i++)
{
for(j=0;j<8;j++)
{
if(ASC16[k*16+i]&(0x80>>j)) //如有数据,画字体 从高位到低位取出字符
{
LCD_DrawPoint(x+j,y+i,fc);
}
else //如果没有数据,画背景 如果背景颜色为0 那么不画背景
{
if ((bc!=0)&&(fc!=bc)) LCD_DrawPoint(x+j,y+i,bc);
}
}
}
x+=8;//字符横坐标8个像素,x+8代表下一个字符的坐标
}
s++;//画完此字符与背景以后,取出下一个字符
}
<span class="token keyword">else</span> <span class="token comment">//GBK编码 说明是中文字符</span>
<span class="token punctuation">{<!-- --></span>
<span class="token keyword">for</span> <span class="token punctuation">(</span>k<span class="token operator">=</span><span class="token number">0</span><span class="token punctuation">;</span>k<span class="token operator"><</span>HZ16_num<span class="token punctuation">;</span>k<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token comment">//遍历中文字库</span>
<span class="token punctuation">{<!-- --></span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token punctuation">(</span>HZ16<span class="token punctuation">[</span>k<span class="token punctuation">]</span><span class="token punctuation">.</span>Index<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token operator">==</span><span class="token operator">*</span><span class="token punctuation">(</span>s<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token operator">&&</span><span class="token punctuation">(</span>HZ16<span class="token punctuation">[</span>k<span class="token punctuation">]</span><span class="token punctuation">.</span>Index<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token operator">==</span><span class="token operator">*</span><span class="token punctuation">(</span>s<span class="token operator">+</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token comment">//可以找到索引,说明字库中有此汉字。一个汉字占两个字节 </span>
<span class="token punctuation">{<!-- --></span>
<span class="token keyword">for</span><span class="token punctuation">(</span>i<span class="token operator">=</span><span class="token number">0</span><span class="token punctuation">;</span>i<span class="token operator"><</span><span class="token number">16</span><span class="token punctuation">;</span>i<span class="token operator">++</span><span class="token punctuation">)</span>
<span class="token punctuation">{<!-- --></span>
<span class="token keyword">for</span><span class="token punctuation">(</span>j<span class="token operator">=</span><span class="token number">0</span><span class="token punctuation">;</span>j<span class="token operator"><</span><span class="token number">8</span><span class="token punctuation">;</span>j<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token comment">//先画左半边</span>
<span class="token punctuation">{<!-- --></span>
<span class="token keyword">if</span><span class="token punctuation">(</span>HZ16<span class="token punctuation">[</span>k<span class="token punctuation">]</span><span class="token punctuation">.</span>Msk<span class="token punctuation">[</span>i<span class="token operator">*</span><span class="token number">2</span><span class="token punctuation">]</span><span class="token operator">&</span><span class="token punctuation">(</span><span class="token number">0x80</span><span class="token operator">>></span>j<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token function">LCD_DrawPoint</span><span class="token punctuation">(</span>x<span class="token operator">+</span>j<span class="token punctuation">,</span>y<span class="token operator">+</span>i<span class="token punctuation">,</span>fc<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">else</span> <span class="token punctuation">{<!-- --></span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token punctuation">(</span>bc<span class="token operator">!=</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token operator">&&</span><span class="token punctuation">(</span>fc<span class="token operator">!=</span>bc<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token function">LCD_DrawPoint</span><span class="token punctuation">(</span>x<span class="token operator">+</span>j<span class="token punctuation">,</span>y<span class="token operator">+</span>i<span class="token punctuation">,</span>bc<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token keyword">for</span><span class="token punctuation">(</span>j<span class="token operator">=</span><span class="token number">0</span><span class="token punctuation">;</span>j<span class="token operator"><</span><span class="token number">8</span><span class="token punctuation">;</span>j<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token comment">//再画右半边</span>
<span class="token punctuation">{<!-- --></span>
<span class="token keyword">if</span><span class="token punctuation">(</span>HZ16<span class="token punctuation">[</span>k<span class="token punctuation">]</span><span class="token punctuation">.</span>Msk<span class="token punctuation">[</span>i<span class="token operator">*</span><span class="token number">2</span><span class="token operator">+</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token operator">&</span><span class="token punctuation">(</span><span class="token number">0x80</span><span class="token operator">>></span>j<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token function">LCD_DrawPoint</span><span class="token punctuation">(</span>x<span class="token operator">+</span>j<span class="token operator">+</span><span class="token number">8</span><span class="token punctuation">,</span>y<span class="token operator">+</span>i<span class="token punctuation">,</span>fc<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">else</span>
<span class="token punctuation">{<!-- --></span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token punctuation">(</span>bc<span class="token operator">!=</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token operator">&&</span><span class="token punctuation">(</span>fc<span class="token operator">!=</span>bc<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token function">LCD_DrawPoint</span><span class="token punctuation">(</span>x<span class="token operator">+</span>j<span class="token operator">+</span><span class="token number">8</span><span class="token punctuation">,</span>y<span class="token operator">+</span>i<span class="token punctuation">,</span>bc<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
s<span class="token operator">+</span><span class="token operator">=</span><span class="token number">2</span><span class="token punctuation">;</span>x<span class="token operator">+</span><span class="token operator">=</span><span class="token number">16</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
由于屏幕只有128* 128,一个汉字占用16,那么一行只能显示8个汉字,太少。16的字太大了,所以我又写了显示12像素字符的函数,思路一样,代码就不放了。
需要注意的是,汉字是需要字库的。ASCII码不多,全部取模也就是100多个(有效的还不到100个),但是汉字可就多了,比如说GBK编码共收录了21886个汉字和图片,那么按照我们设计的结构体来储存(汉字占2个字节,编码占32个字节,其实还可以优化,不用储存汉字),共需要34* 21886=744124byte = 744KB。理论上来讲,STM32F405RG的flash空间是1024KB,能放下这个字库,只不过程序的编译与下载就会变得很缓慢,十分不利于调试,几乎没人这么干。
因此,目前,对于汉字,我们还是用到哪个汉字,对哪个汉字取模。
还需要注意一个问题,工程内文件的编码格式要一致,不能这个文件用UTF-8,那个是GB2312,因为我们的函数查找汉字要依赖于比较编码值,同一个汉字不同编码格式的编码值不一样。
显示效果
我在主函数中调用以上辛辛苦苦编写的函数,省略无关的代码以后,如下:
/* USER CODE BEGIN 2 */
ST7735S_CPT144_Initial();
LCD_Test(500);
LCD_DrawBTN(0,98,126,126,YELLOW);
LCD_Show_Image(0,0,99,99,gImage_git100);
LCD_DrawLine(100,0,127,100,BLUE);
LCD_DrawLine(127,0,100,100,PURPLE);
LCD_DrawFont_GBK16(0,99,BLACK,WHITE,"屏幕驱动");
LCD_DrawFont_GBK16(64,99,BLACK,WHITE,"geekYatao");
LCD_DrawFont_GBK12(0,115,RED,GREEN,"智联有道");
LCD_DrawFont_GBK12(48,115,RED,GREEN,"STM32F4+HAL");
//LCD_BG_Image();
/* USER CODE END 2 */
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
刷屏速度尚可,但还是勉强能看出来刷屏幕的过程。最终显示效果不错。
还可以优化
调用SPI刷屏是要占用总线的,可以使用DMA+SPI,在定时器内刷屏。如果达到每40ms刷屏一次,那么就能达到25帧,看上去就不会太卡顿。
使用DMA发送大量数据有优势,因此可以用大数组把每一个点的信息都储存下来。为了方便发送,这个数组类型可以设置为unsigned char,共128* 128* 2=32768个元素。
另外关于字库,既然单片机的flash放的下,那么无需外接硬件就能实现全字库的功能:把编译过的二进制字库,放到单片机flash中靠后的指定地址(避免被程序覆盖),然后需要字库的时候,让程序来读取指定的地址就好。
本文用到的部分代码在这里代码