IEEE TRANSACTIONS ON NUCLEAR SCIENCE, VOL. XX, NO. XX, XXXX 2020 1

ARGG-HDL: A High Level Python Based Object-Oriented HDL Framework (Oct 2020) R. Peschke, K. Nishimura, G. Varner

Abstract---We present a High-Level Python-based Hardware the actual implementation it is difficult to achieve that. Here Description Language (ARGG-HDL), It uses Python as its ARGG-HDL can really shine because it allows the user on source language and converts it to standard VHDL. Compared one hand to write the firmware as low-level as is necessary to other approaches of building converters from a high-level programming language into a hardware description language, to fulfill the task, and on the other hand gives a clear path of this new approach aims to maintain an object-oriented paradigm integrating high level features into the design. Another popular throughout the entire process. Instead of removing all the high- code generation tool is ”MyHDL” [4]. It allows the user to level features from Python to make it into an HDL, this approach write firmware in Python. This is a task it does very well but goes the opposite way. It tries to show how certain features from in doing so it loses many of the features that make Python a high-level language can be implemented in an HDL, providing the corresponding benefits of high-level programming for the a high-level programming language. Most noticeable here is user. classes. It provides rudimentary support for grouping signals together in containers but this is the highest level of abstraction Index Terms---Computer languages, Object oriented program- ming, Open source software, Runtime library that one can achieve. With ARGG-HDL classes are a core part of its design, furthermore ARGG-HDL sees itself as a platform for the user to create their own abstractions on-top I.INTRODUCTION of the existing ones. As shown later in this document virtually ARGG-HDL is a library that allows the user to write Python every behavior of the library can be customised for individual [1] code and convert it to VHDL (VHSIC-HDL, Very High classes. Speed Integrated Circuit Hardware Description Language) [2]. This document shows three of its most defining features. Firstly II.OBJECT-ORIENTED DESIGN ARGG-HDL is fully object oriented, which allows the user to Object-oriented Design has been the foundation of virtually create high-level objects. This Object Oriented approach allows all modern programming languages. Object-Oriented design has the users of the library to express their intentions in a cleaner been proven excellent at hiding complexity. Humans are able to manner, which can result in much cleaner code. Secondly, it use extremely complicated technology, such as computer chips, allows the user to write very generic code. This reduces the since its complexity is hidden inside powerful abstractions, amount of code duplication. Due to its high-level templating which reduce its complexity to just pushing buttons. mechanism, it can be used to write highly generic code. Thirdly, it uses a preexisting language, which allows the user to use A. Programming in VHDL without objects the already well known and very powerful tools, which are provided in the Python language. Instead of having to learn Listing 1 shows a typical example of an entity declaration both digital logic design practices AND a completely new in VHDL. The entity declared on 1 is a queue or ”First in First language, the user only has to learn a few extensions provided out” (FIFO) module (see figure 1). A FIFO consists of two by this library. sides. The data input side and the data output side. In order

arXiv:2011.02626v1 [cs.PL] 5 Nov 2020 to stay synchronized each side provides three signals. For the Data Input side these signals are: write data, write enable, and A. Competing Approaches full. For the correct use of this FIFO, it is crucial to handle The idea of generating firmware from a high-level program- these signals precisely according to specification. Yet when ming language is certainly not new; in fact some of these we look at the entity declaration the only thing that reveals approaches have been around for decades. So far there exist which signals belong to which side of the FIFO is a vague at least 30+ approaches for High level Synthesis (HLS) [3] naming convention. VHDL has no concept to describe these and firmware generation, but what sets ARGG-HDL apart three signals belonging to one interface. A main reason for from the others is its high level approach to writing low level this is that each port has to be defined as an input or an output. firmware. If compared to, for example, ”VivadoHLS,” it is clear Therefore it is not possible to make a record with these three that ”VivadoHLS” provides very strong support for generating signals. In this case the entire record and all its signals, would algorithms out of existing c/c++ code and porting them over either be an input port or an output port. to firmware, but the code written in c/c++ has very little Figure 2 shows, how two entities communicate with each resemblance to the firmware generated. In fact most of the other using three signals. As can be seen, it is not obvious time the generated code will only be used as a black box IP that these three signals actually define one interface. The only core. For many applications this is a solid approach but in thing that reveals this information is a naming convention. cases where one still wants to have the low level control over Inside each entity, the signals are also handled loosely. Their IEEE TRANSACTIONS ON NUCLEAR SCIENCE, VOL. XX, NO. XX, XXXX 2020 2

e n t i t y FIFO cc i s din dout g e n e r i c ( Synchronous DATA WIDTH : natural := 16; wen ren FIFO DEPTH : natural := 5 full empty ); p o r t ( c l k : in s t d l o g i c ; Fig. 1. FIFO with native interface r s t : in s t d l o g i c ; e n t i t y FIFO cc i s −− Primary Interface g e n e r i c ( s d a t a : in s t d l o g i c v e c t o r ( DATA WIDTH : natural := 16; DATA WIDTH−1 downto 0 ) ; DEPTH : natural := 5 s v a l i d : in s t d l o g i c ; ); s Ready : out std l o g i c ; p o r t ( −− Secondary Interface c l k : in s t d l o g i c ; m Data : out std l o g i c v e c t o r ( r s t : in s t d l o g i c ; DATA WIDTH−1 downto 0 ) ; d i n : in s t d l o g i c v e c t o r ( m valid : out std l o g i c ; DATA WIDTH−1 downto 0 ) ; m ready : in s t d l o g i c wen : in s t d l o g i c ; ); r e n : in s t d l o g i c ; end FIFO cc ; dout : out std l o g i c v e c t o r ( Listing 2. Declaration of AXI4-Stream FIFO. Compared to the native FIFO the DATA WIDTH−1 downto 0 ) ; primary and secondary side are again only indicated by a naming convention. full : out std l o g i c ; empty : out std l o g i c ); end FIFO cc ; B. Programming in ARGG-HDL with Objects Listing 1. Typical declaration of a FIFO with a native interface [5]. As shown in figure 1, the FIFO consists of three signals handling the writing of Data to C. The Basics the FIFO (din,wen, full) and three output signals (dout, ren, empty). The only hint that the user has on how to use this FIFO is a naming convention. But To start with ARGG-HDL a prior knowledge of any of course a naming convention does not communicate the timing behavior of hardware design language is not required but, if possible, the the interface. Compared the the more modern AXI4-Stream the native FIFO names used to describe certain constructs are chosen to be interface has the additional disadvantage of having two different interfaces for the data input side and the data output side. similar to VHDL, therefore user with a VHDL background will find the overall structure familiar. Listing 3 shows a simple example of a counter. The Example starts by creating a class which inherits from ”v entity” this marks the object as an properties are usually checked/set at very different stages of the entity object. V entities work very similar to to entities in program. Therefore, the user code and the interface code are VHDL In our example the entity has no ports and defines completely intermingled. This can get unmanageable quickly only one function, the ”architecture” function. Similarly to and leads to the typical ”write only code”, that is seen so often the architecture body of an entity in VHDL this contains the in VHDL. internal structure of the entity. Inside the architecture function the first thing that we create is a clock generator. The clock generator is derived from v entity object itself. It has one (public) member which is the clock. Since it is a normal Entity A Entity B Python object the clock generated by the entity clock generator Process Block Process Block Data can be accessed like any other member of a Python class with sequential ”clkgen.clk”. After this two signals are being created. One is sequential Valid statements statements sequential the counter, the other one is the maximum value to which sequential statements statements the counter should count. Both of these signals are of the sequential sequential statements statements Ready type ”v slv(32)”, which translates to a 32 bit standard logic sequential sequential statements vector. The second argument in ”max cnt = v slv(32,300)” statements sequential is the default value. Since this is ordinary Python code the sequential statements statements sequential initial value could have been given in hexadecimal as well. sequential statements statements The default initial value is zero. The next construct is a function definition ”def proc():” which is modified by a decorator ”@rising edge(clkgen.clk)”. This special decorated takes the function that is generated here and appends it to Output Interface User Code the ”on change” list of the signal it received as an argument Code Input ”clkgen.clk”. But instead of executing on ever signal changed Fig. 2. User Code and Interface code is intermingled. Interface code needs to this decorator makes sure that it is only executed on a rising be re-implemented for each entity. Interface code is hard to recognize. Inputs edge. Once the simulation has started the function ”proc” gets and outputs are independent objects. executed on every rising edge of the signal ”clkgen.clk”. The IEEE 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 ) : Entity A Entity B

def i n i t ( s e l f ) : Process Block Process Block

super (). i n i t () Interface Code Pull Pull Interface Code

self.architecture() Secondary Primary User Code sequential sequential Data User Code statements statements Interface sequential Valid Object Interface sequential statements Object statements Ready sequential sequential @architecture statements statements def architecture(self ): Interface Code Push Push Interface Code c l k g e n = c l k generator ()

Output Interface User Code c o u n t e r = v s l v ( 3 2 ) Input Code max cnt = v slv(32,300) Fig. 3. User Code and Interface code are clearly separated. Reusing of well known (tested) functions. Interface code is immediately recognizable @rising edge(clkgen.clk) 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 : the directionality defined in the interface class. In a secondary c o u n t e r << 0 port, the directionality is flipped. e n d architecture () Inside the entity the interface is handle by a handler class. This class contains the API for interacting with the interface. Listing 3. my first test bench Thereby, the user code and the interface code is separated. This makes it easier to write clean code that is easily understood. In addition, since the user never directly interacts with the function proc does three things. First it increments the counter interface signals, the interface author can ensure that the signal by one. For this it uses the stream (bit-shift) operator. interface is used correctly. The idea is that the new value gets stream into the existing and 2) Example: Clocked Entity sending data via AXI4-Stream persistent object. Since the object might be connected to other link: Listing 4 shows an example of a data source entity. In object it is important that there is no new object generated but this case the entity just counts up numbers. For interaction the existing object is modified. Similar to C++, ARGG-HDL with other entities, it uses the AXI4-Stream interface [6]. Since uses the stream operator to stream data into objects (signals). it is a data source, it uses the ”port primary” version of the In addition to the left shift operator (stream in operator) there interface. On the architecture level it first defines a handler is a right shift operator (stream out operator). Future examples object called ”data out”, which handles the interaction with the will show them in action. Next, the counter is compared to interface. The handler is used inside the process block. The max cnt and if it is bigger or equal it is then set to zero again. process block is executed on every rising edge of the clock. The last statement is ”end architecture()” which handles some Inside the process block the handler is first checked with the book keeping such as giving all the signals the correct name ”if statement”. Since the handler class is a high level object etc. It needs to be executed at the end of the ”architecture” the author has the possibility to overload the ”bool” function, block. which is called inside the test block of the ”if” statement. On the use of streamer (bit shift) operators: In other This allows for expressive code. If the bool function of programming languages, such as C++ overloading the streamer the interface handler returns true, the interface is ready to use. (bit-shift) operator to send data into an object (or read from it) Since the interface class has only one purpose, it is immediately has been part of the language for decades. It is a clear way of clear what this statement means. It means that the interface is communicating to the user that this object is not replaced with ready to send data. On the next line, the data can be assigned a new object but its content is modified. Of course it would be to the handler object, which is equivalent to calling the send possible to use a ”next” function (or property) to achieve the data function. The user of the library never needs to know the same behavior. In the end it is a matter of taste which style internal structure of this interface nor does the user need to one prefers but overall using the streamer (bit-shift) operator know the protocol for sending data. The users can always be gives a very natural indication of data flow. sure that, as long as they are only using the functions provided On variables and signals: ARGG-HDL allows you to by the API, the interface will always work correctly. define signals as either variable or signal. However since this 3) Example: Interface Class: How the interface class works ”variableness” and ”signalness” is part of the object definition is now described. Shown in Listing 5 is the interface class there is no need for two separate assignment operators. The for an AXI4-Stream interface with a data width of 32 bits. It streamer (bit-shift) operator will work on both types. defines all signals it needs to operate. It sends from primary 1) Communication between Entities with Classes and Ap- to secondary the following signals: valid, last and data. From plication Programming Interfaces (API): In ARGG-HDL the secondary to primary it sends the ready signal. Primary and interface is defined as an object (see figure 3). The interface secondary are defined relative to the data flow. The data flows object contains all three signals as well as their directionality. from primary to secondary. Each entity has one port for the entire interface. This way it 4) Example: Interface Handler Class: Listing 6 describes is clear that, these signals belong to one interface. Instead of how the interface handler class works, the interface class defining a port as either input or output, the port are defined as has to inherit from an ARGG-HDL base class called either primary or secondary. In a primary port, the signals have ”v class primary”. The constructor takes one argument, which IEEE 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 hdl .examples import * c l a s s Counter ( v c l k e n t i t y ) : c l a s s axiStream 32 ( v c l a s s t r a n s ) : def i n i t (self, clk ): def i n i t ( s e l f ) : super (). i n i t ( c l k ) super (). i n i t () self.Dout = port out(axiStream 32 ( ) ) self.valid = port o u t ( v s l ( ) ) self.architecture() self.data = port o u t ( v s l v ( 3 2 ) ) self.ready = port i n ( v s l ( ) ) @architecture def architecture(self ): Listing 5. This class describes the signals required for the interface. In addition d a t a = v s l v ( 3 2 ) to just storing the type of the data, it also stores the direction of the data. By d a t a o u t = g e t handle(self .Dout) definition, data flows from primary to secondary. port out defines a signal that goes from primary to secondary. port in defines a signal that goes from @rising edge(self.clk) secondary to primary def proc ( ) : i f d a t a o u t : d a t a o u t << d a t a d a t a << d a t a + 1 Listing 4. Instead of defining individual inputs and outputs, an interface object is defined. The object ”axiStream 32” contains all information about input and output signals. The object brings a handler with it. The handler is used for communication with the interface. The handler provides the API for the 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 ) : interface. The user never directly interacts with the data members. def i n i t ( s e l f , Axi Out ) : super (). i n i t () self.tx = variable p o r t o u t ( Axi Out ) Axi Out << s e l f . t x

def s e n d data(self , dataIn ): self.tx.valid << 1 is the interface class. Then defines a variable port of that type self.tx.data << d a t a I n and connects the two objects together. Note that since these def r e a d y t o send(self ): two object, ”axi out” and ”TX”, are objects they know exactly return not self.tx.valid which signal has to go in which direction in order to make def onPull(self ): the interface work. The handler class defines three additional i f self.tx.ready: functions. First is the send data function. It takes any data self.tx.valid << 0 it receives and sends it out through the interface class. In self.tx.last << 0 addition, it also sets the steering signal ”valid” to high. The Listing 6. This class handles the primary(sender) side of the interface. The constructor takes the Interface class as argument. It makes a local copy in a second function is the ”ready to send” function. It is used to variable. It connects the local copy to the Input argument. The object knows check if the interface is ready to send new data. The interface exactly which signals have to go in which direction. The onPull functions is only ready to send data if it is not already sending data. allows the creator of the interface class to inject functionality on every clock Therefore, the ”valid” signal needs to be low in order to send cycle (before the user’s code) more data. The third function is the ” on Pull” function. This function is called on every clock cycle at the beginning of the process block. In this case, it checks if the ”ready” signal send from the secondary is high. If yes, it means that the secondary has read the current data and is ready to receive new data. If the ready signal is high, the steering signals can be reset and c l a s s t b ( v e n t i t y ) : def i n i t ( s e l f ) : new data can be sent. super (). i n i t ()

5) Example: Entities are Objects: Entities are themselves @architecture def architecture(self ): objects, therefore each port of an entity can be accessed as c l k g e n = c l k generator () a member variable of this object. The example in Listing 7 cnt = Counter(clkgen.clk) shows how different entities can be connected together. Inside axPrint = AxiPrint(clkgen.clk) a x P r i n t . D in << c n t . Dout the architecture block of the entity, we first create a clock e n d architecture () generator entity. The instance is called ”clkgen”. The instance has an output port ”clk”, which can be accessed like any other 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 (self, clk ): member variable in Python. We use the clock provided by super (). i n i t ( c l k ) ”clkgen” in the constructor for the other entities. Now that we s e l f . D in = p o r t in(axiStream 32 ( ) ) have created the entity ”cnt” (short for counter) and ”axiPrint” Listing 7. AxiPrint is an entity that prints out the values of the stream. It we can connect its signals together. We do this by simply uses the same interface class as ”Counter” but since it wants to consume the data it is a secondary port. In order to connect ”Counter” with ”AxiPrint” the assigning the output of the counter to the input of the ”axi Data in / Data out Member needs to be connected. Since the interface class print” object. Since the interface is an object itself, it knows knows which signal goes in which direction the signals can just assigned to exactly which signals have to go in which direction. The signals each other. do not need to be repeated on the test bench entity. Everything is contained inside the entities themselves. IEEE TRANSACTIONS ON NUCLEAR SCIENCE, VOL. XX, NO. XX, XXXX 2020 5

Peripheral Process

Clock ARGG_HDL Class

Data Combinatorial block i_Data Data Entity A Entity B Data enable If empty == 1: I_enable Read_data i_Data enable << 0 empty Else: i_empty enable << i_enable Fig. 4. A common problem is that data needs to be processed in different stages. For Example: Data comes in THEN stored in FIFO THEN Filtered THEN Modified 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 Fig. 5. Native FIFO interface: requires enable to be set to ’0’ immediately when empty goes to ’1’. This needs to be done using combinatorial logic. 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 (self ,clk=v s l ( ) ) : super (). i n i t ( c l k ) | s t r e a m d e l a y one(self.clk) \ s e l f . Axi in = pipeline in(axiStream 32 ( ) ) | s t r e a m d e l a y one(self.clk) \ s e l f . Axi out = pipeline out(axiStream 32 ( ) ) | \ self.architecture() s e l f . D Out e n d architecture () def architecture(self ): Listing 9. InputDelay: Takes in a data stream and sends out a data stream. axiSalve = get handle(self .Axi in ) Internally it uses two instances of stream delay one to create an delay of axPrimary = get handle(self .Axi out ) two clock cycles. By adding more instances of stream delay one in the pipe the delay can be made longer. Every entity that supports an AXI4-Stream @rising edge(self.clk) data stream can be used inside the pipe. Examples: Filter, FIFO, stages of def proc ( ) : readout system, stream splitter/merger etc. The length can be determent by a i f a x i S a l v e and axPrimary : parameter through a loop axPrimary << a x i S a l v e In Listing 9 one can see an entity that uses two instances of e n d architecture () the ”stream delay one” entity. As one can see it simply pipes the Listing 8. stream delay one takes an input data stream and delays it by one data input to the first entity then to the second entity and then clock cycle. For the library to know which port to use for the input/output back out to the data output. By adding, more instances of the they have to be marked as stream ports by using ”port Stream Secondary” / ”port Stream Primary”. There can only be one of each type ”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. D. Example: Pipe Lines E. Classes: Combination of Data and Functions A common approach to data processing is to subdivide the Let us have a deeper look into classes. Classes are usually task into different stages. For example we start with a data thought of as the combination of data and functions. That means source, which first needs to get stored in a FIFO, then filtered, they can contain both member data and member functions. then modified, and lastly sent to another module somewhere Hardware Description Languages (HDL), usually to deal with else. Many programming languages have for these kind of many different types of data, and functions. For example, there tasks a special operation: the so-called pipe line operator. Most are variables, signals and ports. In addition to normal functions, languages use the bitwise ”or” operator as the pipe operator. HDLs have to deal with functions, procedures, processes, and ARGG-HDL also uses the bitwise ”or” operator ”—”. entities. In order for an object to be a fully high-level object, 1) Example: stream delay one Pipe Line: The example it is mandatory that classes in ARGG-HDL are able to contain in Listing 8 shows how one can use this pipeline operator. all these different parts of the language. On creation of a new entity, one has simply to replace the ”port primary” keyword with the ”port stream primary” 1) Example: Class with Variables and Signals: Classes keyword. This creates a new primary port, which will be that contain variables, signals, and combinatorial logic (asyn- used for the pipeline operator. In the same way does the chronous push/pull functions) are useful for defining interfaces ”port stream secondary” port defines a secondary port, which to other entities. In the example, shown in figure 5, we want is used for the pipeline operator. There can only be one of to access a FIFO with a native interface. This specific FIFO each type. The purpose of the entity shown in this listing is to requires the enable signal to be always zero if the empty signal delay the data from an AXI4-Stream by one clock cycle. is high. It needs to be done immediately and cannot wait for the c l a s s InputDelay(v c l k e n t i t y ) : next clock cycle; therefore, it needs to be done asynchronously. def i n i t (self ,clk=v s l ( ) ) : In ARGG-HDL it is possible to encapsulate all this complexity super (). i n i t ( c l k ) s e l f . D In = pipeline in(axiStream 32 ( ) ) inside the interface handler class. The user of the class does s e l f . D Out = pipeline out(axiStream 32 ( ) ) not need to know that there is any additional logic. In fact, if self.architecture() the author of the classes uses the same public API, the user @architecture def architecture(self ): can switch back and forth between both types of interfaces s e l f . D In \ without having to change the code. IEEE 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 ) : ARGG_HDL Class super (). i n i t () Variable Record Member1

self.rx2 = free t y p e ( Data ) Member2 s e l f . rx2 << Data In self.rx1 = free t y p e ( Data ) Signal Record self.rx = variable p o r t i n ( Data ) MemberA s e l f . rx << s e l f . rx1 MemberB self.buff = small buffer(Data.data) MemberC self.enable1 = v v a r i a b l e ( v s l ( ) ) self.empty1 = v v a r i a b l e ( v s l ( ) ) self.architecture()

@architecture def architecture(self ): @combinational () Increment ( self , Self _ sig ) def p2 ( ) : self .rx2.enable << v s w i t c h ( Self _ sig MemberA <= Self _ sig MemberA + 1 ; 0 , [ v case(self.rx2.empty != 0, If Self _ sig MemberA > 1000 then self .rx1.enable) Self _ sig MemberA <= 0; ]) self .rx1.empty << self .rx2.empty End if; self.rx1.data << self.rx2.data self Member 1 := self Member 1 + 1 ; Listing 10. Handler class for a secondary port of a native FIFO (simplified). If self Member 1 > 2000 then

self Member 1 := 0; A simplified version of this handler class is shown in Listing End if; 10. As shown, it defines three sets of interface object: ”rx”, ”rx1” and rx2. The interface object ”Data In” is connected to the signal ”rx2”. The qualifier ”free type” prevents this object Fig. 6. Translation. The ARGG-HDL Class gets separated into its variables from becoming part of the ”NativeFIFO in” signal record. and signals. Since handler classes are always associated to exactly one process block the variables and signal records are given to the procedure with read This is necessary since a signal needs to have only one driver and write access (inout). and the driver for (parts) of these objects is going to be in combinational block. The Signal ”rx1” is connected to the variable port ”rx”. The connection between ”rx1” and ”rx2” is both the signal record and the variable record. Only interface done in the combinational block inside the architecture function classes (and primitive data types such as std logic vector) can of the ”NativeFIFO in”. Functions marked with combinational be used by more then one process block. are executed as soon as one of their captured objects changes. The first statement of the combinational block is the conditional III.GENERIC PROGRAMMING assignment of ”rx2.enable”. The first argument to the switch statement is the default value which is used when non of the The next feature any language needs in order to be considered cases match. There always needs to be an default statement. a high-level language is generic programming. Even though The second argument is a list cases. The first argument to the VHDL provides a limited amount of generic programming it case is the condition and the second argument is the value falls short when compared to a high-level language. Given the it returns if the condition is true. From this one can see that dynamic nature of Python it naturally provides possibilities for ”rx2.enable” is going to be zero except when ”rx2.empty” is very generic programming. ARGG-HDL tries to preserve the not true. In other words when the FIFO is not empty. The other generic programming model from Python as much as possible. two signals are just forwarded from ”rx2” to ”rx1” without change. This feature allows the user to create objects that mix variables signal clocked processes and non clocked processes. A. Example Classes in ARGG-HDL are translated into VHDL by first 1) Example: Interface Classes: Using templates in ARGG- dividing its members into signals or variables. It then creates HDL works very similar to Python. Listing 11 shows the actual a record for each type. In the next step, it translates the implementation of the interface class for an AXI4-Stream member functions. Similarly to Python, the first argument is interface. The data type is an argument to the constructor of the object itself. Since the object now consists of two records, the class. The data type can be every plain type, that includes the function/procedure gets both of these records as arguments. standard logic vectors of different sizes, integers etc. In addition, Since handler classes are associated with only one process it can also be records and arrays. For every new data type, the block. each function/procedure gets read/write (inout) access to templating mechanism will create a new class. IEEE TRANSACTIONS ON NUCLEAR SCIENCE, VOL. XX, NO. XX, XXXX 2020 7

c l a s s axiStream(v c l a s s t r a n s ) : c l a s s t b ( v e n t i t y ) : def i n i t (self ,Axitype): def i n i t ( s e l f ) : super (). i n i t () super (). i n i t () self.valid = port o u t ( v s l ( ) ) self.architecture() self.last = port o u t ( v s l ( ) ) @architecture self.data = port out( Axitype ) def architecture(self ): self.ready = port i n ( v s l ( ) ) c l k g e n = v c r e a t e ( c l k generator ()) Listing 11. For the actual implementation of the AXI4-Stream interface class c n t = v create(Counter(clkgen.clk)) the data object is a template variable. For each new data type the templating c n t o u t = g e t handle(cnt.Data out ) machine will create a new class. Data type can also be records and arrays. d a t a = v s l v ( 3 2 ) o p t data = optional t ( v s l v ( 3 2 ) )

@rising edge(clkgen.clk) 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 proc ( ) : def i n i t ( s e l f , Axi Out ) : c n t o u t >> d a t a self.tx = variable p o r t o u t ( Axi Out ) c n t o u t >> o p t d a t a Axi Out << s e l f . t x e n d architecture () def s e n d data(self , dataIn ): Listing 14. Optional t is a generic container that stores data of a certain type, self.tx.valid << 1 as well as a bit that indicates if the data is valid or not. cnt out is an instance self.tx.data << d a t a I n of a counter entity which uses the AXI4-Stream interface. It is written long Listing 12. Also the handler classes are templates. In addition to the class before optional t. Since optional t has a reset function and an assignment being a template so are the member functions. That means as long as the operator that accepts this data type it will work. assignment operator is defined for the types: of ”self.tx.data” and ”dataIn” it will work.

2) Example: Interface Handler Classes: In addition to the class itself being a template, all its member functions are templates too. This means that as long there is a valid assignment from ”dataIn” type to ”self.tx.data” in Listing 12 this code will compile and it will work. Listing 13 shows a practical example of a member function template. We see the secondary handler class of an AXI4- Stream. Its purpose is to readout the data received. The data are first stored in an internal buffer. The readout can be done Fig. 7. This picture shows the result of the simulation of Listing 14. It with the right shift/stream out operator. The first thing the shows how the AXI4-Stream handler class interacts with the two different objects (data and opt data). The ”data” object is a standard logic vector. The function does is it resets the output object. Then it checks if ”opt data” object is and optional data object. This means it has an extra bit the internal buffer has data. If the internal buffer has data, the which indicates if the data is valid or not. When reading out data from an data is then written to the output object. This function is clearly AXI4-Stream interface using the right shift (stream out) operator the target is first reset, then the handler checks if it received data and only if it has written for the case when the data output type is of the exact received data it will write the data to the target. In red the raw AXI4-Stream same type as the internal data type. But since it is a template input is shown. One can see that every N clock cycle a new value is sent. it accepts any type that has a valid assignment operator for On reset the standard logic vector sets all bits to zero. If no new value was received the ”data” object will remain zero. The ”opt data” object sets on this data type. reset only the valid bit to zero. As one can see from the picture both ”data” A practical use case is shown in Listing 14. It shows a test and ”opt data” get set correctly to the new value as soon as it is receives. This bench with a counter as a data source and one process block, shows that even different data types can be set correctly by the AXI4-Stream interface handler as long as they provide a compatible set of public interface using the data. As described before the stream out operator functions. is a template, which means, it accepts every type that has the following two features. Firstly, it needs to have a reset function and secondly it needs have an assignment operator for this data type. The newly created optional t class has both of these features and therefore it can be used as a target for the readout of the AXI4-Stream. optional t is a container class which stores a data object alongside a valid bit that indicates if the data is valid or not. 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 secondary ): def r s h i f t (self , rhs): The result of the simulation of this test bench can be seen rhs.reset() in figure 7. Every ten clock cycles the counter sends a new 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 : number via the AXI4-Stream. In this simulation, data is a r h s << s e l f . d a t a i n t e r n a l 2 s e l f . d a t a i n t e r n a l w a s r e a d 2 << 1 standard logic vector. The simulation shows that the output Listing 13. AXI4-Stream receiver handler stores the data received by the data object is zero except for the clock cycles where it received AXI4-Stream first internal before the user can extract it. If the handler does data. Comparing that with the optional t object shows that its not have any data it will just reset the output object and nothing else. The internal data member always stays on the previous value. Just output object must either be of the same time as the stream data Or must be of a type that accepts the data type as input. the valid member changes according to the data being valid or not. IEEE TRANSACTIONS ON NUCLEAR SCIENCE, VOL. XX, NO. XX, XXXX 2020 8

AST Library from ARGG HDL ARGG HDL Python visitor Object Convert

Python File VHDL File

Fig. 9.

for 15 different languages, such as C/C++, Java, .Net, and Fig. 8. Shown is the use of the widely used editor Visual Studio Code with a Haskell. Further more Python provides support for inter-process plugin for Plotly. This allows to plot the connection diagram while debugging the code. This Picture shows that with very minimal amount of work the user communication such as network sockets. Especially the support has access to high level features that are available for the Python language. for network sockets and TCP/UDP communication allows the Being a library for one of the most widely used language gives ARGG-HDL user a straight forward integration of the co-simulation. A immediate access to immense amount of extremely powerful libraries. common mode of communication between PC and FPGA is the TCP or the UDP protocol. In this scenario the FPGA can IV.ARGG-HDL ISNOTALANGUAGE, ITISA LIBRARY be mimicked by a Python simulation of the firmware which A. Seamless integration with other Python libraries listens on the address/port instead of the FPGA. When designing ARGG-HDL the question came up to either V.CONVERSIONTO VHDL make it a completely new language, make it a precompiler for VHDL or use a preexisting high-level programming language. The conversion from Python to VHDL is separated into By considering the unfeasible amount of work that would different steps. In the first step the entity that should be be required to build just the precompiler, it became clear converted needs to be instantiated. During the instantiating that the only reasonable solution was to use a preexisting process all the connections between the individual signals language. Therefore ARGG-HDL is not a language it is a entities and classes are created. At this point the entity would library. This gives it instantly access to all the high quality be able to run as a simulation in Python. The entities and all tools that are available for Python. Since Python is cross sub-entities have been instantiated at least once. Only objects platform, simulations in ARGG-HDL run on any platform that are currently instantiated get translated to VHDL. The that supports Python. For the simulation, all libraries that are library has a shadow register which contains all ARGG-HDL available for Python can immediately be used with ARGG- objects that are created during one session. In the next step it HDL. This includes all the powerful data visualization libraries starts looping over all objects that have been created. such as ”Matplotlib”, ”Plotly” etc. The same is true for file In order to allow the user of the library a more granular access. Python has very powerful libraries when it comes to control over the conversion to VHDL every part of the file IO. For example, currently it uses the library PY VCD for conversion can be modified. In the end, the Python code and writing ”variable change dump” files. However, it could easily the VHDL code can be made completely independent of each make use of the ”Pandas” library. Alternatively, if necessary other. For this, every VHDL file is subdivided into the blocks the data could be stored in a ”TTree” from the CERN ROOT shown in figure 11. Every object in ARGG-HDL has the option library. All these powerful libraries are just one pip install to modify any of these blocks. away. In addition, Python has a rich ecosystem of editors and A. The different stages of the conversion tool chains. Virtually all editors support features such as auto The Conversion is subdivided into three stages (see figure completion, step debugging, data visualization etc. Furthermore, 9). Firstly parsing of the Python source files. This is done Python already has a packaging system in place. That means, with a standard Python library called AST. It is part of the if one wants to share code with the world it can be uploaded reference implementation of CPython. Second, the ARGG-HDL to PyPi. This will not only take care of the distribution of the visitors, in order to traverse the AST, ARGG-HDL has its own library itself but will also take care of the dependencies. visitor class. This class does not only traverses through the One of the many features that are instantly available for syntax tree but also carries all the context information. For ARGG-HDL is, for example, plotting the connection diagram example, it contains a list of all the functions and variables that in real-time while running the program (see figure 8). are defined in this context. In addition, it stores information about the environment, for example if the code that is currently B. Co-Simulation looked at is a process, function, procedure, etc. It then make the Python is known for its outstanding integration to other decision which function to call from the ARGG-HDL object languages. Virtually all modern programming languages have converter. It is important to note that the visitor itself does not the possibility to interact with Python. The official Python web- make any conversion it just calls the function from the ARGG- side has examples on how to connect Python to other languages HDL converters. Last is the ARGG-HDL object converters. IEEE TRANSACTIONS ON NUCLEAR SCIENCE, VOL. XX, NO. XX, XXXX 2020 9

def body LShift(astParser ,Node): HLPyHDL Hierarchy Shadow Hierarchy rhs = astParser.Unfold body(Node. right) Used for Conversion Base lhs = astParser.Unfold body(Node. left ) 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 ) Base r h s = r h s . h d l c o n v e r t e r .getValue(rhs, lhs , astParser) return l h s . h d l c o n v e r t e r .reassign(lhs, rhs, astParser) Listing 15. ARGG-HDL AST visitor for value assignment (simplified).

def reassign(self , obj, rhs, astParser=None): Primitive Types Class Types Entities 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 target +asOp + s t r ( r h s ) Primitive Types Class Types Entities Listing 16. ARGG-HDL object hdl converter for assigning a new value to an object (simplified).

Fig. 10. Every object in ARGG-HDL has a meta object which handles the conversion to VHDL. For consistency the object and the meta object has the same inheritance hierarchy. objects themselves. This gives a consistent behavior. The actual working of the converter is understood with a practical example. The code in Listing 15 shows the visitor for the assignment Packet File Types Definitions

Includes operator. The first thing it does is it unfolds the arguments. Subtypes

Packet Header Where ”rhs” stands for right hand side and is the source of the Types Definitions Constants assignment and ”lhs” stands for left hand side, and is the target

Function Declaration Records of the assignment. Once this is done the visitor asks the target what type it expect. Next it calls the ”getValue” function of the Packet Body

Record Members hdl converter. This function will return the ”value object” of the source object. In the last step it calls the ”reasign” function Function Definition Record Default of the target. This is the generic part, it will be called for any assignment operation that is found in the conversion. In the next example we see a practical implementation of this. Entity File Architecture Body Example: std logic vector (SLV) assigned to SLV. Since it Includes Entity Instantiation is already a primitive type ”reassign type” will just return Entity Definition

Generic List Combinatorial Logic the object itself with no changes done. The same is true for ”getValue”. It will just return the object itself with no Port List Process changes. Lastly the ”reassign” function connects these two Sensitivity List Architecture objects together. A simplified version of the ”reassign” function Architecture Header Process Header is shown in Listing 16. It shows that in this case it converts the

Architecture Body Process Body assignment from Python into VHDL. The only thing it needs to do is to extract the correct assignment operator. Depending on the target being either a variable or a signal, the operator has to be either ”¡=” or ”:=”. 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. Example: Assigning AXI4-Stream secondary handler to The Python code and VHDL code can be made completely independent of each AXI4-Stream primary handler. In this example, the ”getValue” other (if necessary). Each Object can overload this function. Each function function of the hdl converter of the secondary handler is worth has a default but can be freely modified. a deeper look. It is clear that the user of the class wants to assign the data value to the target. In order to extract the data from Each object possesses its own converter, which defines how the AXI4-Stream secondary handler one has to call ”read data” it is supposed to be converted to VHDL. This converter also function and assign the value to a variable. Since this function defines how each interaction with the object is converted. This needs to modify the handler object and the function cannot gives the user a very granular way of defining how the object modify the input, this function needs to be implemented as a will converted to VHDL. In addition, its modular design makes procedure. Procedures can modify the input but cannot have it very easy to replace one set of object converters for another a return value. This unnecessary limitation of VHDL can be set, which would allow a conversion to, for example, overcome by creating a custom ”getValue” function for the or even C++. hdl converter. Listing 17 shows a simplified version of the ”getValue” function from the hdl converter of the AXI4-Stream secondary B. The ARGG-HDL object Converter handler. First it creates a name for a variable that will be used In ARGG-HDL each object has a member called to buffer the data. On the next line it tries to get the variable hdl converter. This object contains all the code for the from the list of defined variables. This is done to not create conversion to VHDL. As shown in figure 10, the hdl converter more than one of these buffer variables. If no variable with objects follow the same inheritance hierarchy as the the that name could be found the function returns ”None”. In this IEEE TRANSACTIONS ON NUCLEAR SCIENCE, VOL. XX, NO. XX, XXXX 2020 10

def getValue(self , obj, ReturnToObj, astParser): def reassign(self , obj, rhs, astParser): r e t = o b j . h d l c o n v e r t e r . call member func ( vhdl name = s t r ( o b j ) + ” b u f f ” obj , ” s e n d data”, [obj, rhs], astParser buff = astParser.try g e t variable(vhdl name ) ) i f r e t i s None : i f b u f f i s None : astParser .Missing template=True b u f f = v variable(obj.rx.data) return ” $ $ m i s s i n g template$$” b u f f . hdl name = vhdl name return r e t astParser .LocalVar.append(buff) Listing 18. Shown is the reassign function of the AXI4-Stream primary handler. 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 data”,[obj, buff],astParser ) VI.CONCLUSIONS i f h d l i s None : astParser .Missing template=True This document has shown that features from high-level pro- return b u f f gramming languages can be ported to firmware development.

REFERENCES astParser .AddStatementBefore(hdl) return b u f f [1] G. van Rossum 1995, Python tutorial, Centrum voor Wiskunde en Listing 17. Shown is the getValue function of the AXI4-Stream secondary Informatica (CWI) (Amsterdam) handler. [2] ”IEEE Standard VHDL Language Reference Manual,” in IEEE Std 1076-1987 , vol., no., pp.1-218, 31 March 1988, doi: 10.1109/IEEESTD.1988.122645. [3] Vivado Design Suite User Guide(High-Level Synthesis), UG902 (v2019.2) January 13, 2020 [4] FPGA ’15: Proceedings of the 2015 ACM/SIGDA International Sympo- case we enter the ”if” statement. In this we create a variable sium on Field-Programmable Gate Arrays February 2015 Pages 28 - 31 of the same type as the data. The new variable does not have https://doi.org/10.1145/2684746.2689092 an name yet. It receives it name on the next line. On the last [5] Embedded FIFO Generator v1.0 Logi CORE IP Product Guide PG327 (v1.0) July 14, 2020 line of the ”if” statement we append this variable to the local [6] AXI4-Stream FIFO v4.1LogiCORE IP Product GuideVivado Design variable list (process/function scope). SuitePG080 April 6, 2016 After the variable is successfully created we can use it to readout the AXI4-Stream secondary handler. This object is readout with the ”read data” function. This function takes two arguments. The handler object itself and the output buffer variable. The function ”call member func” implements a member function call with the given parameters. Since functions are usually templates in ARGG-HDL, it is possible, that on the first call of the function the required function has not yet been created from the template. In this case the function return ”None”. In this case the conversion was not successful therefore this function has to inform the ”astParser” that there is a template missing. This means that this object will be added to the conversion queue again and will be reprocessed once all the other objects have been processed. By then the necessary function will be created and it can be used. In this case the function that was just generated will be added as a statement before the current statement. The return of this getValue function is the buffer variable. With this trick it is possible to create functions in ARGG-HDL that modify the input as well as having a return type. For completeness Listing 18 shows the implementation of the ”reassign” function of the AXI4-Stream primary handler. The implementation is straight forward. It asks the templating mechanism for a function call to the function ”send data” if the function exist it returns this function call, otherwise it returns a string that says ”missing template”. The function has to return a string. It could just as well return an empty string but for debugging purposes it can sometimes be helpful to see at which stage of the conversion something went wrong. It is only important if the function was not found the ”Missing template” member is set to ”True”.