Pirate TV for the esp32
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

497 lines
14 KiB

/**
* @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 <string.h>
#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);
}
}