树莓派使用 Python 编写/移植 ST7735S 0.96 寸彩色 LCD 屏幕驱动(SPI 通信)

本站提供了很多现成的屏幕驱动供大家使用,也写了很多文章教大家如何绘图、显示中文、动画制作等。现在,让我们来自己编写/移植一款彩色屏幕的驱动,这样大家以后也可以把驱动移植到其它平台,或者举一反三,自行学会编写其它型号的屏幕驱动。当然,你也可以移植到其它语言比如 nodejs,Lua,C 来驱动此屏幕。本站已经实现了 C, NodeJS, Lua, Python 各种语言的版本,可以运行在单片机,树莓派,ESP8266/32等各种平台。

先介绍一下 ST7735S LCD 屏幕,这个屏幕的主控型号是使用的 ST7735S,分辨率是 160*80,接口是 SPI。它是 LCD 屏幕,色彩和可视角度都比 TFT 好多了,尤其是色彩鲜艳度和细腻度。此款屏幕可以接 3.3V 或者 5V。注意,不接背光接口它也能亮,只是会发生闪烁现象和屏幕灰暗,所以看到背光闪烁多半是因为电压不够或者没有接背光导致的。它支持 262K 颜色,即 565 格式,使用 2 个字节表示 RGB 颜色。

上面的图片是本店中出售的 ST7735S SPI 接口的屏幕,分辨率是 160*80。

【luma-lcd 英文文档】 【luma-lcd Github】

上面是参考资料,之前没有阅读过本站文章的同学,可以当做进阶资料了解一下。

打开硬件 SPI

屏幕需要使用 SPI 进行通信,所以树莓派先要打开硬件 SPI 功能。首先检查下是否打开了 SPI 功能。执行下面的命令:

$ ls /dev/spi*

如果看到有 spidev0.0 或者 spidev0.1 就打开了,接下来这步可以跳过。如果什么也没看到,则按下面的方法打开 SPI。

打开 SPI 方法请查看这篇文章:树莓派使用 Python 驱动 SSD1306(IIC/SPI 通信)

硬件接线

下面是树莓派的 GPIO 引脚图,引脚编号请参考此图。

将屏幕的 SCL(有的也叫SCLK, SCK) 接树莓派的 23 号引脚,将屏幕的 SDA(有的也叫MOSI, SDI) 接到树莓派的 19 号引脚,将屏幕的 RST 接到树莓派的 22 号引脚,将屏幕的 RS(有的也叫DC) 接到树莓派的 18 号引脚,将屏幕的 CS 接到树莓派的 24 号引脚。最后,将屏幕的 LED 接 3.3V 或者 5V 电源口。如果你之前购买过 SSD1306 SPI 接口的屏幕,会发现接线是一模一样的,接线不用变。屏幕的 VCC 和 GND 是电源,记得也要接,可以接 3.3V。因为搞电子模块的大家都知道电源是必备的,之前的文章就省略了,然后很多人问我屏幕为什么不亮,我也是很无奈啊。

注意:请一定以屏幕的实际引脚编号为准,如你不清楚如何接,请本店咨询客服,电源反接必烧毁,本店概不负责,屏幕会产生大量热量,注意烫伤手。

如果你接了风扇,风扇可以接其它的电源口,树莓派有 2 个 3v 电源和 2 个 5v 电源。学会变通,不要死盯着这一个电源接口,只要不短路和过载,一般都没什么问题。在本店购买的屏幕除非特殊说明,默认接 3.3V 电压,防止烧坏。其它地方购买的屏幕,请一定注意电源,烧坏本人不负责。

驱动移植

如果你有一些 TFT 屏幕基础的同学,会让你更容易理解此篇文章。如果没有也不要紧,先说一下驱动屏幕的原理。

屏幕的工作原理不用多说,其本质就是一个一个像素点组成的二维平面。每一个像素点都具有 RGB 三原色,可以分别独立控制。通过控制每个像素点的 RGB 三原色背光可以透过的量,就可以再混合成各种不同的颜色,理论上颜色种类是无穷的,因为每一个通道透过的量都是连续的。但事实不是这样的,我们经常能看到 65K,262K,1600万色,10.7亿色,这些是什么呢?因为我们在计算机里面,都是二进制,是不连续的,无法表示出连续的值,这也导致了我们控制屏幕的时候,也无法发送连续的值。以此款屏幕来说,颜色格式是 565,也就是用 2 个字节来表示一个像素点,2 个字节刚好 18 位,高 5 位表示 R,中 6 位表示 G,低 5 位表示 B。所以我们在控制此屏幕显示的时候,本质就是在组装符合我们想要的字节数据,然后发送给屏幕。好了,原理说完了,下面开始写代码。

既然是移植,那肯定得有个能跑的例程。厂家一般会给一个单片机的例程,因为单片机简单,代码很容易看懂,也方便根据例程进行二次开发。我们也是以单片机的例程出发,把它移植到各个不同的平台。此文章我们会介绍三种语言,分别对应三个平台:单片机(C语言),ESP8266(Lua语言),树莓派(Python语言)。看懂了其中一个,你就能把它移植到任何其它平台,也可以自己编写其它型号的屏幕驱动,甚至驱动其它类型的电子模块。正所谓举一反三,原理都是差不多的。

好了,废话少说,先来看厂家给出的例程,是 STC89C52RC 的,STC12 系列也可以用。代码有点长,其它不相干的去掉了,如果侵犯了您的版权,请及时和我联系。

#include<reg51.h>
#include<absacc.h>
#include<intrins.h>
#include<string.h>
#define uchar unsigned char
#define uint unsigned int

//如果您使用的单片机不是STC12系列(如STC89C52)请屏蔽此宏定义
#define MCU_STC12
#ifdef MCU_STC12
sfr P3M1  = 0xB1;
sfr P3M0  = 0xB2;
#endif

//---------------------------液晶屏接线说明-------------------------------------//
//接线前请参考液晶屏说明书第10页引脚定义
sbit bl        =P1^5; //接模块BL引脚,背光可以采用IO控制或者PWM控制,也可以直接接到高电平常亮
sbit scl       =P1^0; //接模块CLK引脚,接裸屏Pin9_SCL
sbit sda       =P1^1; //接模块DIN/MOSI引脚,接裸屏Pin8_SDA
sbit rs        =P1^3; //接模块D/C引脚,接裸屏Pin7_A0
sbit cs        =P1^4; //接模块CE引脚,接裸屏Pin12_CS
sbit reset     =P1^2; //接模块RST引脚,接裸屏Pin6_RES
//---------------------------End of液晶屏接线---------------------------------//

//定义常用颜色
#define RED     0xf800
#define GREEN   0x07e0
#define BLUE    0x001f
#define WHITE   0xffff
#define BLACK   0x0000
#define YELLOW  0xFFE0
#define GRAY0   0xEF7D
#define GRAY1   0x8410
#define GRAY2   0x4208

void delay_ms(uint time)
{
  uint i, j;
  for(i = 0; i < time; i++)
    for(j = 0; j < 250; j++);
}

//向SPI总线传输一个8位数据
void SPI_WriteData(uchar Data)
{
  unsigned char i=0;
  for(i=8;i>0;i--)
  {
    if(Data&0x80)
      sda=1;
    else
      sda=0;
    scl=0;
    scl=1;
    Data<<=1;
  }
}

//向液晶屏写一个8位指令
void Lcd_WriteIndex(uchar Data)
{
  cs=0;
  rs=0;
  SPI_WriteData(Data);
  cs=1;
}

//向液晶屏写一个8位数据
void Lcd_WriteData(uchar Data)
{
  unsigned char i=0;
  cs=0;
  rs=1;
  SPI_WriteData(Data);
  cs=1;
}

//向液晶屏写一个16位数据
void LCD_WriteData_16Bit(unsigned int Data)
{
  unsigned char i=0;
  cs=0;
  rs=1;
  SPI_WriteData(Data>>8); //写入高8位数据
  SPI_WriteData(Data); //写入低8位数据
  cs=1;
}

void Reset()
{
  reset=0;
  delay_ms(100);
  reset=1;
  delay_ms(100);
}

void Lcd_initial()
{
  Reset(); //Reset before LCD Init.

  Lcd_WriteIndex(0x11); // Sleep exit
  delay_ms (120);

  Lcd_WriteIndex(0x21);
  Lcd_WriteIndex(0xB1);
  Lcd_WriteData(0x05);
  Lcd_WriteData(0x3A);
  Lcd_WriteData(0x3A);
  Lcd_WriteIndex(0xB2);
  Lcd_WriteData(0x05);
  Lcd_WriteData(0x3A);
  Lcd_WriteData(0x3A);
  Lcd_WriteIndex(0xB3);
  Lcd_WriteData(0x05);  
  Lcd_WriteData(0x3A);
  Lcd_WriteData(0x3A);
  Lcd_WriteData(0x05);
  Lcd_WriteData(0x3A);
  Lcd_WriteData(0x3A);
  Lcd_WriteIndex(0xB4);
  Lcd_WriteData(0x03);
  Lcd_WriteIndex(0xC0);
  Lcd_WriteData(0x62);
  Lcd_WriteData(0x02);
  Lcd_WriteData(0x04);
  Lcd_WriteIndex(0xC1);
  Lcd_WriteData(0xC0);
  Lcd_WriteIndex(0xC2);
  Lcd_WriteData(0x0D);
  Lcd_WriteData(0x00);
  Lcd_WriteIndex(0xC3);
  Lcd_WriteData(0x8D);
  Lcd_WriteData(0x6A);
  Lcd_WriteIndex(0xC4);
  Lcd_WriteData(0x8D);
  Lcd_WriteData(0xEE);
  Lcd_WriteIndex(0xC5);
  Lcd_WriteData(0x0E);
  Lcd_WriteIndex(0xE0);
  Lcd_WriteData(0x10);
  Lcd_WriteData(0x0E);
  Lcd_WriteData(0x02);
  Lcd_WriteData(0x03);
  Lcd_WriteData(0x0E);
  Lcd_WriteData(0x07);
  Lcd_WriteData(0x02);
  Lcd_WriteData(0x07);
  Lcd_WriteData(0x0A);
  Lcd_WriteData(0x12);
  Lcd_WriteData(0x27);
  Lcd_WriteData(0x37);
  Lcd_WriteData(0x00);
  Lcd_WriteData(0x0D);
  Lcd_WriteData(0x0E);
  Lcd_WriteData(0x10);
  Lcd_WriteIndex(0xE1);
  Lcd_WriteData(0x10);
  Lcd_WriteData(0x0E);
  Lcd_WriteData(0x03);
  Lcd_WriteData(0x03);
  Lcd_WriteData(0x0F);
  Lcd_WriteData(0x06);
  Lcd_WriteData(0x02);
  Lcd_WriteData(0x08);
  Lcd_WriteData(0x0A);
  Lcd_WriteData(0x13);
  Lcd_WriteData(0x26);
  Lcd_WriteData(0x36);
  Lcd_WriteData(0x00);
  Lcd_WriteData(0x0D);
  Lcd_WriteData(0x0E);
  Lcd_WriteData(0x10);
  Lcd_WriteIndex(0x3A);
  Lcd_WriteData(0x05);
  Lcd_WriteIndex(0x36);
  Lcd_WriteData(0xC8);
  Lcd_WriteIndex(0x29);
}

/*************************************************
函数名:LCD_Set_Region
功能:设置lcd显示区域,在此区域写点数据自动换行
入口参数:xy起点和终点
返回值:无
*************************************************/
void Lcd_SetRegion(unsigned int x_start,unsigned int y_start,unsigned int x_end,unsigned int y_end)
{
  Lcd_WriteIndex(0x2a);
  Lcd_WriteData(0x00);
  Lcd_WriteData(x_start+0x1A);
  Lcd_WriteData(0x00);
  Lcd_WriteData(x_end+0x1A);

  Lcd_WriteIndex(0x2b);
  Lcd_WriteData(0x00);
  Lcd_WriteData(y_start+1);
  Lcd_WriteData(0x00);
  Lcd_WriteData(y_end+1);
  Lcd_WriteIndex(0x2c);
}

void Lcd_fill_color(int color)
{
  uchar i,j;
  Lcd_SetRegion(0,0,80-1,160-1);
  for (i=0;i<160;i++)
    for (j=0;j<80;j++)
      LCD_WriteData_16Bit(color);
}

void main(void)
{
  Lcd_initial();
  Lcd_fill_color(YELLOW);
}

以上就是 STC89C52RC 下的例程,玩过单片机的应该都能自己跑起来,这里我就不多说了。重点分析一下这个程序。

程序开头是每个单片机程序必备的头文件以及方便使用的宏定义。然后就是引脚的定义,接着后面是常用的颜色定义。这里注意一下,我们是使用 2 个字节来表示一个像素点,而一个像素点有 RGB(565)三个通道,所以总共是 16 位,高位在前。RS 表示 SPI 协议中的 DC 引脚,说实话我也搞不懂设计这个板子的人为啥好好的 DC 不叫非要叫 RS。

再后面就是几个基础方法的定义了。延时函数是针对 STC89 系列单片机的,主频是 12MHz,如果换成其它平台和晶振,要相应的调整一下。

我们使用模拟 SPI 方式,后面几个函数看名字就能看出来,分别是向屏幕发送命令和数据用的。DC 引脚就是用来切换命令和数据的,DC 拉低就是发送命令,拉高就是发送数据,也非常简单。有了这几个函数后,我们就可以使用它们和屏幕进行通信了。还有一个是复位函数,把 RST 引脚拉低一段时间就复位了。上面这些基本是通用的,放到其它 SPI 屏幕或者模块也是可以复用的,移植的重点也是这些基础函数,在没有硬件 SPI 的平台使用 GPIO 模拟,有硬件 SPI 的平台使用硬件 SPI。其它应用层都是一样的可以复用。理解了这点就很好写代码了。

下面是驱动屏幕的重点。Lcd_initial 里面向屏幕发送了很多命令和数据,这是初始化屏幕必须要的,在复位后,必须要先发送初始化数据,屏幕才能正常工作。至于初始化中的各种命令和数据是干嘛的,请各位同学自己查看数据手册,它写的肯定比我更清楚。初始化后,就是向屏幕发送像素数据了,这个时候屏幕就能显示了。在发送像素数据前,要先进行设置窗口,设置完后,发送的数据只会在窗口内展示,超出自动换行。如果是全屏幕的刷新,窗口设置为全屏幕就行,如果是局部刷新,窗口设置为想要刷新的区域就行了。这点在做局部刷新的时候会用到,提高刷新率。

总结起来,驱动这个屏幕有 3 个步骤:复位,初始化,填充像素。

移植到 树莓派

下面我们移植到树莓派上,使用 Python 驱动。树莓派是自带硬件 SPI 的,我们只需要用树莓派的硬件 SPI 替换就好了,然后使用 Python 重写一遍,甚至你用其它语言写也是一样的。废话少说,上代码:

# -*- coding: UTF-8 -*-

import time
from luma.core.interface.serial import spi
from luma.core.render import canvas
from PIL import Image, ImageDraw, ImageFont

serial = spi(port=0, device=0, bus_speed_hz=16000000)

def delay_ms(ms):
  time.sleep(ms * 0.001)

def lcd_writeCmd(cmd):
  serial.command(cmd)

def lcd_writeData(data):
  serial.data([data])

def lcd_reset():
  serial._gpio.output(serial._RST, serial._gpio.LOW)
  delay_ms(100)
  serial._gpio.output(serial._RST, serial._gpio.HIGH)

def lcd_init():
  lcd_reset()
  lcd_writeCmd(0x11)
  delay_ms(120)
  lcd_writeCmd(0x21)
  lcd_writeCmd(0xB1)
  lcd_writeData(0x05)
  lcd_writeData(0x3A)
  lcd_writeData(0x3A)
  lcd_writeCmd(0xB2)
  lcd_writeData(0x05)
  lcd_writeData(0x3A)
  lcd_writeData(0x3A)
  lcd_writeCmd(0xB3)
  lcd_writeData(0x05)
  lcd_writeData(0x3A)
  lcd_writeData(0x3A)
  lcd_writeData(0x05)
  lcd_writeData(0x3A)
  lcd_writeData(0x3A)
  lcd_writeCmd(0xB4)
  lcd_writeData(0x03)
  lcd_writeCmd(0xC0)
  lcd_writeData(0x62)
  lcd_writeData(0x02)
  lcd_writeData(0x04)
  lcd_writeCmd(0xC1)
  lcd_writeData(0xC0)
  lcd_writeCmd(0xC2)
  lcd_writeData(0x0D)
  lcd_writeData(0x00)
  lcd_writeCmd(0xC3)
  lcd_writeData(0x8D)
  lcd_writeData(0x6A)
  lcd_writeCmd(0xC4)
  lcd_writeData(0x8D)
  lcd_writeData(0xEE)
  lcd_writeCmd(0xC5)
  lcd_writeData(0x0E)
  lcd_writeCmd(0xE0)
  lcd_writeData(0x10)
  lcd_writeData(0x0E)
  lcd_writeData(0x02)
  lcd_writeData(0x03)
  lcd_writeData(0x0E)
  lcd_writeData(0x07)
  lcd_writeData(0x02)
  lcd_writeData(0x07)
  lcd_writeData(0x0A)
  lcd_writeData(0x12)
  lcd_writeData(0x27)
  lcd_writeData(0x37)
  lcd_writeData(0x00)
  lcd_writeData(0x0D)
  lcd_writeData(0x0E)
  lcd_writeData(0x10)
  lcd_writeCmd(0xE1)
  lcd_writeData(0x10)
  lcd_writeData(0x0E)
  lcd_writeData(0x03)
  lcd_writeData(0x03)
  lcd_writeData(0x0F)
  lcd_writeData(0x06)
  lcd_writeData(0x02)
  lcd_writeData(0x08)
  lcd_writeData(0x0A)
  lcd_writeData(0x13)
  lcd_writeData(0x26)
  lcd_writeData(0x36)
  lcd_writeData(0x00)
  lcd_writeData(0x0D)
  lcd_writeData(0x0E)
  lcd_writeData(0x10)
  lcd_writeCmd(0x3A)
  lcd_writeData(0x05)
  lcd_writeCmd(0x36)
  lcd_writeData(0xC8)
  lcd_writeCmd(0x29)

def lcd_setRegion(x1, y1, x2, y2):
  lcd_writeCmd(0x2a)
  lcd_writeData(0x00)
  lcd_writeData(x1 + 0x1A)
  lcd_writeData(0x00)
  lcd_writeData(x2 + 0x1A)
  lcd_writeCmd(0x2b)
  lcd_writeData(0x00)
  lcd_writeData(y1 + 1)
  lcd_writeData(0x00)
  lcd_writeData(y2 + 1)
  lcd_writeCmd(0x2c)

def lcd_fill(color1, color2):
  lcd_setRegion(0, 0, 79, 159)
  for i in range(1,161):
    for j in range(1,81):
      lcd_writeData(color1)
      lcd_writeData(color2)


lcd_init()
lcd_fill(0xf8, 0x00)
lcd_fill(0xff, 0xe0)
serial.cleanup()

非常简单,我们基本就是把函数命名整理了一下,然后替换了 SPI 实现,就直接切换到 Python 了。有几个修改:我们没有自己封装 SPI 部分,而是使用 luma 库提供的 api,因为之前的文章我们都是使用这个库的,为了以后方便集成到 luma 库,并且也为了让大家对这个库更深入的理解和应用,所以我们直接复用就行。最后,我们在刷完屏后手动释放了 SPI 总线,这是个好习惯,用完释放资源,避免出现什么奇怪的问题。

硬件接线请查看文章开头。将它保存为文件,执行它就能看到屏幕在刷屏了。

本店出售的屏幕,无论是 IIC 还是 SPI 通信的,大部分驱动都是这个套路,先复位,然后发送初始化命令,最后发送像素数据填充。刚接触这方面编程的同学暂时先不用理解每个指令的作用,也不用纠结初始化数据是怎么得来的。先把它点亮,点亮后再去仔细查看数据手册,每个指令都可以在数据手册中找到很详细的描述,你也可以更改其它的参数实现不同的效果。这个套路同样可以用在其它的电子模块上,写多了就熟悉了。

上面我们只进行了应用层的介绍,但整个过程还有很多其它的知识点是没介绍的,如果想要深入了解,大家可以自己去查看资料。比如 SPI 协议,565 颜色格式转换,LCD 屏幕显示原理,树莓派 GPIO 使用方法等。

到这里,最基本的驱动移植已经做完了,能点亮就成功了一大半了,剩下的就是如何封装更友好的图形 api,比如点、线、面、图片、文字之类的。用过 luma 的已经大概知道要怎么做了,对的,就是使用 pillow 这个库完成绘图,然后将 pillow 的像素数据转换成 RGB(565) 格式,再刷新到屏幕上,就可以让屏幕具有图形功能了。这个已经在之前的文章介绍过了,请各位自己查看 树莓派使用 Python 驱动 SSD1306 中文字体、图片动画高级教程。注意,这个 display 函数没有现成的,需要你自己写,相信这点小问题难不倒你。

再看看其它同类文章吧!

有想说的评论一句吧!