[原创] 【年终回炉 赛昉星光2】04-驱动微雪4.26inch电子墨水屏

CoderX9527   2026-3-21 16:13 楼主
 
背景
微雪 4.26inch 电子墨水屏介绍
各项参数
通信方式
硬件连接
GPIO 输入输出、中断测试
输入、输出相关的API
中断相关的API
测试代码
实测结果
SPI 通信测试
安装 spidev 库
测试代码
实测结果
电子墨水屏驱动移植
驱动分析
VisionFive2 类的实现
电子墨水屏效果展示
背景
微雪 4.26inch 电子墨水屏介绍
官方资料链接:
https://www.waveshare.net/wiki/4.26inch_e-Paper_HAT_Manual#.E8.BF.90.E8.A1.8Cpython.E4.BE.8B.E7.A8.8B
wd_161044f18pbbp1mjkrxb2k.png
各项参数
wd_161044yng2kg7cn8duk6bf.png
通信方式
wd_161045ytt2lzl3knzxltml.png
  • CSB(CS):从机片选信号,低电平有效,为低电平的时候,芯片使能。
  • SCL(SCK/SCLK):串行时钟信号。
  • D/C(DC):数据/命令控制信号,低电平时写入命令(Command);高电平时写入数据(Data/parameter)。
  • SDA(DIN):串行数据信号。
  • 时序:CPHL=0,CPOL=0,即 SPI 模式0。【切记】
硬件连接
wd_161045tw7afohpuovpphsd.png
wd_161045fk4nrxynlkqxfeqv.png
由于参考树莓派驱动,所以需要对照树莓派的硬件连接,40PIN排针直接插上去,所以必须要对好引脚。
下表,左侧是屏和树莓派的引脚对应关系,右侧红字是我比对40PIN引脚,对应的星光2引脚。
40PIN中所用的几个管脚功能可以配置成一样的,其中 spi 正好对应,只需要把几个GPIO配置成对应的输入、输出模式即可。
wd_161045rqybjfo3y2gff3f2.png
即星光2资源需求:
  • GPIO 输出输出,可以包括中断;
  • SPI 通信
GPIO 输入输出、中断测试
查看 VisionFive.gpio 帮助文档的办法,在 python 命令行界面输入 help(GPIO) 就可以看到相关的文档。
导出 VisionFive.gpio 帮助文档到 gpio_help.txt 的命令为 python -m pydoc VisionFive.gpio > gpio_help.txt
输入、输出相关的API
wd_161045yl4t024ql30jmqrj.png
中断相关的API
wd_161045eoao8j72mt0zgdom.png
测试代码
验证 GPIO 三种功能:GPIO44 每 2 秒翻转电平(输出)、GPIO61 每 0.5 秒扫描一次状态(输入轮询)、GPIO36 通过中断回调检测边沿变化。
 
# -*- coding: utf-8 -*-
import VisionFive.gpio as GPIO
import time
import logging
import sys

# --- 配置 Logging ---
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s',
    handlers=[
        logging.StreamHandler(sys.stdout),
        logging.FileHandler("gpio_official_test.log")
    ]
)
logger = logging.getLogger("VisionFive2_GPIO")

# --- 全局常量定义 (BCM 编号) ---
# 输出引脚:GPIO44 -> Board Pin 40
OUT_PIN = 44   
# 扫描输入引脚:GPIO61 -> Board Pin 38
IN_SCAN_PIN = 61  
# 中断输入引脚:GPIO36 -> Board Pin 36
IN_INT_PIN = 36   

# 状态记录
out_state = GPIO.LOW
last_toggle_time = 0
last_scan_time = 0

def detect_callback(pin, edge_type):
    """
    官方手册规范回调函数
    edge_type: 1 为 Rising (上升沿), 2 为 Falling (下降沿)
    """
    if edge_type == 1:
        msg = "Rising edge is detected (上升沿)"
    elif edge_type == 2:
        msg = "Falling edge is detected (下降沿)"
    else:
        msg = f"Unknown edge type: {edge_type}"
    
    logger.info("🔔 [INTERRUPT] Pin: GPIO%d | %s", pin, msg)

def setup():
    """
    硬件初始化:完全遵循官方手册示例
    """
    global last_toggle_time, last_scan_time
    
    try:
        # 设置编号模式
        GPIO.setmode(GPIO.BCM)
        GPIO.setwarnings(False)

        # 1. 配置输出 (Board 40)
        GPIO.setup(OUT_PIN, GPIO.OUT, initial=GPIO.LOW)
        logger.info("✅ Setup: GPIO44 (Board 40) 输出已就绪")

        # 2. 配置扫描输入 (Board 38)
        GPIO.setup(IN_SCAN_PIN, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
        logger.info("✅ Setup: GPIO61 (Board 38) 扫描已就绪")

        # 3. 配置中断输入 (Board 36)
        GPIO.setup(IN_INT_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        
        # 按照官方手册用法添加事件检测
        # bouncetime=20 设置 20ms 的消抖时间
        GPIO.add_event_detect(
            IN_INT_PIN, 
            GPIO.BOTH, 
            callback=detect_callback, 
            bouncetime=20
        )
        logger.info("✅ Setup: GPIO36 (Board 36) 中断已就绪 (Mode: BOTH)")

        last_toggle_time = time.time()
        last_scan_time = time.time()
        
    except Exception as e:
        logger.error("❌ Setup 异常: %s", str(e))
        sys.exit(1)

def loop():
    """
    非阻塞任务调度循环
    """
    global out_state, last_toggle_time, last_scan_time
    current_time = time.time()

    # 任务 1: GPIO44 每 2 秒切换一次输出
    if current_time - last_toggle_time >= 2.0:
        out_state = not out_state
        GPIO.output(OUT_PIN, out_state)
        logger.info("💡 [OUTPUT] GPIO44 -> %s", "HIGH" if out_state else "LOW")
        last_toggle_time = current_time

    # 任务 2: GPIO61 每 0.5 秒扫描一次输入
    if current_time - last_scan_time >= 0.5:
        val = GPIO.input(IN_SCAN_PIN)
        logger.info("🔍 [INPUT ] GPIO61 -> %d", val)
        last_scan_time = current_time

    # 适当休眠,降低 CPU 占用
    time.sleep(0.01)

def main():
    """
    程序主入口
    """
    logger.info("🚀 VisionFive 2 GPIO 官方规范测试程序启动...")
    setup()
    
    try:
        while True:
            loop()
    except KeyboardInterrupt:
        logger.info("⏹ 用户停止程序")
    finally:
        # 移除事件监听并清理
        GPIO.remove_event_detect(IN_INT_PIN)
        GPIO.cleanup()
        logger.info("🧹 资源已清理并安全退出")

if __name__ == "__main__":
    main()

 

 

实测结果
在测试时用一根杜邦线A端接 GPIO44,B端解除 GPIO61 演示输入功能;然后杜邦线B端接入 GPIO36 演示中断触发功能。
wd_161045w92dsg32sogsyood.png
SPI 通信测试
首先使用 VisionFive.gpio 库中的 spi 驱动,测试失败,自收自发不能成功,放弃了,转而使用 spidev 库。
安装 spidev 库
输入 pip install spidev 就可以。
wd_161045xa8su8s188qq2919.png
测试代码
下面是VisionFive 2 的 SPI 压力测试程序,通过 spidev 库遍历 4 种 SPI 模式和 5 种速率(100K-10MHz)。核心逻辑:每个循环配置一种参数,发送动态生成的字符串数据,通过 xfer2 同步传输并校验回环数据,验证 SPI 通信可靠性。程序持续循环测试所有组合,支持 Ctrl+C 安全退出并关闭 SPI 设备。
 
# -*- coding: utf-8 -*-
import spidev
import time
import logging
import sys

# --- 配置 Logging ---
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s',
    handlers=[logging.StreamHandler(sys.stdout)]
)
logger = logging.getLogger("VF2_Spidev_Matrix")

# --- 测试矩阵配置 ---
BUS = 1
DEVICE = 0  # 对应 /dev/spidev1.0
SPEEDS = [100000, 500000, 1000000, 5000000, 10000000] # 100K 到 10M
MODES = [0, 1, 2, 3]

# 状态追踪
speed_idx = 0
mode_idx = 0
spi = None

def setup():
    """初始化 spidev 实例"""
    global spi
    try:
        spi = spidev.SpiDev()
        spi.open(BUS, DEVICE)
        logger.info("✅ 已打开 SPI 设备: /dev/spidev%d.%d", BUS, DEVICE)
        return True
    except Exception as e:
        logger.error("❌ 无法打开 SPI 设备: %s (请检查权限: sudo chmod 666 /dev/spidev1.0)", str(e))
        return False

def loop():
    """主测试循环:遍历所有模式和速率"""
    global speed_idx, mode_idx, spi
    
    current_speed = SPEEDS[speed_idx]
    current_mode = MODES[mode_idx]
    
    # 1. 配置当前测试参数
    spi.max_speed_hz = current_speed
    spi.mode = current_mode
    
    # 2. 生成动态测试数据
    test_str = f"M{current_mode}-S{current_speed//1000}K"
    test_data = [ord(c) for c in test_str]
    
    logger.info("--- 测试配置: Mode %d | Speed %d Hz ---", current_mode, current_speed)
    
    try:
        # 3. 执行同步传输
        # xfer2 会在传输期间保持 CS (片选) 信号有效,最适合回环测试
        start_time = time.perf_counter()
        resp = spi.xfer2(test_data)
        duration = (time.perf_counter() - start_time) * 1000 # 毫秒
        
        # 4. 校验数据
        if resp == test_data:
            recv_str = "".join([chr(b) for b in resp])
            logger.info("✅ [成功] 耗时: %.2fms | 收到数据: '%s'", duration, recv_str)
        else:
            logger.error("❌ [失败] 数据不一致!")
            logger.error("发送: %s", test_data)
            logger.error("接收: %s", resp)
            
    except Exception as e:
        logger.error("❌ 传输过程中发生错误: %s", str(e))

    # 5. 更新索引,遍历矩阵
    speed_idx += 1
    if speed_idx >= len(SPEEDS):
        speed_idx = 0
        mode_idx += 1
        if mode_idx >= len(MODES):
            mode_idx = 0
            logger.info("🎉 所有测试矩阵已完成一遍。")

    print("-" * 55)
    time.sleep(1)

def main():
    logger.info("🚀 开始星光 2 SPI 压力测试 (spidev 库)")
    logger.info("测试范围: 模式 0-3, 速率 100K-10M")
    
    if not setup():
        sys.exit(1)
        
    try:
        while True:
            loop()
    except KeyboardInterrupt:
        logger.info("⏹ 用户手动停止测试")
    finally:
        if spi:
            spi.close()
        logger.info("🧹 SPI 已关闭,测试程序退出")

if __name__ == "__main__":
    main()

 


实测结果
测试模式0~3,测试速率100K, 500K, 1M, 5M 和 10M 。所有测试都通过了。
wd_161045hcj3e6zg33mqy3m3.png
电子墨水屏驱动移植
微雪 4.26inch e-Paper HAT 官方资料链接
https://www.waveshare.net/wiki/4.26inch_e-Paper_HAT_Manual#.E8.BF.90.E8.A1.8Cpython.E4.BE.8B.E7.A8.8B
首先安装依赖 sudo apt install python3-pip python3-pil python3-numpy
wd_161045dphrnr2ir1x87ux1.png
驱动分析
下载微雪电子墨水屏驱动,并改名为 waveshare_epaper_sbc,它的驱动层在 python/lib/waveshare_epd/epdconfig.py 文件中。
wd_161045aknuudwhwdrnmvak.png
这份驱动原本支持 RaspberryPi, JetsonNano, SunriseX3。我之前移植过 TI AM62L EVM 驱动,所以比较清楚。
这份文件的结构如下:
  • 先定义 VisionFive2, RaspberryPi 等类,实现 GPIO, SPI 驱动接口
  • 然后在文件末尾判定当前在哪种系统上,然后创建一个 implementation 对象,它的实例化来自 VisionFive2, RaspbverryPi 某一个类;
  • 最后把 implementation 的各个属性注册到 sys.modules 上。
wd_161045qwzng1qk55zao1wq.png
wd_161045fzmz9p8xxxk0e08y.png
VisionFive2 类的实现
星光 2 (VisionFive 2) 适配的硬件驱动抽象层,核心逻辑如下:
  • 双库集成:结合官方 VisionFive.gpio(使用 BOARD 物理引脚 编号)与标准 spidev 库,实现了对 E-Ink 或 LCD 屏幕外设的低层控制。
  • 硬件 SPI 优化:放弃 GPIO 模拟片选,直接利用 SoC 硬件 CS 引脚(GPIO49)以提升传输效率,并固定在 Mode 04MHz 速率。
  • 状态化资源管理:引入 _spi_opened 标志位,确保 SPI 句柄单次开启、持久复用,避免重复初始化导致的系统泄露。
  • 完善的生命周期:通过 module_exit 和析构函数 (del) 实现双重保障,确保在程序崩溃或退出时,能自动释放 GPIO 资源并关闭电源(PWR/RST)以进入低功耗状态。

 


class VisionFive2:
    # Pin definition
    # GPIO 采用 BOARD 编号而非 BCM 编号
    RST_PIN = 11 # GCM.GPIO42

    DC_PIN = 22 # BCM.GPIO50

    PWR_PIN = 12 # BCM.GPIO38

    BUSY_PIN = 18 # BCM.GPIO51

    # CS 采用硬件,不用 GPIO 模拟
    CS_PIN = 24 # BCM.GPIO49, 实际上用SPI控制CS而非GPIO

    def __init__(self):
        import VisionFive.gpio as GPIO
        import spidev
        # 核心修改 1:在构造函数中创建 SPI 对象,并设置初始化状态位
        self.GPIO = GPIO
        self.SPI = spidev.SpiDev()
        self._spi_opened = False

        # 初始化 GPIO 资源
        GPIO.setmode(GPIO.BOARD)
        GPIO.setup(self.RST_PIN, GPIO.OUT)
        GPIO.setup(self.DC_PIN, GPIO.OUT)
        GPIO.setup(self.PWR_PIN, GPIO.OUT)
        GPIO.setup(self.BUSY_PIN, GPIO.IN, GPIO.PUD_DOWN)
        # GPIO.setup(self.CS_PIN, GPIO.OUT)

    def digital_write(self, pin, value):
        if pin == self.CS_PIN:
            # CS 采用硬件,不用 GPIO 模拟
            pass
        else:
            self.GPIO.output(pin, value)

    def digital_read(self, pin):
        return self.GPIO.input(pin)

    def delay_ms(self, delaytime):
        time.sleep(delaytime / 1000.0)

    def spi_writebyte(self, data):
        self.SPI.writebytes(data)

    def spi_writebyte2(self, data):
        self.SPI.writebytes2(data)

    def module_init(self, cleanup=False):
        # 每次初始化都确保电源打开
        self.GPIO.output(self.PWR_PIN, True)

        if cleanup:
            find_dirs = [
                os.path.dirname(os.path.realpath(__file__)),
                '/usr/local/lib',
                '/usr/lib',
            ]
            self.DEV_SPI = None
            for find_dir in find_dirs:
                val = int(os.popen('getconf LONG_BIT').read())
                logging.debug("System is %d bit"%val)
                if val == 64:
                    so_filename = os.path.join(find_dir, 'DEV_Config_64.so')
                else:
                    so_filename = os.path.join(find_dir, 'DEV_Config_32.so')
                if os.path.exists(so_filename):
                    from ctypes import CDLL
                    self.DEV_SPI = CDLL(so_filename)
                    break
            if self.DEV_SPI is None:
                raise RuntimeError('Cannot find DEV_Config.so')
            self.DEV_SPI.DEV_Module_Init()
        else:
            # 核心修改 2:检查标记位,确保 /dev/spidev1.0 只 open 一次
            if not self._spi_opened:
                self.SPI.open(1, 0)
                self.SPI.max_speed_hz = 4000000
                self.SPI.mode = 0b00
                self._spi_opened = True
                # logging.debug("SPI Device opened and cached.")
        return 0

    def module_exit(self, cleanup=False):
        # 正常刷新过程中的 exit 只关闭显示器电源,进入低功耗
        self.GPIO.output(self.RST_PIN, False)
        self.GPIO.output(self.DC_PIN, False)
        self.GPIO.output(self.PWR_PIN, False)

        # 核心修改 3:只有当 cleanup=True (线程退出) 或实例销毁时才真正关闭 SPI
        if cleanup and self._spi_opened:
            self.SPI.close()
            self._spi_opened = False

            # 释放 GPIO 资源
            self.GPIO.cleanup(self.RST_PIN)
            self.GPIO.cleanup(self.DC_PIN)
            self.GPIO.cleanup(self.PWR_PIN)
            self.GPIO.cleanup(self.BUSY_PIN)
            # self.GPIO.cleanup(self.CS_PIN)
            # logging.debug("SPI and GPIO resources fully released.")

    def __del__(self):
        """
        核心修改 4:析构函数。
        如果应用层忘记调用 module_exit(cleanup=True),
        Python 解释器在回收对象时会强制清理句柄。
        """
        try:
            self.module_exit(cleanup=True)
        except:
            pass


电子墨水屏效果展示

【赛昉-星光2驱动微雪4.26inch电子墨水屏】 
 

 
 
wd_161045m42lllslclldc0l6.png

点个灯吧

回复评论

暂无评论,赶紧抢沙发吧
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复