基于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(时钟线):同步数据传输的时钟信号。
- SPI:使用全双工通信,数据可以在两个方向上同时传输。SPI 采用 四线制(MISO、MOSI、SCK、CS/SS):
- 连接结构:I2C 更适合有多个设备的总线结构,而 SPI 的多从设备需要额外的片选线。
- SPI:通常用于 点对点(一个主设备连接一个从设备)或 多从结构(一个主设备与多个从设备通信)。在多从结构中,每个从设备需要一个独立的 CS 线。
- I2C:用于 多主多从结构,所有设备共享 SDA 和 SCL 线,每个设备通过唯一地址区分,节省了引脚。



整个项目中用到的是 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为什么乘以 8max_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_vcomDC、lut_ww、lut_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_display和ds_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_display 和 ds_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 = 19 个 Byte 来存储。共有 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 用于全局刷新的函数
屏幕刷新机制
- 墨水屏的显示数据分两部分:
- 旧数据(背景): 用
0x10命令加载,决定屏幕上原有内容的基础状态。 - 新数据(图像): 用
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)
程序运行时报错:

日志显示是重复安装了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();

浙公网安备 33010602011771号