Конечно, драйвер - это громко сказано

.
Обычно на прерывании от таймера считывали состояние всех контактов (обработчик прерывания должен быть как можно меньше и по возможности линейным). На фоне проверяли их состояние, если 1, то +1 в счётчик, увеличиваем то определённого уровня, если 0, то -1 в счётчике, вычитаем до 0. Принцип интегрирования.
unsigned char InSig[N]; // Состояние считанного сигнала.
unsigned char StatusSig[N]; // Состояние сигнала.
unsigned char Counter[N]; // Счётчик состояния.
// Получить состояние сигнала.
unsigned char GetState ( unsigned char N ) // N - номер сигнала.
{
// Проверка наличия изменения.
if ( InSig[N] != StatusSig[N] )
{ // Изменение есть!
// Проверим как изменился сигнал.
if ( InSig[N] == 0 )
{
if ( Counter[N] != 0 )
Counter[N]--;
}
else
{
if ( Counter[N] != Level )
Counter[N]++;
}
// Определим Новое состояние сигнала.
if ( Counter[N] == 0 ) StatusSig[N] = 0;
else if ( Counter[N] == Level ) StatusSig[N] = 1;
}
return StatusSig[N];
}
Это просто демонстрация, на самом деле состояние сигнала хранятся в бинарном виде в бинарном массиве.
Согласитесь на "С" это более читаемо, чем на ассемблере.
Иногда печатная плат нового устройства не была готова, но чтобы не тормозить разработку, программу писал на "С" для компьютера и отлаживал, а когда была готова железка достаточно было этот исходник перекомпилировать (используйте условную компиляцию, пишите в Posix стандарте, тогда будет максимальная переносимость) под соответствующий процессор железки. Время разработки ПО резко сокращалось.
Всем всего доброго!