ESP32 development – SPI drive ink screen

How should I put it, I feel that I have learned in vain before, and I read it again from beginning to end.

Mainly refer to the source code given by the manufacturer, but there are only STM32 programs, but the difference is not bad, just take it and change it, and the second is to carefully check the chip manual.

Okay, the biggest gain is that I learned how to read the manual, if there is a problem, I can read the manual! !

If you want to display the ink screen, you need to use the spi to drive it. The spi is used to send commands and data. In essence, it is to set the registers.

The command sent is the address, and the data is the value to be set, which is actually no different from I2C.

The specific principle of spi is very comprehensive in other articles.

I’m too lazy to write…

Ok, let’s record the process I did.

Before doing it, let’s take a look at how many pins are needed and what these pins are used for.

Check the manual, and the hardware schematic diagram of our development board.

First of all, these four lines are what we must have.

Other options are Command/Data, Reset and Busy etc.

So in summary, we need seven wires.

MISO, MOSI, CLK, CS, D/C, RES, BUSY.

With these, we can set up our GPIO.

Only the last four lines need to be initialized for GPIO. Don’t ask me how to know, that’s what I read in the routine.

In fact, for our main control chip, BUSY is an input, and CS, D/C, and RES are all outputs.

Among them, BUSY has an interrupt, because when selecting the mode, the SPI0 mode is selected, the rising edge is sampled, and the falling edge is shifted out, so at this time, when the falling edge of BUSY is set to trigger an interrupt, the data change is allowed (the understanding here seems to be a little bit question).

Code below:

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( & amp;io_conf);//initialize chip selection

    //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( & amp;io_conf);//Initialize 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( & amp;io_conf);//reset

    io_conf.intr_type = GPIO_INTR_NEGEDGE;//Why use the falling edge to trigger the interrupt here, doubtful
    //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);
   
}

This part refers to the official STM32 code, as shown in the figure below.

GPIO is basically set up.

Write the code for SPI below.

Also refer to the official routine.

Initialize first:

void screen_spi_init(void)
{
    esp_err_t ret;
    spi_bus_config_t buscfg = {
        .miso_io_num = PIN_NUM_MISO, // MISO signal line
        .mosi_io_num = PIN_NUM_MOSI, // MOSI signal line
        .sclk_io_num = PIN_NUM_CLK, // SCLK signal line
        .quadwp_io_num = -1, // WP signal line, dedicated to D2 of QSPI
        .quadhd_io_num = -1, // HD signal line, dedicated to D3 of QSPI
        .max_transfer_sz = 64*8, // Maximum transfer data size

    };
    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, & amp; buscfg, 0);
    ESP_ERROR_CHECK(ret);
    //Attach the LCD to the SPI bus
    ret=spi_bus_add_device(HSPI_HOST, & amp; devcfg, & amp; spi);
    ESP_ERROR_CHECK(ret);
    
}

There is nothing to say, just refer to the manual directly.

After initialization, it is time to write the function of sending commands and sending data, in preparation for writing the screen driver later.

Define a spi handle.

spi_device_handle_t spi;

Define a transfer structure and initialize it.

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.

The definition of the structure is shown in the above code.

From there, write our code.

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( & amp;t, 0, sizeof(t)); //Zero out the transaction
    // t.flags=SPI_TRANS_USE_TXDATA;
    t.length=8; //Command is 8 bits
    t.tx_buffer= & amp;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( & amp;t, 0, sizeof(t)); //Zero out the transaction
    t.length=8; //Len is in bytes, transaction length is in bits.
    t.tx_buffer = & amp; 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.
}

At this point, the SPI code is basically finished.

Although it is relatively simple to say, it will take some time to actually spend time watching it.

The last thing is to write the driver.

At this point, we need to go through the manual and transplant the code.

First look at the flow chart of driving a screen.

This is actually very easy to understand.

Then go to the source code given.

This is initialization.

Take a look and find that it is basically written according to the flow chart.

Open the chip manual, we need to know what these addresses are, what they are set to, and what they mean, so that we can modify them according to our needs.

This paragraph is easy to understand, reset.

Turn the address to 12 and find that it is still reset.

Turn the chip manual to the two addresses 01 and 11.

It is found that 01 is the default value, we can use the default value.

And 11 is set to increase in the x direction and decrease in the y direction.

Remember this, because this is very important when writing the driver later, because it means that our y_start starts from a high address, and x_start starts from a low address.

If you don’t know this, it is easy to have the image inverted.

The following commands can be deduced by analogy, one by one to query, understand what it means, and just need what you need.

code show as below:

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();
\t\t
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 changed to //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);
\t  \t
  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();
}

Once you know this, everything else is easy to handle.

Write one by one with reference to the code in 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();
}

Among them, full screen swiping is very simple, but partial swiping is more troublesome, which needs to be located in a certain area.

So this is very related to the decreasing and increasing direction of the x and y addresses we set before. Knowing this, it is much easier to write a partial refresh.

Because the screen I use is 200*200.

The x direction is one of 8 bits, so the X direction is 0-24.

The y direction is 0-199.

So the total is 25*200, a total of 5000 data.

This is also the array we will display the images in later.

The reason why partial brushing is troublesome is because every partial brushing needs to reset the starting address and counting address of X and Y, and there is nothing else.

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;//This is because it will flip up and down
    if(y_start>=256)//Judge whether there is more than 256 here, normally it will not exceed
    {
        y_start1=y_start2/256;
        y_start2=y_start2%6;
    }
    y_end1=0;
    y_end2=y_start2 + PART_COLUMN-1;
    if(y_end2>=256)
    {
        y_end1=y_end2/256;
        y_end2=y_end2%6;
    }

// 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);
\t
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);
\t
spi_send_cmd(0x24); //Write Black and White image to RAM
partial_new();

refresh_part();
deep_sleep();
}

Basically, there is nothing to pay attention to.

Here X and Y can be adjusted by yourself, according to your preferences, as long as the image can be displayed normally.

OK, it’s basically over here.

The last test was normal.

Afterwards, make your own picture, take the model and display it, and then post the picture! ! !

This article refers to ESP32 technical reference manual, SSD1681 chip technical manual, ESP32API writing specification, ink screen manufacturer STM32 source code, etc…