Skip to content

I2C Data Injection In Panasonic CRT

charlysan edited this page Aug 1, 2024 · 8 revisions

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.

Table of contents

Disclaimer

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.

Tools and equipment used

Sniff and decode i2c data

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

Reading E2 EEPROM (TV Settings)

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...

Writing to E2 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.

Interfacing I2C BUS Using a Raspberry Pi and Python

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.

Trying to read one byte from EEPROM

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

Avoiding data corruption on the bus

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

Bootloader Mode

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.

vctpi cli tool

The cli-tool (that I called vctpi) used above can be found here.

VCT architecture

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

Configure Component Video Input as RGB

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/YPbPr to YCrCb Matrix

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

Synchronization

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

Applying the changes

Let's summarize what we need in order to enable RGB through component inputs:

  1. VSP Sub Address 0x4B[8] -> YUVSEL = 1
  2. VSP Sub Address 0x49[3] -> ADCSEL = 1
  3. 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:

  1. Subaddress 0x4b:
$ python vtc_cli.py --rwas 0x58 0x4b
0x80 0x08

YUVSEL = 1 => 0x81 0x08

  1. Subaddress 0x49:
$ python vtc_cli.py --rwas 0x58 0x49
0xD7 0x54

ADCSEL = 1 => 0xD7 0x5C

  1. 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)

First test using Amiga 500 video output

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.

Clamping

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.

Persisting the changes

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:

Brute Force

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.

Modifying the firmware

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.

Inject data while i2c bus is busy (Clock Stretching)

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:

  1. Put TV in AV3 mode
  2. Pull SCL low for about 300 ms
  3. Detect the condition where SCL stays high for at least 200 ms
  4. 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...

Using ESP8266 to inject data into the bus

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.

Pictures and videos