diff --git a/main.c b/main.c index 13d8e81..cfcd1a7 100644 --- a/main.c +++ b/main.c @@ -1,5 +1,5 @@ -/* firmware template for STM8S microcontroller - * Copyright (C) 2019-2020 King Kévin +/* firmware for STM8S-microcontroller-based HDMI firewall programmer + * Copyright (C) 2019-2021 King Kévin * SPDX-License-Identifier: GPL-3.0-or-later */ #include @@ -8,8 +8,47 @@ #include "stm8s.h" #include "main.h" +#include "i2c_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 +inline void get_ram_section_length() { + __asm__("mov _RAM_SEG_LEN, #l_RAM_SEG"); +} + +// copy functions to RAM +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) @@ -18,6 +57,110 @@ static void wait_10us(uint32_t us10) for (volatile uint32_t t = 0; t < us10; t++); // burn energy } +// 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) +{ + // 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]) { + 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 + 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)) { + return 256; + } else { + return 0; // EDID is broken + } + } + } else { + return 0; + } + } else if (2 == edid[18]) { // EDID 2.0 256-byte structure + if (checksum_ok(&edid[0], 256)) { + return 256; + } else { + 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) +{ + return ram_eeprom_blockprog(edid, edid_length()); +} + +// read EDID from I²C memory +// return if succeeded +static bool read_edid(void) +{ + const uint8_t address[] = {0}; + if (!i2c_master_setup(false)) { + return false; + } + if (I2C_MASTER_RC_NONE != i2c_master_address_read(I2C_SLAVE, false, address, ARRAY_LENGTH(address), edid, ARRAY_LENGTH(edid))) { + return false; + } + i2c_master_release(); // release I²C again + return true; +} + +// write EDID to I²C memory +// return if succeeded +static bool write_edid(void) +{ + const uint8_t address[] = {0}; + if (!i2c_master_setup(false)) { + return false; + } + if (I2C_MASTER_RC_NONE != i2c_master_address_write(I2C_SLAVE, false, address, ARRAY_LENGTH(address), edid, edid_length())) { + return false; + } + i2c_master_release(); // release I²C again + return true; +} + void main(void) { sim(); // disable interrupts (while we reconfigure them) @@ -26,6 +169,24 @@ void main(void) 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; // keep I²C + 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 @@ -33,6 +194,9 @@ void main(void) 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) + // 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 @@ -40,14 +204,81 @@ void main(void) IWDG->PR.fields.PR = IWDG_PR_DIV256; // set prescale to longest time (1.02s) IWDG->KR.fields.KEY = IWDG_KR_KEY_REFRESH; // reset watchdog + // 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 + } + load_edid(); // load EDID from EEPROM + bool edid_valid = (0 != edid_length()); // verify if EDID is valid + 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(100); // wait 1 ms for the noise to be gone + if (0 == (RW_BUTTON_PORT->IDR.reg & RW_BUTTON_PIN)) { // ensure the button is pressed (the pull-up is really weak) + wait_10us(10000); // debounce for 100 ms + 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 + } + led_error = false; // reset error state + if (press_duration < 30) { // less than 3 sec + run_led_on(); // indicate we started + if (read_edid()) { // read EDID from I²C + edid_valid = (0 != edid_length()); // verify if EDID is valid + if (edid_valid) { // read EDID is valid + IWDG_KR = IWDG_KR_KEY_REFRESH; // reset watchdog + led_error = !save_edid(); // save to EEPROM + IWDG_KR = IWDG_KR_KEY_REFRESH; // reset watchdog + } else { // read EDID is not valid + 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 + led_error = true; // indicate read error + load_edid(); // re-load EDID from EEPROM + edid_valid = (0 != edid_length()); // verify if EDID is valid + } + i2c_master_release(); // release I²C again + } else { // button pressed > 3s + run_led_on(); // indicate we started + if (edid_valid) { + led_error = !write_edid(); // write EDID to I²C EEPROM + } else { + 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, including periodic AWU) + wfi(); // go to sleep (wait for any interrupt, also starting AWU) } } } @@ -55,5 +286,11 @@ void main(void) 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 +} diff --git a/main.h b/main.h index f3ea966..d37db0d 100644 --- a/main.h +++ b/main.h @@ -1,2 +1,37 @@ // get length of array #define ARRAY_LENGTH(x) (sizeof(x) / sizeof((x)[0])) + +// LED to indicate valid EDID +// sink to witch on +// off = firmware not running +// on = valid EDID +// blink = no valid EDID +#define EDID_LED_PORT GPIO_PD +#define EDID_LED_PIN PD4 +#define edid_led_on() {EDID_LED_PORT->ODR.reg &= ~EDID_LED_PIN;} +#define edid_led_off() {EDID_LED_PORT->ODR.reg |= EDID_LED_PIN;} +#define edid_led_toggle() {EDID_LED_PORT->ODR.reg ^= EDID_LED_PIN;} + +// LED to indicate operation read/write result +// sink to switch on +// off = no operation started +// on = operation succeeded +// blink = operation failed +#define RUN_LED_PORT GPIO_PD +#define RUN_LED_PIN PD6 +#define run_led_on() {RUN_LED_PORT->ODR.reg &= ~RUN_LED_PIN;} +#define run_led_off() {RUN_LED_PORT->ODR.reg |= RUN_LED_PIN;} +#define run_led_toggle() {RUN_LED_PORT->ODR.reg ^= RUN_LED_PIN; + +// button to start read/write operation +// press shorts it to ground +// short press = read EDID +// long press (> 3s) = write EDID +#define RW_BUTTON_PORT GPIO_PD +#define RW_BUTTON_PIN PD3 + +// start address of EEPROM +#define EEPROM_ADDR 0x4000 + +// address of I²C EEPROM slave device containing EDID information +#define I2C_SLAVE 0x50