ESP32-S2 generic parallel input over I2S

KWolfe81
Posts: 14
Joined: Mon Dec 13, 2021 7:00 pm

ESP32-S2 generic parallel input over I2S

Postby KWolfe81 » Mon Dec 13, 2021 7:13 pm

Does anyone have guidance on getting the most generic, simple way to read a parallel input bus on an ESP32-S2 processor?

There's obviously the ESP-camera library, but is way bloated for my needs and will take me a while to widdle it down. There's also CNLohr's example for the ESP32 (found here). However, switching pins and obvious registers was not enough to get it working. I'll keep going down that road, but was hoping someone could point to something much quicker.

Thanks!

KWolfe81
Posts: 14
Joined: Mon Dec 13, 2021 7:00 pm

Re: ESP32-S2 generic parallel input over I2S

Postby KWolfe81 » Tue Dec 14, 2021 10:36 pm

I've been able to clock data in and it looks to be somewhat working. Migrating any of the ESP32 projects to ESP32S2 required two additions:

- Explicitly enabling the I2S module clock ( I2S0.clkm_conf ). This may have been done in other projects but I didn't see it.
- Toggling V_Sync. The ESP32 projects all tied the value high and relied on the pixel clock. That doesn't seem to be working for me.

The biggest issue I'm having now is that I cannot configure an 8-bit word via 'I2S0.sample_rate_conf.rx_bits_mod' 16, 24, and 32 bit seems to be working as expected.

In any case, here's my code as it is right now. Copying it here for anyone in the future and the off chance someone has feedback:

EDIT: Code removed, see later posts in this thread for a more up-to-date version.
Last edited by KWolfe81 on Mon Dec 20, 2021 9:03 pm, edited 1 time in total.

KWolfe81
Posts: 14
Joined: Mon Dec 13, 2021 7:00 pm

Re: ESP32-S2 generic parallel input over I2S

Postby KWolfe81 » Wed Dec 15, 2021 6:40 pm

Quick follow-up: 8-bit mode (rx_bits_mod = 8) does work, but data ingest starts with I2S0I_DATA_IN8_IDX, not I2S0I_DATA_IN0_IDX.

KWolfe81
Posts: 14
Joined: Mon Dec 13, 2021 7:00 pm

Re: ESP32-S2 generic parallel input over I2S

Postby KWolfe81 » Fri Dec 17, 2021 10:47 pm

OK, now I am running into a bug that I am completely stuck on. My data is being received in duplicate. I am outputting a bit clock from a function generator and a data line that consists of square waves at half the bit clock. I am only looking at a single data line and expect data received to toggle 0x01, 0x00, 0x01, 0x00...., but I am getting 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00....

It's as if I am clocking data in on the negative edges, however, I am getting the exact number of interrupts I expect given the number of clock pulses in.
]Untitled.png
]Untitled.png (12.05 KiB) Viewed 7424 times

KWolfe81
Posts: 14
Joined: Mon Dec 13, 2021 7:00 pm

Re: ESP32-S2 generic parallel input over I2S

Postby KWolfe81 » Sat Dec 18, 2021 12:46 am

Fully self contained example of my error below. This generates bit clock (2 MHz), data clock (1 MHz), a pulse on the word-clk to start data flowing, and outputs the DMA buffer to show the error. Grrr...

EDIT: Code removed, see later posts in this thread for a more up-to-date version.
Last edited by KWolfe81 on Mon Dec 20, 2021 9:02 pm, edited 1 time in total.

KWolfe81
Posts: 14
Joined: Mon Dec 13, 2021 7:00 pm

Re: ESP32-S2 generic parallel input over I2S

Postby KWolfe81 » Mon Dec 20, 2021 9:02 pm

Last post on this topic. Got it working, requires rx_dma_equal = 1. I'm not super confident of the meaning of this register, but there ya go. Good luck to any that stray here from the future.

Code: Select all

#include "driver/gpio.h"
#include "driver/i2s.h"
#include "driver/ledc.h"
#include "driver/periph_ctrl.h"
#include "esp_err.h"
#include "esp_intr.h"
#include "esp_log.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "rom/lldesc.h"
#include "soc/gpio_sig_map.h"
#include "soc/i2s_reg.h"
#include "soc/i2s_struct.h"
#include "soc/io_mux_reg.h"
#include "soc/soc.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>


void print_buff( void *p_buffer, uint16_t buffer_size );

#define ARRAY_SIZE(array)  (sizeof(array) / sizeof(array[0]))

typedef void ( *parallel_data_ready_cb_t )( const uint8_t *p_data, uint16_t data_len );

void parallel_input_manager_init( parallel_data_ready_cb_t cb );

static void _parallel_data_ready_cb( const uint8_t *p_data, uint16_t data_len );
static uint8_t *p_parallel_data;
static uint32_t parallel_data_len;
static uint32_t isr_cnt = 0;

//-----------------------------------------------------------------------------
void app_main()
{
	parallel_input_manager_init( _parallel_data_ready_cb );
	while ( 1 )
	{
		static uint8_t last = 0;
		if ( last != isr_cnt )
		{
			printf( "ISR Cnt: %i\n", isr_cnt );
			last = isr_cnt;
		}

		vTaskDelay( 1000 / portTICK_RATE_MS );
		if ( parallel_data_len != 0 )
		{
			print_buff( p_parallel_data, parallel_data_len );
			parallel_data_len = 0;
		}
	}
}

//-----------------------------------------------------------------------------
static void _parallel_data_ready_cb( const uint8_t *p_data, uint16_t data_len )
{
	if ( parallel_data_len == 0 )
	{
		p_parallel_data = (uint8_t*)p_data;
		parallel_data_len = data_len;
	}
}

//-----------------------------------------------------------------------------
typedef struct
{
  bool                      initialized;
  parallel_data_ready_cb_t  data_ready_cb;
} p_i_manager_t;

p_i_manager_t s_pi = { 0 };

// Assumes one word per channel
#define MAX_DMA_TRANSFER_SIZE   ( 1024 )
#define NUM_CHAINED_DMAS		(    2 )

DMA_ATTR uint8_t      dma_buff[NUM_CHAINED_DMAS][MAX_DMA_TRANSFER_SIZE];
DMA_ATTR lldesc_t     dma_descriptor[NUM_CHAINED_DMAS];

static void IRAM_ATTR i2s_isr(void* arg);
static intr_handle_t i2s_intr_handle;

static const uint8_t  data0_pin = 17;
static const uint8_t  pixel_clk_pin = 15;
static const uint8_t  word_width_bits = 8;		// Doesn't really matter for this example

static const uint16_t bclk_mhz = 5;				    // Must divide into 40MHz evenly (i2s clk / 2)

//-----------------------------------------------------------------------------
void parallel_input_manager_init( parallel_data_ready_cb_t cb )
{
  s_pi.data_ready_cb = cb;
 
  //---------------------------------------------------------------------------
  // Generate a fake data input pattern
  ledc_timer_config_t ledc_timer_dclk =
  {
    .duty_resolution = LEDC_TIMER_2_BIT,      // resolution of PWM duty
    .freq_hz = bclk_mhz * 1000 * 1000 / 3,		// Half the bit clock
    .speed_mode = LEDC_LOW_SPEED_MODE,        // timer mode
    .timer_num = LEDC_TIMER_1,      				  // timer index
    .clk_cfg = LEDC_AUTO_CLK,                 // Auto select the source clock
  };

  ledc_channel_config_t ledc_channel_dclk =
  {
    .channel    = LEDC_CHANNEL_0,
    .duty       = 2 << ( ledc_timer_dclk.duty_resolution - LEDC_TIMER_1_BIT - 1 ),
    .gpio_num   = data0_pin,
    .speed_mode = ledc_timer_dclk.speed_mode,
    .hpoint     = 0,
    .timer_sel  = ledc_timer_dclk.timer_num,
    .intr_type  = LEDC_INTR_DISABLE,
    .flags.output_invert = false,
  };

  periph_module_enable( PERIPH_LEDC_MODULE );

  ledc_timer_config( &ledc_timer_dclk );
  ledc_channel_config( &ledc_channel_dclk );

  //---------------------------------------------------------------------------
  // Set up our databus and I2S data signals as inputs
  gpio_config_t conf =
  {
    .mode = GPIO_MODE_INPUT,
    .pull_up_en = GPIO_PULLUP_DISABLE,
    .pull_down_en = GPIO_PULLDOWN_DISABLE,
    .intr_type = GPIO_INTR_DISABLE
  };

  // Input the data GPIO pins to the I2S module
  conf.mode = GPIO_MODE_INPUT;
  conf.pin_bit_mask = 1LL << data0_pin;
  gpio_config( &conf );
  gpio_matrix_in( data0_pin, I2S0I_DATA_IN8_IDX, false );
  gpio_matrix_out( data0_pin, LEDC_LS_SIG_OUT0_IDX, false, false ); // Needs to happen after gpio_config
  
  // Output the pixel clock to the peripherals
  conf.mode = GPIO_MODE_OUTPUT;
  conf.pin_bit_mask = 1LL << pixel_clk_pin;
  gpio_config( &conf );
  gpio_matrix_out( pixel_clk_pin, I2S0I_WS_OUT_IDX, false, false );

  uint8_t   parallel_word_byte_length = 1;
  uint16_t  bytes_per_sample          = parallel_word_byte_length * word_width_bits;
  uint16_t  samples_per_dma           = MAX_DMA_TRANSFER_SIZE / bytes_per_sample;

  periph_module_enable( PERIPH_I2S0_MODULE );                 // Enable and configure I2S peripheral
  I2S0.conf.rx_reset = 1;
  I2S0.conf.rx_reset = 0;
  I2S0.conf.rx_fifo_reset = 1;
  I2S0.conf.rx_fifo_reset = 0;
  I2S0.lc_conf.in_rst = 1;
  I2S0.lc_conf.in_rst = 0;

  I2S0.conf.rx_slave_mod = 0;                 // Enable master mode, clocking is internally generated
  I2S0.conf2.val = 0;                         // Clear all the bits
  I2S0.conf2.lcd_en = 1;                      // Enable parallel mode
  I2S0.conf2.camera_en = 1;                   // Use HSYNC/VSYNC/HREF to control sampling
  I2S0.conf2.cam_clk_loopback = 1;            // Use master PCLK as the rx clk
                                              // Equivalent to setting 'pixel_clk_pin' as an input and routing via gpio_matrix_in
  I2S0.clkm_conf.clk_sel = 2;                 // 0: No clock  1: APLL_CLK   2: PLL_160M_CLK  3: No clock
  I2S0.clkm_conf.clk_en = 1;                  // Enable clock gate
  
  static const uint16_t i2s_clk_mhz = 80;     // Based on 'I2S0.clkm_conf.clk_sel' setting above
                        
  // Configure clock divider
  I2S0.clkm_conf.clkm_div_a = 0;              // Fractional clock divider denominator value
  I2S0.clkm_conf.clkm_div_b = 0;              // Fractional clock divider numerator value
  I2S0.clkm_conf.clkm_div_num = 2;            // Integral I2S clock divider value
  I2S0.sample_rate_conf.rx_bck_div_num = (i2s_clk_mhz / ( I2S0.clkm_conf.clkm_div_num * bclk_mhz ) );   // i2s clk is divided before reaches BCK output
  printf( "I2S BClk (actual): %i MHz\n", i2s_clk_mhz / ( I2S0.clkm_conf.clkm_div_num * I2S0.sample_rate_conf.rx_bck_div_num ) );

  I2S0.conf.rx_right_first = 0;               // Receive right channel data first
  I2S0.conf.rx_msb_right = 0;                 // Place right channel data at the MSB in the receive FIFO
  I2S0.conf.rx_msb_shift = 0;                 // Enable receiver in Phillips standard mode
  I2S0.conf.rx_mono = 0;                      // Enable receiver in mono mode
  I2S0.conf.rx_short_sync = 0;                // Enable receiver in PCM standard mode
  I2S0.conf.rx_dma_equal = 1;                 // TBH: I'm not 100% sure I understand what this does
  I2S0.timing.val = 0;                        // No delays on any of the timing sequences 
  I2S0.fifo_conf.dscr_en = 1;                 // Enable I2S DMA mode
  I2S0.conf_chan.rx_chan_mod = 1;             // Store right channel data only (dependant upon conf.rx_msb_right)

  I2S0.sample_rate_conf.rx_bits_mod = 8 * parallel_word_byte_length;  // Bit length of I2S receiver channel (word width, must be multiple of 8)
                                                                      // Value of 0 = 32-bit.  When = 8, LSB = I2S0I_DATA_IN8_IDX, else LSB = I2S0I_DATA_IN0_IDX

  for ( uint8_t idx = 0; idx < NUM_CHAINED_DMAS; idx++ )
  {
	  dma_descriptor[idx].length = 0;                                   // Number of byte written to the buffer
	  dma_descriptor[idx].size = samples_per_dma * bytes_per_sample;    // In bytes, must be in whole number of words
	  dma_descriptor[idx].owner = 1;                                    // The allowed operator is the DMA controller
	  dma_descriptor[idx].sosf = 0;                                     // Start of sub-frame. Also likely not used with I2S
	  dma_descriptor[idx].buf = ( uint8_t * )dma_buff[idx];
	  dma_descriptor[idx].offset = 0;
	  dma_descriptor[idx].empty = 0;
	  dma_descriptor[idx].eof = 0; 								                      // indicates the end of the linked list
	  dma_descriptor[idx].qe.stqe_next = &dma_descriptor[ ( idx + 1 ) % NUM_CHAINED_DMAS ];       // pointer to the next descriptor	  
  }
  
  I2S0.rx_eof_num = samples_per_dma * bytes_per_sample;     // The length of data to be received
  I2S0.in_link.addr = ( uint32_t )&dma_descriptor[0];
  I2S0.in_link.start = 1;                                   // Start the inlink descriptor
  I2S0.int_ena.val = 0;                                     // Disable all interrupts
  I2S0.int_clr.val = I2S0.int_raw.val;                      // Clear interrupt flags
  I2S0.int_ena.in_suc_eof = 1;                              // Trigger interrupt when current descriptor is handled

  esp_intr_alloc(ETS_I2S0_INTR_SOURCE, ESP_INTR_FLAG_LEVEL1 | ESP_INTR_FLAG_IRAM, i2s_isr, (void*)&I2S0, &i2s_intr_handle);
  esp_intr_enable(i2s_intr_handle);
  I2S0.conf.rx_start = 1;                                   // Start I2S & the DMA
  
  // For I2S in parallel camera input mode, data reception starts with a falling edge on I2S0I_V_SYNC_IDX while H_SYNC = H_ENABLE = 1
  gpio_matrix_in( 0x38, I2S0I_H_SYNC_IDX, false );          // 0x3C sets signal low (0), 0x38 sets signal high (1)
  gpio_matrix_in( 0x38, I2S0I_H_ENABLE_IDX, false );
  gpio_matrix_in( 0x38, I2S0I_V_SYNC_IDX, false );
  gpio_matrix_in( 0x3C, I2S0I_V_SYNC_IDX, false );

  s_pi.initialized = true;
}

//-----------------------------------------------------------------------------
static void IRAM_ATTR i2s_isr(void* arg)
{
  I2S0.int_clr.val = I2S0.int_raw.val;
  isr_cnt++;
  if ( s_pi.data_ready_cb )
  {
    lldesc_t *p_dma_descriptor = (lldesc_t *)I2S0.in_eof_des_addr;
    s_pi.data_ready_cb( ( const uint8_t *)p_dma_descriptor->buf, p_dma_descriptor->size );
  }
}

//-----------------------------------------------------------------------------
void print_buff_custom_format( void *p_buffer, uint16_t buffer_size, uint8_t bytes_per_line, uint32_t base_header_line_cnt,
                               uint32_t base_header_address_offset )
{
  char      line_header[20] = "";
  char      line_text[( bytes_per_line * 3 ) + 1]; // 2 ASCII Characters + space + null
  uint16_t  line_cnt      = 0;
  uint8_t   line_byte_cnt = 0;
  uint8_t   *p_next_in  = ( uint8_t * )p_buffer;
  char      *p_next_out = line_text;

  if ( buffer_size > bytes_per_line )
  {
    printf( "\n" );
  }

  while ( buffer_size )
  {
    if ( line_byte_cnt == 0 )
    {
      sprintf( line_header, "[%04i-0x%04x]: 0x", line_cnt + base_header_line_cnt,
               ( line_cnt * bytes_per_line ) + base_header_address_offset );
      line_cnt++;
      line_byte_cnt = 0;
      p_next_out    = line_text;
    }

    p_next_out += sprintf( p_next_out, "%02x ", *p_next_in );
    p_next_in++;
    buffer_size--;
    line_byte_cnt++;

    if ( ( line_byte_cnt == bytes_per_line ) || ( buffer_size == 0 ) )
    {
      printf( "%s%s\n", line_header, line_text );
      line_byte_cnt = 0;
    }
  }
}

//-----------------------------------------------------------------------------
void print_buff( void *p_buffer, uint16_t buffer_size )
{
  print_buff_custom_format( p_buffer, buffer_size, 16, 0, 0 );
}

ParkSun
Posts: 3
Joined: Mon May 16, 2022 10:40 am

Re: ESP32-S2 generic parallel input over I2S

Postby ParkSun » Mon May 16, 2022 11:22 am

Is there a header file that goes with this or is this the complete code?

Who is online

Users browsing this forum: No registered users and 141 guests