Hacer ejemplos detector cruce por cero

Sugerencia: reiniciar contadores que se incrementan en una ISR puede provocar condición de carrera, deberías hacerlo llamando una función como esta:

void resetCounters() {
  detachInterrupt(0);
  duration = 0;
  pulsecount = 0;
  attachInterrupt(0, freqCounterCallback, RISING);
}

Se supone que para la obtención de los valores la historia es la misma; mira que millis() también hace algo así.

Recuerda que la CPU es de 8 bits, así que incluso copiar variables de 32 bits toma al menos 4 instrucciones máquina (y en medio de alguna de ellas puede entrar la interrupción); lo cual hace estas operaciones sean "moleculares" (no "atómicas").
Dicho en otras palabras: cuando el programa principal y una interrupción intentan acceder a la misma variable al mismo tiempo, el valor resultante es indeterminado (para la lógica del código, termina siendo "incorrecto").

Para este caso puntual, dos fallos pueden ocurrir (recordando que unsigned int se compone de 2 bytes):

Al leer la variable: si antes de copiar el último byte se dispara la interrupción, y en esta ocurre un acarreo en el incremento/suma (más de un byte se tiene que modificar); el valor recuperado en ese instante podría estar por encima de lo que debería.
Por ejemplo: el contador está en 255 (0x00FF), según código se necesita recuperar el valor de dicho contador; se lee el primer byte pero luego se dispara la interrupción la cual incrementará el contador. 255 definitivamente va a causar acarreo (a nivel binario), incrementando también el segundo byte (¡el programa principal aún no lo ha leído por culpa de la interrupción!). Después de 255 sigue 256 (0x0100), entonces el programa principal al reanudarse lee el segundo byte de la variable, en cuál ahora tiene el valor de 1.
Si me sigues la idea, sabrás qué acaba de ocurrir: al copiar el primer byte antes de la interrupción (0xFF), y el segundo ya después (0x01); el valor recuperado es 0x01FF o 511. Definitivamente 511 no está después de 255, ¿o me equivoco?

Al reiniciar la variable: no es problema cuando la interrupción solo incrementa (excepto cuando un desbordamiento o cambio de signo tenía que suceder); pero cuando es una suma con otro valor que no sea 0 ni 1, podría acabar en algo diferente de cero aunque (según programa principal) debería ser cero.
De nuevo, el procesamiento sigue siendo de byte en byte, con la posibilidad de ser interrumpido "en el camino".

Para los que tienen experiencia programando para PC, es la misma problemática de la concurrencia (también conocido como programación multitarea, multihilo o paralela). Aunque la interrupción no es paralelismo verdadero, el concepto es el mismo debido a que estamos hablando de un cambio súbdito en el flujo del programa; el cuál no es deterministico (es impredecible).

La probabilidad de ocurrencia de semejante fallo es muy pequeña, pero sigue siendo posible; he ahí el detalle.

Dato curioso: si la CPU es de 8 bits, eso quiere decir que las operaciones (básicas) con variables de ese tamaño (o menor), es imposible que sean interrumpidas. Las banderas de interrupción son revisadas cada ciclo de instrucción, no en cada ciclo de reloj.

PD 1: todo lo que acabo de mencionar aplica sólo para variables globales volatile; para las que no lo son es todavía más complicado de explicar su comportamiento (a menos que se estudie el código binario resultante), ya que el compilador suele permitir que hayan copias de las más usadas en los registros de propósito general, haciendo que rara vez hayan valores actualizados en RAM. Con volatile, el compilador está obligado a que la copia en RAM siempre esté actualizada.

PD 2: si alguien pretende refutar mi sugerencia, entonces que me demuestre que el compilador siempre deshabilita (temporalmente) las interrupciones cuando se opera con 16 bits o más; aunque esto empeore el rendimiento al insertar 3 instrucciones máquina adicionales por cada operación.