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.
i2s parallel interface to monochrome lcd with 4bit data bus
-
- Posts: 9761
- Joined: Thu Nov 26, 2015 4:08 am
Re: i2s parallel interface to monochrome lcd with 4bit data bus
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...
Nice image, by the way. Wondering if you can do grayscale by sending an image at 240fps and doing tdm...
Re: i2s parallel interface to monochrome lcd with 4bit data bus
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.
I would be interested to see the source to create data including sync signals.
Re: i2s parallel interface to monochrome lcd with 4bit data bus
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.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.
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.
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.jumjum123 wrote:I would be interested to see the source to create data including sync signals.
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 (4.24 KiB) Viewed 9848 times
-
- IMG_0761.JPG (140.97 KiB) Viewed 9848 times
-
- IMG_0765.JPG (91.76 KiB) Viewed 9848 times
Re: i2s parallel interface to monochrome lcd with 4bit data bus
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;
}
Re: i2s parallel interface to monochrome lcd with 4bit data bus
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.
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++;
}
}
Re: i2s parallel interface to monochrome lcd with 4bit data bus
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.
I used a modified version of Chuck Lohr's 3d graphics code to demonstrate the application.
Who is online
Users browsing this forum: Bing [Bot], MicroController and 116 guests