Sequential logic is built from primitives such as registers and memories A key concept in sequential logic is the Sequential logic updates on the rising or less commonly falling edge of the clock and holds its value throughout the clock cycle clock

Reg_spec

The clock and related reset and clear signals are grouped together in a type called a Multiple sequential elements are then able to refer to the same Reg_spec t # open Hardcaml # let clock Signal input clock 1 val clock Signal t wire names clock width 1 # let clear Signal input clear 1 val clear Signal t wire names clear width 1 # let spec Signal Reg_spec create clock clear val spec Signal Reg_spec t <abstr> Reg_spec t

Registers, Pipelines

A simple register takes a signal as input and basically delays it for one cycle The pipeline function will delay its input for multiple cycles # let d_in Signal input d_in 8 val d_in Signal t wire names d_in width 8 # let q_out Signal reg spec enable Signal vdd d_in val q_out Signal t register width 8 clock clock clock_edge Rising clear clear clear_to 0b00000000 data_in d_in # let q_out_after_3_clocks Signal pipeline spec enable Signal vdd n 3 d_in val q_out_after_3_clocks Signal t register width 8 clock clock clock_edge Rising clear clear clear_to 0b00000000 data_in register

Registers with Feedback

We noted previously that combinational logic could not contain cycles We can lift this restriction with sequential logic so long as the cycle passes through a register or memory The function encodes a simple example of this pattern For example to build a counter we need to access the current value to produce the next one reg_fb # let counter Signal reg_fb spec enable Signal vdd width 8 f fun d > Signal d 1 val counter Signal t register width 8 clock clock clock_edge Rising clear clear clear_to 0b00000000 data_in wire

Wires

In Hardcaml a is a signal which can be declared before providing its input driver Logically it does nothing it just passes its input through to its output Wires can later be assigned an input driver Apart from the fact they logically do nothing they are really useful It is how the function is implemented Without wires we cannot express the above function since the input to refers to its own output Two words of caution when using wires Combinational cycles We can t stop you from creating them but we can throw an exception when you do It s worth repeating cycles must pass through a sequential primitive The hairy unassigned wire exception Hardcaml will detect when you forget to assign a value to a wire but the error will not be especially useful in finding out where You can enable an extra level of debugging information by setting the following value which will help track down where the wire was originally defined wire # let w Signal wire 1 val w Signal t wire width 1 # Signal w < vdd unit # w Signal t wire width 1 data_in 0b1 reg_fb # let reg_fb spec enable w f let d Signal wire w in let q Signal reg spec enable f d in Signal d < q q val reg_fb Signal Reg_spec t > enable Signal t > w int > Signal t > Signal t > Signal t <fun> reg Caller_id set_mode Top_of_stack

State Machines

State machines are just a complex combination of registers and multiplexers so it is possible to build them directly with the primitives described here but not very conveniently Rather we recommend using the Always DSL

Memories

Core asynchronous memory primitive Hardcaml provides the primitive for describing memory structures They are synchronously written using Each write port can have a different clock The memory is read asynchronously using The read data is returned as an array one for each read port RAMs On their own s are not that useful Instead we provide the module which can target the physical RAM blocks in FPGAs i e BlockRAM or UltraRAM in Xilinx devices This is done by instantiating a multiport_memory and a register placed on either the read address or output data Note that this means the read ports are now synchronous and read data is returned one cycle later Each read port can have a different clock For this to work we use a feature of FPGA synthesizers called RTL RAM inference This process is notoriously finicky so read the tool reports to ensure it is doing what you expect I m looking at you Vivado Practical considerations The RAM structures provided with Hardcaml are very flexible regarding the number of read and write ports they can provide Physically however FPGAs provide RAMs with 1 or 2 ports If you specify more than this you will probably not get the results you intended The library offers a more targeted solution for Xilinx FPGAs and may be more suitable for applications where precise control of the Vendor RAM primitive is required multiport_memory write_ports let clock Signal input clock 1 let address Signal input address 8 let write_enable Signal input write_enable 1 let data Signal input data 32 # let write_port Write_port write_clock clock write_address address write_enable write_enable write_data data val write_port Signal t Write_port t Hardcaml Write_port write_clock wire names clock width 1 write_address wire names address width 8 write_enable wire names write_enable width 1 write_data wire names data width 32 read_addresses # let read_address address val read_address Signal t wire names address width 8 # let q Signal multiport_memory 256 write_ports |write_port| read_addresses |read_address| val q Signal t array | memory_read_port width 32 memory multiport_memory read_addresses address | multiport_memory Ram # let read_port Read_port read_clock clock read_address read_enable Signal input read_enable 1 val read_port Signal t Read_port t Hardcaml Read_port read_clock wire names clock width 1 read_address wire names address width 8 read_enable wire names read_enable width 1 # let q Ram create collision_mode Read_before_write size 256 write_ports |write_port| read_ports |read_port| val q Signal t array | register width 32 clock clock clock_edge Rising enable read_enable data_in memory_read_port | hardcaml_xilinx