(software) Interrupt driven functions

Hey all,

I’m trying to build a component for a 3rd party device, where I have no source or firmware for. After analyzing it seems it something resembling SPI closely, but not quite. For arguments sake, lets assume it is an SPI master device, and the ESPHome device would be the slave.

So far I have seen two variants, an older model, that included an interrupt line (very similar to CS of SPI). A newer model seems to abandon CS in favor of ‘encoding’ it in the clock signal (more on this in a moment).

The nice thing of a CS signal for a slave device (assuming it is implemented properly, e.g. asserted on start, de-asserted at end, and not ‘forever on’), is that we know exactly we have to start processing data. Since (for now) we have to rely on bit-banging, a hardware peripherial that reads in the data on CS is not possible.

So then we can either while(digital_read()) the pin (with timeout) until it changes, or trigger an interrupt. I saw esphome certainly supports interrupt driven pins.

Now here is my conundrum. ISR’s should be short and sweet, e.g. inform the main loop to read out the SPI buffer or use it to trigger the mainloop (if it is fast enough) to start reading the pin.

In this case however, esphome will trigger ‘our’ loop() function every ~16ms. So we’d have to be really lucky to enter our loop() just after the ISR triggers. Reading the (whole) SPI frame during the ISR is absolutely not something we’d want. Ideally, we’d set a callback function that in the ISR would call (after re-enabling all interrupts again) or some such. But I don’t think ESPHome is setup to handle this cleanly?

Just FYI on the second case, which is not super dissimilar, is that the chip-select is encoded in the clock pin, in that there is a 40ms pause of the clock between a 10ms frame of data. So we could monitor the clock pin like before (with timeout) until it has been asserted for at least 5ms (assuming the time between bits and bytes is shorter then 5ms, or even wait for the full 40ms, but that would be quite wasteful and unnecessary) to ‘sync’ to the end/start of a frame.

Reason I’m mentioning it for now, is just to get some thoughts. My concerns are (for the future) is that if this is SPI, and we can use a hardware SPI periphieral, we’d now have dual ‘role’ of the clock pin, e.g. monitor on the sync moment, to trigger the SPI peripherial to do things. I don’t know how common it is to reconfigure pins at runtime.

Pulling the clock in on two pins is a nother way, use a GPIO to monitor the clock, if the 5ms of assertion have been reached, enable the SPI hardware peripheral and toggle the CS manually, but that ‘costs’ two additional GPIO’s that may not be available, the clock monitor pin, the CS assertion pin, and the actual CS of the peripheral. Where I’m not sure what the consequences are of connecting a clock pin (in my particular case after a level shifter, so it’s not so bad) to two pins …

13 days over and no help…
Let me me help as much I can with my experience and limited resources…

I encountered this when I reverse engineer a product I want to use with esphome.

Scenario-
esp8266 is connected to an stm8s to expand the inputs.
Stm takes the 8 inputs and send over to esp to process - only two pins are used.
No idea which protocal used.
Used a logic analyser with Pulse View ( I don’t have an oscilloscope. And scenario like this, logic analyser is your best friend) to get the pulses of the two signal lines coming from stm to esp.
Two weeks of RND…
Realised it’s SPI protocol, stm using the interrupt pin of esp to let know there is data available and using the same pin esp returns the clock. Other pin transfers the data.
Wrote a custome component… voila…

Are there any custom components examples that work with the new ESPHOME infrastructure?

Previous ESPHOME allowed inclusion of Arduino style interrupts by including libraries that had the Arduino ISRs in the .yaml file.

The current infrastructure allows inclusion of ISR libraries in a “my_components” directory.

I have a simple ISR(lcdE_isr) triggered by the falling edge of an LCD interface ‘E’ signal that simply sets a flag. The main code checks this flag, processes data and then resets the flag.

Using the standard ‘attachInterrupt’ fails with multiple errors.
It is difficult to understand the code that allows the compilation to succeed:

#include "FunctionalInterrupt.h"  	
...
attachInterrupt(digitalPinToInterrupt(enable), std::bind(&LCD::lcdE_isr, this), FALLING); // RISING, FALLING, CHANGE

I am still debugging …

Short answer:
No. Custom components have been deprecated. They are no longer supported and the code that allowed them to work has been removed.

They have been replaced by external components which are supposed to be easier to use (but not to develop).

There is an external component that brings back the deprecated capability of custom components. It might work for you.

Thanks Neel.
I have already included that in my current attempt at a solution.
Portion of code in yaml:

external_components:
  # use all components from a local folder
  - source:
      type: local
      path: my_components
esphome:
  name: "${name}"
  on_boot:
    then:
      - logger.log: "!!!!->on_boot: ${project_name}, ${project_version}"
  project:
    name: "${project_name}"
    version: "${project_version}"
  libraries:
    - "spi"
    - "Wire"
  includes:
    - my_components/libraries/LCD.cpp
    - my_components/libraries/LCD.h

My problem is in the interrupt handler setup{} code.
Do you have any comments/suggestions for the “attachInterrupt” code?

These are copilots suggestions:
The provided code appears to be functional, but there are a few potential issues and areas for improvement:

1. Interrupt Service Routine (ISR)

  • The lcdE_isr function is marked with IRAM_ATTR, which is correct for ISRs on ESP32. However:
    • Ensure that no non-ISR-safe functions (e.g., std::bind) are used within the ISR or its setup (attachInterrupt).
    • The ISR modifies this->pindata and this->LCDE_Flag, which are volatile. This is correct, but ensure that these variables are accessed atomically in other parts of the code to avoid race conditions.

attachInterrupt Usage

  • The attachInterrupt function uses std::bind, which may allocate memory dynamically. This is not recommended for ISRs. Consider using a static function or lambda if possible.

Fix:

attachInterrupt(digitalPinToInterrupt(enable), { lcdE_isr(); }, FALLING

Unfortunately, neither worked.
The code compiles, downloads, but alas no data !!

As far as I can tell esphome is NOT a good choice if you NEED interrupts. There is no native support for it and most/all components do not depend on interrupts.

Having said that, it is possible to get interrupts to work, but it likely isn’t easy if you don’t really know what you are doing. I was able to get software serial to work in esphome as an external component. It uses interrupts.

Isn’t there some support for the LCD you are using?
Why do you need interrupts?
Using AI to generate esphome code is usually going to cause more problems than solutions.

Hi Neel,
The device is used to monitor legacy items that use a standard LCD 16-pin interface.
It monitors the ‘E’ signal and then captures the info for later transmission over BT or WiFi.
The interrupt is required to trigger the capture of the data in less than 50uS. It is buffered and then later processed into data items.

When I have this kind of task, I do it without Esphome. I dedicate one Esp32 only for that task and send the data to Esphome board.

1 Like

This is something that is not within the things which esphome does well. You might be able to make it work, but why? You should probably use a better tool for this task.

1 Like

This is not new code. It has been running as a custom component for several years. The recent version update means it won’t compile. And I am not sure yet whether the new ‘architecture’ will allow it to run. The code was developed independently from AI, but I use copilot to review.

The reason that I chose esphome is that it runs independently from HA. If the HA server is down you can still access any esphome device via the web interface.

In my standalone system (an offroad RV), sometimes the server is down (or powered down to save battery). When that happens you can still control and monitor all devices and their web pages via a smartphone.

My next project is to integrate that access into an esphome touch-display.

Who wrote the code?

Your choices are (from easier to harder):

  1. Keep using the old esphome version
  2. Use the external component that allows custom components to continue working
  3. Convert the old custom component to an external component

copilot seems like a generally bad idea for esphome. I have yet to see it do something well in that space.

The “new” architecture requires you to do some things to get it to generate code that then might or might not compile that then might or might not actually do what you want.

One of the advantages of esphome is that it lets you hook up predefined bits of code (components) without needing to understand how to program in c++. This works really well if you stay within the constraints (only use well-crafted components). As soon as you need to do something outside of this well worn path, it can get incredibly difficult.

Hi Neel,
I wrote the code - derived from an existing Arduino component.
The original question asked if there were any examples that allowed inclusion of Arduino style interrupts - I was hoping that someone had figured it out. I can get a reasonable distance using the excellent work of RobertKlep. But the new architecture still complains about “attach_interrupt” in the way that it is coded in arduino style.
Indeed, interrupts are possible and there are examples in the component library (eg pulse_counter_sensor) that help in coding. I understand that using the “external_component” path is probably the way to go, and I have already started coding the skeleton of that:

void LCDRdSensorStore::setup(InternalGPIOPin *pin_clock, InternalGPIOPin *pin_data, InternalGPIOPin *pin_enable) {
  pin_enable->setup();
  this->pin_enable_ = pin_enable->to_isr();
  pin_enable->attach_interrupt(LCDRdSensorStore::interrupt_enable, this, gpio::INTERRUPT_FALLING_EDGE);
}

void IRAM_ATTR LCDRdSensorStore::interrupt_enable(LCDRdSensorStore *arg) {
  bool data_bit = arg->pin_data_.digital_read();
}

However … there are a lot of Arduino coders out there that have little or no experience with c++, but have written valuable libraries and code. And there are some coders out there who understand some c++ and the value of classes and data abstraction.

But …

this->pin_enable_ = pin_enable->to_isr();

???

I am sure I will get there eventually, and thankyou for your help.

The simple answer is that what you are trying to is not something that will be easy. esphome doesn’t provide any examples of using interrupts and it generally doesn’t appear to use them, even where I would expect (binary sensor) and uses polling instead. So, if you want to use interrupts you have to find the magic to make them work. This will likely involve “going behind esphome’s back” to make it work.

You haven’t shared a complete example of what you have tried and exactly what error message(s) you got. Like with many things, once you figure it out the error message makes sense. But, if you haven’t seen the particular error before, the message can be very hard to interpret.

Hi Neel,
The code works now and with interrupts with the new esphome version.
My mistake was pretty simple in the original post above - and copilot picked it up!

  	attachInterrupt(digitalPinToInterrupt(enable), std::bind(&LCD::lcdE_isr, this), RISING); // RISING, FALLING, CHANGE

instead of

  	attachInterrupt(digitalPinToInterrupt(this->enable), std::bind(&LCD::lcdE_isr, this), RISING); // RISING, FALLING, CHANGE

enable was the pin number captured during the creation of the LCD instance.

But I will take your advice and continue learning about the new external_component architecture. It has a lot more features and checking. Once again, thanks for your help and advice.

Glad you got it working.