Browse Source
Initial commit: ESP32 Channel3 RF TV Broadcast
Initial commit: ESP32 Channel3 RF TV Broadcast
ESP32 port of Channel3 - broadcasts analog NTSC/PAL TV signals using I2S DMA at 80MHz. Features include: - RF broadcast on Channel 3 (61.25 MHz) - Web UI for configuration - MQTT integration with Home Assistant - Weather display via Open-Meteo API - Screen rotation with transitions (fade, wipe, dissolve) - 3D graphics engine - Uploaded image display - Settings export/import Hardware: ESP32 with GPIO 22 as RF output Build: ESP-IDF v5.5.2 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>master
commit
0f692012a5
31 changed files with 8791 additions and 0 deletions
-
25.gitignore
-
12CMakeLists.txt
-
121README.md
-
10build.bat
-
43build.ps1
-
11build_helper.ps1
-
10build_only.ps1
-
7components/tablemaker/CMakeLists.txt
-
55components/tablemaker/CbTable.c
-
41components/tablemaker/CbTable.h
-
66components/tablemaker/broadcast_tables.c
-
32components/tablemaker/broadcast_tables.h
-
26flash.ps1
-
10flash_only.ps1
-
602main/3d.c
-
76main/3d.h
-
21main/CMakeLists.txt
-
63main/Kconfig.projbuild
-
4272main/user_main.c
-
497main/video_broadcast.c
-
70main/video_broadcast.h
-
10monitor.ps1
-
5partitions.csv
-
26rebuild.ps1
-
19run_build.bat
-
5run_build.cmd
-
2231sdkconfig
-
38sdkconfig.defaults
-
84tools/README.md
-
8tools/stream_lcars.bat
-
295tools/stream_video.py
@ -0,0 +1,25 @@ |
|||||
|
# Build output |
||||
|
build/ |
||||
|
*.bin |
||||
|
*.elf |
||||
|
*.map |
||||
|
|
||||
|
# IDE |
||||
|
.vscode/ |
||||
|
.idea/ |
||||
|
|
||||
|
# Logs |
||||
|
*.log |
||||
|
build_full.log |
||||
|
build_log*.txt |
||||
|
build_output.txt |
||||
|
|
||||
|
# Backup files |
||||
|
*.old |
||||
|
*.bak |
||||
|
*~ |
||||
|
|
||||
|
# OS files |
||||
|
.DS_Store |
||||
|
Thumbs.db |
||||
|
*.mp4 |
||||
@ -0,0 +1,12 @@ |
|||||
|
# Channel3 ESP32 Port - RF Broadcast via I2S DMA |
||||
|
# This is an ESP-IDF project that ports the ESP8266 Channel3 firmware to ESP32 |
||||
|
|
||||
|
cmake_minimum_required(VERSION 3.16) |
||||
|
|
||||
|
# Set default build type to Release if not specified |
||||
|
if(NOT CMAKE_BUILD_TYPE) |
||||
|
set(CMAKE_BUILD_TYPE Release) |
||||
|
endif() |
||||
|
|
||||
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake) |
||||
|
project(channel3_esp32) |
||||
@ -0,0 +1,121 @@ |
|||||
|
# Channel3 ESP32 Port |
||||
|
|
||||
|
ESP32 port of the Channel3 analog NTSC/PAL television broadcast firmware. |
||||
|
|
||||
|
## Overview |
||||
|
|
||||
|
This project ports the ESP8266 Channel3 firmware to ESP32, maintaining the ability to broadcast RF signals on Channel 3 (61.25 MHz) directly from a GPIO pin. |
||||
|
|
||||
|
**WARNING**: RF broadcast without proper licensing may be illegal in your jurisdiction. This project is for educational purposes only. |
||||
|
|
||||
|
## How It Works |
||||
|
|
||||
|
The ESP32 outputs an 80 MHz bitstream via I2S DMA. Pre-computed waveform patterns create harmonics at the Channel 3 carrier frequency (61.25 MHz for luma). The GPIO pin acts as an antenna, radiating RF directly. |
||||
|
|
||||
|
### Key Differences from ESP8266 Version |
||||
|
|
||||
|
| Component | ESP8266 | ESP32 | |
||||
|
|-----------|---------|-------| |
||||
|
| Clock source | 160 MHz / 2 | 160 MHz / 2 (PLL_D2) | |
||||
|
| DMA | SLC (sdio_queue) | GDMA (lldesc_t) | |
||||
|
| I2S mode | Standard I2S | LCD/parallel mode | |
||||
|
| ISR attach | ets_isr_attach() | esp_intr_alloc() | |
||||
|
| Framework | ESP8266 RTOS SDK | ESP-IDF 5.x | |
||||
|
|
||||
|
## Building |
||||
|
|
||||
|
### Prerequisites |
||||
|
|
||||
|
- ESP-IDF 5.x installed and configured |
||||
|
- ESP32 development board (original ESP32, not ESP32-S2/S3/C3) |
||||
|
|
||||
|
### Build Commands |
||||
|
|
||||
|
```bash |
||||
|
# Set up ESP-IDF environment |
||||
|
. $IDF_PATH/export.sh |
||||
|
|
||||
|
# Configure the project (optional - to change settings) |
||||
|
idf.py menuconfig |
||||
|
|
||||
|
# Build |
||||
|
idf.py build |
||||
|
|
||||
|
# Flash |
||||
|
idf.py -p /dev/ttyUSB0 flash |
||||
|
|
||||
|
# Monitor |
||||
|
idf.py -p /dev/ttyUSB0 monitor |
||||
|
``` |
||||
|
|
||||
|
## Configuration |
||||
|
|
||||
|
Use `idf.py menuconfig` to access settings under "Channel3 Configuration": |
||||
|
|
||||
|
- **Video Standard**: NTSC (default) or PAL |
||||
|
- **I2S Data Output GPIO**: GPIO pin for RF output (default: GPIO22) |
||||
|
- **WiFi SoftAP SSID**: Access point name (default: "Channel3") |
||||
|
- **WiFi SoftAP Password**: Access point password |
||||
|
|
||||
|
## Hardware Setup |
||||
|
|
||||
|
1. Connect a short wire (antenna) to the configured GPIO pin (default GPIO22) |
||||
|
2. Tune an analog TV to Channel 3 |
||||
|
3. The ESP32 will broadcast directly - no external components needed |
||||
|
|
||||
|
**Note**: The RF output is very low power. The antenna must be very close to the TV antenna for reception. |
||||
|
|
||||
|
## Project Structure |
||||
|
|
||||
|
``` |
||||
|
esp32_channel3/ |
||||
|
├── CMakeLists.txt # Main project CMake file |
||||
|
├── sdkconfig.defaults # Default SDK configuration |
||||
|
├── main/ |
||||
|
│ ├── CMakeLists.txt # Main component build file |
||||
|
│ ├── Kconfig.projbuild # Configuration options |
||||
|
│ ├── video_broadcast.c # I2S DMA video generation (ESP32) |
||||
|
│ ├── video_broadcast.h # Video broadcast header |
||||
|
│ ├── 3d.c # Fixed-point 3D graphics engine |
||||
|
│ ├── 3d.h # 3D graphics header |
||||
|
│ └── user_main.c # Application entry & demo screens |
||||
|
└── components/ |
||||
|
└── tablemaker/ |
||||
|
├── CMakeLists.txt # Component build file |
||||
|
├── broadcast_tables.c # Premodulated RF waveforms |
||||
|
├── broadcast_tables.h # Table definitions |
||||
|
├── CbTable.c # NTSC/PAL line type lookup |
||||
|
└── CbTable.h # Line type definitions |
||||
|
``` |
||||
|
|
||||
|
## Technical Notes |
||||
|
|
||||
|
### I2S LCD Mode |
||||
|
|
||||
|
The ESP32 I2S peripheral is configured in LCD mode for parallel output. This allows continuous DMA output at high bitrates without the overhead of standard I2S framing. |
||||
|
|
||||
|
### Clock Configuration |
||||
|
|
||||
|
The target is 80 MHz output to match the original ESP8266 implementation: |
||||
|
- ESP32 PLL_D2 clock: 160 MHz |
||||
|
- Divider: 2 |
||||
|
- Output: 80 MHz |
||||
|
|
||||
|
### DMA Operation |
||||
|
|
||||
|
DMA descriptors are configured in a circular buffer. The ISR is called on each buffer completion (EOF), filling the buffer with the next line's premodulated data. |
||||
|
|
||||
|
## Known Limitations |
||||
|
|
||||
|
1. **RF Quality**: ESP32 GPIO slew rate and output characteristics differ from ESP8266. RF quality may vary. |
||||
|
2. **Timing Sensitivity**: Video signal generation is timing-critical. Heavy system load may cause visible artifacts. |
||||
|
3. **Legal Restrictions**: Unlicensed RF transmission is illegal in most jurisdictions. |
||||
|
|
||||
|
## Credits |
||||
|
|
||||
|
Original ESP8266 Channel3: Charles Lohr (CNLohr) |
||||
|
ESP32 Port: Based on original architecture |
||||
|
|
||||
|
## License |
||||
|
|
||||
|
See LICENSE file in the parent directory. |
||||
@ -0,0 +1,10 @@ |
|||||
|
@echo off |
||||
|
echo Starting ESP-IDF build... > build_log.txt |
||||
|
call C:\Espressif\idf_cmd_init.bat esp-idf-v5.5.2 >> build_log.txt 2>&1 |
||||
|
echo Changing to project directory... >> build_log.txt |
||||
|
cd /d C:\git\channel3\esp32_channel3 |
||||
|
echo Setting target to esp32... >> build_log.txt |
||||
|
idf.py set-target esp32 >> build_log.txt 2>&1 |
||||
|
echo Building project... >> build_log.txt |
||||
|
idf.py build >> build_log.txt 2>&1 |
||||
|
echo Build complete. Exit code: %ERRORLEVEL% >> build_log.txt |
||||
@ -0,0 +1,43 @@ |
|||||
|
$ErrorActionPreference = "Continue" |
||||
|
|
||||
|
# Clear MSYS environment variables that confuse ESP-IDF |
||||
|
Remove-Item Env:MSYSTEM -ErrorAction SilentlyContinue |
||||
|
Remove-Item Env:MSYSTEM_PREFIX -ErrorAction SilentlyContinue |
||||
|
Remove-Item Env:MSYSTEM_CARCH -ErrorAction SilentlyContinue |
||||
|
Remove-Item Env:MSYSTEM_CHOST -ErrorAction SilentlyContinue |
||||
|
Remove-Item Env:MINGW_CHOST -ErrorAction SilentlyContinue |
||||
|
Remove-Item Env:MINGW_PREFIX -ErrorAction SilentlyContinue |
||||
|
Remove-Item Env:MINGW_PACKAGE_PREFIX -ErrorAction SilentlyContinue |
||||
|
$env:MSYSTEM = "" |
||||
|
|
||||
|
# Set ESP-IDF environment variables |
||||
|
$env:IDF_PATH = "C:\Espressif\frameworks\esp-idf-v5.5.2" |
||||
|
$env:IDF_TOOLS_PATH = "C:\Espressif" |
||||
|
$env:IDF_PYTHON_ENV_PATH = "C:\Espressif\python_env\idf5.5_py3.11_env" |
||||
|
|
||||
|
# Add tools to PATH (correct versions) |
||||
|
$toolPaths = @( |
||||
|
"C:\Espressif\python_env\idf5.5_py3.11_env\Scripts", |
||||
|
"C:\Espressif\tools\cmake\3.30.2\bin", |
||||
|
"C:\Espressif\tools\ninja\1.12.1", |
||||
|
"C:\Espressif\tools\xtensa-esp-elf\esp-14.2.0_20251107\xtensa-esp-elf\bin", |
||||
|
"C:\Espressif\tools\esp32ulp-elf\2.38_20240113\esp32ulp-elf\bin", |
||||
|
"C:\Espressif\tools\idf-git\2.44.0\cmd", |
||||
|
"C:\Espressif\tools\idf-python\3.11.2\python.exe" |
||||
|
) |
||||
|
|
||||
|
$env:PATH = ($toolPaths -join ";") + ";" + $env:PATH |
||||
|
|
||||
|
# Change to project directory |
||||
|
Set-Location "C:\git\channel3\esp32_channel3" |
||||
|
|
||||
|
Write-Host "ESP-IDF Path: $env:IDF_PATH" |
||||
|
Write-Host "Working directory: $(Get-Location)" |
||||
|
Write-Host "Starting build..." |
||||
|
|
||||
|
# Run idf.py |
||||
|
$python = "C:\Espressif\python_env\idf5.5_py3.11_env\Scripts\python.exe" |
||||
|
$idfpy = "$env:IDF_PATH\tools\idf.py" |
||||
|
|
||||
|
Write-Host "Building..." |
||||
|
& $python $idfpy build |
||||
@ -0,0 +1,11 @@ |
|||||
|
$env:IDF_PYTHON_ENV_PATH = "C:\Espressif\python_env\idf5.5_py3.11_env" |
||||
|
$env:IDF_PATH = "C:\Espressif\frameworks\esp-idf-v5.5.2" |
||||
|
$env:IDF_TOOLS_PATH = "C:\Espressif" |
||||
|
$env:MSYSTEM = $null |
||||
|
$env:SHELL = $null |
||||
|
$env:SHLVL = $null |
||||
|
$env:TERM = $null |
||||
|
Set-Location "C:\git\channel3\esp32_channel3" |
||||
|
. "C:\Espressif\Initialize-Idf.ps1" |
||||
|
idf.py build |
||||
|
idf.py -p COM5 flash |
||||
@ -0,0 +1,10 @@ |
|||||
|
$env:IDF_PYTHON_ENV_PATH = "C:\Espressif\python_env\idf5.5_py3.11_env" |
||||
|
$env:IDF_PATH = "C:\Espressif\frameworks\esp-idf-v5.5.2" |
||||
|
$env:IDF_TOOLS_PATH = "C:\Espressif" |
||||
|
$env:MSYSTEM = $null |
||||
|
$env:SHELL = $null |
||||
|
$env:SHLVL = $null |
||||
|
$env:TERM = $null |
||||
|
Set-Location "C:\git\channel3\esp32_channel3" |
||||
|
. "C:\Espressif\Initialize-Idf.ps1" |
||||
|
idf.py build |
||||
@ -0,0 +1,7 @@ |
|||||
|
idf_component_register( |
||||
|
SRCS |
||||
|
"broadcast_tables.c" |
||||
|
"CbTable.c" |
||||
|
INCLUDE_DIRS |
||||
|
"." |
||||
|
) |
||||
@ -0,0 +1,55 @@ |
|||||
|
/** |
||||
|
* @file CbTable.c |
||||
|
* @brief Line type lookup tables for NTSC/PAL video signal generation |
||||
|
* |
||||
|
* These tables define the signal type for each half-line in a video frame. |
||||
|
* The values are packed as nibbles - even lines use low nibble, odd use high nibble. |
||||
|
* |
||||
|
* Original Copyright 2015 <>< Charles Lohr |
||||
|
* ESP32 Port 2024 |
||||
|
*/ |
||||
|
|
||||
|
#include "CbTable.h" |
||||
|
|
||||
|
const uint8_t CbLookupPAL[313] = { |
||||
|
0x11, 0x04, 0x20, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, |
||||
|
0x22, 0x52, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x22, 0x22, 0x22, 0x22, 0x22, 0x00, 0x13, 0x01, 0x20, 0x22, |
||||
|
0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x52, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x22, 0x22, 0x22, 0x22, 0x22, 0x00, 0x66, |
||||
|
}; |
||||
|
|
||||
|
const uint8_t CbLookupNTSC[263] = { |
||||
|
0x00, 0x10, 0x11, 0x00, 0x20, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x25, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, |
||||
|
0x22, 0x22, 0x22, 0x02, 0x00, 0x13, 0x41, 0x00, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, |
||||
|
0x22, 0x22, 0x22, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, |
||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x22, 0x22, 0x22, 0x22, |
||||
|
0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x06, |
||||
|
}; |
||||
@ -0,0 +1,41 @@ |
|||||
|
/** |
||||
|
* @file CbTable.h |
||||
|
* @brief Line type lookup tables for NTSC/PAL video signal generation |
||||
|
* |
||||
|
* Defines the state machine for each scanline (263 lines NTSC, 313 lines PAL). |
||||
|
* Each entry specifies the line type: sync pulses, blanking, colorburst, active video. |
||||
|
* |
||||
|
* Original Copyright 2015 <>< Charles Lohr |
||||
|
* ESP32 Port 2024 |
||||
|
*/ |
||||
|
|
||||
|
#ifndef CBTABLE_H |
||||
|
#define CBTABLE_H |
||||
|
|
||||
|
#include <stdint.h> |
||||
|
#include "sdkconfig.h" |
||||
|
|
||||
|
// Line type definitions |
||||
|
#define FT_STA_d 0 // Short Sync A |
||||
|
#define FT_STB_d 1 // Long Sync B |
||||
|
#define FT_B_d 2 // Black/Blanking |
||||
|
#define FT_SRA_d 3 // Short to long sync transition |
||||
|
#define FT_SRB_d 4 // Long to short sync transition |
||||
|
#define FT_LIN_d 5 // Active video line |
||||
|
#define FT_CLOSE 6 // End frame |
||||
|
#define FT_MAX_d 7 // Number of line types |
||||
|
|
||||
|
// Line lookup tables (each nibble is a line type) |
||||
|
extern const uint8_t CbLookupPAL[313]; |
||||
|
extern const uint8_t CbLookupNTSC[263]; |
||||
|
|
||||
|
// Select the appropriate table based on video standard |
||||
|
#ifdef CONFIG_VIDEO_PAL |
||||
|
#define VIDEO_LINES 625 |
||||
|
#define CbLookup CbLookupPAL |
||||
|
#else |
||||
|
#define VIDEO_LINES 525 |
||||
|
#define CbLookup CbLookupNTSC |
||||
|
#endif |
||||
|
|
||||
|
#endif // CBTABLE_H |
||||
@ -0,0 +1,66 @@ |
|||||
|
/** |
||||
|
* @file broadcast_tables.c |
||||
|
* @brief Premodulated waveform lookup tables for NTSC/PAL RF broadcast |
||||
|
* |
||||
|
* These tables contain precomputed RF waveform patterns that, when output |
||||
|
* at 80 MHz, create the necessary harmonics for Channel 3 broadcast. |
||||
|
* |
||||
|
* Original Copyright 2015 <>< Charles Lohr |
||||
|
* ESP32 Port 2024 |
||||
|
*/ |
||||
|
|
||||
|
#include "broadcast_tables.h" |
||||
|
|
||||
|
const uint32_t premodulated_table[918] = { |
||||
|
0xbbddddee, 0xbbfddfee, 0xffffddee, 0xbdff9cef, 0xdff9deff, 0xffbdeffd, 0xfb9cffde, 0x39dffdce, 0xbbddffff, 0xbfffddef, 0xffffffff, 0xbffffdff, 0xffffdfff, 0xfffdffff, 0xfbdffffe, 0xbbffffef, 0xb9dddcee, 0x9999cccc, |
||||
|
0xee77773b, 0xeff777fb, 0xffff773b, 0xfee73ff7, 0xee77ff73, 0xef7fff3b, 0xf7fef7bf, 0x7fef73ff, 0xee77ffff, 0xfff77fff, 0xffffffff, 0xfff7ffff, 0xfef7fffb, 0xef7fff7b, 0xfffff7bf, 0xffff7fff, 0xee677333, 0x66663333, |
||||
|
0xbbddddee, 0xbbfddfee, 0xffffddee, 0xb9fffdef, 0x9fffdcef, 0xfff9ceff, 0xff9deffc, 0xfbdfffce, 0xbbddffff, 0xbdfffdef, 0xffffffff, 0xbffffdef, 0xbfffdeff, 0xfffddfff, 0xffbdffff, 0xfbdffffe, 0xb9dddcce, 0x9999cccc, |
||||
|
0xee77773b, 0xeff777fb, 0xffff773b, 0x7fef7bff, 0xfef73ff7, 0xee77ff73, 0xe77ff73b, 0xf7fe73bf, 0xee77ffff, 0xffef7bff, 0xffffffff, 0xffff7fff, 0xfef7ffff, 0xef7ffffb, 0xefffffbf, 0xfffff7ff, 0xeee773bb, 0x66663333, |
||||
|
0xbbddddee, 0xbbfddfee, 0xffffddee, 0x39dffdce, 0xbdfffdef, 0x9ffbdeff, 0xffb9cfff, 0xfb9dffdc, 0xbbddffff, 0xfbdfffce, 0xffffffff, 0xfbdffffe, 0xbffffdef, 0xffffdeff, 0xfffdffff, 0xfffdfffe, 0xb99dddce, 0x9999cccc, |
||||
|
0xee77773b, 0xeff777fb, 0xffff773b, 0xfffe73bf, 0xffe77bff, 0xfef7bff3, 0xee7fff7b, 0xe7fff73b, 0xee77ffff, 0xffff77ff, 0xffffffff, 0xffff77ff, 0xffff7fff, 0xfef7ffff, 0xef7fff7b, 0xfffff7bf, 0xe667733b, 0x66663333, |
||||
|
0xbbddddee, 0xbbfddfee, 0xffffddee, 0xfb9cffde, 0x39dffdce, 0xbdff9cef, 0xdff9deff, 0xffbdeffd, 0xbbddffff, 0xfbdffffe, 0xffffffff, 0xfbdffffe, 0xfbffffef, 0xbffffdef, 0xffffdfff, 0xfffdffff, 0xbb9dddce, 0x9999cccc, |
||||
|
0xee77773b, 0xeff777fb, 0xffff773b, 0xe7fef7bf, 0x7fef73ff, 0xfee73ff7, 0xee77ff73, 0xef7fff3b, 0xee77ffff, 0xf7fff7bf, 0xffffffff, 0xfffff7bf, 0xffff7fff, 0xfff77fff, 0xfef7fffb, 0xef7fff7b, 0xe677733b, 0x66663333, |
||||
|
0xbbddddee, 0xbbfddfee, 0xffffddee, 0xff9deffc, 0xfbdfffde, 0xb9fffdef, 0x9fffdcef, 0xfff9ceff, 0xbbddffff, 0xffbdffff, 0xffffffff, 0xffbdffff, 0xfbdffffe, 0xbffffdef, 0xbfffdfff, 0xffffdfff, 0xbb9ddcce, 0x9999cccc, |
||||
|
0xee77773b, 0xeff777fb, 0xffff773b, 0xe77ff73b, 0xf7fe73bf, 0x7fef7bff, 0xfee73ff7, 0xee77ff73, 0xee77ffff, 0xefffff3b, 0xffffffff, 0xeffffffb, 0xfffff7ff, 0xffff7fff, 0xfef7ffff, 0xeffffffb, 0xe677773b, 0x66663333, |
||||
|
0xbbddddee, 0xbbfddfee, 0xffffddee, 0xffb9cfff, 0xfb9dffdc, 0x7bdfffce, 0xbdfffdef, 0x9ffbdeff, 0xbbddffff, 0xfffddfff, 0xffffffff, 0xfffdffff, 0xfffdfffe, 0xfbdffffe, 0xbffffdef, 0xffffdeff, 0xbb9dccee, 0x9999cccc, |
||||
|
0xee77773b, 0xeff777fb, 0xffff773b, 0xee7fff7b, 0xe7fff73b, 0xfffe73bf, 0xffe77bff, 0xfef7bff3, 0xee77ffff, 0xef7fff7b, 0xffffffff, 0xef7fff7b, 0xeffff7bf, 0xffff77ff, 0xffff7fff, 0xfef7ffff, 0xee77773b, 0x66663333, |
||||
|
0xbbddddee, 0xbbfddfee, 0xffffddee, 0xdffbdeff, 0xffbdeffd, 0xfb9cffde, 0x39dffdce, 0xbdff9cef, 0xbbddffff, 0xffffdeff, 0xffffffff, 0xffffdfff, 0xfffdffff, 0xfbdffffe, 0xfbffffef, 0xbffffdef, 0xbb99dcee, 0x9999cccc, |
||||
|
0xee77773b, 0xeff777fb, 0xffff773b, 0xee77fff3, 0xef7fff3b, 0xe7fef7bf, 0x7fef73ff, 0xfee73fff, 0xee77ffff, 0xfef7fffb, 0xffffffff, 0xfef7fffb, 0xef7fff7b, 0xfffff7bf, 0xffff7fff, 0xfff77fff, 0xee777733, 0x66663333, |
||||
|
0xbbddddee, 0xbbfddfee, 0xffffddee, 0x9dffdcef, 0xfff9ceff, 0xffbdeffc, 0xfbdfffde, 0xb9fffdef, 0xbbddffff, 0xbfffdcff, 0xffffffff, 0xbfffdfff, 0xffffdfff, 0xffbdffff, 0xfbdffffe, 0xbffffdef, 0xb9dddcee, 0x9999cccc, |
||||
|
0xee77773b, 0xeff777fb, 0xffff773b, 0xfee73ff7, 0xee77ff73, 0xe77ff73b, 0xf7fe73bf, 0x7fef7bff, 0xee77ffff, 0xfef7ffff, 0xffffffff, 0xfff7ffff, 0xfffffffb, 0xeffffffb, 0xfffff7bf, 0xffff7fff, 0xee677733, 0x66663333, |
||||
|
0xbbddddee, 0xbbfddfee, 0xffffddee, 0xbdfffdef, 0x9ffbdcff, 0xffb9cfff, 0xfb9dfffc, 0x7bdfffce, 0xbbddffff, 0xbdfffdef, 0xffffffff, 0xbffffdef, 0xffffdeff, 0xfffdffff, 0xfffdfffe, 0xfbdffffe, 0xb9dddcce, 0x9999cccc, |
||||
|
0xee77773b, 0xeff777fb, 0xffff773b, 0xffe77bff, 0xfef7bff3, 0xee7fff7b, 0xe77ff73b, 0xf7fe73bf, 0xee77ffff, 0xffef7fff, 0xffffffff, 0xffff7fff, 0xfef7ffff, 0xef7fff7b, 0xeffff7bf, 0xffff77ff, 0xeee773bb, 0x66663333, |
||||
|
0xbbddddee, 0xbbfddfee, 0xffffddee, 0x39dffdce, 0xbdff9cef, 0xdffbdeff, 0xffbdcffd, 0xfb9cffde, 0xbbddffff, 0xbbdfffee, 0xffffffff, 0xfbffffee, 0xbffffdef, 0xffffdfff, 0xfffdffff, 0xffdffffe, 0xb99ddcce, 0x9999cccc, |
||||
|
0xee77773b, 0xeff777fb, 0xffff773b, 0x7fef73ff, 0xfee73fff, 0xfe77fff3, 0xef7fff3b, 0xe7fef7bf, 0xee77ffff, 0xffff7bff, 0xffffffff, 0xffff7fff, 0xfff77fff, 0xfef7ffff, 0xef7fff7b, 0xfffff7bf, 0xeee773bb, 0x66663333, |
||||
|
0xbbddddee, 0xbbfddfee, 0xffffddee, 0xfbdeffde, 0xb9dffdce, 0x9dffdcef, 0xdff9ceff, 0xffbdeffc, 0xbbddffff, 0xfbdfffde, 0xffffffff, 0xfbdffffe, 0xbffffdef, 0xbfffdfff, 0xffffdfff, 0xffbdffff, 0xb99dddce, 0x9999cccc, |
||||
|
0xee77773b, 0xeff777fb, 0xffff773b, 0xf7fe77bf, 0x7fef7bff, 0xfee73ff7, 0xee77ff73, 0xe77ff73b, 0xee77ffff, 0xfffff7bf, 0xffffffff, 0xfffff7bf, 0xffff7fff, 0xfff7ffff, 0xfefffffb, 0xefffff7b, 0xe677733b, 0x66663333, |
||||
|
0xbbddddee, 0xbbfddfee, 0xffffddee, 0xfb9dfffc, 0x7bdfffce, 0xb9fffdef, 0x9ffbdcff, 0xffb9cfff, 0xbbddffff, 0xffbdfffe, 0xffffffff, 0xfffdfffe, 0xfbdffffe, 0xbffffdef, 0xffffdeff, 0xfffddfff, 0xbb9dddce, 0x9999cccc, |
||||
|
0xee77773b, 0xeff777fb, 0xffff773b, 0xe77ff73b, 0xf7fe73bf, 0xffe77bff, 0xfef7bff7, 0xee77ff7b, 0xee77ffff, 0xeffff7bf, 0xffffffff, 0xeffff7bf, 0xfffff7ff, 0xffff7fff, 0xfef7ffff, 0xef7fff7b, 0xe677773b, 0x66663333, |
||||
|
0xbbddddee, 0xbbfddfee, 0xffffddee, 0xffbdcffd, 0xfb9cffde, 0x39dffdce, 0xbdff9cef, 0xdffbdeff, 0xbbddffff, 0xffbdffff, 0xffffffff, 0xfffdffff, 0xffdffffe, 0xfbffffee, 0xbffffdef, 0xffffdfff, 0xbb9dccee, 0x9999cccc, |
||||
|
0xee77773b, 0xeff777fb, 0xffff773b, 0xef7fff7b, 0xe7fff7bf, 0x7fef73ff, 0xfee73fff, 0xfe77fff3, 0xee77ffff, 0xef7fff7b, 0xffffffff, 0xef7fff7b, 0xfffff7bf, 0xffff7fff, 0xfff77fff, 0xfef7ffff, 0xee77773b, 0x66663333, |
||||
|
0xbbddddee, 0xbbfddfee, 0xffffddee, 0xdff9ceff, 0xffbdeffc, 0xfb9effde, 0xb9dffdce, 0x9dffdcef, 0xbbddffff, 0xffffdfff, 0xffffffff, 0xffffdfff, 0xffbdffff, 0xfbdffffe, 0xbffffdef, 0xbfffddff, 0xbb99dcee, 0x9999cccc, |
||||
|
0xee77773b, 0xeff777fb, 0xffff773b, 0xee77ff73, 0xef7ff73b, 0xf7fef7bf, 0x7fef7bff, 0xfee73ff7, 0xee77ffff, 0xee77fffb, 0xffffffff, 0xfefffffb, 0xefffff7b, 0xfffff7bf, 0xffff7fff, 0xfff7ffff, 0xee777733, 0x66663333, |
||||
|
0xbbddddee, 0xbbfddfee, 0xffffddee, 0x9ffbdcff, 0xffb9cfff, 0xff9dfffc, 0x7bdfffce, 0xb9fffdef, 0xbbddffff, 0xbfffdeff, 0xffffffff, 0xffffdeff, 0xfffddfff, 0xffbdfffe, 0xfbdffffe, 0xbffffdef, 0xb9dddcee, 0x9999cccc, |
||||
|
0xee77773b, 0xeff777fb, 0xffff773b, 0xfef7bff7, 0xee77ff7b, 0xe77ff73b, 0xf7fe73bf, 0xffe77bff, 0xee77ffff, 0xfef7ffff, 0xffffffff, 0xfef7ffff, 0xef7ffffb, 0xeffff7bf, 0xfffff7ff, 0xffff7fff, 0xee677733, 0x66663333, |
||||
|
0xbbddddee, 0xbbfddfee, 0xffffddee, 0xbdff9cef, 0x9ffbdeff, 0xffbdcffd, 0xfb9cffde, 0x39dffdce, 0xbbddffff, 0xbdfffdef, 0xffffffff, 0xbffffdef, 0xffffdeff, 0xfffdffff, 0xffdffffe, 0xfbdfffee, 0xb9dddcce, 0x9999cccc, |
||||
|
0xee77773b, 0xeff777fb, 0xffff773b, 0xfee73fff, 0xfe77fff3, 0xef7fff7b, 0xe7fff7bf, 0x7fef73ff, 0xee77ffff, 0xffe77fff, 0xffffffff, 0xffff7fff, 0xfef7ffff, 0xef7fff7b, 0xfffff7bf, 0xffff7fff, 0xee6773bb, 0x66663333, |
||||
|
0xbbddddee, 0xbbfddfee, 0xffffddee, 0xb9dffdce, 0x9dffdcef, 0xdff9deff, 0xffbdeffc, 0xfb9cffde, 0xbbddffff, 0xb9fffdef, 0xffffffff, 0xbffffdef, 0xbffffdff, 0xffffdfff, 0xfffdffff, 0xfbdffffe, 0xb9dddcce, 0x9999cccc, |
||||
|
0xee77773b, 0xeff777fb, 0xffff773b, 0x7fef7bff, 0xfee73ff7, 0xee77ff73, 0xef7ff73b, 0xf7fef7bf, 0xee77ffff, 0xffff7bff, 0xffffffff, 0xffff7fff, 0xfff7ffff, 0xfef7fffb, 0xefffff7b, 0xfffff7bf, 0xeee773bb, 0x66663333, |
||||
|
0xbbddddee, 0xbbfddfee, 0xffffddee, 0x7bdfffce, 0xb9fffdef, 0x9fffdcff, 0xffb9ceff, 0xff9dfffc, 0xbbddffff, 0xfbdfffde, 0xffffffff, 0xfbdffffe, 0xbffffdef, 0xbfffdeff, 0xfffddfff, 0xffbdfffe, 0xb99dddce, 0x9999cccc, |
||||
|
0xee77773b, 0xeff777fb, 0xffff773b, 0xf7fe73bf, 0x7fef7bff, 0xfef7bff7, 0xee77ff7b, 0xe77ff73b, 0xee77ffff, 0xfffff7bf, 0xffffffff, 0xfffff7ff, 0xffff7fff, 0xfef7ffff, 0xef7ffffb, 0xeffff7bf, 0xe667733b, 0x66663333, |
||||
|
0xbbddddee, 0xbbfddfee, 0xffffddee, 0xfb9dffdc, 0x39dffdce, 0xbdffbcef, 0x9ffbdeff, 0xffbdcffd, 0xbbddffff, 0xff9dfffe, 0xffffffff, 0xffddfffe, 0xfbdfffee, 0xbffffdef, 0xffffdeff, 0xfffdffff, 0xbb9dddce, 0x9999cccc, |
||||
|
0xee77773b, 0xeff777fb, 0xffff773b, 0xe7fff73f, 0x7ffe73bf, 0xffe73bff, 0xfef7fff3, 0xef7fff7b, 0xee77ffff, 0xfffff7bf, 0xffffffff, 0xfffff7bf, 0xffff7fff, 0xffff7fff, 0xfef7ffff, 0xef7fff7b, 0xe677773b, 0x66663333, |
||||
|
0xbbddddee, 0xbbfddfee, 0xffffddee, 0xffbdeffd, 0xfb9cffde, 0x39dffdce, 0xbdffdcef, 0xdff9deff, 0xbbddffff, 0xffbdffff, 0xffffffff, 0xfffdffff, 0xfbdffffe, 0xbbffffef, 0xbffffdff, 0xffffdfff, 0xbb9dccce, 0x9999cccc, |
||||
|
0xee77773b, 0xeff777fb, 0xffff773b, 0xef7ff73b, 0xf7fef7bf, 0x7fef73ff, 0xfee73ff7, 0xee77ff73, 0xee77ffff, 0xef7fff7b, 0xffffffff, 0xef7fff7b, 0xfffff7bf, 0xffff7fff, 0xfff7ffff, 0xfef7fffb, 0xe677773b, 0x66663333, |
||||
|
0xbbddddee, 0xbbfddfee, 0xffffddee, 0xfff9ceff, 0xff9deffc, 0xfbdfffce, 0xb9fffdef, 0x9fffdcef, 0xbbddffff, 0xfffddfff, 0xffffffff, 0xfffddfff, 0xffbdffff, 0xfbdffffe, 0xbffffdef, 0xbfffdeff, 0xbb99ccee, 0x9999cccc, |
||||
|
0xee77773b, 0xeff777fb, 0xffff773b, 0xee77ff7b, 0xe77ff73b, 0xf7fe73bf, 0x7fef7bff, 0xfef7bff7, 0xee77ffff, 0xee7fff7b, 0xffffffff, 0xef7ffffb, 0xefffffbf, 0xfffff7ff, 0xffff7fff, 0xfef7ffff, 0xee777733, 0x66663333, |
||||
|
0xbbddddee, 0xbbfddfee, 0xffffddee, 0x9ffbdeff, 0xffb9cffd, 0xfb9dffdc, 0x39dffdce, 0xbdfffcef, 0xbbddffff, 0xffffdeff, 0xffffffff, 0xffffdeff, 0xfffdffff, 0xffddfffe, 0xfbdffffe, 0xbffffdef, 0xb999dcee, 0x9999cccc, |
||||
|
0xee77773b, 0xeff777fb, 0xffff773b, 0xfef7fff3, 0xef7fff7b, 0xe7fff73f, 0xfffe73bf, 0xffe77bff, 0xee77ffff, 0xfef7ffff, 0xffffffff, 0xfef7ffff, 0xef7fff7b, 0xfffff7bf, 0xffff77ff, 0xffff7fff, 0xee777733, 0x66663333, |
||||
|
0xbbddddee, 0xbbfddfee, 0xffffddee, 0xbdff9cef, 0xdff9deff, 0xffbdeffd, 0xfb9cffde, 0x39dffdce, 0xbbddffff, 0xbfffddef, 0xffffffff, 0xbffffdff, 0xffffdfff, 0xfffdffff, 0xfbdffffe, 0xbbffffef, 0xb9dddcee, 0x9999cccc, |
||||
|
0xee77773b, 0xeff777fb, 0xffff773b, 0xfee73ff7, 0xee77ff73, 0xef7fff3b, 0xf7fef7bf, 0x7fef73ff, 0xee77ffff, 0xfff77fff, 0xffffffff, 0xfff7ffff, 0xfef7fffb, 0xef7fff7b, 0xfffff7bf, 0xffff7fff, 0xee677333, 0x66663333, |
||||
|
0xbbddddee, 0xbbfddfee, 0xffffddee, 0xb9fffdef, 0x9fffdcef, 0xfff9ceff, 0xff9deffc, 0xfbdfffce, 0xbbddffff, 0xbdfffdef, 0xffffffff, 0xbffffdef, 0xbfffdeff, 0xfffddfff, 0xffbdffff, 0xfbdffffe, 0xb9dddcce, 0x9999cccc, |
||||
|
0xee77773b, 0xeff777fb, 0xffff773b, 0x7fef7bff, 0xfef73ff7, 0xee77ff73, 0xe77ff73b, 0xf7fe73bf, 0xee77ffff, 0xffef7bff, 0xffffffff, 0xffff7fff, 0xfef7ffff, 0xef7ffffb, 0xefffffbf, 0xfffff7ff, 0xeee773bb, 0x66663333, |
||||
|
0xbbddddee, 0xbbfddfee, 0xffffddee, 0x39dffdce, 0xbdfffdef, 0x9ffbdeff, 0xffb9cfff, 0xfb9dffdc, 0xbbddffff, 0xfbdfffce, 0xffffffff, 0xfbdffffe, 0xbffffdef, 0xffffdeff, 0xfffdffff, 0xfffdfffe, 0xb99dddce, 0x9999cccc, |
||||
|
0xee77773b, 0xeff777fb, 0xffff773b, 0xfffe73bf, 0xffe77bff, 0xfef7bff3, 0xee7fff7b, 0xe7fff73b, 0xee77ffff, 0xffff77ff, 0xffffffff, 0xffff77ff, 0xffff7fff, 0xfef7ffff, 0xef7fff7b, 0xfffff7bf, 0xe667733b, 0x66663333, |
||||
|
0xbbddddee, 0xbbfddfee, 0xffffddee, 0xfb9cffde, 0x39dffdce, 0xbdff9cef, 0xdff9deff, 0xffbdeffd, 0xbbddffff, 0xfbdffffe, 0xffffffff, 0xfbdffffe, 0xfbffffef, 0xbffffdef, 0xffffdfff, 0xfffdffff, 0xbb9dddce, 0x9999cccc, |
||||
|
}; |
||||
@ -0,0 +1,32 @@ |
|||||
|
/** |
||||
|
* @file broadcast_tables.h |
||||
|
* @brief Premodulated waveform lookup tables for NTSC/PAL RF broadcast |
||||
|
* |
||||
|
* These tables contain 1408-bit patterns per color, chosen as an exact harmonic |
||||
|
* of both NTSC chroma (3.579545 MHz) and Channel 3 luma (61.25 MHz). |
||||
|
* |
||||
|
* Original Copyright 2015 <>< Charles Lohr |
||||
|
* ESP32 Port 2024 |
||||
|
*/ |
||||
|
|
||||
|
#ifndef BROADCAST_TABLES_H |
||||
|
#define BROADCAST_TABLES_H |
||||
|
|
||||
|
#include <stdint.h> |
||||
|
|
||||
|
#define PREMOD_ENTRIES 44 |
||||
|
#define PREMOD_ENTRIES_WITH_SPILL 51 |
||||
|
#define PREMOD_SIZE 18 |
||||
|
|
||||
|
// Color level indices for the premodulated table |
||||
|
#define SYNC_LEVEL 17 |
||||
|
#define COLORBURST_LEVEL 16 |
||||
|
#define BLACK_LEVEL 0 |
||||
|
#define GRAY_LEVEL 1 |
||||
|
#define WHITE_LEVEL 10 |
||||
|
|
||||
|
// Premodulated table: 918 entries (51 * 18) |
||||
|
// Each entry is a 32-bit word containing the precomputed RF waveform |
||||
|
extern const uint32_t premodulated_table[918]; |
||||
|
|
||||
|
#endif // BROADCAST_TABLES_H |
||||
@ -0,0 +1,26 @@ |
|||||
|
$ErrorActionPreference = "Continue" |
||||
|
|
||||
|
# Clear MSYS environment variables |
||||
|
Remove-Item Env:MSYSTEM -ErrorAction SilentlyContinue |
||||
|
$env:MSYSTEM = "" |
||||
|
|
||||
|
# Set ESP-IDF environment |
||||
|
$env:IDF_PATH = "C:\Espressif\frameworks\esp-idf-v5.5.2" |
||||
|
$env:IDF_TOOLS_PATH = "C:\Espressif" |
||||
|
$env:IDF_PYTHON_ENV_PATH = "C:\Espressif\python_env\idf5.5_py3.11_env" |
||||
|
|
||||
|
$toolPaths = @( |
||||
|
"C:\Espressif\python_env\idf5.5_py3.11_env\Scripts", |
||||
|
"C:\Espressif\tools\cmake\3.30.2\bin", |
||||
|
"C:\Espressif\tools\ninja\1.12.1", |
||||
|
"C:\Espressif\tools\xtensa-esp-elf\esp-14.2.0_20251107\xtensa-esp-elf\bin" |
||||
|
) |
||||
|
$env:PATH = ($toolPaths -join ";") + ";" + $env:PATH |
||||
|
|
||||
|
Set-Location "C:\git\channel3\esp32_channel3" |
||||
|
|
||||
|
$python = "C:\Espressif\python_env\idf5.5_py3.11_env\Scripts\python.exe" |
||||
|
$idfpy = "$env:IDF_PATH\tools\idf.py" |
||||
|
|
||||
|
Write-Host "Flashing to COM5..." |
||||
|
& $python $idfpy -p COM5 flash monitor |
||||
@ -0,0 +1,10 @@ |
|||||
|
$env:IDF_PYTHON_ENV_PATH = "C:\Espressif\python_env\idf5.5_py3.11_env" |
||||
|
$env:IDF_PATH = "C:\Espressif\frameworks\esp-idf-v5.5.2" |
||||
|
$env:IDF_TOOLS_PATH = "C:\Espressif" |
||||
|
$env:MSYSTEM = $null |
||||
|
$env:SHELL = $null |
||||
|
$env:SHLVL = $null |
||||
|
$env:TERM = $null |
||||
|
Set-Location "C:\git\channel3\esp32_channel3" |
||||
|
. "C:\Espressif\Initialize-Idf.ps1" |
||||
|
idf.py -p COM5 flash |
||||
@ -0,0 +1,602 @@ |
|||||
|
/** |
||||
|
* @file 3d.c |
||||
|
* @brief Fixed-point 3D graphics engine implementation |
||||
|
* |
||||
|
* Provides matrix-based 3D transformations and rendering primitives. |
||||
|
* Uses 256 = 1.0 fixed-point math (8-bit fractional part). |
||||
|
* |
||||
|
* Original Copyright 2015 <>< Charles Lohr |
||||
|
* ESP32 Port 2024 |
||||
|
*/ |
||||
|
|
||||
|
#include "3d.h" |
||||
|
#include <string.h> |
||||
|
#include <stdio.h> |
||||
|
|
||||
|
// Matrix element indices |
||||
|
#define m00 0 |
||||
|
#define m01 1 |
||||
|
#define m02 2 |
||||
|
#define m03 3 |
||||
|
#define m10 4 |
||||
|
#define m11 5 |
||||
|
#define m12 6 |
||||
|
#define m13 7 |
||||
|
#define m20 8 |
||||
|
#define m21 9 |
||||
|
#define m22 10 |
||||
|
#define m23 11 |
||||
|
#define m30 12 |
||||
|
#define m31 13 |
||||
|
#define m32 14 |
||||
|
#define m33 15 |
||||
|
|
||||
|
// Global state |
||||
|
uint8_t *frontframe; |
||||
|
int16_t ModelviewMatrix[16]; |
||||
|
int16_t ProjectionMatrix[16]; |
||||
|
uint8_t CNFGBGColor; |
||||
|
uint8_t CNFGLastColor; |
||||
|
uint16_t LTW = FBW; |
||||
|
uint8_t CNFGDialogColor; |
||||
|
int CNFGPenX, CNFGPenY; |
||||
|
|
||||
|
// Function pointer for pixel plotting |
||||
|
void (*CNFGTackPixel)(int x, int y); |
||||
|
|
||||
|
// Sine lookup table (0-127 = 0 to pi) |
||||
|
static const uint8_t sintable[128] = { |
||||
|
0, 6, 12, 18, 25, 31, 37, 43, 49, 55, 62, 68, 74, 80, 86, 91, |
||||
|
97, 103, 109, 114, 120, 125, 131, 136, 141, 147, 152, 157, 162, 166, 171, 176, |
||||
|
180, 185, 189, 193, 197, 201, 205, 208, 212, 215, 219, 222, 225, 228, 230, 233, |
||||
|
236, 238, 240, 242, 244, 246, 247, 249, 250, 251, 252, 253, 254, 254, 255, 255, |
||||
|
255, 255, 255, 254, 254, 253, 252, 251, 250, 249, 247, 246, 244, 242, 240, 238, |
||||
|
236, 233, 230, 228, 225, 222, 219, 215, 212, 208, 205, 201, 197, 193, 189, 185, |
||||
|
180, 176, 171, 166, 162, 157, 152, 147, 141, 136, 131, 125, 120, 114, 109, 103, |
||||
|
97, 91, 86, 80, 74, 68, 62, 55, 49, 43, 37, 31, 25, 18, 12, 6, |
||||
|
}; |
||||
|
|
||||
|
int16_t tdSIN(uint8_t iv) |
||||
|
{ |
||||
|
if (iv > 127) { |
||||
|
return -sintable[iv - 128]; |
||||
|
} else { |
||||
|
return sintable[iv]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
int16_t tdCOS(uint8_t iv) |
||||
|
{ |
||||
|
return tdSIN(iv + 64); |
||||
|
} |
||||
|
|
||||
|
void MakeXRotationMatrix(uint8_t angle, int16_t *f) |
||||
|
{ |
||||
|
f[0] = 256; f[1] = 0; f[2] = 0; f[3] = 0; |
||||
|
f[4] = 0; f[5] = tdCOS(angle); f[6] = -tdSIN(angle); f[7] = 0; |
||||
|
f[8] = 0; f[9] = tdSIN(angle); f[10] = tdCOS(angle); f[11] = 0; |
||||
|
f[12] = 0; f[13] = 0; f[14] = 0; f[15] = 256; |
||||
|
} |
||||
|
|
||||
|
void MakeYRotationMatrix(uint8_t angle, int16_t *f) |
||||
|
{ |
||||
|
f[0] = tdCOS(angle); f[1] = 0; f[2] = tdSIN(angle); f[3] = 0; |
||||
|
f[4] = 0; f[5] = 256; f[6] = 0; f[7] = 0; |
||||
|
f[8] = -tdSIN(angle); f[9] = 0; f[10] = tdCOS(angle); f[11] = 0; |
||||
|
f[12] = 0; f[13] = 0; f[14] = 0; f[15] = 256; |
||||
|
} |
||||
|
|
||||
|
void tdIdentity(int16_t *matrix) |
||||
|
{ |
||||
|
matrix[0] = 256; matrix[1] = 0; matrix[2] = 0; matrix[3] = 0; |
||||
|
matrix[4] = 0; matrix[5] = 256; matrix[6] = 0; matrix[7] = 0; |
||||
|
matrix[8] = 0; matrix[9] = 0; matrix[10] = 256; matrix[11] = 0; |
||||
|
matrix[12] = 0; matrix[13] = 0; matrix[14] = 0; matrix[15] = 256; |
||||
|
} |
||||
|
|
||||
|
void Perspective(int fovx, int aspect, int zNear, int zFar, int16_t *out) |
||||
|
{ |
||||
|
int16_t f = fovx; |
||||
|
out[0] = f * 256 / aspect; out[1] = 0; out[2] = 0; out[3] = 0; |
||||
|
out[4] = 0; out[5] = f; out[6] = 0; out[7] = 0; |
||||
|
out[8] = 0; out[9] = 0; |
||||
|
out[10] = 256 * (zFar + zNear) / (zNear - zFar); |
||||
|
out[11] = 2 * zFar * zNear / (zNear - zFar); |
||||
|
out[12] = 0; out[13] = 0; out[14] = -256; out[15] = 0; |
||||
|
} |
||||
|
|
||||
|
void MakeTranslate(int x, int y, int z, int16_t *out) |
||||
|
{ |
||||
|
tdIdentity(out); |
||||
|
out[m03] += x; |
||||
|
out[m13] += y; |
||||
|
out[m23] += z; |
||||
|
} |
||||
|
|
||||
|
void tdTranslate(int16_t *f, int16_t x, int16_t y, int16_t z) |
||||
|
{ |
||||
|
int16_t ftmp[16]; |
||||
|
tdIdentity(ftmp); |
||||
|
ftmp[m03] += x; |
||||
|
ftmp[m13] += y; |
||||
|
ftmp[m23] += z; |
||||
|
tdMultiply(f, ftmp, f); |
||||
|
} |
||||
|
|
||||
|
void tdScale(int16_t *f, int16_t x, int16_t y, int16_t z) |
||||
|
{ |
||||
|
f[m00] = (f[m00] * x) >> 8; |
||||
|
f[m01] = (f[m01] * x) >> 8; |
||||
|
f[m02] = (f[m02] * x) >> 8; |
||||
|
f[m03] = (f[m03] * x) >> 8; |
||||
|
|
||||
|
f[m10] = (f[m10] * y) >> 8; |
||||
|
f[m11] = (f[m11] * y) >> 8; |
||||
|
f[m12] = (f[m12] * y) >> 8; |
||||
|
f[m13] = (f[m13] * y) >> 8; |
||||
|
|
||||
|
f[m20] = (f[m20] * z) >> 8; |
||||
|
f[m21] = (f[m21] * z) >> 8; |
||||
|
f[m22] = (f[m22] * z) >> 8; |
||||
|
f[m23] = (f[m23] * z) >> 8; |
||||
|
} |
||||
|
|
||||
|
void tdRotateEA(int16_t *f, int16_t x, int16_t y, int16_t z) |
||||
|
{ |
||||
|
int16_t ftmp[16]; |
||||
|
|
||||
|
int16_t cx = tdCOS(x); |
||||
|
int16_t sx = tdSIN(x); |
||||
|
int16_t cy = tdCOS(y); |
||||
|
int16_t sy = tdSIN(y); |
||||
|
int16_t cz = tdCOS(z); |
||||
|
int16_t sz = tdSIN(z); |
||||
|
|
||||
|
// Row major, manually transposed |
||||
|
ftmp[m00] = (cy * cz) >> 8; |
||||
|
ftmp[m10] = ((((sx * sy) >> 8) * cz) - (cx * sz)) >> 8; |
||||
|
ftmp[m20] = ((((cx * sy) >> 8) * cz) + (sx * sz)) >> 8; |
||||
|
ftmp[m30] = 0; |
||||
|
|
||||
|
ftmp[m01] = (cy * sz) >> 8; |
||||
|
ftmp[m11] = ((((sx * sy) >> 8) * sz) + (cx * cz)) >> 8; |
||||
|
ftmp[m21] = ((((cx * sy) >> 8) * sz) - (sx * cz)) >> 8; |
||||
|
ftmp[m31] = 0; |
||||
|
|
||||
|
ftmp[m02] = -sy; |
||||
|
ftmp[m12] = (sx * cy) >> 8; |
||||
|
ftmp[m22] = (cx * cy) >> 8; |
||||
|
ftmp[m32] = 0; |
||||
|
|
||||
|
ftmp[m03] = 0; |
||||
|
ftmp[m13] = 0; |
||||
|
ftmp[m23] = 0; |
||||
|
ftmp[m33] = 1; |
||||
|
|
||||
|
tdMultiply(f, ftmp, f); |
||||
|
} |
||||
|
|
||||
|
void tdMultiply(int16_t *fin1, int16_t *fin2, int16_t *fout) |
||||
|
{ |
||||
|
int16_t fotmp[16]; |
||||
|
|
||||
|
fotmp[m00] = ((int32_t)fin1[m00] * (int32_t)fin2[m00] + (int32_t)fin1[m01] * (int32_t)fin2[m10] + (int32_t)fin1[m02] * (int32_t)fin2[m20] + (int32_t)fin1[m03] * (int32_t)fin2[m30]) >> 8; |
||||
|
fotmp[m01] = ((int32_t)fin1[m00] * (int32_t)fin2[m01] + (int32_t)fin1[m01] * (int32_t)fin2[m11] + (int32_t)fin1[m02] * (int32_t)fin2[m21] + (int32_t)fin1[m03] * (int32_t)fin2[m31]) >> 8; |
||||
|
fotmp[m02] = ((int32_t)fin1[m00] * (int32_t)fin2[m02] + (int32_t)fin1[m01] * (int32_t)fin2[m12] + (int32_t)fin1[m02] * (int32_t)fin2[m22] + (int32_t)fin1[m03] * (int32_t)fin2[m32]) >> 8; |
||||
|
fotmp[m03] = ((int32_t)fin1[m00] * (int32_t)fin2[m03] + (int32_t)fin1[m01] * (int32_t)fin2[m13] + (int32_t)fin1[m02] * (int32_t)fin2[m23] + (int32_t)fin1[m03] * (int32_t)fin2[m33]) >> 8; |
||||
|
|
||||
|
fotmp[m10] = ((int32_t)fin1[m10] * (int32_t)fin2[m00] + (int32_t)fin1[m11] * (int32_t)fin2[m10] + (int32_t)fin1[m12] * (int32_t)fin2[m20] + (int32_t)fin1[m13] * (int32_t)fin2[m30]) >> 8; |
||||
|
fotmp[m11] = ((int32_t)fin1[m10] * (int32_t)fin2[m01] + (int32_t)fin1[m11] * (int32_t)fin2[m11] + (int32_t)fin1[m12] * (int32_t)fin2[m21] + (int32_t)fin1[m13] * (int32_t)fin2[m31]) >> 8; |
||||
|
fotmp[m12] = ((int32_t)fin1[m10] * (int32_t)fin2[m02] + (int32_t)fin1[m11] * (int32_t)fin2[m12] + (int32_t)fin1[m12] * (int32_t)fin2[m22] + (int32_t)fin1[m13] * (int32_t)fin2[m32]) >> 8; |
||||
|
fotmp[m13] = ((int32_t)fin1[m10] * (int32_t)fin2[m03] + (int32_t)fin1[m11] * (int32_t)fin2[m13] + (int32_t)fin1[m12] * (int32_t)fin2[m23] + (int32_t)fin1[m13] * (int32_t)fin2[m33]) >> 8; |
||||
|
|
||||
|
fotmp[m20] = ((int32_t)fin1[m20] * (int32_t)fin2[m00] + (int32_t)fin1[m21] * (int32_t)fin2[m10] + (int32_t)fin1[m22] * (int32_t)fin2[m20] + (int32_t)fin1[m23] * (int32_t)fin2[m30]) >> 8; |
||||
|
fotmp[m21] = ((int32_t)fin1[m20] * (int32_t)fin2[m01] + (int32_t)fin1[m21] * (int32_t)fin2[m11] + (int32_t)fin1[m22] * (int32_t)fin2[m21] + (int32_t)fin1[m23] * (int32_t)fin2[m31]) >> 8; |
||||
|
fotmp[m22] = ((int32_t)fin1[m20] * (int32_t)fin2[m02] + (int32_t)fin1[m21] * (int32_t)fin2[m12] + (int32_t)fin1[m22] * (int32_t)fin2[m22] + (int32_t)fin1[m23] * (int32_t)fin2[m32]) >> 8; |
||||
|
fotmp[m23] = ((int32_t)fin1[m20] * (int32_t)fin2[m03] + (int32_t)fin1[m21] * (int32_t)fin2[m13] + (int32_t)fin1[m22] * (int32_t)fin2[m23] + (int32_t)fin1[m23] * (int32_t)fin2[m33]) >> 8; |
||||
|
|
||||
|
fotmp[m30] = ((int32_t)fin1[m30] * (int32_t)fin2[m00] + (int32_t)fin1[m31] * (int32_t)fin2[m10] + (int32_t)fin1[m32] * (int32_t)fin2[m20] + (int32_t)fin1[m33] * (int32_t)fin2[m30]) >> 8; |
||||
|
fotmp[m31] = ((int32_t)fin1[m30] * (int32_t)fin2[m01] + (int32_t)fin1[m31] * (int32_t)fin2[m11] + (int32_t)fin1[m32] * (int32_t)fin2[m21] + (int32_t)fin1[m33] * (int32_t)fin2[m31]) >> 8; |
||||
|
fotmp[m32] = ((int32_t)fin1[m30] * (int32_t)fin2[m02] + (int32_t)fin1[m31] * (int32_t)fin2[m12] + (int32_t)fin1[m32] * (int32_t)fin2[m22] + (int32_t)fin1[m33] * (int32_t)fin2[m32]) >> 8; |
||||
|
fotmp[m33] = ((int32_t)fin1[m30] * (int32_t)fin2[m03] + (int32_t)fin1[m31] * (int32_t)fin2[m13] + (int32_t)fin1[m32] * (int32_t)fin2[m23] + (int32_t)fin1[m33] * (int32_t)fin2[m33]) >> 8; |
||||
|
|
||||
|
memcpy(fout, fotmp, sizeof(fotmp)); |
||||
|
} |
||||
|
|
||||
|
void tdPTransform(int16_t *pin, int16_t *f, int16_t *pout) |
||||
|
{ |
||||
|
int16_t ptmp[2]; |
||||
|
ptmp[0] = ((pin[0] * f[m00] + pin[1] * f[m01] + pin[2] * f[m02]) >> 8) + f[m03]; |
||||
|
ptmp[1] = ((pin[0] * f[m10] + pin[1] * f[m11] + pin[2] * f[m12]) >> 8) + f[m13]; |
||||
|
pout[2] = ((pin[0] * f[m20] + pin[1] * f[m21] + pin[2] * f[m22]) >> 8) + f[m23]; |
||||
|
pout[0] = ptmp[0]; |
||||
|
pout[1] = ptmp[1]; |
||||
|
} |
||||
|
|
||||
|
void td4Transform(int16_t *pin, int16_t *f, int16_t *pout) |
||||
|
{ |
||||
|
int16_t ptmp[3]; |
||||
|
ptmp[0] = (pin[0] * f[m00] + pin[1] * f[m01] + pin[2] * f[m02] + pin[3] * f[m03]) >> 8; |
||||
|
ptmp[1] = (pin[0] * f[m10] + pin[1] * f[m11] + pin[2] * f[m12] + pin[3] * f[m13]) >> 8; |
||||
|
ptmp[2] = (pin[0] * f[m20] + pin[1] * f[m21] + pin[2] * f[m22] + pin[3] * f[m23]) >> 8; |
||||
|
pout[3] = (pin[0] * f[m30] + pin[1] * f[m31] + pin[2] * f[m32] + pin[3] * f[m33]) >> 8; |
||||
|
pout[0] = ptmp[0]; |
||||
|
pout[1] = ptmp[1]; |
||||
|
pout[2] = ptmp[2]; |
||||
|
} |
||||
|
|
||||
|
void LocalToScreenspace(int16_t *coords_3v, int16_t *o1, int16_t *o2) |
||||
|
{ |
||||
|
int16_t tmppt[4] = { coords_3v[0], coords_3v[1], coords_3v[2], 256 }; |
||||
|
td4Transform(tmppt, ModelviewMatrix, tmppt); |
||||
|
td4Transform(tmppt, ProjectionMatrix, tmppt); |
||||
|
if (tmppt[3] >= 0) { |
||||
|
*o1 = -1; |
||||
|
*o2 = -1; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (CNFGLastColor > 15) { |
||||
|
// Half-height mode |
||||
|
*o1 = (256 * tmppt[0] / tmppt[3]) / 8 + (FBW / 2); |
||||
|
*o2 = (256 * tmppt[1] / tmppt[3]) / 8 + (FBH / 2); |
||||
|
} else { |
||||
|
*o1 = ((256 * tmppt[0] / tmppt[3]) / 8 + (FBW / 2)) / 2; |
||||
|
*o2 = ((256 * tmppt[1] / tmppt[3]) / 8 + (FBH / 2)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
static void CNFGTackPixelW(int x, int y) |
||||
|
{ |
||||
|
frontframe[(x + y * FBW) >> 2] |= 2 << ((x & 3) << 1); |
||||
|
} |
||||
|
|
||||
|
static void CNFGTackPixelB(int x, int y) |
||||
|
{ |
||||
|
frontframe[(x + y * FBW) >> 2] &= ~(2 << ((x & 3) << 1)); |
||||
|
} |
||||
|
|
||||
|
static void CNFGTackPixelG(int x, int y) |
||||
|
{ |
||||
|
uint8_t *ffs = &frontframe[(x + y * FBW2) >> 1]; |
||||
|
if (x & 1) { |
||||
|
*ffs = (*ffs & 0x0f) | (CNFGLastColor << 4); |
||||
|
} else { |
||||
|
*ffs = (*ffs & 0xf0) | CNFGLastColor; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void CNFGColor(uint8_t col) |
||||
|
{ |
||||
|
CNFGLastColor = col; |
||||
|
if (col == 16) { |
||||
|
LTW = FBW; |
||||
|
CNFGTackPixel = CNFGTackPixelB; |
||||
|
} else if (col == 17) { |
||||
|
LTW = FBW; |
||||
|
CNFGTackPixel = CNFGTackPixelW; |
||||
|
} else { |
||||
|
LTW = FBW / 2; |
||||
|
CNFGTackPixel = CNFGTackPixelG; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
int LABS(int x) |
||||
|
{ |
||||
|
return (x < 0) ? -x : x; |
||||
|
} |
||||
|
|
||||
|
// Bresenham's line algorithm |
||||
|
void CNFGTackSegment(int x0, int y0, int x1, int y1) |
||||
|
{ |
||||
|
int deltax = x1 - x0; |
||||
|
int deltay = y1 - y0; |
||||
|
int error = 0; |
||||
|
int x; |
||||
|
int sy = LABS(deltay); |
||||
|
int ysg = (y0 > y1) ? -1 : 1; |
||||
|
int y = y0; |
||||
|
|
||||
|
if (x0 < 0 || x0 >= LTW) return; |
||||
|
if (y0 < 0 || y0 >= FBH) return; |
||||
|
if (x1 < 0 || x1 >= LTW) return; |
||||
|
if (y1 < 0 || y1 >= FBH) return; |
||||
|
|
||||
|
if (CNFGLastColor) { |
||||
|
if (deltax == 0) { |
||||
|
if (y1 == y0) { |
||||
|
CNFGTackPixel(x1, y); |
||||
|
return; |
||||
|
} |
||||
|
for (; y != y1 + ysg; y += ysg) |
||||
|
CNFGTackPixel(x1, y); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
int deltaerr = LABS((deltay * 256) / deltax); |
||||
|
int xsg = (x0 > x1) ? -1 : 1; |
||||
|
|
||||
|
for (x = x0; x != x1; x += xsg) { |
||||
|
CNFGTackPixel(x, y); |
||||
|
error = error + deltaerr; |
||||
|
while (error >= 128 && y >= 0 && y < FBH) { |
||||
|
y = y + ysg; |
||||
|
CNFGTackPixel(x, y); |
||||
|
error = error - 256; |
||||
|
} |
||||
|
} |
||||
|
CNFGTackPixel(x1, y1); |
||||
|
} else { |
||||
|
if (deltax == 0) { |
||||
|
if (y1 == y0) { |
||||
|
CNFGTackPixel(x1, y); |
||||
|
return; |
||||
|
} |
||||
|
for (; y != y1 + ysg; y += ysg) |
||||
|
CNFGTackPixel(x1, y); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
int deltaerr = LABS((deltay * 256) / deltax); |
||||
|
int xsg = (x0 > x1) ? -1 : 1; |
||||
|
|
||||
|
for (x = x0; x != x1; x += xsg) { |
||||
|
CNFGTackPixel(x, y); |
||||
|
error = error + deltaerr; |
||||
|
while (error >= 128 && y >= 0 && y < FBH) { |
||||
|
y = y + ysg; |
||||
|
CNFGTackPixel(x, y); |
||||
|
error = error - 256; |
||||
|
} |
||||
|
} |
||||
|
CNFGTackPixel(x1, y1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Geodesic sphere vertices |
||||
|
static const int16_t verts[] = { |
||||
|
0, -256, 0, |
||||
|
185, -114, 134, -70, -114, 217, -228, -114, 0, -70, -114, -217, |
||||
|
185, -114, -134, 70, 114, 217, -185, 114, 134, -185, 114, -134, |
||||
|
70, 114, -217, 228, 114, 0, 0, 256, 0, 108, -217, 79, |
||||
|
-41, -217, 127, 67, -134, 207, 108, -217, -79, 217, -134, 0, |
||||
|
-134, -217, 0, -176, -134, 127, -41, -217, -127, -176, -134, -127, |
||||
|
67, -134, -207, 243, 0, -79, 243, 0, 79, 150, 0, 207, |
||||
|
0, 0, 256, -150, 0, 207, -243, 0, 79, -243, 0, -79, |
||||
|
-150, 0, -207, 0, 0, -256, 150, 0, -207, 176, 134, 127, |
||||
|
-67, 134, 207, -217, 134, 0, -67, 134, -207, 176, 134, -127, |
||||
|
134, 217, 0, 41, 217, 127, -108, 217, 79, -108, 217, -79, |
||||
|
41, 217, -127 |
||||
|
}; |
||||
|
|
||||
|
// Geodesic sphere edge indices |
||||
|
static const uint16_t indices[] = { |
||||
|
42, 36, 36, 3, 3, 42, 42, 39, 39, 36, 6, 39, 42, 6, 39, 0, |
||||
|
0, 36, 48, 3, 36, 48, 36, 45, 45, 48, 15, 48, 45, 15, 0, 45, |
||||
|
54, 39, 6, 54, 54, 51, 51, 39, 9, 51, 54, 9, 51, 0, 60, 51, |
||||
|
9, 60, 60, 57, 57, 51, 12, 57, 60, 12, 57, 0, 63, 57, 12, 63, |
||||
|
63, 45, 45, 57, 63, 15, 69, 3, 48, 69, 48, 66, 66, 69, 30, 69, |
||||
|
66, 30, 15, 66, 75, 6, 42, 75, 42, 72, 72, 75, 18, 75, 72, 18, |
||||
|
3, 72, 81, 9, 54, 81, 54, 78, 78, 81, 21, 81, 78, 21, 6, 78, |
||||
|
87, 12, 60, 87, 60, 84, 84, 87, 24, 87, 84, 24, 9, 84, 93, 15, |
||||
|
63, 93, 63, 90, 90, 93, 27, 93, 90, 27, 12, 90, 96, 69, 30, 96, |
||||
|
96, 72, 72, 69, 96, 18, 99, 75, 18, 99, 99, 78, 78, 75, 99, 21, |
||||
|
102, 81, 21, 102, 102, 84, 84, 81, 102, 24, 105, 87, 24, 105, 105, 90, |
||||
|
90, 87, 105, 27, 108, 93, 27, 108, 108, 66, 66, 93, 108, 30, 114, 18, |
||||
|
96, 114, 96, 111, 111, 114, 33, 114, 111, 33, 30, 111, 117, 21, 99, 117, |
||||
|
99, 114, 114, 117, 33, 117, 120, 24, 102, 120, 102, 117, 117, 120, 33, 120, |
||||
|
123, 27, 105, 123, 105, 120, 120, 123, 33, 123, 108, 111, 108, 123, 123, 111, |
||||
|
}; |
||||
|
|
||||
|
void Draw3DSegment(int16_t *c1, int16_t *c2) |
||||
|
{ |
||||
|
int16_t sx0, sy0, sx1, sy1; |
||||
|
LocalToScreenspace(c1, &sx0, &sy0); |
||||
|
LocalToScreenspace(c2, &sx1, &sy1); |
||||
|
CNFGTackSegment(sx0, sy0, sx1, sy1); |
||||
|
} |
||||
|
|
||||
|
void DrawGeoSphere(void) |
||||
|
{ |
||||
|
int i; |
||||
|
int nrv = sizeof(indices) / sizeof(uint16_t); |
||||
|
for (i = 0; i < nrv; i += 2) { |
||||
|
int16_t *c1 = (int16_t*)&verts[indices[i]]; |
||||
|
int16_t *c2 = (int16_t*)&verts[indices[i + 1]]; |
||||
|
Draw3DSegment(c1, c2); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Font character map and data |
||||
|
static const unsigned short FontCharMap[128] = { |
||||
|
65535, 0, 10, 20, 32, 44, 56, 68, 70, 65535, 65535, 80, 92, 65535, 104, 114, |
||||
|
126, 132, 138, 148, 156, 166, 180, 188, 200, 206, 212, 218, 224, 228, 238, 244, |
||||
|
65535, 250, 254, 258, 266, 278, 288, 302, 304, 310, 316, 324, 328, 226, 252, 330, |
||||
|
332, 342, 348, 358, 366, 372, 382, 392, 400, 410, 420, 424, 428, 262, 432, 436, |
||||
|
446, 460, 470, 486, 496, 508, 516, 522, 536, 542, 548, 556, 568, 572, 580, 586, |
||||
|
598, 608, 622, 634, 644, 648, 654, 662, 670, 682, 692, 700, 706, 708, 492, 198, |
||||
|
714, 716, 726, 734, 742, 750, 760, 768, 782, 790, 794, 802, 204, 810, 820, 384, |
||||
|
828, 836, 844, 850, 860, 864, 872, 880, 890, 894, 902, 908, 916, 920, 928, 934, |
||||
|
}; |
||||
|
|
||||
|
static const unsigned char FontCharData[949] = { |
||||
|
0x00, 0x01, 0x20, 0x21, 0x03, 0x23, 0x23, 0x14, 0x14, 0x83, 0x00, 0x01, 0x20, 0x21, 0x04, 0x24, |
||||
|
0x24, 0x13, 0x13, 0x84, 0x01, 0x21, 0x21, 0x23, 0x23, 0x14, 0x14, 0x03, 0x03, 0x01, 0x11, 0x92, |
||||
|
0x11, 0x22, 0x22, 0x23, 0x23, 0x14, 0x14, 0x03, 0x03, 0x02, 0x02, 0x91, 0x01, 0x21, 0x21, 0x23, |
||||
|
0x23, 0x01, 0x03, 0x21, 0x03, 0x01, 0x12, 0x94, 0x03, 0x23, 0x13, 0x14, 0x23, 0x22, 0x22, 0x11, |
||||
|
0x11, 0x02, 0x02, 0x83, 0x12, 0x92, 0x12, 0x12, 0x01, 0x21, 0x21, 0x23, 0x23, 0x03, 0x03, 0x81, |
||||
|
0x03, 0x21, 0x21, 0x22, 0x21, 0x11, 0x03, 0x14, 0x14, 0x23, 0x23, 0x92, 0x01, 0x10, 0x10, 0x21, |
||||
|
0x21, 0x12, 0x12, 0x01, 0x12, 0x14, 0x03, 0xa3, 0x02, 0x03, 0x03, 0x13, 0x02, 0x12, 0x13, 0x10, |
||||
|
0x10, 0xa1, 0x01, 0x23, 0x03, 0x21, 0x02, 0x11, 0x11, 0x22, 0x22, 0x13, 0x13, 0x82, 0x00, 0x22, |
||||
|
0x22, 0x04, 0x04, 0x80, 0x20, 0x02, 0x02, 0x24, 0x24, 0xa0, 0x01, 0x10, 0x10, 0x21, 0x10, 0x14, |
||||
|
0x14, 0x03, 0x14, 0xa3, 0x00, 0x03, 0x04, 0x04, 0x20, 0x23, 0x24, 0xa4, 0x00, 0x20, 0x00, 0x02, |
||||
|
0x02, 0x22, 0x10, 0x14, 0x20, 0xa4, 0x01, 0x21, 0x21, 0x23, 0x23, 0x03, 0x03, 0x01, 0x20, 0x10, |
||||
|
0x10, 0x14, 0x14, 0x84, 0x03, 0x23, 0x23, 0x24, 0x24, 0x04, 0x04, 0x83, 0x01, 0x10, 0x10, 0x21, |
||||
|
0x10, 0x14, 0x14, 0x03, 0x14, 0x23, 0x04, 0xa4, 0x01, 0x10, 0x21, 0x10, 0x10, 0x94, 0x03, 0x14, |
||||
|
0x23, 0x14, 0x10, 0x94, 0x02, 0x22, 0x22, 0x11, 0x22, 0x93, 0x02, 0x22, 0x02, 0x11, 0x02, 0x93, |
||||
|
0x01, 0x02, 0x02, 0xa2, 0x02, 0x22, 0x22, 0x11, 0x11, 0x02, 0x02, 0x13, 0x13, 0xa2, 0x11, 0x22, |
||||
|
0x22, 0x02, 0x02, 0x91, 0x02, 0x13, 0x13, 0x22, 0x22, 0x82, 0x10, 0x13, 0x14, 0x94, 0x10, 0x01, |
||||
|
0x20, 0x91, 0x10, 0x14, 0x20, 0x24, 0x01, 0x21, 0x03, 0xa3, 0x21, 0x10, 0x10, 0x01, 0x01, 0x23, |
||||
|
0x23, 0x14, 0x14, 0x03, 0x10, 0x94, 0x00, 0x01, 0x23, 0x24, 0x04, 0x03, 0x03, 0x21, 0x21, 0xa0, |
||||
|
0x21, 0x10, 0x10, 0x01, 0x01, 0x12, 0x12, 0x03, 0x03, 0x14, 0x14, 0x23, 0x02, 0xa4, 0x10, 0x91, |
||||
|
0x10, 0x01, 0x01, 0x03, 0x03, 0x94, 0x10, 0x21, 0x21, 0x23, 0x23, 0x94, 0x01, 0x23, 0x11, 0x13, |
||||
|
0x21, 0x03, 0x02, 0xa2, 0x02, 0x22, 0x11, 0x93, 0x31, 0xc0, 0x03, 0xa1, 0x00, 0x20, 0x20, 0x24, |
||||
|
0x24, 0x04, 0x04, 0x00, 0x12, 0x92, 0x01, 0x10, 0x10, 0x14, 0x04, 0xa4, 0x01, 0x10, 0x10, 0x21, |
||||
|
0x21, 0x22, 0x22, 0x04, 0x04, 0xa4, 0x00, 0x20, 0x20, 0x24, 0x24, 0x04, 0x12, 0xa2, 0x00, 0x02, |
||||
|
0x02, 0x22, 0x20, 0xa4, 0x20, 0x00, 0x00, 0x02, 0x02, 0x22, 0x22, 0x24, 0x24, 0x84, 0x20, 0x02, |
||||
|
0x02, 0x22, 0x22, 0x24, 0x24, 0x04, 0x04, 0x82, 0x00, 0x20, 0x20, 0x21, 0x21, 0x12, 0x12, 0x94, |
||||
|
0x00, 0x04, 0x00, 0x20, 0x20, 0x24, 0x04, 0x24, 0x02, 0xa2, 0x00, 0x02, 0x02, 0x22, 0x22, 0x20, |
||||
|
0x20, 0x00, 0x22, 0x84, 0x11, 0x11, 0x13, 0x93, 0x11, 0x11, 0x13, 0x84, 0x20, 0x02, 0x02, 0xa4, |
||||
|
0x00, 0x22, 0x22, 0x84, 0x01, 0x10, 0x10, 0x21, 0x21, 0x12, 0x12, 0x13, 0x14, 0x94, 0x21, 0x01, |
||||
|
0x01, 0x04, 0x04, 0x24, 0x24, 0x22, 0x22, 0x12, 0x12, 0x13, 0x13, 0xa3, 0x04, 0x01, 0x01, 0x10, |
||||
|
0x10, 0x21, 0x21, 0x24, 0x02, 0xa2, 0x00, 0x04, 0x04, 0x14, 0x14, 0x23, 0x23, 0x12, 0x12, 0x02, |
||||
|
0x12, 0x21, 0x21, 0x10, 0x10, 0x80, 0x23, 0x14, 0x14, 0x03, 0x03, 0x01, 0x01, 0x10, 0x10, 0xa1, |
||||
|
0x00, 0x10, 0x10, 0x21, 0x21, 0x23, 0x23, 0x14, 0x14, 0x04, 0x04, 0x80, 0x00, 0x04, 0x04, 0x24, |
||||
|
0x00, 0x20, 0x02, 0x92, 0x00, 0x04, 0x00, 0x20, 0x02, 0x92, 0x21, 0x10, 0x10, 0x01, 0x01, 0x03, |
||||
|
0x03, 0x14, 0x14, 0x23, 0x23, 0x22, 0x22, 0x92, 0x00, 0x04, 0x20, 0x24, 0x02, 0xa2, 0x00, 0x20, |
||||
|
0x10, 0x14, 0x04, 0xa4, 0x00, 0x20, 0x20, 0x23, 0x23, 0x14, 0x14, 0x83, 0x00, 0x04, 0x02, 0x12, |
||||
|
0x12, 0x21, 0x21, 0x20, 0x12, 0x23, 0x23, 0xa4, 0x00, 0x04, 0x04, 0xa4, 0x04, 0x00, 0x00, 0x11, |
||||
|
0x11, 0x20, 0x20, 0xa4, 0x04, 0x00, 0x00, 0x22, 0x20, 0xa4, 0x01, 0x10, 0x10, 0x21, 0x21, 0x23, |
||||
|
0x23, 0x14, 0x14, 0x03, 0x03, 0x81, 0x00, 0x04, 0x00, 0x10, 0x10, 0x21, 0x21, 0x12, 0x12, 0x82, |
||||
|
0x01, 0x10, 0x10, 0x21, 0x21, 0x23, 0x23, 0x14, 0x14, 0x03, 0x03, 0x01, 0x04, 0x93, 0x00, 0x04, |
||||
|
0x00, 0x10, 0x10, 0x21, 0x21, 0x12, 0x12, 0x02, 0x02, 0xa4, 0x21, 0x10, 0x10, 0x01, 0x01, 0x23, |
||||
|
0x23, 0x14, 0x14, 0x83, 0x00, 0x20, 0x10, 0x94, 0x00, 0x04, 0x04, 0x24, 0x24, 0xa0, 0x00, 0x03, |
||||
|
0x03, 0x14, 0x14, 0x23, 0x23, 0xa0, 0x00, 0x04, 0x04, 0x24, 0x14, 0x13, 0x24, 0xa0, 0x00, 0x01, |
||||
|
0x01, 0x23, 0x23, 0x24, 0x04, 0x03, 0x03, 0x21, 0x21, 0xa0, 0x00, 0x01, 0x01, 0x12, 0x12, 0x14, |
||||
|
0x12, 0x21, 0x21, 0xa0, 0x00, 0x20, 0x20, 0x02, 0x02, 0x04, 0x04, 0xa4, 0x10, 0x00, 0x00, 0x04, |
||||
|
0x04, 0x94, 0x01, 0xa3, 0x10, 0x20, 0x20, 0x24, 0x24, 0x94, 0x00, 0x91, 0x02, 0x04, 0x04, 0x24, |
||||
|
0x24, 0x22, 0x23, 0x12, 0x12, 0x82, 0x00, 0x04, 0x04, 0x24, 0x24, 0x22, 0x22, 0x82, 0x24, 0x04, |
||||
|
0x04, 0x03, 0x03, 0x12, 0x12, 0xa2, 0x20, 0x24, 0x24, 0x04, 0x04, 0x02, 0x02, 0xa2, 0x24, 0x04, |
||||
|
0x04, 0x02, 0x02, 0x22, 0x22, 0x23, 0x23, 0x93, 0x04, 0x01, 0x02, 0x12, 0x01, 0x10, 0x10, 0xa1, |
||||
|
0x23, 0x12, 0x12, 0x03, 0x03, 0x14, 0x14, 0x23, 0x23, 0x24, 0x24, 0x15, 0x15, 0x84, 0x00, 0x04, |
||||
|
0x03, 0x12, 0x12, 0x23, 0x23, 0xa4, 0x11, 0x11, 0x12, 0x94, 0x22, 0x22, 0x23, 0x24, 0x24, 0x15, |
||||
|
0x15, 0x84, 0x00, 0x04, 0x03, 0x13, 0x13, 0x22, 0x13, 0xa4, 0x02, 0x04, 0x02, 0x13, 0x12, 0x14, |
||||
|
0x12, 0x23, 0x23, 0xa4, 0x02, 0x04, 0x03, 0x12, 0x12, 0x23, 0x23, 0xa4, 0x02, 0x05, 0x04, 0x24, |
||||
|
0x24, 0x22, 0x22, 0x82, 0x02, 0x04, 0x04, 0x24, 0x25, 0x22, 0x22, 0x82, 0x02, 0x04, 0x03, 0x12, |
||||
|
0x12, 0xa2, 0x22, 0x02, 0x02, 0x03, 0x03, 0x23, 0x23, 0x24, 0x24, 0x84, 0x11, 0x14, 0x02, 0xa2, |
||||
|
0x02, 0x04, 0x04, 0x14, 0x14, 0x23, 0x24, 0xa2, 0x02, 0x03, 0x03, 0x14, 0x14, 0x23, 0x23, 0xa2, |
||||
|
0x02, 0x03, 0x03, 0x14, 0x14, 0x12, 0x13, 0x24, 0x24, 0xa2, 0x02, 0x24, 0x04, 0xa2, 0x02, 0x03, |
||||
|
0x03, 0x14, 0x22, 0x23, 0x23, 0x85, 0x02, 0x22, 0x22, 0x04, 0x04, 0xa4, 0x20, 0x10, 0x10, 0x14, |
||||
|
0x14, 0x24, 0x12, 0x82, 0x10, 0x11, 0x13, 0x94, 0x00, 0x10, 0x10, 0x14, 0x14, 0x04, 0x12, 0xa2, |
||||
|
0x01, 0x10, 0x10, 0x11, 0x11, 0xa0, 0x03, 0x04, 0x04, 0x24, 0x24, 0x23, 0x23, 0x12, 0x12, 0x83, |
||||
|
0x10, 0x10, 0x11, 0x94, 0x21 |
||||
|
}; |
||||
|
|
||||
|
void CNFGDrawText(const char *text, int scale) |
||||
|
{ |
||||
|
const unsigned char *lmap; |
||||
|
int16_t iox = (int16_t)CNFGPenX; |
||||
|
int16_t ioy = (int16_t)CNFGPenY; |
||||
|
|
||||
|
int place = 0; |
||||
|
unsigned short index; |
||||
|
int bQuit = 0; |
||||
|
while (text[place]) { |
||||
|
unsigned char c = text[place]; |
||||
|
|
||||
|
switch (c) { |
||||
|
case 9: |
||||
|
iox += 12 * scale; |
||||
|
break; |
||||
|
case 10: |
||||
|
iox = (int16_t)CNFGPenX; |
||||
|
ioy += 6 * scale; |
||||
|
break; |
||||
|
default: |
||||
|
index = FontCharMap[c & 0x7f]; |
||||
|
if (index == 65535) { |
||||
|
iox += 3 * scale; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
lmap = &FontCharData[index]; |
||||
|
do { |
||||
|
int x1 = (int)((((*lmap) & 0x70) >> 4) * scale + iox); |
||||
|
int y1 = (int)(((*lmap) & 0x0f) * scale + ioy); |
||||
|
int x2 = (int)((((*(lmap + 1)) & 0x70) >> 4) * scale + iox); |
||||
|
int y2 = (int)(((*(lmap + 1)) & 0x0f) * scale + ioy); |
||||
|
lmap++; |
||||
|
CNFGTackSegment(x1, y1, x2, y2); |
||||
|
bQuit = *lmap & 0x80; |
||||
|
lmap++; |
||||
|
} while (!bQuit); |
||||
|
|
||||
|
iox += 3 * scale; |
||||
|
} |
||||
|
place++; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void CNFGDrawBox(int x1, int y1, int x2, int y2) |
||||
|
{ |
||||
|
unsigned lc = CNFGLastColor; |
||||
|
CNFGColor(CNFGDialogColor); |
||||
|
CNFGTackRectangle(x1, y1, x2, y2); |
||||
|
CNFGColor(lc); |
||||
|
CNFGTackSegment(x1, y1, x2, y1); |
||||
|
CNFGTackSegment(x2, y1, x2, y2); |
||||
|
CNFGTackSegment(x2, y2, x1, y2); |
||||
|
CNFGTackSegment(x1, y2, x1, y1); |
||||
|
} |
||||
|
|
||||
|
void CNFGTackRectangle(short x1, short y1, short x2, short y2) |
||||
|
{ |
||||
|
short ly = 0, my = 0, lx = 0, mx = 0; |
||||
|
short y, x; |
||||
|
if (y1 < y2) { ly = y1; my = y2; } |
||||
|
else { ly = y2; my = y1; } |
||||
|
if (x1 < x2) { lx = x1; mx = x2; } |
||||
|
else { lx = x2; mx = x1; } |
||||
|
|
||||
|
for (y = ly; y <= my; y++) |
||||
|
for (x = lx; x <= mx; x++) |
||||
|
CNFGTackPixel(x >> 1, y); |
||||
|
} |
||||
|
|
||||
|
// Perlin noise functions |
||||
|
int16_t tdNoiseAt(int16_t x, int16_t y) |
||||
|
{ |
||||
|
return ((x * 13244321 + y * 33442927)); |
||||
|
} |
||||
|
|
||||
|
static inline int16_t tdFade(int16_t f) |
||||
|
{ |
||||
|
return f; |
||||
|
} |
||||
|
|
||||
|
int16_t tdFLerp(int16_t a, int16_t b, int16_t t) |
||||
|
{ |
||||
|
int16_t fr = tdFade(t); |
||||
|
return (a * (256 - fr) + b * fr) >> 8; |
||||
|
} |
||||
|
|
||||
|
static inline int16_t tdFNoiseAt(int16_t x, int16_t y) |
||||
|
{ |
||||
|
int ix = x; |
||||
|
int iy = y; |
||||
|
int16_t fx = x - ix; |
||||
|
int16_t fy = y - iy; |
||||
|
|
||||
|
int16_t a = tdNoiseAt(ix, iy); |
||||
|
int16_t b = tdNoiseAt(ix + 1, iy); |
||||
|
int16_t c = tdNoiseAt(ix, iy + 1); |
||||
|
int16_t d = tdNoiseAt(ix + 1, iy + 1); |
||||
|
|
||||
|
int16_t top = tdFLerp(a, b, fx); |
||||
|
int16_t bottom = tdFLerp(c, d, fx); |
||||
|
|
||||
|
return tdFLerp(top, bottom, fy); |
||||
|
} |
||||
|
|
||||
|
int16_t tdPerlin2D(int16_t x, int16_t y) |
||||
|
{ |
||||
|
int ndepth = 5; |
||||
|
int depth; |
||||
|
int16_t ret = 0; |
||||
|
for (depth = 0; depth < ndepth; depth++) { |
||||
|
int16_t nx = (x * 256) / (256 << (ndepth - depth - 1)); |
||||
|
int16_t ny = (y * 256) / (256 << (ndepth - depth - 1)); |
||||
|
ret += tdFNoiseAt(nx, ny) / (256 << (depth + 1)); |
||||
|
} |
||||
|
return ret; |
||||
|
} |
||||
@ -0,0 +1,76 @@ |
|||||
|
/** |
||||
|
* @file 3d.h |
||||
|
* @brief Fixed-point 3D graphics engine |
||||
|
* |
||||
|
* Provides matrix-based 3D transformations and rendering primitives |
||||
|
* for the Channel3 video output. Uses 256 = 1.0 fixed-point math. |
||||
|
* |
||||
|
* Original Copyright 2015 <>< Charles Lohr |
||||
|
* ESP32 Port 2024 |
||||
|
*/ |
||||
|
|
||||
|
#ifndef _3D_H |
||||
|
#define _3D_H |
||||
|
|
||||
|
#include <stdint.h> |
||||
|
#include "video_broadcast.h" |
||||
|
|
||||
|
// External references |
||||
|
extern int gframe; |
||||
|
extern uint8_t *frontframe; |
||||
|
extern int16_t ProjectionMatrix[16]; |
||||
|
extern int16_t ModelviewMatrix[16]; |
||||
|
extern int CNFGPenX, CNFGPenY; |
||||
|
extern uint8_t CNFGBGColor; |
||||
|
extern uint8_t CNFGLastColor; |
||||
|
extern uint8_t CNFGDialogColor; |
||||
|
|
||||
|
// Drawing primitives |
||||
|
void CNFGTackSegment(int x0, int y0, int x1, int y1); |
||||
|
int LABS(int x); |
||||
|
|
||||
|
// Pixel plotting function pointer (set by CNFGColor) |
||||
|
extern void (*CNFGTackPixel)(int x, int y); |
||||
|
|
||||
|
// Coordinate transformation |
||||
|
void LocalToScreenspace(int16_t *coords_3v, int16_t *o1, int16_t *o2); |
||||
|
|
||||
|
// Trigonometry (lookup table based) |
||||
|
int16_t tdSIN(uint8_t iv); |
||||
|
int16_t tdCOS(uint8_t iv); |
||||
|
|
||||
|
/** |
||||
|
* @brief Set drawing color |
||||
|
* @param col Color value: |
||||
|
* 0-15: Standard density colors |
||||
|
* 16: Black, double-density |
||||
|
* 17: White, double-density |
||||
|
*/ |
||||
|
void CNFGColor(uint8_t col); |
||||
|
|
||||
|
// Matrix operations |
||||
|
void tdTranslate(int16_t *f, int16_t x, int16_t y, int16_t z); |
||||
|
void tdScale(int16_t *f, int16_t x, int16_t y, int16_t z); |
||||
|
void tdRotateEA(int16_t *f, int16_t x, int16_t y, int16_t z); |
||||
|
void tdMultiply(int16_t *fin1, int16_t *fin2, int16_t *fout); |
||||
|
void tdPTransform(int16_t *pin, int16_t *f, int16_t *pout); |
||||
|
void td4Transform(int16_t *pin, int16_t *f, int16_t *pout); |
||||
|
void MakeTranslate(int x, int y, int z, int16_t *out); |
||||
|
void Perspective(int fovx, int aspect, int zNear, int zFar, int16_t *out); |
||||
|
void tdIdentity(int16_t *matrix); |
||||
|
void MakeYRotationMatrix(uint8_t angle, int16_t *f); |
||||
|
void MakeXRotationMatrix(uint8_t angle, int16_t *f); |
||||
|
|
||||
|
// High-level drawing |
||||
|
void DrawGeoSphere(void); |
||||
|
void Draw3DSegment(int16_t *c1, int16_t *c2); |
||||
|
void CNFGDrawText(const char *text, int scale); |
||||
|
void CNFGDrawBox(int x1, int y1, int x2, int y2); |
||||
|
void CNFGTackRectangle(short x1, short y1, short x2, short y2); |
||||
|
|
||||
|
// Perlin noise |
||||
|
int16_t tdPerlin2D(int16_t x, int16_t y); |
||||
|
int16_t tdFLerp(int16_t a, int16_t b, int16_t t); |
||||
|
int16_t tdNoiseAt(int16_t x, int16_t y); |
||||
|
|
||||
|
#endif // _3D_H |
||||
@ -0,0 +1,21 @@ |
|||||
|
idf_component_register( |
||||
|
SRCS |
||||
|
"user_main.c" |
||||
|
"video_broadcast.c" |
||||
|
"3d.c" |
||||
|
INCLUDE_DIRS |
||||
|
"." |
||||
|
REQUIRES |
||||
|
driver |
||||
|
esp_timer |
||||
|
esp_wifi |
||||
|
nvs_flash |
||||
|
esp_netif |
||||
|
esp_http_server |
||||
|
esp_http_client |
||||
|
mqtt |
||||
|
PRIV_REQUIRES |
||||
|
tablemaker |
||||
|
json |
||||
|
mbedtls |
||||
|
) |
||||
@ -0,0 +1,63 @@ |
|||||
|
menu "Channel3 Configuration" |
||||
|
|
||||
|
choice VIDEO_STANDARD |
||||
|
prompt "Video Standard" |
||||
|
default VIDEO_NTSC |
||||
|
help |
||||
|
Select the video standard for broadcast output. |
||||
|
|
||||
|
config VIDEO_NTSC |
||||
|
bool "NTSC (North America, Japan)" |
||||
|
config VIDEO_PAL |
||||
|
bool "PAL (Europe, Australia)" |
||||
|
endchoice |
||||
|
|
||||
|
config I2S_DATA_GPIO |
||||
|
int "I2S Data Output GPIO" |
||||
|
default 22 |
||||
|
range 0 39 |
||||
|
help |
||||
|
GPIO pin for I2S data output (RF broadcast). |
||||
|
This pin will output the 80MHz modulated signal. |
||||
|
|
||||
|
choice WIFI_MODE |
||||
|
prompt "WiFi Mode" |
||||
|
default WIFI_MODE_STATION |
||||
|
help |
||||
|
Select WiFi operation mode. |
||||
|
|
||||
|
config WIFI_MODE_STATION |
||||
|
bool "Station (connect to existing network)" |
||||
|
config WIFI_MODE_SOFTAP |
||||
|
bool "SoftAP (create own access point)" |
||||
|
endchoice |
||||
|
|
||||
|
config WIFI_STA_SSID |
||||
|
string "WiFi Network SSID" |
||||
|
default "MyNetwork" |
||||
|
depends on WIFI_MODE_STATION |
||||
|
help |
||||
|
SSID of the WiFi network to connect to. |
||||
|
|
||||
|
config WIFI_STA_PASS |
||||
|
string "WiFi Network Password" |
||||
|
default "MyPassword" |
||||
|
depends on WIFI_MODE_STATION |
||||
|
help |
||||
|
Password for the WiFi network. |
||||
|
|
||||
|
config WIFI_SOFTAP_SSID |
||||
|
string "WiFi SoftAP SSID" |
||||
|
default "Channel3" |
||||
|
depends on WIFI_MODE_SOFTAP |
||||
|
help |
||||
|
SSID for the ESP32 SoftAP mode. |
||||
|
|
||||
|
config WIFI_SOFTAP_PASS |
||||
|
string "WiFi SoftAP Password" |
||||
|
default "channel3tv" |
||||
|
depends on WIFI_MODE_SOFTAP |
||||
|
help |
||||
|
Password for the ESP32 SoftAP mode. Leave empty for open network. |
||||
|
|
||||
|
endmenu |
||||
4272
main/user_main.c
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,497 @@ |
|||||
|
/** |
||||
|
* @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); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,70 @@ |
|||||
|
/** |
||||
|
* @file video_broadcast.h |
||||
|
* @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 GPIO pin acts as an antenna, |
||||
|
* radiating RF directly on Channel 3 (61.25 MHz). |
||||
|
* |
||||
|
* Original ESP8266 version: Copyright 2015 <>< Charles Lohr |
||||
|
* ESP32 Port 2024 |
||||
|
*/ |
||||
|
|
||||
|
#ifndef VIDEO_BROADCAST_H |
||||
|
#define VIDEO_BROADCAST_H |
||||
|
|
||||
|
#include <stdint.h> |
||||
|
#include "sdkconfig.h" |
||||
|
|
||||
|
// Framebuffer dimensions |
||||
|
// FBW is "double-pixels" for double-resolution monochrome width |
||||
|
#define FBW 232 |
||||
|
#define FBW2 (FBW / 2) // Actual width in true pixels |
||||
|
|
||||
|
#ifdef CONFIG_VIDEO_PAL |
||||
|
#define FBH 264 |
||||
|
#else |
||||
|
#define FBH 220 |
||||
|
#endif |
||||
|
|
||||
|
// DMA buffer configuration |
||||
|
#define DMABUFFERDEPTH 3 |
||||
|
|
||||
|
// Global variables |
||||
|
extern int gframe; // Current frame number |
||||
|
extern uint16_t framebuffer[((FBW2/4)*(FBH))*2]; // Double-buffered framebuffer |
||||
|
extern uint32_t last_internal_frametime; // Last frame rendering time |
||||
|
extern int8_t jam_color; // Color jam for RF testing (-1 = disabled) |
||||
|
|
||||
|
/** |
||||
|
* @brief Initialize the I2S DMA video broadcast system |
||||
|
* |
||||
|
* Sets up: |
||||
|
* - APLL clock for 80 MHz output |
||||
|
* - I2S in LCD/parallel mode |
||||
|
* - DMA descriptors in circular buffer configuration |
||||
|
* - ISR for line-by-line rendering |
||||
|
* - GPIO routing for RF output |
||||
|
*/ |
||||
|
void video_broadcast_init(void); |
||||
|
|
||||
|
/** |
||||
|
* @brief Stop video broadcast |
||||
|
* |
||||
|
* Disables I2S output and releases resources |
||||
|
*/ |
||||
|
void video_broadcast_stop(void); |
||||
|
|
||||
|
/** |
||||
|
* @brief Temporarily pause video broadcast for flash operations |
||||
|
* |
||||
|
* Disables the I2S interrupt to prevent cache conflicts during NVS writes |
||||
|
*/ |
||||
|
void video_broadcast_pause(void); |
||||
|
|
||||
|
/** |
||||
|
* @brief Resume video broadcast after pause |
||||
|
*/ |
||||
|
void video_broadcast_resume(void); |
||||
|
|
||||
|
#endif // VIDEO_BROADCAST_H |
||||
@ -0,0 +1,10 @@ |
|||||
|
$env:IDF_PYTHON_ENV_PATH = "C:\Espressif\python_env\idf5.5_py3.11_env" |
||||
|
$env:IDF_PATH = "C:\Espressif\frameworks\esp-idf-v5.5.2" |
||||
|
$env:IDF_TOOLS_PATH = "C:\Espressif" |
||||
|
$env:MSYSTEM = $null |
||||
|
$env:SHELL = $null |
||||
|
$env:SHLVL = $null |
||||
|
$env:TERM = $null |
||||
|
Set-Location "C:\git\channel3\esp32_channel3" |
||||
|
. "C:\Espressif\Initialize-Idf.ps1" |
||||
|
idf.py -p COM5 monitor |
||||
@ -0,0 +1,5 @@ |
|||||
|
# Name, Type, SubType, Offset, Size, Flags |
||||
|
# Custom partition table with larger NVS for image storage |
||||
|
nvs, data, nvs, 0x9000, 0x10000, |
||||
|
phy_init, data, phy, 0x19000, 0x1000, |
||||
|
factory, app, factory, 0x20000, 0x100000, |
||||
@ -0,0 +1,26 @@ |
|||||
|
$ErrorActionPreference = "Continue" |
||||
|
|
||||
|
# Clear MSYS environment variables |
||||
|
Remove-Item Env:MSYSTEM -ErrorAction SilentlyContinue |
||||
|
$env:MSYSTEM = "" |
||||
|
|
||||
|
# Set ESP-IDF environment |
||||
|
$env:IDF_PATH = "C:\Espressif\frameworks\esp-idf-v5.5.2" |
||||
|
$env:IDF_TOOLS_PATH = "C:\Espressif" |
||||
|
$env:IDF_PYTHON_ENV_PATH = "C:\Espressif\python_env\idf5.5_py3.11_env" |
||||
|
|
||||
|
$toolPaths = @( |
||||
|
"C:\Espressif\python_env\idf5.5_py3.11_env\Scripts", |
||||
|
"C:\Espressif\tools\cmake\3.30.2\bin", |
||||
|
"C:\Espressif\tools\ninja\1.12.1", |
||||
|
"C:\Espressif\tools\xtensa-esp-elf\esp-14.2.0_20251107\xtensa-esp-elf\bin" |
||||
|
) |
||||
|
$env:PATH = ($toolPaths -join ";") + ";" + $env:PATH |
||||
|
|
||||
|
Set-Location "C:\git\channel3\esp32_channel3" |
||||
|
|
||||
|
$python = "C:\Espressif\python_env\idf5.5_py3.11_env\Scripts\python.exe" |
||||
|
$idfpy = "$env:IDF_PATH\tools\idf.py" |
||||
|
|
||||
|
Write-Host "Building..." |
||||
|
& $python $idfpy build |
||||
@ -0,0 +1,19 @@ |
|||||
|
@echo off |
||||
|
REM Clear MSYS environment variables that confuse ESP-IDF |
||||
|
set MSYSTEM= |
||||
|
set MSYSTEM_PREFIX= |
||||
|
set MSYSTEM_CARCH= |
||||
|
set MSYSTEM_CHOST= |
||||
|
set MINGW_CHOST= |
||||
|
set MINGW_PREFIX= |
||||
|
set MINGW_PACKAGE_PREFIX= |
||||
|
|
||||
|
set IDF_PATH=C:\Espressif\frameworks\esp-idf-v5.5.2 |
||||
|
set IDF_TOOLS_PATH=C:\Espressif |
||||
|
set IDF_PYTHON_ENV_PATH=C:\Espressif\python_env\idf5.5_py3.11_env |
||||
|
set PATH=C:\Espressif\python_env\idf5.5_py3.11_env\Scripts;C:\Espressif\tools\cmake\3.30.2\bin;C:\Espressif\tools\ninja\1.12.1;C:\Espressif\tools\xtensa-esp-elf\esp-14.2.0_20251107\xtensa-esp-elf\bin;C:\Espressif\tools\esp32ulp-elf\2.38_20240113\esp32ulp-elf\bin;C:\Espressif\tools\idf-git\2.44.0\cmd;%PATH% |
||||
|
|
||||
|
cd /d C:\git\channel3\esp32_channel3 |
||||
|
echo Starting build... |
||||
|
C:\Espressif\python_env\idf5.5_py3.11_env\Scripts\python.exe C:\Espressif\frameworks\esp-idf-v5.5.2\tools\idf.py build > build_output.txt 2>&1 |
||||
|
echo Build exit code: %ERRORLEVEL% |
||||
@ -0,0 +1,5 @@ |
|||||
|
@echo off |
||||
|
echo Running PowerShell build script... |
||||
|
powershell.exe -ExecutionPolicy Bypass -NoProfile -Command "& {cd 'C:\git\channel3\esp32_channel3'; .\build.ps1}" > C:\git\channel3\esp32_channel3\ps_output.txt 2>&1 |
||||
|
echo Done. Exit code: %ERRORLEVEL% |
||||
|
type C:\git\channel3\esp32_channel3\ps_output.txt |
||||
2231
sdkconfig
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,38 @@ |
|||||
|
# Channel3 ESP32 - Default SDK Configuration |
||||
|
|
||||
|
# Use 240MHz CPU frequency for maximum performance |
||||
|
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y |
||||
|
|
||||
|
# Enable APLL for precise audio clock generation |
||||
|
CONFIG_ESP32_APLL_ENABLED=y |
||||
|
|
||||
|
# Increase task watchdog timeout for video generation |
||||
|
CONFIG_ESP_TASK_WDT_TIMEOUT_S=10 |
||||
|
|
||||
|
# Place frequently called functions in IRAM |
||||
|
CONFIG_COMPILER_OPTIMIZATION_PERF=y |
||||
|
|
||||
|
# Enable WiFi |
||||
|
CONFIG_ESP_WIFI_ENABLED=y |
||||
|
|
||||
|
# WiFi Station Mode - connect to existing network |
||||
|
# Video starts AFTER WiFi connects to avoid cache conflict |
||||
|
CONFIG_WIFI_MODE_STATION=y |
||||
|
CONFIG_WIFI_STA_SSID="Super Exmodiar Lvl2.5" |
||||
|
CONFIG_WIFI_STA_PASS="Commodore" |
||||
|
|
||||
|
# I2S configuration |
||||
|
CONFIG_I2S_ISR_IRAM_SAFE=y |
||||
|
|
||||
|
# Console output |
||||
|
CONFIG_ESP_CONSOLE_UART_DEFAULT=y |
||||
|
CONFIG_ESP_CONSOLE_UART_BAUDRATE=115200 |
||||
|
|
||||
|
# Flash size (adjust as needed) |
||||
|
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y |
||||
|
|
||||
|
# Partition table |
||||
|
CONFIG_PARTITION_TABLE_SINGLE_APP=y |
||||
|
|
||||
|
# Disable brownout detector during RF operations |
||||
|
CONFIG_ESP_BROWNOUT_DET=n |
||||
@ -0,0 +1,84 @@ |
|||||
|
# Channel3 ESP32 Streaming Tools |
||||
|
|
||||
|
## stream_video.py |
||||
|
|
||||
|
Stream video files to the ESP32 Channel3 RF Broadcast over WiFi. |
||||
|
|
||||
|
### Requirements |
||||
|
|
||||
|
- Python 3.6+ |
||||
|
- NumPy (`pip install numpy`) |
||||
|
- FFmpeg installed and in PATH |
||||
|
|
||||
|
### Basic Usage |
||||
|
|
||||
|
```bash |
||||
|
ffmpeg -i video.mp4 -vf "scale=116:220" -f rawvideo -pix_fmt rgb24 - | python stream_video.py <ESP32_IP> |
||||
|
``` |
||||
|
|
||||
|
### Examples |
||||
|
|
||||
|
**Stream a video file (with correct aspect ratio):** |
||||
|
```bash |
||||
|
ffmpeg -i myvideo.mp4 -vf "scale=293:220:force_original_aspect_ratio=decrease,pad=293:220:(ow-iw)/2:(oh-ih)/2,scale=116:220" -f rawvideo -pix_fmt rgb24 - | python stream_video.py 192.168.1.100 |
||||
|
``` |
||||
|
|
||||
|
> **Note:** The TV has wide pixels (PAR ~2.53:1). We first scale to 293x220 (116×2.53=293) |
||||
|
> to preserve aspect ratio, then squash to 116x220 for the physical pixels. |
||||
|
|
||||
|
**Stream at a specific frame rate (e.g., 15 fps):** |
||||
|
```bash |
||||
|
ffmpeg -i myvideo.mp4 -vf "scale=116:220,fps=15" -f rawvideo -pix_fmt rgb24 - | python stream_video.py 192.168.1.100 -f 15 |
||||
|
``` |
||||
|
|
||||
|
**Stream webcam (Windows):** |
||||
|
```bash |
||||
|
ffmpeg -f dshow -i video="Your Webcam Name" -vf "scale=116:220" -f rawvideo -pix_fmt rgb24 - | python stream_video.py 192.168.1.100 |
||||
|
``` |
||||
|
|
||||
|
**Stream webcam (Linux):** |
||||
|
```bash |
||||
|
ffmpeg -f v4l2 -i /dev/video0 -vf "scale=116:220" -f rawvideo -pix_fmt rgb24 - | python stream_video.py 192.168.1.100 |
||||
|
``` |
||||
|
|
||||
|
**Stream desktop (Windows):** |
||||
|
```bash |
||||
|
ffmpeg -f gdigrab -i desktop -vf "scale=116:220" -f rawvideo -pix_fmt rgb24 - | python stream_video.py 192.168.1.100 |
||||
|
``` |
||||
|
|
||||
|
**Stream desktop (Linux):** |
||||
|
```bash |
||||
|
ffmpeg -f x11grab -i :0.0 -vf "scale=116:220" -f rawvideo -pix_fmt rgb24 - | python stream_video.py 192.168.1.100 |
||||
|
``` |
||||
|
|
||||
|
### Options |
||||
|
|
||||
|
| Option | Description | |
||||
|
|--------|-------------| |
||||
|
| `-p, --port PORT` | TCP port (default: 5000) | |
||||
|
| `-f, --fps FPS` | Target frame rate (default: 30) | |
||||
|
| `--no-dither` | Disable Floyd-Steinberg dithering (faster but lower quality) | |
||||
|
|
||||
|
### How it Works |
||||
|
|
||||
|
1. FFmpeg decodes the video and scales it to 116x220 pixels, outputting raw RGB24 frames |
||||
|
2. The Python script reads each frame, applies Floyd-Steinberg dithering to reduce to 16 colors |
||||
|
3. Frames are packed as 4 bits per pixel (two pixels per byte) |
||||
|
4. Packed frames (12,760 bytes each) are sent to the ESP32 over TCP |
||||
|
5. The ESP32 displays each frame on the analog TV broadcast |
||||
|
|
||||
|
### Troubleshooting |
||||
|
|
||||
|
**"Connection refused"** |
||||
|
- Make sure the ESP32 is powered on and connected to WiFi |
||||
|
- Check that you're using the correct IP address |
||||
|
- The stream server runs on port 5000 by default |
||||
|
|
||||
|
**Low frame rate** |
||||
|
- Try `--no-dither` for faster processing |
||||
|
- Reduce target FPS with `-f 15` |
||||
|
- Make sure your WiFi connection is stable |
||||
|
|
||||
|
**Video looks stretched** |
||||
|
- Use the aspect ratio preservation ffmpeg filter shown above |
||||
|
- The display is 116x220 pixels with approximately 2.5:1 pixel aspect ratio |
||||
@ -0,0 +1,8 @@ |
|||||
|
@echo off |
||||
|
REM Stream lcars.mp4 to ESP32 Channel3 |
||||
|
REM Make sure ffmpeg is in your PATH and numpy is installed (pip install numpy) |
||||
|
REM |
||||
|
REM The TV has a pixel aspect ratio of ~2.53:1 (wide pixels) |
||||
|
REM So we scale to 293x220 first (116*2.53=293), then squash to 116x220 |
||||
|
|
||||
|
ffmpeg -f lavfi -i color=black:293x220 -i "%~dp0lcars.mp4" -filter_complex "[1:v]scale=293:220:force_original_aspect_ratio=decrease[vid];[0:v][vid]overlay=(W-w)/2:(H-h)/2,scale=116:220" -map 0:a? -f rawvideo -pix_fmt rgb24 -shortest - | python "%~dp0stream_video.py" 10.0.0.59 -f 60 --grayscale |
||||
@ -0,0 +1,295 @@ |
|||||
|
#!/usr/bin/env python3 |
||||
|
""" |
||||
|
Stream video to ESP32 Channel3 RF Broadcast |
||||
|
|
||||
|
This script takes RGB24 frames from ffmpeg via stdin, dithers them to 16 colors, |
||||
|
packs them as 4bpp, and streams them to the ESP32 over TCP. |
||||
|
|
||||
|
Usage: |
||||
|
ffmpeg -i video.mp4 -vf "scale=116:220" -f rawvideo -pix_fmt rgb24 - | python stream_video.py <ESP32_IP> |
||||
|
|
||||
|
Options: |
||||
|
-p, --port Port number (default: 5000) |
||||
|
-f, --fps Target frame rate (default: 30) |
||||
|
--no-dither Disable Floyd-Steinberg dithering (faster but lower quality) |
||||
|
""" |
||||
|
|
||||
|
import sys |
||||
|
import socket |
||||
|
import argparse |
||||
|
import time |
||||
|
import numpy as np |
||||
|
|
||||
|
# Image dimensions |
||||
|
WIDTH = 116 |
||||
|
HEIGHT = 220 |
||||
|
FRAME_SIZE_RGB = WIDTH * HEIGHT * 3 |
||||
|
FRAME_SIZE_4BPP = WIDTH * HEIGHT // 2 |
||||
|
|
||||
|
# CGA-like 16 color palette (RGB values) |
||||
|
PALETTE = np.array([ |
||||
|
[0, 0, 0], # 0: Black |
||||
|
[0, 0, 170], # 1: Blue |
||||
|
[0, 170, 0], # 2: Green |
||||
|
[0, 170, 170], # 3: Cyan |
||||
|
[170, 0, 0], # 4: Red |
||||
|
[170, 0, 170], # 5: Magenta |
||||
|
[170, 85, 0], # 6: Brown |
||||
|
[170, 170, 170], # 7: Light Gray |
||||
|
[85, 85, 85], # 8: Dark Gray |
||||
|
[85, 85, 255], # 9: Light Blue |
||||
|
[85, 255, 85], # 10: Light Green |
||||
|
[85, 255, 255], # 11: Light Cyan |
||||
|
[255, 85, 85], # 12: Light Red |
||||
|
[255, 85, 255], # 13: Light Magenta |
||||
|
[255, 255, 85], # 14: Yellow |
||||
|
[255, 255, 255], # 15: White |
||||
|
], dtype=np.float32) |
||||
|
|
||||
|
# Luminance values for each palette color (ITU-R BT.601) |
||||
|
# Y = 0.299*R + 0.587*G + 0.114*B |
||||
|
PALETTE_LUMINANCE = np.array([ |
||||
|
0.299*0 + 0.587*0 + 0.114*0, # 0: Black = 0 |
||||
|
0.299*0 + 0.587*0 + 0.114*170, # 1: Blue = 19.4 |
||||
|
0.299*0 + 0.587*170 + 0.114*0, # 2: Green = 99.8 |
||||
|
0.299*0 + 0.587*170 + 0.114*170, # 3: Cyan = 119.2 |
||||
|
0.299*170 + 0.587*0 + 0.114*0, # 4: Red = 50.8 |
||||
|
0.299*170 + 0.587*0 + 0.114*170, # 5: Magenta = 70.2 |
||||
|
0.299*170 + 0.587*85 + 0.114*0, # 6: Brown = 100.7 |
||||
|
0.299*170 + 0.587*170 + 0.114*170, # 7: Light Gray = 170 |
||||
|
0.299*85 + 0.587*85 + 0.114*85, # 8: Dark Gray = 85 |
||||
|
0.299*85 + 0.587*85 + 0.114*255, # 9: Light Blue = 104.4 |
||||
|
0.299*85 + 0.587*255 + 0.114*85, # 10: Light Green = 185.3 |
||||
|
0.299*85 + 0.587*255 + 0.114*255, # 11: Light Cyan = 204.6 |
||||
|
0.299*255 + 0.587*85 + 0.114*85, # 12: Light Red = 135.9 |
||||
|
0.299*255 + 0.587*85 + 0.114*255, # 13: Light Magenta = 155.2 |
||||
|
0.299*255 + 0.587*255 + 0.114*85, # 14: Yellow = 235.6 |
||||
|
0.299*255 + 0.587*255 + 0.114*255, # 15: White = 255 |
||||
|
], dtype=np.float32) |
||||
|
|
||||
|
# Palette indices sorted by luminance (darkest to brightest) |
||||
|
GRAYSCALE_ORDER = np.argsort(PALETTE_LUMINANCE) # [0,1,4,5,8,2,6,9,3,12,13,7,10,11,14,15] |
||||
|
SORTED_LUMINANCE = PALETTE_LUMINANCE[GRAYSCALE_ORDER] |
||||
|
|
||||
|
|
||||
|
def find_nearest_grayscale_fast(img): |
||||
|
""" |
||||
|
Convert RGB image to grayscale and map to palette by luminance. |
||||
|
This gives 16 distinct gray levels on a B&W TV. |
||||
|
""" |
||||
|
# Convert to grayscale using luminance formula |
||||
|
gray = 0.299 * img[:,:,0] + 0.587 * img[:,:,1] + 0.114 * img[:,:,2] |
||||
|
|
||||
|
# Find nearest luminance level |
||||
|
gray_expanded = gray[:, :, np.newaxis] # (H, W, 1) |
||||
|
lum_expanded = SORTED_LUMINANCE[np.newaxis, np.newaxis, :] # (1, 1, 16) |
||||
|
distances = np.abs(gray_expanded - lum_expanded) |
||||
|
nearest_idx = np.argmin(distances, axis=2) |
||||
|
|
||||
|
# Map back to actual palette index |
||||
|
return GRAYSCALE_ORDER[nearest_idx].astype(np.uint8) |
||||
|
|
||||
|
|
||||
|
def find_nearest_colors_fast(img): |
||||
|
""" |
||||
|
Find nearest palette color for each pixel using vectorized operations. |
||||
|
|
||||
|
Args: |
||||
|
img: numpy array of shape (H, W, 3) with RGB values (float32) |
||||
|
|
||||
|
Returns: |
||||
|
numpy array of shape (H, W) with palette indices 0-15 |
||||
|
""" |
||||
|
# Reshape for broadcasting: (H, W, 3) -> (H, W, 1, 3) |
||||
|
img_expanded = img[:, :, np.newaxis, :] |
||||
|
# PALETTE shape: (16, 3) -> (1, 1, 16, 3) |
||||
|
palette_expanded = PALETTE[np.newaxis, np.newaxis, :, :] |
||||
|
|
||||
|
# Calculate squared distances to all palette colors |
||||
|
# Result shape: (H, W, 16) |
||||
|
distances = np.sum((img_expanded - palette_expanded) ** 2, axis=3) |
||||
|
|
||||
|
# Find index of minimum distance for each pixel |
||||
|
return np.argmin(distances, axis=2).astype(np.uint8) |
||||
|
|
||||
|
|
||||
|
def dither_frame_fast(frame): |
||||
|
""" |
||||
|
Convert RGB frame to 16-color indexed using optimized Floyd-Steinberg dithering. |
||||
|
Uses vectorized row operations for better performance. |
||||
|
|
||||
|
Args: |
||||
|
frame: numpy array of shape (HEIGHT, WIDTH, 3) with RGB values |
||||
|
|
||||
|
Returns: |
||||
|
numpy array of shape (HEIGHT, WIDTH) with palette indices 0-15 |
||||
|
""" |
||||
|
img = frame.astype(np.float32) |
||||
|
output = np.zeros((HEIGHT, WIDTH), dtype=np.uint8) |
||||
|
|
||||
|
for y in range(HEIGHT): |
||||
|
# Process entire row at once for color matching |
||||
|
row = np.clip(img[y], 0, 255) |
||||
|
|
||||
|
# Find nearest colors for entire row |
||||
|
row_expanded = row[:, np.newaxis, :] # (W, 1, 3) |
||||
|
palette_expanded = PALETTE[np.newaxis, :, :] # (1, 16, 3) |
||||
|
distances = np.sum((row_expanded - palette_expanded) ** 2, axis=2) # (W, 16) |
||||
|
indices = np.argmin(distances, axis=1).astype(np.uint8) |
||||
|
output[y] = indices |
||||
|
|
||||
|
# Calculate errors for entire row |
||||
|
chosen_colors = PALETTE[indices] # (W, 3) |
||||
|
errors = row - chosen_colors # (W, 3) |
||||
|
|
||||
|
# Distribute errors (Floyd-Steinberg) |
||||
|
# Right pixel: 7/16 |
||||
|
if y < HEIGHT: |
||||
|
img[y, 1:, :] += errors[:-1, :] * (7.0 / 16.0) |
||||
|
|
||||
|
# Next row |
||||
|
if y + 1 < HEIGHT: |
||||
|
# Bottom-left: 3/16 |
||||
|
img[y + 1, :-1, :] += errors[1:, :] * (3.0 / 16.0) |
||||
|
# Bottom: 5/16 |
||||
|
img[y + 1, :, :] += errors * (5.0 / 16.0) |
||||
|
# Bottom-right: 1/16 |
||||
|
img[y + 1, 1:, :] += errors[:-1, :] * (1.0 / 16.0) |
||||
|
|
||||
|
return output |
||||
|
|
||||
|
|
||||
|
def dither_frame_none(frame): |
||||
|
""" |
||||
|
Convert RGB frame to 16-color indexed without dithering (fastest). |
||||
|
|
||||
|
Args: |
||||
|
frame: numpy array of shape (HEIGHT, WIDTH, 3) with RGB values |
||||
|
|
||||
|
Returns: |
||||
|
numpy array of shape (HEIGHT, WIDTH) with palette indices 0-15 |
||||
|
""" |
||||
|
return find_nearest_colors_fast(frame.astype(np.float32)) |
||||
|
|
||||
|
|
||||
|
def pack_4bpp_fast(indexed_frame): |
||||
|
""" |
||||
|
Pack indexed frame (0-15 values) into 4bpp format using vectorized operations. |
||||
|
Two pixels per byte: high nibble = first pixel, low nibble = second pixel. |
||||
|
|
||||
|
Args: |
||||
|
indexed_frame: numpy array of shape (HEIGHT, WIDTH) with values 0-15 |
||||
|
|
||||
|
Returns: |
||||
|
bytes object of length FRAME_SIZE_4BPP |
||||
|
""" |
||||
|
flat = indexed_frame.flatten() |
||||
|
# Take pairs of pixels and pack them |
||||
|
high_nibbles = flat[0::2].astype(np.uint8) << 4 |
||||
|
low_nibbles = flat[1::2].astype(np.uint8) |
||||
|
packed = high_nibbles | low_nibbles |
||||
|
return packed.tobytes() |
||||
|
|
||||
|
|
||||
|
def main(): |
||||
|
parser = argparse.ArgumentParser( |
||||
|
description='Stream video to ESP32 Channel3 RF Broadcast', |
||||
|
formatter_class=argparse.RawDescriptionHelpFormatter, |
||||
|
epilog=""" |
||||
|
Examples: |
||||
|
Basic usage (with PAR correction for wide TV pixels): |
||||
|
ffmpeg -f lavfi -i color=black:293x220 -i video.mp4 -filter_complex "[1:v]scale=293:220:force_original_aspect_ratio=decrease[vid];[0:v][vid]overlay=(W-w)/2:(H-h)/2,scale=116:220" -f rawvideo -pix_fmt rgb24 -shortest - | python stream_video.py 192.168.1.100 |
||||
|
|
||||
|
Stream webcam: |
||||
|
ffmpeg -f dshow -i video="Your Webcam" -vf "scale=116:220" -f rawvideo -pix_fmt rgb24 - | python stream_video.py 192.168.1.100 |
||||
|
|
||||
|
Fast mode (no dithering): |
||||
|
ffmpeg -i video.mp4 -vf "scale=116:220" -f rawvideo -pix_fmt rgb24 - | python stream_video.py 192.168.1.100 --no-dither -f 60 |
||||
|
""" |
||||
|
) |
||||
|
parser.add_argument('host', help='ESP32 IP address') |
||||
|
parser.add_argument('-p', '--port', type=int, default=5000, help='Port number (default: 5000)') |
||||
|
parser.add_argument('-f', '--fps', type=float, default=30, help='Target frame rate (default: 30)') |
||||
|
parser.add_argument('--no-dither', action='store_true', help='Disable dithering (faster)') |
||||
|
parser.add_argument('--grayscale', '--bw', action='store_true', help='Grayscale mode for B&W TVs (16 distinct gray levels)') |
||||
|
|
||||
|
args = parser.parse_args() |
||||
|
|
||||
|
# Calculate frame timing |
||||
|
frame_interval = 1.0 / args.fps |
||||
|
|
||||
|
print(f"Connecting to {args.host}:{args.port}...") |
||||
|
|
||||
|
try: |
||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
||||
|
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # Disable Nagle's algorithm |
||||
|
sock.connect((args.host, args.port)) |
||||
|
print(f"Connected! Streaming at {args.fps} fps target...") |
||||
|
print("Press Ctrl+C to stop") |
||||
|
except Exception as e: |
||||
|
print(f"Connection failed: {e}") |
||||
|
sys.exit(1) |
||||
|
|
||||
|
frame_count = 0 |
||||
|
start_time = time.time() |
||||
|
|
||||
|
# Select processing function based on mode |
||||
|
if args.grayscale: |
||||
|
# Grayscale mode: map by luminance for 16 distinct gray levels on B&W TV |
||||
|
dither_func = lambda f: find_nearest_grayscale_fast(f.astype(np.float32)) |
||||
|
print("Grayscale mode: mapping to 16 luminance levels") |
||||
|
elif args.no_dither: |
||||
|
dither_func = dither_frame_none |
||||
|
else: |
||||
|
dither_func = dither_frame_fast |
||||
|
|
||||
|
try: |
||||
|
while True: |
||||
|
frame_start = time.time() |
||||
|
|
||||
|
# Read one RGB24 frame from stdin |
||||
|
raw_data = sys.stdin.buffer.read(FRAME_SIZE_RGB) |
||||
|
if len(raw_data) < FRAME_SIZE_RGB: |
||||
|
print(f"\nEnd of stream after {frame_count} frames") |
||||
|
break |
||||
|
|
||||
|
# Convert to numpy array |
||||
|
frame = np.frombuffer(raw_data, dtype=np.uint8).reshape((HEIGHT, WIDTH, 3)) |
||||
|
|
||||
|
# Dither to 16 colors |
||||
|
indexed = dither_func(frame) |
||||
|
|
||||
|
# Pack to 4bpp |
||||
|
packed = pack_4bpp_fast(indexed) |
||||
|
|
||||
|
# Send to ESP32 |
||||
|
try: |
||||
|
sock.sendall(packed) |
||||
|
except Exception as e: |
||||
|
print(f"\nSend error: {e}") |
||||
|
break |
||||
|
|
||||
|
frame_count += 1 |
||||
|
|
||||
|
# Frame rate limiting |
||||
|
elapsed = time.time() - frame_start |
||||
|
if elapsed < frame_interval: |
||||
|
time.sleep(frame_interval - elapsed) |
||||
|
|
||||
|
# Progress indicator |
||||
|
if frame_count % 30 == 0: |
||||
|
actual_fps = frame_count / (time.time() - start_time) |
||||
|
print(f"\rFrames: {frame_count}, FPS: {actual_fps:.1f} ", end='', flush=True) |
||||
|
|
||||
|
except KeyboardInterrupt: |
||||
|
print(f"\nStopped after {frame_count} frames") |
||||
|
|
||||
|
finally: |
||||
|
sock.close() |
||||
|
elapsed = time.time() - start_time |
||||
|
if elapsed > 0: |
||||
|
print(f"Average FPS: {frame_count / elapsed:.1f}") |
||||
|
|
||||
|
|
||||
|
if __name__ == '__main__': |
||||
|
main() |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue