/* firmware for STM8S-microcontroller-based HDMI firewall programmer * Copyright (C) 2019-2021 King Kévin * SPDX-License-Identifier: GPL-3.0-or-later */ #include #include #include #include "stm8s.h" #include "main.h" #include "softi2c_master.h" #include "eeprom_blockprog.h" // blink RUN LED to show error static bool led_error = false; // set when the button is pressed static volatile bool rw_button_pressed = false; // AWU tick count static volatile uint8_t awu_tick = 0; // to store the current E-EDID (EDID + extension) static uint8_t edid[256]; // function RAM (code in RAM) uint8_t f_ram[112 + 5]; // use RAM_SEG size in main.map (plus some margin) // function for saving EDID + extension to EEPROM, to put in RAM bool (*ram_eeprom_blockprog)(const uint8_t* data, uint16_t length); // size of RAM segment volatile uint8_t RAM_SEG_LEN; // get the size of the RAM segment static inline void get_ram_section_length() { __asm__("mov _RAM_SEG_LEN, #l_RAM_SEG"); } // copy functions to RAM static bool ram_cpy() { get_ram_section_length(); if (RAM_SEG_LEN > ARRAY_LENGTH(f_ram)) { return false; } for (uint8_t i = 0; i < RAM_SEG_LEN; i++) { f_ram[i] = ((uint8_t *)eeprom_blockprog)[i]; } ram_eeprom_blockprog = (bool (*)(const uint8_t* data, uint16_t length)) &f_ram; return true; } // blocking wait (in 10 us steps, up to UINT32_MAX / 10) static void wait_10us(uint32_t us10) { us10 = ((us10 / (1 << CLK->CKDIVR.fields.HSIDIV)) * 1000) / 206; // calibrated for 1 ms while (us10--); // burn energy } void putc(char c) { while (!UART1->SR.fields.TXE); // wait until TX buffer is empty UART1->DR.reg = c; // put character in buffer to be transmitted // don't wait until the transmission is complete } void puts(const char* s) { if (NULL == s) { return; } while (*s) { putc(*s++); IWDG_KR = IWDG_KR_KEY_REFRESH; // reset watchdog } } void putn(uint8_t n) { n &= 0x0f; // ensure it's a nibble if (n < 0xa) { putc('0' + n); } else { putc('a' + (n - 0x0a)); } } void puth(uint8_t h) { putn(h >> 4); putn(h & 0x0f); } // verify (E-)EDID checksums // the sum of the bytes (including checksum at the end) must be 0 static bool checksum_ok(const uint8_t* data, uint16_t length) { uint8_t checksum = 0; for (uint16_t i = 0; i < length; i++) { checksum += data[i]; } return (0 == checksum); } // return if the current EDID is valid /* from HDMI 1.3a specification * All Sinks shall contain an CEA-861-D compliant E-EDID data structure. * A Source shall read the EDID 1.3 and first CEA Extension to determine the capabilities supported by the Sink. * The first 128 bytes of the E-EDID shall contain an EDID 1.3 structure. The contents of this structure shall also meet the requirements of CEA-861-D. * * HDMI 2.1 uses CTA-861-G * this uses EDID 1.4 structure * EDID 1.3/1.4 is 128 bytes long, and can point to 128 bytes extension * EDID 2.0 with its 256 bytes does not seem to be used in HDMI at all * DisplayID with its variable-length structure meant to replace E-EDID only seems to be used in DisplayPort * I have no idea how more than 1 extension is supported since technically the ROM is limited to 256 bytes */ static uint16_t edid_length(void) { puts("EDID check: "); // check EDID 1.3/1.4 fixed pattern header if (0x00 != edid[0] || 0xff != edid[1] || 0xff != edid[2] || 0xff != edid[3] || 0xff != edid[4] || 0xff != edid[5] || 0xff != edid[6] || 0x00 != edid[7]) { puts("invalid header\r\n"); return 0; } if (1 == edid[18]) { // EDID 1.3/1.4 128-byte structure if (checksum_ok(&edid[0], 128)) { if (0 == edid[126]) { // no extension puts("128 bytes\r\n"); return 128; } else { // extension available // the usual extension is CEA EDID Timing Extension (with extension tag 02), but we allow others // no idea how more than 1 extension is supported if (checksum_ok(&edid[128], 128)) { puts("256 bytes (with extension)\r\n"); return 256; } else { puts("extension CRC error\r\n"); return 0; // EDID is broken } } } else { puts("CRC error\r\n"); return 0; } } else if (2 == edid[18]) { // EDID 2.0 256-byte structure if (checksum_ok(&edid[0], 256)) { puts("256 bytes (no extension)\r\n"); return 256; } else { puts("CRC error\r\n"); return 0; } } return 0; } // load EDID + extension from EEPROM static void load_edid(void) { for (uint16_t i = 0; i < ARRAY_LENGTH(edid); i++) { edid[i] = *(uint8_t*)(EEPROM_ADDR + i); } } // save EDID + extension in EEPROM static bool save_edid(void) { // modify EDID to include the character indicating the firewall const char firewall_indicator = '|'; // sun character to indicate the firewall // ensure descriptor 4 is for Display name if ((0 == edid[108]) && (0 == edid[109]) && (0 == edid[110]) && (0xfc == edid[111]) && (0 == edid[112])) { // ensure descriptor 4 is for Display name uint8_t last_c; // position of last character for (last_c = 113; last_c < 126 && edid[last_c] != '\n'; last_c++); // find position for inserting our character if (firewall_indicator != edid[last_c - 1]) { // the last character is not yet the sun if (last_c > 125) { // ensure we insert as the last possible character last_c = 125; } edid[last_c++] = firewall_indicator; // insert sun if (last_c < 126) { edid[last_c++] = '\n'; // insert LF to terminate string } while (last_c < 126) { edid[last_c++] = ' '; // insert padding space } // calculate new checksum uint8_t checksum = 0; for (uint8_t i = 0; i < 127; i++) { checksum += edid[i]; } edid[127] = (256 - checksum); } } return ram_eeprom_blockprog(edid, edid_length()); } // size in byte of a page in the I²C EEPROM (for faster page write) #define I2C_EEPROM_PAGE_SIZE 16U // read EDID from I²C memory // return if succeeded static bool read_edid(void) { if (!softi2c_master_setup(10)) { puts("I²C setup failed"); return false; } // read all pages // in theory I could read the whole address space at once, but in practice short clock pulses appear after some bytes, corrupting the data flow. I have no idea what the cause of theses glitches is (I even used WFI to reduce EMI as recommended in the errata) for (uint16_t i = 0; i < ARRAY_LENGTH(edid); i += I2C_EEPROM_PAGE_SIZE) { const uint8_t address[] = {i}; // address of page to read uint8_t data[I2C_EEPROM_PAGE_SIZE]; // data from page bool same = false; // if the data read is the same while (!same) { // read until the data is the same const bool rc = softi2c_master_address_read(I2C_SLAVE, address, ARRAY_LENGTH(address), data, ARRAY_LENGTH(data)); // read I²C EEPROM data if (!rc) { puts("I²C read failed"); return false; } // check if the data is the same and copy it to EDID same = true; for (uint16_t j = 0; j < I2C_EEPROM_PAGE_SIZE; j++) { if (data[j] != edid[i + j]) { same = false; } edid[i + j] = data[j]; // save last read data } } } softi2c_master_release(); // release I²C again return true; } // write EDID to I²C memory // return if succeeded static bool write_edid(void) { if (!softi2c_master_setup(10)) { puts("I²C setup failed"); return false; } // write all page const uint16_t length = edid_length(); for (uint16_t i = 0; i < length; i += I2C_EEPROM_PAGE_SIZE) { const uint8_t address[] = {i}; const bool rc = softi2c_master_address_write(I2C_SLAVE, address, ARRAY_LENGTH(address), &edid[i], I2C_EEPROM_PAGE_SIZE); // write I²C EEPROM page if (!rc) { puts("I²C write failed"); return false; } wait_10us((5 + 1) * 100); // wait 5 ms for the page write to complete } softi2c_master_release(); // release I²C again return true; } void main(void) { sim(); // disable interrupts (while we reconfigure them) CLK->CKDIVR.fields.HSIDIV = CLK_CKDIVR_HSIDIV_DIV0; // don't divide internal 16 MHz clock CLK->CKDIVR.fields.CPUDIV = CLK_CKDIVR_CPUDIV_DIV0; // don't divide CPU frequency to 16 MHz while (!CLK->ICKR.fields.HSIRDY); // wait for internal oscillator to be ready // save power by disabling unused peripheral CLK_PCKENR1 = CLK_PCKENR1_I2C | CLK_PCKENR1_UART1234; // keep I²C and UART CLK_PCKENR2 = CLK_PCKENR2_AWU; // keep AWU // configure LEDs EDID_LED_PORT->DDR.reg |= EDID_LED_PIN; // switch pin to output EDID_LED_PORT->CR1.reg &= ~EDID_LED_PIN; // use in open-drain mode edid_led_off(); // start with LED off RUN_LED_PORT->DDR.reg |= RUN_LED_PIN; // switch pin to output RUN_LED_PORT->CR1.reg &= ~RUN_LED_PIN; // use in open-drain mode run_led_off(); // start with LED off // configure read/write button RW_BUTTON_PORT->DDR.reg &= ~RW_BUTTON_PIN; // switch pin to input RW_BUTTON_PORT->CR1.reg |= RW_BUTTON_PIN; // pull up RW_BUTTON_PORT->CR2.reg |= RW_BUTTON_PIN; // enable external interrupt EXTI->CR1.fields.PDIS = EXTI_FALLING_EDGE; // interrupt only on falling edges (WARNING hard coded port) // configure auto-wakeup (AWU) to be able to refresh the watchdog // 128 kHz LSI used by default in option bytes CKAWUSEL // we skip measuring the LS clock frequency since there is no need to be precise AWU->TBR.fields.AWUTB = 10; // interval range: 128-256 ms AWU->APR.fields.APR = 0x3e; // set time to 256 ms AWU_CSR |= AWU_CSR_AWUEN; // enable AWU (start only when entering wait or active halt mode) // configure UART for debug output UART1->CR1.fields.M = 0; // 8 data bits UART1->CR3.fields.STOP = 0; // 1 stop bit UART1->BRR2.reg = 0x0B; // set baud rate to 115200 (at 16 MHz) UART1->BRR1.reg = 0x08; // set baud rate to 115200 (at 16 MHz) UART1->CR2.fields.TEN = 1; // enable TX // load function in RAM ram_cpy(); // configure independent watchdog (very loose, just it case the firmware hangs) IWDG->KR.fields.KEY = IWDG_KR_KEY_REFRESH; // reset watchdog IWDG->KR.fields.KEY = IWDG_KR_KEY_ENABLE; // start watchdog IWDG->KR.fields.KEY = IWDG_KR_KEY_ACCESS; // allows changing the prescale IWDG->PR.fields.PR = IWDG_PR_DIV256; // set prescale to longest time (1.02s) IWDG->KR.fields.KEY = IWDG_KR_KEY_REFRESH; // reset watchdog puts("\r\nCuVoodoo HDMI firewall programmer ready\r\n"); // erase saved EDID when button is pressed on boot if (0 == (RW_BUTTON_PORT->IDR.reg & RW_BUTTON_PIN)) { // button is pressed while booting for (uint16_t i = 0; i < ARRAY_LENGTH(edid); i++) { edid[i] = 0; // create empty EDID } ram_eeprom_blockprog(edid, ARRAY_LENGTH(edid)); // erase EDID puts("EEPROM EDID erased\r\n"); } load_edid(); // load EDID from EEPROM bool edid_valid = (0 != edid_length()); // verify if EDID is valid if (edid_valid) { puts("EEPROM EDID valid\r\n"); } else { puts("EEPROM EDID not valid\r\n"); } rim(); // re-enable interrupts bool action = false; // if an action has been performed while (true) { IWDG_KR = IWDG_KR_KEY_REFRESH; // reset watchdog // handle button press if (rw_button_pressed) { wait_10us(10000); // wait 100 ms for the noise to be gone IWDG_KR = IWDG_KR_KEY_REFRESH; // reset watchdog if (0 == (RW_BUTTON_PORT->IDR.reg & RW_BUTTON_PIN)) { // ensure the button is pressed (the pull-up is really weak) run_led_off(); // clear RUN LED to see result at the end uint8_t press_duration = 1; // start counting how long the button is pressed while (0 == (RW_BUTTON_PORT->IDR.reg & RW_BUTTON_PIN)) { // wait until button is depressed wait_10us(10000); // wait for 100 ms press_duration++; IWDG_KR = IWDG_KR_KEY_REFRESH; // reset watchdog if (press_duration >= 30) { // 3 seconds already passed break; // stop waiting for button to be released } } led_error = false; // reset error state if (press_duration < 30) { // less than 3 sec puts("read I²C EDID: "); run_led_on(); // indicate we started if (read_edid()) { // read EDID from I²C puts(" OK\r\n"); edid_valid = (0 != edid_length()); // verify if EDID is valid if (edid_valid) { // read EDID is valid puts("I²C EDID valid\r\n"); IWDG_KR = IWDG_KR_KEY_REFRESH; // reset watchdog /* puts("EDID data:\r\n"); for (uint16_t i = 0; i < ARRAY_LENGTH(edid); i++) { puth(edid[i]); putc(' '); } puts("\r\n"); */ if (save_edid()) {; // save to EEPROM puts("I²C EDID saved to EEPROM\r\n"); } else { led_error = true; // indicate write error puts("could not save EDID to EEPROM\r\n"); load_edid(); // re-load EDID from EEPROM edid_valid = (0 != edid_length()); // verify if EDID is valid } IWDG_KR = IWDG_KR_KEY_REFRESH; // reset watchdog } else { // read EDID is not valid puts("I²C EDID not valid, reloading from EEPROM\r\n"); led_error = true; // indicate read error load_edid(); // re-load EDID from EEPROM edid_valid = (0 != edid_length()); // verify if EDID is valid } } else { // read error puts("\r\n"); // error should have been printed led_error = true; // indicate read error load_edid(); // re-load EDID from EEPROM edid_valid = (0 != edid_length()); // verify if EDID is valid } } else { // button pressed > 3s run_led_on(); // indicate we started if (edid_valid) { // the current EDID we have is valid and is ready to be written led_error = true; // error indication will be cleared if everything succeeded puts("writing I²C EDID: "); if (write_edid()) { // write EDID to I²C EEPROM succeeded puts("OK\r\n"); const uint16_t target_length = edid_length(); // remember the length of the EDID we programmed puts("reading back EDID: "); if (read_edid()) { // read EDID back from I²C puts("OK\r\n"); if (edid_length() == target_length) { // ensure the EDID length between what we programmed and read is the same puts("verifying EDID: "); bool identical_edid = true; // to find out if EDID is identical for (uint16_t i = 0; i < target_length; i++) { // compare EDID if (edid[i] != *(uint8_t*)(EEPROM_ADDR + i)) { // ensure the data is the same identical_edid = false; // EDID is not identical break; // stop comparing } } if (identical_edid) { // EDID has been successfully programmed puts("OK\r\n"); led_error = false; // no error happened } else { puts("failed (wrong data)\r\n"); } } else { puts("failed (not length)\r\n"); } } else { puts("\r\n"); // error should have been printed } puts("reloading EEPROM EDID\r\n"); load_edid(); // re-load EDID from EEPROM after it has been overwritten from the read edid_valid = (0 != edid_length()); // verify if EDID is valid (should be as before } else { puts("\r\n"); // error should have been printed } } else { puts("\r\n"); // error should have been printed led_error = true; // we can't program an invalid EDID } } } action = true; // remember we performed an action rw_button_pressed = false; // clear flag } // update LED if ((awu_tick & 0x7) < 4) { // on period of blink (every second) edid_led_on(); // on period if (led_error) { run_led_on(); // start blinking } } else { if (!edid_valid) { // EDID not valid edid_led_off(); // blink to let user know } if (led_error) { run_led_off(); // blink to indicate error } } if (action) { // something has been performed, check if other flags have been set meanwhile action = false; // clear flag } else { // nothing down wfi(); // go to sleep (wait for any interrupt, also starting AWU) } } } void awu(void) __interrupt(IRQ_AWU) // auto wakeup { volatile uint8_t awuf = AWU_CSR; // clear interrupt flag by reading it (reading is required, and volatile prevents compiler optimization) awu_tick++; // increment tick count // let the main loop kick the dog } void rw_button_isr(void) __interrupt(IRQ_EXTI3) // button pressed { rw_button_pressed = true; // notify main loop }