1597 - Symulacja cyfrowych układów kombinacyjnych

Artykuł ten poświęcony jest komputerowej symulacji cyfrowych układów kombinacyjnych, tzn. składających się z bramek, a także koderów i dekoderów. Zagadnienie zostało zilustrowane przykładami w C++, ze względu na łatwość operowania wskaźnikami i wysoki stopień abstrakcji danych oferowany przez ten język

W układach cyfrowych wyróżniamy trzy stany logiczne: wysoki H (ang. high), niski L (ang. low) i stan wielkiej impedancji Z. Stany L i H odpowiadają niskiemu (w TTL ok 0-0,4V) i wysokiemu (w TTL ok 2,4-5V) poziomowi napięcia. Stan Z oznacza, że wyjście, na którym on występuje nie oddziaływuje na stan przewodu, do którego jest podłączone (między tym wyjściem a masą lub zasilaniem występuje wielka impedancja). Aby w przyszłości było łatwiej było korzystać z tych oznaczeń, w programie przykładowym zostały zdefiniowane stałe zgodnie z dodatnią konwencją logiczną (L=0, H=1). Stan Z umownie oznaczyłem jako Z=2.

Zacznę od opisania elementu elektronicznego. Zadanie to spełnia abstrakcyjna klasa Element (dla nieokreślonego elementu nie istnieje sensowna implementacja jego metod wirtualnych). W skład tej klasy muszą wejść wszystkie metody i pola danych dla wszystkich elementów. Jeżeli chodzi o dane, wprowadziłem tylko jedną zmienną - traktowanieZ. Określa ona, w jaki sposób będzie traktowany na wejściu danego elementu stan Z. Dla układów TTL jest to stan H, i na taką wartość ustawia to pole konstruktor klasy Element. Mimo że nie uwzględniłem tu metody pozwalającej na zmianę zawartości tego pola, to w rzeczywistym programie należałoby uwzględnić fakt, że część lub wszystkie elementy będą traktowały wejściowy stan Z jako L. Taka modyfikacja nie będzie miała wpływu na część symulacyjną programu, która w dalszym ciągu będzie działała poprawnie. W klasie Element znajduje się również wirtualna metoda Symuluj(), która określa działanie danego elementu. Pojęcie każdego elementu będzie klasą pochodnej klasy element; nie będzie więc można utworzyć obiektu tej klasy, jeśli nie zdefiniujemy w niej metody Symuluj (nielogiczne byłoby tworzenie elementu, którego działanie nie jest określone).

Elementy musimy w jakiś sposób ze sobą łączyć. W tym celu wprowadziłem pojęcia przewodu i końcówki. Klasa Końcówka modeluje coś w rodzaju wyprowadzenia układu (zarówno wejścia, jak i wyjścia). Najważniejszym jej elementem jest wskaźnik do klasy Przewód, która służy do łączenia ze sobą dwóch końcówek. Należy zwrócić uwagę, że nie tylko klasa Końcówka wskazuje na Przewód, a le również Przewód zawiera wskaźnik do klasy Końcówka.Ustawieniem wszystkich tych wskazań zajmuje się konstruktor Przewód(Końcówka*,Końcówka*).

Obie klasy mają pole stan, ale ma ono nieco inne znaczenia. W klasie Przewód oznacza ono stan występujący na linii łączącej dwa wyprowadzenia, natomiast w klasie Końcówka - stan, jaki podaje dane wyprowadzane na linię. Jeśli zatem dana końcówka jest źródłem sygnału (np. wyjściem bramki), to pole stan będzie ustawiane na wartość podawaną na końcówkę przez element. Natomiast jeśli jest ona wejściem, to stan jest ciągle ustawiony na Z, Jest to wykorzystane w metodzie Wpisz(int). Zajmuje się ona wpisywaniem nowego stanu na dany przewód wg. następującego algorytmu :

  • jeżeli bieżącym stanem przewodu jest Z, to wpisz nowy stan;
  • jeżeli stan jest przeciwny do bieżącego i co najmniej jedno źródło nadaje ten stan, to wywołaj wyjątek sprzeczność;
  • jeżeli powyższe warunki nie są spełnione, wpisz nowy stan

Zarówno przewody, jak i elementy będziemy trzymać w listach jednokierunkowych. Są one tworzone za pomocą wzorca. Lista zadeklarowanego na początki programu, ListaElem i ListaPrzewód są klasami pochodnymi klas utworzonych z tego wzorca. O ile sam wzorzec implementuje podstawowe operacje na listach pewnego typu (ze względu na objętość ich liczba została ograniczone do niezbędnego minimum), o tyle wymienione klasy rozbudowują go o charakterystyczne dla konkretnej listy metody. W przypadku listy przewodów jest to metoda Drukuj() - odpowiedzialna za wydrukowanie stanów wszystkich przewodów, a w przypadku listy elementów metoda Symuluj() - odpowiedzialna za obliczenie stanów logicznych w całym układzie.

Metodę Symuluj() stosujemy po kolei dla każdego elementu. Jeśli w wyniku jej wykonania zostanie zwrócona wartość większa od zera, to znaczy, że w danym przejściu układ został zmodyfikowany. Symulacja zostanie przerwana, gdy żaden element nie zgłosi zmiany stanu na swoich wyjściach (wyjściu) lub gdy liczba przejść będzie większa od n. Za n podstawiamy taką wartość, po przekroczeniu której możemy mieć pewność, że układ nie ma stanu stabilnego (np. układ przerzutnika astabilnego). Wtedy zgłaszany jest wyjątek stanu nieokreślonego. Metoda symuluj realizuje więc idee "rozchodzenia" się znanych stanów logicznych po całym układzie. W zrozumieniu tego procesu powinien pomóc nam schemat układu. Na początku są nam znane stany przewodów 6 i 7, reszta jest w stanie Z. Na tej podstawie możemy ustalić stan przewodu 3 (H), a za pomocą pola traktowanieZ - stan przewodu 2(L). Teraz zostaje już tylko określenie stanu przewodu 1 na podstawie stanów 2 i 3 (1 = H).

W celu "ucieleśnienia" list, tworzymy dwa obiekty globalne: jeden klasy ListaElem, a drugi klasy ListaPrzewod. Należy zwrócić uwagę, że listy mają dostęp do składowych prywatnych i chronionych swoich elementów, co gwarantują deklaracje klas list jako zaprzyjaźnionych w klasach Element i Przewód.

Właściwie zostało już tylko dodanie konkretnych elementów i rozpoczęcie symulacji. W programie przykładowym uwzględniłem klasy bramkaNAND oraz zasilania i masy, które również są traktowane jako elementy. Przyjrzyjmy się teraz klasie bramkaNAND.

W części prywatnej widzimy deklaracje trzech wyprowadzeń we1, we2, wy. Nie trudno się domyśleć, że są to wejścia i wyjścia bramki. W części publicznej znajdują się metody zwracające wskaźniki do poszczególnych "końcówek" bramki, a także metoda Symuluj(), której dostarczenie jest konieczne, aby klasa została uznana za wirtualną. W celu zidentyfikowania stanu wejść korzysta ona z metody StanPrzewodu(int). Zwraca ona stan przewodu, do którego dołączone jest wejście w postaci H lub L (stan Z zostaje zmieniony na traktowanieZ). Uwzględnia również przypadek, gdy wskaźnik dowiązanie ma wartość zerową.

Funkcja główna programu tworzy w pamięci układ pokazany na rysunku, dodając odpowiednie pozycje do listy elementów i listy przewodów. Następnie wywołuje metodę ListaElem::Symuluj() i próbuje wyłapać jeden z wyjątków, po czym - jeśli nie nastąpiła sprzeczność, wyświetla wynik.

Posługując się tym prostym programem, można obliczyć stany logiczne praktycznie w dowolnym układzie o charakterze kombinacyjnym. Oczywiście rzeczywisty program wymagałby rozbudowania całości o nowe elementy (także trój-stanowe), a przede wszystkim o możliwość łatwej edycji układów w środowisku graficznym. W artykule tym starałem się przedstawić jedynie idee, całą resztę pozostawiając czytelnikowi.

Symulacja układów sekwencyjnych, czyli zawierających przerzutniki, generatory itp. nie stanowi większych problemów, za to dość mocno zwiększyło objętość przedstawionego tu programu. Podam więc jedynie pewne wskazówki dotyczące rozwiązania tego problemu. Po pierwsze symulacja układów sekwencyjnych wiąże się z ciągłym wykonywaniem ListaElem::Symuluj(). Należy zlikwidować pojęcie stanu nieokreślonego, gdyż takie zjawisko jest naturalną konsekwencją stosowania generatorów. W związku z tym wyniki trzeba wyświetlać przez cały czas symulacji, a nie tylko na końcu, i to najlepiej w postaci synchronicznych przebiegów czasowych. Po drugie, należy zapewnić synchronizację pomiędzy generatorami różnych częstotliwości. Można to zrobić znajdując pewien przedział czasu, o który możemy się przesuwać nie ryzykując, że któryś z generatorów zmieni stan wyjścia pomiędzy dwoma krokami (np. mając częstotliwość 2 Hz i 5 Hz możemy się posuwać o 1/10s). Możemy również dodać do poszczególnych elementów czasy propagacji, wprowadzając pewne opóźnienie w przepisywaniu danych (należy uwzględnić to przy synchronizacji częstotliwości) Dzięki temu uzyskamy całkiem dobry program do symulacji układów cyfrowych.

 #include <iostream.h>
 #define null 0
 const L = 0;
 const H = 1;
 const Z = 2;

 // klasy wyjątków
 class sprzecz();
 class nieokr();
 sprzecz sprzecznosc;
 nieokr nieokreslony;

 // wzorzec listy
 template <class T> class Lista
 {
   protected:
   T *lista;
   long licznik;
   public:
   Lista() (lista = null; licznik = 0; }
   void Dodaj(T *);
   ˜Lista();
 }

 template <class T> void Lista<T>::Dodaj(T *element)
 {
   element -> nastepny = lista;
   lista = element;
   licznik++;
 }

 template <Class T> Lista<T>::~Lista()
 {
   T *aktualny;
   aktualny = lista;
   while (aktualny != null)
   {
     lista = aktualny -> nastepny;
     delete aktualny;
     aktualny = lista;
   }
 }

 // pojecie elementu elektronicznego
 class Element
 {
   friend class Lista<Element>;
   protected:
   int traktowanieZ;
   Element *nastepny;
   public:
   Element() (traktowanieZ = H)
   virtual unsigned char Symuluj() = 0;
   Element *Nastepny() const (return this -> nastepny; )
 };
 class Przewod;

 // pojęcie końcówki elementu elektronicznego
 class Koncowka
 {
   public:
   int stan;
   Przewod *dowiazanie;
   int StanPrzewodu(int traktZ);
   Koncowka() ( stan = Z; dwiazanie = null; }
 };

 // pojęcie przewodu
 class Przewod
 {
   friend class Lista<Przewod>;
   private:
   int stan;
   Przewod *nastepny;
   Koncowka *zrodlo1, *zrodlo2;
   public:
   void Drukuj();
   Przewod *Nastepny() const {return this -> następny; )
   Przewod( Koncowka *z1, Koncowka *z2)
   {
     zrodlo1 = z1; zrodlo2 = z2; stan = Z; nastepny = NULL;
     z1 -> dowiązanie = z2 -> dowiazanie = this;
   }
   int Wpisz(int);
   int Stan() { return stan; }
 }

 void Przewod::Drukuj()
 {
   swith (stan)
   {
     case 0: { cout << "L"; break; }
     case 1: { cout << "H"; break; }
     case 2: { cout << "Z"; break; }
   }
 }

 int Przewod::Wpisz (int nowystan)
 {
   if (stan == Z) { stan = nowystan; return 0; }
   if (nowystan == Z) return 0;
   If ((zrodlo1 != null) && (zrodlo2 != null)
     if ((stan != nowystan) && ((zrodlo1 -> stan == stan)\
       || (zrodlo2 -> stan == stan))) throw sprzecznosc;
   stan = nowystan;
   return (0);
 }

 int Koncowka::StanPrzewodu(int traktZ)
 {
   int stanp;
   if (dowiazanie == null) stanp = Z;
   else stanp = dowiazanie -> Stan();
   if (stan == Z) return traktZ; 
   else return stanp;
 }

 // rozbudowanie listy elementów i przewodów
 class ListaElem: public Lista<Element>
 {
   public:
   int Symuluj();
 };

 int ListaElem::Symuluj()
 {
   long n = 2 * licznik + 1;
   int zmiany, pom;
   Element *aktualny;
   for (long k = 0; k < n; k++)
   {
     aktualny = lista;
     zmiany = 0;
     while (aktualny != null)
     {
       pom = aktualny -> Symuluj();
       if (pom > 0) zmiany = 1;
       aktualny = aktualny -> Nastepny();
     }
     if (!zmiany) return 0;
   }
   throw (nieokreslony);
 }

 class ListaPrzewod: public Lista<Przewod>
 {
   public:
   void Drukuj();
 };

 void ListaPrzewod::Drukuj()
 {
   Przewod *aktualny = lista;
   int numer = 0;
   while (aktualny != null)
   {
     cout << "Przweod:" << numer << "znajduje się w stanie ";
     aktualny -> Drukuj();
     cout << "\n";
     numer++;
     aktualny = aktualny -> Nastepny();
   }
 }

 ListaPrzewod ListaPrzewodow;
 ListaElem ListaElementow;

 // klasy modelujące elementy kombinacyjne

 class bramkaNAND: public Element
 {
   private:
   Koncowka we1, we2, wy;
   public:
   unsigned char Symuluj();
   Koncowka *We1() { return &we1; }
   Koncowka *We2() { return &we2; }
   Koncowka *Wy() { return &wy; }
 };

 unsigned chet bramkaNAND::Symuluj()
 {
   int swe1 = we1.StanPrzewodu(traktowanieZ),\
     swe2 = we2.StanPrzewodu(traktowanieZ);
   int poprzedni = wy.stan;
   wy.stan = !(swe1 & swe2);
   wy.dowiazanie -> Wypisz(wy.stan);
   if (poprzednie != wy.stan) return 1; //modyfikacja
   else return 0;
 }

 class Zasilanie: public Element
 {
   private:
   Koncowka wy;
   public:
   Zasilanie() { wy.stan = H; }
   unsigned char Symuluj()
   {
     wy.dowiazanie -> Wpisz(H);
     return 0;
   }
   Koncowka *Wy() { return &wy }
 };

 class Masa: public Element
 {
   private:
   Koncowka wy;
   public:
   Masa() { wy.stan = L; }
   unsigned char Symuluj()
   {
     wy.dowiazanie -> Wpisz(L);
     return 0; 
   }
   Koncowka *Wy() { return &wy; }
 };

 // funkcja główna - test
 int main()
 {
   bramkaNAND *bramka1 = new bramkaNAND,\
     *bramka2 = new bramkaNAND,\
     *bramka3 = new bramkaNAND;
   Zasilanie *zas1 = new Zasilanie;
   Masa *masa1 = new Masa;
   ListaElementow.Dodaj(bramka1);
   ListaElementow.Dodaj(bramka2);
   ListaElementow.Dodaj(bramka3);

   ListaElementow.Dodaj(zas1);
   ListaElementow.Dodaj(masa1);

   ListaPrzewodow.Dodaj\
     (new Przewod(bramka1 -> We1(), zas1 -> Wy()));
   ListaPrzewodow.Dodaj\
     (new Przewod(bramka1 -> We2(), masa1 -> Wy()));
   ListaPrzewodow.Dodaj\
     (new Przewod(bramka2 -> We1(), null));
   ListaPrzewodow.Dodaj\
     (new Przewod(bramka2 -> We2(), null));
   ListaPrzewodow.Dodaj\
     (new Przewod(bramka3 -> We1(), bramka1 -> Wy()));
   ListaPrzewodow.Dodaj\
     (new Przewod(bramka3 -> We2(), bramka2 -> Wy()));
   ListaPrzewodow.Dodaj\
     (new Przewod(bramka3 -> Wy(), null));
   try { 
     ListaElementow.Symuluj();
   }
   catch (sprzecz)
   {
     cout << "Sprzecznosc w ukladzie\n";
     return 0;
   }

   catch (nieokr)
   {
     cout << "Nie można ustalic konkretnego stanu\n";
   }
   cout << "Ok.\n"; ListaPrzewodow.Drukuj();
   return 0;
 }

Bartosz Paliświat
PC Kurier 15/97, 17 Lipca 1997r.