-
Notifications
You must be signed in to change notification settings - Fork 0
I2C Data Injection In Panasonic CRT
This guide explains how to sniff and decode data transmitted through the I2C bus of a Panasonic TC-21FX30LA CRT powered by a Micronas VCT 49xyI controller. Additionally, it covers how to inject data into the bus using a Raspberry Pi to modify VCT registers and external memory. Finally, it presents a practical use case where Component analog video inputs (YPbPr) are forced to behave as RGB analog, and shows how a cheap ESP8266 microcontroller can be used for this task without causing data corruption on the bus.
- Disclaimer
- Tools and equipment used
- Sniff and decode i2c data
- Reading E2 EEPROM (TV Settings)
- Writing to E2 EEPROM
- Interfacing I2C BUS Using a Raspberry Pi and Python
- VCT architecture
- Configure Component Video Input as RGB
- Persisting the changes
- Using ESP8266 to inject data into the bus
- Pictures and videos
This guide is intended for educational purposes only. The procedures described herein involve modifying the internal operation of Panasonic TC-21FX30LA CRT, which may result in unintended consequences, including but not limited to rendering the TV inoperable.
The author of this guide assumes no responsibility or liability for any damage, data loss, or hardware malfunction that may occur as a result of following the instructions provided. Users proceed at their own risk.
By using this guide, you acknowledge and agree that the author is not liable for any direct, indirect, incidental, special, or consequential damages resulting from the use of the information contained herein.
- Panasonic TC-21FX30LA CRT (Chassis GP41)
- Micronas VCT 49xyI
- Logic Analyzer
- Raspberry Pi
- pigpio Python Library
- ESP8266 NodeMcu Microcontroller
- esp-i2c lib
This TV has a service port labeled as A8:
Service port (A8)
Let's hook up the logic analyzer to SCL, SDA and GND. Startup sequence (when TV is turned on) looks like this:
i2c - startup
The first part shows some interesting readings:
i2c - startup E2
Decoding i2c data at startup exposes the following W/R sequence:
i2c - E2 read
This translates to:
1. Write 0x10 to addr. 0x50
2. Read addr 0x50
3. Write 0x11 to addr. 0x51
4. Read addr 0x51
...
Micronas VCT datasheet doesn't mention any address in this range (0x50...0x57). So, this must be a communication to an external peripheral. If you take a closer look at Panasonic Service manual, you will find a section that contains the default values for the settings stored in memory:
E2 memory map
So, it is reading the TV settings stored in an external memory accessed via i2c bus:
0x36 0x00 0x02 0x01 0x00 0x10 0x02 0x0F ...
Where 0x50
points to the start of memory, and the data written specifies the offset:
Read byte located at offset 0x10 of page 1:
i2c: write 0x10 to 0x50
i2c: read 0x05
Each page has 256 bytes (0x00-0xFF), and based on the service manual, there are 8 pages:
8 x 256 = 2048 => 2KB
A closer inspection of the board reveals a small IC on the bottom side (beneath VCT) that looks like M24C16 or similar EEPROM.
So, we know how to read to E2 memory. Let's see how to write to this EEPROM...
Sniffing a write operation should be straightforward. Just start logic analyzer and open service menu and go to EAROM Editor
, and change some value (for example offset 0x10 for page 1: overwrite 0x36 with 0x37).
Save i2c decoded dump (as terminal data), and look for this sequence:
write to 0x50 ack data: 0x10
You will soon find this:
write to 0x50 ack data: 0x10 0x37
So, to write 1 byte to offset 0x10 at page 1 (first 256 bytes) you need to write two consecutive bytes to that address, where the first byte should point to the offset, and the second byte will be the data to be written
We can now read and write to the external EEPROM.
Now that we know how to read and write bytes to external EEPROM via I2C BUS, let's try to implement a tool to do it using Raspberry Pi and Python.
For more flexibility I will use a library called pigpio, that allows you to communicate through I2C BUS using bit-banging (Software), on any GPIO pin.
A read byte operation looks like this:
write to 0x50 ack data: 0x10
read to 0x50 ack data: 0x36
Or in different words:
1. i2c: write byte_offset to addr
2. i2c: read byte from addr
Let's use bb_i2c_zip function:
bb_i2c_zip
Taking into account the above specs., #1 write would look like this:
# [Set I2C address, addr, start, write, number of bytes to write, offset, stop]
cmd = [4, addr, 2, 7, 1, offset, 3]
And for #2:
# [Set I2C address, addr, start, read, number of bytes to read, stop, End]
cmd = [4, addr, 2, 6, 1, 3, 0]
And by combining both you might end up with this function:
def read_byte_from_ADDR_Offset(self, addr, offset):
self.gpio.bb_i2c_open(self.SDA, self.SCL, self.I2C_SPEED)
cmd = [4, addr, 2, 7, 1, offset, 3]
self.gpio.bb_i2c_zip(self.SDA, cmd)
time.sleep(self.DELAY_READ_BYTE)
cmd = [4, addr, 2, 6, 1, 3, 0]
(count, data) = self.gpio.bb_i2c_zip(self.SDA, cmd)
self.gpio.bb_i2c_close(self.SDA)
return data
Where DELAY_READ_BYTE
can be used to add a small delay between write and read (if necessary)
The main class for your library might look like this:
class VCTI2C(object):
SDA_GPIO_PIN = 2
SCL_GPIO_PIN = 3
FAI_GPIO_PIN = 17
I2C_SPEED = 40000
GPIO_HIGH = 1
GPIO_LOW = 0
DELAY_READ_BYTE = 0.00
def __init__(
self,
SDA=SDA_GPIO_PIN,
SCL=SCL_GPIO_PIN,
FA1=FAI_GPIO_PIN,
I2C_SPEED = I2C_SPEED,
):
self.SDA = SDA
self.SCL = SCL
self.FA1 = FA1
self.I2C_SPEED = I2C_SPEED
# Initialize gpio
self.gpio = pigpio.pi()
# Set input pullups
self.gpio.set_pull_up_down(self.FA1, pigpio.PUD_UP)
self.gpio.set_pull_up_down(self.SDA, pigpio.PUD_UP)
self.gpio.set_pull_up_down(self.SCL, pigpio.PUD_UP)
# i2c pins as input by default
self.gpio.set_mode(self.SDA, pigpio.INPUT)
self.gpio.set_mode(self.SCL, pigpio.INPUT)
self.gpio.set_mode(self.FA1, pigpio.OUTPUT)
Important: For I2C to work, you must pull up both lines (SDA and SCL).
And you can then expose this as a cli-tool using argparse
.
In next section I will explain what FA1
line is.
You might want to connect your RPi GPIO to A8
port and try to read your first byte. If you do this, you will find (as I did) that sometimes you get the expected value, and some other times you don't. You might also get to a point where your TV shuts down and doesn't respond anymore.
$ python vct_cli.py --rbo 0x50 0x10
0x36
$ python vct_cli.py --rbo 0x50 0x10
0x10
$ python vct_cli.py --rbo 0x50 0x10
0xB0
The random behavior we've seen in last test occurs because VCT Controller is constantly using I2C BUS to communicate to all its peripherals (DRX, MSP, VSP, DDP), and you are injecting data while the BUS is busy. This could generate data corruption in the BUS and you can end getting a different value as the one you were expecting, and even worse (this happened to me) you can end up modifying registers and overwriting random memory offsets that could lead to brick your TV.
So, as A8
is a service port, there must be a way to communicate with the TV without interfering with the bus.
A closer look at the schematics shows that A8
port also exposes a pin labeled F1
, and this pin goes to Micronas port P16:
FA1 Schematics
And this pin is also pulled high to 3.3v.
So, this must be used as an input port that triggers some process when pulled low. Let's try that, and connect FA1 to GND while the logic analyzer is capturing data:
Pulling FA1 low
You get "silence" on the bus while FA1 is pulled low (360 ms after you pull it low), and after releasing it, everything seems to return to normal. You will also notice that the OSD disappears while FA1 is low, and the screen "shakes" a bit after release.
A closer look at I2C dump reveals that after releasing FA1
some sections of E2 are accessed. So, to me, this looks like a something similar to a "soft reset", where some settings are restored when FA1
is released.
Let's see what happens if we try to read addr. 0x50 at offset 0x10 while FA1
is low. For this I will add some logic that pulls FA1
low before read operation, and then release. That's FAI_GPIO_PIN
defined at VCTI2C
class:
$ python vct_cli.py --rbo 0x50 0x10
0x36
$ python vct_cli.py --rbo 0x50 0x10
0x36
$ python vct_cli.py --rbo 0x50 0x10
0x36
You get a clean and consistent read!
At this point, I'm not sure what the real usage of FA1
port is, and if pulling it down might have other consequences; but for the moment it allows me to interact with I2C BUS while the TV is on, without generating data corruption.
It should be straightforward to write a tool that dumps the whole E2 memory, as well as writing it back from a binary file.
TODO: add mention to my tool
You can also startup the TV in bootloader
mode. This will allow you to interact with Micronas IC without turning the tube on. In this mode you will also be able to access the firmware through TVT module. This is great for experimentation, as you can feely interact with the IC without worrying about the tube. However, you will not be able to visualize anything in "realtime", as the tube will be off.
To enter bootloader mode you have to pull SCL
low while turning on the TV.
Thanks Ondrej for sharing that information with me.
The cli-tool (that I called vctpi) used above can be found here.
VCT 49xyI has a modular design that includes multiple products within a single IC:
VCT 49xyI Architecture
And everything is connected to i2c bus:
VCT 49xyI I2C BUS
The Addressing for each module is the following:
Block | 8b Write Addr. | 8b Read Addr. | 7b Addr. |
---|---|---|---|
DRX | 0x8E | 0x8F | 0x47 |
MSP | 0x8C | 0x8D | 0x46 |
VSP | 0xB0 | 0xB1 | 0x58 |
DDP | 0xBC | 0xBD | 0x5E |
TVT | 0xD0 | 0xD1 | 0x68 |
Take into account that Logic2
i2c decoder will use 7 bit address, and it will specify if it is a read or write operation.
For example:
write to 0x58 ack data: 0x4B
read to 0x58 ack data: 0x81 0x00
This is writing to address 0x58 (aka 0xB0), which is the VSP (Video Processor).
The datasheet states the following regarding VSP i2c bus interface:
The VSP has 16-bit I2C registers only. The individual registers are addressed by an 8-bit subaddress following the device address.
And if you look for subaddress 0x4B you will find this:
VSP Subaddresses
VSP Subaddress 0x4B
So, we can see from logic analyzer dump that VCT is reading VSP register in charge of storing 4 configurations:
- Offset Adjustment for Fastblank channel (FBLOFFST)
- RGB Matrix (YUVSEL)
- Softmix Mode (SMOP)
- Offset Adjustment for R and B channel (RBOFST)
And it's getting the default value: 0x8100
Which can be translated to:
FBLOFFST (bits 15-10) = b100000 => 32
YUVSEL (bit 8) = b0 => YCrCb/YPrPb
SMOP (bit 7) = b0 => dynamic
RBOFST (bits 2-0) = b000 => RB, pedestal offset visible
Let's have a look at the schematics for rear video inputs:
Rear Video Inputs
Which are connected to VCT IC like this:
Video Inputs VCT
VCT Datasheet states the following about VINs:
VIN 1–11 − Analog Video Input (Fig. 4–15) These are the analog video inputs. A CVBS, S-VHS, YCrCb or RGB/FB signal is converted using the luma, chroma and component AD converters. The input signals must be AC-coupled by 100nF. In case of an analog fast blank signal carrying alpha blending information the input signal must be DC-coupled.
This means that VIN10-8 used in this TV for component video input could be configured as RGB inputs.
So, let's try to find out all the parts involved to achieve this...
RGB or YPbPr signals are converted to the YCrCb format by a matrix operation (YUVMAT). In case of YCrCb input the matrix is bypassed (YUVSEL).
YUVSEL
has these specs:
VSP (0x58) Sub Address: 0x4B[8]
RGB Matrix
0: YCrCb/YPrPb
1: RGB
For RGB it should be set to 1
We need to think about how to send Sync signal. VSP provides many options:
RGB Sync
So, we could send Sync on Green; but I really liked the idea of sending sync through CVBS.
For analog input, there are 4 ADCs available: ADC3, ADC4, ADC5, ADC6
RGB ADCs
And for YCrCb
, the TV is using ADC4 (G/Y). We would like to use ADC6 instead.
ADCSEL
has these specs:
VSP (0x58) Sub Address: 0x49[3]
Select ADC for sync source
0: ADC4
1: ADC6
For FB synchronous to CVBS it should be set to 1
Now, we are just missing one part. We need to some how send CVBS (Composite Video IN 1) to ADC6.
Let's have a look again at VINs:
CVBS Video Input
CVBS is connected to VIN7
So, we need to configure VINSEL
to point to VIN7
if we want to inject sync through Composite Video 1
CVBS Video Input
VINSEL6
has these specs:
VSP (0x58) Sub Address: 0x3f[3:0]
Video Input Select ADC6
0000: off
0001: VIN1
...
0111: VIN7
VINSEL6
must be set to 7
Let's summarize what we need in order to enable RGB through component inputs:
-
VSP Sub Address 0x4B[8]
->YUVSEL = 1
-
VSP Sub Address 0x49[3]
->ADCSEL = 1
-
VSP Sub Address 0x3f[3:0]
->VINSEL6 = 7
This means that I might be able to achieve the goal by changing only 3 bits! (VINSEL7 is probably set to b1111 -> off)
Based on table 2-8, we would also need to set GOFST
and RBOFST
to adjust offset for RGB channels, but let's leave those for later and run a quick test.
My plan to apply the changes will be the following:
Step 1:
a. Turn on TV and set it to AV3
(Component)
b. Read 3 registers involved: 0x4b, 0x49 and 0x3f
c. Write down these three registers and calculate what the value should be for each of them to meet the requirements.
Step 2:
a. Pull FA1 low
b. Take reg values from step #1b, and write the whole 16 bits for each of them
So, let's start by reading the registers:
- Subaddress
0x4b
:
$ python vtc_cli.py --rwas 0x58 0x4b
0x80 0x08
YUVSEL = 1
=> 0x81 0x08
- Subaddress
0x49
:
$ python vtc_cli.py --rwas 0x58 0x49
0xD7 0x54
ADCSEL = 1
=> 0xD7 0x5C
- Subaddress
0x3F
:
$ python vtc_cli.py --rwas 0x58 0x3f
0x4A 0x0F
VINSEL6 = 7
=> 0x4A 0x07
And you can use this Python code to apply those changes:
def write_word_to_addr_subaddr(self, addr, sub_addr, word_high, word_low):
self.gpio.bb_i2c_open(self.SDA, self.SCL, self.I2C_SPEED)
cmd = [4, addr, 2, 7, 3, sub_addr, word_high, word_low, 3, 0]
(count, data) = self.gpio.bb_i2c_zip(self.SDA, cmd)
self.gpio.bb_i2c_close(self.SDA)
return data
vct.write_word_to_addr_subaddr(0x58, 0x4b, 0x81, 0x08)
vct.write_word_to_addr_subaddr(0x58, 0x49, 0xd7, 0x5c)
vct.write_word_to_addr_subaddr(0x58, 0x3f, 0x4a, 0x07)
Let's pull FA1 low and try this out using AMIGA 500 video output...
Amiga 500 test - FA1 pulled low
It works!
However, if you release FA1
back to high, you get a color-distorted image:
Amiga 500 test - FA1 released
This means that some default configurations are being restored as part of this "soft-reset" sequence triggered after releasing FA1
.
A closer look at the analyzer dump revels the guilty:
write to 0x58 ack data: 0x4B 0x80 0x08
The TV is setting YUVSEL
back to YCrCb/YPrPb
I'll need to figure out an alternative way of applying the changes without using FA1
.
Even though I have already been able to get a very clear picture from RGB input, VCT Datasheet states the following:
On the digital side, a correction of the analog clamping value must be performed to reconstruct the black level. This is achieved by RBOFST and GOFST. When using the dynamic softmix-mode with fast-blank, clamping of fast-blank input must be disabled by CLAMP_FBL.
And based on table 2-8
, for RGB with fast-blank, synchronous to CVBS
we should also consider the following:
GOFST (sub 0x47[12:10]) = 0
RBOFST (sub 0x4b[3:1]) = 0
CLAMP_FBL6 (sub 0x43[3]) = 1 (clamping disabled)
Let's read these registers:
GOFST
:
$ python vtc_cli.py --rwas 0x58 0x47
0xAC 0x02
Where 0x47[12:10]
is b011
-> 11: -160 (e.g. G or Y with sync, no pedestal offset visible)
So, to meet that specs. we should set 0x47
to 0xA0 0x02
RBOFST
:
$ python vtc_cli.py --rwas 0x58 0x4b
0x81 0x08
Where 0x4b[3:1]
is b100
-> 100: -255 (CrCb negative pedestal offset)
So, to meet the specs. we should set 0x4b
to 0x81 0x00
CLAMP_FBL6
:
$ python vtc_cli.py --rwas 0x58 0x43
0x00 0x24
Where 0x43[3]
is b0
-> 0: normal clamping
So, to meet the specs. we should set 0x43
to 0x00 0x2C
- I tried changing
CLAMP_FBL6
and I got sync issues - Changing
RBOFST
didn't show any effect at first sight - Changing
GOFST
produces a "green saturated" image
So, taking into account the above tests, I decided to just leave those with default values from YPrPb.
We've seen that pulling FA1
low to apply the changes works for testing, but it will not be useful as a long term solution, as the TV restores the default values for YPrPb
configuration. So, at this point I thought about some alternatives:
For first option I could check if E2 EEPROM contains the values for VSP registers YUVSEL
, ADCSEL
and VINSEL6
. This would be great, as it would allow me to switch from Component to RGB just by going to the service menu and edit the memory using the builtin hex editor. In order to find out if some memory offset contains the values for those registers I could run some "brute-force" script with something like this:
a. read VSP register and store current value
b. Write to memory space N
c. read VSP register and compare current value with previous one,
c.1. if they missmatch then we found the offset that stores the setting we need to change.
c.2. if they match, then continue with next offset (N+1)
d. Loop until N=2048
This is dangerous and I could modify some setting that might damage the TV, but I decided I would give it a try anyway.
I wrote a simple script that executes the sequence described above and I was able to detect an offset that affects VSP subaddress 0x4B
, but anything for YUVSEL
AND VINSEL6
, and I froze the TV multiple times during this process; fortunately I didn't break anything.
So, I was not able to discover a setting stored in EEPROM that controls RGB video inputs; and this makes sense as this chassis has been designed for America market (no SCART), so it makes sense that this configuration is hardcoded in firmware and not as a configurable setting stored in EEPROM.
Another option could be to reverse engineer the firmware, find where this setting is hardcoded and try to inject a condition that could allow me to switch from component to RGB (we don't want to loose component input). This might be doable, but it would be very time consuming; so I discarded it.
We've seen that trying to inject data to i2c bus can lead to data corruption and unpredictable consequences, but maybe I could somehow find a way to write when the bus is stable (SCL and SDA high). So, lest's see how the write sequence at 100 KHz looks like:
RGB write sequence at 100 KHz
And let's have a look at the bus after AV3
has been setup:
BUS after after AV3 setup
There are some "gaps" where the bus is idle (SCL and SDA are high), and the largest dT is about 7 ms
. This should be enough time to inject our sequence at 100KHz. However, in order to detect these "idle gaps" I have to define a minimum threshold (Let's say about 3 ms
); the problem with this approach is that there are a lot of "gaps" between 2 and 4 ms.
I could create some logic to detect a 4 ms
gap, and then start the writing sequence from there. So, I might get lucky and hit the 7 ms
gap and I should be OK, but I could also end up in a gap of 4.3 ms
, and I would be writing data while the bus is busy; so, this approach might work, but it is not deterministic.
At this point I was out of alternatives. I started to read more about advanced i2c communications, and stumbled across the concept of Clock Stretching:
Clock stretching is a method for any I2C device to slow down the bus. The I2C clock speed is controlled by the master, but clock stretching allows all devices to slow down or even halt I2C communication. Clock stretching is performed by holding the SCL line low.
So, let's see how Micronas IC behaves if I force SCL
low for a long time (e.g. 1s):
Clock stretching - Pull SCL Low for 1000 ms
After releasing SCL
I got 3 big "gaps" about 600 ms
each. The image shakes a bit, but I don't get any "soft reset" sequence, and the behavior seems to be deterministic (I always get 3 600 ms
gaps after holding SCL low for more than 200 ms
). So, this is what I was looking for, we can force the IC to generate these big gaps by holding SCL
low and inject our sequence in the middle without worrying about generating data corruption. My approach would be as follows:
- Put TV in
AV3
mode - Pull
SCL
low for about300 ms
- Detect the condition where
SCL
stays high for at least200 ms
- Write RGB config sequence
How can I detect that SCL
stays high for at least 200 ms
? I can't use pigpio
wait_for_edge function because of this:
PIGPIO wait_for_edge
So, I need something faster; I can't rely on this library for this task. Let's try it out with some microcontroller...
I needed a way to detect a condition where SCL
stays high for at least 200 ms
, and then write to the bus. An Arduino should be capable of doing this using interrupts, but I wanted to do it using the most simplest and cheapest device I might have around; the answer was ESP8266
. This microcontroller works with 3.3v
, which is perfect for our use case as A8
i2c lines are pulled high to 3.3v
. So, I first grabbed a NodeMCU
board I had, and wrote a very simple firmware that detects this condition using an interrupt, and then writes to the bus:
// YPbPr2RGB i2c switcher for Panasonic TC-21FX30LA (Micronas VCT-49xyI)
// Ver. 0.1 - July 20204
#include <ESP8266WiFi.h>
#include <esp_i2c.h>
#define SCL_LOW_PULL_MS 300 // Pull down SCL for X ms
#define SCL_HIGH_WAIT_MS 200 // Wait until SCL stays high for at least X ms
#define I2C_CLOCK 50000L // I2C clock 50 KHz
const uint8_t VSP_ADDR = 0x58;
const uint8_t SDA_PIN = D6;
const uint8_t SCL_PIN = D5;
volatile unsigned long sclHighTimer = 0;
volatile bool sclHigh = false;
void ICACHE_RAM_ATTR handleSCLChange() {
if (digitalRead(SCL_PIN) == HIGH) {
sclHighTimer = millis(); // Start the timer when SCL goes high
sclHigh = true;
} else {
sclHigh = false;
}
}
void setup() {
// Pin Setup
pinMode(SDA_PIN, INPUT_PULLUP);
pinMode(SCL_PIN, INPUT_PULLUP);
// Disable WiFi
WiFi.mode(WIFI_OFF);
// I2C Setup
esp_i2c_init(SDA_PIN, SCL_PIN);
esp_i2c_set_clock(I2C_CLOCK);
// Interrupt to detect SCL state change
attachInterrupt(digitalPinToInterrupt(SCL_PIN), handleSCLChange, CHANGE);
// Pull the SCL pin low for 300ms
pinMode(SCL_PIN, OUTPUT);
digitalWrite(SCL_PIN, LOW);
delay(SCL_LOW_PULL_MS);
pinMode(SCL_PIN, INPUT_PULLUP);
// Initialize timer
sclHighTimer = millis();
// Wait until SCL stays high for at least SCL_HIGH_WAIT_MS
while (true) {
if (sclHigh && (millis() - sclHighTimer) > SCL_HIGH_WAIT_MS) {
uint8_t dataToSend[3];
// YUVSEL -> RGB: 0x4b 0x81 0x08
dataToSend[0] = 0x4b;
dataToSend[1] = 0x81;
dataToSend[2] = 0x08;
esp_i2c_write_buf(VSP_ADDR, dataToSend, 3, 1);
// ADC_SEL -> 6: 0x49 0xd7 0x5c
dataToSend[0] = 0x49;
dataToSend[1] = 0xd7;
dataToSend[2] = 0x5c;
esp_i2c_write_buf(VSP_ADDR, dataToSend, 3, 1);
// VINSEL6 -> 7: 0x3f 0x4a 0x07
dataToSend[0] = 0x3f;
dataToSend[1] = 0x4a;
dataToSend[2] = 0x07;
esp_i2c_write_buf(VSP_ADDR, dataToSend, 3, 1);
break;
}
}
}
void loop() {
}
Notice that I am not even using the main loop. As I just need to inject this data once, I thought the best approach would be to just execute everything at setup (when you turn on ESP8266), and then you could turn it off and forget about extra power consumption; In other words, you could turn on ESP8266 for about 3 seconds using a push button and then turn it off by releasing the button. This is what I got using NodeMcu board:
ESP8266 (pull SCL low and then write)
And this is the result with Amiga 500 RGB output connected straight to TV's Component Video Input, and sync is connected to AV1:
NodeMcu injecting data sequence
Notice how the TV can't synchronize the video signal coming from A500 (as it is expecting sync at Y). When ESP8266 writes to VSP registers, AV3 switches to sync from CVBS (composite video in 1), and you get a very nice and sharp image.
The firmware for ESP8266
and installation instructions can be found here.