Von der Polling Sache würde ich Abstand nehmen, da sich die Programmlaufzeit mit jeder Programmänderung wieder verändert und voll in die Messung mit eingeht.

Deshalb würde die Zeiterfassung komplett in Interrupts laufen lassen.
Als mögliche Quellen kämen beim ATMEGA16: INT0, INT1, INT2 sowie der ICP1 in Frage.
Man könnte auch noch den analog Komperator dazu verwenden.
Als Zeitquelle kommt der Timer1 in Frage.
Der Ablauf wäre:
INT0 liest die Zeit aus dem Timer1 ( + Uberlaufvariable ) aus und speichert sie in einer Variable

INT1 liest die Zeit aus dem Timer1 ( + Uberlaufvariable ) aus und zieht den Wert von INT0 ab, speichert diesen Wert ab und setzt ein Flag.

INT2 liest die Zeit aus dem Timer1 ( + Uberlaufvariable ) aus und zieht den Wert von INT0 ab, speichert diesen Wert ab und setzt ein Flag.

ICP1 liest die Zeit aus dem Timer1 ( + Uberlaufvariable ) aus und zieht den Wert von INT0 ab, speichert diesen Wert ab und setzt ein Flag.

Im Hauptprogramm werden dann die Flags abgefragt und die Messwerte ( Geschwindigkeit ) daraus berechnet.
Wenn die Messwerte verarbeitet wurden werden im Hauptprogramm die Flags wieder gelöscht.
Der bezug auf Timer 0 deshalb, wenn eine Lichtschranke nicht reagiert, wäre die komplette Messung der nachfolgenden Lichtschranken nicht mehr auswertbar.

Man kann auch im Timer1Overflow Interrupt noch eine weitere Variable hochzählen, die die höherwertigen Bytes der Zeitmessung darstellen.

Einen Schönheitsfehler hat die Messmethode mit dem Überlauf noch- während des Zeitmessinterrupts ( z.B. INT0 ) könnte gleichzeitig noch ein Timer Overflow Interrupt anliegen, der aber nicht verarbeitet werden kann.
Als "Bastellösung" könnte man kurz vor dem Auslesen der Timer Messwerte die Interrupts mit "SEI" freigeben ( sind normalerweise während eines laufenden Interrupts gesperrt ) und danach mit "CLI" wieder sperren.
Dadurch verringert sich die Chance auf eine Fehlmessung auf ein paar wenige Prozessortakte.
Also:
#asm ("sei");
#asm ("nop");
#asm ("cli");
ui_int0low=TCNT1;
li_int0high=ui_tcnt1overflow;
li_int0high=(li_int0high*65536)+ui_int0low;
ub_int0flag=1; // Bei INT0 wäre es eigentlich nicht nötig

Der gewünschte Messwert liegt also jetzt in der Variablen li_int0high als 32Bit Integer Wert.
Das wäre eigentlich schon die Ganze Interruptroutine.
Der Rest ( Berechnung ) kann bequem in der Hauptroutine erledigt werden.
Inlineassembler könnte das Problem noch weiter entschärfen.

Die Prellgeschichte könnte man durch ein MonoFlop lösen.
Wird ein Signal von der Lichtschranke ausgegeben wird eine MonoFlop getriggert, das für mindestens 10ms den Pegel festhält.
In dieser Zeit sollten eigentlich alle Prellimpulse abgeklungnen sein.
Ebenso könnte bei Deaktivieren des Gebers verfahren werden.
Das Ganze sollte sich mit ein paar Logikgattern lösen lassen.

Zur Lichtschrankengeschichte.
So lange man Lichtschranken des gleichen Typs verwendet, sollten auch die Latenzzeiten in etwa gleich sein.
Das bedeutet, die Messung kommt zwar später an, stimmt aber, da ja alle Lichtschranken verspätet reagieren.