一 环境搭建
使用的ST7715S驱动的1.8寸彩色屏,主控是我们熟悉的树莓派Pico。软件环境是micropython。连接是屏幕直接从Pico取3.3V的供电,然后总线用的SPI。
ST7735 Pin | Pico Pin |
---|---|
VCC | 3.3V |
GND | GND |
SCL (SCK) | GP10 |
SDA (MOSI) | GP11 |
RES (RST) | GP17 |
DC(A0) | GP16 |
CS | GP18 |
和下面这篇也算是姊妹篇,只是一个侧重SPI协议,一个侧重显示驱动。可以结合着一起看。
总线学习3--SPI-CSDN博客
驱动是来自:https://github.com/boochow/MicroPython-ST7735
所以这里主要还是学习大神的代码。
ST7735S的数据手册:ST7735S Datasheet, PDF - Alldatasheet
基本上所有的驱动开发工作,都是围绕着数据手册。对上层软件工程师来说,其实数据手册这个名字有点误导人,叫做接口文档一下就秒懂。。。移植的一般不用太怎么看这个,但是如果是从头做,或者是要改一下内容,就得自己慢慢啃这个200页的文档了。。。
二 启动
启动阶段的代码是这样的:
spi = SPI(2, baudrate=20000000, polarity=0, phase=0, sck=Pin(14), mosi=Pin(13), miso=Pin(12))
tft=TFT(spi,16,17,18)
tft.initr()
tft.rgb(True)
tft.fill(TFT.BLACK)
基本上就是TFT代码初始化,一个initr。
TFT Init
def __init__( self, spi, aDC, aReset, aCS) :"""aLoc SPI pin location is either 1 for 'X' or 2 for 'Y'.aDC is the DC pin and aReset is the reset pin."""self._size = ScreenSizeself._offset = bytearray([0,0])self.rotate = 0 #Vertical with top toward pins.self._rgb = True #color order of rgb.self.tfa = 0 #top fixed areaself.bfa = 0 #bottom fixed areaself.dc = machine.Pin(aDC, machine.Pin.OUT, machine.Pin.PULL_DOWN)self.reset = machine.Pin(aReset, machine.Pin.OUT, machine.Pin.PULL_DOWN)self.cs = machine.Pin(aCS, machine.Pin.OUT, machine.Pin.PULL_DOWN)self.cs(1)self.spi = spiself.colorData = bytearray(2)self.windowLocData = bytearray(4)
这里就是很多PIN口设置,比较要注意的是offset,colorData,windowsLocData。
这里有个奇怪的点,self.colorData = bytearray(2)只用了2个byte,但是rgb不是要3个byte吗?查了一下,原来为了减少数据量,ST7735使用了RGB565,2个byte,如下:
在许多显示屏控制器(如ST7735)中,颜色数据通常使用16位格式而不是24位格式来表示颜色。这意味着每个像素的颜色由2个字节(16位)表示,而不是3个字节(24位)。这种16位颜色格式通常被称为RGB565格式。
RGB565 格式
在RGB565格式中,颜色数据按如下方式编码:
- 5位用于红色(R),范围为0到31
- 6位用于绿色(G),范围为0到63
- 5位用于蓝色(B),范围为0到31
这种格式可以有效地将颜色压缩为16位,从而减少数据传输量,并且对许多嵌入式系统来说,这种压缩是非常重要的。
示例
假设你有一个颜色
(R, G, B)
,例如(255, 0, 0)
(纯红色),在RGB565格式中编码如下:
- 红色:255 对应的5位表示为 11111(31)
- 绿色:0 对应的6位表示为 000000(0)
- 蓝色:0 对应的5位表示为 00000(0)
最终的16位值是:
11111 000000 00000
,即0b1111100000000000
或十六进制的0xF800
。
不过这样肯定会导致色彩数不足,显示效果没那么好了。。。
这里说一个题外话,作为一个游戏迷,看看一下以前的主机是怎么显示的:
主机名 | 颜色格式 | 用法 | 色彩数 |
FC | 红色R 2位| 绿色G 2位| 蓝色B 2位 | 64 | |
MD | RGB333 | 红色R 3位| 绿色G 3位| 蓝色B 3位 | 512 |
SFC | RGB555 | 红色R 5位| 绿色G 5位| 蓝色B 5位 | 32768 |
GBA | RGB565 | 红色R 5位| 绿色G 6位| 蓝色B 5位 | 65536 |
PS | RGB555 | 红色R 5位| 绿色G 5位| 蓝色B 5位 | 32768 |
PS2 | RGB888 | 红色R 8位| 绿色G 8位| 蓝色B 8位 | 16,777,216 |
PS5 | RGB10A2 | 红色R 10位| 绿色G 10位| 蓝色B 10位| Alpha 4位 | 超过10亿 |
屏幕初始化
def initr( self ) :'''Initialize a red tab version.'''self._reset()self._writecommand(TFT.SWRESET) #Software reset.time.sleep_us(150)self._writecommand(TFT.SLPOUT) #out of sleep mode.time.sleep_us(500)data3 = bytearray([0x01, 0x2C, 0x2D]) #fastest refresh, 6 lines front, 3 lines back.self._writecommand(TFT.FRMCTR1) #Frame rate control.self._writedata(data3)self._writecommand(TFT.FRMCTR2) #Frame rate control.self._writedata(data3)data6 = bytearray([0x01, 0x2c, 0x2d, 0x01, 0x2c, 0x2d])self._writecommand(TFT.FRMCTR3) #Frame rate control.self._writedata(data6)time.sleep_us(10)data1 = bytearray(1)self._writecommand(TFT.INVCTR) #Display inversion controldata1[0] = 0x07 #Line inversion.self._writedata(data1)self._writecommand(TFT.PWCTR1) #Power controldata3[0] = 0xA2data3[1] = 0x02data3[2] = 0x84self._writedata(data3)self._writecommand(TFT.PWCTR2) #Power controldata1[0] = 0xC5 #VGH = 14.7V, VGL = -7.35Vself._writedata(data1)data2 = bytearray(2)self._writecommand(TFT.PWCTR3) #Power controldata2[0] = 0x0A #Opamp current smalldata2[1] = 0x00 #Boost frequencyself._writedata(data2)self._writecommand(TFT.PWCTR4) #Power controldata2[0] = 0x8A #Opamp current smalldata2[1] = 0x2A #Boost frequencyself._writedata(data2)self._writecommand(TFT.PWCTR5) #Power controldata2[0] = 0x8A #Opamp current smalldata2[1] = 0xEE #Boost frequencyself._writedata(data2)self._writecommand(TFT.VMCTR1) #Power controldata1[0] = 0x0Eself._writedata(data1)self._writecommand(TFT.INVOFF)self._writecommand(TFT.MADCTL) #Power controldata1[0] = 0xC8self._writedata(data1)self._writecommand(TFT.COLMOD)data1[0] = 0x05self._writedata(data1)self._writecommand(TFT.CASET) #Column address set.self.windowLocData[0] = 0x00self.windowLocData[1] = 0x00self.windowLocData[2] = 0x00self.windowLocData[3] = self._size[0] - 1self._writedata(self.windowLocData)self._writecommand(TFT.RASET) #Row address set.self.windowLocData[3] = self._size[1] - 1self._writedata(self.windowLocData)dataGMCTRP = bytearray([0x0f, 0x1a, 0x0f, 0x18, 0x2f, 0x28, 0x20, 0x22, 0x1f,0x1b, 0x23, 0x37, 0x00, 0x07, 0x02, 0x10])self._writecommand(TFT.GMCTRP1)self._writedata(dataGMCTRP)dataGMCTRN = bytearray([0x0f, 0x1b, 0x0f, 0x17, 0x33, 0x2c, 0x29, 0x2e, 0x30,0x30, 0x39, 0x3f, 0x00, 0x07, 0x03, 0x10])self._writecommand(TFT.GMCTRN1)self._writedata(dataGMCTRN)time.sleep_us(10)self._writecommand(TFT.DISPON)time.sleep_us(100)self._writecommand(TFT.NORON) #Normal display on.time.sleep_us(10)self.cs(1)
这个就是一系列SPI命令的组合。这部分一般是来自厂家或者自己去数据手册翻。。这哥们注释写的还不错,可以看到主要就是电源的设置。之后就是行列和颜色的设置,这里有个特别点的。就是命令和数据的不同。另外除了initr,还有三个初始化函数initb,initb2和initg,整体大同小异。以后有空再看吧。
#@micropython.nativedef _writecommand( self, aCommand ) :'''Write given command to the device.'''self.dc(0)self.cs(0)self.spi.write(bytearray([aCommand]))self.cs(1)#@micropython.nativedef _writedata( self, aData ) :'''Write given data to the device. This may beeither a single int or a bytearray of values.'''self.dc(1)self.cs(0)self.spi.write(aData)self.cs(1)
用逻辑分析仪来看,发送命令和数据区别就是dc那条线。拉低就是命令,拉高就是数据。从之前的图来看,第三行就是dc。也就是开始有一些命令,后面就全是数据了。
命令都是8位为一组,也就是一个byte。像这样的INVOFF = 0x20。
rgb主要是设置屏幕格式,是RGB色彩还是BGR色彩,这里要用bytearray转成字节码。
def _setMADCTL( self ) :'''Set screen rotation and RGB/BGR format.'''self._writecommand(TFT.MADCTL)rgb = TFTRGB if self._rgb else TFTBGRself._writedata(bytearray([TFTRotations[self.rotate] | rgb]))
fill则是初始化屏幕颜色,直接是绘制整个屏幕矩形这样。
三 图形库的封装
初始完屏幕之后,后面主要就是发送各种数据,做各种图形的操作了,为了方便上层操作,做了很多封装,基本上分以下几类。
1 底层接口封装
def _vscrolladdr(self, addr) :
def _setColor( self, aColor ) :
def _draw( self, aPixels ) :
def _setwindowpoint( self, aPos ) :
def _setwindowloc( self, aPos0, aPos1 ) :
def _writecommand( self, aCommand ) :
def _writedata( self, aData ) :
def _pushcolor( self, aColor ) :
def _setMADCTL( self ) :
def _reset( self ) :
都是以_为开始的函数。这里面_writecommand,_writedata更底层一些。然后所有的图形操作都分成了点和面(包含线条)。
点就是_setwindowpoint,_pushcolor组合。第一个封装的都是self._writecommand(TFT.CASET),_writecommand(TFT.RASET),_writecommand(TFT.RAMWR)这几个SPI的接口。_pushcolor则是直接用SPI数据口将色彩信息传上去,没有使用命令。
面就是_setwindowloc,_setColor和_draw组合。SPI接口和上面差不多一样。
2 上层接口封装
def rotation( self, aRot )
def pixel( self, aPos, aColor ) :
def text( self, aPos, aString, aColor, aFont, aSize = 1, nowrap = False ) :
def char( self, aPos, aChar, aColor, aFont, aSizes ) :
def line( self, aStart, aEnd, aColor ) :
def vline( self, aStart, aLen, aColor ) :
def hline( self, aStart, aLen, aColor ) :
def rect( self, aStart, aSize, aColor ) :
def fillrect( self, aStart, aSize, aColor ) :
def circle( self, aPos, aRadius, aColor ) :
def fillcircle( self, aPos, aRadius, aColor ) :
def fill( self, aColor = BLACK ) :
def image( self, x0, y0, x1, y1, data ) :
def setvscroll(self, tfa, bfa) :
def vscroll(self, value) :
简单整理一下,大概是这么几类,画图形,比如circle,画线,文字,操作点。这些都比较上层了,直接调用封装的底层接口,和硬件没啥关系,搞软件的人瞅一眼就能懂了。
四 示例分析
下面可以具体看一两个。
这个是画垂直线。
例1 Vline
这个就是画一条垂直线。
def vline( self, aStart, aLen, aColor ) :'''Draw a vertical line from aStart for aLen. aLen may be negative.'''start = (clamp(aStart[0], 0, self._size[0]), clamp(aStart[1], 0, self._size[1]))stop = (start[0], clamp(start[1] + aLen, 0, self._size[1]))#Make sure smallest y 1st.if (stop[1] < start[1]):start, stop = stop, startself._setwindowloc(start, stop)self._setColor(aColor)self._draw(aLen)
前面主要是对线的处理,判断位置,有没有出界等等。后面调用了三个下层一些的接口来处理。
Setwindowloc
def _setwindowloc( self, aPos0, aPos1 ) :'''Set a rectangular area for drawing a color to.'''self._writecommand(TFT.CASET) #Column address set.self.windowLocData[0] = self._offset[0]self.windowLocData[1] = self._offset[0] + int(aPos0[0])self.windowLocData[2] = self._offset[0]self.windowLocData[3] = self._offset[0] + int(aPos1[0])self._writedata(self.windowLocData)self._writecommand(TFT.RASET) #Row address set.self.windowLocData[0] = self._offset[1]self.windowLocData[1] = self._offset[1] + int(aPos0[1])self.windowLocData[2] = self._offset[1]self.windowLocData[3] = self._offset[1] + int(aPos1[1])self._writedata(self.windowLocData)self._writecommand(TFT.RAMWR) #Write to RAM.
这个就是设置要绘图的区域。居然还要调用硬件接口去设置,觉得有点略奇怪。。。
然后是setcolor
def _setColor( self, aColor ) :self.colorData[0] = aColor >> 8self.colorData[1] = aColorself.buf = bytes(self.colorData) * 32
这个基本上都是软件操作。
最后就是draw,这个也是显示接口老熟人了,不管什么屏,最后都有这个接口。
# @micropython.nativedef _draw( self, aPixels ) :'''Send given color to the device aPixels times.'''self.dc(1)self.cs(0)for i in range(aPixels//32):self.spi.write(self.buf)rest = (int(aPixels) % 32)if rest > 0:buf2 = bytes(self.colorData) * restself.spi.write(buf2)self.cs(1)
这里就是绘制颜色,将指定的区域设置成需要的颜色。
从这里可以看到,彩色屏幕就是基本上就是围绕色彩来处理的。就是两个步骤,指定区域,指定色彩。。。
例2 circle
知道了这个原理,那么矩形框应该就是很简单的了。圆形该怎么处理呢?
# @micropython.nativedef circle( self, aPos, aRadius, aColor ) :'''Draw a hollow circle with the given radius and color with aPos as center.'''self.colorData[0] = aColor >> 8self.colorData[1] = aColorxend = int(0.7071 * aRadius) + 1rsq = aRadius * aRadiusfor x in range(xend) :y = int(sqrt(rsq - x * x))xp = aPos[0] + xyp = aPos[1] + yxn = aPos[0] - xyn = aPos[1] - yxyp = aPos[0] + yyxp = aPos[1] + xxyn = aPos[0] - yyxn = aPos[1] - xself._setwindowpoint((xp, yp))self._writedata(self.colorData)self._setwindowpoint((xp, yn))self._writedata(self.colorData)self._setwindowpoint((xn, yp))self._writedata(self.colorData)self._setwindowpoint((xn, yn))self._writedata(self.colorData)self._setwindowpoint((xyp, yxp))self._writedata(self.colorData)self._setwindowpoint((xyp, yxn))self._writedata(self.colorData)self._setwindowpoint((xyn, yxp))self._writedata(self.colorData)self._setwindowpoint((xyn, yxn))self._writedata(self.colorData)# @micropython.nativedef fillcircle( self, aPos, aRadius, aColor ) :'''Draw a filled circle with given radius and color with aPos as center'''rsq = aRadius * aRadiusfor x in range(aRadius) :y = int(sqrt(rsq - x * x))y0 = aPos[1] - yey = y0 + y * 2y0 = clamp(y0, 0, self._size[1])ln = abs(ey - y0) + 1;self.vline((aPos[0] + x, y0), ln, aColor)self.vline((aPos[0] - x, y0), ln, aColor)
可以看到,圆形的处理会慢上几个数量级。首先,画空心圆是按照点来处理的。实心圆则是变成线条来处理。要增加几十上百倍的self._writecommand(TFT.CASET),self._writecommand(TFT.RASET)调用。
例3 图片
最后再看看图片:
f=open('test128x160.bmp', 'rb')
if f.read(2) == b'BM': #headerdummy = f.read(8) #file size(4), creator bytes(4)offset = int.from_bytes(f.read(4), 'little')hdrsize = int.from_bytes(f.read(4), 'little')width = int.from_bytes(f.read(4), 'little')height = int.from_bytes(f.read(4), 'little')if int.from_bytes(f.read(2), 'little') == 1: #planes must be 1depth = int.from_bytes(f.read(2), 'little')if depth == 24 and int.from_bytes(f.read(4), 'little') == 0:#compress method == uncompressedprint("Image size:", width, "x", height)rowsize = (width * 3 + 3) & ~3if height < 0:height = -heightflip = Falseelse:flip = Truew, h = width, heightif w > 128: w = 128if h > 160: h = 160tft._setwindowloc((0,0),(w - 1,h - 1))for row in range(h):if flip:pos = offset + (height - 1 - row) * rowsizeelse:pos = offset + row * rowsizeif f.tell() != pos:dummy = f.seek(pos)for col in range(w):bgr = f.read(3)tft._pushcolor(TFTColor(bgr[2],bgr[1],bgr[0]))
可以看到,这个函数其实分成两部分。
首先是解析BMP文件格式,主要是长和宽。
然后按照长宽逐个点遍历去处理,首先读取BMP的bgr信息,bgr = f.read(3),然后调用接口将颜色一个一个的写进去。tft._pushcolor(TFTColor(bgr[2],bgr[1],bgr[0]))。
这边也顺带介绍一下BGR格式。
BGR颜色模型,全称为Blue-Green-Red,是一种颜色编码方式,常用于计算机图形学和数字图像处理中。BGR颜色模型是RGB颜色模型的一个变种,主要区别在于颜色通道的顺序。在RGB模型中,颜色由红(Red)、绿(Green)、蓝(Blue)三个颜色通道的组合来表示,而在BGR模型中,则是按照蓝、绿、红的顺序来编码颜色。
以下是BGR颜色模型的一些关键点:
1. **通道顺序**:BGR中的每个像素由三个8位(1字节)的整数表示,分别是蓝色(Blue)、绿色(Green)和红色(Red)的强度值。这三个值通常以BGR的顺序存储。
2. **颜色深度**:BGR通常用于24位颜色深度的图像,即每个像素使用3个字节来存储颜色信息。
3. **位操作**:在BGR编码中,一个像素的颜色可以通过位操作来获取和设置。例如,如果有一个32位整数存储了一个BGR颜色值,可以通过位移和掩码操作来提取每个颜色通道的值。
4. **应用领域**:BGR颜色模型在某些图像处理库和硬件接口中较为常见,比如OpenCV库在处理图像时默认使用BGR颜色空间。
5. **转换为RGB**:如果需要将BGR颜色转换为RGB颜色,只需交换第一个和最后一个字节的值即可。
6. **示例**:假设有一个BGR颜色值为`0x00BBGGRR`,其中`BB`是蓝色分量的16进制表示,`GG`是绿色分量,`RR`是红色分量。转换为RGB颜色值就是`0xRRGGBB`。
7. **编程实现**:在编程中,BGR颜色通常表示为一个整数,例如在Python中,可以使用以下方式表示BGR颜色:
```python
bgr_color = 0x00BBGGRR
```8. **颜色空间转换**:在图像处理中,可能需要在不同的颜色空间之间转换,如从BGR转换到HSV或LAB颜色空间,以适应特定的视觉处理或图像分析任务。
BGR颜色模型的使用主要是由于某些技术或历史原因,在特定的库或硬件中采用,而在大多数通用的图形和图像处理标准中,RGB模型更为常见。
这里再说一个小话题。BMP的色彩存储是RGB888,我们知道这个屏是RGB565。应该怎么转换呢?
#@micropython.native
def TFTColor( aR, aG, aB ) :'''Create a 16 bit rgb value from the given R,G,B from 0-255.This assumes rgb 565 layout and will be incorrect for bgr.'''return ((aR & 0xF8) << 8) | ((aG & 0xFC) << 3) | (aB >> 3)
RGB是三个颜色,每个颜色用0到FF来表示,不同的值表示不同的色彩强度。以红色为例,如果要压缩到5位应该怎么做呢?
r5 = (r * 31) // 255
假如色彩值是100,那么算出来大概是12.15,保留整数就是12。
这里代码怎么做的呢?直接扔掉低三位。
100的二进制是110 0100,扔掉低三位就是1100,换算成10进制就是12。明显移位的方法效率高得多。
位操作可以看这篇:C的位操作-CSDN博客
五 小结
从上面可以看出,显示驱动主要就是两个部分。
1 显示屏的初始化。设置PIN脚,然后直接就是一串SPI命令,找厂商要就行了。
2 屏幕SPI命令的封装。从ST7735的SPI命令来看,其实就是两个接口。要绘制哪里?要绘制成什么颜色?就这两个接口,然后所有的上层接口,都是围绕这两个接口的封装。画图,画线,画圆,显示图片,颜色有时候要转换成屏要的2个byte格式,让上层更方便使用。
对了,这个驱动是基于python的,很多地方都确实慢的出奇,真要堪用,还是得上C/C++。
好了,基本上就是这么回事,驱动很难吗?好像也不太难。。。