diff --git a/esphome/components/hdmi_cec/__init__.py b/esphome/components/hdmi_cec/__init__.py index 386bac5b879..4a16f36ed97 100644 --- a/esphome/components/hdmi_cec/__init__.py +++ b/esphome/components/hdmi_cec/__init__.py @@ -186,7 +186,8 @@ async def to_code(config: ConfigType) -> None: if (physical_address := config.get(CONF_PHYSICAL_ADDRESS)) is not None: cg.add(var.set_physical_address(physical_address)) if (osd_name := config.get(CONF_OSD_NAME)) is not None: - cg.add(var.set_osd_name(cg.shared_progmem_string(osd_name), len(osd_name))) + cg.extern_progmem_array("esphome::hdmi_cec::OSD_NAME", cg.char, osd_name) + cg.add_define("HDMI_CEC_OSD_NAME_LENGTH", len(osd_name)) if (uart_id := config.get(CONF_UART_ID)) is not None: cg.add_define("USE_HDMI_CEC_UART") cg.add(var.set_uart(await cg.get_variable(uart_id))) @@ -204,10 +205,10 @@ async def to_code(config: ConfigType) -> None: {conf[CONF_DATA][0] for conf in for_us if CONF_DATA in conf} ) if claimed_opcodes: - table = cg.shared_progmem_array( - "hdmi_cec_claimed_opcodes", cg.uint8, claimed_opcodes + cg.extern_progmem_array( + "esphome::hdmi_cec::CLAIMED_OPCODES", cg.uint8, claimed_opcodes ) - cg.add(var.set_claimed_opcodes(table, len(claimed_opcodes))) + cg.add_define("HDMI_CEC_CLAIMED_OPCODE_COUNT", len(claimed_opcodes)) if all(CONF_DATA in conf for conf in for_us): cg.add_define("USE_HDMI_CEC_FEATURE_ABORT") diff --git a/esphome/components/hdmi_cec/hdmi_cec.cpp b/esphome/components/hdmi_cec/hdmi_cec.cpp index 8c970d429e2..e29ceba2f3c 100644 --- a/esphome/components/hdmi_cec/hdmi_cec.cpp +++ b/esphome/components/hdmi_cec/hdmi_cec.cpp @@ -45,27 +45,13 @@ static constexpr uint8_t OPCODE_REPORT_PHYSICAL_ADDRESS = 0x84; static constexpr uint8_t OPCODE_GET_CEC_VERSION = 0x9F; static constexpr uint8_t OPCODE_CEC_VERSION = 0x9E; static constexpr uint8_t CEC_VERSION_1_4 = 0x05; -static constexpr uint8_t BROADCAST_ADDRESS = 0xF; -/// The CEC device type a logical address stands for, for "Report Physical Address" +/// The CEC device type a logical address stands for, for "Report Physical Address": one nibble per +/// address, from 0 up. 0 TV, 1 Recording Device, 3 Tuner, 4 Playback Device, 5 Audio System; the +/// reserved, specific use and unregistered addresses count as a playback device static uint8_t device_type_of(uint8_t logical_address) { - switch (logical_address) { - case 0x0: - return 0x00; // TV - case 0x1: - case 0x2: - case 0x9: - return 0x01; // Recording Device - case 0x3: - case 0x6: - case 0x7: - case 0xA: - return 0x03; // Tuner - case 0x5: - return 0x05; // Audio System - default: - return 0x04; // Playback Device - } + constexpr uint64_t device_types = 0x4444431433543110ULL; + return (device_types >> (4 * logical_address)) & 0xF; } Frame::Frame(uint8_t initiator_addr, uint8_t destination_addr, std::span payload) { @@ -103,12 +89,12 @@ void HDMICEC::dump_config() { (this->physical_address_ >> 8) & 0xF, (this->physical_address_ >> 4) & 0xF, this->physical_address_ & 0xF); } - if (this->osd_name_ != nullptr) { - char name[Frame::MAX_PAYLOAD_LENGTH]; - progmem_memcpy(name, this->osd_name_, this->osd_name_length_); - name[this->osd_name_length_] = '\0'; - ESP_LOGCONFIG(TAG, " OSD name: %s", name); - } +#ifdef HDMI_CEC_OSD_NAME_LENGTH + char name[HDMI_CEC_OSD_NAME_LENGTH + 1]; + progmem_memcpy(name, OSD_NAME, HDMI_CEC_OSD_NAME_LENGTH); + name[HDMI_CEC_OSD_NAME_LENGTH] = '\0'; + ESP_LOGCONFIG(TAG, " OSD name: %s", name); +#endif #ifdef USE_HDMI_CEC_UART ESP_LOGCONFIG(TAG, " Transmit: UART"); #endif @@ -131,10 +117,11 @@ void HDMICEC::loop() { #ifdef HDMI_CEC_MESSAGE_CALLBACK_COUNT this->message_callbacks_.call(frame->initiator_addr(), frame->destination_addr(), frame->payload()); #endif - // Only a frame directed at us gets a reply; a broadcast is never answered, and a device that only - // listens (address 0xF) has nothing to say + // Only a frame directed at us from a registered device gets a reply: a broadcast is never + // answered, a device that only listens (address 0xF) has nothing to say, and an unregistered + // initiator cannot be answered directly if (!this->monitor_mode_ && !frame->is_broadcast() && frame->destination_addr() == this->address_ && - !this->is_claimed_(frame->payload()[0])) { + frame->initiator_addr() != Frame::BROADCAST_ADDRESS && !is_claimed_(frame->payload()[0])) { this->reply_builtin_(frame); } this->frames_queue_.pop_front(); @@ -148,45 +135,43 @@ void HDMICEC::loop() { } } -bool HDMICEC::is_claimed_(uint8_t opcode) const { - for (uint8_t i = 0; i < this->claimed_opcode_count_; i++) { - const uint8_t claimed = progmem_read_byte(&this->claimed_opcodes_[i]); - if (claimed >= opcode) { - return claimed == opcode; +bool HDMICEC::is_claimed_(uint8_t opcode) { +#ifdef HDMI_CEC_CLAIMED_OPCODE_COUNT + for (uint8_t i = 0; i < HDMI_CEC_CLAIMED_OPCODE_COUNT; i++) { + if (progmem_read_byte(&CLAIMED_OPCODES[i]) == opcode) { + return true; } } +#endif return false; } void HDMICEC::reply_builtin_(const Frame *frame) { const uint8_t source = frame->initiator_addr(); const uint8_t opcode = frame->payload()[0]; - if (opcode == OPCODE_FEATURE_ABORT || source == BROADCAST_ADDRESS) { - return; // an abort is never answered, and an unregistered device cannot be answered directly - } switch (opcode) { + case OPCODE_FEATURE_ABORT: + return; // never answered case OPCODE_GET_CEC_VERSION: this->send(source, {OPCODE_CEC_VERSION, CEC_VERSION_1_4}); return; case OPCODE_GIVE_PHYSICAL_ADDRESS: if (this->physical_address_ != UNSET_PHYSICAL_ADDRESS) { // Reported to everybody, with the device type the logical address stands for - this->send(BROADCAST_ADDRESS, + this->send(Frame::BROADCAST_ADDRESS, {OPCODE_REPORT_PHYSICAL_ADDRESS, static_cast(this->physical_address_ >> 8), static_cast(this->physical_address_), device_type_of(this->address_)}); return; } break; - case OPCODE_GIVE_OSD_NAME: - if (this->osd_name_ != nullptr) { - uint8_t reply[Frame::MAX_PAYLOAD_LENGTH] = {OPCODE_SET_OSD_NAME}; - progmem_memcpy(reply + 1, this->osd_name_, this->osd_name_length_); - this->send(source, std::span(reply, 1 + this->osd_name_length_)); - return; - } - break; - default: - break; +#ifdef HDMI_CEC_OSD_NAME_LENGTH + case OPCODE_GIVE_OSD_NAME: { + uint8_t reply[1 + HDMI_CEC_OSD_NAME_LENGTH] = {OPCODE_SET_OSD_NAME}; + progmem_memcpy(reply + 1, OSD_NAME, HDMI_CEC_OSD_NAME_LENGTH); + this->send(source, std::span(reply, sizeof(reply))); + return; + } +#endif } #ifdef USE_HDMI_CEC_FEATURE_ABORT // Nothing here or in an automation answers it: tell the initiator, as the bus expects @@ -295,54 +280,54 @@ bool HDMICEC::transmit_frame_(const Frame *frame) { if (!this->send_start_bit_()) { return false; } - const auto bytes = frame->bytes(); - for (size_t byte_inx = 0; byte_inx < bytes.size(); byte_inx++) { - const uint8_t current_byte = bytes[byte_inx]; - // Most significant bit first. The four initiator address bits watch for a collision with - // another initiator that started at the same moment; a lower address wins by holding the line - for (int8_t i = 7; i >= 0; i--) { - const bool bit_value = (current_byte >> i) & 0x1; - if (!this->send_bit_(bit_value) && byte_inx == 0 && i >= 4) { - ESP_LOGD(TAG, "Bus collision on sending initiator address 0x%X on attempt %u", frame->initiator_addr(), - this->transmit_attempts_); - return false; - } -#ifdef USE_HDMI_CEC_UART - if (this->uart_ != nullptr && byte_inx == 0 && i == 4) { - // The address is out and uncontested; the UART sends the rest while loop() goes on - this->transmit_on_uart_(frame); - return true; - } -#endif + // The four initiator address bits, most significant first, watch for a collision with another + // initiator that started at the same moment; a lower address wins by holding the line + const uint8_t header = frame->bytes()[0]; + for (int8_t i = 7; i >= 4; i--) { + if (!this->send_bit_((header >> i) & 0x1)) { + ESP_LOGD(TAG, "Bus collision on sending initiator address 0x%X on attempt %u", frame->initiator_addr(), + this->transmit_attempts_); + return false; } - // End of message bit, then the acknowledge bit which the receiver may pull low - this->send_bit_(byte_inx == bytes.size() - 1); - this->send_bit_(true); } + this->transmit_body_(frame); return true; } #ifdef USE_HDMI_CEC_UART -void HDMICEC::transmit_on_uart_(const Frame *frame) { +void HDMICEC::transmit_body_(const Frame *frame) { // At 2083 baud one UART byte (start bit, 8 data bits, stop bit) spans two CEC bits of five UART // bits each. The UART start bit is low and its stop bit high, so a byte always begins the first // CEC bit low and ends the second one high; the data bits, sent least significant first, fill in // the rest: a logical 0 is low for three UART bits, a logical 1 for one. static const uint8_t TWO_BITS_TO_UART_BYTE[4] PROGMEM = {0x8C, 0xEC, 0x8F, 0xEF}; // 00, 01, 10, 11 - uint8_t data[Frame::MAX_LENGTH * 5]; // ten CEC bits per byte, two per UART byte + uint8_t data[Frame::MAX_LENGTH * 5 - 2]; // ten CEC bits per byte, two per UART byte, minus the address size_t length = 0; const auto bytes = frame->bytes(); for (size_t byte_inx = 0; byte_inx < bytes.size(); byte_inx++) { // The byte, its end of message bit and the acknowledge bit (sent high, the receiver pulls it low) const uint16_t block = (static_cast(bytes[byte_inx]) << 2) | (byte_inx == bytes.size() - 1 ? 0x2 : 0) | 0x1; - // The four initiator address bits of the header went out on the pin already + // The header starts after its initiator address bits for (int8_t pair = (byte_inx == 0 ? 2 : 4); pair >= 0; pair--) { data[length++] = progmem_read_byte(&TWO_BITS_TO_UART_BYTE[(block >> (2 * pair)) & 0x3]); } } this->uart_->write_array(data, length); } +#else +void HDMICEC::transmit_body_(const Frame *frame) { + const auto bytes = frame->bytes(); + for (size_t byte_inx = 0; byte_inx < bytes.size(); byte_inx++) { + // The header starts after its initiator address bits + for (int8_t i = (byte_inx == 0 ? 3 : 7); i >= 0; i--) { + this->send_bit_((bytes[byte_inx] >> i) & 0x1); + } + // End of message bit, then the acknowledge bit which the receiver may pull low + this->send_bit_(byte_inx == bytes.size() - 1); + this->send_bit_(true); + } +} #endif bool HDMICEC::send_start_bit_() { diff --git a/esphome/components/hdmi_cec/hdmi_cec.h b/esphome/components/hdmi_cec/hdmi_cec.h index fc62b52d019..49a96755219 100644 --- a/esphome/components/hdmi_cec/hdmi_cec.h +++ b/esphome/components/hdmi_cec/hdmi_cec.h @@ -25,6 +25,14 @@ class UARTComponent; namespace esphome::hdmi_cec { +// Generated from the configuration, in flash +#ifdef HDMI_CEC_OSD_NAME_LENGTH +extern const char OSD_NAME[] PROGMEM; +#endif +#ifdef HDMI_CEC_CLAIMED_OPCODE_COUNT +extern const uint8_t CLAIMED_OPCODES[] PROGMEM; +#endif + // Release/acquire store and load. The read-modify-write forms of std::atomic are not available on // every target (the ESP8266 has none), so a writer computes the new value itself; that is safe here // because every atomic in this component has exactly one writer. @@ -42,6 +50,7 @@ class Frame { public: static constexpr size_t MAX_LENGTH = 16; // HDMI-CEC 1.4 static constexpr size_t MAX_PAYLOAD_LENGTH = MAX_LENGTH - 1; + static constexpr uint8_t BROADCAST_ADDRESS = 0xF; // also the address of an unregistered device Frame() = default; /// The payload is at most MAX_PAYLOAD_LENGTH bytes; send() checks that @@ -49,7 +58,7 @@ class Frame { ESPHOME_ALWAYS_INLINE uint8_t initiator_addr() const { return this->data_[0] >> 4; } ESPHOME_ALWAYS_INLINE uint8_t destination_addr() const { return this->data_[0] & 0xf; } - ESPHOME_ALWAYS_INLINE bool is_broadcast() const { return this->destination_addr() == 0xf; } + ESPHOME_ALWAYS_INLINE bool is_broadcast() const { return this->destination_addr() == BROADCAST_ADDRESS; } ESPHOME_ALWAYS_INLINE size_t size() const { return this->size_; } std::span bytes() const { return {this->data_.data(), this->size_}; } /// The bytes after the header, starting with the opcode @@ -113,16 +122,6 @@ class HDMICEC : public Component { /// A lambda may change it at run time, for instance after reading it from the HDMI sink's EDID void set_physical_address(uint16_t physical_address) { this->physical_address_ = physical_address; } uint16_t get_physical_address() const { return this->physical_address_; } - /// Answer "Give OSD Name" with this name; it lives in flash - void set_osd_name(const char *name, uint8_t length) { - this->osd_name_ = name; - this->osd_name_length_ = length; - } - /// Opcodes an on_message automation answers itself, sorted, in flash; no built-in reply for them - void set_claimed_opcodes(const uint8_t *opcodes, uint8_t count) { - this->claimed_opcodes_ = opcodes; - this->claimed_opcode_count_ = count; - } #ifdef USE_HDMI_CEC_UART /// Send the frame body from this UART (its TX pin on the line through a diode) instead of bit-banging it void set_uart(uart::UARTComponent *uart) { this->uart_ = uart; } @@ -177,9 +176,9 @@ class HDMICEC : public Component { static void ack_timer_isr_s(uint alarm_num); #endif - /// The built-in replies to a frame addressed to us that no automation claims + /// The built-in replies to a frame directed at us that no automation claims void reply_builtin_(const Frame *frame); - bool is_claimed_(uint8_t opcode) const; + static bool is_claimed_(uint8_t opcode); // Transmit side, in loop() void transmit_message_(); @@ -190,10 +189,9 @@ class HDMICEC : public Component { bool send_start_bit_(); /// Send one bit; false when a logical 1 found the line held low by another initiator bool send_bit_(bool bit_value); -#ifdef USE_HDMI_CEC_UART - /// Hand the frame after the initiator address to the UART, five UART bits per CEC bit - void transmit_on_uart_(const Frame *frame); -#endif + /// Drive the frame after the initiator address: bit-banged, or from the UART, five UART bits + /// per CEC bit, while loop() goes on + void transmit_body_(const Frame *frame); ESPHOME_ALWAYS_INLINE void set_pin_input_high_() { this->isr_pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); } ESPHOME_ALWAYS_INLINE void set_pin_output_low_() { this->isr_pin_.pin_mode(gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN); @@ -214,8 +212,6 @@ class HDMICEC : public Component { #endif InternalGPIOPin *pin_; ISRInternalGPIOPin isr_pin_; - const char *osd_name_{nullptr}; // in flash - const uint8_t *claimed_opcodes_{nullptr}; // in flash, sorted #ifdef USE_HDMI_CEC_UART uart::UARTComponent *uart_{nullptr}; #endif @@ -232,6 +228,7 @@ class HDMICEC : public Component { uint32_t last_falling_edge_us_{0}; // interrupt only uint32_t last_attempt_us_{0}; // loop() only; a refused start bit drives no edge, so it is timed here uint16_t required_idle_period_{0}; // loop() only + uint16_t physical_address_{UNSET_PHYSICAL_ADDRESS}; // Shared with the interrupt. The tally of our own frame is written by the interrupt while // transmitting_ is set and no end of message has passed yet; loop() reads it once eom_received_ @@ -250,10 +247,7 @@ class HDMICEC : public Component { bool recv_ack_queued_{false}; bool prev_pin_level_{true}; - uint16_t physical_address_{UNSET_PHYSICAL_ADDRESS}; - uint8_t osd_name_length_{0}; - uint8_t claimed_opcode_count_{0}; - uint8_t address_; // logical address, 0xf when only listening + uint8_t address_; // logical address, Frame::BROADCAST_ADDRESS when only listening bool promiscuous_mode_{false}; bool monitor_mode_{false}; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 9adf85971d6..5ff0c2d58bf 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -154,7 +154,9 @@ #define USE_LVGL_TOUCHSCREEN #define USE_MDNS #define USE_MDNS_STORE_SERVICES +#define HDMI_CEC_CLAIMED_OPCODE_COUNT 1 #define HDMI_CEC_MESSAGE_CALLBACK_COUNT 2 +#define HDMI_CEC_OSD_NAME_LENGTH 7 #define USE_HDMI_CEC_FEATURE_ABORT #define USE_HDMI_CEC_UART #define I2C_PORT_FREQUENCY_COUNT 1 diff --git a/tests/components/hdmi_cec/common-uart.yaml b/tests/components/hdmi_cec/common-uart.yaml index 06d22566441..73bdc02058e 100644 --- a/tests/components/hdmi_cec/common-uart.yaml +++ b/tests/components/hdmi_cec/common-uart.yaml @@ -2,12 +2,4 @@ hdmi_cec: id: cec pin: ${cec_pin} # HDMI pin 13, with HDMI pin 17 as ground address: 0x5 - physical_address: 0x4000 - osd_name: ESPHome uart_id: uart_bus # its tx_pin drives the same line through a diode - on_message: - - opcode: 0x8F # Give Device Power Status - then: - - hdmi_cec.send: - destination: !lambda return source; - data: [0x90, 0x01] # Report Power Status: standby