ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
hdmi_cec.h
Go to the documentation of this file.
1#pragma once
2
3#include <array>
4#include <atomic>
5#include <initializer_list>
6#include <span>
7
10#include "esphome/core/gpio.h"
12
13#ifdef USE_ESP32
14#include "driver/gptimer.h"
15#endif
16#ifdef USE_RP2040
17#include <hardware/timer.h>
18#endif
19
20namespace esphome::hdmi_cec {
21
22// Release/acquire store and load. The read-modify-write forms of std::atomic are not available on
23// every target (the ESP8266 has none), so a writer computes the new value itself; that is safe here
24// because every atomic in this component has exactly one writer.
25template<typename T> ESPHOME_ALWAYS_INLINE void atomic_set(std::atomic<T> &atom, T value) {
26 atom.store(value, std::memory_order_release);
27}
28template<typename T> ESPHOME_ALWAYS_INLINE T atomic_get(const std::atomic<T> &atom) {
29 return atom.load(std::memory_order_acquire);
30}
31
35class Frame {
36 public:
37 static constexpr size_t MAX_LENGTH = 16; // HDMI-CEC 1.4
38 static constexpr size_t MAX_PAYLOAD_LENGTH = MAX_LENGTH - 1;
39
40 Frame() = default;
42 Frame(uint8_t initiator_addr, uint8_t destination_addr, std::span<const uint8_t> payload);
43
44 ESPHOME_ALWAYS_INLINE uint8_t initiator_addr() const { return this->data_[0] >> 4; }
45 ESPHOME_ALWAYS_INLINE uint8_t destination_addr() const { return this->data_[0] & 0xf; }
46 ESPHOME_ALWAYS_INLINE bool is_broadcast() const { return this->destination_addr() == 0xf; }
47 ESPHOME_ALWAYS_INLINE size_t size() const { return this->size_; }
48 std::span<const uint8_t> bytes() const { return {this->data_.data(), this->size_}; }
50 std::span<const uint8_t> payload() const { return this->bytes().subspan(1); }
51 ESPHOME_ALWAYS_INLINE void clear() { this->size_ = 0; }
53 ESPHOME_ALWAYS_INLINE bool push_back(uint8_t data) {
54 if (this->size_ == MAX_LENGTH) {
55 return false;
56 }
57 this->data_[this->size_++] = data;
58 return true;
59 }
60
61 protected:
62 std::array<uint8_t, MAX_LENGTH> data_{};
63 uint8_t size_{0};
64};
65
70template<size_t SIZE> class FrameRingBuffer {
71 static_assert((SIZE & (SIZE - 1)) == 0, "SIZE must be a power of two");
72
73 public:
74 ESPHOME_ALWAYS_INLINE Frame *front() { return this->is_empty() ? nullptr : this->slot_(this->front_inx_); }
75 ESPHOME_ALWAYS_INLINE void pop_front() { this->advance_(this->front_inx_); }
76 ESPHOME_ALWAYS_INLINE Frame *back() { return this->count_() == SIZE ? nullptr : this->slot_(this->back_inx_); }
77 ESPHOME_ALWAYS_INLINE void push_back() { this->advance_(this->back_inx_); }
78 ESPHOME_ALWAYS_INLINE bool is_empty() const { return this->count_() == 0; }
79
80 protected:
81 ESPHOME_ALWAYS_INLINE uint8_t count_() const {
82 return static_cast<uint8_t>(atomic_get(this->back_inx_) - atomic_get(this->front_inx_));
83 }
84 ESPHOME_ALWAYS_INLINE Frame *slot_(const std::atomic<uint8_t> &inx) { return &this->store_[atomic_get(inx) % SIZE]; }
85 ESPHOME_ALWAYS_INLINE void advance_(std::atomic<uint8_t> &inx) {
86 atomic_set<uint8_t>(inx, static_cast<uint8_t>(atomic_get(inx) + 1));
87 }
88
89 std::array<Frame, SIZE> store_{};
90 std::atomic<uint8_t> front_inx_{0};
91 std::atomic<uint8_t> back_inx_{0};
92};
93
97class HDMICEC : public Component {
98 public:
100 void setup() override;
101 void dump_config() override;
102 void loop() override;
103
104 void set_promiscuous_mode(bool promiscuous) { this->promiscuous_mode_ = promiscuous; }
105 void set_monitor_mode(bool monitor_mode) { this->monitor_mode_ = monitor_mode; }
106#ifdef HDMI_CEC_MESSAGE_CALLBACK_COUNT
109 template<typename F> void add_on_message_callback(F &&callback) {
110 this->message_callbacks_.add(std::forward<F>(callback));
111 }
112#endif
113
115 bool send(uint8_t source, uint8_t destination, std::span<const uint8_t> payload);
116 bool send(uint8_t destination, std::span<const uint8_t> payload) {
117 return this->send(this->address_, destination, payload);
118 }
120 bool send(uint8_t destination, std::initializer_list<uint8_t> payload) {
121 return this->send(destination, std::span<const uint8_t>(payload.begin(), payload.size()));
122 }
123
124 protected:
131
132 // Receive side, in the interrupt
133 static void gpio_isr_s(HDMICEC *self);
134 void gpio_isr_();
135 void reset_receiver_();
137 void send_ack_();
138 // A hardware one-shot timer releases the line after the acknowledge bit (gptimer on the ESP32,
139 // timer 1 on the ESP8266, a hardware alarm on the RP2040), so the interrupt returns at once;
140 // without one the interrupt holds the line for the 1.5 ms itself
141 bool setup_ack_timer_();
142 void start_ack_timer_();
143#ifdef USE_ESP32
144 static bool ack_timer_isr_s(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *arg);
145#endif
146#ifdef USE_ESP8266
147 static void ack_timer_isr_s();
148#endif
149#ifdef USE_RP2040
150 static void ack_timer_isr_s(uint alarm_num);
151#endif
152
153 // Transmit side, in loop()
154 void transmit_message_();
156 void finish_attempt_(bool sent_ok);
158 bool transmit_frame_(const Frame *frame);
159 bool send_start_bit_();
161 bool send_bit_(bool bit_value);
162 ESPHOME_ALWAYS_INLINE void set_pin_input_high_() { this->isr_pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); }
163 ESPHOME_ALWAYS_INLINE void set_pin_output_low_() {
165 this->isr_pin_.digital_write(false);
166 }
167
168 static constexpr size_t QUEUE_SIZE = 4;
169 // Each queue is 70 bytes, so two single-byte fields follow each to fill the alignment gap
170 FrameRingBuffer<QUEUE_SIZE> frames_queue_; // received, interrupt to loop()
172 uint8_t recv_byte_{0};
173 FrameRingBuffer<QUEUE_SIZE> send_queue_; // to send, both ends on loop()
176#ifdef HDMI_CEC_MESSAGE_CALLBACK_COUNT
177 StaticCallbackManager<HDMI_CEC_MESSAGE_CALLBACK_COUNT, void(uint8_t, uint8_t, std::span<const uint8_t>)>
179#endif
182#ifdef USE_ESP32
183 gptimer_handle_t ack_timer_{nullptr};
184#endif
185#ifdef USE_RP2040
186 int ack_alarm_{-1};
187#endif
188
189 // The word-sized fields come first and the single-byte ones form one run, so nothing is padded
190 std::atomic<uint32_t> last_edge_us_{0}; // the bus is busy until this is some time ago; interrupt writes
191 Frame *recv_frame_{nullptr}; // interrupt only; null when the queue was full or the frame too long
192 uint32_t last_falling_edge_us_{0}; // interrupt only
193 uint32_t last_attempt_us_{0}; // loop() only; a refused start bit drives no edge, so it is timed here
194 uint16_t required_idle_period_{0}; // loop() only
195
196 // Shared with the interrupt. The tally of our own frame is written by the interrupt while
197 // transmitting_ is set and no end of message has passed yet; loop() reads it once eom_received_
198 // says the frame is over
199 std::atomic<bool> eom_received_{false}; // set for every frame, ours or not
200 std::atomic<uint8_t> dropped_frames_{0};
203 bool transmitting_{false};
204
205 // Receive state, interrupt only
206 uint8_t recv_header_{0}; // kept outside the frame so a dropped frame can still be judged
207 uint8_t recv_length_{0};
209 bool recv_eom_{false};
210 bool recv_ack_queued_{false};
211 bool prev_pin_level_{true};
212
213 uint8_t address_; // logical address, 0xf when only listening
214 bool promiscuous_mode_{false};
215 bool monitor_mode_{false};
216};
217
218} // namespace esphome::hdmi_cec
uint8_t address
Definition bl0906.h:4
Copy of GPIOPin that is safe to use from ISRs (with no virtual functions)
Definition gpio_pin.h:98
void digital_write(bool value)
Definition gpio.cpp:227
void pin_mode(gpio::Flags flags)
Definition gpio.cpp:239
CallbackManager backed by StaticVector for compile-time-known callback counts.
Definition helpers.h:1884
One CEC bus message: a header byte with the initiator and destination addresses, then up to 15 payloa...
Definition hdmi_cec.h:35
ESPHOME_ALWAYS_INLINE bool push_back(uint8_t data)
False when the frame is full already; the bus allows no more.
Definition hdmi_cec.h:53
std::span< const uint8_t > bytes() const
Definition hdmi_cec.h:48
static constexpr size_t MAX_LENGTH
Definition hdmi_cec.h:37
static constexpr size_t MAX_PAYLOAD_LENGTH
Definition hdmi_cec.h:38
std::span< const uint8_t > payload() const
The bytes after the header, starting with the opcode.
Definition hdmi_cec.h:50
ESPHOME_ALWAYS_INLINE uint8_t initiator_addr() const
Definition hdmi_cec.h:44
ESPHOME_ALWAYS_INLINE size_t size() const
Definition hdmi_cec.h:47
ESPHOME_ALWAYS_INLINE bool is_broadcast() const
Definition hdmi_cec.h:46
ESPHOME_ALWAYS_INLINE uint8_t destination_addr() const
Definition hdmi_cec.h:45
std::array< uint8_t, MAX_LENGTH > data_
Definition hdmi_cec.h:62
ESPHOME_ALWAYS_INLINE void clear()
Definition hdmi_cec.h:51
A queue of frames between one producer and one consumer, for instance the receive interrupt and loop(...
Definition hdmi_cec.h:70
std::atomic< uint8_t > front_inx_
Definition hdmi_cec.h:90
ESPHOME_ALWAYS_INLINE void pop_front()
Definition hdmi_cec.h:75
ESPHOME_ALWAYS_INLINE void advance_(std::atomic< uint8_t > &inx)
Definition hdmi_cec.h:85
ESPHOME_ALWAYS_INLINE Frame * back()
Definition hdmi_cec.h:76
std::atomic< uint8_t > back_inx_
Definition hdmi_cec.h:91
ESPHOME_ALWAYS_INLINE void push_back()
Definition hdmi_cec.h:77
ESPHOME_ALWAYS_INLINE Frame * slot_(const std::atomic< uint8_t > &inx)
Definition hdmi_cec.h:84
ESPHOME_ALWAYS_INLINE uint8_t count_() const
Definition hdmi_cec.h:81
ESPHOME_ALWAYS_INLINE Frame * front()
Definition hdmi_cec.h:74
ESPHOME_ALWAYS_INLINE bool is_empty() const
Definition hdmi_cec.h:78
std::array< Frame, SIZE > store_
Definition hdmi_cec.h:89
The HDMI-CEC bus on one GPIO pin.
Definition hdmi_cec.h:97
std::atomic< uint32_t > last_edge_us_
Definition hdmi_cec.h:190
void dump_config() override
Definition hdmi_cec.cpp:58
FrameRingBuffer< QUEUE_SIZE > send_queue_
Definition hdmi_cec.h:173
void setup() override
Definition hdmi_cec.cpp:42
static void gpio_isr_s(HDMICEC *self)
Definition hdmi_cec.cpp:333
std::atomic< uint8_t > dropped_frames_
Definition hdmi_cec.h:200
bool send(uint8_t source, uint8_t destination, std::span< const uint8_t > payload)
Queue a frame for transmission; false when the queue is full or in monitor mode.
Definition hdmi_cec.cpp:96
gptimer_handle_t ack_timer_
Definition hdmi_cec.h:183
static void ack_timer_isr_s()
Definition hdmi_cec.cpp:304
StaticCallbackManager< HDMI_CEC_MESSAGE_CALLBACK_COUNT, void(uint8_t, uint8_t, std::span< const uint8_t >)> message_callbacks_
Definition hdmi_cec.h:178
ISRInternalGPIOPin isr_pin_
Definition hdmi_cec.h:181
bool send(uint8_t destination, std::initializer_list< uint8_t > payload)
For lambdas: id(cec).send(0x0, {0x44, 0x41});.
Definition hdmi_cec.h:120
void add_on_message_callback(F &&callback)
Called with the initiator, the destination and the payload (opcode first) of every received frame; th...
Definition hdmi_cec.h:109
void send_ack_()
Pull the line low for the acknowledge bit of a frame addressed to us.
Definition hdmi_cec.cpp:258
ESPHOME_ALWAYS_INLINE void set_pin_output_low_()
Definition hdmi_cec.h:163
static constexpr size_t QUEUE_SIZE
Definition hdmi_cec.h:168
void finish_attempt_(bool sent_ok)
Keep the frame for another attempt, or drop it once the attempts are used up.
Definition hdmi_cec.cpp:181
ReceiverState receiver_state_
Definition hdmi_cec.h:208
void set_promiscuous_mode(bool promiscuous)
Definition hdmi_cec.h:104
bool send(uint8_t destination, std::span< const uint8_t > payload)
Definition hdmi_cec.h:116
void set_monitor_mode(bool monitor_mode)
Definition hdmi_cec.h:105
std::atomic< bool > eom_received_
Definition hdmi_cec.h:199
HDMICEC(InternalGPIOPin *pin, uint8_t address)
Definition hdmi_cec.h:99
bool send_bit_(bool bit_value)
Send one bit; false when a logical 1 found the line held low by another initiator.
Definition hdmi_cec.cpp:236
ESPHOME_ALWAYS_INLINE void set_pin_input_high_()
Definition hdmi_cec.h:162
FrameRingBuffer< QUEUE_SIZE > frames_queue_
Definition hdmi_cec.h:170
InternalGPIOPin * pin_
Definition hdmi_cec.h:180
bool transmit_frame_(const Frame *frame)
Drive the frame onto the line; false when another initiator sent at the same time.
Definition hdmi_cec.cpp:193
ESPHOME_ALWAYS_INLINE void atomic_set(std::atomic< T > &atom, T value)
Definition hdmi_cec.h:25
ESPHOME_ALWAYS_INLINE T atomic_get(const std::atomic< T > &atom)
Definition hdmi_cec.h:28
static void uint32_t