Skip to content

lrlbh/lcd

Repository files navigation

来源

我总是喜欢在网上买,最有性价比,最便宜的屏幕。

然而每次找驱动都要很多时间,micropython使用者较少,一些驱动根本找不到,而且换屏幕都要换驱动,接口不同,改代码也是非常恶心,就算找到了,一些驱动速度非常慢,基本也就是看个亮。

我打算自己写一个驱动看看。

当我写到第二个的时候,我发现所有屏幕操作逻辑都是相同的。和过去学系统编程、网络编程一样,这些操作是有规范的,只需要重写_init即可

当我写到第三个的时候发现,大部分屏幕,都不需要校准参数,所以_init也不需要重写,而且大部分商家发送资料根本无法对应屏幕,所谓校准参数有多大用,也是存疑。所以我写了这个通用驱动。

当然我只学了一两个星期,也没有找到合适的教程,结论都是写代码过程中的归纳总结,所以我的结论肯定有大量的错误,清楚来龙去脉即可,不要相信,请指正。

使用示例

初始化

# SPI
spi = SPI(
    1,
    baudrate=100_000_000,
    polarity=0,
    phase=0,
    sck=12,
    mosi=13,
    miso=None,  # 10
)

# 在lib目录中找到lcd.py
# 大部分屏幕都可以使用lcd来初始化
# 无法工作的屏幕可以在lib目录中寻找 "驱动名称.py",里面重写了lcd.py中的_init函数
# 如果无法找到,只需要将商家提供的初始化代码发送给gpt,让它重写_init方法即可
# 已知需要校准的屏幕:
	# gca9a01,需要使用商家提供的校准参数,否则工作怪异
	# gc9107,需要使用商家提供的校准参数,否则有点细微问题
st = lcd.LCD(
    spi,	# spi 对象
    cs=47,	# cs 引脚,不使用传入None
    dc=21,  # dc 引脚
    rst = 14,	# 重启引脚,软件初始传入None
    bl=48,		# 背光引脚,不使用传入None
    size=lcd.LCD.Size.st7796, # 驱动原始分辨率,用于被裁剪的屏幕,在4个旋转角度,自动生成对应偏移
    # 在 LCD.Size 中定义了一些已知的分辨率,参数格式 st7796 = (320, 480)
    旋转=0,	# 4角度旋转 0~3
    color_bit=16,	# 16 18 24,实际上16bit,和其他看不出区别
    逆CS=False,		# True = 高电平使能,我没到遇到高电平使能的,主要考虑引脚复用
    像素缺失=lcd.LCD.像素缺失.st7789_1_14, 	# 实际屏幕在0旋转时,4个角度丢失的像素
    # 可用st._test_像素裁剪()查看丢失的像素,格式(0,0,0,0)代表 上 下 左 右
)._init(反色=1,左右镜像= 1,rgb=1)  # 考虑到初始化需要加延迟,所以单独一个函数方便使用 asyncio
	# 黑白颜色反了用 反色
    # 左右倒了用 左右镜像
    # 红蓝颜色反了用 rgb
    
    
# 实际上初始化大概率只需这三个参数: spi,dc,驱动分辨率
st = lcd.LCD(
    spi,
    dc=21,
    size=lcd.LCD.Size.st7796
)._init()

字符显示

st.txt(
    字符串="阿斯顿asd",	# 要显示什么字符串
    x=20,	# 宽起点
    y=20,	# 高起点
    size=32,	# 字体大小
    字体色=st.color.,	# 字体色
    背景色=st.color.,	# 背景色
    缓存=True,	# 本次显示的字符中,有未被缓存的,是否加入缓存。有缓存是包用的,不受影响。
)

################使用取模软件时,字体右旋转90度###############################

# 常用软件导入
# lcd里面有一个_char属性用于保存点阵字符,里面为了方便调试加了几个字符,可以查看加入点阵字符的方式
st._char[32] = {}
st._char[32]["阿"] = bytes([0x00,0x00...])

# 从字库中加载
# 字库生成软件在这里 https://github.com/lrlbh/lcd字库
# 某天不小心把加载完整字库的函数删了,现在只能用选择性加载

# 方式1: def 字符中内置了16,24,32,40,48,56,64,72的ascii和几个常用字符,你可以继续添加
st.def_字符.all = "的身份人格完善的法律就能很快就"
st.load_bmf("/字库.bmf")

# 方式2:
st.load_bmf("/字库.bmf",{
    16:"caxzsdgfsdfgDADSZF撒法帝国",
    32:"zxcgvsedfg的说法是德国"
})

小工具

# 清屏,这个屏幕一次清除的,如果内存小,里面发送数据循环一下
st.fill(st.color.)

# 颜色
st.color.XX # 可以访问内置颜色,不同实例指向不同bit,多屏幕多实例情况下,可以放心使用
st.color_fn(255,255,255) # RGB顺序创建一个颜色,不同bit指向不同函数,修改bit也没事

# 同一款驱动基本只有一个分辨率,然而有多种分辨率屏幕,实际上有些没有使用,遇到这种情况需要计算
# 运行后显示一个图案,四周有边框,白色表示左上方
# 通过初始化时的参数 像素缺失= (0,0,0,0) 丢弃四边的像素,知道边框出现即可
# 调整旋转0即可,其他3个角度会自动适配,前提是初始化时驱动分辨率参数 size=(240,320) 传入正确
st._test_像素裁剪()	


# 旋转屏幕,查看偏移有没有问题
st._test()
# 4角度旋转测试
while True:
    for i in range(0, 4):
        st = lcd.LCD(
            spi,
            cs=47,
            dc=21,
            rst=14,
            bl=48,
            size=驱动,
            旋转=i,
            color_bit=16,
            逆CS=False,
            像素缺失=像素补偿,
        )._init()#反色=True, RGB=True)
        # udp.send(f"-----------------旋转{i}---------------------")
        # udp.send(f"驱动分辨率w-h{st._width_驱动, st._height_驱动}")
        # udp.send(f"逻辑分辨率w-h{st._width, st._height}")
        # udp.send("----------------------------------------------")
        st._test()
        time.sleep(1)

波形显示

# 波形测试
st.fill(st.color.)
bx = st.new_波形(
    w起点=20,	# 宽起点
    h起点=20,	# 高起点
    size_w=200,	# 波形区域宽
    size_h=50,	# 波形区域高
    波形像素=[3, 3, 3],	# 波形性有多粗,列表是因为可以传入多个通道数据
    多少格=998,	# 加了网格不好看,已废弃,没用
    data_min=[0, 0, 0],	# 每个通道,显示范围
    data_max=[33, 66, 99],# 每个通道,显示范围
    波形色=[st.color., st.color.绿, st.color.], # 每个通道的颜色
    背景色=st.color., # 波形区域背景色
)

bx1 = st.new_波形(
    w起点=20,
    h起点=90,
    size_w=200,
    size_h=100,
    波形像素=[6, 6, 6],
    多少格=998,
    data_min=[0, 0, 0],
    data_max=[33, 66, 99],
    波形色=[st.color., st.color.绿, st.color.],
    背景色=st.color.,
)


bx2 = st.new_波形(
    w起点=20,
    h起点=210,
    size_w=200,
    size_h=50,
    波形像素=[6, 6, 6],
    多少格=998,
    data_min=[0, 0, 0],
    data_max=[33, 66, 99],
    波形色=[st.color., st.color.绿, st.color.],
    背景色=st.color.,
)


bx3 = st.new_波形(
    w起点=20,
    h起点=280,
    size_w=200,
    size_h=40,
    波形像素=[4, 4, 4],
    多少格=998,
    data_min=[0, 0, 0],
    data_max=[33, 66, 99],
    波形色=[st.color., st.color.绿, st.color.],
    背景色=st.color.,
)


t1, t2, t3 = 0, 0, 0
tt1, tt2, tt3 = 1, 1, 1

while True:

    bx.append_data([t1, t2, t3])	# 添加一个点数据
    bx.更新()	# 里面有一份环形内存,保存了完整波形数据,不用每次都更新,有空了更新一下就行

    bx1.append_data([t1, t2, t3])
    bx1.更新()

    bx2.append_data([t1, t2, t3])
    bx2.更新()

    bx3.append_data([t1, t2, t3])
    bx3.更新()

    # udp.send(t1)
    # udp.send(t2)
    # udp.send(t3)
    if t1 >= 33:
        tt1 = -1
    if t2 >= 66:
        tt2 = -1
    if t3 >= 99:
        tt3 = -1
    if t1 <= 0:
        tt1 = 1
    if t2 <= 0:
        tt2 = 1
    if t3 <= 0:
        tt3 = 1
    t1 += tt1
    t2 += tt2
    t3 += tt3

可直接运行的示例

# 也许可以在 示例.py 中找到最新的

import time
from machine import SPI
try:
    import lcd
except ImportError:
    from lib import lcd


def get_st(旋转):
    # 老板子引脚
    spi = SPI(
        1,
        baudrate=100_000_000,
        polarity=0,
        phase=0,
        sck=12,
        mosi=13,
        miso=None,  # 10
    )

    # lcd.def_字符.all = "的身份人格完善的法律就能很快就"
    return lcd.LCD(
        spi,
        cs=47,
        dc=21,
        # rst=None,
        rst=14,
        bl=48,
        size=lcd.LCD.Size.st7789,
        旋转=旋转,
        color_bit=16,
        逆CS=False,
        像素缺失=(0, 0, 0, 0),
    )._init(反色=1, 左右镜像=1, rgb=1)  # .load_bmf("/字库.bmf")

    # st.fill(st.color.白)

    # st.load_bmf(
    #     "/字库.bmf",
    #     {
    #         16: "caxzsdgfsdfgDADSZF撒法帝国",
    #         32: "zxcgvsedfg的说法是德国",
    #     },
    # )

# 显示字符
st = get_st(3)
st.txt(
    字符串="阿斯顿asd",
    x=20,
    y=20,
    size=32,
    字体色=st.color.,
    背景色=st.color.,
    缓存=True,
)
time.sleep(4)

# 丢弃像素
# st._test_像素裁剪()
# while True:
#     pass


# 4角度旋转
for i in range(0, 4):
    st = get_st(i)
    st._test()
    time.sleep(3)
    # time.sleep(1000000000)


# 波形
st = get_st(3)
bx = st.new_波形(
    w起点=20,
    h起点=20,
    size_w=200,
    size_h=50,
    波形像素=[3, 3, 3],
    多少格=998,
    data_min=[0, 0, 0],
    data_max=[33, 66, 99],
    波形色=[st.color., st.color.绿, st.color.],
    背景色=st.color.,
)

bx1 = st.new_波形(
    w起点=20,
    h起点=90,
    size_w=200,
    size_h=100,
    波形像素=[6, 6, 6],
    多少格=998,
    data_min=[0, 0, 0],
    data_max=[33, 66, 99],
    波形色=[st.color., st.color.绿, st.color.],
    背景色=st.color.,
)


bx2 = st.new_波形(
    w起点=20,
    h起点=210,
    size_w=200,
    size_h=50,
    波形像素=[6, 6, 6],
    多少格=998,
    data_min=[0, 0, 0],
    data_max=[33, 66, 99],
    波形色=[st.color., st.color.绿, st.color.],
    背景色=st.color.,
)


bx3 = st.new_波形(
    w起点=20,
    h起点=280,
    size_w=200,
    size_h=40,
    波形像素=[4, 4, 4],
    多少格=998,
    data_min=[0, 0, 0],
    data_max=[33, 66, 99],
    波形色=[st.color., st.color.绿, st.color.],
    背景色=st.color.,
)


t1, t2, t3 = 0, 0, 0
tt1, tt2, tt3 = 1, 1, 1

while True:
    bx.append_data([t1, t2, t3])
    bx.更新()

    bx1.append_data([t1, t2, t3])
    bx1.更新()

    bx2.append_data([t1, t2, t3])
    bx2.更新()

    bx3.append_data([t1, t2, t3])
    bx3.更新()

    # udp.send(t1)
    # udp.send(t2)
    # udp.send(t3)
    if t1 >= 33:
        tt1 = -1
    if t2 >= 66:
        tt2 = -1
    if t3 >= 99:
        tt3 = -1
    if t1 <= 0:
        tt1 = 1
    if t2 <= 0:
        tt2 = 1
    if t3 <= 0:
        tt3 = 1
    t1 += tt1
    t2 += tt2
    t3 += tt3

可以更新功能

  • 有一个硬件波形,试试看

  • 快速一次性加载所有字体的函数,很早就被误删了,需要重新实现,不过先得重新查看一下自制bmf的格式

  • 3线spi,还可以省略 DC 引脚

  • 使用新的刷新逻辑,先上后下,在左在右

    • 当初为了实现列刷新,我简单看了一下4个角度显示内容,发现只要两个简单操作就可以
    • 内容右旋90度然后补偿一个逻辑高度就可以
    • 内容左旋转90度,补偿一个逻辑宽度就可以
    • 当时随便选了一个补偿高度的,随着时间发展,我发现了一个很大弊端,使用gpt实现代码时,它完全无法理解这种刷新逻辑 先(下 -- >上) 后(左-->右),或许是我语言表达能力有问题,我总是得多次纠正它,总之与gpt冲突的代码无疑是非常愚蠢的代码
  • 创建一行显示区域,记录里面的内容,更新时只更新变化的字符。此区域还可以分段,每段自增坐标,自动产长度,每段可以分上中下,显示不同大小字符。-->部分尝试了下,目前尝试部分,还可以加一个更新时使用默认起点用第一个 : 记录

  • 软件波形生成,多像素在往上下增长,目前是多像素单边增长,另外记录上一次波形坐标当变化激烈时也使用一个像素连接,算了随便搞下,有别的事了,上下增长没写,简单连接了下

  • 使用示例

  • TE引脚播放视频测试

  • 用杜邦线连接,cs使能时,dc切换时,需要加点延迟

  • 减少颜色

  • 预设色彩为类属性,多屏幕使用不同bit会冲突

  • 某些数据,会占用大量内存,多个实例总体维护一份即可

  • 单个字符缓存大小,由加载字符时最大尺寸字符决定,算了有时也会手动添加几个

  • 已经是列刷新了,字符显示可以加快,不使用多次 _set_window

  • 波形更新,添加min数据、多波形单独像素 和 自适应模式,没有管自适应,每次变化都要生成1帧数据应该快不了

  • 4线SPI下,理论上非常简单就可以节省引脚,测试无误后支持它

    • 1个SPI设备,3个引脚,SCL,SDA,DC
    • 2个SPI设备,4个引脚,SCL,SDA,DC,CS,CS反向
    • 3个及以上SPI设备
      • 似乎在很多地方,听说一种二进制 转 数据位的芯片,2CS挂4个设备,3个CS挂8个设备
      • 上面方法还是需要大于一个引脚,猜测应该有1扩N的数字芯片,就是不知道是否可以白嫖
  • 更多驱动

    • 以前购买过的屏幕

    • 7789 240 * 320 240 * 280 240 * 240 135 * 240 170 * 320

    • GC9A01 240 * 240

    • 7735 80 * 160 128 * 160 128 * 128

    • GC9107 128 * 128

    • ili9488 320*240

    • st7796 320* 240

速度问题记录 (测试ESP32-S3,SPI=80M)

# 刷新大量数据时 (ESP32S3)
	# 推算实际为	30MHZ
    # 示波器查看 40MHZ
    # 网上搜索	 80MHZ
	# 官网某个文档看到,默认引脚80MHZ,非默认引脚40MHZ,未验证
    
# 问题1:少10Mhz问题,单独记录每一次刷新屏幕耗时,刷新460800字节,出现了2种数据	(已解决)
    # 1、速度 95 ~ 105ms, 基本稳定在100已内,推算SPI为40MHZ完全符合
    # 2、速度 160 ~ 180ms
        # 我在开发时,使用的是网络更新,有一个单独的线程频繁发送网络请求,也许是网络线程打断了
        # 由于SPI.write(),传入的是memoryview,所以忘记查看是否会是GC导致了
        
# 问题2:少40Mhz问题(已解决)
	# 问题也许来自分频(?),不过我不知道那是什么东西,不过我大约听说过SPI频率和时钟树(?)有关
    # 基于问题,推测SPI无法设置指定频率,只能选择几个固定频率,所以指定的频率未曾生效
    	# 解决,给一个 >= 最高频率的频率
        # 10MHZ、20MHZ、26.66MHZ、40MHZ、80MHZ,随便指定了几个频率返回的频率,示波器只验证了80MHZ
        # 似乎没规律,有兴趣在搜一下看看,实际原因
    # 80MHZ写入上面同样的数据返回时间,推算SPI为80MHZ完全符合
    	# 1、速度 49ms最多,50ms次之
        # 2、速度 119 ~ 121ms


# 使用问题已经完全解决,剩下两个小问题
	# 1、SPI写入时,偶尔耗时会变多60~70MS
    	# 判断网络线程工作的原因
        # 因为无论频率如何,平均时间总是多了20MS左右。
        # 如果是GC数据量都翻倍了。
    # 2、SPI频率只能使用一些固定值,是因为什么

ESP32S3 N16R8模组 硬盘和网络访问速度

# 官方模组硬盘访问速度
0.8MiB~1MiB

# 非官方模组硬盘访问速度
1.1MiB~1.3MiB

# 在github上看到过,有人通过修改固件硬盘读取速度达到了6M还是8M去了,修改前速度和我基本一致


# WIFI速度 N年前拉宽带自带的路由器,距离几十厘米

# Wi-Fi 已连接: 192.168.1.11
# TCP 已连接
1.6155909MiB/s 


# Wi-Fi已连接: 192.168.1.2
# UDP监听中...
2.8595062MiB/s 


# 1472,UDP单包计时 最快325us 
>>> 1_000_000 / 325 * 1472 / 1024 / 1024
4.319411057692308

>>> 1_000_000 / 325 * 1500 / 1024 / 1024
4.401573768028847

>>> 1_000_000 / 300 * 1500 / 1024 / 1024
4.76837158203125

>>> 1_000_000 / 300 * 1500 / 1024 / 1024 * 8
38.14697265625

几个速度记录

MPY: soft reboot
空循环1W次耗时: 24773
空循环2W次耗时: 49393
空循环4W次耗时: 99071
循环1W次空函数耗时: 88534
循环2W次空函数耗时: 177037
循环4W次空函数耗时: 353925
循环1W次复杂类空成员函数数耗时: 110882
循环2W次复杂类空成员函数数耗时: 222157
循环4W次复杂类空成员函数数耗时: 444956
循环1W次简单类空成员函数耗时: 108202
循环2W次简单类空成员函数耗时: 216783
循环4W次简单类空成员函数耗时: 433752
循环1W次全局变量耗时: 37346
循环2W次全局变量耗时: 74733
循环4W次全局变量耗时: 149199
循环1W次简单类空成员变量耗时: 40254
循环2W次简单类空成员变量耗时: 80546
循环4W次简单类空成员变量耗时: 161271
循环1W次复杂类空成员变量耗时: 40491
循环2W次复杂类空成员变量耗时: 80717
循环4W次复杂类空成员变量耗时: 161248
循环1W次if耗时: 47888
循环2W次if耗时: 96006
循环4W次if耗时: 240192