zigEFI supports multiple ignition control strategies, from fully mechanical to full ECU control. The mode is selected in EngineConfig.ignition.mode at compile time.
config: .mode = .disabled
The distributor's mechanical and vacuum advance mechanisms control timing. The ECU reads the VR pickup signal for RPM measurement only. No ignition outputs are used.
Use this when:
- Running a stock distributor with functional advance mechanisms
- Developing and testing fuel-only control before adding ignition
- You don't trust the ECU yet (smart)
Wiring:
Distributor VR pickup ──→ uaEFI VR2 input (PE1) [RPM only]
Ignition module ──→ Coil ──→ Distributor cap [stock wiring]
config: .mode = .distributor
The ECU replaces the factory ignition module (e.g. Ford Duraspark II, TFI, GM HEI module). The distributor cap still routes spark to the correct cylinder — the ECU controls when the spark fires using a programmable advance table.
Requirements:
- Lock out mechanical advance (weld centrifugal weights or use a locked distributor)
- Block vacuum advance (cap the vacuum port)
- Remove the factory ignition module
- Wire the VR pickup directly to the ECU
- Wire the ECU ignition output to a coil driver or directly to the coil
Wiring:
Distributor VR pickup ──→ uaEFI VR2 input (PE1) [position + RPM]
uaEFI IGN1 output (PC13) ──→ Coil driver ──→ Coil [timing control]
Coil high voltage ──→ Distributor cap ──→ Plugs [spark distribution]
How it works:
- VR pickup generates a pulse as each reluctor tooth passes (V8 = 4 pulses/crank rev)
- Timer input capture ISR records the pulse timestamp
- Period between pulses gives RPM
- Look up advance from the 2D table (RPM x MAP)
- Calculate fire delay relative to the VR pulse:
degrees_between_pulses = 360 / distributor_pulses_per_rev (V8 = 90 degrees)
time_per_degree = pulse_period_us / degrees_between_pulses
fire_delay = (ref_angle - desired_advance) * time_per_degree
Where ref_angle is the mechanical angle of the VR pickup with advance locked out (typically 10 degrees BTDC on a Ford).
- Schedule dwell start:
dwell_start = fire_time - dwell_us - At
dwell_start: set IGN1 HIGH (coil charging) - At
fire_time: set IGN1 LOW (coil releases, spark fires)
Timing diagram:
VR pulse next VR pulse
│ │
▼ ▼
────┤ ├────
│ │
│ ┌─────────┐ │
IGN1│ │ DWELL │ │
────┘ └─────────┘───┘
↑ ↑
dwell start SPARK
Advance table:
The advance table is 16x16, RPM x MAP, with values in degrees BTDC. More advance at light load (high vacuum / low MAP), less at heavy load (high MAP / low vacuum). This replaces both the mechanical weights (RPM-based advance) and vacuum canister (load-based advance).
.advance_table = .{
// vacuum ◄──────────────────────────────► full load
// 15 20 25 30 35 40 50 60 70 80 85 90 95 100 kPa
.{ 34, 33, 32, 31, 29, 28, 26, 24, 22, 20, 18, 17, 16, 15 }, // 2000 RPM
.{ 38, 37, 36, 34, 33, 31, 29, 27, 25, 23, 21, 19, 18, 17 }, // 3000 RPM
// more advance at high RPM + light load
// less advance at high RPM + heavy load (knock prevention)
};Dwell compensation:
Coil charge time (dwell) must increase as battery voltage drops to maintain spark energy. The dwell_voltage_comp table maps voltage to dwell time:
14V = 3000 us (base)
12V = 3600 us
10V = 4200 us
8V = 5000 us
Ford-specific notes (1982 SBF 302):
- Duraspark II distributor: 8-tooth reluctor, ~10 degrees BTDC mechanical reference
- Remove the blue Duraspark II ignition module (on the fender or firewall)
- VR pickup has 2 wires (orange + purple on Ford) — connect to uaEFI VR2 input
- Use a coil driver module between uaEFI IGN1 and the coil, or a logic-level coil (e.g. LS2 truck coil)
- The uaEFI IGN outputs have on-board smart coil drivers rated for most modern coils
- Stock Duraspark coil is fine but draws more current — verify against uaEFI driver ratings
config: .mode = .coil_on_plug
NOT YET IMPLEMENTED — this section documents the architecture for future development.
Full ECU-controlled ignition with individual coil outputs. No distributor — the ECU fires each coil directly. Requires crank and cam position sensors for cylinder identification.
Modern engines use a toothed wheel on the crankshaft (typically 36-1 or 60-2 pattern) and a cam sensor for cylinder identification.
Trigger wheel patterns:
| Pattern | Description | Common on |
|---|---|---|
| 36-1 | 36 teeth, 1 missing (10 degrees/tooth) | Ford, GM, many aftermarket |
| 60-2 | 60 teeth, 2 missing (6 degrees/tooth) | GM LS, many modern engines |
| 36-1-1 | 36 teeth, 2 singles missing | Some Bosch applications |
Cam sync patterns:
| Pattern | Description | Purpose |
|---|---|---|
| Single tooth | 1 pulse per cam revolution | Identifies cylinder #1 for sequential |
| Multi-tooth | Multiple pulses at known positions | Faster sync, VVT position feedback |
1. Crank ISR fires on every tooth edge
2. Measure tooth-to-tooth period
3. Detect missing tooth: period > 1.5x average → gap found
4. Count teeth from gap → precise crank angle (within 6-10 degrees)
5. Interpolate between teeth for sub-tooth-degree precision
6. Cam sync pulse identifies which revolution (1 of 2 in 4-stroke cycle)
7. After sync: schedule each coil output at its cylinder's advance angle
Inputs:
- Crank VR/Hall sensor → uaEFI VR2 (PE1) or VR1 (PE0)
- Cam Hall sensor → uaEFI HALL1 (PE12)
Outputs (wasted spark, V8 = 4 coils):
- IGN1 (PC13) → Coil pack 1 (cyl 1+6)
- IGN2 (PE5) → Coil pack 2 (cyl 5+8)
- IGN3 (PE4) → Coil pack 3 (cyl 4+3)
- IGN4 (PE3) → Coil pack 4 (cyl 2+7)
Outputs (coil-on-plug, V8 = 8 coils):
- IGN1-IGN6 → 6 individual coils
- Plus 2 additional coil outputs via INJ7/INJ8 or other GPIO
src/engine/
crank_decoder.zig Tooth counting, gap detection, angle tracking
cam_sync.zig Cylinder identification from cam sensor
scheduler.zig Angle-based event scheduling (replaces simple timer delay)
| Aspect | Distributor | Coil-on-Plug |
|---|---|---|
| Position resolution | ~90 degrees (V8) | 6-10 degrees (per tooth) |
| Outputs | 1 (IGN1) | 4-8 (one per coil) |
| Cam sensor needed | No | Yes |
| Sync time | Immediate (1 pulse) | 1-2 crank revolutions |
| Cylinder ID | Distributor cap handles it | Cam sensor + software |
| Code complexity | Low | High |
Distributor mode has one input, one output, and simple delay math. Coil-on-plug needs:
- Real-time tooth counting at up to 400+ teeth/second (7500 RPM * 60 teeth / 60 sec)
- Sub-degree interpolation between teeth for precise timing
- Synchronization state machine (unsync → crank sync → full sync)
- Per-cylinder dwell and fire scheduling
- Loss-of-sync detection and recovery
- VVT angle feedback (if variable valve timing is used)
This is the hardest part of ECU firmware — it's where rusEFI and Speeduino invest most of their complexity. zigEFI will tackle it after the distributor mode is proven.
.disabled— fuel-only, distributor handles timing (current).distributor— ECU-controlled timing, single output (next).coil_on_plug— full crank/cam decoding, individual coils (future)
Each mode is a superset of the previous. The VR input capture and advance table lookup from distributor mode are reused in coil-on-plug mode — just with higher resolution position tracking and multiple outputs.