Częstotliwość, według definicji, jest to wielkość fizyczna określająca liczbę wystąpień jakiegoś zjawiska w jednostce czasu. Jednostką czasu w układzie SI jest sekunda, a zjawiskiem, jakie chcemy badać, jest zbocze rosnące mierzonego sygnału prostokątnego. Zatem: aby zmierzyć częstotliwość sygnału, musimy po prostu policzyć, ile występuje zboczy tego sygnału w ciągu jednej sekundy.
Na płytce User Interface Board nie mamy żadnych układów umożliwiających kondycjonowanie różnych sygnałów – badany przebieg doprowadzony będzie prosto do pinu układu FPGA. Wynika z tego bardzo ważne ograniczenie: sygnał pochodzący z zewnętrznego generatora musi mieścić się w przedziale od 0 do 3,3 V. Badany przebieg powinien mieć kształt prostokątny – pomiar fali sinusoidalnej, trójkątnej lub innej może dać niepoprawny wynik.
Przeanalizujmy schemat z rysunku 1, wygenerowany automatycznie przez narzędzie Netlist Analyzer na podstawie syntezy kodu źródłowego w Verilogu, aby lepiej zrozumieć ideę działania miernika częstotliwości.
Układ będzie miał trzy wejścia. Do wejścia zegarowego Clock podłączony zostanie generator kwarcowy o częstotliwości 25 MHz, umieszczony na płytce MachXO2 Mega, prezentowanej już w EP. Wejście Reset działa dokładnie tak samo, jak we wszystkich dotychczasowych projektach. Do wejścia SignalAsync_i doprowadzić należy sygnał, którego częstotliwość ma zostać zmierzona.
Badany przebieg oczywiście nie jest zsynchronizowany z domeną zegarową. Aby uniknąć problemu metastabilności, należy go najpierw zsynchronizować. W tym celu trzeba ów sygnał „przepuścić” przez moduł Synchronizer, widoczny po lewej stronie schematu. Temat ten został bardzo obszernie omówiony w 11 odcinku kursu poświęconym tematowi statycznej analizy czasowej (opublikowanym w EP 09/2023).
Synchronizator daje nam sygnał o takiej samej częstotliwości i wypełnieniu, jak sygnał wejściowy, ale my potrzebujemy wykrywać zbocza rosnące sygnału. W tym celu zastosujemy moduł EdgeDetector. Obserwuje on wejście Signal_i, a kiedy zmienia się jego stan, wówczas na wyjściach RisingEdge_o lub FallingEdge_o ustawiany jest – na jeden takt zegarowy – stan wysoki (w zależności od tego, czy moduł wykrył, odpowiednio, zobacze rosnące czy opadające). Nas interesuje tylko wykrywanie zbocza rosnącego i każde takie zdarzenie będziemy zliczać za pomocą licznika Counter.
Popatrzmy teraz na licznik Counter oraz jego wyjście q. Jest ono doprowadzone do wejścia a sumatora add_5 (nazwa wygenerowana została automatycznie), który do obecnego stanu licznika dodaje jedynkę, doprowadzoną „na sztywno” do wejścia b. Wyjście wspomnianego sumatora prowadzi do wejścia d1 multipleksera mux_6 (nazwa również wygenerowana automatycznie), a do wejścia d0 doprowadzany jest aktualny stan licznika, niepowiększony o 1. Zatem multiplekser przepuści dalej aktualny stan lub stan powiększony o 1 – decyduje o tym wejście cond multipleksera, sterowane przez wyjście RisingEdge_o detektora zbocza. Kiedy zostanie wykryte zbocze rosnące, wówczas stan tego wejścia jest wysoki przez czas jednego taktu zegarowego, a multiplekser przekazuje do swojego wyjścia stan wejścia d1, czyli stan licznika zwiększony o 1. W stanie spoczynku, kiedy nic nie jest wykrywane, multiplekser łączy wyjście z wejściem d0, czyli przekazuje dalej obecny stan licznika.
Miernik częstotliwości potrzebuje wzorca czasu jednej sekundy. Posłużymy się tutaj dobrze znanym i używanym chyba w każdym odcinku modułem StrobeGenerator. Za pomocą parametru PERIOD_US skonfigurujemy go tak, aby dokładnie co jedną sekundę ustawiał swoje wyjście Strobe_o w stan wysoki na jeden takt zegarowy.
Wyjście tego modułu pełni dwie funkcje. Pierwszą z nich jest sterowanie multiplekserem Counter_25__I_0 (automat nazwał multiplekser licznikiem…?!), którego wejście d0 połączone jest z wyjściem multipleksera mux_6, omawianego wcześniej, a wejście d1 połączone jest z masą, czyli z liczbą zero. Wyjście tego multipleksera doprowadzone jest do wejścia licznika Counter, który de facto stanowi zbiór 26 przerzutników D połączonych równolegle. Te przerzutniki, z każdym zboczem rosnącym sygnału zegarowego, wpisują do swojej pamięci: wartość licznika powiększoną o 1, aktualną wartość licznika (czyli nie zmieniają swojego stanu) lub same zera. Ostatnia wymieniona możliwość ma zastosowanie wtedy, kiedy kończy się czas pomiaru i cała operacja zaczyna się od nowa. Wróćmy do multipleksera i licznika: kiedy wyjście z modułu StrobeGenerator znajduje się w stanie wysokim, to w kolejnym takcie zegarowym licznik Counter zostanie zresetowany, ponieważ multiplekser Counter_25__I_O na wejście licznika poda same zera.
Drugie zastosowanie sygnału generowanego przez moduł StrobeGenerator obejmuje uruchamianie konwersji liczby binarnej z wyjścia licznika na format dziesiętny BCD, aby pokazać ją na wyświetlaczu LED w formacie zrozumiałym dla człowieka. Sygnał z wyjścia StrobeGenerator jest zatem doprowadzony do wejścia Start_i modułu DoubleDabble w wersji sekwencyjnej (poznaliśmy go w 22 odcinku kursu, opublikowanym w EP 08/2024).
Może trochę dziwić, że jeden sygnał powoduje jednocześnie zerowanie licznika i konwersję jego zawartości na format dziesiętny. Jednak nie ma w tym nic nieprawidłowego! Ustawienie stanu wysokiego na wyjściu modułu StrobeGenerator uruchamia dwie operacje (które wykonywane są jednocześnie w tym samym takcie zegarowym):
- Zerowanie licznika, tzn. po wystąpieniu zbocza rosnącego sygnału zegarowego, stan tego licznika zostanie ustawiony na zero.
- Uruchomienie konwersji stanu licznika na format BCD, tzn. po wystąpieniu zbocza rosnącego sygnału zegarowego, obecny stan licznika zostanie odczytany przez moduł DoubleDabble i skopiowany do jego wewnętrznego rejestru, a następnie rozpocznie się konwersja.
Dochodzimy do ostatniego modułu, czyli sterownika wyświetlacza DisplayMultiplex. Do jego wejścia Data_i doprowadzono wprost wynik z wyjścia BCD_o modułu DoubleDabble. Moduł ten nie potrzebuje żadnych sygnałów wyzwalających, więc zmiana na wejściu danych znajduje od razu odzwierciedlenie na wyjściach sterujących katodami i segmentami wyświetlacza multipleksowanego LED.