使用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源码等…

物联沃分享整理
物联沃-IOTWORD物联网 » 使用ESP32驱动水墨屏的SPI接口

发表评论