基于ESP32的桌面小屏幕实战[14]:SPI驱动墨水屏

1. 基本概念

SPI是串行外设接口(Serial Peripheral Interface)的缩写,是一种高速的,全双工,同步的通信总线,它可以使单片机与各种外围设备以串行方式进行通信以交换信息,并且在芯片的管脚上占用四根线。

SPI和I2C的区别:

  • 速度:SPI 更适合需要高带宽的应用,如显示屏或高速数据流,I2C 则适合低速外围设备如传感器。
    • SPI是高速的,支持的速度通常达到几 Mbps 甚至几十 Mbps
    • I2C速度较低,常见速率为 100 kbps(标准模式)
  • 传输方式:SPI 数据传输更灵活,I2C 更简单并且节省引脚。
    • SPI:使用全双工通信,数据可以在两个方向上同时传输。SPI 采用 四线制(MISO、MOSI、SCK、CS/SS):
      • MOSI(主出从入):主设备发送数据给从设备。
      • MISO(主入从出):从设备发送数据给主设备。
      • SCK(时钟):由主设备产生的时钟信号。
      • CS/SS(片选/从选择):用于选择特定从设备。
    • I2C:使用半双工通信,数据在同一时刻只能单向传输。I2C 双线制(SDA、SCL):
      • SDA(数据线):传输数据位。
      • SCL(时钟线):同步数据传输的时钟信号。
  • 连接结构:I2C 更适合有多个设备的总线结构,而 SPI 的多从设备需要额外的片选线。
    • SPI:通常用于 点对点(一个主设备连接一个从设备)或 多从结构(一个主设备与多个从设备通信)。在多从结构中,每个从设备需要一个独立的 CS 线。
    • I2C:用于 多主多从结构,所有设备共享 SDA 和 SCL 线,每个设备通过唯一地址区分,节省了引脚。

img

img

img
整个项目中用到的是 SPI 0 模式

2. 屏幕接口

SCLK IO25 SPI 串口通信时钟信号线。
SDI IO26 SPI 串口通信数据信号线。
CS IO27 片选,低电平有效。
D/C IO14 数据/命令 读写选择,高电平为数据,低电平为命令。
RES IO12 电子纸复位信号,低电平有效。
BUSY IO13 电子纸刷新时,BUSY 引脚发出忙信号给主 MCU,此时 MCU 无法对电子纸驱动 IC 进行读写操作;电子纸刷新完成后,BUSY 引脚发出闲置状态信号,此时 MCU 可以对电子纸驱动 IC 进行读写操作。GDEW 系列电子纸 BUSY 引脚忙状态为高电平(GDEH 系列为低电平),BUSY 引脚空闲状态反之。

3. 墨水屏原理

152×152个像素,19Byte=152Bit,19Byte×152Bit的数组
1Byte=8Bit, 1 个Byte代表 8 个像素点
19Byte=152Bit,19 个Byte就是152个像素点

4. 代码

代码分为2大部分,一部分是SPI部分,一部分是屏幕驱动部分。

4.1 ds_spi.c文件

文件位置:/main/src/hal/ds_spi.c

4.1.1 包含头文件及宏定义

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"

#include "ds_gpio.h"


#define DMA_CHAN    2   //使用的 DMA 通道编号 2

#define PIN_NUM_MISO 33 //MISO(Master In Slave Out,主入从出)引脚的编号为 33
#define PIN_NUM_MOSI 26 //MOSI(Master Out Slave In,主出从入)引脚的编号为 26
#define PIN_NUM_CLK  25 //时钟引脚(SCK)的编号为 25
#define PIN_NUM_CS   27 //片选(CS, Chip Select)引脚的编号为 27

//To speed up transfers, every SPI transfer sends a bunch of lines. This define specifies how many. More means more memory use,
//but less overhead for setting up / finishing transfers. Make sure 240 is dividable by this.
#define PARALLEL_LINES 16   //每次 SPI 传输发送的行数为 16
  • #define DMA_CHAN 2:SPI 通信可能需要使用 DMA(直接存储器访问)来提高传输速度和效率,尤其在高速数据传输时。DMA 通道 2 将用于管理 SPI 数据传输。
  • #define PARALLEL_LINES 16:用于控制 SPI 传输中一次发送的数据行数,目的是加速传输速度。发送的行数越多,传输速度越快,因为设置和完成传输的开销相对减少。注释中提到“240 应该可以被这个值整除”,这通常是因为某种显示或数据布局的分辨率为 240,因此将行数设置为 16 时,能够保证分辨率 240 刚好是 16 的整数倍。

4.1.2 spi_send_cmd 函数发送命令

该函数用于通过 SPI 接口向从设备发送一个 8 位的命令字节

spi_device_handle_t spi;    //定义 SPI 设备句柄 spi,用于代表已初始化的 SPI 设备。

void spi_send_cmd(const uint8_t cmd)    //接收参数为 8 位命令
{
    esp_err_t ret;  //用于存储 spi_device_polling_transmit 函数的返回值,并检查传输是否成功。
    spi_transaction_t t;    //定义了一个 spi_transaction_t 类型的结构体变量 t,用于存储 SPI 传输的信息(例如发送的数据、数据长度等)。
    ds_gpio_set_screen_dc(0);   //将 D/C(数据/命令控制引脚)设为 0,指示当前要发送的是命令而不是数据
    ds_gpio_set_screen_cs(0);   //将片选引脚 CS 设为 0,表示激活 SPI 从设备,使其准备接受数据。
    memset(&t, 0, sizeof(t));       //将结构体变量 t 清零,确保之前的数据不会影响当前的传输。
    // t.flags=SPI_TRANS_USE_TXDATA;
    t.length=8;                     //设置命令(传输长度)为 8 位
    t.tx_buffer=&cmd;               //设置 tx_buffer 指针指向要发送的数据 cmd
    t.user=(void*)0;                //设置 user 指针为空(0),通常用于传输附加的用户数据或状态。
    ret=spi_device_polling_transmit(spi, &t);  //执行数据传输
    ds_gpio_set_screen_cs(1);   //将片选引脚 CS 设为 1,表示结束传输,释放 SPI 从设备,使其停止接受数据。
    assert(ret==ESP_OK);            //检查传输是否成功
}
  • ds_gpio_set_screen_dc(0);:通常,D/C 为 0 时表示发送命令,为 1 时表示发送数据。
  • spi_device_polling_transmit 是一种同步传输方式,会阻塞程序,直到传输完成。

4.1.3 spi_send_data 函数发送数据

该函数通过 SPI 接口向从设备发送一个 8 位的数据字节。

void spi_send_data(const uint8_t data)
{
    esp_err_t ret;
    spi_transaction_t t;
    ds_gpio_set_screen_dc(1);
    ds_gpio_set_screen_cs(0);
    memset(&t, 0, sizeof(t));       //Zero out the transaction
    t.length=8;                 //Len is in bytes, transaction length is in bits.
    t.tx_buffer=&data;               //Data
    t.user=(void*)1;                //D/C needs to be set to 1
    ret=spi_device_polling_transmit(spi, &t);  //Transmit!
    ds_gpio_set_screen_cs(1);
    assert(ret==ESP_OK);            //Should have had no issues.
}

4.1.4 回调函数

该函数用于在每次 SPI 传输开始之前执行一些操作,主要是控制 D/C(数据/命令)引脚的状态。

//This function is called (in irq context!) just before a transmission starts. It will
//set the D/C line to the value indicated in the user field.
void spi_pre_transfer_callback(spi_transaction_t *t)//接受一个指向 spi_transaction_t 类型的指针 t
{
    int dc=(int)t->user;//从 t->user 中读取 D/C 引脚的状态,并将其转换为 int 类型,存储到变量 dc 中
    printf("dc callback\n");//用来确认回调函数被调用
    ds_gpio_set_screen_dc(dc);//根据 dc 的值设置 D/C 引脚的电平,控制 SPI 传输的模式
}

4.1.5 屏幕 SPI 初始化

初始化 SPI 总线和设备,将屏幕设备连接到 SPI 总线上。

void screen_spi_init(void)
{
    esp_err_t ret;
    spi_bus_config_t buscfg={ //该结构体用于配置 SPI 总线的硬件参数
        .miso_io_num = PIN_NUM_MISO,                // MISO信号线
        .mosi_io_num = PIN_NUM_MOSI,                // MOSI信号线
        .sclk_io_num = PIN_NUM_CLK,                 // SCLK信号线
        .quadwp_io_num = -1,                        // WP信号线,专用于QSPI的D2
        .quadhd_io_num = -1,                        // HD信号线,专用于QSPI的D3
        .max_transfer_sz = 64*8,                    // 最大传输数据大小

    };
    spi_device_interface_config_t devcfg={//该结构体用于配置 SPI 从设备接口的参数
        .clock_speed_hz=15*1000*1000,  //设置时钟速度为 15 MHz
        .mode=0,   //SPI 工作在模式 0,即时钟空闲时为低电平,数据在上升沿采样
        .queue_size=7,   //队列大小设置为 7,允许同时排队的事务数为 7 个
        // .pre_cb=spi_pre_transfer_callback,  //Specify pre-transfer callback to handle D/C line
    };
    //初始化 SPI 总线
    ret=spi_bus_initialize(HSPI_HOST, &buscfg, 0);
    ESP_ERROR_CHECK(ret);
    //将屏幕设备连接到指定的 SPI 总线上
    ret=spi_bus_add_device(HSPI_HOST, &devcfg, &spi);
    ESP_ERROR_CHECK(ret);
    
}
  • WP(Write Protect)引脚

    在 SPI 或 QSPI(四线 SPI)模式下,WP 引脚(写保护引脚)通常用于控制写保护功能。它在某些情况下可以防止对存储器设备(如 SPI 闪存)的意外写入。该引脚在 QSPI 模式下用于作为数据传输的 D2 引脚,而在标准 SPI 模式下则通常未使用(即设为 -1)。

  • HD(Hold)引脚

    HD 引脚(保持引脚)用于控制数据保持功能,通常在串行闪存或存储器中使用。该引脚可以让存储器在通信过程中暂停或保持数据,类似于一个“暂停”功能。在 QSPI 模式下,它作为数据传输的 D3 引脚。在标准 SPI 模式中不使用时,通常也会设置为 -1。

  • max_transfer_sz = 64*8为什么乘以 8

    max_transfer_sz 表示最大传输数据大小,以位(bits)为单位。乘以 8 的原因是为了将字节转换为位。在 SPI 设置中,有时希望传输的最大数据大小用位表示,所以定义为 64 字节,即 64*8 = 512 位。

  • HSPI 主机

    • 在 ESP32 上,有多个 SPI 外设控制器,分别称为 HSPI 和 VSPI。HSPI 主机是 ESP32 的一个 SPI 外设控制器,可以用于与外部设备(如传感器、显示屏等)进行 SPI 通信。
    • ESP32 内部包含多个 SPI 控制器,每个控制器都可以在主设备模式或从设备模式下运行,以实现多个 SPI 通道。
  • DMA(Direct Memory Access,直接内存访问)

    DMA 是一种硬件功能,允许在处理器无需参与的情况下,将数据在内存和外设(例如 SPI 总线)之间直接传输。这种机制可以提高数据传输效率,并减少 CPU 负担,使得 CPU 可以在其他任务上处理更多的计算操作。

4.1.6 测试 SPI 接口是否正常工作

通过 SPI 向外设发送一个简单的命令和数据,确保 SPI 通信在基础层面上是正常的。

void screen_spi_test(){
    spi_send_cmd(0x55); //发送命令 0x55(即 8 位二进制 01010101)
    vTaskDelay(10 / portTICK_PERIOD_MS);  //延时 10 毫秒
    spi_send_data(0x55);  //发送数据 0x55
}

4.2 屏幕驱动代码

文件位置:/main/src/driver/ds_screen.c

4.2.1 包含头文件

#include <string.h>
#include <stdio.h>

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "sdkconfig.h"

#include "ds_screen.h"
#include "ds_gpio.h"
#include "ds_spi.h"
#include "ds_data_image.h"

4.2.2 Look-Up Table (LUT) 配置

函数 lut()lut1() 用于将不同的LUT数据上传到电子墨水屏。不同的 LUT (Look-Up Table,查找表)用于显示模式(全屏刷新和局部刷新)时的不同效果。LUT数据通常影响屏幕的显示速度、对比度和灰度。

/*************************EPD display init function******************************************************/
//LUT download
static void lut(void)//全局刷新
{
	unsigned int count;
  /* 第一步:向 0x20 寄存器发送数据,配置 VCOM(栅极驱动电压)*/
	spi_send_cmd(0x20);
	for(count=0;count<44;count++)	     //循环 44 次
		{spi_send_data(lut_vcomDC[count]);}//将 lut_vcomDC 数组中的数据逐一写入寄存器。这些数据决定了显示屏的电压控制特性。

  /* 第二步:向 0x21 寄存器发送数据,配置白-白更新(全白像素的刷新)*/
	spi_send_cmd(0x21);//发送指令 0x21,用于设置 LUT 数据,定义白色像素如何在刷新时呈现
	for(count=0;count<42;count++)	     //循环 42 次
		{spi_send_data(lut_ww[count]);}   //将 lut_ww 数组的元素写入,配置白色到白色(即全白像素的)更新方式。
	
  /* 第三步:向 0x22 寄存器发送数据,配置黑-白更新(黑色像素转为白色)*/
	spi_send_cmd(0x22);//发送指令 0x22,用于控制黑色像素如何转变为白色
	for(count=0;count<42;count++)	     //循环 42 次
		{spi_send_data(lut_bw[count]);} //将 lut_bw 数组的元素写入,用于设置黑色像素在变为白色时的更新特性。

  /* 第四步:向 0x23 寄存器发送数据,配置白-黑更新(白色像素转为黑色)*/
	spi_send_cmd(0x23);//送指令 0x23,用于配置白色像素如何转变为黑色
	for(count=0;count<42;count++)	    //循环 42 次 
		{spi_send_data(lut_wb[count]);} //将 lut_wb 数组的元素写入,设置白色像素变为黑色的更新方式。

  /* 第五步:向 0x24 寄存器发送数据,配置黑-黑更新(全黑像素的刷新)*/
	spi_send_cmd(0x24);//发送指令 0x24,用于配置全黑像素的刷新
	for(count=0;count<42;count++)	     //循环 42 次
		{spi_send_data(lut_bb[count]);} //将 lut_bb 数组的元素写入,定义黑色像素在刷新为黑色时的显示特性
}

static void lut1(void)//局部刷新
{
	unsigned int count;
	spi_send_cmd(0x20);
	for(count=0;count<44;count++)	     
		{spi_send_data(lut_vcom1[count]);}

	spi_send_cmd(0x21);
	for(count=0;count<42;count++)	     
		{spi_send_data(lut_ww1[count]);}   
	
	spi_send_cmd(0x22);
	for(count=0;count<42;count++)	     
		{spi_send_data(lut_bw1[count]);} 

	spi_send_cmd(0x23);
	for(count=0;count<42;count++)	     
		{spi_send_data(lut_wb1[count]);} 

	spi_send_cmd(0x24);
	for(count=0;count<42;count++)	     
		{spi_send_data(lut_bb1[count]);}   
}

lut_vcomDClut_wwlut_bw等数组在 main/include/data/ds_data_image.h 中定义

4.2.3 检查电子墨水屏幕的“忙碌”状态

//Detection busy
static void lcd_chkstatus(void)
{
	int busy;//用于存储屏幕的忙碌状态
  /* do-while 循环反复检查屏幕的状态,直到 busy 变为 0(空闲状态)*/
	do
	{
		spi_send_cmd(0x71);//通过 SPI 接口向电子墨水屏幕发送命令 0x71
		busy = ds_gpio_get_screen_busy();    //读取屏幕的忙碌状态,返回一个表示状态的值,1 表示繁忙,0 表示空闲
	}
	while(busy);   
	// vTaskDelay(100 / portTICK_PERIOD_MS);                
}	

4.2.4 屏幕初始化函数

屏幕初始化包括屏幕复位、电源配置、分辨率和时钟、VCOM 电压、检查状态。

/* 屏幕初始化 */
static void init_display(){
	vTaskDelay(10 / portTICK_PERIOD_MS);//延迟 10 毫秒,为屏幕初始化提供缓冲时间,确保硬件准备就绪
  /* 通过 GPIO 控制复位引脚 screen_rst */
	ds_gpio_set_screen_rst(0);		// 对屏幕进行硬复位
	vTaskDelay(10 / portTICK_PERIOD_MS);
	ds_gpio_set_screen_rst(1);//结束复位过程

	/* 电源设置 */
	spi_send_cmd(0x01);			//POWER SETTING 
	spi_send_data (0x03);//电源控制参数
	spi_send_data (0x00);//Reserved(保留)
	spi_send_data (0x2b);//电压选择参数
	spi_send_data (0x2b);//电压稳定时间
	spi_send_data (0x03);//其他电源相关参数

	/* 配置升压软启动 */
	spi_send_cmd(0x06);         //boost soft start
	spi_send_data (0x17);		//A相设置
	spi_send_data (0x17);		//B相设置
	spi_send_data (0x17);		//C相设置

	/* 启动加载电源 */
	spi_send_cmd(0x04);  
	lcd_chkstatus();//检查屏幕忙碌状态,确保屏幕空闲后再继续操作

	/* 配置屏幕面板参数 */
	spi_send_cmd(0x00);			//panel setting
	spi_send_data(0xbf);		//LUT from OTP,128x296 使用屏幕的 OTP(One-Time Programmable) LUT,分辨率为 128x296
	spi_send_data(0x0d);		//VCOM to 0V fast 配置 VCOM 快速恢复
		
	/* 配置屏幕的时钟频率 */
	spi_send_cmd(0x30);			//PLL setting
	spi_send_data (0x3a);   	// 设置时钟频率为 100Hz。3a 100HZ   29 150Hz 39 200HZ	31 171HZ

	/* 配置屏幕分辨率 */
	spi_send_cmd(0x61);			//resolution setting
	spi_send_data (0x98);   // 设置水平分辨率
	spi_send_data (0x00);		
	spi_send_data (0x98);   // 设置垂直分辨率
		
	/* 配置 VCOM 直流偏移电压,用于调整屏幕对比度。 */
	spi_send_cmd(0x82);			//vcom_DC setting  	
	spi_send_data (0x28);	//设置 VCOM 的直流电压

	/* 配置 VCOM 和数据间隔参数 */
	spi_send_cmd(0X50);			//VCOM AND DATA INTERVAL SETTING			
	spi_send_data(0x97);		//配置显示模式 WBmode:VBDF 17|D7 VBDW 97 VBDB 57		WBRmode:VBDF F7 VBDW 77 VBDB 37  VBDR B7
}

4.2.5 进入深度睡眠函数

/////////////////////////////Enter deep sleep mode////////////////////////
static void deep_sleep(void) //进入深度睡眠
{
  spi_send_cmd(0X50);//VCOM 和数据间隔设置
  spi_send_data(0xf7);//设置屏幕的显示模式和功耗相关参数
  spi_send_cmd(0X02);  	//关闭屏幕电源
  lcd_chkstatus();//等待屏幕处于空闲状态,确保电源关闭命令已完成执行,避免操作冲突
  spi_send_cmd(0X07);  	//进入深度睡眠
  spi_send_data(0xA5);//确认码,用于确保命令正确执行
}

4.2.6 用于全局刷新的函数

//图片全刷-全白函数:设置黑色背景、白色显示层叠加
static void ds_screen_display_white(void){
	unsigned int i;//声明一个循环控制变量 i,用于遍历屏幕上的像素数据。
	spi_send_cmd(0x10);//发送命令 0x10,用于写入显示存储区的黑白像素数据。
	for(i=0;i<2888;i++){//循环 2888 次
		spi_send_data(0x00);//0x00 表示黑色,按照电子墨水屏驱动协议,向显示存储区写入黑色数据
	}
	spi_send_cmd(0x13);//发送命令 0x13,用于写入全局刷新数据或白色覆盖数据
	for(i=0;i<2888;i++){//循环 2888 次
		spi_send_data(0xff);//0xff 表示白色,在显示层叠加白色内容
	}  	 		
}

//图片全刷-数据函数:设置黑色背景、填充图像数据
void ds_screen_full_display_data(const uint8_t *data){
	unsigned int i;//声明一个循环控制变量 i,用于遍历和写入屏幕上的像素数据。
	spi_send_cmd(0x10);//发送命令 0x10,指示开始写入黑白像素数据,通常表示即将开始写入背景数据区域。
	for(i=0;i<2888;i++)
	{
		spi_send_data(0x00); //0x00 表示黑色,将屏幕背景区域全部填充为黑色。
	}  
	spi_send_cmd(0x13);//发送命令 0x13,指示开始写入图像数据区域,用于实际显示的数据刷新。
	for(i=0;i<2888;i++)	     
	{
		spi_send_data(data[i]);  //data[i] 表示 data 数组中的第 i 个元素,也即屏幕的每个像素点的数据。
	}  
}

//全刷 不带数据
void ds_screen_full_display(void pic_display(void)){
  init_display();//初始化墨水屏
  pic_display(); 				//执行实际的图像数据写入
  lut(); 						//设置查找表(LUT),优化电压设置以确保电子墨水显示效果的稳定性和一致性。
  spi_send_cmd(0x12); //刷新显示屏,将屏幕上新写入的数据(例如图像内容)真正显示出来。
  lcd_chkstatus();//检查屏幕是否仍在忙碌中,以确保屏幕刷新操作已完成。
  deep_sleep();//进入深度睡眠模式
}

//全刷 带数据
/*
参数:
display_func:一个函数指针,用于显示图像数据。该函数指针接受一个 const uint8_t * 类型的参数,即指向图像数据的指针。
data:const uint8_t * 类型,指向图像数据的数组,包含要显示的数据内容。
*/
void ds_screen_full_display_bydata(void display_func(const uint8_t *data),const uint8_t *data){
  init_display();//初始化墨水屏
  display_func(data); 				//执行图像数据的显示
  lut(); 						//设置查找表(LUT),优化电压设置以确保电子墨水显示效果的稳定性和一致性。
  spi_send_cmd(0x12);//刷新显示屏,将屏幕上新写入的数据(例如图像内容)真正显示出来。
  lcd_chkstatus();//检查屏幕是否仍在忙碌中,以确保屏幕刷新操作已完成。
  deep_sleep();//进入深度睡眠模式
}
  • ds_screen_full_displayds_screen_full_display_bydata有什么区别
    • void ds_screen_full_display(void pic_display(void));
      • 接收一个函数指针 pic_display,不带参数,表示特定的图片显示逻辑。
      • 图片显示逻辑由 pic_display 预先定义,与数据内容无关。
    • void ds_screen_full_display_bydata(void display_func(const uint8_t *data), const uint8_t *data);
      • 参数1:函数指针 display_func,用于显示具体的图像数据。
      • 参数2:图像数据 data,是一个数组指针,提供待显示的内容。
      • 支持动态传递不同的图像数据内容。

4.2.7 用于局部刷新的函数

//局部刷 不带数据
/*
参数:
接收局部显示区域的起始和结束坐标(x_start、x_end、y_start、y_end)
以及两个函数指针 partial_old 和 partial_new
*/
void ds_screen_partial_display(unsigned int x_start,unsigned int x_end,unsigned int y_start,unsigned int y_end ,
								void partial_old(void),
								void partial_new(void))
{
  init_display();//初始化显示器
  /* 设置显示器的电压和显示间隔 
   * 设定显示电压偏移,以优化电子墨水屏的性能,并配置显示间隔 */
  spi_send_cmd(0x82);			//vcom_DC setting
  spi_send_data (0x08);	
  spi_send_cmd(0X50);
  spi_send_data(0x47);
  lut1();//加载局部刷新用的查找表(LUT),确保刷新模式的性能稳定
  spi_send_cmd(0x91);		//进入局部显示模式
  /* 设置局部显示的分辨率
   * 设置需要刷新的区域,通过 x_start、x_end 和 y_start、y_end 确定屏幕刷新边界 */
  spi_send_cmd(0x90);		//resolution setting
  spi_send_data (x_start);   //x-start     
  spi_send_data (x_end-1);	 //x-end	

	spi_send_data (y_start/256);
	spi_send_data (y_start%256);   //y-start    
		
	spi_send_data (y_end/256);		
	spi_send_data (y_end%256-1);  //y-end
	spi_send_data (0x28);	

  /* 写入旧数据 
   * 发送指令 0x10 以写入旧图像数据到显示器的 SRAM,
   * 并使用 partial_old(old_data) 将 old_data 数组传递到部分屏幕区域的旧图像。*/
	spi_send_cmd(0x10);	       //writes Old data to SRAM for programming
	partial_old();
  /* 写入新数据
   * 发送指令 0x13 以写入新图像数据到显示器的 SRAM,
   * 并使用 partial_new(new_data) 将 new_data 数组写入到新图像数据区域。 */
	spi_send_cmd(0x13);		   //writes New data to SRAM.
	partial_new();
  
  /* 刷新显示
   * 发送 0x12 指令以刷新显示器,
   * 并调用 lcd_chkstatus() 等待显示刷新完成。 */
	spi_send_cmd(0x12);		 //DISPLAY REFRESH 		 
	lcd_chkstatus();
	deep_sleep();//进入深度睡眠模式
	vTaskDelay(200 / portTICK_PERIOD_MS); //延时 200 毫秒,确保在下一次刷新操作前留出足够的时间
}

//局部刷 带数据
/*
参数:
x_start, x_end: 局部刷新区域的横向起始和结束位置(x轴范围)。
y_start, y_end: 局部刷新区域的纵向起始和结束位置(y轴范围)。
partial_old: 	用于加载旧数据的函数指针。
old_data: 		旧数据指针,表示当前显示的图像数据。
partial_new: 	用于加载新数据的函数指针。
new_data: 		新数据指针,表示需要更新到屏幕的图像数据。
*/
void ds_screen_partial_display_bydata(unsigned int x_start,unsigned int x_end,unsigned int y_start,unsigned int y_end ,
										void partial_old(const uint8_t *data),const uint8_t *old_data,
										void partial_new(const uint8_t *data),const uint8_t *new_data) 
{
	/* 初始化显示器 */
	init_display();	//调用初始化函数,复位并配置显示器,使其准备接收命令和数据。
	/* 配置电压和显示间隔 */
	spi_send_cmd(0x82);			//vcom_DC setting  	
    spi_send_data (0x08);	
	spi_send_cmd(0X50);
	spi_send_data(0x47);	
	/* 加载局部刷新的查找表 (LUT) */	
	lut1();	//设置局部刷新操作的查找表(LUT),确保在部分模式下有合适的波形驱动。
	/* 进入局部模式 */
	spi_send_cmd(0x91);		//启用局部刷新模式,让屏幕只刷新指定区域,而不是整块刷新。
	/* 设置刷新区域 */
	spi_send_cmd(0x90);		//resolution setting
	spi_send_data (x_start);   //x-start     
	spi_send_data (x_end-1);	 //x-end	

	spi_send_data (y_start/256);
	spi_send_data (y_start%256);   //y-start    
		
	spi_send_data (y_end/256);		
	spi_send_data (y_end%256-1);  //y-end
	spi_send_data (0x28);	

	/* 加载旧图像数据 */
	spi_send_cmd(0x10);	       //发送 0x10 指令,要求加载旧数据到显示器的 SRAM。
	partial_old(old_data);	//调用 partial_old 函数,使用旧数据 (old_data) 来覆盖指定的刷新区域。
	/* 加载新图像数据 */
	spi_send_cmd(0x13);		   //发送 0x13 指令,要求加载新数据到显示器的 SRAM。
	partial_new(new_data);	//调用 partial_new 函数,将新数据 (new_data) 加载到指定的刷新区域。

	/* 刷新屏幕 */
	spi_send_cmd(0x12);		 //发送 0x12 指令,触发屏幕的刷新操作,将加载的旧数据和新数据生效。
	lcd_chkstatus();	//调用 lcd_chkstatus 检查屏幕忙状态,等待刷新完成。
	/* 进入深度睡眠 */
	deep_sleep();
	/* 延时操作 */
	vTaskDelay(200 / portTICK_PERIOD_MS);   //延时200毫秒,确保显示器有足够时间处理刷新后的操作,并避免下一次刷新时发生冲突。
}

ds_screen_partial_displayds_screen_partial_display_bydata 有什么区别?

  • 数据传递方式
    • ds_screen_partial_display:
      • 不接受外部数据指针 (const uint8_t *data)。
      • 使用函数指针 partial_old() 和 partial_new() 来生成旧数据和新数据,这些函数通常在内部生成或直接操作屏幕的数据。
    • ds_screen_partial_display_bydata:
      • 接收外部数据指针 old_data 和 new_data,通过函数指针 partial_old(const uint8_t *data) 和 partial_new(const uint8_t *data) 加载这些外部数据。
      • 允许用户传递自定义数据,提供更高的灵活性。

墨水屏像素存储逻辑
墨水屏通常采用黑白两色,每个像素可以用 1 位(Bit)表示。1 个 Byte(8 位)表示 8 个像素。每行有 152 个像素,需要 152 / 8 = 19Byte 来存储。共有 152 行,数据结构为 152×19。所以需要定义一个大小为 152 × 19 的二维数组来表示屏幕像素。

uint8_t partial_data[152][19];	//二维数组,大小为 152 × 19
uint8_t partial_data_array[2888];//全部像素 2888 个字节,全屏像素缓冲区

/* 屏幕初始化,设置每个像素都为白色(0xFF) */
void ds_screen_partial_data_init(){
	for(int index = 0;index < 152 ;index ++){ //遍历墨水屏的每一行像素数据。
		for(int yindex = 0;yindex < 19 ;yindex ++){ //遍历当前行的每一个 Byte
			partial_data[index][yindex] = 0xff;
		}
	}
}
/* 将指定区域的数据加载到墨水屏的局部刷新数据缓冲区 partial_data 中 
 * 参数:
 * x_start, x_end: 局部刷新区域的 X 坐标(列)。
 * y_start, y_end: 局部刷新区域的 Y 坐标(行)。
 * data: 待写入区域的数据指针。*/
void ds_screen_partial_data_add(unsigned int x_start,unsigned int x_end,unsigned int y_start,unsigned int y_end ,const uint8_t *data){
	/* 变量计算 */
	uint8_t x_len = x_end - x_start;		// 计算横向区域的像素长度
	uint8_t x_data_location = x_start/8;	// 起始像素对应的字节索引(第几个 Byte)
	uint8_t x_size = x_len/8;   			// 横向区域需要占用的字节数
	int data_index = 0;						// 数据指针的索引
	/* 处理边界对齐 */
	if(x_start % 8 != 0){
		x_data_location ++;	// 若起始像素不对齐到字节边界,需额外占用一个字节
	}
	if(x_len % 8 != 0){
		x_size ++;	// 若区域宽度不是 8 的倍数,需扩展 1 个字节以容纳余下的像素
	}
	//x_data_location=0 x_size=5  
	//90 126
	/* 打印调试信息 */
	printf("x_data_location %d x_size%d\n",x_data_location,x_size);
	printf("ystart %d y_end%d\n",y_start,y_end);
	/* 更新数据 */
	for(int x_index = y_start ;x_index < y_end;x_index ++){ // 遍历指定行范围
		for(int y_index = x_data_location ;y_index < (x_data_location+x_size);y_index ++){
			//遍历 x_data_location 到 (x_data_location + x_size) 的字节范围。
			partial_data[x_index][y_index] = (~data[data_index]);
			data_index++;
		}
	}
}

4.2.8 用于全局刷新的函数

屏幕刷新机制

  • 墨水屏的显示数据分两部分:
    1. 旧数据(背景): 用 0x10 命令加载,决定屏幕上原有内容的基础状态。
    2. 新数据(图像): 用 0x13 命令加载,决定屏幕上实际显示的内容。
  • 刷新屏幕的核心是依次更新这两部分数据,然后进行全屏刷新。

屏幕像素与数据的关系

  • 屏幕大小为 152 × 152 像素。
  • 每 1 字节(8 位)对应 8 个像素点。
    • 总字节数:
      • \(\frac{152 \times 152}{8} = 2888\) 字节。
  • 数据缓冲区 partial_data_array[2888] 中的每个字节表示屏幕的一部分内容。
//图片全刷-全白函数
/*
函数功能:
将缓冲区 partial_data_array 中的数据发送到屏幕,用于全屏刷新。
在刷新之前,屏幕会先被初始化为全白背景。
*/
static void ds_screen_display_data(void){
	unsigned int i;
	/* 初始化显示数据的第一阶段 */
	spi_send_cmd(0x10);//发送命令 0x10,设置墨水屏的缓冲区,准备写入旧数据
	for(i=0;i<2888;i++){
		spi_send_data(0x00);  //置显示的背景为黑色
	}
	spi_send_cmd(0x13);
	for(i=0;i<2888;i++){//发送命令 0x13,设置墨水屏的缓冲区,准备写入新数据(显示内容)。
		spi_send_data(partial_data_array[i]);  //将 partial_data_array 缓冲区中的每个字节逐一发送到屏幕。
	}  	 		
}

void ds_screen_partial_data_copy(){
	int data_index = 0;  //用于在一维数组 partial_data_array 中定位数据存储位置
	for(int index = 0;index < 152 ;index ++){//遍历二维数组 partial_data 的行索引(index)
		for(int yindex = 0;yindex < 19 ;yindex ++){//遍历当前行的每一列(yindex)
			partial_data_array[data_index] = partial_data[index][yindex];
			data_index ++;
		}
	}
	ds_screen_full_display(ds_screen_display_data);
}

//接口初始化
void init_screen_interface(){
    ds_screen_gpio_init();
    screen_spi_init();
}

//初始化
void ds_screen_init(){
	ds_screen_full_display(ds_screen_display_white);
}

//清屏为白色
void ds_screen_clean_white(){
	ds_screen_init();
	vTaskDelay(2000 / portTICK_PERIOD_MS);      
}

5. 编译运行

打开 DesktopScreenV4.0.3 切换到 dev8

git checkout dev8

可以看到新增两个文件一个是SPI驱动(main/src/hal/ds_spi.c),一个是屏幕驱动(main/src/driver/ds_screen.c)

程序运行时报错:
img
日志显示是重复安装了GPIO中断服务。经检查,是主函数初始化时出的问题。

    init_ft6336();
    init_screen_interface();
    ds_screen_init();

init_ft6336();ds_touch_gpio_init();中已经有了gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT);,而init_screen_interface();ds_screen_gpio_init();中又有gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT);

在 ESP-IDF 中,gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT) 用于初始化 GPIO 中断服务。该函数只能调用一次,因为它设置全局的中断服务。如果在程序中重复调用该函数,就会导致错误 gpio_install_isr_service(450): GPIO isr service already installed

解决办法:
统一调用gpio_install_isr_service()

void init_gpio_isr_service() {
    gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT);
}

主函数加一行代码

    init_gpio_isr_service();// 初始化一次 ISR 服务
    init_ft6336();
    init_screen_interface();
    ds_screen_init();
posted @ 2025-11-19 09:44  茴香豆的茴  阅读(360)  评论(0)    收藏  举报