ARGG-HDL: A High Level Python Based Object-Oriented HDL Framework
IIEEE TRANSACTIONS ON NUCLEAR SCIENCE, VOL. XX, NO. XX, XXXX 2020 1
ARGG-HDL: A High Level Python BasedObject-Oriented HDL Framework (Oct 2020)
R. Peschke, K. Nishimura, G. Varner
Abstract ---We present a High-Level Python-based HardwareDescription Language (ARGG-HDL), It uses Python as itssource language and converts it to standard VHDL. Comparedto other approaches of building converters from a high-levelprogramming language into a hardware description language,this new approach aims to maintain an object-oriented paradigmthroughout the entire process. Instead of removing all the high-level features from Python to make it into an HDL, this approachgoes the opposite way. It tries to show how certain features froma high-level language can be implemented in an HDL, providingthe corresponding benefits of high-level programming for theuser.
Index Terms ---Computer languages, Object oriented program-ming, Open source software, Runtime library
I. I
NTRODUCTION
ARGG-HDL is a library that allows the user to write Python[1] code and convert it to VHDL (VHSIC-HDL, Very HighSpeed Integrated Circuit Hardware Description Language) [2].This document shows three of its most defining features. FirstlyARGG-HDL is fully object oriented, which allows the user tocreate high-level objects. This Object Oriented approach allowsthe users of the library to express their intentions in a cleanermanner, which can result in much cleaner code. Secondly, itallows the user to write very generic code. This reduces theamount of code duplication. Due to its high-level templatingmechanism, it can be used to write highly generic code. Thirdly,it uses a preexisting language, which allows the user to usethe already well known and very powerful tools, which areprovided in the Python language. Instead of having to learnboth digital logic design practices AND a completely newlanguage, the user only has to learn a few extensions providedby this library.
A. Competing Approaches
The idea of generating firmware from a high-level program-ming language is certainly not new; in fact some of theseapproaches have been around for decades. So far there existat least 30+ approaches for High level Synthesis (HLS) [3]and firmware generation, but what sets ARGG-HDL apartfrom the others is its high level approach to writing low levelfirmware. If compared to, for example, ”VivadoHLS,” it is clearthat ”VivadoHLS” provides very strong support for generatingalgorithms out of existing c/c++ code and porting them overto firmware, but the code written in c/c++ has very littleresemblance to the firmware generated. In fact most of thetime the generated code will only be used as a black box IPcore. For many applications this is a solid approach but incases where one still wants to have the low level control over the actual implementation it is difficult to achieve that. HereARGG-HDL can really shine because it allows the user onone hand to write the firmware as low-level as is necessaryto fulfill the task, and on the other hand gives a clear path ofintegrating high level features into the design. Another popularcode generation tool is ”MyHDL” [4]. It allows the user towrite firmware in Python. This is a task it does very well butin doing so it loses many of the features that make Pythona high-level programming language. Most noticeable here isclasses. It provides rudimentary support for grouping signalstogether in containers but this is the highest level of abstractionthat one can achieve. With ARGG-HDL classes are a corepart of its design, furthermore ARGG-HDL sees itself as aplatform for the user to create their own abstractions on-topof the existing ones. As shown later in this document virtuallyevery behavior of the library can be customised for individualclasses. II. O
BJECT - ORIENTED D ESIGN
Object-oriented Design has been the foundation of virtuallyall modern programming languages. Object-Oriented design hasbeen proven excellent at hiding complexity. Humans are able touse extremely complicated technology, such as computer chips,since its complexity is hidden inside powerful abstractions,which reduce its complexity to just pushing buttons.
A. Programming in VHDL without objects
Listing 1 shows a typical example of an entity declarationin VHDL. The entity declared on 1 is a queue or ”First in Firstout” (FIFO) module (see figure 1). A FIFO consists of twosides. The data input side and the data output side. In orderto stay synchronized each side provides three signals. For theData Input side these signals are: write data, write enable, andfull. For the correct use of this FIFO, it is crucial to handlethese signals precisely according to specification. Yet whenwe look at the entity declaration the only thing that revealswhich signals belong to which side of the FIFO is a vaguenaming convention. VHDL has no concept to describe thesethree signals belonging to one interface. A main reason forthis is that each port has to be defined as an input or an output.Therefore it is not possible to make a record with these threesignals. In this case the entire record and all its signals, wouldeither be an input port or an output port.Figure 2 shows, how two entities communicate with eachother using three signals. As can be seen, it is not obviousthat these three signals actually define one interface. The onlything that reveals this information is a naming convention.Inside each entity, the signals are also handled loosely. Their a r X i v : . [ c s . P L ] N ov EEE TRANSACTIONS ON NUCLEAR SCIENCE, VOL. XX, NO. XX, XXXX 2020 2
Synchronous FIFOdinwenfull doutrenempty
Fig. 1. FIFO with native interfacee n t i t y FIFO cc i s g e n e r i c (DATA WIDTH : n a t u r a l := 1 6 ;DEPTH : n a t u r a l := 5) ;p o r t (c l k : in s t d l o g i c ;r s t : in s t d l o g i c ;d i n : in s t d l o g i c v e c t o r (DATA WIDTH−1 downto 0 ) ;wen : in s t d l o g i c ;r e n : in s t d l o g i c ;dout : o u t s t d l o g i c v e c t o r (DATA WIDTH−1 downto 0 ) ;f u l l : o u t s t d l o g i c ;empty : o u t s t d l o g i c) ;end FIFO cc ;Listing 1. Typical declaration of a FIFO with a native interface [5]. As shownin figure 1, the FIFO consists of three signals handling the writing of Data tothe FIFO (din,wen, full) and three output signals (dout, ren, empty). The onlyhint that the user has on how to use this FIFO is a naming convention. Butof course a naming convention does not communicate the timing behavior ofthe interface. Compared the the more modern AXI4-Stream the native FIFOinterface has the additional disadvantage of having two different interfacesfor the data input side and the data output side. properties are usually checked/set at very different stages of theprogram. Therefore, the user code and the interface code arecompletely intermingled. This can get unmanageable quicklyand leads to the typical ”write only code”, that is seen so oftenin VHDL. Entity B
Entity A sequential statementssequential statementssequential statements
Process Block
Process Block sequential statementssequential statements Ready
Valid
Datasequential statements sequential statementssequential statementssequential statements sequential statementssequential statements sequential statementsInterface Code
User Code
OutputInput
Fig. 2. User Code and Interface code is intermingled. Interface code needs tobe re-implemented for each entity. Interface code is hard to recognize. Inputsand outputs are independent objects. e n t i t y FIFO cc i s g e n e r i c (DATA WIDTH : n a t u r a l := 1 6 ;DEPTH : n a t u r a l := 5) ;p o r t (c l k : in s t d l o g i c ;r s t : in s t d l o g i c ;−− Primary I n t e r f a c es d a t a : in s t d l o g i c v e c t o r (DATA WIDTH−1 downto 0 ) ;s v a l i d : in s t d l o g i c ;s Ready : o u t s t d l o g i c ;−− Secondary I n t e r f a c em Data : o u t s t d l o g i c v e c t o r (DATA WIDTH−1 downto 0 ) ;m valid : o u t s t d l o g i c ;m ready : in s t d l o g i c) ;end FIFO cc ;Listing 2. Declaration of AXI4-Stream FIFO. Compared to the native FIFO theprimary and secondary side are again only indicated by a naming convention. B. Programming in ARGG-HDL with ObjectsC. The Basics
To start with ARGG-HDL a prior knowledge of anyhardware design language is not required but, if possible, thenames used to describe certain constructs are chosen to besimilar to VHDL, therefore user with a VHDL background willfind the overall structure familiar. Listing 3 shows a simpleexample of a counter. The Example starts by creating a classwhich inherits from ”v entity” this marks the object as anentity object. V entities work very similar to to entities inVHDL In our example the entity has no ports and definesonly one function, the ”architecture” function. Similarly tothe architecture body of an entity in VHDL this contains theinternal structure of the entity. Inside the architecture functionthe first thing that we create is a clock generator. The clockgenerator is derived from v entity object itself. It has one(public) member which is the clock. Since it is a normalPython object the clock generated by the entity clock generatorcan be accessed like any other member of a Python class with”clkgen.clk”. After this two signals are being created. One isthe counter, the other one is the maximum value to whichthe counter should count. Both of these signals are of thetype ”v slv(32)”, which translates to a 32 bit standard logicvector. The second argument in ”max cnt = v slv(32,300)”is the default value. Since this is ordinary Python code theinitial value could have been given in hexadecimal as well.The default initial value is zero. The next construct is afunction definition ”def proc():” which is modified by adecorator ”@rising edge(clkgen.clk)”. This special decoratedtakes the function that is generated here and appends it tothe ”on change” list of the signal it received as an argument”clkgen.clk”. But instead of executing on ever signal changedthis decorator makes sure that it is only executed on a risingedge. Once the simulation has started the function ”proc” getsexecuted on every rising edge of the signal ”clkgen.clk”. The
EEE TRANSACTIONS ON NUCLEAR SCIENCE, VOL. XX, NO. XX, XXXX 2020 3 c l a s s m y f i r s t t e s t b e n c h ( v e n t i t y ) : def i n i t ( s e l f ) : super ( ) . i n i t ( )s e l f . a r c h i t e c t u r e ( )@ a r c h i t e c t u r e def a r c h i t e c t u r e ( s e l f ) :c l k g e n = c l k g e n e r a t o r ( )c o u n t e r = v s l v ( 3 2 )max cnt = v s l v ( 3 2 , 3 0 0 )@rising edge ( c l k g e n . c l k ) def proc ( ) :c o u n t e r << c o u n t e r + 1 i f c o u n t e r > = max cnt :c o u n t e r <<
0e n d a r c h i t e c t u r e ( )Listing 3. my first test bench function proc does three things. First it increments the countersignal by one. For this it uses the stream (bit-shift) operator.The idea is that the new value gets stream into the existing andpersistent object. Since the object might be connected to otherobject it is important that there is no new object generated butthe existing object is modified. Similar to C++, ARGG-HDLuses the stream operator to stream data into objects (signals).In addition to the left shift operator (stream in operator) thereis a right shift operator (stream out operator). Future exampleswill show them in action. Next, the counter is compared tomax cnt and if it is bigger or equal it is then set to zero again.The last statement is ”end architecture()” which handles somebook keeping such as giving all the signals the correct nameetc. It needs to be executed at the end of the ”architecture”block.On the use of streamer (bit shift) operators: In otherprogramming languages, such as C++ overloading the streamer(bit-shift) operator to send data into an object (or read from it)has been part of the language for decades. It is a clear way ofcommunicating to the user that this object is not replaced witha new object but its content is modified. Of course it would bepossible to use a ”next” function (or property) to achieve thesame behavior. In the end it is a matter of taste which styleone prefers but overall using the streamer (bit-shift) operatorgives a very natural indication of data flow.On variables and signals: ARGG-HDL allows you todefine signals as either variable or signal. However since this”variableness” and ”signalness” is part of the object definitionthere is no need for two separate assignment operators. Thestreamer (bit-shift) operator will work on both types.
1) Communication between Entities with Classes and Ap-plication Programming Interfaces (API):
In ARGG-HDL theinterface is defined as an object (see figure 3). The interfaceobject contains all three signals as well as their directionality.Each entity has one port for the entire interface. This way itis clear that, these signals belong to one interface. Instead ofdefining a port as either input or output, the port are defined aseither primary or secondary. In a primary port, the signals have
Entity BEntity A
DataReadyValid
Interface
CodeUser Code
OutputInput
Process BlockPullPush sequential statementssequential statements sequential statements U s e r C o d e Interface CodeInterface CodeProcess BlockPullPush sequential statementssequential statementssequential statements U s e r C o d e Interface CodeInterface Code
Interface ObjectInterface Object P r i m a r y S e c o nd a r y Fig. 3. User Code and Interface code are clearly separated. Reusing of wellknown (tested) functions. Interface code is immediately recognizable the directionality defined in the interface class. In a secondaryport, the directionality is flipped.Inside the entity the interface is handle by a handler class.This class contains the API for interacting with the interface.Thereby, the user code and the interface code is separated. Thismakes it easier to write clean code that is easily understood.In addition, since the user never directly interacts with theinterface signals, the interface author can ensure that theinterface is used correctly.
2) Example: Clocked Entity sending data via AXI4-Streamlink:
Listing 4 shows an example of a data source entity. Inthis case the entity just counts up numbers. For interactionwith other entities, it uses the AXI4-Stream interface [6]. Sinceit is a data source, it uses the ”port primary” version of theinterface. On the architecture level it first defines a handlerobject called ”data out”, which handles the interaction with theinterface. The handler is used inside the process block. Theprocess block is executed on every rising edge of the clock.Inside the process block the handler is first checked with the”if statement”. Since the handler class is a high level objectthe author has the possibility to overload the ”bool” function,which is called inside the test block of the ”if” statement.This allows for expressive code. If the bool function ofthe interface handler returns true, the interface is ready to use.Since the interface class has only one purpose, it is immediatelyclear what this statement means. It means that the interface isready to send data. On the next line, the data can be assignedto the handler object, which is equivalent to calling the senddata function. The user of the library never needs to know theinternal structure of this interface nor does the user need toknow the protocol for sending data. The users can always besure that, as long as they are only using the functions providedby the API, the interface will always work correctly.
3) Example: Interface Class:
How the interface class worksis now described. Shown in Listing 5 is the interface classfor an AXI4-Stream interface with a data width of 32 bits. Itdefines all signals it needs to operate. It sends from primaryto secondary the following signals: valid, last and data. Fromsecondary to primary it sends the ready signal. Primary andsecondary are defined relative to the data flow. The data flowsfrom primary to secondary.
4) Example: Interface Handler Class:
Listing 6 describeshow the interface handler class works, the interface classhas to inherit from an ARGG-HDL base class called”v class primary”. The constructor takes one argument, which
EEE TRANSACTIONS ON NUCLEAR SCIENCE, VOL. XX, NO. XX, XXXX 2020 4 from a r g g h d l import * from a r g g h d l . examples import * c l a s s Counter ( v c l k e n t i t y ) : def i n i t ( s e l f , c l k ) : super ( ) . i n i t ( c l k )s e l f . Dout = p o r t o u t ( a x i S t r e a m 3 2 ( ) )s e l f . a r c h i t e c t u r e ( )@ a r c h i t e c t u r e def a r c h i t e c t u r e ( s e l f ) :d a t a = v s l v ( 3 2 )d a t a o u t = g e t h a n d l e ( s e l f . Dout )@rising edge ( s e l f . c l k ) def proc ( ) : i f d a t a o u t :d a t a o u t << d a t ad a t a << d a t a + 1Listing 4. Instead of defining individual inputs and outputs, an interface objectis defined. The object ”axiStream 32” contains all information about inputand output signals. The object brings a handler with it. The handler is usedfor communication with the interface. The handler provides the API for theinterface. The user never directly interacts with the data members. is the interface class. Then defines a variable port of that typeand connects the two objects together. Note that since thesetwo object, ”axi out” and ”TX”, are objects they know exactlywhich signal has to go in which direction in order to makethe interface work. The handler class defines three additionalfunctions. First is the send data function. It takes any datait receives and sends it out through the interface class. Inaddition, it also sets the steering signal ”valid” to high. Thesecond function is the ”ready to send” function. It is used tocheck if the interface is ready to send new data. The interfaceis only ready to send data if it is not already sending data.Therefore, the ”valid” signal needs to be low in order to sendmore data. The third function is the ” on Pull” function. Thisfunction is called on every clock cycle at the beginning of theprocess block. In this case, it checks if the ”ready” signal sendfrom the secondary is high. If yes, it means that the secondaryhas read the current data and is ready to receive new data. Ifthe ready signal is high, the steering signals can be reset andnew data can be sent.
5) Example: Entities are Objects:
Entities are themselvesobjects, therefore each port of an entity can be accessed asa member variable of this object. The example in Listing 7shows how different entities can be connected together. Insidethe architecture block of the entity, we first create a clockgenerator entity. The instance is called ”clkgen”. The instancehas an output port ”clk”, which can be accessed like any othermember variable in Python. We use the clock provided by”clkgen” in the constructor for the other entities. Now that wehave created the entity ”cnt” (short for counter) and ”axiPrint”we can connect its signals together. We do this by simplyassigning the output of the counter to the input of the ”axiprint” object. Since the interface is an object itself, it knowsexactly which signals have to go in which direction. The signalsdo not need to be repeated on the test bench entity. Everythingis contained inside the entities themselves. c l a s s a x i S t r e a m 3 2 ( v c l a s s t r a n s ) : def i n i t ( s e l f ) : super ( ) . i n i t ( )s e l f . v a l i d = p o r t o u t ( v s l ( ) )s e l f . d a t a = p o r t o u t ( v s l v ( 3 2 ) )s e l f . r e a d y = p o r t i n ( v s l ( ) )Listing 5. This class describes the signals required for the interface. In additionto just storing the type of the data, it also stores the direction of the data. Bydefinition, data flows from primary to secondary. port out defines a signalthat goes from primary to secondary. port in defines a signal that goes fromsecondary to primary c l a s s a x i S t r e a m p r i m a r y ( v c l a s s p r i m a r y ) : def i n i t ( s e l f , Axi Out ) : super ( ) . i n i t ( )s e l f . t x = v a r i a b l e p o r t o u t ( Axi Out )Axi Out << s e l f . t x def s e n d d a t a ( s e l f , d a t a I n ) :s e l f . t x . v a l i d <<
1s e l f . t x . d a t a << d a t a I n def r e a d y t o s e n d ( s e l f ) : return not s e l f . t x . v a l i d def o n P u l l ( s e l f ) : i f s e l f . t x . r e a d y :s e l f . t x . v a l i d <<
0s e l f . t x . l a s t << c l a s s t b ( v e n t i t y ) : def i n i t ( s e l f ) : super ( ) . i n i t ( )@ a r c h i t e c t u r e def a r c h i t e c t u r e ( s e l f ) :c l k g e n = c l k g e n e r a t o r ( )c n t = Counter ( c l k g e n . c l k )a x P r i n t = A x i P r i n t ( c l k g e n . c l k )a x P r i n t . D in << c n t . Doute n d a r c h i t e c t u r e ( ) c l a s s A x i P r i n t ( v c l k e n t i t y ) : def i n i t ( s e l f , c l k ) : super ( ) . i n i t ( c l k )s e l f . D in = p o r t i n ( a x i S t r e a m 3 2 ( ) )Listing 7. AxiPrint is an entity that prints out the values of the stream. Ituses the same interface class as ”Counter” but since it wants to consume thedata it is a secondary port. In order to connect ”Counter” with ”AxiPrint” theData in / Data out Member needs to be connected. Since the interface classknows which signal goes in which direction the signals can just assigned toeach other.
EEE TRANSACTIONS ON NUCLEAR SCIENCE, VOL. XX, NO. XX, XXXX 2020 5
Data DataEntity A Entity B
Clock Peripheral
Fig. 4. A common problem is that data needs to be processed in differentstages.For Example: Data comes in THEN stored in FIFO THEN Filtered THENModified THEN stored in FIFO THEN ... THEN ...Many languages such as bash and PowerShell have an easy way to build these”pipelines”, usually by using the ”or” (”—”) operator c l a s s s t r e a m d e l a y o n e ( v c l k e n t i t y ) : def i n i t ( s e l f , c l k = v s l ( ) ) : super ( ) . i n i t ( c l k )s e l f . Axi in = p i p e l i n e i n ( a x i S t r e a m 3 2 ( ) )s e l f . Axi out = p i p e l i n e o u t ( a x i S t r e a m 3 2 ( ) )s e l f . a r c h i t e c t u r e ( ) def a r c h i t e c t u r e ( s e l f ) :a x i S a l v e = g e t h a n d l e ( s e l f . Axi in )axPrimary = g e t h a n d l e ( s e l f . Axi out )@rising edge ( s e l f . c l k ) def proc ( ) : i f a x i S a l v e and axPrimary :axPrimary << a x i S a l v ee n d a r c h i t e c t u r e ( )Listing 8. stream delay one takes an input data stream and delays it by oneclock cycle. For the library to know which port to use for the input/outputthey have to be marked as stream ports by using ”port Stream Secondary” /”port Stream Primary”. There can only be one of each type D. Example: Pipe Lines
A common approach to data processing is to subdivide thetask into different stages. For example we start with a datasource, which first needs to get stored in a FIFO, then filtered,then modified, and lastly sent to another module somewhereelse. Many programming languages have for these kind oftasks a special operation: the so-called pipe line operator. Mostlanguages use the bitwise ”or” operator as the pipe operator.ARGG-HDL also uses the bitwise ”or” operator ”—”.
1) Example: stream delay one Pipe Line:
The examplein Listing 8 shows how one can use this pipeline operator.On creation of a new entity, one has simply to replacethe ”port primary” keyword with the ”port stream primary”keyword. This creates a new primary port, which will beused for the pipeline operator. In the same way does the”port stream secondary” port defines a secondary port, whichis used for the pipeline operator. There can only be one ofeach type. The purpose of the entity shown in this listing is todelay the data from an AXI4-Stream by one clock cycle. c l a s s
I n p u t D e l a y ( v c l k e n t i t y ) : def i n i t ( s e l f , c l k = v s l ( ) ) : super ( ) . i n i t ( c l k )s e l f . D In = p i p e l i n e i n ( a x i S t r e a m 3 2 ( ) )s e l f . D Out = p i p e l i n e o u t ( a x i S t r e a m 3 2 ( ) )s e l f . a r c h i t e c t u r e ( )@ a r c h i t e c t u r e def a r c h i t e c t u r e ( s e l f ) :s e l f . D In \ ProcessARGG_HDL Class
Dataenable empty Combinatorial blockIf empty == 1: enable << 0
Else: enable << i_enable i_DataI_enable i_empty Read_data i_Data
Fig. 5. Native FIFO interface: requires enable to be set to ’0’ immediatelywhen empty goes to ’1’. This needs to be done using combinatorial logic. | s t r e a m d e l a y o n e ( s e l f . c l k ) \| s t r e a m d e l a y o n e ( s e l f . c l k ) \| \ s e l f . D Oute n d a r c h i t e c t u r e ( )Listing 9. InputDelay: Takes in a data stream and sends out a data stream.Internally it uses two instances of stream delay one to create an delay oftwo clock cycles. By adding more instances of stream delay one in the pipethe delay can be made longer. Every entity that supports an AXI4-Streamdata stream can be used inside the pipe. Examples: Filter, FIFO, stages ofreadout system, stream splitter/merger etc. The length can be determent by aparameter through a loop In Listing 9 one can see an entity that uses two instances ofthe ”stream delay one” entity. As one can see it simply pipes thedata input to the first entity then to the second entity and thenback out to the data output. By adding, more instances of the”stream delay one” entity together one can increase the delay.It is even possible to make the number of ”stream delay one”instances a template argument.
E. Classes: Combination of Data and Functions
Let us have a deeper look into classes. Classes are usuallythought of as the combination of data and functions. That meansthey can contain both member data and member functions.Hardware Description Languages (HDL), usually to deal withmany different types of data, and functions. For example, thereare variables, signals and ports. In addition to normal functions,HDLs have to deal with functions, procedures, processes, andentities. In order for an object to be a fully high-level object,it is mandatory that classes in ARGG-HDL are able to containall these different parts of the language.
1) Example: Class with Variables and Signals:
Classesthat contain variables, signals, and combinatorial logic (asyn-chronous push/pull functions) are useful for defining interfacesto other entities. In the example, shown in figure 5, we wantto access a FIFO with a native interface. This specific FIFOrequires the enable signal to be always zero if the empty signalis high. It needs to be done immediately and cannot wait for thenext clock cycle; therefore, it needs to be done asynchronously.In ARGG-HDL it is possible to encapsulate all this complexityinside the interface handler class. The user of the class doesnot need to know that there is any additional logic. In fact, ifthe author of the classes uses the same public API, the usercan switch back and forth between both types of interfaceswithout having to change the code.
EEE TRANSACTIONS ON NUCLEAR SCIENCE, VOL. XX, NO. XX, XXXX 2020 6 c l a s s
NativeFIFO in ( v c l a s s h a n l d e ) : def i n i t ( s e l f , Data In ) : super ( ) . i n i t ( )s e l f . rx2 = f r e e t y p e ( Data )s e l f . rx2 << Data Ins e l f . rx1 = f r e e t y p e ( Data )s e l f . rx = v a r i a b l e p o r t i n ( Data )s e l f . rx << s e l f . rx1s e l f . b u f f = s m a l l b u f f e r ( Data . d a t a )s e l f . e n a b l e 1 = v v a r i a b l e ( v s l ( ) )s e l f . empty1 = v v a r i a b l e ( v s l ( ) )s e l f . a r c h i t e c t u r e ( )@ a r c h i t e c t u r e def a r c h i t e c t u r e ( s e l f ) :@combinational ( ) def p2 ( ) :s e l f . rx2 . e n a b l e << v s w i t c h (0 ,[ v c as e ( s e l f . rx2 . empty != 0 ,s e l f . rx1 . e n a b l e )] )s e l f . rx1 . empty << s e l f . rx2 . emptys e l f . rx1 . d a t a << s e l f . rx2 . d a t aListing 10. Handler class for a secondary port of a native FIFO (simplified). A simplified version of this handler class is shown in Listing10. As shown, it defines three sets of interface object: ”rx”,”rx1” and rx2. The interface object ”Data In” is connected tothe signal ”rx2”. The qualifier ”free type” prevents this objectfrom becoming part of the ”NativeFIFO in” signal record.This is necessary since a signal needs to have only one driverand the driver for (parts) of these objects is going to be incombinational block. The Signal ”rx1” is connected to thevariable port ”rx”. The connection between ”rx1” and ”rx2” isdone in the combinational block inside the architecture functionof the ”NativeFIFO in”. Functions marked with combinationalare executed as soon as one of their captured objects changes.The first statement of the combinational block is the conditionalassignment of ”rx2.enable”. The first argument to the switchstatement is the default value which is used when non of thecases match. There always needs to be an default statement.The second argument is a list cases. The first argument to thecase is the condition and the second argument is the valueit returns if the condition is true. From this one can see that”rx2.enable” is going to be zero except when ”rx2.empty” isnot true. In other words when the FIFO is not empty. The othertwo signals are just forwarded from ”rx2” to ”rx1” withoutchange. This feature allows the user to create objects that mixvariables signal clocked processes and non clocked processes.Classes in ARGG-HDL are translated into VHDL by firstdividing its members into signals or variables. It then createsa record for each type. In the next step, it translates themember functions. Similarly to Python, the first argument isthe object itself. Since the object now consists of two records,the function/procedure gets both of these records as arguments.Since handler classes are associated with only one processblock. each function/procedure gets read/write (inout) access to
ARGG_HDL ClassVariable RecordSignal Record
MemberAMemberBMemberC
Increment ( , )self Self_sig Member1Member2If > 1000 thenEnd if; Self_sig MemberA <= 0;If > 2000 thenEnd if; := 0;self Member1self Member1 <= + 1 ;
Self_sig MemberA := + 1 ;self Member1 self Member1
Self_sig MemberA
Self_sig MemberA
Fig. 6. Translation. The ARGG-HDL Class gets separated into its variablesand signals. Since handler classes are always associated to exactly one processblock the variables and signal records are given to the procedure with readand write access (inout). both the signal record and the variable record. Only interfaceclasses (and primitive data types such as std logic vector) canbe used by more then one process block.III. G
ENERIC P ROGRAMMING
The next feature any language needs in order to be considereda high-level language is generic programming. Even thoughVHDL provides a limited amount of generic programming itfalls short when compared to a high-level language. Given thedynamic nature of Python it naturally provides possibilities forvery generic programming. ARGG-HDL tries to preserve thegeneric programming model from Python as much as possible.
A. Example1) Example: Interface Classes:
Using templates in ARGG-HDL works very similar to Python. Listing 11 shows the actualimplementation of the interface class for an AXI4-Streaminterface. The data type is an argument to the constructor ofthe class. The data type can be every plain type, that includesstandard logic vectors of different sizes, integers etc. In addition,it can also be records and arrays. For every new data type, thetemplating mechanism will create a new class.
EEE TRANSACTIONS ON NUCLEAR SCIENCE, VOL. XX, NO. XX, XXXX 2020 7 c l a s s a x i S t r e a m ( v c l a s s t r a n s ) : def i n i t ( s e l f , Axitype ) : super ( ) . i n i t ( )s e l f . v a l i d = p o r t o u t ( v s l ( ) )s e l f . l a s t = p o r t o u t ( v s l ( ) )s e l f . d a t a = p o r t o u t ( Axitype )s e l f . r e a d y = p o r t i n ( v s l ( ) )Listing 11. For the actual implementation of the AXI4-Stream interface classthe data object is a template variable. For each new data type the templatingmachine will create a new class. Data type can also be records and arrays. c l a s s a x i s S t r e a m s e n d e r ( v c l a s s p r i m a r y ) : def i n i t ( s e l f , Axi Out ) :s e l f . t x = v a r i a b l e p o r t o u t ( Axi Out )Axi Out << s e l f . t x def s e n d d a t a ( s e l f , d a t a I n ) :s e l f . t x . v a l i d <<
1s e l f . t x . d a t a << d a t a I nListing 12. Also the handler classes are templates. In addition to the classbeing a template so are the member functions. That means as long as theassignment operator is defined for the types: of ”self.tx.data” and ”dataIn” itwill work.
2) Example: Interface Handler Classes:
In addition tothe class itself being a template, all its member functionsare templates too. This means that as long there is a validassignment from ”dataIn” type to ”self.tx.data” in Listing 12this code will compile and it will work.Listing 13 shows a practical example of a member functiontemplate. We see the secondary handler class of an AXI4-Stream. Its purpose is to readout the data received. The dataare first stored in an internal buffer. The readout can be donewith the right shift/stream out operator. The first thing thefunction does is it resets the output object. Then it checks ifthe internal buffer has data. If the internal buffer has data, thedata is then written to the output object. This function is clearlywritten for the case when the data output type is of the exactsame type as the internal data type. But since it is a templateit accepts any type that has a valid assignment operator forthis data type.A practical use case is shown in Listing 14. It shows a testbench with a counter as a data source and one process block,using the data. As described before the stream out operatoris a template, which means, it accepts every type that hasthe following two features. Firstly, it needs to have a resetfunction and secondly it needs have an assignment operatorfor this data type. The newly created optional t class has bothof these features and therefore it can be used as a target for c l a s s a x i s S t r e a m r e c e i v e r ( v c l a s s s e c o n d a r y ) : def r s h i f t ( s e l f , r h s ) :r h s . r e s e t ( ) i f s e l f . d a t a i n t e r n a l i s v a l i d 2 :r h s << s e l f . d a t a i n t e r n a l 2s e l f . d a t a i n t e r n a l w a s r e a d 2 << c l a s s t b ( v e n t i t y ) : def i n i t ( s e l f ) : super ( ) . i n i t ( )s e l f . a r c h i t e c t u r e ( )@ a r c h i t e c t u r e def a r c h i t e c t u r e ( s e l f ) :c l k g e n = v c r e a t e ( c l k g e n e r a t o r ( ) )c n t = v c r e a t e ( Counter ( c l k g e n . c l k ) )c n t o u t = g e t h a n d l e ( c n t . D a t a o u t )d a t a = v s l v ( 3 2 )o p t d a t a = o p t i o n a l t ( v s l v ( 3 2 ) )@rising edge ( c l k g e n . c l k ) def proc ( ) :c n t o u t >> d a t ac n t o u t >> o p t d a t ae n d a r c h i t e c t u r e ( )Listing 14. Optional t is a generic container that stores data of a certain type,as well as a bit that indicates if the data is valid or not. cnt out is an instanceof a counter entity which uses the AXI4-Stream interface. It is written longbefore optional t. Since optional t has a reset function and an assignmentoperator that accepts this data type it will work.Fig. 7. This picture shows the result of the simulation of Listing 14. Itshows how the AXI4-Stream handler class interacts with the two differentobjects (data and opt data). The ”data” object is a standard logic vector. The”opt data” object is and optional data object. This means it has an extra bitwhich indicates if the data is valid or not. When reading out data from anAXI4-Stream interface using the right shift (stream out) operator the targetis first reset, then the handler checks if it received data and only if it hasreceived data it will write the data to the target. In red the raw AXI4-Streaminput is shown. One can see that every N clock cycle a new value is sent.On reset the standard logic vector sets all bits to zero. If no new value wasreceived the ”data” object will remain zero. The ”opt data” object sets onreset only the valid bit to zero. As one can see from the picture both ”data”and ”opt data” get set correctly to the new value as soon as it is receives. Thisshows that even different data types can be set correctly by the AXI4-Streaminterface handler as long as they provide a compatible set of public interfacefunctions. the readout of the AXI4-Stream. optional t is a container classwhich stores a data object alongside a valid bit that indicatesif the data is valid or not.The result of the simulation of this test bench can be seenin figure 7. Every ten clock cycles the counter sends a newnumber via the AXI4-Stream. In this simulation, data is astandard logic vector. The simulation shows that the outputdata object is zero except for the clock cycles where it receiveddata. Comparing that with the optional t object shows that itsinternal data member always stays on the previous value. Justthe valid member changes according to the data being validor not. EEE TRANSACTIONS ON NUCLEAR SCIENCE, VOL. XX, NO. XX, XXXX 2020 8
Fig. 8. Shown is the use of the widely used editor Visual Studio Code with aplugin for Plotly. This allows to plot the connection diagram while debuggingthe code. This Picture shows that with very minimal amount of work the userhas access to high level features that are available for the Python language.Being a library for one of the most widely used language gives ARGG-HDLimmediate access to immense amount of extremely powerful libraries.
IV. ARGG-HDL
IS NOT A LANGUAGE , IT IS A L IBRARY
A. Seamless integration with other Python libraries
When designing ARGG-HDL the question came up to eithermake it a completely new language, make it a precompiler forVHDL or use a preexisting high-level programming language.By considering the unfeasible amount of work that wouldbe required to build just the precompiler, it became clearthat the only reasonable solution was to use a preexistinglanguage. Therefore ARGG-HDL is not a language it is alibrary. This gives it instantly access to all the high qualitytools that are available for Python. Since Python is crossplatform, simulations in ARGG-HDL run on any platformthat supports Python. For the simulation, all libraries that areavailable for Python can immediately be used with ARGG-HDL. This includes all the powerful data visualization librariessuch as ”Matplotlib”, ”Plotly” etc. The same is true for fileaccess. Python has very powerful libraries when it comes tofile IO. For example, currently it uses the library PY VCD forwriting ”variable change dump” files. However, it could easilymake use of the ”Pandas” library. Alternatively, if necessarythe data could be stored in a ”TTree” from the CERN ROOTlibrary. All these powerful libraries are just one pip installaway.In addition, Python has a rich ecosystem of editors andtool chains. Virtually all editors support features such as autocompletion, step debugging, data visualization etc. Furthermore,Python already has a packaging system in place. That means,if one wants to share code with the world it can be uploadedto PyPi. This will not only take care of the distribution of thelibrary itself but will also take care of the dependencies.One of the many features that are instantly available forARGG-HDL is, for example, plotting the connection diagramin real-time while running the program (see figure 8).
B. Co-Simulation
Python is known for its outstanding integration to otherlanguages. Virtually all modern programming languages havethe possibility to interact with Python. The official Python web-side has examples on how to connect Python to other languages
AST Library from Python ARGG HDL visitor ARGG HDL Object Convert
Python File VHDL File
Fig. 9. for 15 different languages, such as C/C++, Java, .Net, andHaskell. Further more Python provides support for inter-processcommunication such as network sockets. Especially the supportfor network sockets and TCP/UDP communication allows theuser a straight forward integration of the co-simulation. Acommon mode of communication between PC and FPGA isthe TCP or the UDP protocol. In this scenario the FPGA canbe mimicked by a Python simulation of the firmware whichlistens on the address/port instead of the FPGA.V. C
ONVERSION TO
VHDLThe conversion from Python to VHDL is separated intodifferent steps. In the first step the entity that should beconverted needs to be instantiated. During the instantiatingprocess all the connections between the individual signalsentities and classes are created. At this point the entity wouldbe able to run as a simulation in Python. The entities and allsub-entities have been instantiated at least once. Only objectsthat are currently instantiated get translated to VHDL. Thelibrary has a shadow register which contains all ARGG-HDLobjects that are created during one session. In the next step itstarts looping over all objects that have been created.In order to allow the user of the library a more granularcontrol over the conversion to VHDL every part of theconversion can be modified. In the end, the Python code andthe VHDL code can be made completely independent of eachother. For this, every VHDL file is subdivided into the blocksshown in figure 11. Every object in ARGG-HDL has the optionto modify any of these blocks.
A. The different stages of the conversion
The Conversion is subdivided into three stages (see figure9). Firstly parsing of the Python source files. This is donewith a standard Python library called AST. It is part of thereference implementation of CPython. Second, the ARGG-HDLvisitors, in order to traverse the AST, ARGG-HDL has its ownvisitor class. This class does not only traverses through thesyntax tree but also carries all the context information. Forexample, it contains a list of all the functions and variables thatare defined in this context. In addition, it stores informationabout the environment, for example if the code that is currentlylooked at is a process, function, procedure, etc. It then make thedecision which function to call from the ARGG-HDL objectconverter. It is important to note that the visitor itself does notmake any conversion it just calls the function from the ARGG-HDL converters. Last is the ARGG-HDL object converters.
EEE TRANSACTIONS ON NUCLEAR SCIENCE, VOL. XX, NO. XX, XXXX 2020 9
BasePrimitive Types EntitiesClass Types
Shadow Hierarchy Used for ConversionBasePrimitive Types EntitiesClass TypesHLPyHDL Hierarchy
Fig. 10. Every object in ARGG-HDL has a meta object which handles theconversion to VHDL. For consistency the object and the meta object has thesame inheritance hierarchy.
Packet File Includes
Packet HeaderPacket BodyEntity File IncludesEntity Definition Generic List
Port ListArchitecture
Architecture HeaderArchitecture Body Architecture Body
Entity InstantiationCombinatorial LogicProcess
Sensitivity ListProcess HeaderProcess BodyTypes DefinitionsFunction DeclarationFunction Definition
Types Definitions
SubtypesConstantsRecords Record MembersRecord Default
Fig. 11. Any VHDL file consists of the blocks shown above. Each ARGG-HDL object has a function overload that can inject code at any of these location.The Python code and VHDL code can be made completely independent of eachother (if necessary). Each Object can overload this function. Each functionhas a default but can be freely modified.
Each object possesses its own converter, which defines howit is supposed to be converted to VHDL. This converter alsodefines how each interaction with the object is converted. Thisgives the user a very granular way of defining how the objectwill converted to VHDL. In addition, its modular design makesit very easy to replace one set of object converters for anotherset, which would allow a conversion to, for example, Verilogor even C++.
B. The ARGG-HDL object Converter
In ARGG-HDL each object has a member calledhdl converter. This object contains all the code for theconversion to VHDL. As shown in figure 10, the hdl converterobjects follow the same inheritance hierarchy as the the def b o d y L S h i f t ( a s t P a r s e r , Node ) :r h s = a s t P a r s e r . Unfold body ( Node . r i g h t )l h s = a s t P a r s e r . Unfold body ( Node . l e f t )l h s = l h s . h d l c o n v e r t e r . r e a s s i g n t y p e ( l h s )r h s = r h s . h d l c o n v e r t e r . g e t V a l u e ( rhs , l h s , a s t P a r s e r ) return l h s . h d l c o n v e r t e r . r e a s s i g n ( l h s , rhs , a s t P a r s e r )Listing 15. ARGG-HDL AST visitor for value assignment (simplified). def r e a s s i g n ( s e l f , obj , rhs , a s t P a r s e r =None ) :asOp = o b j . h d l c o n v e r t e r . g e t a s s i g n m e n t o p ( o b j ) return t a r g e t +asOp + s t r ( r h s )Listing 16. ARGG-HDL object hdl converter for assigning a new value toan object (simplified). objects themselves. This gives a consistent behavior. The actualworking of the converter is understood with a practical example.The code in Listing 15 shows the visitor for the assignmentoperator. The first thing it does is it unfolds the arguments.Where ”rhs” stands for right hand side and is the source of theassignment and ”lhs” stands for left hand side, and is the targetof the assignment. Once this is done the visitor asks the targetwhat type it expect. Next it calls the ”getValue” function of thehdl converter. This function will return the ”value object” ofthe source object. In the last step it calls the ”reasign” functionof the target. This is the generic part, it will be called for anyassignment operation that is found in the conversion. In thenext example we see a practical implementation of this.Example: std logic vector (SLV) assigned to SLV. Since itis already a primitive type ”reassign type” will just returnthe object itself with no changes done. The same is truefor ”getValue”. It will just return the object itself with nochanges. Lastly the ”reassign” function connects these twoobjects together. A simplified version of the ”reassign” functionis shown in Listing 16. It shows that in this case it converts theassignment from Python into VHDL. The only thing it needsto do is to extract the correct assignment operator. Dependingon the target being either a variable or a signal, the operatorhas to be either ”¡=” or ”:=”.Example: Assigning AXI4-Stream secondary handler toAXI4-Stream primary handler. In this example, the ”getValue”function of the hdl converter of the secondary handler is wortha deeper look. It is clear that the user of the class wants to assignthe data value to the target. In order to extract the data fromthe AXI4-Stream secondary handler one has to call ”read data”function and assign the value to a variable. Since this functionneeds to modify the handler object and the function cannotmodify the input, this function needs to be implemented as aprocedure. Procedures can modify the input but cannot havea return value. This unnecessary limitation of VHDL can beovercome by creating a custom ”getValue” function for thehdl converter.Listing 17 shows a simplified version of the ”getValue”function from the hdl converter of the AXI4-Stream secondaryhandler. First it creates a name for a variable that will be usedto buffer the data. On the next line it tries to get the variablefrom the list of defined variables. This is done to not createmore than one of these buffer variables. If no variable withthat name could be found the function returns ”None”. In this
EEE TRANSACTIONS ON NUCLEAR SCIENCE, VOL. XX, NO. XX, XXXX 2020 10 def g e t V a l u e ( s e l f , obj , ReturnToObj , a s t P a r s e r ) :vhdl name = s t r ( o b j ) + ” b u f f ”b u f f = a s t P a r s e r . t r y g e t v a r i a b l e ( vhdl name ) i f b u f f i s
None :b u f f = v v a r i a b l e ( o b j . rx . d a t a )b u f f . hdl name = vhdl namea s t P a r s e r . LocalVar . append ( b u f f )h d l = o b j . h d l c o n v e r t e r . call member func (obj , ” r e a d d a t a ” , [ obj , b u f f ] , a s t P a r s e r) i f h d l i s
None :a s t P a r s e r . M i s s i n g t e m p l a t e =True return b u f fa s t P a r s e r . AddStatementBefore ( h d l ) return b u f fListing 17. Shown is the getValue function of the AXI4-Stream secondaryhandler. case we enter the ”if” statement. In this we create a variableof the same type as the data. The new variable does not havean name yet. It receives it name on the next line. On the lastline of the ”if” statement we append this variable to the localvariable list (process/function scope).After the variable is successfully created we can use it toreadout the AXI4-Stream secondary handler. This object isreadout with the ”read data” function. This function takestwo arguments. The handler object itself and the outputbuffer variable. The function ”call member func” implementsa member function call with the given parameters. Sincefunctions are usually templates in ARGG-HDL, it is possible,that on the first call of the function the required function hasnot yet been created from the template. In this case the functionreturn ”None”. In this case the conversion was not successfultherefore this function has to inform the ”astParser” that thereis a template missing. This means that this object will beadded to the conversion queue again and will be reprocessedonce all the other objects have been processed. By then thenecessary function will be created and it can be used. In thiscase the function that was just generated will be added asa statement before the current statement. The return of thisgetValue function is the buffer variable. With this trick it ispossible to create functions in ARGG-HDL that modify theinput as well as having a return type.For completeness Listing 18 shows the implementation ofthe ”reassign” function of the AXI4-Stream primary handler.The implementation is straight forward. It asks the templatingmechanism for a function call to the function ”send data” if thefunction exist it returns this function call, otherwise it returnsa string that says ”missing template”. The function has toreturn a string. It could just as well return an empty string butfor debugging purposes it can sometimes be helpful to see atwhich stage of the conversion something went wrong. It is onlyimportant if the function was not found the ”Missing template”member is set to ”True”. def r e a s s i g n ( s e l f , obj , rhs , a s t P a r s e r ) :r e t = o b j . h d l c o n v e r t e r . v h d l c a l l m e m b e r f u n c (obj , ” s e n d d a t a ” , [ obj , r h s ] , a s t P a r s e r) i f r e t i s
None :a s t P a r s e r . M i s s i n g t e m p l a t e =True return ” $ $ m i s s i n g t e m p l a t e $ $ ” return r e tListing 18. Shown is the reassign function of the AXI4-Stream primaryhandler.
VI. C
ONCLUSIONS