i2s parallel interface to monochrome lcd with 4bit data bus

pataga
Posts: 73
Joined: Sat Aug 12, 2017 5:53 am

Re: i2s parallel interface to monochrome lcd with 4bit data bus

Postby pataga » Thu Oct 12, 2017 6:16 am

ESP_Sprite, you were right about the weird ordering, I noticed this only with a bitmap image. Had to swap odd and even bytes in the source image buffer before extracting the 4bit data for each packet.

But i still have a problem with the line synchronization, left and right borders are swapped. Still hopeful.
IMG_0759.JPG
IMG_0759.JPG (230.82 KiB) Viewed 9852 times

ESP_Sprite
Posts: 9761
Joined: Thu Nov 26, 2015 4:08 am

Re: i2s parallel interface to monochrome lcd with 4bit data bus

Postby ESP_Sprite » Thu Oct 12, 2017 6:32 am

Maybe something you overlooked: It's not the pixels that are swapped, it's the actual bytes getting loaded into the I2S path. So instead of just swapping the pixels around, you should swap around the way you write into the I2S buffer. If you don't, the sync signals will still be written 'straight' and me emitted in the wrong place.

Nice image, by the way. Wondering if you can do grayscale by sending an image at 240fps and doing tdm...

jumjum123
Posts: 199
Joined: Mon Oct 17, 2016 3:11 pm

Re: i2s parallel interface to monochrome lcd with 4bit data bus

Postby jumjum123 » Thu Oct 12, 2017 7:18 am

Based on my poor knowledge, could this be used as a first step to build a VGA driver for a monitor ?
I would be interested to see the source to create data including sync signals.

pataga
Posts: 73
Joined: Sat Aug 12, 2017 5:53 am

Re: i2s parallel interface to monochrome lcd with 4bit data bus

Postby pataga » Thu Oct 12, 2017 10:27 am

ESP_Sprite wrote:It's not the pixels that are swapped, it's the actual bytes getting loaded into the I2S path. So instead of just swapping the pixels around, you should swap around the way you write into the I2S buffer. If you don't, the sync signals will still be written 'straight' and me emitted in the wrong place.
doh, you're right of course. I did try to fix it, and got an improvement. Right edge is clean, but am still not getting the whole line. Have tried latching previous line on first pixel, or current line on last pixel. First option looks better - shows more of the right side, left border is smaller.

With the original PIC24 graphic controller, the HS latch pulse is on the last pixel of the row, but it's a short pulse (< 1/2 clock period, and positive edge is when clock is low). As per the info I have, the line data is latched on the falling edge of the HS pulse.

With this setup, the HS pulse is one full clock period with positive edge synced to clock positive edge. I tried inverting the HS pulse with 'gpio_matrix_out()', but the lcd doesn't like that, no image at all.

But maybe there is something else I haven't taken care of.

jumjum123 wrote:I would be interested to see the source to create data including sync signals.
In the code below that generates this image, I am basically hacking the FR toggle by placing two copies of the same image in a buffer with FR = 0 for the first and FR= 1 for the second, the i2s just loops around both. I suppose this could be done via an interrupt setting a gpio rather than through the i2s parallel bus.

Code: Select all

#define NUM_COLS	60
#define NUM_ROWS	160

#define FRAME_SIZE (NUM_COLS * NUM_ROWS)

#define BIT_HS	(uint8_t)(1<<4)
#define BIT_VS	(uint8_t)(1<<5)
#define BIT_FR	(uint8_t)(1<<6)

// 
//  Image data for lena
// 

const uint8_t lenaBitmap[] =
{
	0x55, 0xEB, 0x65, 0x7B,... etc 
	
};

uint8_t frameBuffer[2*FRAME_SIZE];


static void createTestImage(uint8_t* pFB, uint8_t* pImg) {
	uint8_t* pPkt = pFB;
	uint8_t* pSrc = pImg;
	for (int row = 0; row < NUM_ROWS; row++) {
		int col = 0;
		while (col < NUM_COLS) {
			uint8_t d4;
			uint8_t d8a = *pSrc++;
			uint8_t d8b = *pSrc++;
            d4 = d8b>>4;		
			if (row == 0)  d4 |= BIT_VS;
			*pPkt++ = d4;
			col++;
			
			d4 = d8b&0xf;
			//if (col == 57) d4 |= BIT_HS; // latch current line on the last clock
			if (row == 0) d4 |= BIT_VS;
			*pPkt++ = d4;
			col++;
			
            d4 = d8a>>4;		
			if (col == 2) d4 |= BIT_HS; // latch previous line on the first clock
			if (row == 0)  d4 |= BIT_VS;
			*pPkt++ = d4;
			col++;
			
			d4 = d8a&0xf;
			if (row == 0) d4 |= BIT_VS;
			*pPkt++ = d4;
			col++;
			}
		}	 
	
	memcpy(pPkt, pFB, FRAME_SIZE);
	for (int inx = 0; inx < FRAME_SIZE; inx++) {
		*pPkt |= BIT_FR;
		pPkt++;
		}
	}
	

void app_main(){
    gpio_set_direction(PIN_LED, GPIO_MODE_DEF_OUTPUT);
    gpio_set_direction(PIN_DOFF, GPIO_MODE_DEF_OUTPUT);
	gpio_set_level(PIN_DOFF, 0);

    gpio_set_direction(PIN_BIASEN, GPIO_MODE_DEF_OUTPUT);
	gpio_set_level(PIN_BIASEN, 0);

    gpio_set_direction(PIN_BTN, GPIO_MODE_DEF_INPUT);
    gpio_pullup_en(PIN_BTN);

	createTestImage(frameBuffer, (uint8_t*)lenaBitmap);
	
    i2s_parallel_buffer_desc_t bufdesc[2];
    i2s_parallel_config_t cfg = {
        .gpio_bus = {15, 	// 0 : d0 
					2, 		// 1 : d1
					4, 		// 2 : d2
					16, 	// 3 : d3
					17, 	// 4 : HS
					5, 		// 5 : VS
					18,		// 6 : FR
					-1},	// 7 : no connection
        .gpio_clk = 19, 	// XCK
        .bits = I2S_PARALLEL_BITS_8,
        .clkspeed_hz = 2000*1000,
        .bufa = &bufdesc[0],
        .bufb = &bufdesc[1]
    };


    bufdesc[0].memory = frameBuffer;
    bufdesc[0].size = 2*FRAME_SIZE;

    bufdesc[1].memory = frameBuffer; // not used for now, so point to the same buffer
    bufdesc[1].size = 2*FRAME_SIZE;
	
	gpio_set_level(PIN_BIASEN, 1); // enable lcd bias voltage V0
	vTaskDelay(50 / portTICK_PERIOD_MS); 
	gpio_set_level(PIN_DOFF, 1);  // enable lcd
	vTaskDelay(50 / portTICK_PERIOD_MS); 
    i2s_parallel_setup(&I2S1, &cfg);

    printf("I2S setup done.\n");

    //int backbuf_id = 0; //which buffer is the backbuffer, as in, which one is not active so we can write to it
	int level = 0;
    while(1) {
        //Show our work!
        //i2s_parallel_flip_to_buffer(&I2S1, backbuf_id);
        //backbuf_id ^= 1;
        vTaskDelay(1000 / portTICK_PERIOD_MS); 
        gpio_set_level(PIN_LED, level);
		level = !level;
		
        if (gpio_get_level(PIN_BTN)) {
			} 
		else {
			}
		}
}
Attachments
lena.png
lena.png (4.24 KiB) Viewed 9840 times
IMG_0761.JPG
IMG_0761.JPG (140.97 KiB) Viewed 9840 times
IMG_0765.JPG
IMG_0765.JPG (91.76 KiB) Viewed 9840 times

pataga
Posts: 73
Joined: Sat Aug 12, 2017 5:53 am

Re: i2s parallel interface to monochrome lcd with 4bit data bus

Postby pataga » Thu Oct 12, 2017 10:55 am

And this is my simplified i2s_parallel.c for 8bit lcd mode ...

Code: Select all

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

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "freertos/queue.h"

#include "soc/i2s_struct.h"
#include "soc/i2s_reg.h"
#include "driver/periph_ctrl.h"
#include "soc/io_mux_reg.h"
#include "rom/lldesc.h"
#include "esp_heap_caps.h"
#include "i2s_parallel.h"

typedef struct {
    volatile lldesc_t* dmadesc_a;
	volatile lldesc_t* dmadesc_b;
    int desccount_a;
	int	desccount_b;
} i2s_parallel_state_t;

static i2s_parallel_state_t* i2s_state[2] = {NULL, NULL};

#define DMA_MAX (4096-4)

//Calculate the amount of dma descs needed for a buffer desc
static int calc_needed_dma_descs_for(i2s_parallel_buffer_desc_t *desc) {
    int ret = (desc->size + DMA_MAX - 1)/DMA_MAX;
    return ret;
	}

static void fill_dma_desc(volatile lldesc_t* dmadesc, i2s_parallel_buffer_desc_t* bufdesc) {
    int n = 0;
    int len = bufdesc->size;
    uint8_t* data = (uint8_t*)bufdesc->memory;
    while(len) {
		int dmalen = len;
		if (dmalen > DMA_MAX) dmalen = DMA_MAX;
		dmadesc[n].size = dmalen;
		dmadesc[n].length = dmalen;
		dmadesc[n].buf = data;
		dmadesc[n].eof = 0;
		dmadesc[n].sosf = 0;
		dmadesc[n].owner = 1;
		dmadesc[n].qe.stqe_next = (lldesc_t*)&dmadesc[n+1];
		dmadesc[n].offset = 0;
		len -= dmalen;
		data += dmalen;
		n++;
		}
    //Loop last back to first
    dmadesc[n-1].qe.stqe_next=(lldesc_t*)&dmadesc[0];
    printf("fill_dma_desc: filled %d descriptors\n", n);
}

static void gpio_setup_out(int gpio, int sig, int inv) {
    if (gpio == -1) return;
    PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[gpio], PIN_FUNC_GPIO);
    gpio_set_direction(gpio, GPIO_MODE_DEF_OUTPUT);
    gpio_matrix_out(gpio, sig, inv, false);
	}


static void dma_reset(i2s_dev_t *dev) {
    dev->lc_conf.in_rst=1; 
	dev->lc_conf.in_rst=0;
    dev->lc_conf.out_rst=1; 
	dev->lc_conf.out_rst=0;
	}

static void fifo_reset(i2s_dev_t *dev) {
    dev->conf.rx_fifo_reset=1; 
	dev->conf.rx_fifo_reset=0;
    dev->conf.tx_fifo_reset=1; 
	dev->conf.tx_fifo_reset=0;
	}

static int i2snum(i2s_dev_t *dev) {
    return (dev == &I2S0)? 0 : 1;
	}

void i2s_parallel_setup(i2s_dev_t *dev, const i2s_parallel_config_t *cfg) {
    //Figure out which signal numbers to use for routing
    printf("Setting up parallel I2S bus at I2S%d\n", i2snum(dev));
    int sig_data_base, sig_clk;
    if (dev == &I2S0) {
        sig_data_base = I2S0O_DATA_OUT0_IDX;
        sig_clk = I2S0O_WS_OUT_IDX;
		} 
	else {
        if (cfg->bits == I2S_PARALLEL_BITS_32 ) {
            sig_data_base = I2S1O_DATA_OUT0_IDX;
			} 
		else 
        if (cfg->bits == I2S_PARALLEL_BITS_8 ) {
            sig_data_base = I2S1O_DATA_OUT0_IDX;
			} 
		else {
            //Because of... reasons... the 16-bit values for i2s1 appear on d8...d23
            sig_data_base = I2S1O_DATA_OUT8_IDX;
			}
        sig_clk = I2S1O_WS_OUT_IDX;
		}
    
    //Route the signals
    gpio_setup_out(cfg->gpio_bus[0], sig_data_base+0, false); // D0
    gpio_setup_out(cfg->gpio_bus[1], sig_data_base+1, false); // D1
    gpio_setup_out(cfg->gpio_bus[2], sig_data_base+2, false); // D2
    gpio_setup_out(cfg->gpio_bus[3], sig_data_base+3, false); // D3
    gpio_setup_out(cfg->gpio_bus[4], sig_data_base+4, false); // HS
    gpio_setup_out(cfg->gpio_bus[5], sig_data_base+5, false); // VS
    gpio_setup_out(cfg->gpio_bus[6], sig_data_base+6, false); // FR
		
    //ToDo: Clk/WS may need inversion?
    //gpio_setup_out(cfg->gpio_clk, sig_clk, true);
    gpio_setup_out(cfg->gpio_clk, sig_clk, false);
    
    //Power on dev
    if (dev == &I2S0) {
        periph_module_enable(PERIPH_I2S0_MODULE);
		} 
	else {
        periph_module_enable(PERIPH_I2S1_MODULE);
		}
    //Initialize I2S dev
    dev->conf.rx_reset = 1; 
	dev->conf.rx_reset = 0;
    dev->conf.tx_reset = 1; 
	dev->conf.tx_reset = 0;
    dma_reset(dev);
    fifo_reset(dev);
    
    //Enable LCD mode
    dev->conf2.val = 0;
    dev->conf2.lcd_en = 1;
    dev->conf2.lcd_tx_wrx2_en = 1; // HN
    dev->conf2.lcd_tx_sdx2_en = 0; // HN
    
    dev->sample_rate_conf.val = 0;
    dev->sample_rate_conf.rx_bits_mod = cfg->bits;
    dev->sample_rate_conf.tx_bits_mod = cfg->bits;
    dev->sample_rate_conf.rx_bck_div_num = 4; //ToDo: Unsure about what this does...
    dev->sample_rate_conf.tx_bck_div_num = 4;
    
    dev->clkm_conf.val = 0;
    dev->clkm_conf.clka_en = 0;
    dev->clkm_conf.clkm_div_a = 63;
    dev->clkm_conf.clkm_div_b = 63;
    //We ignore the possibility for fractional division here.
    dev->clkm_conf.clkm_div_num = 80000000L/cfg->clkspeed_hz;
    
    dev->fifo_conf.val = 0;
    dev->fifo_conf.rx_fifo_mod_force_en = 1; 
    dev->fifo_conf.tx_fifo_mod_force_en = 1; // HN
    dev->fifo_conf.tx_fifo_mod = 1; // HN
    dev->fifo_conf.tx_fifo_mod = 1;
    dev->fifo_conf.rx_data_num = 32; //Thresholds. 
    dev->fifo_conf.tx_data_num = 32;
    dev->fifo_conf.dscr_en = 1;
	
	
    
    dev->conf1.val = 0;
    dev->conf1.tx_stop_en = 0;
    dev->conf1.tx_pcm_bypass = 1;
    
    dev->conf_chan.val = 0;
    dev->conf_chan.tx_chan_mod = 1; // HN
    dev->conf_chan.rx_chan_mod = 1;
    
    //Invert ws to be active-low... ToDo: make this configurable
    dev->conf.tx_right_first = 1;
    dev->conf.rx_right_first = 1;
    
    dev->timing.val = 0;
    
    //Allocate DMA descriptors
    i2s_state[i2snum(dev)] = malloc(sizeof(i2s_parallel_state_t));
    i2s_parallel_state_t *st = i2s_state[i2snum(dev)];
    st->desccount_a = calc_needed_dma_descs_for(cfg->bufa);
	printf("st->descccount_a = %d\r\n", st->desccount_a);
    st->desccount_b = calc_needed_dma_descs_for(cfg->bufb);
	printf("st->descccount_b = %d\r\n", st->desccount_b);
    st->dmadesc_a = heap_caps_malloc(st->desccount_a*sizeof(lldesc_t), MALLOC_CAP_DMA);
    st->dmadesc_b = heap_caps_malloc(st->desccount_b*sizeof(lldesc_t), MALLOC_CAP_DMA);
    
    //and fill them
    fill_dma_desc(st->dmadesc_a, cfg->bufa);
    fill_dma_desc(st->dmadesc_b, cfg->bufb);
    
    //Reset FIFO/DMA -> needed? Doesn't dma_reset/fifo_reset do this?
    dev->lc_conf.in_rst = 1; 
	dev->lc_conf.out_rst = 1; 
	dev->lc_conf.ahbm_rst = 1; 
	dev->lc_conf.ahbm_fifo_rst = 1;
    dev->lc_conf.in_rst = 0; 
	dev->lc_conf.out_rst = 0; 
	dev->lc_conf.ahbm_rst = 0; 
	dev->lc_conf.ahbm_fifo_rst = 0;
    dev->conf.tx_reset = 1; 
	dev->conf.tx_fifo_reset = 1; 
	dev->conf.rx_fifo_reset = 1;
    dev->conf.tx_reset = 0; 
	dev->conf.tx_fifo_reset = 0; 
	dev->conf.rx_fifo_reset = 0;
    
    //Start dma on front buffer
    dev->lc_conf.val = I2S_OUT_DATA_BURST_EN | I2S_OUTDSCR_BURST_EN | I2S_OUT_DATA_BURST_EN;
    dev->out_link.addr = ((uint32_t)(&st->dmadesc_a[0]));
    dev->out_link.start = 1;
    dev->conf.tx_start = 1;
	}


//Flip to a buffer: 0 for bufa, 1 for bufb
void i2s_parallel_flip_to_buffer(i2s_dev_t *dev, int bufid) {
    int no = i2snum(dev);
    if (i2s_state[no] == NULL) return;
    lldesc_t *active_dma_chain;
    if (bufid == 0) {
        active_dma_chain = (lldesc_t*)&i2s_state[no]->dmadesc_a[0];
		} 
	else {
        active_dma_chain = (lldesc_t*)&i2s_state[no]->dmadesc_b[0];
		}

    i2s_state[no]->dmadesc_a[i2s_state[no]->desccount_a-1].qe.stqe_next = active_dma_chain;
    i2s_state[no]->dmadesc_b[i2s_state[no]->desccount_b-1].qe.stqe_next = active_dma_chain;
	}

pataga
Posts: 73
Joined: Sat Aug 12, 2017 5:53 am

Re: i2s parallel interface to monochrome lcd with 4bit data bus

Postby pataga » Fri Oct 13, 2017 7:30 am

Hacked a solution ! I eventually figured that the data packets during which the HS pulse was high were ignored by the LCD, since the HS pulse was a full clock period in width. I ended up padding each row at the end with 4 extra packets (32 bits) with HS high, and it works !

BTW, I had another problem with my previous solution, the VS pulse also needs to be delayed a few clocks into the first row and continue for a few clocks into the second row. That became obvious with another test bitmap with a rectangle 1-pixel border 240x160.

And I have decided to go for a hw solution to generate FR : edge triggered D latch to convert each VS pulse into a level toggle. 74LVC1G80 is tiny (SOT23), and just $0.06, can stick that on the breakout board for the lcd connector. So the i2s bus just needs to be 6bits (4 bits data + HS + VS). Once I have working code for a back-buffer, with the D-latch hardware, I will publish the solution on github.

ESP_Sprite, many thanks again for your help.

Code: Select all

#define NUM_COLS	64
#define NUM_ROWS	160

#define FRAME_SIZE (NUM_COLS * NUM_ROWS)

#define BIT_HS	(uint8_t)(1<<4)
#define BIT_VS	(uint8_t)(1<<5)
#define BIT_FR	(uint8_t)(1<<6)

#if 0
#include "rectbmp.h"
#endif

#if 1
#include "lenabmp.h"
#endif

#define delayMs(ms) vTaskDelay(ms / portTICK_PERIOD_MS)

uint8_t frameBuffer[2*FRAME_SIZE];


static void createTestImage(uint8_t* pFB, uint8_t* pImg) {
	uint8_t* pPkt = pFB;
	uint8_t* pSrc = pImg;
	for (int row = 0; row < NUM_ROWS; row++) {
		uint8_t d4;
		int col = 0;
		while (col < 60) {
			uint8_t d8a = *pSrc++;
			uint8_t d8b = *pSrc++;
            d4 = d8b>>4;		
			if (row == 0 && col > 5)  d4 |= BIT_VS;
			if (row == 1 && col < 5)  d4 |= BIT_VS;
			*pPkt++ = d4;
			col++;
			
			d4 = d8b&0xf;
			if (row == 0 && col > 5)  d4 |= BIT_VS;
			if (row == 1 && col < 5)  d4 |= BIT_VS;
			*pPkt++ = d4;
			col++;
			
            d4 = d8a>>4;		
			if (row == 0 && col > 5)  d4 |= BIT_VS;
			if (row == 1 && col < 5)  d4 |= BIT_VS;
			*pPkt++ = d4;
			col++;
			
			d4 = d8a&0xf;
			if (row == 0 && col > 5)  d4 |= BIT_VS;
			if (row == 1 && col < 5)  d4 |= BIT_VS;
			*pPkt++ = d4;
			col++;
			}
			
		d4 = BIT_HS;
		if (row == 0 && col > 5)  d4 |= BIT_VS;
		if (row == 1 && col < 5)  d4 |= BIT_VS;
		*pPkt++ = d4;
		*pPkt++ = d4;
		*pPkt++ = d4;
		*pPkt++ = d4;
		}	 
	
	memcpy(pPkt, pFB, FRAME_SIZE);
	for (int inx = 0; inx < FRAME_SIZE; inx++) {
		*pPkt |= BIT_FR;
		pPkt++;
		}
	}
	
IMG_20171013_122950.jpg
IMG_20171013_122950.jpg (112.16 KiB) Viewed 9822 times
IMG_20171013_122717.jpg
IMG_20171013_122717.jpg (65.81 KiB) Viewed 9822 times

pataga
Posts: 73
Joined: Sat Aug 12, 2017 5:53 am

Re: i2s parallel interface to monochrome lcd with 4bit data bus

Postby pataga » Sun Oct 15, 2017 11:27 am

As promised, esp-idf project published on github at https://github.com/har-in-air/ESP32-LCD-I2S

I used a modified version of Chuck Lohr's 3d graphics code to demonstrate the application.

Who is online

Users browsing this forum: No registered users and 80 guests