W jednym z moich poprzednich tekstów w ramach porad i wskazówek (EdW 11/2024) omówiliśmy funkcję random() Arduino. Ilekroć słyszę termin „losowy”, zawsze przychodzi mi na myśl pasek Tour of Accounting z czwartku 25 października 2001 r., w którym Dilbert zostaje przedstawiony młodszemu księgowemu opisywanemu jako ekspert od generowania liczb losowych. Młodszy księgowy powtarza wciąż jedną cyfrę: „Dziewięć, dziewięć, dziewięć, dziewięć, dziewięć, dziewięć...”, co skłania Dilberta do zapytania: „Jesteś pewien, że to losowe?”. Młodszy księgowy odpowiada: „Na tym polega problem z losowością – nigdy nie można być pewnym”. Nigdy nie wypowiedziano prawdziwszych słów.
Tak wiele przepisów i smaków
Kiedy po raz pierwszy zapoznajesz się z koncepcją liczb losowych, wszystko wydaje się mieć sens. Dopiero później okazuje się, że istnieje wiele różnych „przepisów i smaków”. Na przykład, odruchową reakcją na informację „oto zestaw liczb losowych xxx w zakresie od yyy do zzz” może być oczekiwanie „równomiernego rozkładu” w całej dopuszczalnej przestrzeni liczb. W niektórych przypadkach można jednak oczekiwać „rozkładu normalnego”, znanego również jako „rozkład gaussowski” (https://bit.ly/2PNdtPz) lub nawet „rozkładu niestandardowego” (https://bit.ly/3g3UqeT).
Inną kwestią jest sposób implementacji generatora liczb losowych jako części języka programowania komputerowego. Częścią tego jest format instrukcji używanej do wywołania generatora i sposób, w jaki zwraca on wyniki. Na przykład w przypadku Arduino kolejne wywołania random(-3, 3) zwrócą losowe wartości całkowite wybrane ze zbioru zawierającego -3, -2, -1, 0, 1 i 2. Oznacza to, że wartość minimalna, która może być ujemna, 0 lub dodatnia, jest „włączająca”, podczas gdy wartość maksymalna jest „wyłączająca”. W niektórych językach wartość minimalna zawsze wynosi 0 i/lub wartość maksymalna jest całkowita. Ponadto funkcja random() Arduino jest nieulotna, co oznacza, że – o ile nie podejmiemy kroków, aby temu zapobiec – za każdym razem, gdy włączymy lub zresetujemy mikrokontroler i ponownie uruchomimy program, otrzymamy tę samą sekwencję wartości pseudolosowych.
Dla porównania, rozważmy funkcję RAND() w arkuszu kalkulacyjnym Microsoft Excel. Funkcja ta zwraca wartość rzeczywistą z przedziału od 0 do 1. Właśnie otworzyłem program Excel, umieściłem kursor w jednej z komórek, wpisałem =RAND(), nacisnąłem klawisz <Enter> i otrzymałem wartość 0.657414199. W tym przypadku funkcja jest zmienna, ponieważ kiedy zapisałem, a następnie ponownie otworzyłem arkusz kalkulacyjny, nową wyświetlaną wartością było 0.980944837. Do nas należy pobranie tych wartości i manipulowanie nimi w dowolny sposób. Na przykład, gdybyśmy chcieli wygenerować losowe wartości całkowite od 0 do 100, użylibyśmy =ROUND(RAND() * 100, 0). Spowoduje to pobranie wartości rzeczywistej z zakresu od 0 do 1, pomnożenie jej przez 100, a następnie zaokrąglenie do najbliższej liczby całkowitej (tj. 0 miejsc po przecinku). A gdybyśmy chcieli wygenerować wartości całkowite z przedziału od -50 do +50? Żaden problem! Wszystko, co musielibyśmy zrobić, to zmodyfikować naszą poprzednią instrukcję tak, aby brzmiała =(ROUND(RAND() * 100, 0)) – 50.
Od oprogramowania do sprzętu
OK, przechodzimy z tematu na temat tak płynnie, jak młode kozice górskie, więc warto się odpowiednio przygotować. Zacznijmy od tego, że termin „przerzutnik typu D” odnosi się do elementu pamięci zwanego rejestrem, który może przechowywać pojedynczy bit (cyfrę binarną) danych w postaci 0 lub 1.
Jak wskazuje symbol (dla tych z nas, którzy potrafią czytać tajemny „język symboli”), jest to urządzenie wyzwalane zboczem. Narastające (0 do 1) przejście (zbocze) na wejściu zegara załaduje do rejestru dowolną wartość 0 lub 1 obecną na wejściu data_in. Po krótkim opóźnieniu ta nowa wartość przejdzie przez bramki logiczne tworzące rejestr i pojawi się na wyjściu data_out.
Wewnętrzne opóźnienie rejestru jest ważne, ponieważ pozwala nam na łańcuchowe połączenie szeregu rejestrów w celu utworzenia bardziej wyrafinowanej funkcji zwanej rejestrem przesuwającym. Rozważmy na przykład 3-bitowy rejestr przesuwający typu serial-in, serial-out (SISO).
Kiedy taktujemy ten rejestr, każdy element załaduje wartość, którą aktualnie widzi na swoim wejściu. Załóżmy, że zaczynamy z trzema rejestrami zawierającymi odpowiednio 0, 1, 0 (uprośćmy to do 010) i że podajemy na wejście d2 wartość logiczną 1. Kiedy taktujemy rejestr, będzie on zawierał 101. Jeśli pozostawimy wejście na 1 i ponownie taktujemy rejestr, będzie on zawierał 110. Kolejne taktowanie spowoduje, że rejestr będzie zawierał 111. Jeśli teraz ustawimy wejście d2 na 0 i ponownie taktujemy rejestr, będzie on zawierał 011 i tak dalej.
Ponieważ zamierzamy narysować kilka takich rejestrów, a jak wiemy, mają one wspólny zegar, na potrzeby tych dyskusji możemy uprościć nasz 3-bitowy symbol rejestru przesuwającego SISO, usuwając zegar. Ponadto, nic nie stoi na przeszkodzie aby w tym momencie dokonać niewielkiej modyfikacji, która pozwoli nam uzyskać dostęp do wszystkich bitów rejestru jednocześnie, co skutkuje tym, co nazywamy rejestrem przesuwnym szeregowym, równoległym (SIPO).
Teraz, żeby było śmieszniej, załóżmy, że weźmiemy wyjście z naszego 3-bitowego rejestru przesuwającego SISO i podamy je z powrotem na wejście. Załóżmy również, że mamy jakiś sposób na wstępne załadowanie tego rejestru, aby miał początkową wartość 001. Po pierwszym taktowaniu rejestr będzie zawierał 100. Następne taktowanie da wynik 010. I jeszcze jeden zegar przywróci nam pierwotną wartość 001.