/** * @file video_broadcast.c * @brief ESP32 Video Broadcast - RF modulation via I2S DMA * * This module generates analog NTSC/PAL television signals by outputting * an 80 MHz bitstream through I2S DMA. The premodulated waveforms create * harmonics at 61.25 MHz (Channel 3 luma) for RF broadcast. * * Key differences from ESP8266 version: * - Uses ESP32 I2S LCD mode for parallel output * - Uses GDMA (lldesc_t) instead of SLC (sdio_queue) * - Uses esp_intr_alloc() instead of ets_isr_attach() * - Uses APLL for clock generation * * Original ESP8266 version: Copyright 2015 <>< Charles Lohr * ESP32 Port 2024 */ #include "video_broadcast.h" #include "broadcast_tables.h" #include "CbTable.h" #include "sdkconfig.h" #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "driver/i2s_std.h" #include "driver/gpio.h" #include "esp_log.h" #include "esp_attr.h" #include "esp_timer.h" #include "soc/i2s_reg.h" #include "soc/i2s_struct.h" #include "soc/gpio_sig_map.h" #include "hal/gpio_ll.h" #include "rom/lldesc.h" #include "esp_rom_gpio.h" #include "esp_private/periph_ctrl.h" #include "esp_intr_alloc.h" #include "soc/rtc.h" static const char *TAG = "video_broadcast"; // Video timing constants #ifdef CONFIG_VIDEO_PAL #define LINE_BUFFER_LENGTH 160 #define SHORT_SYNC_INTERVAL 5 #define LONG_SYNC_INTERVAL 75 #define NORMAL_SYNC_INTERVAL 10 #define LINE_SIGNAL_INTERVAL 150 #define COLORBURST_INTERVAL 10 #else #define LINE_BUFFER_LENGTH 159 #define SHORT_SYNC_INTERVAL 6 #define LONG_SYNC_INTERVAL 73 #define SERRATION_PULSE_INT 67 #define NORMAL_SYNC_INTERVAL 12 #define LINE_SIGNAL_INTERVAL 147 #define COLORBURST_INTERVAL 4 #endif #define I2SDMABUFLEN LINE_BUFFER_LENGTH #define LINE32LEN I2SDMABUFLEN // I2S configuration for 80 MHz output // ESP32 can use APLL or divide from 240 MHz // Target: 80 MHz bit clock // Using I2S LCD mode: clock = source / (N + b/a) // For 80 MHz from 240 MHz APB: divider = 3 // Global state int8_t jam_color = -1; int gframe = 0; uint16_t framebuffer[((FBW2/4)*(FBH))*2]; uint32_t last_internal_frametime = 0; // Internal state static int gline = 0; static int linescratch; static uint8_t pixline; // Premodulation table pointers static const uint32_t *tablestart; static const uint32_t *tablept; static const uint32_t *tableend; static uint32_t *curdma; // DMA descriptors and buffers static lldesc_t dma_desc[DMABUFFERDEPTH] __attribute__((aligned(4))); static uint32_t dma_buffer[I2SDMABUFLEN * DMABUFFERDEPTH] __attribute__((aligned(4))); // I2S interrupt handle static intr_handle_t i2s_intr_handle = NULL; // Timing measurement static uint32_t systimex = 0; static uint32_t systimein = 0; /** * @brief Fill DMA buffer with premodulated waveform data * @param qty Number of 32-bit words to fill * @param color Color/level index into premodulation table */ static inline void IRAM_ATTR fillwith(uint16_t qty, uint8_t color) { if (qty & 1) { *(curdma++) = tablept[color]; tablept += PREMOD_SIZE; } qty >>= 1; for (linescratch = 0; linescratch < qty; linescratch++) { *(curdma++) = tablept[color]; tablept += PREMOD_SIZE; *(curdma++) = tablept[color]; tablept += PREMOD_SIZE; if (tablept >= tableend) { tablept = tablept - tableend + tablestart; } } } /** * @brief Short sync pulse (FT_STA) */ static void IRAM_ATTR FT_STA(void) { pixline = 0; // Reset framebuffer line counter fillwith(SHORT_SYNC_INTERVAL, SYNC_LEVEL); fillwith(LONG_SYNC_INTERVAL, BLACK_LEVEL); fillwith(SHORT_SYNC_INTERVAL, SYNC_LEVEL); fillwith(LINE32LEN - (SHORT_SYNC_INTERVAL + LONG_SYNC_INTERVAL + SHORT_SYNC_INTERVAL), BLACK_LEVEL); } /** * @brief Long sync pulse (FT_STB) */ static void IRAM_ATTR FT_STB(void) { #ifdef CONFIG_VIDEO_PAL #define FT_STB_BLACK_INTERVAL SHORT_SYNC_INTERVAL #else #define FT_STB_BLACK_INTERVAL NORMAL_SYNC_INTERVAL #endif fillwith(LONG_SYNC_INTERVAL, SYNC_LEVEL); fillwith(FT_STB_BLACK_INTERVAL, BLACK_LEVEL); fillwith(LONG_SYNC_INTERVAL, SYNC_LEVEL); fillwith(LINE32LEN - (LONG_SYNC_INTERVAL + FT_STB_BLACK_INTERVAL + LONG_SYNC_INTERVAL), BLACK_LEVEL); } /** * @brief Black/blanking line (FT_B) */ static void IRAM_ATTR FT_B(void) { fillwith(NORMAL_SYNC_INTERVAL, SYNC_LEVEL); fillwith(2, BLACK_LEVEL); fillwith(COLORBURST_INTERVAL, COLORBURST_LEVEL); fillwith(LINE32LEN - NORMAL_SYNC_INTERVAL - 2 - COLORBURST_INTERVAL, (pixline < 1) ? GRAY_LEVEL : BLACK_LEVEL); } /** * @brief Short to long sync transition (FT_SRA) */ static void IRAM_ATTR FT_SRA(void) { fillwith(SHORT_SYNC_INTERVAL, SYNC_LEVEL); fillwith(LONG_SYNC_INTERVAL, BLACK_LEVEL); #ifdef CONFIG_VIDEO_PAL fillwith(LONG_SYNC_INTERVAL, SYNC_LEVEL); fillwith(LINE32LEN - (SHORT_SYNC_INTERVAL + LONG_SYNC_INTERVAL + LONG_SYNC_INTERVAL), BLACK_LEVEL); #else fillwith(SERRATION_PULSE_INT, SYNC_LEVEL); fillwith(LINE32LEN - (SHORT_SYNC_INTERVAL + LONG_SYNC_INTERVAL + SERRATION_PULSE_INT), BLACK_LEVEL); #endif } /** * @brief Long to short sync transition (FT_SRB) */ static void IRAM_ATTR FT_SRB(void) { #ifdef CONFIG_VIDEO_PAL fillwith(LONG_SYNC_INTERVAL, SYNC_LEVEL); fillwith(SHORT_SYNC_INTERVAL, BLACK_LEVEL); fillwith(SHORT_SYNC_INTERVAL, SYNC_LEVEL); fillwith(LINE32LEN - (LONG_SYNC_INTERVAL + SHORT_SYNC_INTERVAL + SHORT_SYNC_INTERVAL), BLACK_LEVEL); #else fillwith(SERRATION_PULSE_INT, SYNC_LEVEL); fillwith(NORMAL_SYNC_INTERVAL, BLACK_LEVEL); fillwith(SHORT_SYNC_INTERVAL, SYNC_LEVEL); fillwith(LINE32LEN - (SERRATION_PULSE_INT + NORMAL_SYNC_INTERVAL + SHORT_SYNC_INTERVAL), BLACK_LEVEL); #endif } /** * @brief Active video line (FT_LIN) */ static void IRAM_ATTR FT_LIN(void) { fillwith(NORMAL_SYNC_INTERVAL, SYNC_LEVEL); fillwith(1, BLACK_LEVEL); fillwith(COLORBURST_INTERVAL, COLORBURST_LEVEL); fillwith(11, BLACK_LEVEL); #define HDR_SPD (NORMAL_SYNC_INTERVAL + 1 + COLORBURST_INTERVAL + 11) int fframe = gframe & 1; uint16_t *fbs = (uint16_t*)(&framebuffer[((pixline * (FBW2/2)) + (((FBW2/2)*(FBH)) * fframe)) / 2]); for (linescratch = 0; linescratch < FBW2/4; linescratch++) { uint16_t fbb = fbs[linescratch]; *(curdma++) = tablept[(fbb >> 0) & 15]; tablept += PREMOD_SIZE; *(curdma++) = tablept[(fbb >> 4) & 15]; tablept += PREMOD_SIZE; *(curdma++) = tablept[(fbb >> 8) & 15]; tablept += PREMOD_SIZE; *(curdma++) = tablept[(fbb >> 12) & 15]; tablept += PREMOD_SIZE; if (tablept >= tableend) { tablept = tablept - tableend + tablestart; } } fillwith(LINE32LEN - (HDR_SPD + FBW2), BLACK_LEVEL); pixline++; } /** * @brief End frame marker (FT_CLOSE_M) */ static void IRAM_ATTR FT_CLOSE_M(void) { #ifdef CONFIG_VIDEO_PAL fillwith(SHORT_SYNC_INTERVAL, SYNC_LEVEL); fillwith(LONG_SYNC_INTERVAL, BLACK_LEVEL); fillwith(SHORT_SYNC_INTERVAL, SYNC_LEVEL); fillwith(LINE32LEN - (SHORT_SYNC_INTERVAL + LONG_SYNC_INTERVAL + SHORT_SYNC_INTERVAL), BLACK_LEVEL); #else fillwith(NORMAL_SYNC_INTERVAL, SYNC_LEVEL); fillwith(2, BLACK_LEVEL); fillwith(4, COLORBURST_LEVEL); fillwith(LINE32LEN - NORMAL_SYNC_INTERVAL - 6, WHITE_LEVEL); #endif gline = -1; gframe++; last_internal_frametime = systimex; systimex = 0; systimein = (uint32_t)esp_timer_get_time(); } // Line type function table static void (*CbTable[FT_MAX_d])(void) = { FT_STA, FT_STB, FT_B, FT_SRA, FT_SRB, FT_LIN, FT_CLOSE_M }; /** * @brief I2S DMA interrupt handler * * Called when a DMA buffer completes transmission. * Fills the completed buffer with the next line's data. */ static void IRAM_ATTR i2s_isr(void *arg) { // Clear interrupt typeof(I2S0.int_st) status = I2S0.int_st; I2S0.int_clr.val = status.val; if (status.out_eof) { // Get the descriptor that just finished lldesc_t *finish_desc = (lldesc_t*)I2S0.out_eof_des_addr; curdma = (uint32_t*)finish_desc->buf; if (jam_color < 0) { // Normal operation - generate video line int lk = 0; if (gline & 1) { lk = (CbLookup[gline >> 1] >> 4) & 0x0f; } else { lk = CbLookup[gline >> 1] & 0x0f; } systimein = (uint32_t)esp_timer_get_time(); CbTable[lk](); systimex += (uint32_t)esp_timer_get_time() - systimein; gline++; } else { // Jam mode - fill with single color for RF testing fillwith(LINE32LEN, jam_color); } } } /** * @brief Configure I2S for 80 MHz serial bitstream output * * This uses standard I2S mode (not LCD mode) to output a serial bitstream * on the data pin at 80 MHz. The premodulated patterns create RF harmonics * at Channel 3 frequency (61.25 MHz). */ static void configure_i2s(void) { // Enable I2S peripheral periph_module_enable(PERIPH_I2S0_MODULE); // Reset I2S I2S0.conf.tx_reset = 1; I2S0.conf.tx_reset = 0; I2S0.conf.rx_reset = 1; I2S0.conf.rx_reset = 0; // Reset FIFO I2S0.conf.tx_fifo_reset = 1; I2S0.conf.tx_fifo_reset = 0; I2S0.conf.rx_fifo_reset = 1; I2S0.conf.rx_fifo_reset = 0; // Reset DMA I2S0.lc_conf.in_rst = 1; I2S0.lc_conf.in_rst = 0; I2S0.lc_conf.out_rst = 1; I2S0.lc_conf.out_rst = 0; // Disable LCD mode - use standard serial I2S I2S0.conf2.lcd_en = 0; I2S0.conf2.lcd_tx_wrx2_en = 0; I2S0.conf2.lcd_tx_sdx2_en = 0; I2S0.conf2.camera_en = 0; // Configure for serial transmission (like ESP8266) I2S0.conf.tx_msb_right = 0; I2S0.conf.tx_right_first = 0; I2S0.conf.tx_slave_mod = 0; // Master mode I2S0.conf.tx_mono = 1; // Mono - single channel I2S0.conf.tx_short_sync = 0; I2S0.conf.tx_msb_shift = 0; // No shift, output raw bits // Configure FIFO for 32-bit mono I2S0.fifo_conf.tx_fifo_mod = 3; // 32-bit single channel I2S0.fifo_conf.tx_fifo_mod_force_en = 1; I2S0.fifo_conf.dscr_en = 1; // Enable DMA // Configure channel - mono mode I2S0.conf_chan.tx_chan_mod = 3; // Single channel on data out // Configure sample bits - 32 bits per sample I2S0.sample_rate_conf.tx_bits_mod = 32; // Configure clock for 80 MHz bit clock // PLL_D2_CLK = 160 MHz (when CPU at 240 MHz) // We want BCK = 80 MHz // Master clock divider: 160 / 2 = 80 MHz // BCK divider: 1 (pass through) I2S0.clkm_conf.clkm_div_num = 2; // Divide 160 MHz by 2 = 80 MHz I2S0.clkm_conf.clkm_div_b = 0; I2S0.clkm_conf.clkm_div_a = 1; I2S0.clkm_conf.clk_en = 1; I2S0.clkm_conf.clka_en = 0; // Use PLL_D2_CLK (160 MHz) // BCK = MCLK / tx_bck_div_num // We want BCK = 80 MHz, MCLK = 80 MHz, so div = 1 I2S0.sample_rate_conf.tx_bck_div_num = 1; // Don't start yet I2S0.conf.tx_start = 0; ESP_LOGI(TAG, "I2S configured: serial mode, 80 MHz target bit clock"); } /** * @brief Setup DMA descriptors in circular configuration */ static void setup_dma_descriptors(void) { for (int i = 0; i < DMABUFFERDEPTH; i++) { dma_desc[i].size = I2SDMABUFLEN * 4; dma_desc[i].length = I2SDMABUFLEN * 4; dma_desc[i].buf = (uint8_t*)&dma_buffer[i * I2SDMABUFLEN]; dma_desc[i].owner = 1; dma_desc[i].sosf = 0; dma_desc[i].eof = 1; dma_desc[i].qe.stqe_next = (i < DMABUFFERDEPTH - 1) ? &dma_desc[i + 1] : &dma_desc[0]; // Initialize buffer to black memset((void*)dma_desc[i].buf, 0, I2SDMABUFLEN * 4); } } /** * @brief Configure GPIO for I2S data output */ static void configure_gpio(void) { int gpio_num = CONFIG_I2S_DATA_GPIO; // Configure GPIO as high-speed output gpio_set_direction(gpio_num, GPIO_MODE_OUTPUT); gpio_set_pull_mode(gpio_num, GPIO_FLOATING); // Set high drive strength for better RF output gpio_set_drive_capability(gpio_num, GPIO_DRIVE_CAP_3); // Route I2S0 serial data to GPIO // In standard I2S mode, DATA_OUT23 is the serial data (MSB first) esp_rom_gpio_connect_out_signal(gpio_num, I2S0O_DATA_OUT23_IDX, false, false); ESP_LOGI(TAG, "I2S serial data output on GPIO %d (high drive)", gpio_num); } void video_broadcast_init(void) { ESP_LOGI(TAG, "Initializing video broadcast system"); // Initialize table pointers tablestart = &premodulated_table[0]; tablept = &premodulated_table[0]; tableend = &premodulated_table[PREMOD_ENTRIES * PREMOD_SIZE]; // Initialize state jam_color = -1; gframe = 0; gline = 0; pixline = 0; memset(framebuffer, 0, sizeof(framebuffer)); // Configure I2S peripheral configure_i2s(); // Setup DMA descriptors setup_dma_descriptors(); // Configure GPIO configure_gpio(); // Register interrupt handler esp_intr_alloc(ETS_I2S0_INTR_SOURCE, ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL1, i2s_isr, NULL, &i2s_intr_handle); // Enable interrupt on TX EOF I2S0.int_ena.out_eof = 1; // Link DMA descriptor I2S0.out_link.addr = (uint32_t)&dma_desc[0]; I2S0.out_link.start = 1; // Start transmission I2S0.conf.tx_start = 1; ESP_LOGI(TAG, "Video broadcast started"); #ifdef CONFIG_VIDEO_PAL ESP_LOGI(TAG, "Video standard: PAL (%d lines)", VIDEO_LINES); #else ESP_LOGI(TAG, "Video standard: NTSC (%d lines)", VIDEO_LINES); #endif ESP_LOGI(TAG, "Framebuffer: %dx%d pixels", FBW2, FBH); } void video_broadcast_stop(void) { // Only stop if video was ever initialized if (!i2s_intr_handle) { ESP_LOGI(TAG, "Video broadcast not running"); return; } ESP_LOGI(TAG, "Stopping video broadcast"); // Stop transmission I2S0.conf.tx_start = 0; I2S0.out_link.stop = 1; // Disable interrupt I2S0.int_ena.out_eof = 0; // Free interrupt esp_intr_free(i2s_intr_handle); i2s_intr_handle = NULL; // Disable I2S peripheral periph_module_disable(PERIPH_I2S0_MODULE); ESP_LOGI(TAG, "Video broadcast stopped"); } void video_broadcast_pause(void) { if (i2s_intr_handle) { esp_intr_disable(i2s_intr_handle); } } void video_broadcast_resume(void) { if (i2s_intr_handle) { esp_intr_enable(i2s_intr_handle); } }