i2s DMA pixel data / clock input

samsonx
Posts: 3
Joined: Fri Jun 26, 2020 7:07 pm

i2s DMA pixel data / clock input

Postby samsonx » Fri Jun 26, 2020 7:41 pm

Hello, I am attempting to use the esp32 to decode frames from a pinball DMD display. The i2s peripheral on the esp32 seems like it should work. I am using camera slave receiving mode, but am having trouble configuring the DMA/fifo to pull the data in correctly. The display data comes in rows of pixels; 128 bits per row. It is likely I am going about the wrong way but..
dmd_row_capture.PNG
dmd_row_capture.PNG (73.25 KiB) Viewed 3256 times
I am using VS Code, PlatformIO, and Arduino Framework

Code: Select all

#include "soc/soc.h"
#include "soc/gpio_sig_map.h"
#include "soc/i2s_reg.h"
#include "soc/i2s_struct.h"
#include "soc/io_mux_reg.h"
#include "driver/gpio.h"
#include "driver/periph_ctrl.h"
#include "driver/i2s.h"
#include "rom/lldesc.h"

#include <TFT_eSPI.h>
TFT_eSPI tft = TFT_eSPI(135, 240); // Invoke custom library

#ifndef TFT_DISPOFF
#define TFT_DISPOFF 0x28
#endif

#ifndef TFT_SLPIN
#define TFT_SLPIN   0x10
#endif

#define TFT_MOSI            19
#define TFT_SCLK            18
#define TFT_CS              5
#define TFT_DC              16
#define TFT_RST             23

#define TFT_BL          21  // Display backlight control pin

DMA_ATTR uint16_t dma_buff[2][64]; // I2S_RX_FIFO_MOD = 0; 16-bit dual channel data, 128 bytes of these represents a row of dmd data
DMA_ATTR lldesc_t dma_descriptor[2]; // cyclical buffer of 2

uint16_t picture[128][240];

// DMD display size
#define ROW_COUNT 32
#define COL_COUNT 128
uint8_t dots[ROW_COUNT][COL_COUNT]; //test image that is being generated on Teensy

#define DMD_DE        GPIO_NUM_36
#define DMD_ROW_DATA  GPIO_NUM_37
#define DMD_ROW_CLK   GPIO_NUM_38
#define DMD_COL_LATCH GPIO_NUM_39
#define DMD_PIX_CLK   GPIO_NUM_32
#define DMD_SDATA     GPIO_NUM_33


unsigned long startTime;
unsigned long endTime;
unsigned int frames = 1;

static intr_handle_t i2s_intr_handle;
static void i2s_isr(void* arg);
static void dma_reset();
static void fifo_reset();

void setup() {
    Serial.begin(115200);
    Serial.println(ESP.getSdkVersion());

    tft.init();
    tft.setRotation(3); // landscape upside-down
    tft.fillScreen(TFT_BLACK);
    tft.setTextSize(2);
    tft.setTextColor(TFT_WHITE);
    tft.setCursor(0, 0);
    tft.setTextDatum(MC_DATUM);
    tft.setTextSize(1);

    if (TFT_BL > 0) {
        pinMode(TFT_BL, OUTPUT);
        digitalWrite(TFT_BL, HIGH);
    }
    tft.setSwapBytes(true);

  // create test image
    for (int row = 0; row < ROW_COUNT; row++) {
      for (int col = 0; col < 64; col++) {
        if (((row + col) % 8) == 0) {
          dots[row][col] = 15;
        }
        else {
          dots[row][col] = 0;
        }
        if (row < 16)
        {
          dots[row][col + 64] = col >> 2;
        }
        else
        {
          dots[row][col + 64] = 15 - (col >> 2);
        }

        if (col < 16)
        {
          dots[row][col] = col;
        }
      }   
    }


  
    // display test image in console and convert to on/off
    for (int row = 0; row < ROW_COUNT; row++) {  
      for (int col = 0; col < COL_COUNT; col++) {  
        if (dots[row][col] & 0x08) {
          dots[row][col] = 1;
        }
        else {
          dots[row][col] = 0;
        }
        Serial.print(dots[row][col]);
      }
      Serial.println(" "); 
    }
     Serial.println(" ");

    // display test image on LCD
    tft.pushImage(0, 32, 128, 32, (uint8_t*)dots);

    
    //buff 0
    dma_descriptor[0].length = 0; //number of byte written to the buffer
    dma_descriptor[0].size = sizeof(dma_buff[0]); //max size of the buffer in bytes
    dma_descriptor[0].owner = 1;
    dma_descriptor[0].sosf = 1;
    dma_descriptor[0].buf = (uint8_t*)&dma_buff[0][0];
    dma_descriptor[0].offset = 0;
    dma_descriptor[0].empty = 0;
    dma_descriptor[0].eof = 0;  // was 0
    //pointer to the next descriptor
    dma_descriptor[0].qe.stqe_next = &dma_descriptor[1];

    //buff 1
    dma_descriptor[1].length = 0; //number of byte written to the buffer
    dma_descriptor[1].size = sizeof(dma_buff[1]); //max size of the buffer in bytes
    dma_descriptor[1].owner = 1;
    dma_descriptor[1].sosf = 1;
    dma_descriptor[1].buf = (uint8_t*)&dma_buff[1][0];
    dma_descriptor[1].offset = 0;
    dma_descriptor[1].empty = 0;
    dma_descriptor[1].eof = 1;  // was 0
    //pointer to the next descriptor
    dma_descriptor[1].qe.stqe_next = &dma_descriptor[0];
 
    //data inputs
    pinMode(DMD_DE, INPUT);       // H-SYNC ?
    pinMode(DMD_ROW_DATA, INPUT); // V-SYNC ?
    pinMode(DMD_ROW_CLK, INPUT);
    pinMode(DMD_COL_LATCH, INPUT);
    pinMode(DMD_PIX_CLK, INPUT);  // CLK
    pinMode(DMD_SDATA, INPUT);    // DATA

    gpio_matrix_in(DMD_SDATA,  I2S0I_DATA_IN0_IDX, false);  
    gpio_matrix_in(DMD_PIX_CLK, I2S0I_WS_IN_IDX, false);  // word select as clk?
    //gpio_matrix_in(DMD_PIX_CLK, I2S0I_BCK_IN_IDX, false); //clk

    //for i2s in parallel camera input mode data is receiver only when V_SYNC = H_SYNC = H_ENABLE = 1. We don't use these inputs so simply set them High
    gpio_matrix_in(0x38, I2S0I_V_SYNC_IDX, false);
    gpio_matrix_in(0x38, I2S0I_H_SYNC_IDX, false);  //0x30 sends 0, 0x38 sends 1
    gpio_matrix_in(0x38 /*DMD_DE*/, I2S0I_H_ENABLE_IDX, false);

    
    // Enable and configure I2S peripheral
    periph_module_enable(PERIPH_I2S0_MODULE);

    // Enable slave mode (sampling clock is external)
    I2S0.conf.rx_slave_mod = 1; 
    // Enable parallel mode
    I2S0.conf2.lcd_en = 1;

    // Use HSYNC/VSYNC/HREF to control sampling
    I2S0.conf2.camera_en = 1;

    I2S0.clkm_conf.val=0;
    I2S0.clkm_conf.clka_en=0;
    I2S0.clkm_conf.clkm_div_a=0;
    I2S0.clkm_conf.clkm_div_b=0;
    //We ignore the possibility for fractional division here.
    I2S0.clkm_conf.clkm_div_num=1;
       
    // FIFO will sink data to DMA
    I2S0.fifo_conf.dscr_en = 1;
    I2S0.fifo_conf.tx_fifo_mod_force_en = 1;
    I2S0.fifo_conf.rx_fifo_mod_force_en = 1;
    // FIFO configuration
    I2S0.fifo_conf.rx_data_num=64; //Thresholds. 
    I2S0.fifo_conf.tx_data_num=0;

    I2S0.fifo_conf.rx_fifo_mod = 0;
    I2S0.conf_chan.rx_chan_mod = 0;
    // Clear flags which are used in I2S serial mode
    I2S0.sample_rate_conf.val=0;
    I2S0.conf.rx_right_first = 0;
    I2S0.conf.rx_msb_right = 0;
    I2S0.conf.rx_msb_shift = 0;
    I2S0.conf.rx_mono = 0;
    I2S0.conf.rx_short_sync = 0;
    I2S0.timing.val = 0;

    //start i2s
    I2S0.rx_eof_num = 64; //0xFFFFFFFF;
    I2S0.in_link.addr = (uint32_t)&dma_descriptor[0];
    I2S0.in_link.start = 1;
    I2S0.int_clr.val = I2S0.int_raw.val;
    I2S0.int_ena.val = 0; // was 0
    I2S0.int_ena.in_suc_eof = 1; // I2S_IN_SUC_EOF_INT: Triggered when all data have been received; interrupt

    //Set int handler
    esp_err_t err = esp_intr_alloc(ETS_I2S0_INTR_SOURCE, 0, &i2s_isr, (void*)&I2S0, NULL);
    
    if (err != ESP_OK) ESP_LOGE(TAG, "Error esp_intr_alloc %s", esp_err_to_name(err));
    if (err == ESP_OK) Serial.println("esp_intr_alloc returned ESP_OK"); 

    
    //err = esp_intr_enable(i2s_intr_handle);
    //if (err != ESP_OK) ESP_LOGE(TAG, "error in esp_intr_enable: %s", esp_err_to_name(err));
    //if (err == ESP_OK) Serial.println("esp_intr_enable returned ESP_OK");

    //start i2s + dma
    I2S0.conf.rx_start = 1;
}


static void IRAM_ATTR i2s_isr(void* arg) {
    
    if (I2S0.int_st.in_suc_eof) {
      I2S0.int_clr.val = I2S0.int_st.val;
      lldesc_t *finish_desc = (lldesc_t*)I2S0.in_eof_des_addr;

    // TODO: take buffer data and add the row to the growing frame
      
    } else {
      I2S0.int_clr.val = I2S0.int_st.val;
      Serial.print("unexpected interrupt: ");
      Serial.println(I2S0.int_st.val);
    }
}

static void dma_reset() {
  I2S0.lc_conf.in_rst=1; I2S0.lc_conf.in_rst=0;
  I2S0.lc_conf.out_rst=1; I2S0.lc_conf.out_rst=0;
}

static void fifo_reset() {
  I2S0.conf.rx_fifo_reset=1; I2S0.conf.rx_fifo_reset=0;
  I2S0.conf.tx_fifo_reset=1; I2S0.conf.tx_fifo_reset=0;
}

void loop() {

}


Now using 128bytes to represent 128bits seem very inefficient but perhaps that how to do it?

Also, examples I've seen for camera slave mode don't use the I2S0I_BCK_IN_IDX config and instead use the I2S0I_WS_IN_IDX.. this seems odd to me.
camera_slave.PNG
camera_slave.PNG (77.73 KiB) Viewed 3256 times
Also, I am not using any of the HSYNC/VSYNC/or HREF, but I have tried changing these without success. The esp32 datasheet doesn't give much details into how those signals are used to interpret the pulse stream. I suspect I need to use these...

The buffer full Interrupt get fired and I have a breakpoint in the ISR via JTAG, but the buffer I receive never seems to look even close to what I see on the logic analyzer.

Sorry for the messy code I've patched together! Any advice on how this should be configured is much appreciated!

samsonx
Posts: 3
Joined: Fri Jun 26, 2020 7:07 pm

Re: i2s DMA pixel data / clock input

Postby samsonx » Sat Jul 04, 2020 12:14 am

So made some progress with this; however having trouble aligning the frame rows correctly.. additionally it seems I really shouldn't be using LCD mode/parallel mode. It kinda works however takes 128byte for a single row of 128 on/off pixels. The signals actually seem better suited for a more standard i2s input. I am currently using i2s_read() and receive data but I am having trouble configuring the modes to match the data.

Code: Select all

#include <driver/i2s.h>
#include "freertos/queue.h"

#define DMD_DE        GPIO_NUM_36
#define DMD_ROW_DATA  GPIO_NUM_37
#define DMD_ROW_CLK   GPIO_NUM_38
#define DMD_COL_LATCH GPIO_NUM_39
#define DMD_PIX_CLK   GPIO_NUM_32
#define DMD_SDATA     GPIO_NUM_33

const i2s_port_t I2S_PORT = I2S_NUM_0;
const int BLOCK_SIZE = 8;

void setup() {
  Serial.begin(115200);
  Serial.println("Configuring I2S...");
  esp_err_t err;

  // The I2S config as per the example
  const i2s_config_t i2s_config = {
      .mode = i2s_mode_t(I2S_MODE_SLAVE | I2S_MODE_RX),
      .sample_rate = 4000000,                         // 4MHz
      .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
      .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
      .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_LSB),
      .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,     // Interrupt level 1
      .dma_buf_count = 4,                           // number of buffers
      .dma_buf_len = BLOCK_SIZE                     // samples per buffer
  };

  // The pin config as per the setup
  const i2s_pin_config_t pin_config = {
      .bck_io_num = DMD_PIX_CLK,   // BCKL
      .ws_io_num = DMD_ROW_CLK,    // LRCL
      .data_out_num = -1, // not used
      .data_in_num = DMD_SDATA   // DOUT
  };

  // Configuring the I2S driver and pins.
  // This function must be called before any I2S driver read/write operations.
  err = i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL);
  if (err != ESP_OK) {
    Serial.printf("Failed installing driver: %d\n", err);
    while (true);
  }
  err = i2s_set_pin(I2S_PORT, &pin_config);
  if (err != ESP_OK) {
    Serial.printf("Failed setting pin: %d\n", err);
    while (true);
  }
  Serial.println("I2S driver installed.");
}

void loop() {

  uint16_t samples[BLOCK_SIZE];
  size_t bytes_read;
  esp_err_t ret;
  ret = i2s_read(I2S_PORT, 
                &samples, 
                BLOCK_SIZE,
                &bytes_read, 
                1000);
  
  for (int i = 0; i < sizeof(samples) / sizeof(samples[0]); ++i) {
    for(uint32_t mask = 0x8000; mask; mask >>= 1){
      if(mask & samples[i])
        Serial.print('1');
      else
        Serial.print('0');
    }
  }
  Serial.println();
}
128bit frames.PNG
128bit frames.PNG (157.65 KiB) Viewed 3171 times
Not sure how to make it work as one frame is 128 bytes, and ws (ROW_CLK) flips each frame. It seem I should be able to configure this as 32-bit/sample, and 4 samples would make a full frame - and use LEFT/RIGHT channels for Frame 1 and 2? Does that make sense or is the largest a frame can be is 32-bits? Additionally the WS and BCK are not aligned like you see in the examples in the ESP32 tech datasheet.
WS and BCK.PNG
WS and BCK.PNG (16.43 KiB) Viewed 3171 times
Also, left channel/right channel (in reality these are consecutive frames, the difference of the two make a pseudo grey scale) come in at two different clock rates (500kHz and 1MHz) though the doesn't seem like it should matter for slave receiving mode.

Can anyone assist in configuring i2s for this use case? Eventually I expect to use a queue to handle the reads instead of reading them in in the loop() function.

Thanks in advance!

Who is online

Users browsing this forum: No registered users and 25 guests