使用ESP32驱动水墨屏的SPI接口
怎么说呢,感觉自己之前都白学了,又从头到尾看了一遍。
主要参考厂家给的源码,不过只有STM32的程序,但是大差不差,拿过来改一下就可以了,其次就是仔细查看芯片手册。
好的,最大的收获就是学会了如何翻手册,有问题翻手册!!
想要让水墨屏显示起来,需要利用spi来进行驱动,spi用来发送命令和数据,本质上来说还是设置寄存器。
发送的命令是地址,数据是要设置的值,这跟I2C其实没什么差别。
具体的什么spi的原理,其他文章都说的很全。
我也懒得写….
好,下面就记录一下我做的过程吧。
在做之前,先来看一下需要几个引脚,以及这些引脚都用来干啥。
查手册,以及我们开发板的硬件原理图。
首先这四根线是我们必须要有的。
还有选择是命令/数据,复位以及忙线等。
所以综合来说,我们需要七根线。
MISO、MOSI、CLK、CS、D/C、RES、BUSY。
有了这些,就可以对我们的GPIO进行设置了。
需要进行GPIO初始化的只有后面四根线,别问我怎么知道,我翻的例程里面就是这么写的。
其实对于我们主控芯片来说,BUSY是输入,CS、D/C、RES都是输出。
其中BUSY存在中断,因为在选模式的时候,选的是SPI0的模式,上升沿采样、下降沿移出,所以此时设置BUSY下下降沿的时候触发中断,允许数据变化(这里理解好像有一点点问题)。
下面上代码:
void ds_screen_gpio_init(){
gpio_config_t io_conf;
//disable interrupt
io_conf.intr_type = GPIO_PIN_INTR_DISABLE;
//set as output mode
io_conf.mode = GPIO_MODE_OUTPUT;
//bit mask of the pins that you want to set,e.g.GPIO18/19
io_conf.pin_bit_mask = SCREEN_GPIO_OUTPUT_CS_SEL;
//disable pull-down mode
io_conf.pull_down_en = 0;
//disable pull-up mode
io_conf.pull_up_en = 0;
//configure GPIO with the given settings
gpio_config(&io_conf);//初始化片选
//bit mask of the pins that you want to set,e.g.GPIO18/19
io_conf.pin_bit_mask = SCREEN_GPIO_OUTPUT_DC_SEL;
//configure GPIO with the given settings
gpio_config(&io_conf);//初始化D/C
//bit mask of the pins that you want to set,e.g.GPIO18/19
io_conf.pin_bit_mask = SCREEN_GPIO_OUTPUT_RES_SEL;
//configure GPIO with the given settings
gpio_config(&io_conf);//复位
io_conf.intr_type = GPIO_INTR_NEGEDGE;//这里为啥要用下降沿来进行中断触发,存疑
//bit mask of the pins, use GPIO4/5 here
io_conf.pin_bit_mask = SCREEN_GPIO_INTPUT_BUSY_SEL;
//set as input mode
io_conf.mode = GPIO_MODE_INPUT;
//enable pull-up mode
io_conf.pull_up_en = 1;
gpio_config(&io_conf);
}
本部分参考官方STM32代码,如下图所示。
GPIO基本就设置完了。
下面编写SPI的代码。
同样是参考官方例程。
首先初始化:
void screen_spi_init(void)
{
esp_err_t ret;
spi_bus_config_t buscfg={
.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={
.clock_speed_hz=15*1000*1000, //Clock out at 26 MHz
.mode=0, //SPI mode 0
.queue_size=7, //We want to be able to queue 7 transactions at a time
// .pre_cb=spi_pre_transfer_callback, //Specify pre-transfer callback to handle D/C line
};
//Initialize the SPI bus
ret=spi_bus_initialize(HSPI_HOST, &buscfg, 0);
ESP_ERROR_CHECK(ret);
//Attach the LCD to the SPI bus
ret=spi_bus_add_device(HSPI_HOST, &devcfg, &spi);
ESP_ERROR_CHECK(ret);
}
这没啥好说的,直接参考手册就可以了。
初始化之后就是编写发送命令和发送数据的函数了,为了后面编写屏幕驱动做准备。
定义一个spi句柄。
spi_device_handle_t spi;
定义一个传输的结构体,并进行初始化。
spi_transaction_t t;
memset(&t, 0, sizeof(t));
struct spi_transaction_t {
uint32_t flags; ///< Bitwise OR of SPI_TRANS_* flags
uint16_t cmd; /**< Command data, of which the length is set in the ``command_bits`` of spi_device_interface_config_t.
*
* <b>NOTE: this field, used to be "command" in ESP-IDF 2.1 and before, is re-written to be used in a new way in ESP-IDF 3.0.</b>
*
* Example: write 0x0123 and command_bits=12 to send command 0x12, 0x3_ (in previous version, you may have to write 0x3_12).
*/
uint64_t addr; /**< Address data, of which the length is set in the ``address_bits`` of spi_device_interface_config_t.
*
* <b>NOTE: this field, used to be "address" in ESP-IDF 2.1 and before, is re-written to be used in a new way in ESP-IDF3.0.</b>
*
* Example: write 0x123400 and address_bits=24 to send address of 0x12, 0x34, 0x00 (in previous version, you may have to write 0x12340000).
*/
size_t length; ///< Total data length, in bits
size_t rxlength; ///< Total data length received, should be not greater than ``length`` in full-duplex mode (0 defaults this to the value of ``length``).
void *user; ///< User-defined variable. Can be used to store eg transaction ID.
union {
const void *tx_buffer; ///< Pointer to transmit buffer, or NULL for no MOSI phase
uint8_t tx_data[4]; ///< If SPI_TRANS_USE_TXDATA is set, data set here is sent directly from this variable.
};
union {
void *rx_buffer; ///< Pointer to receive buffer, or NULL for no MISO phase. Written by 4 bytes-unit if DMA is used.
uint8_t rx_data[4]; ///< If SPI_TRANS_USE_RXDATA is set, data is received directly to this variable
};
} ; //the rx data should start from a 32-bit aligned address to get around dma issue.
结构体的定义如上代码所示。
由此,编写我们的代码。
void spi_send_cmd(const uint8_t cmd)
{
esp_err_t ret;
spi_transaction_t t;
ds_gpio_set_screen_dc(0);
ds_gpio_set_screen_cs(0);
memset(&t, 0, sizeof(t)); //Zero out the transaction
// t.flags=SPI_TRANS_USE_TXDATA;
t.length=8; //Command is 8 bits
t.tx_buffer=&cmd; //The data is the cmd itself
t.user=(void*)0; //D/C needs to be set to 0
ret=spi_device_polling_transmit(spi, &t); //Transmit!
ds_gpio_set_screen_cs(1);
assert(ret==ESP_OK); //Should have had no issues.
}
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.
}
到这里SPI的代码基本就写完了。
虽然说起来比较简单,但是其实花时间去看,还是会要点时间的。
最后就是编写驱动了。
到了这里,我们需要去翻手册,和移植代码。
首先看驱动一个屏幕的流程图。
这样其实就很好理解了。
然后再去看给的源码。
这是初始化。
观察一下,发现基本是按照流程图来编写的。
打开芯片手册,我们需要知道这些地址都是些什么,设置了什么,代表什么意思,以便根据我们的需求进行修改。
这一段很好理解,复位。
将地址翻到12,发现仍然是复位。
将芯片手册翻到01和11这两个地址。
发现01都是默认值,我们默认即可。
而11设置的是x方向递增,y方向递减。
这个得记住 ,因为这在后面编写驱动程序的时候非常重要,因为这就意味着我们的y_start是从高地址开始的,而x_start是从低地址开始的。
如果不清楚这个的话,很容易出现图像倒过来的现象。
后面的命令就以此类推,一个个去查询,弄懂是什么意思,自己需要什么就可以了。
代码如下:
static void init_display(){
vTaskDelay(10 / portTICK_PERIOD_MS);
ds_gpio_set_screen_rst(0); // Module reset
vTaskDelay(10 / portTICK_PERIOD_MS);
ds_gpio_set_screen_rst(1);
vTaskDelay(100 / portTICK_PERIOD_MS);
lcd_chkstatus();
spi_send_cmd(0x12); //SWRESET
lcd_chkstatus();
spi_send_cmd(0x01); //Driver output control
spi_send_data(0xC7);
spi_send_data(0x00);
spi_send_data(0x01);
spi_send_cmd(0x11); //data entry mode
spi_send_data(0x01);
spi_send_cmd(0x44); //set Ram-X address start/end position
spi_send_data(0x00);
spi_send_data(0x18); //0x0C-->(18+1)*8=200 改为//0x18 -->(24+1)*8 =200
spi_send_cmd(0x45); //set Ram-Y address start/end position
spi_send_data(0xC7); //0xC7-->(199+1)=200
spi_send_data(0x00);
spi_send_data(0x00);
spi_send_data(0x00);
spi_send_cmd(0x3C); //BorderWavefrom
spi_send_data(0x05);
spi_send_cmd(0x18); //Read built-in temperature sensor
spi_send_data(0x80);
spi_send_cmd(0x4E); // set RAM x address count to 0;
spi_send_data(0x00);
spi_send_cmd(0x4F); // set RAM y address count to 0X199;
spi_send_data(0xC7);
spi_send_data(0x00);
vTaskDelay(100 / portTICK_PERIOD_MS);
lcd_chkstatus();
}
知道了这些,其他就很好办了。
参照STM32中的代码一个个编写。
void deep_sleep(void) //Enter deep sleep mode
{
spi_send_cmd(0x10); //enter deep sleep
spi_send_data(0x01);
vTaskDelay(100 / portTICK_PERIOD_MS);
}
void refresh(void)
{
spi_send_cmd(0x22); //Display Update Control
spi_send_data(0xF7);
spi_send_cmd(0x20); //Activate Display Update Sequence
lcd_chkstatus();
}
void refresh_part(void)
{
spi_send_cmd(0x22); //Display Update Control
spi_send_data(0xFF);
spi_send_cmd(0x20); //Activate Display Update Sequence
lcd_chkstatus();
}
其中,屏幕全刷很简单,比较麻烦的是局部刷,需要定位到某个区域。
所以这就跟我们之前设置的x和y地址递减递增的方向很有联系了,知道这个,再去编写局部刷新就简单很多了。
因为我用的屏幕是200*200的。
x方向是8位一个,所以X方向是0-24。
y方向是0-199。
所以算起来一共是25*200,共5000个数据。
这也是我们后面显示图像的数组。
局刷之所以麻烦,是因为每次局部刷都需要重新设置一下X和Y的起始地址和计数地址,其他没有什么。
void ds_screen_partial_display(unsigned int x_start,unsigned int y_start,void partial_new(void),unsigned int PART_COLUMN,unsigned int PART_LINE){
unsigned int i;
unsigned int x_end,y_start1,y_start2,y_end1,y_end2;
x_start=x_start/8;
x_end=x_start+PART_LINE/8-1;
y_start1=0;
y_start2=200 - y_start;//这里是因为上下会翻转嘛
if(y_start>=256)//判断一下这里有没有超过256,正常不会超
{
y_start1=y_start2/256;
y_start2=y_start2%256;
}
y_end1=0;
y_end2=y_start2+PART_COLUMN-1;
if(y_end2>=256)
{
y_end1=y_end2/256;
y_end2=y_end2%256;
}
// Add hardware reset to prevent background color change
ds_gpio_set_screen_rst(0); // Module reset
vTaskDelay(10 / portTICK_PERIOD_MS);
ds_gpio_set_screen_rst(1);
vTaskDelay(10 / portTICK_PERIOD_MS);
//Lock the border to prevent flashing
spi_send_cmd(0x3C); //BorderWavefrom,
spi_send_data(0x80);
spi_send_cmd(0x44); // set RAM x address start/end, in page 35
spi_send_data(x_start); // RAM x address start at 00h;
spi_send_data(x_end); // RAM x address end at 0fh(15+1)*8->128
spi_send_cmd(0x45); // set RAM y address start/end, in page 35
spi_send_data(y_start2); // RAM y address start at 0127h;
spi_send_data(y_start1); // RAM y address start at 0127h;
spi_send_data(y_end2); // RAM y address end at 00h;
spi_send_data(y_end1); // ????=0
spi_send_cmd(0x4E); // set RAM x address count to 0;
spi_send_data(x_start);
spi_send_cmd(0x4F); // set RAM y address count to 0X127;
spi_send_data(y_start2);
spi_send_data(y_start1);
spi_send_cmd(0x24); //Write Black and White image to RAM
partial_new();
refresh_part();
deep_sleep();
}
基本上就没有什么注意点了。
这里X和Y可以自己调节,根据自己的喜好来,只要图像能够正常显示就行。
OK,到这里基本上就结束了。
最后测试正常。
等之后做好自己的图片,取模显示出来,再放照片!!!
本篇参考ESP32技术参考手册、SSD1681芯片技术手册、ESP32API编写规范,水墨屏厂家STM32源码等…