1. Today's topic
Discrete inputs through optocouplers: what actually happens between an external 220 V / 24 V / 12 V signal and an ESP32/STM32 GPIO, and why the output may have pulsation, bounce, delays and false edges. The main idea: an optocoupler does not turn an external signal into an ideal logic level. It turns it into LED current, then phototransistor current, and only then do the pull and filtering circuits produce a level for the MCU.
2. Why this matters
Traffic-light phase inputs may be:
220 V AC
24 V DC
12 V DC
an industrial line
a long cable with interference
a signal through a relay/optocoupler/moduleThe MCU sees only GPIO 0/1. Between them:
- AC after an optocoupler may pulse at 50/100 Hz;
- the phototransistor leaves saturation slowly;
- optocoupler CTR varies;
- a long cable picks up interference;
- a weak pull resistor produces slow edges;
- GPIO interrupts may capture false events.
Therefore, you cannot simply do this:
if (gpio_get_level(INPUT_RED)) {
phase = RED;
}3. The basic circuit
External signal
-> current-limiting resistor
-> LED inside the optocoupler
-> optical coupling
-> phototransistor
-> pull-up / pull-down
-> ESP32/STM32 GPIOA typical circuit:
3.3 V
│
Rpullup
│
├── GPIO
│
optocoupler collector
emitter
│
GNDInversion is common:
external signal present -> GPIO = 0
external signal absent -> GPIO = 1Make this explicit in code:
#define INPUT_ACTIVE_LEVEL 04. DC is simpler than AC
For 24 V DC:
24 V present -> optocoupler LED stays on
24 V absent -> LED is offFor 220 V AC:
+ half-cycle
0
- half-cycle
0If the circuit conducts during only one half-cycle, the output pulses at 50 Hz. If it conducts during both, it pulses at 100 Hz. Presence of 220 V AC therefore often needs to be detected as:
were there pulses within the last 20-50 ms?rather than:
what is the GPIO level right now?5. A software AC detector
#define AC_PRESENT_TIMEOUT_MS 40
typedef struct {
int64_t last_active_us;
bool present;
} ac_input_detector_t;Logic:
if an active level is observed:
last_active_us = now
if now - last_active_us < 40 ms:
AC is present
else:
AC is absentAt 50 Hz, the period is 20 ms. A 30–50 ms window normally spans several half-cycles.
6. Pull-up
The MCU’s internal pull resistor is often weak and varies considerably. An external pull-up is preferable for an optocoupler input:
10 kΩ - lower power consumption, slower edges
4.7 kΩ - faster and more robust
2.2 kΩ - stronger pull, but more phototransistor current7. Optocoupler CTR
CTR = phototransistor current / LED current. It depends on:
- The individual device;
- temperature;
- ageing;
- LED current;
- manufacturing batch;
- saturation conditions.
Check the worst case:
minimum LED current
minimum CTR
maximum temperature
ageing
the strongest pull resistor8. An RC filter
An RC filter smooths interference but adds delay.
τ = R × CAn example:
R = 10 kΩ
C = 100 nF
τ = 1 msBy contrast, 100 kΩ and 1 µF give 100 ms, which may delay events too much.
9. Schmitt trigger
For slow or noisy edges, a buffer with hysteresis is useful:
external signal
-> protection
-> current limiting
-> optocoupler
-> pull-up
-> RC / filter
-> Schmitt trigger / comparator
-> MCU GPIOHysteresis removes oscillation near the threshold.
10. GPIO interrupts or polling
GPIO interrupt:
- Fast timestamp capture;
- good for clean edges;
- poor for noisy AC after an optocoupler.
Polling:
- Filtering is simpler;
- fewer noise-related problems;
- convenient for HIL;
- timestamp uncertainty can be up to one polling period.
For traffic-light inputs, it is better to start with polling every 1–5 ms.
11. A DC input detector
For DC, an integrator can be used:
typedef struct {
uint8_t acc;
bool stable;
} dc_input_filter_t;
#define FILTER_MAX 10
#define FILTER_ON_THRESHOLD 8
#define FILTER_OFF_THRESHOLD 2
static void dc_input_update(dc_input_filter_t *f, bool raw_active)
{
if (raw_active) {
if (f->acc < FILTER_MAX) f->acc++;
} else {
if (f->acc > 0) f->acc--;
}
if (f->acc >= FILTER_ON_THRESHOLD) {
f->stable = true;
} else if (f->acc <= FILTER_OFF_THRESHOLD) {
f->stable = false;
}
}12. Connection to the architecture
input_driver
reads raw GPIO
input_filter
converts raw into stable_present
phase_detector
determines RED/YELLOW/GREEN/OFF/CONFLICT
phase_task
creates phase_event_t with a timestamp
transport_task
sends the event13. Phase conflicts
RED = 1, YELLOW = 0, GREEN = 0 -> RED
RED = 0, YELLOW = 1, GREEN = 0 -> YELLOW
RED = 0, YELLOW = 0, GREEN = 1 -> GREEN
RED = 0, YELLOW = 0, GREEN = 0 -> OFF/UNKNOWN
RED = 1, YELLOW = 1, GREEN = 0 -> RED_YELLOW or TRANSITION
RED = 1, YELLOW = 0, GREEN = 1 -> CONFLICTImpossible combinations must be logged explicitly.
14. Common mistakes
- Reading an AC input as an ordinary level;
- using one filter for AC and DC;
- choosing excessively long debounce;
- omitting an external pull resistor;
- forgetting inversion;
- doing heavy processing in a GPIO ISR;
- not measuring the actual signal with an oscilloscope.
15. Practical task
Create INPUT_FILTER_POLICY.md:
# Input filter policy
## Input types
- AC optocoupler input:
Detect presence of pulses within a time window.
Do not use instantaneous GPIO level as final state.
- DC optocoupler input:
Use integrator/debounce filter.
## Timing
- Polling period: 1-5 ms
- AC present timeout: start with 40 ms for 50 Hz mains
- Stable confirm time: 20-50 ms
- Timestamp policy:
Capture raw sample timestamp immediately.
## Signal inversion
All input channels must define active level explicitly.
## Diagnostics
Track:
- raw pulse count
- stable transitions
- glitches
- time since last pulse
- impossible phase combinations16. A short recap
An optocoupler does not provide an ideal logic level. For an AC input, the correct model is:
the signal is active
if there were pulses within the last 30-50 msrather than:
the signal is active
if gpio_get_level() equals active_level right nowExercise
Using τ=R×C, calculate the nominal time constant for R=10 kΩ and C=100 nF. Explain what this number does not establish about the complete input detector.
Self-check criteria: Calculate 1 ms and distinguish an RC time constant from full detector timing or hardware validation.
Show the supplied answer
τ=10000×100×10^-9 s=0.001 s=1 ms. It is an RC time constant, not the complete phase-detection latency or a validated threshold. Optocoupler behaviour, signal shape, polling and confirmation policy also affect the detector; the source’s examples require measurement for the actual design.