显示学习5(基于树莓派Pico) -- 彩色LCD的驱动

一 环境搭建

和这篇也算是姊妹篇,只是一个侧重SPI协议,一个侧重显示驱动。

总线学习3--SPI-CSDN博客

驱动是来自:https://github.com/boochow/MicroPython-ST7735

所以这里主要还是学习大神的代码。

二 启动

启动阶段的代码是这样的:

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 = ScreenSize
    self._offset = bytearray([0,0])
    self.rotate = 0                    #Vertical with top toward pins.
    self._rgb = True                   #color order of rgb.
    self.tfa = 0                       #top fixed area
    self.bfa = 0                       #bottom fixed area
    self.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 = spi
    self.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

不过这样肯定会导致色彩数不足,显示效果没那么好了。。。

屏幕初始化

 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 control
    data1[0] = 0x07                             #Line inversion.
    self._writedata(data1)

    self._writecommand(TFT.PWCTR1)               #Power control
    data3[0] = 0xA2
    data3[1] = 0x02
    data3[2] = 0x84
    self._writedata(data3)

    self._writecommand(TFT.PWCTR2)               #Power control
    data1[0] = 0xC5   #VGH = 14.7V, VGL = -7.35V
    self._writedata(data1)

    data2 = bytearray(2)
    self._writecommand(TFT.PWCTR3)               #Power control
    data2[0] = 0x0A   #Opamp current small
    data2[1] = 0x00   #Boost frequency
    self._writedata(data2)

    self._writecommand(TFT.PWCTR4)               #Power control
    data2[0] = 0x8A   #Opamp current small
    data2[1] = 0x2A   #Boost frequency
    self._writedata(data2)

    self._writecommand(TFT.PWCTR5)               #Power control
    data2[0] = 0x8A   #Opamp current small
    data2[1] = 0xEE   #Boost frequency
    self._writedata(data2)

    self._writecommand(TFT.VMCTR1)               #Power control
    data1[0] = 0x0E
    self._writedata(data1)

    self._writecommand(TFT.INVOFF)

    self._writecommand(TFT.MADCTL)               #Power control
    data1[0] = 0xC8
    self._writedata(data1)

    self._writecommand(TFT.COLMOD)
    data1[0] = 0x05
    self._writedata(data1)

    self._writecommand(TFT.CASET)                #Column address set.
    self.windowLocData[0] = 0x00
    self.windowLocData[1] = 0x00
    self.windowLocData[2] = 0x00
    self.windowLocData[3] = self._size[0] - 1
    self._writedata(self.windowLocData)

    self._writecommand(TFT.RASET)                #Row address set.
    self.windowLocData[3] = self._size[1] - 1
    self._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.native
  def _writecommand( self, aCommand ) :
    '''Write given command to the device.'''
    self.dc(0)
    self.cs(0)
    self.spi.write(bytearray([aCommand]))
    self.cs(1)

  #@micropython.native
  def _writedata( self, aData ) :
    '''Write given data to the device.  This may be
       either 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 TFTBGR
    self._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接口和上面差不多一样。

在RGB颜色模型中,一个点的颜色通常由红色(Red)、绿色(Green)和蓝色(Blue)三个颜色通道的强度值来表示。每个通道的值通常可以在0到255的范围内变化(对于8位颜色深度),这样就可以组合出大约1677万种不同的颜色。

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, start
    self._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 >> 8
    self.colorData[1] = aColor
    self.buf = bytes(self.colorData) * 32

这个基本上都是软件操作。

最后就是draw,这个也是显示接口老熟人了,不管什么屏,最后都有这个接口。

#   @micropython.native
  def _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) * rest
        self.spi.write(buf2)
    self.cs(1)

这里就是绘制颜色,将指定的区域设置成需要的颜色。

从这里可以看到,彩色屏幕就是基本上就是围绕色彩来处理的。就是两个步骤,指定区域,指定色彩。。。

例2 circle

知道了这个原理,那么矩形框应该就是很简单的了。圆形该怎么处理呢?

#   @micropython.native
  def circle( self, aPos, aRadius, aColor ) :
    '''Draw a hollow circle with the given radius and color with aPos as center.'''
    self.colorData[0] = aColor >> 8
    self.colorData[1] = aColor
    xend = int(0.7071 * aRadius) + 1
    rsq = aRadius * aRadius
    for x in range(xend) :
      y = int(sqrt(rsq - x * x))
      xp = aPos[0] + x
      yp = aPos[1] + y
      xn = aPos[0] - x
      yn = aPos[1] - y
      xyp = aPos[0] + y
      yxp = aPos[1] + x
      xyn = aPos[0] - y
      yxn = aPos[1] - x

      self._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.native
  def fillcircle( self, aPos, aRadius, aColor ) :
    '''Draw a filled circle with given radius and color with aPos as center'''
    rsq = aRadius * aRadius
    for x in range(aRadius) :
      y = int(sqrt(rsq - x * x))
      y0 = aPos[1] - y
      ey = y0 + y * 2
      y0 = 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':  #header
    dummy = 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 1
        depth = int.from_bytes(f.read(2), 'little')
        if depth == 24 and int.from_bytes(f.read(4), 'little') == 0:#compress method == uncompressed
            print("Image size:", width, "x", height)
            rowsize = (width * 3 + 3) & ~3
            if height < 0:
                height = -height
                flip = False
            else:
                flip = True
            w, h = width, height
            if w > 128: w = 128
            if h > 160: h = 160
            tft._setwindowloc((0,0),(w - 1,h - 1))
            for row in range(h):
                if flip:
                    pos = offset + (height - 1 - row) * rowsize
                else:
                    pos = offset + row * rowsize
                if 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模型更为常见。

五 小结

从上面可以看出,显示驱动主要就是两个部分。

1 显示屏的初始化。设置PIN脚,然后直接就是一串SPI命令,找厂商要就行了。

2 屏幕SPI命令的封装。从ST7735的SPI命令来看,其实就是两个接口。要绘制哪里?要绘制成什么颜色?就这两个接口,然后所有的上层接口,都是围绕这两个接口的封装。画图,画线,画圆,显示图片,颜色有时候要转换成屏要的2个byte格式,让上层更方便使用。

对了,这个驱动是基于python的,很多地方都确实慢的出奇,真要堪用,还是得上C/C++。

好了,基本上就是这么回事,驱动很难吗?好像也不太难。。。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 树莓派Pico是树莓派推出的一款微型电脑,Pico中文版是针对中国市场推出的版本。Pico-Python-SDK是树莓派官方提供的软件开发工具包,用于编写和运行Python程序。Pico-Python-SDK中文版是根据中国用户的需求进行本土化开发的版本。 树莓派Pico是一款体积小巧但功能强大的微型电脑,搭载了ARM Cortex-M0+处理器和26个GPIO口,可用于连接各种传感器、执行各种控制任务。同时,Pico支持MicroPython和C/C++两种编程语言,非常适合初学者和嵌入式系统开发人员。 Pico-Python-SDK中文版是官方提供的软件开发工具包,它提供了一系列的API和库函数,可以帮助开发者更方便地编写和调试Python程序。无论是控制外部硬件设备还是实现各种功能,Pico-Python-SDK都提供了丰富的功能和示例代码,使开发工作更加便捷。 Pico-Python-SDK中文版还提供了中文文档和教程,有助于开发者更快速地上手。通过学习这些文档和教程,开发者可以了解Pico的基本原理、功能特性以及编程方法,从而高效地开发出各种应用。 总的来说,树莓派Pico-Python-SDK中文版为中国用户提供了一个方便、易用的软件开发平台,帮助开发者快速上手并实现各种创意和项目。无论是学习编程还是进行嵌入式系统开发Pico-Python-SDK中文版都是一个不错的选择。 ### 回答2: 树莓派pico-python-sdk-中文版是为了方便中国开发者使用而推出的一款软件开发工具包。它是基于树莓派pico微控制器设计的,通过使用Python语言进行开发。该SDK提供了丰富的功能和接口,使得使用者能够轻松地进行树莓派pico的开发工作。 树莓派pico-python-sdk-中文版包含了各种模块和库,如GPIO库、PWM库、I2C库、SPI库等,这些库可以帮助开发者通过编写Python代码来操控各种外设和传感器。例如,使用GPIO库可以控制LED灯的开关,使用PWM库可以控制舵机的转动,使用I2C库可以与其他设备进行通信,使用SPI库可以实现高速数据传输等等。 此外,树莓派pico-python-sdk-中文版还提供了丰富的示例代码和文档,帮助开发者快速上手。开发者可以参考这些示例代码和文档,了解如何使用SDK中的功能和接口,从而加速自己的开发进程。 总之,树莓派pico-python-sdk-中文版是一款功能强大、易于学习和使用的软件开发工具包。它不仅方便中国开发者使用树莓派pico进行开发工作,还提供了丰富的功能和接口,使得开发者能够更加便捷地实现各种应用和项目。无论是初学者还是有经验的开发者,都可以通过使用这个SDK来实现自己的创意和想法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值