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
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);
|
|
}
|
|
}
|