Constrained Random Verification onder de knie met OSVVM
Constrained random verification is een testbench-strategie die vertrouwt op het genereren van pseudo-random transacties voor het device under test (DUT). Het doel is functionele coverage van een aantal vooraf gedefinieerde evenementen te bereiken door willekeurige interactie met de DUT.
Open Source VHDL Verification Methodology (OSVVM) is een gratis VHDL-bibliotheek die een aantal handige pakketten bevat voor het creëren van constrained random testbenches. We zijn met name geïnteresseerd in de RandomPkg en CoveragePck, die we in dit artikel zullen gebruiken. Ik raad aan de OSVVM GitHub-pagina te bezoeken om meer te leren over de features van deze bibliotheek.
Het Device Under Test
Ik ga direct in op een voorbeeld om beter uit te leggen hoe een constrained random testbench verschilt van de klassieke testbench, die directed tests gebruikt. We creëerden een ringbuffer FIFO in het vorige artikel op deze blog, maar we creëerden geen zelf-controlerende testbench om de correctheid van de module te verifiëren.
We gaan een goede testbench maken voor de ringbuffer FIFO die constrained random verification gebruikt.
entity ring_buffer is generic ( RAM_WIDTH : natural; RAM_DEPTH : natural ); port ( clk : in std_logic; rst : in std_logic; -- Write port wr_en : in std_logic; wr_data : in std_logic_vector(RAM_WIDTH - 1 downto 0); -- Read port rd_en : in std_logic; rd_valid : out std_logic; rd_data : out std_logic_vector(RAM_WIDTH - 1 downto 0); -- Flags empty : out std_logic; empty_next : out std_logic; full : out std_logic; full_next : out std_logic; -- Het aantal elementen in de FIFO fill_count : out integer range RAM_DEPTH - 1 downto 0 ); end ring_buffer;
De entiteit van de ringbuffer-module wordt in de code hierboven getoond. We gaan de DUT behandelen als een zwarte doos, wat betekent dat we geen kennis aannemen over hoe de DUT geïmplementeerd is. Immers, dit artikel gaat over de testbench, niet over de ringbuffer FIFO.
We zullen de DUT in de testbench instantieren door de entity-instantiatie-methode te gebruiken. De instantiatie is triviaal, dus ik laat de code voor nu weg, maar het kan later in dit artikel gedownload worden.
De DUT-generics zullen gemapt worden op de volgende waarden:
- RAM_WIDTH: 16
- RAM_DEPTH: 256
Testbench-strategie
Laten we de teststrategie doorlopen voordat we iets implementeren. De afbeelding hieronder toont het hoofconcept van de testbench die we gaan maken.
We zullen willekeurige schrijftransacties uitvoeren aan de invoerkant van de DUT. De invoerdata wordt op elke klokcyclus op een willekeurige waarde gezet, en de strobes op de write-enable-invoer zullen een willekeurige duur hebben.
Op dezelfde manier zullen we reads op willekeurige momenten uitvoeren. We zullen het read-enable-signaal in bursts activeren die duren voor een willekeurig aantal klokcycli.
Er zal een behavioraal model parallel met de DUT draaien. Dit is een FIFO die anders geïmplementeerd is dan de ringbuffer die in de DUT gebruikt wordt, maar nog steeds dezelfde interface heeft. In tegenstelling tot de DUT hoeft het behaviorale model niet synthetiseerbaar te zijn. Dit geeft ons de vrijheid om geavanceerde VHDL-programmeerfuncties te gebruiken voor het creëren ervan.
We zullen de uitgang van de DUT vergelijken met de uitgang van het behaviorale model in een apart proces. Dit proces is uitsluitend verantwoordelijk voor het doen van deze vergelijking op elke klokcyclus door assert-statements te gebruiken. Als de twee FIFO-implementaties op enig moment anders gedragen, zal een assertion-failure de simulatie met een fout laten stoppen.
Ten slotte zullen we functionele coverage-data verzamelen door de transacties te observeren die naar en van de DUT gaan. Een functioneel coverage-punt kan betekenen: bijvoorbeeld een gelijktijdige read en write, of dat de FIFO minstens één keer vol is. We zullen deze evenementen monitoren in onze hoofd-testbench-sequencer-proces. De simulatie zal stoppen wanneer alle functionele coverage-gebeurtenissen die we monitoren, zijn voorgekomen.
De OSVVM-bibliotheek Importeren
De OSVVM-bibliotheek kan gebruikt worden met elke simulator die VHDL-2008 ondersteunt. Het zit mogelijk al in de standaardbibliotheken die bij je simulator horen. Het zit inbegrepen in de ModelSim PE Student Edition, die gratis gedownload kan worden van Mentor Graphics.
ModelSim wordt geleverd met een oudere versie van OSVVM, maar dat is OK, het heeft alles wat we nodig hebben. We kunnen gewoon de random- en coverage-pakketten importeren als volgt:
library osvvm; use osvvm.RandomPkg.all; use osvvm.CoveragePkg.all;
De nieuwste versie van de OSVVM-bibliotheek kan altijd gedownload worden van de GitHub-pagina. Doe dit als je simulator het niet inbegrepen heeft, of als je de nieuwste features van de bibliotheek wilt gebruiken.
De OSVVM-variabelen Declareren
De OSVVM-bibliotheek bevat pakketten met protected types. Variabelen die van deze types gemaakt worden, zouden in scope beperkt zijn tot het proces waarin ze gedefinieerd zijn. Daarom zullen we ze in plaats daarvan declareren als shared variables in het declaratieve gebied van de testbench-architectuur, zoals getoond in de code hieronder.
-- OSVVM-variabelen shared variable rv : RandomPType; shared variable bin1, bin2, bin3, bin4, bin5, bin6 : CovPType;
De rv variabele van type RandomPType is voor het genereren van willekeurige waarden. We hebben er maar één nodig, want we kunnen hetzelfde object in elk proces gebruiken waar we willekeurige waarden moeten genereren. De laatste coderegel declareert zes variabelen van type CovPType.
We hebben zes coverage-variabelen gedeclareerd omdat we zes coverage-doelen gaan hebben, we zullen deze objecten "bins" noemen. De shared variables moeten geïnitialiseerd worden voordat ze gebruikt kunnen worden voor het verzamelen van coverage-data. We doen dit door de AddBins procedure aan te roepen op elk van de CovPType bins.
-- Coverage bins instellen bin1.AddBins("Write while empty", ONE_BIN); bin2.AddBins("Read while full", ONE_BIN); bin3.AddBins("Read and write while almost empty", ONE_BIN); bin4.AddBins("Read and write while almost full", ONE_BIN); bin5.AddBins("Read without write when almost empty", ONE_BIN); bin6.AddBins("Write without read when almost full", ONE_BIN);
We leveren een string-beschrijving van de coverage-bin als eerste parameter aan de AddBins procedure. Deze string verschijnt weer aan het einde van de simulatie wanneer we de statistieken voor elke coverage-bin printen. Zoals je uit de tekstvoorstellingen kunt zien, gaan we de bins gebruiken om te controleren of sommige zeer specifieke corner-cases zijn voorgekomen.
AddBins is een overgeladen procedure die gebruikt kan worden voor het creëren van meerdere scoreborden binnen de bin-variabelen. Echter, we zullen maar één scorebord per bin hebben. Daarom leveren we de convenience-constante ONE_BIN als parameter aan de AddBins procedure. Dit initialiseert de CovPType variabelen met één bin elk. De scoreborden vertegenwoordigd door de bins worden als gedekt beschouwd wanneer de evenementen die ze monitoren minstens één keer zijn voorgekomen.
Willekeurige Invoer Genereren
Laten we beginnen met het creëren van het proces dat invoerdata voor de DUT genereert. De ringbuffer-FIFO is ontworpen om pogingen tot overschrijven en over-lezen te negeren. Daarom kunnen we simpelweg willekeurige data in bursts van willekeurige duur schrijven. We hoeven niet na te denken of de DUT daadwerkelijk klaar is om de data te absorberen of niet.
PROC_WRITE : process begin wr_en <= rv.RandSlv(1)(1) and not rst; for i in 0 to rv.RandInt(0, 2 * RAM_DEPTH) loop wr_data <= rv.RandSlv(RAM_WIDTH); wait until rising_edge(clk); end loop; end process;
De enige overweging die dit proces maakt is dat de DUT niet in reset is. We activeren of deactiveren willekeurig het write-enable-signaal in de eerste regel van dit proces, maar het zal alleen geactiveerd zijn als rst '0' is.
De daaropvolgende for-lus schrijft willekeurige data naar de DUT voor een willekeurig aantal klokcycli, zelfs als het enable-signaal niet actief is. We kunnen dit doen omdat de DUT de wr_data poort moet negeren tenzij het wr_en signaal '1' is. Na de for-lus zal het programma terugkeren naar het begin van het proces, wat een nieuwe willekeurige schrijftransactie trigger.
Willekeurige Reads Uitvoeren
Het proces dat data van de DUT leest is vergelijkbaar met het schrijfproces. We kunnen het rd_en signaal op willekeurige momenten activeren omdat de DUT ontworpen is om leespogingen te negeren wanneer leeg. De data die op de rd_data poort verschijnt wordt niet daadwerkelijk gecontroleerd. Dit proces controleert alleen het read-enable-signaal.
PROC_READ : process begin rd_en <= rv.RandSlv(1)(1) and not rst; for i in 0 to rv.RandInt(0, 2 * RAM_DEPTH) loop wait until rising_edge(clk); end loop; end process;
Behaviorale Verificatie
We zullen een behavioraal model van de DUT binnen onze testbench construeren om het gedrag te verifiëren. Dit is een bekende testbench-strategie. Eerst voeden we het behaviorale model gelijktijdig met dezelfde invoer als de DUT. Daarna kunnen we de uitgang van de twee vergelijken om te checken of de DUT het correcte gedrag heeft.
De afbeelding hierboven toont de basislayout van zo'n testbench. Het behaviorale model werkt parallel met de DUT. We gebruiken het als blauwdruk om de outputs van de DUT te checken.
De Testbench FIFO
We zullen een gekoppelde lijst gebruiken om het behaviorale model te implementeren. Gekoppelde lijsten kunnen niet gesynthetiseerd worden, maar ze zijn perfect voor testbenches. Je herinnert je misschien het Hoe een Linked List in VHDL te maken artikel als je een vaste lezer van deze blog bent. We zullen de code eruit gebruiken om het behaviorale model voor de ringbuffer-FIFO te implementeren.
package DataStructures is type LinkedList is protected procedure Push(constant Data : in integer); impure function Pop return integer; impure function IsEmpty return boolean; end protected; end package DataStructures;
De pakketdeclaratie voor de Linked List FIFO wordt in de code hierboven getoond. Het is een protected type dat de drie functies heeft; Push, Pop, en IsEmpty. Deze worden gebruikt voor het toevoegen en verwijderen van elementen uit de FIFO, evenals voor het checken of er nul elementen over zijn.
-- Testbench FIFO die de DUT emuleert shared variable fifo : LinkedList;
Protected types zijn klasse-achtige constructen in VHDL. We zullen een object van de linked list maken door een shared variable te declareren in het declaratieve gebied van de testbench, zoals getoond in de code hierboven.
Het Behaviorale Model
Om het gedrag van de ringbuffer-FIFO volledig na te bootsen, declareren we twee nieuwe signalen die de DUT's uitgangssignalen spiegelen. Het eerste signaal bevat de uitgangsdata van het behaviorale model, en het tweede is het bijbehorende valid-signaal.
-- Testbench FIFO signalen signal fifo_out : integer; signal fifo_out_valid : std_logic := '0';
De code hierboven toont de declaratie van de twee uitgangssignalen van het behaviorale model. We hebben geen speciale invoersignalen nodig voor het behaviorale model, want ze zijn dezelfde als die verbonden met de DUT. We gebruiken signalen om de DUT-uitgang na te bootsen omdat het ons in staat stelt om gemakkelijk coverage-data te verzamelen, zoals we later in dit artikel zullen zien.
PROC_BEHAVIORAL_MODEL : process begin wait until rising_edge(clk) and rst = '0'; -- Een write emuleren if wr_en = '1' and full = '0' then fifo.Push(to_integer(unsigned(wr_data))); report "Push " & integer'image(to_integer(unsigned(wr_data))); end if; -- Een read emuleren if rd_en = '1' and empty = '0' then fifo_out <= fifo.Pop; fifo_out_valid <= '1'; else fifo_out_valid <= '0'; end if; end process;
Het proces dat het behaviorale model van de ringbuffer-FIFO implementeert, wordt in de code hierboven getoond. Dit proces wordt getriggerd op elke stijgende flank van de klok, als het reset-signaal niet actief is.
Het behaviorale model duwt een nieuwe waarde naar de testbench-FIFO wanneer het wr_en signaal geassert is terwijl het full signaal '0' is. Op dezelfde manier werkt de leeslogica in het behaviorale-modelproces door te luisteren naar de rd_en en empty signalen. De laatste wordt gecontroleerd door de DUT, maar we zullen verifiëren dat het werkt in het volgende proces dat we creëren.
De Outputs Controleren
Alle logica die verantwoordelijk is voor het checken van de DUT's outputs, is verzameld in een proces genaamd «PROC_VERIFY». We gebruiken assert-statements om te checken of de DUT's outputs overeenkomen met die van het behaviorale model. We checken ook of de DUT en het behaviorale model het eens zijn over wanneer de FIFO leeg is.
De code voor het verificatieproces wordt hieronder getoond.
PROC_VERIFY : process begin wait until rising_edge(clk) and rst = '0'; -- Check dat DUT en TB FIFO leeg rapporteren gelijktijdig assert (empty = '1' and fifo.IsEmpty) or (empty = '0' and not fifo.IsEmpty) report "empty=" & std_logic'image(empty) & " while fifo.IsEmpty=" & boolean'image(fifo.IsEmpty) severity failure; -- Check dat de valid-signalen matchen assert rd_valid = fifo_out_valid report "rd_valid=" & std_logic'image(rd_valid) & " while fifo_out_valid=" & std_logic'image(fifo_out_valid) severity failure; -- Check dat de uitgang van de DUT matcht met de TB FIFO if rd_valid then assert fifo_out = to_integer(unsigned(rd_data)) report "rd_data=" & integer'image(to_integer(unsigned(rd_data))) & " while fifo_out=" & integer'image(fifo_out) severity failure; report "Pop " & integer'image(fifo_out); end if; end process;
Het proces wordt getriggerd door de stijgende flank van de klok, zoals we zien aan de eerste coderegel. De DUT is een geklokt proces, en de downstream-logica die verbonden is met de DUT wordt ook verwacht synchroon te zijn met het kloksignaal. Daarom is het zinvol om de outputs op de stijgende klokflank te checken.
Het tweede codeblok checkt of het empty signaal van de DUT alleen geassert wordt wanneer de testbench-FIFO leeg is. De DUT en het behaviorale model moeten allebei akkoord gaan dat de FIFO leeg is of niet voor deze test te slagen.
Daarop volgt een vergelijking van de read-data-valid-signalen. De DUT en het behaviorale model zouden data gelijktijdig moeten uitvoeren, anders is er iets mis.
Ten slotte checken we of de uitgangsdata van de DUT overeenkomt met het volgende element dat we van de testbench-FIFO poppen. Dit gebeurt natuurlijk alleen als het rd_valid signaal geassert is, wat betekent dat het rd_data signaal gesampled kan worden.
Coverage-data Verzamelen
Om de hoofdstroom van de testbench te sturen, zullen we een sequencer-proces creëren. Dit proces initialiseert de coverage-bins, draait de tests, en stopt de testbench wanneer alle coverage-doelen bereikt zijn. De code hieronder toont het complete «PROC_SEQUENCER» proces.
PROC_SEQUENCER : process begin -- Coverage bins instellen bin1.AddBins("Write while empty", ONE_BIN); bin2.AddBins("Read while full", ONE_BIN); bin3.AddBins("Read and write while almost empty", ONE_BIN); bin4.AddBins("Read and write while almost full", ONE_BIN); bin5.AddBins("Read without write when almost empty", ONE_BIN); bin6.AddBins("Write without read when almost full", ONE_BIN); wait until rising_edge(clk); wait until rising_edge(clk); rst <= '0'; wait until rising_edge(clk); loop wait until rising_edge(clk); -- Coverage-data verzamelen bin1.ICover(to_integer(wr_en = '1' and empty = '1')); bin2.ICover(to_integer(rd_en = '1' and full = '1')); bin3.ICover(to_integer(rd_en = '1' and wr_en = '1' and empty = '0' and empty_next = '1')); bin4.ICover(to_integer(rd_en = '1' and wr_en = '1' and full = '0' and full_next = '1')); bin5.ICover(to_integer(rd_en = '1' and wr_en = '0' and empty = '0' and empty_next = '1')); bin6.ICover(to_integer(rd_en = '0' and wr_en = '1' and full = '0' and full_next = '1')); -- Stop de test wanneer alle coverage-doelen bereikt zijn exit when bin1.IsCovered and bin2.IsCovered and bin3.IsCovered and bin4.IsCovered and bin5.IsCovered and bin6.IsCovered; end loop; report("Coverage goals met"); -- Zorg dat de DUT leeg is voor het beëindigen van de test wr_en <= force '0'; rd_en <= force '1'; loop wait until rising_edge(clk); exit when empty = '1'; end loop; -- Coverage-data printen bin1.WriteBin; bin2.WriteBin; bin3.WriteBin; bin4.WriteBin; bin5.WriteBin; bin6.WriteBin; finish; end process;
Eerst initialiseren we de coverage-bin-objecten door de AddBins procedure op ze aan te roepen, zoals we al eerder in dit artikel bespraken. Daarna, nadat de reset vrijgegeven is, gaan we coverage-data verzamelen. Op elke stijgende flank van de klok zal de code binnen de lus-construct uitgevoerd worden.
Het eerste codeblok binnen de lus is voor het verzamelen van coverage-data. We kunnen de ICover procedure op de bin aanroepen om een hit op het coverage-punt dat het vertegenwoordigt te registreren. Als we de integer-parameter 0 leveren, heeft de aanroep geen effect. Als we de integer-parameter 1 gebruiken, telt het als een hit.
Er is maar één «bin» binnen elk van de coverage-bin-objecten, omdat we ze geïnitialiseerd hebben met de ONE_BIN constante. Deze enkele bin kan bereikt worden door ICover(1) aan te roepen. We kunnen een hit of miss op het coverage-punt registreren door onze Boolean-expressies te converteren naar de integers 1 of 0 met de to_integer functie.
Na het registreren van de coverage-data, checken we of alle coverage-doelen bereikt zijn door de IsCovered functie op alle bins aan te roepen. Dan verlaten we de lus als alle coverage-doelen bereikt zijn.
We zorgen ervoor dat de DUT leeg is voordat we de test beëindigen. Om dit te bereiken, nemen we controle over van de schrijver- en lezer-processen door het wr_en signaal te forceren naar '0' en het rd_en signaal naar '1'.
Ten slotte printen we statistieken over hoe vaak elk coverage-doel geraakt is door de WriteBin functie op elk van de coverage-bins aan te roepen. Het finish keyword aan het einde van het proces zal de simulator de simulatie laten stoppen.
De Testbench Draaien
Je kunt het gehele ModelSim-project met alle VHDL-bestanden downloaden via het formulier hieronder.
Nadat we het project geladen hebben door het do-bestand dat in de Zip zit uit te voeren, kunnen we de testbench gewoon draaien door «runtb» in de ModelSim-console te typen. De runtime van de testbench zal willekeurig zijn omdat de coverage-doelen willekeurig bereikt worden. Echter, de testresultaten zijn reproduceerbaar omdat er eigenlijk een pseudo-random reeks gebruikt wordt.
We initialiseerden geen seed in onze code, wat betekent dat de standaard-seed-waarde gebruikt wordt voor de pseudo-random generator. Het is mogelijk een andere seed in te stellen door de InitSeed procedure aan te roepen op het RandomPType object, wat een andere random-reeks produceert.
De Console-uitvoer
De uitvoer die naar de ModelSim-console geprint wordt nadat we het «runtb» commando geven, wordt hieronder getoond. Er zal veel willekeuriges pushen naar en poppen van de testbench-FIFO zijn tijdens de simulatie.
VSIM 2> runtb # ** Warning: Design size of 15929 statements or 2 leaf instances exceeds # ModelSim PE Student Edition recommended capacity. # Expect performance to be quite adversely affected. # ** Note: Push 34910 # Time: 790 ns Iteration: 0 Instance: /ring_buffer_tb ... # ** Note: Pop 37937 # Time: 83100 ns Iteration: 0 Instance: /ring_buffer_tb # ** Note: Pop 13898 # Time: 83110 ns Iteration: 0 Instance: /ring_buffer_tb # %% WriteBin: # %% Write while empty Bin:(1) Count = 2 AtLeast = 1 # # %% WriteBin: # %% Read while full Bin:(1) Count = 3 AtLeast = 1 # # %% WriteBin: # %% Read and write while almost empty Bin:(1) Count = 106 AtLeast = 1 # # %% WriteBin: # %% Read and write while almost full Bin:(1) Count = 1 AtLeast = 1 # # %% WriteBin: # %% Read without write when almost empty Bin:(1) Count = 1 AtLeast = 1 # # %% WriteBin: # %% Write without read when almost full Bin:(1) Count = 3 AtLeast = 1 # # Break in Process PROC_SEQUENCER at C:/crv/ring_buffer_tb.vhd line 127
Statistieken voor alle coverage-bins worden geprint wanneer alle coverage-doelen bereikt zijn. Sommige bins zijn maar één keer geraakt, terwijl er een 106 keer is geraakt. Maar uiteindelijk is elke bin minstens één keer geraakt. Dus we weten dat alle evenementen waar we coverage-bins voor gedefinieerd hebben, getest en geverifieerd zijn.
De Waveform
Laten we de waveform bekijken om een idee te krijgen van wat de testbench heeft gedaan. De afbeelding hieronder toont de waveform met het fill_count signaal voorgesteld als een analoge waarde. De FIFO is vol wanneer de trace voor dit signaal bovenaan is, en leeg wanneer het onderaan is.

Zoals we uit de waveform kunnen zien, wordt de ringbuffer willekeurig gevuld en geleegd. Toch hebben we deze hellingen en dalingen in vulniveau niet expliciet geprogrammeerd. Toch zien we een organisch patroon van interactie met de DUT.
Meer over Constrained Random Verification
Constrained random verification is een goede testbench-strategie wanneer de testvector te veel permutaties heeft voor een exhaustive test praktisch te zijn. De willekeurige interacties vertonen een meer natuurlijk gedrag dan een directed corner-case test zou doen, zonder accuratesse in te boeten.
We kunnen zeker zijn dat alle corner-cases bereikt zijn, zolang we de coverage-data-verzameling correct hebben opgezet. Het toegevoegde voordeel is dat gerandomiseerde testen meer geneigd zijn om zwaktes in de DUT bloot te leggen waar je niet specifiek voor test. Zolang je alle corner-cases kent, kun je directed tests voor ze maken. Maar corner-cases worden makkelijk over het hoofd gezien, en dat is wanneer je kunt profiteren van de constrained random verification-methodologie.
Dit artikel heeft alleen de oppervlakte aangesneden van wat je kunt doen met constrained random verification. Ik raad aan de docs op de OSVVM GitHub-pagina te lezen om dieper in het onderwerp te duiken.
Ik raad ook SynthWorks' Advanced VHDL Testbenches and Verification-cursus aan, waarmee ik niet geaffilieerd ben. Ik heb de 5-daagse versie van deze fysieke cursus bijgewoond. De cursus wordt gegeven door Jim Lewis, de voorzitter van de VHDL Analysis and Standardization Group (VASG). Over het algemeen een geweldige investering voor elk bedrijf dat hun VHDL-testbenches naar het volgende niveau wil tillen.
VHDL
- De onvoorspelbare uitdagingen van onderhoud: Inzichten van expert Jason Afara
- Geïntegreerde QR, RFID en Temperatuurverificatie Toegangscontrolesysteem
- Hoogspannings-PCB-ontwerp: Differentieel signaalverwerking & simulatieverificatie
- Hurco TM8 hellingbed draaibank – Compact ontwerp, precisie besturing & veelzijdige gereedschapping
- CAGI Rotary Compressor Performance Verificatie Programma: Betrouwbare, Efficiënte Operatie Garanderen
- Cadence onthult Rapid-Performance Emulatie & Prototyping-oplossingen voor miljard-gate SoC-verificatie
- Siemens breidt Veloce-systeem uit met vier nieuwe producten, voor naadloze hardware-ondersteunde verificatie
- VE-VIDES-initiatief onthult betrouwbaar ontwerp- en verificatie-framework voor IoT-beveiliging
- Synopsys onthult complete HBM3 IP-suite voor multi-die-systemen, bandbreedte tot 921 GB/s
- Willekeurige Getallen Genereren in Java: Praktische Gids met Random en Math.random
- Java 8 Streams: Declaratieve benadering voor dataverwerking