From 615a69b616a255e97fa46cad3ef697eac529db83 Mon Sep 17 00:00:00 2001 From: Alexey Melnikov Date: Thu, 14 May 2026 17:57:10 +0800 Subject: [PATCH] Support for CEC * hdmi_cec * Improve HDMI CEC startup * hdmi_cec * Improve HDMI CEC startup * Use ADV7513 interrupt registers for CEC Set ADV7513 main 0xE1 to the CEC I2C map address, 0xE2 to 0x00 to power the CEC block, and 0xE3 |= 0x0E for CEC control setup. Keep HPD forced high through main 0xD6 = 0xC0 after the startup pulse, but clear main 0x94[7] so HPD does not drive the shared HDMI interrupt line. Clear main 0xA1[6] so the monitor-sense/video-active block stays powered; ADV7513 interrupt status registers 0x94-0x97 are only valid while that block is alive. Set main 0x95 = 0x07 to enable only CEC RX-ready interrupts, use main 0x97 to clear RX/TX interrupt latches, and clear 0x96/0x97 at init. Read CEC RX-ready from CEC map 0x49, keep CEC RX enabled with 0x4A = 0x08, release consumed RX slots through 0x4A, and remove fallback polling of RX length registers 0x25/0x37/0x48. Use main 0x97 bits for CEC TX done/retry/arbitration status, keep CEC TX disabled through 0x11 except while sending, and set the CEC clock divider 0x4E = 0x3D. Gate RX handling on fpga_get_hdmi_int() so normal CEC polling no longer performs DDC/CEC I2C reads unless the FPGA HDMI interrupt bit is asserted. Reply to CEC vendor/name discovery so displays can identify MiSTer after registration. --------- Co-authored-by: misteraddons <51079966+misteraddons@users.noreply.github.com> --- MiSTer.ini | 7 +- cfg.cpp | 10 + cfg.h | 5 + hdmi_cec.cpp | 1457 +++++++++++++++++++++++++++++++++++++++++++++++++ hdmi_cec.h | 13 + input.cpp | 22 + input.h | 2 + scheduler.cpp | 34 ++ video.cpp | 23 + video.h | 2 + 10 files changed, 1574 insertions(+), 1 deletion(-) create mode 100644 hdmi_cec.cpp create mode 100644 hdmi_cec.h diff --git a/MiSTer.ini b/MiSTer.ini index 8d340d1..1c9d71e 100644 --- a/MiSTer.ini +++ b/MiSTer.ini @@ -22,10 +22,15 @@ rbf_hide_datecode=0 ; 1 - hides datecodes from rbf file names. Press F2 for q menu_pal=0 ; 1 - PAL mode for menu core hdmi_limited=0 ; 1 - use limited (16..235) color range over HDMI ; 2 - use limited (16..255) color range over HDMI, for VGA converters. +hdmi_cec=0 ; 1 - enable HDMI-CEC via ADV7513, 0 - disable. +hdmi_cec_input_mode=1 ; 0 - ignore CEC remote input, 1 - accept mapped CEC keys. +hdmi_cec_power_on=1 ; 1 - on power-on, try once to make MiSTer the active HDMI input once EDID provides its CEC address. +hdmi_cec_sleep=0 ; 1 - send CEC STANDBY after video_off minutes of no input activity. Requires video_off>0. + ; video_off values 1-4 are presets: 1=15m, 2=30m, 3=45m, 4=60m. +hdmi_cec_wake=0 ; 1 - on input after that idle period, send wake (ImageViewOn/TextViewOn/ActiveSource). direct_video=0 ; 1 - enable core video timing over HDMI, use only with VGA converters. ; 2 - auto-mode for HDMI DACs that report 1024x768 resolution (AG6200, CS5213). ; Power cycle when switching between HDMI displays and DACs. No hot-plug detection. - hdr=0 ; 1 - enable HDR using HLG (recommended for most users) ; 2 - enable HDR using the DCI P3 color space (use color controls to tweak, suggestion: set saturation to 80). fb_size=0 ; 0 - automatic, 1 - full size, 2 - 1/2 of resolution, 4 - 1/4 of resolution. diff --git a/cfg.cpp b/cfg.cpp index 98f75d5..573979d 100644 --- a/cfg.cpp +++ b/cfg.cpp @@ -51,6 +51,11 @@ static const ini_var_t ini_vars[] = { "HDMI_AUDIO_96K", (void*)(&(cfg.hdmi_audio_96k)), UINT8, 0, 1 }, { "DVI_MODE", (void*)(&(cfg.dvi_mode)), UINT8, 0, 1 }, { "HDMI_LIMITED", (void*)(&(cfg.hdmi_limited)), UINT8, 0, 2 }, + { "HDMI_CEC", (void*)(&(cfg.hdmi_cec)), UINT8, 0, 1 }, + { "HDMI_CEC_SLEEP", (void*)(&(cfg.hdmi_cec_sleep)), UINT8, 0, 1 }, + { "HDMI_CEC_WAKE", (void*)(&(cfg.hdmi_cec_wake)), UINT8, 0, 1 }, + { "HDMI_CEC_INPUT_MODE", (void*)(&(cfg.hdmi_cec_input_mode)), UINT8, 0, 1 }, + { "HDMI_CEC_POWER_ON", (void*)(&(cfg.hdmi_cec_power_on)), UINT8, 0, 1 }, { "KBD_NOMOUSE", (void*)(&(cfg.kbd_nomouse)), UINT8, 0, 1 }, { "MOUSE_THROTTLE", (void*)(&(cfg.mouse_throttle)), UINT8, 1, 100 }, { "BOOTSCREEN", (void*)(&(cfg.bootscreen)), UINT8, 0, 1 }, @@ -587,7 +592,12 @@ void cfg_parse() cfg.rumble = 1; cfg.wheel_force = 50; cfg.dvi_mode = 2; + cfg.hdmi_cec = 0; + cfg.hdmi_cec_sleep = 0; + cfg.hdmi_cec_wake = 0; cfg.lookahead = 1; + cfg.hdmi_cec_input_mode = 1; + cfg.hdmi_cec_power_on = 1; cfg.hdr = 0; cfg.hdr_max_nits = 1000; cfg.hdr_avg_nits = 250; diff --git a/cfg.h b/cfg.h index 9a14fe3..1d3a8e4 100644 --- a/cfg.h +++ b/cfg.h @@ -19,6 +19,11 @@ typedef struct { uint8_t hdmi_audio_96k; uint8_t dvi_mode; uint8_t hdmi_limited; + uint8_t hdmi_cec; + uint8_t hdmi_cec_sleep; + uint8_t hdmi_cec_wake; + uint8_t hdmi_cec_input_mode; + uint8_t hdmi_cec_power_on; uint8_t direct_video; uint8_t video_info; float refresh_min; diff --git a/hdmi_cec.cpp b/hdmi_cec.cpp new file mode 100644 index 0000000..3f69a94 --- /dev/null +++ b/hdmi_cec.cpp @@ -0,0 +1,1457 @@ +#include +#include +#include +#include + +#include "hdmi_cec.h" +#include "cfg.h" +#include "fpga_io.h" +#include "hardware.h" +#include "input.h" +#include "smbus.h" +#include "video.h" + +static const uint8_t ADV7513_MAIN_ADDR = 0x39; +static const uint8_t ADV7513_CEC_ADDR = 0x3C; +static const uint8_t ADV7513_EDID_ADDR = 0x3F; + +static const uint8_t MAIN_REG_CEC_I2C_ADDR = 0xE1; +static const uint8_t MAIN_REG_CEC_POWER = 0xE2; +static const uint8_t MAIN_REG_CEC_CTRL = 0xE3; +static const uint8_t MAIN_REG_POWER2 = 0xD6; +static const uint8_t MAIN_REG_MONITOR_SENSE = 0xA1; +static const uint8_t MAIN_REG_HDMI_CFG = 0xAF; +static const uint8_t MAIN_REG_INT0_ENABLE = 0x94; +static const uint8_t MAIN_REG_INT1_ENABLE = 0x95; +static const uint8_t MAIN_REG_INT0_STATUS = 0x96; +static const uint8_t MAIN_REG_INT1_STATUS = 0x97; +static const uint8_t MAIN_INT_EDID_READY = 1 << 2; +static const uint8_t MAIN_REG_EDID_SEGMENT = 0xC4; +static const uint8_t MAIN_REG_EDID_CTRL = 0xC9; + +static const uint8_t CEC_REG_TX_FRAME_HEADER = 0x00; +static const uint8_t CEC_REG_TX_FRAME_DATA0 = 0x01; +static const uint8_t CEC_REG_TX_FRAME_LENGTH = 0x10; +static const uint8_t CEC_REG_TX_ENABLE = 0x11; +static const uint8_t CEC_REG_TX_RETRY = 0x12; +static const uint8_t CEC_REG_RX1_FRAME_HEADER = 0x15; +static const uint8_t CEC_REG_RX2_FRAME_HEADER = 0x27; +static const uint8_t CEC_REG_RX3_FRAME_HEADER = 0x38; +static const uint8_t CEC_REG_RX1_FRAME_LENGTH = 0x25; +static const uint8_t CEC_REG_RX2_FRAME_LENGTH = 0x37; +static const uint8_t CEC_REG_RX3_FRAME_LENGTH = 0x48; +static const uint8_t CEC_REG_RX_STATUS = 0x26; +static const uint8_t CEC_REG_RX_READY = 0x49; +static const uint8_t CEC_REG_RX_BUFFERS = 0x4A; +static const uint8_t CEC_REG_LOG_ADDR_MASK = 0x4B; +static const uint8_t CEC_REG_LOG_ADDR_0_1 = 0x4C; +static const uint8_t CEC_REG_LOG_ADDR_2 = 0x4D; +static const uint8_t CEC_REG_CLK_DIV = 0x4E; +static const uint8_t CEC_REG_SOFT_RESET = 0x50; + +static const uint8_t CEC_INT_RX_RDY1 = 1 << 0; +static const uint8_t CEC_INT_RX_RDY2 = 1 << 1; +static const uint8_t CEC_INT_RX_RDY3 = 1 << 2; +static const uint8_t CEC_INT_RX_RDY_MASK = CEC_INT_RX_RDY1 | CEC_INT_RX_RDY2 | CEC_INT_RX_RDY3; +static const uint8_t CEC_INT_TX_RETRY_TIMEOUT = 1 << 3; +static const uint8_t CEC_INT_TX_ARBITRATION = 1 << 4; +static const uint8_t CEC_INT_TX_DONE = 1 << 5; +static const uint8_t CEC_INT_TX_MASK = CEC_INT_TX_RETRY_TIMEOUT | CEC_INT_TX_ARBITRATION | CEC_INT_TX_DONE; + +static const uint8_t CEC_LOG_ADDR_TV = 0; +static const uint8_t CEC_LOG_ADDR_PLAYBACK1 = 4; +static const uint8_t CEC_LOG_ADDR_PLAYBACK2 = 8; +static const uint8_t CEC_LOG_ADDR_PLAYBACK3 = 11; +static const uint8_t CEC_LOG_ADDR_BROADCAST = 15; + +static const uint8_t CEC_OPCODE_IMAGE_VIEW_ON = 0x04; +static const uint8_t CEC_OPCODE_TEXT_VIEW_ON = 0x0D; +static const uint8_t CEC_OPCODE_STANDBY = 0x36; +static const uint8_t CEC_OPCODE_USER_CONTROL_PRESSED = 0x44; +static const uint8_t CEC_OPCODE_USER_CONTROL_RELEASED = 0x45; +static const uint8_t CEC_OPCODE_GIVE_OSD_NAME = 0x46; +static const uint8_t CEC_OPCODE_SET_OSD_NAME = 0x47; +static const uint8_t CEC_OPCODE_ACTIVE_SOURCE = 0x82; +static const uint8_t CEC_OPCODE_GIVE_PHYSICAL_ADDRESS = 0x83; +static const uint8_t CEC_OPCODE_REPORT_PHYSICAL_ADDRESS = 0x84; +static const uint8_t CEC_OPCODE_REQUEST_ACTIVE_SOURCE = 0x85; +static const uint8_t CEC_OPCODE_SET_STREAM_PATH = 0x86; +static const uint8_t CEC_OPCODE_DEVICE_VENDOR_ID = 0x87; +static const uint8_t CEC_OPCODE_GIVE_DEVICE_VENDOR_ID = 0x8C; +static const uint8_t CEC_OPCODE_MENU_REQUEST = 0x8D; +static const uint8_t CEC_OPCODE_MENU_STATUS = 0x8E; +static const uint8_t CEC_OPCODE_GIVE_DEVICE_POWER_STATUS = 0x8F; +static const uint8_t CEC_OPCODE_REPORT_POWER_STATUS = 0x90; +static const uint8_t CEC_OPCODE_CEC_VERSION = 0x9E; +static const uint8_t CEC_OPCODE_GET_CEC_VERSION = 0x9F; + +static const uint8_t CEC_USER_CONTROL_SELECT = 0x00; +static const uint8_t CEC_USER_CONTROL_UP = 0x01; +static const uint8_t CEC_USER_CONTROL_DOWN = 0x02; +static const uint8_t CEC_USER_CONTROL_LEFT = 0x03; +static const uint8_t CEC_USER_CONTROL_RIGHT = 0x04; +static const uint8_t CEC_USER_CONTROL_ROOT_MENU = 0x09; +static const uint8_t CEC_USER_CONTROL_SETUP_MENU = 0x0A; +static const uint8_t CEC_USER_CONTROL_CONTENTS_MENU = 0x0B; +static const uint8_t CEC_USER_CONTROL_FAVORITE_MENU = 0x0C; +static const uint8_t CEC_USER_CONTROL_EXIT = 0x0D; +static const uint8_t CEC_USER_CONTROL_MEDIA_TOP_MENU = 0x10; +static const uint8_t CEC_USER_CONTROL_MEDIA_CONTEXT_MENU = 0x11; +static const uint8_t CEC_USER_CONTROL_NUMBER_0 = 0x20; +static const uint8_t CEC_USER_CONTROL_NUMBER_1 = 0x21; +static const uint8_t CEC_USER_CONTROL_NUMBER_2 = 0x22; +static const uint8_t CEC_USER_CONTROL_NUMBER_3 = 0x23; +static const uint8_t CEC_USER_CONTROL_NUMBER_4 = 0x24; +static const uint8_t CEC_USER_CONTROL_NUMBER_5 = 0x25; +static const uint8_t CEC_USER_CONTROL_NUMBER_6 = 0x26; +static const uint8_t CEC_USER_CONTROL_NUMBER_7 = 0x27; +static const uint8_t CEC_USER_CONTROL_NUMBER_8 = 0x28; +static const uint8_t CEC_USER_CONTROL_NUMBER_9 = 0x29; +static const uint8_t CEC_USER_CONTROL_INPUT_SELECT = 0x34; +static const uint8_t CEC_USER_CONTROL_DISPLAY_INFO = 0x35; +static const uint8_t CEC_USER_CONTROL_HELP = 0x36; +static const uint8_t CEC_USER_CONTROL_PLAY = 0x44; +static const uint8_t CEC_USER_CONTROL_STOP = 0x45; +static const uint8_t CEC_USER_CONTROL_PAUSE = 0x46; +static const uint8_t CEC_USER_CONTROL_REWIND = 0x48; +static const uint8_t CEC_USER_CONTROL_FAST_FORWARD = 0x49; +static const uint8_t CEC_USER_CONTROL_EPG = 0x53; +static const uint8_t CEC_USER_CONTROL_INITIAL_CONFIGURATION = 0x55; +static const uint8_t CEC_USER_CONTROL_SELECT_MEDIA_FUNCTION = 0x68; +static const uint8_t CEC_USER_CONTROL_SELECT_AV_INPUT_FUNCTION = 0x69; +static const uint8_t CEC_USER_CONTROL_F1_BLUE = 0x71; +static const uint8_t CEC_USER_CONTROL_F2_RED = 0x72; +static const uint8_t CEC_USER_CONTROL_F3_GREEN = 0x73; +static const uint8_t CEC_USER_CONTROL_F4_YELLOW = 0x74; + +static const uint8_t CEC_DEVICE_TYPE_PLAYBACK = 4; +static const uint8_t CEC_POWER_STATUS_ON = 0x00; +static const uint8_t CEC_VERSION_1_4 = 0x05; +static const char *CEC_DEFAULT_OSD_NAME = "MiSTer"; + +static const uint8_t CEC_INPUT_MODE_OFF = 0; +static const uint8_t CEC_INPUT_MODE_ON = 1; + +static const uint16_t CEC_INVALID_PHYS_ADDR = 0xFFFF; +static const unsigned long CEC_BUTTON_TIMEOUT_MS = 500; +static const unsigned long CEC_MAIN_REFRESH_MS = 2000; +static const unsigned long CEC_POLL_INTERVAL_MS = 20; +static const unsigned long CEC_EDID_RETRY_INITIAL_MS = 15000; +static const unsigned long CEC_EDID_RETRY_MAX_MS = 300000; +static const unsigned long CEC_EDID_READY_TIMEOUT_MS = 500; +static const unsigned long CEC_ADVERTISE_STEP_MS = 120; +static const unsigned long CEC_EDID_AFTER_SWITCH_RETRY_MS = 1000; +static const unsigned long CEC_POWER_ON_QUERY_WAIT_MS = 700; +static const unsigned long CEC_POWER_ON_STEP_MS = 120; +static const unsigned long CEC_POWER_ON_VERIFY_WAIT_MS = 700; +static const unsigned long CEC_TX_TIMEOUT_MS = 220; +static const unsigned long CEC_TX_TIMEOUT_RETRY_MS = 500; +static const size_t CEC_EDID_BLOCK_SIZE = 128; +static const size_t CEC_EDID_SEGMENT_SIZE = 256; +static const size_t CEC_EDID_MAX_BLOCKS = 8; +static const uint8_t CEC_ADVERTISE_STARTUP_ATTEMPTS = 1; +static const uint8_t CEC_ADVERTISE_IDENTITY_STEPS = 2; + +typedef struct +{ + uint8_t header; + uint8_t opcode; + uint8_t data[14]; + uint8_t length; +} cec_message_t; + +enum cec_tx_result_t +{ + CEC_TX_RESULT_OK = 0, + CEC_TX_RESULT_NACK, + CEC_TX_RESULT_TIMEOUT +}; + +enum cec_power_on_state_t +{ + CEC_POWER_ON_DONE = 0, + CEC_POWER_ON_REQUEST_BEFORE, + CEC_POWER_ON_WAIT_BEFORE, + CEC_POWER_ON_IMAGE, + CEC_POWER_ON_TEXT, + CEC_POWER_ON_ACTIVE, + CEC_POWER_ON_REQUEST_VERIFY, + CEC_POWER_ON_WAIT_VERIFY +}; + +static bool cec_enabled = false; +static int cec_main_fd = -1; +static int cec_fd = -1; +static uint8_t cec_logical_addr = CEC_LOG_ADDR_PLAYBACK1; +static uint16_t cec_physical_addr = CEC_INVALID_PHYS_ADDR; +static uint16_t cec_pressed_key = 0; +static unsigned long cec_press_deadline = 0; +static unsigned long cec_refresh_deadline = 0; +static unsigned long cec_poll_deadline = 0; +static unsigned long cec_edid_retry_deadline = 0; +static unsigned long cec_edid_retry_delay_ms = 0; +static bool cec_hpd_pulsed = false; +static bool cec_physical_addr_from_edid = false; +static unsigned long cec_reply_phys_deadline = 0; +static unsigned long cec_reply_name_deadline = 0; +static unsigned long cec_reply_version_deadline = 0; +static unsigned long cec_reply_power_deadline = 0; +static unsigned long cec_reply_menu_deadline = 0; +static unsigned long cec_reply_active_deadline = 0; +static unsigned long cec_reply_vendor_deadline = 0; +static uint8_t cec_tx_fail_streak = 0; +static unsigned long cec_tx_suppress_deadline = 0; +static unsigned long cec_advertise_deadline = 0; +static uint8_t cec_advertise_step = 0; +static uint8_t cec_advertise_attempts = 0; +static bool cec_startup_actions_scheduled = false; +static cec_power_on_state_t cec_power_on_state = CEC_POWER_ON_DONE; +static unsigned long cec_power_on_deadline = 0; +static bool cec_reload_video_after_edid = false; +static uint16_t cec_active_physical_addr = 0xFFFF; +static uint32_t cec_input_activity_seq = 0; +static unsigned long cec_idle_deadline = 0; +static bool cec_idle_engaged = false; + +static bool cec_send_message(const cec_message_t *msg, bool with_retry = true); +static bool cec_has_physical_address(void); +static bool cec_send_image_view_on(bool with_retry = true); +static bool cec_send_text_view_on(bool with_retry = true); +static bool cec_send_active_source(bool with_retry = true); +static bool cec_send_request_active_source(bool with_retry = true); +static bool cec_send_report_physical_address(bool with_retry = true); +static bool cec_send_set_osd_name(const char *name, bool with_retry = true); +static bool cec_send_cec_version(uint8_t destination); +static void cec_handle_message(const cec_message_t *msg); +static bool cec_receive_message(cec_message_t *msg); + +static bool cec_debug_enabled(void) +{ + return cfg.debug != 0; +} + +static uint8_t cec_get_input_mode(void) +{ + if (cfg.hdmi_cec_input_mode <= CEC_INPUT_MODE_ON) return cfg.hdmi_cec_input_mode; + return CEC_INPUT_MODE_ON; +} + +static bool cec_is_osd_trigger_button(uint8_t button_code) +{ + return button_code == CEC_USER_CONTROL_EXIT || button_code == CEC_USER_CONTROL_ROOT_MENU; +} + +static bool cec_accept_button_input(uint8_t button_code) +{ + (void)button_code; + uint8_t mode = cec_get_input_mode(); + return mode != CEC_INPUT_MODE_OFF; +} + +static unsigned long cec_idle_sleep_delay_ms(void) +{ + // Tie CEC sleep/wake to the existing video_off timeout. + if (!cfg.video_off) return 0; + + // Match the same preset mapping as video idle blanking: + // 1=15m, 2=30m, 3=45m, 4=60m. + unsigned long minutes = (unsigned long)cfg.video_off; + if (cfg.video_off <= 4) minutes = (unsigned long)cfg.video_off * 15ul; + + return minutes * 60ul * 1000ul; +} + +static const char *cec_get_osd_name(void) +{ + return CEC_DEFAULT_OSD_NAME; +} + +static bool cec_rate_limit(unsigned long *deadline, unsigned long interval_ms) +{ + if (!deadline) return false; + if (!CheckTimer(*deadline)) return false; + *deadline = GetTimer(interval_ms); + return true; +} + +static uint8_t cec_reg_read(uint8_t reg) +{ + if (cec_fd < 0) return 0; + + int value = i2c_smbus_read_byte_data(cec_fd, reg); + return (value < 0) ? 0 : (uint8_t)value; +} + +static bool cec_reg_write(uint8_t reg, uint8_t value) +{ + if (cec_fd < 0) return false; + return i2c_smbus_write_byte_data(cec_fd, reg, value) >= 0; +} + +static uint8_t main_reg_read(uint8_t reg) +{ + if (cec_main_fd < 0) return 0; + + int value = i2c_smbus_read_byte_data(cec_main_fd, reg); + return (value < 0) ? 0 : (uint8_t)value; +} + +static bool main_reg_write(uint8_t reg, uint8_t value) +{ + if (cec_main_fd < 0) return false; + return i2c_smbus_write_byte_data(cec_main_fd, reg, value) >= 0; +} + +static void cec_release_key(void) +{ + if (!cec_pressed_key) return; + + input_cec_send_key(cec_pressed_key, false); + cec_pressed_key = 0; + cec_press_deadline = 0; +} + +static uint16_t cec_button_to_key(uint8_t button_code) +{ + if (cec_is_osd_trigger_button(button_code)) return KEY_MENU; + + switch (button_code) + { + case CEC_USER_CONTROL_UP: return KEY_UP; + case CEC_USER_CONTROL_DOWN: return KEY_DOWN; + case CEC_USER_CONTROL_LEFT: return KEY_LEFT; + case CEC_USER_CONTROL_RIGHT: return KEY_RIGHT; + case CEC_USER_CONTROL_SELECT: return KEY_ENTER; + case CEC_USER_CONTROL_ROOT_MENU: + case CEC_USER_CONTROL_SETUP_MENU: + case CEC_USER_CONTROL_CONTENTS_MENU: + case CEC_USER_CONTROL_FAVORITE_MENU: + case CEC_USER_CONTROL_MEDIA_TOP_MENU: + case CEC_USER_CONTROL_MEDIA_CONTEXT_MENU: + case CEC_USER_CONTROL_INPUT_SELECT: + case CEC_USER_CONTROL_DISPLAY_INFO: + case CEC_USER_CONTROL_HELP: + case CEC_USER_CONTROL_EPG: + case CEC_USER_CONTROL_INITIAL_CONFIGURATION: + case CEC_USER_CONTROL_SELECT_MEDIA_FUNCTION: + case CEC_USER_CONTROL_SELECT_AV_INPUT_FUNCTION: + return 0; + case CEC_USER_CONTROL_EXIT: return KEY_ESC; + case CEC_USER_CONTROL_PLAY: + case CEC_USER_CONTROL_PAUSE: return KEY_SPACE; + case CEC_USER_CONTROL_STOP: return KEY_S; + case CEC_USER_CONTROL_REWIND: return KEY_R; + case CEC_USER_CONTROL_FAST_FORWARD: return KEY_F; + case CEC_USER_CONTROL_NUMBER_0: return KEY_0; + case CEC_USER_CONTROL_NUMBER_1: return KEY_1; + case CEC_USER_CONTROL_NUMBER_2: return KEY_2; + case CEC_USER_CONTROL_NUMBER_3: return KEY_3; + case CEC_USER_CONTROL_NUMBER_4: return KEY_4; + case CEC_USER_CONTROL_NUMBER_5: return KEY_5; + case CEC_USER_CONTROL_NUMBER_6: return KEY_6; + case CEC_USER_CONTROL_NUMBER_7: return KEY_7; + case CEC_USER_CONTROL_NUMBER_8: return KEY_8; + case CEC_USER_CONTROL_NUMBER_9: return KEY_9; + default: return 0; + } +} + +static void cec_handle_button(uint8_t button_code, bool pressed) +{ + if (!pressed) + { + cec_release_key(); + return; + } + + const uint16_t key = cec_button_to_key(button_code); + if (!cec_accept_button_input(button_code)) return; + if (!key) return; + + if (cec_pressed_key && cec_pressed_key != key) + { + cec_release_key(); + } + + if (!cec_pressed_key) + { + input_cec_send_key(key, true); + cec_pressed_key = key; + } + + cec_press_deadline = GetTimer(CEC_BUTTON_TIMEOUT_MS); +} + +static void cec_poll_key_timeout(void) +{ + if (cec_pressed_key && CheckTimer(cec_press_deadline)) + { + cec_release_key(); + } +} + +static void cec_poll_idle_sleep_wake(void) +{ + // Global idle detector based on real input activity (not just OSD/menu navigation), + // tied to global idle blanking ("video_off") timing. + if (!cfg.hdmi_cec_sleep && !cfg.hdmi_cec_wake) return; + + unsigned long delay_ms = cec_idle_sleep_delay_ms(); + if (!delay_ms) + { + cec_idle_deadline = 0; + cec_idle_engaged = false; + cec_input_activity_seq = input_activity_get_seq(); + return; + } + + if (!cec_idle_deadline) + { + cec_input_activity_seq = input_activity_get_seq(); + cec_idle_deadline = GetTimer(delay_ms); + } + + uint32_t seq = input_activity_get_seq(); + if (seq != cec_input_activity_seq) + { + cec_input_activity_seq = seq; + cec_idle_deadline = GetTimer(delay_ms); + + if (cec_idle_engaged) + { + cec_idle_engaged = false; + if (cfg.hdmi_cec_wake) cec_send_wake(); + } + + return; + } + + if (!cec_idle_engaged && CheckTimer(cec_idle_deadline)) + { + cec_idle_engaged = true; + if (cfg.hdmi_cec_sleep) + { + // Avoid powering off the display if MiSTer isn't the active source. + if (cec_active_physical_addr == cec_physical_addr) + { + cec_send_standby(); + } + } + } +} + +static cec_tx_result_t cec_wait_for_tx(unsigned long timeout_ms) +{ + unsigned long timeout = GetTimer(timeout_ms); + uint8_t low_drv_start = cec_reg_read(0x14); + + while (!CheckTimer(timeout)) + { + uint8_t status = main_reg_read(MAIN_REG_INT1_STATUS); + + if (status & (CEC_INT_TX_RETRY_TIMEOUT | CEC_INT_TX_ARBITRATION)) + { + cec_reg_write(CEC_REG_TX_ENABLE, 0x00); + main_reg_write(MAIN_REG_INT1_STATUS, status & CEC_INT_TX_MASK); + return CEC_TX_RESULT_NACK; + } + + if (status & CEC_INT_TX_DONE) + { + cec_reg_write(CEC_REG_TX_ENABLE, 0x00); + main_reg_write(MAIN_REG_INT1_STATUS, CEC_INT_TX_DONE); + return CEC_TX_RESULT_OK; + } + + uint8_t tx_en = cec_reg_read(CEC_REG_TX_ENABLE); + uint8_t low_drv_now = cec_reg_read(0x14); + + if (low_drv_now != low_drv_start && tx_en == 0) + { + return CEC_TX_RESULT_OK; + } + + usleep(2000); + } + + uint8_t low_drv_end = cec_reg_read(0x14); + + if (low_drv_end != low_drv_start) + { + return CEC_TX_RESULT_OK; + } + + cec_reg_write(CEC_REG_TX_ENABLE, 0x00); + main_reg_write(MAIN_REG_INT1_STATUS, CEC_INT_TX_MASK); + return CEC_TX_RESULT_TIMEOUT; +} + +static bool cec_send_message(const cec_message_t *msg, bool with_retry) +{ + if (!cec_enabled || !msg) return false; + if (msg->length < 1 || msg->length > 16) return false; + if (!CheckTimer(cec_tx_suppress_deadline)) return false; + + cec_reg_write(CEC_REG_TX_ENABLE, 0x00); + main_reg_write(MAIN_REG_INT1_STATUS, CEC_INT_TX_MASK); + + cec_reg_write(CEC_REG_TX_FRAME_HEADER, msg->header); + if (msg->length > 1) + { + cec_reg_write(CEC_REG_TX_FRAME_DATA0, msg->opcode); + for (uint8_t i = 0; i < (msg->length - 2); i++) + { + cec_reg_write(CEC_REG_TX_FRAME_DATA0 + 1 + i, msg->data[i]); + } + } + + cec_reg_write(CEC_REG_TX_FRAME_LENGTH, msg->length); + cec_reg_write(CEC_REG_TX_RETRY, with_retry ? 0x20 : 0x00); + cec_reg_write(CEC_REG_TX_ENABLE, 0x01); + + cec_tx_result_t tx_res = cec_wait_for_tx(with_retry ? CEC_TX_TIMEOUT_RETRY_MS : CEC_TX_TIMEOUT_MS); + + if (tx_res == CEC_TX_RESULT_OK) + { + cec_tx_fail_streak = 0; + } + else if (tx_res == CEC_TX_RESULT_NACK) + { + if (cec_tx_fail_streak < 255) cec_tx_fail_streak++; + if (cec_tx_fail_streak >= 8) + { + cec_tx_suppress_deadline = GetTimer(15000); + cec_tx_fail_streak = 0; + if (cec_debug_enabled()) + { + printf("CEC: TX suppressed for 15000ms after repeated failures\n"); + } + } + } + + // Treat timeout without explicit NACK/arbitration as uncertain success. + return tx_res != CEC_TX_RESULT_NACK; +} + +static uint8_t cec_pick_logical_address_from_physical(uint16_t physical_addr) +{ + uint8_t port = (physical_addr >> 12) & 0x0F; + if (port == 2) return CEC_LOG_ADDR_PLAYBACK2; + if (port >= 3) return CEC_LOG_ADDR_PLAYBACK3; + return CEC_LOG_ADDR_PLAYBACK1; +} + +static void cec_program_logical_address(uint8_t addr) +{ + cec_logical_addr = addr & 0x0F; + cec_reg_write(CEC_REG_LOG_ADDR_MASK, 0x10); + cec_reg_write(CEC_REG_LOG_ADDR_0_1, (uint8_t)((0x0F << 4) | cec_logical_addr)); + cec_reg_write(CEC_REG_LOG_ADDR_2, 0x0F); +} + +static void cec_clear_rx_buffers(void) +{ + cec_reg_write(CEC_REG_RX_BUFFERS, 0x0F); + cec_reg_write(CEC_REG_RX_BUFFERS, 0x08); +} + +static bool cec_setup_main_registers(bool clear_status = false) +{ + if (cec_main_fd < 0) return false; + + bool ok = true; + + ok &= main_reg_write(MAIN_REG_CEC_I2C_ADDR, ADV7513_CEC_ADDR << 1); + ok &= main_reg_write(MAIN_REG_CEC_POWER, 0x00); + uint8_t reg_e3 = main_reg_read(MAIN_REG_CEC_CTRL); + ok &= main_reg_write(MAIN_REG_CEC_CTRL, reg_e3 | 0x0E); + + if (!cec_hpd_pulsed) + { + ok &= main_reg_write(MAIN_REG_POWER2, 0x00); + usleep(100000); + ok &= main_reg_write(MAIN_REG_POWER2, 0xC0); + usleep(100000); + cec_hpd_pulsed = true; + } + else + { + ok &= main_reg_write(MAIN_REG_POWER2, 0xC0); + } + + uint8_t reg_a1 = main_reg_read(MAIN_REG_MONITOR_SENSE); + ok &= main_reg_write(MAIN_REG_MONITOR_SENSE, reg_a1 & ~0x40); + + uint8_t reg_af = main_reg_read(MAIN_REG_HDMI_CFG); + uint8_t reg_af_new = (uint8_t)((reg_af & 0x9C) | 0x06); + ok &= main_reg_write(MAIN_REG_HDMI_CFG, reg_af_new); + + uint8_t reg_94 = main_reg_read(MAIN_REG_INT0_ENABLE); + ok &= main_reg_write(MAIN_REG_INT0_ENABLE, reg_94 & ~0x80); + ok &= main_reg_write(MAIN_REG_INT1_ENABLE, CEC_INT_RX_RDY_MASK); + if (clear_status) + { + ok &= main_reg_write(MAIN_REG_INT0_STATUS, 0xFF); + ok &= main_reg_write(MAIN_REG_INT1_STATUS, 0xFF); + } + + if (!ok) + { + if (cec_debug_enabled()) + { + printf("CEC: main register setup failed\n"); + } + } + + return ok; +} + +static bool cec_parse_physical_address(const uint8_t *edid, size_t size, uint16_t *physical_addr) +{ + if (!edid || !physical_addr || size < 256) return false; + + if (edid[0] != 0x00 || edid[1] != 0xFF || edid[2] != 0xFF || edid[3] != 0xFF || + edid[4] != 0xFF || edid[5] != 0xFF || edid[6] != 0xFF || edid[7] != 0x00) + { + return false; + } + + uint8_t ext_count = edid[126]; + for (uint8_t ext = 0; ext < ext_count; ext++) + { + size_t blk_off = 128 * (ext + 1); + if (blk_off + 128 > size) break; + + const uint8_t *blk = &edid[blk_off]; + if (blk[0] != 0x02) continue; + + int dtd_offset = blk[2]; + if (dtd_offset < 4 || dtd_offset > 127) continue; + + int pos = 4; + while (pos < dtd_offset) + { + uint8_t tag_len = blk[pos]; + int tag = (tag_len >> 5) & 0x07; + int len = tag_len & 0x1F; + if (pos + 1 + len > 127) break; + + if (tag == 0x03 && len >= 5) + { + if (blk[pos + 1] == 0x03 && blk[pos + 2] == 0x0C && blk[pos + 3] == 0x00) + { + *physical_addr = (uint16_t)((blk[pos + 4] << 8) | blk[pos + 5]); + return true; + } + } + + pos += len + 1; + } + } + + return false; +} + +static bool cec_parse_physical_address_loose(const uint8_t *edid, size_t size, uint16_t *physical_addr) +{ + if (!edid || !physical_addr || size < 8) return false; + + for (size_t i = 0; i + 4 < size; i++) + { + if (edid[i] == 0x03 && edid[i + 1] == 0x0C && edid[i + 2] == 0x00) + { + uint16_t addr = (uint16_t)((edid[i + 3] << 8) | edid[i + 4]); + if (addr != 0x0000 && addr != 0xFFFF) + { + *physical_addr = addr; + return true; + } + } + } + + return false; +} + +static bool cec_edid_block_checksum_ok(const uint8_t *block) +{ + uint8_t sum = 0; + for (size_t i = 0; i < CEC_EDID_BLOCK_SIZE; i++) sum = (uint8_t)(sum + block[i]); + return sum == 0; +} + +static bool cec_wait_edid_ready(void) +{ + unsigned long timeout = GetTimer(CEC_EDID_READY_TIMEOUT_MS); + while (!CheckTimer(timeout)) + { + if (main_reg_read(MAIN_REG_INT0_STATUS) & MAIN_INT_EDID_READY) return true; + usleep(10000); + } + + return (main_reg_read(MAIN_REG_INT0_STATUS) & MAIN_INT_EDID_READY) != 0; +} + +static bool cec_read_edid_segment(int edid_fd, uint8_t segment, uint8_t *edid, int *read_errors) +{ + if (edid_fd < 0 || !edid) return false; + + main_reg_write(MAIN_REG_EDID_SEGMENT, segment); + main_reg_write(MAIN_REG_INT0_STATUS, MAIN_INT_EDID_READY); + main_reg_write(MAIN_REG_EDID_CTRL, 0x03); + usleep(1000); + main_reg_write(MAIN_REG_EDID_CTRL, 0x13); + + bool ready = cec_wait_edid_ready(); + int errors = 0; + + for (uint16_t i = 0; i < CEC_EDID_SEGMENT_SIZE; i++) + { + int value = i2c_smbus_read_byte_data(edid_fd, (uint8_t)i); + if (value < 0) + { + errors++; + edid[i] = 0; + continue; + } + + edid[i] = (uint8_t)value; + } + + if (read_errors) *read_errors += errors; + if (cec_debug_enabled() && (!ready || errors)) + { + printf("CEC: EDID segment %u ready=%u read_errors=%d\n", segment, ready ? 1 : 0, errors); + } + + return ready && errors == 0; +} + +static bool cec_read_physical_address(uint16_t *physical_addr) +{ + uint8_t edid[CEC_EDID_BLOCK_SIZE * CEC_EDID_MAX_BLOCKS] = {}; + uint16_t addr = CEC_INVALID_PHYS_ADDR; + + if (physical_addr) *physical_addr = CEC_INVALID_PHYS_ADDR; + if (!physical_addr || cec_main_fd < 0) return false; + + int edid_fd = i2c_open(ADV7513_EDID_ADDR, 0); + if (edid_fd < 0) return false; + + int read_errors = 0; + size_t blocks_read = 0; + if (cec_read_edid_segment(edid_fd, 0, edid, &read_errors) && + edid[0] == 0x00 && edid[1] == 0xFF && edid[2] == 0xFF && edid[3] == 0xFF && + edid[4] == 0xFF && edid[5] == 0xFF && edid[6] == 0xFF && edid[7] == 0x00 && + cec_edid_block_checksum_ok(edid)) + { + size_t total_blocks = 1 + edid[126]; + if (total_blocks > CEC_EDID_MAX_BLOCKS) + { + if (cec_debug_enabled()) + { + printf("CEC: EDID has %u blocks, reading first %u\n", (unsigned)total_blocks, (unsigned)CEC_EDID_MAX_BLOCKS); + } + total_blocks = CEC_EDID_MAX_BLOCKS; + } + + blocks_read = total_blocks; + for (uint8_t segment = 1; segment < (uint8_t)((total_blocks + 1) / 2); segment++) + { + if (!cec_read_edid_segment(edid_fd, segment, edid + (segment * CEC_EDID_SEGMENT_SIZE), &read_errors)) + { + blocks_read = segment * 2; + break; + } + } + + for (size_t block = 1; block < blocks_read; block++) + { + if (!cec_edid_block_checksum_ok(edid + (block * CEC_EDID_BLOCK_SIZE))) + { + if (cec_debug_enabled()) printf("CEC: EDID block %u checksum failed\n", (unsigned)block); + blocks_read = block; + break; + } + } + } + + i2c_close(edid_fd); + + size_t edid_size = blocks_read * CEC_EDID_BLOCK_SIZE; + bool found = cec_parse_physical_address(edid, edid_size, &addr); + if (!found) + { + found = cec_parse_physical_address_loose(edid, edid_size ? edid_size : CEC_EDID_SEGMENT_SIZE, &addr); + } + + if (read_errors && cec_debug_enabled()) + { + printf("CEC: EDID read errors=%d\n", read_errors); + } + + if (found) *physical_addr = addr; + return found; +} + +static const uint8_t cec_rx_hdr_regs[] = { CEC_REG_RX1_FRAME_HEADER, CEC_REG_RX2_FRAME_HEADER, CEC_REG_RX3_FRAME_HEADER }; +static const uint8_t cec_rx_len_regs[] = { CEC_REG_RX1_FRAME_LENGTH, CEC_REG_RX2_FRAME_LENGTH, CEC_REG_RX3_FRAME_LENGTH }; +static const uint8_t cec_rx_int_bits[] = { CEC_INT_RX_RDY1, CEC_INT_RX_RDY2, CEC_INT_RX_RDY3 }; + +static bool cec_read_rx_buffer(int index, cec_message_t *msg) +{ + if (!msg || index < 0 || index > 2) return false; + + uint8_t len_raw = cec_reg_read(cec_rx_len_regs[index]); + uint8_t length = len_raw & 0x1F; + if (length < 1 || length > 16) return false; + + msg->length = length; + msg->header = cec_reg_read(cec_rx_hdr_regs[index]); + msg->opcode = (length > 1) ? cec_reg_read(cec_rx_hdr_regs[index] + 1) : 0; + + for (uint8_t i = 0; i < (length > 2 ? length - 2 : 0); i++) + { + msg->data[i] = cec_reg_read(cec_rx_hdr_regs[index] + 2 + i); + } + + // Release the consumed RX buffer slot back to hardware. + uint8_t bit = 1 << index; + cec_reg_write(CEC_REG_RX_BUFFERS, 0x08 | bit); + usleep(200); + cec_reg_write(CEC_REG_RX_BUFFERS, 0x08); + + return true; +} + +static bool cec_hdmi_int_active(void) +{ + return fpga_get_hdmi_int() != 0; +} + +static bool cec_receive_message(cec_message_t *msg) +{ + if (!cec_enabled || !msg) return false; + if (!cec_hdmi_int_active()) return false; + + uint8_t rx_bits = cec_reg_read(CEC_REG_RX_READY) & CEC_INT_RX_RDY_MASK; + if (!rx_bits) + { + main_reg_write(MAIN_REG_INT1_STATUS, CEC_INT_RX_RDY_MASK); + return false; + } + + uint8_t rx_order = cec_reg_read(CEC_REG_RX_STATUS); + int selected = -1; + int oldest = 4; + + for (int i = 0; i < 3; i++) + { + if (!(rx_bits & cec_rx_int_bits[i])) continue; + + int order = (rx_order >> (i * 2)) & 0x03; + if (order > 0 && order < oldest) + { + oldest = order; + selected = i; + } + } + + if (selected < 0) + { + for (int i = 0; i < 3; i++) + { + if (rx_bits & cec_rx_int_bits[i]) + { + selected = i; + break; + } + } + } + + if (selected < 0) return false; + + bool ok = cec_read_rx_buffer(selected, msg); + main_reg_write(MAIN_REG_INT1_STATUS, cec_rx_int_bits[selected]); + + return ok; +} + +static bool cec_send_active_source(bool with_retry) +{ + if (!cec_has_physical_address()) return false; + + cec_message_t msg = {}; + msg.header = (cec_logical_addr << 4) | CEC_LOG_ADDR_BROADCAST; + msg.opcode = CEC_OPCODE_ACTIVE_SOURCE; + msg.data[0] = (uint8_t)(cec_physical_addr >> 8); + msg.data[1] = (uint8_t)(cec_physical_addr & 0xFF); + msg.length = 4; + return cec_send_message(&msg, with_retry); +} + +static bool cec_send_request_active_source(bool with_retry) +{ + cec_message_t msg = {}; + msg.header = (cec_logical_addr << 4) | CEC_LOG_ADDR_BROADCAST; + msg.opcode = CEC_OPCODE_REQUEST_ACTIVE_SOURCE; + msg.length = 2; + return cec_send_message(&msg, with_retry); +} + +static bool cec_send_image_view_on(bool with_retry) +{ + cec_message_t msg = {}; + msg.header = (cec_logical_addr << 4) | CEC_LOG_ADDR_TV; + msg.opcode = CEC_OPCODE_IMAGE_VIEW_ON; + msg.length = 2; + return cec_send_message(&msg, with_retry); +} + +static bool cec_send_text_view_on(bool with_retry) +{ + cec_message_t msg = {}; + msg.header = (cec_logical_addr << 4) | CEC_LOG_ADDR_TV; + msg.opcode = CEC_OPCODE_TEXT_VIEW_ON; + msg.length = 2; + return cec_send_message(&msg, with_retry); +} + +bool cec_send_standby(void) +{ + cec_message_t msg = {}; + msg.header = (cec_logical_addr << 4) | CEC_LOG_ADDR_BROADCAST; + msg.opcode = CEC_OPCODE_STANDBY; + msg.length = 2; + return cec_send_message(&msg); +} + +bool cec_send_wake(void) +{ + if (!cec_enabled) return false; + + bool wake_ok = cec_send_image_view_on(); usleep(20000); + bool text_ok = cec_send_text_view_on(); usleep(20000); + bool active_ok = cec_send_active_source(); + + return wake_ok && text_ok && active_ok; +} + +static bool cec_send_report_physical_address(bool with_retry) +{ + if (!cec_has_physical_address()) return false; + + cec_message_t msg = {}; + msg.header = (cec_logical_addr << 4) | CEC_LOG_ADDR_BROADCAST; + msg.opcode = CEC_OPCODE_REPORT_PHYSICAL_ADDRESS; + msg.data[0] = (uint8_t)(cec_physical_addr >> 8); + msg.data[1] = (uint8_t)(cec_physical_addr & 0xFF); + msg.data[2] = CEC_DEVICE_TYPE_PLAYBACK; + msg.length = 5; + return cec_send_message(&msg, with_retry); +} + +static bool cec_send_set_osd_name(const char *name, bool with_retry) +{ + if (!name) return false; + + cec_message_t msg = {}; + msg.header = (cec_logical_addr << 4) | CEC_LOG_ADDR_TV; + msg.opcode = CEC_OPCODE_SET_OSD_NAME; + + size_t len = strlen(name); + if (len > 14) len = 14; + for (size_t i = 0; i < len; i++) + { + msg.data[i] = (uint8_t)name[i]; + } + + msg.length = (uint8_t)(2 + len); + return cec_send_message(&msg, with_retry); +} + +static bool cec_send_device_vendor_id(bool with_retry = true) +{ + cec_message_t msg = {}; + msg.header = (cec_logical_addr << 4) | CEC_LOG_ADDR_BROADCAST; + msg.opcode = CEC_OPCODE_DEVICE_VENDOR_ID; + msg.data[0] = 0x00; + msg.data[1] = 0x00; + msg.data[2] = 0x00; + msg.length = 5; + return cec_send_message(&msg, with_retry); +} + +static bool cec_send_cec_version(uint8_t destination) +{ + cec_message_t msg = {}; + msg.header = (cec_logical_addr << 4) | destination; + msg.opcode = CEC_OPCODE_CEC_VERSION; + msg.data[0] = CEC_VERSION_1_4; + msg.length = 3; + return cec_send_message(&msg); +} + +static bool cec_send_power_status(uint8_t destination) +{ + cec_message_t msg = {}; + msg.header = (cec_logical_addr << 4) | destination; + msg.opcode = CEC_OPCODE_REPORT_POWER_STATUS; + msg.data[0] = CEC_POWER_STATUS_ON; + msg.length = 3; + return cec_send_message(&msg); +} + +static void cec_set_physical_address(uint16_t physical_addr) +{ + cec_physical_addr = physical_addr; + cec_program_logical_address(cec_pick_logical_address_from_physical(cec_physical_addr)); +} + +static bool cec_has_physical_address(void) +{ + return cec_physical_addr_from_edid && cec_physical_addr != CEC_INVALID_PHYS_ADDR; +} + +static bool cec_is_active_source(void) +{ + return cec_has_physical_address() && cec_active_physical_addr == cec_physical_addr; +} + +static void cec_schedule_power_on_switch(unsigned long delay_ms) +{ + cec_power_on_state = CEC_POWER_ON_DONE; + cec_power_on_deadline = 0; + + if (!cfg.hdmi_cec_power_on) return; + + cec_power_on_state = CEC_POWER_ON_REQUEST_BEFORE; + cec_power_on_deadline = delay_ms ? GetTimer(delay_ms) : 0; +} + +static void cec_schedule_edid_retry(unsigned long delay_ms) +{ + cec_edid_retry_delay_ms = delay_ms; + cec_edid_retry_deadline = delay_ms ? GetTimer(delay_ms) : 0; +} + +static void cec_backoff_edid_retry(void) +{ + if (!cec_edid_retry_delay_ms) cec_edid_retry_delay_ms = CEC_EDID_RETRY_INITIAL_MS; + else if (cec_edid_retry_delay_ms < CEC_EDID_RETRY_MAX_MS) + { + cec_edid_retry_delay_ms *= 2; + if (cec_edid_retry_delay_ms > CEC_EDID_RETRY_MAX_MS) cec_edid_retry_delay_ms = CEC_EDID_RETRY_MAX_MS; + } + + cec_edid_retry_deadline = GetTimer(cec_edid_retry_delay_ms); +} + +static void cec_schedule_advertise(uint8_t attempts, unsigned long delay_ms) +{ + cec_advertise_step = 0; + cec_advertise_attempts = attempts; + cec_advertise_deadline = attempts ? (delay_ms ? GetTimer(delay_ms) : 0) : 0; +} + +static void cec_schedule_startup_actions(void) +{ + cec_startup_actions_scheduled = true; + cec_schedule_advertise(CEC_ADVERTISE_STARTUP_ATTEMPTS, 0); + cec_schedule_power_on_switch(CEC_ADVERTISE_STEP_MS * CEC_ADVERTISE_IDENTITY_STEPS); +} + +static void cec_schedule_post_switch_edid_retry(void) +{ + if (cec_physical_addr_from_edid) return; + + cec_reload_video_after_edid = !video_has_valid_edid(); + cec_schedule_edid_retry(CEC_EDID_AFTER_SWITCH_RETRY_MS); +} + +static void cec_poll_edid_retry(void) +{ + if (cec_physical_addr_from_edid) return; + if (!cec_edid_retry_deadline || !CheckTimer(cec_edid_retry_deadline)) return; + + uint16_t physical_addr = CEC_INVALID_PHYS_ADDR; + if (cec_read_physical_address(&physical_addr)) + { + cec_physical_addr_from_edid = true; + cec_schedule_edid_retry(0); + cec_set_physical_address(physical_addr); + if (cec_startup_actions_scheduled) cec_schedule_advertise(CEC_ADVERTISE_STARTUP_ATTEMPTS, 0); + else cec_schedule_startup_actions(); + if (cec_reload_video_after_edid) + { + cec_reload_video_after_edid = false; + video_reload_edid_mode(); + } + return; + } + + cec_backoff_edid_retry(); +} + +static void cec_poll_advertise(void) +{ + if (!cec_advertise_attempts) return; + if (!CheckTimer(cec_advertise_deadline)) return; + + switch (cec_advertise_step) + { + case 0: + cec_send_report_physical_address(false); + break; + + case 1: + cec_send_set_osd_name(cec_get_osd_name(), false); + break; + } + + cec_advertise_step++; + if (cec_advertise_step >= CEC_ADVERTISE_IDENTITY_STEPS) + { + cec_advertise_step = 0; + cec_advertise_attempts--; + if (cec_advertise_attempts) + { + cec_advertise_deadline = GetTimer(CEC_ADVERTISE_STEP_MS); + } + else + { + cec_advertise_deadline = 0; + } + } + else + { + cec_advertise_deadline = GetTimer(CEC_ADVERTISE_STEP_MS); + } +} + +static void cec_poll_power_on_switch(void) +{ + if (cec_power_on_state == CEC_POWER_ON_DONE) return; + if (!CheckTimer(cec_power_on_deadline)) return; + + switch (cec_power_on_state) + { + case CEC_POWER_ON_REQUEST_BEFORE: + cec_active_physical_addr = 0xFFFF; + cec_send_request_active_source(false); + cec_power_on_state = CEC_POWER_ON_WAIT_BEFORE; + cec_power_on_deadline = GetTimer(CEC_POWER_ON_QUERY_WAIT_MS); + break; + + case CEC_POWER_ON_WAIT_BEFORE: + if (cec_is_active_source()) + { + cec_schedule_post_switch_edid_retry(); + cec_power_on_state = CEC_POWER_ON_DONE; + cec_power_on_deadline = 0; + } + else + { + cec_power_on_state = CEC_POWER_ON_IMAGE; + cec_power_on_deadline = 0; + } + break; + + case CEC_POWER_ON_IMAGE: + cec_send_image_view_on(false); + cec_power_on_state = CEC_POWER_ON_TEXT; + cec_power_on_deadline = GetTimer(CEC_POWER_ON_STEP_MS); + break; + + case CEC_POWER_ON_TEXT: + cec_send_text_view_on(false); + cec_power_on_state = CEC_POWER_ON_ACTIVE; + cec_power_on_deadline = GetTimer(CEC_POWER_ON_STEP_MS); + break; + + case CEC_POWER_ON_ACTIVE: + cec_send_active_source(false); + cec_schedule_post_switch_edid_retry(); + cec_power_on_state = CEC_POWER_ON_REQUEST_VERIFY; + cec_power_on_deadline = GetTimer(CEC_POWER_ON_VERIFY_WAIT_MS); + break; + + case CEC_POWER_ON_REQUEST_VERIFY: + cec_active_physical_addr = 0xFFFF; + cec_send_request_active_source(false); + cec_power_on_state = CEC_POWER_ON_WAIT_VERIFY; + cec_power_on_deadline = GetTimer(CEC_POWER_ON_QUERY_WAIT_MS); + break; + + case CEC_POWER_ON_WAIT_VERIFY: + default: + cec_power_on_state = CEC_POWER_ON_DONE; + cec_power_on_deadline = 0; + break; + } +} + +static void cec_handle_message(const cec_message_t *msg) +{ + if (!msg || msg->length < 2) return; + + uint8_t src = (msg->header >> 4) & 0x0F; + uint8_t dst = msg->header & 0x0F; + if (dst != cec_logical_addr && dst != CEC_LOG_ADDR_BROADCAST) return; + + bool is_user_control = (msg->opcode == CEC_OPCODE_USER_CONTROL_PRESSED) || + (msg->opcode == CEC_OPCODE_USER_CONTROL_RELEASED); + + // Ignore broadcast network chatter unless it's potentially actionable. + if (dst == CEC_LOG_ADDR_BROADCAST && + msg->opcode != CEC_OPCODE_ACTIVE_SOURCE && + msg->opcode != CEC_OPCODE_SET_STREAM_PATH && + msg->opcode != CEC_OPCODE_REQUEST_ACTIVE_SOURCE && + !(is_user_control && src == CEC_LOG_ADDR_TV)) + { + return; + } + + switch (msg->opcode) + { + case CEC_OPCODE_GIVE_PHYSICAL_ADDRESS: + if (cec_has_physical_address() && cec_rate_limit(&cec_reply_phys_deadline, 2000)) cec_send_report_physical_address(); + break; + + case CEC_OPCODE_GIVE_OSD_NAME: + if (cec_rate_limit(&cec_reply_name_deadline, 2000)) cec_send_set_osd_name(cec_get_osd_name()); + break; + + case CEC_OPCODE_GIVE_DEVICE_VENDOR_ID: + if (cec_rate_limit(&cec_reply_vendor_deadline, 5000)) cec_send_device_vendor_id(); + break; + + case CEC_OPCODE_GET_CEC_VERSION: + if (cec_rate_limit(&cec_reply_version_deadline, 5000)) cec_send_cec_version(src); + break; + + case CEC_OPCODE_GIVE_DEVICE_POWER_STATUS: + if (cec_rate_limit(&cec_reply_power_deadline, 5000)) cec_send_power_status(src); + break; + + case CEC_OPCODE_REQUEST_ACTIVE_SOURCE: + if (cec_is_active_source() && cec_rate_limit(&cec_reply_active_deadline, 2000)) cec_send_active_source(); + break; + + case CEC_OPCODE_ACTIVE_SOURCE: + if (msg->length >= 4) + { + uint16_t path = (uint16_t)((msg->data[0] << 8) | msg->data[1]); + cec_active_physical_addr = path; + } + break; + + case CEC_OPCODE_SET_STREAM_PATH: + if (msg->length >= 4) + { + uint16_t path = (uint16_t)((msg->data[0] << 8) | msg->data[1]); + cec_active_physical_addr = path; + if (path == cec_physical_addr && cec_rate_limit(&cec_reply_active_deadline, 2000)) cec_send_active_source(); + } + break; + + case CEC_OPCODE_MENU_REQUEST: + { + if (!cec_rate_limit(&cec_reply_menu_deadline, 1000)) break; + cec_message_t reply = {}; + reply.header = (cec_logical_addr << 4) | src; + reply.opcode = CEC_OPCODE_MENU_STATUS; + reply.data[0] = 0x00; + reply.length = 3; + cec_send_message(&reply); + } + break; + + case CEC_OPCODE_USER_CONTROL_PRESSED: + if (msg->length >= 3) cec_handle_button(msg->data[0], true); + break; + + case CEC_OPCODE_USER_CONTROL_RELEASED: + cec_handle_button(0, false); + break; + + default: + break; + } +} + +bool cec_init(bool enable) +{ + if (!enable) + { + cec_deinit(); + return true; + } + + if (cec_enabled) return true; + + cec_deinit(); + cec_hpd_pulsed = false; + + cec_main_fd = i2c_open(ADV7513_MAIN_ADDR, 0); + if (cec_main_fd < 0) + { + return false; + } + + if (!cec_setup_main_registers(true)) + { + cec_deinit(); + return false; + } + + cec_fd = i2c_open(ADV7513_CEC_ADDR, 0); + if (cec_fd < 0) + { + cec_deinit(); + return false; + } + + cec_reg_write(CEC_REG_SOFT_RESET, 0x01); + usleep(2000); + cec_reg_write(CEC_REG_SOFT_RESET, 0x00); + + cec_reg_write(CEC_REG_TX_ENABLE, 0x00); + cec_reg_write(CEC_REG_CLK_DIV, 0x3D); + cec_clear_rx_buffers(); + main_reg_write(MAIN_REG_INT1_STATUS, 0xFF); + + cec_enabled = true; + cec_reply_phys_deadline = 0; + cec_reply_name_deadline = 0; + cec_reply_version_deadline = 0; + cec_reply_power_deadline = 0; + cec_reply_menu_deadline = 0; + cec_reply_active_deadline = 0; + cec_reply_vendor_deadline = 0; + cec_tx_fail_streak = 0; + cec_tx_suppress_deadline = 0; + cec_advertise_deadline = 0; + cec_advertise_step = 0; + cec_advertise_attempts = 0; + cec_startup_actions_scheduled = false; + cec_power_on_state = CEC_POWER_ON_DONE; + cec_power_on_deadline = 0; + cec_reload_video_after_edid = false; + cec_poll_deadline = 0; + uint16_t physical_addr = CEC_INVALID_PHYS_ADDR; + cec_physical_addr = CEC_INVALID_PHYS_ADDR; + cec_program_logical_address(CEC_LOG_ADDR_PLAYBACK1); + if (cec_read_physical_address(&physical_addr)) + { + cec_physical_addr_from_edid = true; + cec_set_physical_address(physical_addr); + } + else + { + cec_physical_addr_from_edid = false; + } + cec_schedule_edid_retry(cec_physical_addr_from_edid ? 0 : CEC_EDID_RETRY_INITIAL_MS); + cec_refresh_deadline = GetTimer(CEC_MAIN_REFRESH_MS); + + if (cec_debug_enabled()) + { + printf("CEC: logical=%u physical=%X.%X.%X.%X\n", + cec_logical_addr, + (cec_physical_addr >> 12) & 0x0F, + (cec_physical_addr >> 8) & 0x0F, + (cec_physical_addr >> 4) & 0x0F, + cec_physical_addr & 0x0F); + } + + if (cec_physical_addr_from_edid) cec_schedule_startup_actions(); + + cec_input_activity_seq = input_activity_get_seq(); + unsigned long idle_ms = cec_idle_sleep_delay_ms(); + cec_idle_deadline = idle_ms ? GetTimer(idle_ms) : 0; + cec_idle_engaged = false; + + return true; +} + +void cec_deinit(void) +{ + cec_release_key(); + + if (cec_fd >= 0) + { + cec_reg_write(CEC_REG_TX_ENABLE, 0x00); + cec_reg_write(CEC_REG_LOG_ADDR_MASK, 0x00); + i2c_close(cec_fd); + } + + if (cec_main_fd >= 0) + { + main_reg_write(MAIN_REG_INT1_ENABLE, 0x00); + main_reg_write(MAIN_REG_INT1_STATUS, 0xFF); + i2c_close(cec_main_fd); + } + + cec_fd = -1; + cec_main_fd = -1; + cec_enabled = false; + cec_logical_addr = CEC_LOG_ADDR_PLAYBACK1; + cec_physical_addr = CEC_INVALID_PHYS_ADDR; + cec_refresh_deadline = 0; + cec_poll_deadline = 0; + cec_edid_retry_deadline = 0; + cec_edid_retry_delay_ms = 0; + cec_physical_addr_from_edid = false; + cec_reply_phys_deadline = 0; + cec_reply_name_deadline = 0; + cec_reply_version_deadline = 0; + cec_reply_power_deadline = 0; + cec_reply_menu_deadline = 0; + cec_reply_active_deadline = 0; + cec_reply_vendor_deadline = 0; + cec_tx_fail_streak = 0; + cec_tx_suppress_deadline = 0; + cec_advertise_deadline = 0; + cec_advertise_step = 0; + cec_advertise_attempts = 0; + cec_startup_actions_scheduled = false; + cec_power_on_state = CEC_POWER_ON_DONE; + cec_power_on_deadline = 0; + cec_reload_video_after_edid = false; + cec_active_physical_addr = 0xFFFF; + cec_input_activity_seq = 0; + cec_idle_deadline = 0; + cec_idle_engaged = false; +} + +void cec_poll(void) +{ + if (!cec_enabled) return; + if (!CheckTimer(cec_poll_deadline)) return; + cec_poll_deadline = GetTimer(CEC_POLL_INTERVAL_MS); + + if (CheckTimer(cec_refresh_deadline)) + { + cec_setup_main_registers(); + cec_refresh_deadline = GetTimer(CEC_MAIN_REFRESH_MS); + } + + cec_poll_edid_retry(); + cec_poll_advertise(); + + cec_message_t msg = {}; + int max_msgs = 1; + for (int i = 0; i < max_msgs; i++) + { + if (!cec_receive_message(&msg)) break; + cec_handle_message(&msg); + } + + cec_poll_power_on_switch(); + cec_poll_idle_sleep_wake(); + cec_poll_key_timeout(); +} + +bool cec_is_enabled(void) +{ + return cec_enabled; +} diff --git a/hdmi_cec.h b/hdmi_cec.h new file mode 100644 index 0000000..745218b --- /dev/null +++ b/hdmi_cec.h @@ -0,0 +1,13 @@ +#ifndef HDMI_CEC_H +#define HDMI_CEC_H + +#include + +bool cec_init(bool enable); +void cec_deinit(void); +void cec_poll(void); +bool cec_is_enabled(void); +bool cec_send_standby(void); +bool cec_send_wake(void); + +#endif diff --git a/input.cpp b/input.cpp index d7e1e1e..4703802 100644 --- a/input.cpp +++ b/input.cpp @@ -46,6 +46,17 @@ char joy_bnames[NUMBUTTONS][32] = {}; int joy_bcount = 0; static struct pollfd pool[NUMDEV + 3]; int xbe2_shift = 0; +static volatile uint32_t input_activity_seq = 0; + +static inline void input_mark_activity(void) +{ + input_activity_seq++; +} + +uint32_t input_activity_get_seq(void) +{ + return input_activity_seq; +} static bool gcdb_use_usb_bcd_device(uint16_t vid, uint16_t pid) { @@ -2078,6 +2089,13 @@ void input_uinp_destroy() } } +void input_cec_send_key(uint16_t key, bool pressed) +{ + if (!key) return; + input_mark_activity(); + user_io_kbd(key, pressed ? 1 : 0); +} + static unsigned long uinp_repeat = 0; static struct input_event uinp_ev; static void uinp_send_key(uint16_t key, int press) @@ -5641,6 +5659,8 @@ int input_test(int getchar) memset(&ev, 0, sizeof(ev)); if (read(pool[i].fd, &ev, sizeof(ev)) == sizeof(ev)) { + if (ev.type == EV_KEY || ev.type == EV_ABS || ev.type == EV_REL) input_mark_activity(); + if (getchar) { if (ev.type == EV_KEY && ev.value >= 1) @@ -6138,6 +6158,8 @@ int input_test(int getchar) uint8_t data[4] = {}; if (read(pool[i].fd, data, sizeof(data))) { + input_mark_activity(); + int edev = i; int dev = i; if (input[i].bind >= 0) edev = input[i].bind; // mouse to event diff --git a/input.h b/input.h index 117c02a..69f2ced 100644 --- a/input.h +++ b/input.h @@ -101,6 +101,8 @@ void sysled_enable(int en); void input_notify_mode(); int input_poll(int getchar); int is_key_pressed(int key); +void input_cec_send_key(uint16_t key, bool pressed); +uint32_t input_activity_get_seq(void); void start_map_setting(int cnt, int set = 0, advancedButtonMap *code_store = NULL); int get_map_set(); diff --git a/scheduler.cpp b/scheduler.cpp index afa29b2..a0c24cd 100644 --- a/scheduler.cpp +++ b/scheduler.cpp @@ -8,11 +8,17 @@ #include "fpga_io.h" #include "osd.h" #include "profiling.h" +#include "cfg.h" +#include "hardware.h" +#include "hdmi_cec.h" static cothread_t co_scheduler = nullptr; static cothread_t co_poll = nullptr; static cothread_t co_ui = nullptr; static cothread_t co_last = nullptr; +static unsigned long cec_retry = 0; +static bool cec_init_failed_logged = false; +static bool scheduler_ui_ran_once = false; static void scheduler_wait_fpga_ready(void) { @@ -35,6 +41,32 @@ static void scheduler_co_poll(void) input_poll(0); } + if (cfg.hdmi_cec) + { + if (scheduler_ui_ran_once && !cec_is_enabled() && CheckTimer(cec_retry)) + { + if (!cec_init(true)) + { + if (cfg.debug && !cec_init_failed_logged) printf("CEC: init failed\n"); + cec_init_failed_logged = true; + cec_retry = GetTimer(3000); + } + else + { + cec_init_failed_logged = false; + cec_retry = 0; + } + } + + if (cec_is_enabled()) cec_poll(); + } + else + { + if (cec_is_enabled()) cec_deinit(); + cec_retry = 0; + cec_init_failed_logged = false; + } + scheduler_yield(); } } @@ -43,6 +75,8 @@ static void scheduler_co_ui(void) { for (;;) { + scheduler_ui_ran_once = true; + { SPIKE_SCOPE("co_ui", 1000); HandleUI(); diff --git a/video.cpp b/video.cpp index 6065d88..5ab153d 100644 --- a/video.cpp +++ b/video.cpp @@ -2618,6 +2618,29 @@ void video_init() video_set_mode(&v_def, 0); } +bool video_has_valid_edid() +{ + return is_edid_valid(); +} + +bool video_reload_edid_mode() +{ + if (cfg.direct_video) return false; + if (cfg.video_conf[0] || cfg.video_conf_pal[0] || cfg.video_conf_ntsc[0]) return false; + + bzero(edid, sizeof(edid)); + raw_edid_mfg_id_valid = false; + support_FHD = 0; + + video_mode_load(); + if (!vmode_def && !is_edid_valid()) return false; + + printf("EDID: applying refreshed HDMI mode.\n"); + video_set_mode(&v_def, 0); + user_io_send_buttons(1); + return true; +} + static int api1_5 = 0; int hasAPI1_5() diff --git a/video.h b/video.h index bb1dd65..5186225 100644 --- a/video.h +++ b/video.h @@ -34,6 +34,8 @@ struct VideoInfo extern VideoInfo current_video_info; void video_init(); +bool video_has_valid_edid(); +bool video_reload_edid_mode(); int video_get_scaler_flt(int type); void video_set_scaler_flt(int type, int n);