A Chip is device containing streams, sinks and processes.
Typically a Chip is used to describe a single device. You need to provide the Chip object with a list of all the sinks (device outputs). You don’t need to include any process, variables or streams. By analysing the sinks, the chip can work out which processes and streams need to be included in the device.
Example:
>>> from chips import *
>>> from chips.VHDL_plugin import Plugin
>>> switches = InPort("SWITCHES", 8)
>>> serial_in = SerialIn("RX")
>>> leds = OutPort(switches, "LEDS")
>>> serial_out = SerialOut(serial_in, "TX")
>>> #We need to tell the Chip that leds and serial_out are part of
>>> #the device. The Chip can work out for itself that switches and
>>> #serial_in are part of the device.
>>> s = Chip(
... leds,
... serial_out,
... )
>>> plugin = Plugin()
>>> s.write_code(plugin)
Processes are used to define the programs that will be executed in the target Chip. Each Process contains a single program made up of instructions. When a Chip is simulated, or run in real hardware, the program within each process will be run concurrently.
Any Stream may be used as the input to a Process. Only one process may read from any particular stream. A Process may read from a Stream using the read method. The read method accepts a Variable as its argument. A read from a Stream will stall execution of the Process until data is available. Similarly, the stream will be stalled, until data is read from it. This provides a handy way to synchronise processes together, and simplifies the design of concurrent systems.
Example:
>>> from chips import *
>>> #sending process
>>> theoutput = Output()
>>> count = Variable(0)
>>> Process(16,
... #wait for 1 second
... count.set(1000),
... While(count,
... count.set(count-1),
... WaitUs()
... ),
... #send some data
... theoutput.write(123),
... )
Process(...
>>> #receiving process
>>> target_variable = Variable(100)
>>> Process(16,
... #This instruction will stall the process until data is available
... theoutput.read(target_variable),
... #This instruction will not be run for 1 second
... #..
... )
Process(...
An Output is a special Stream that can be written to by a Process. Only one Process may write to any particular stream. Like any other Stream, an Output may be:
- Read by a Process.
- Consumed by a Sink.
- Modified to form another Stream.
A Process may write to an Output stream using the write method. The write method accepts an expression as its argument. A write to an output will stall the process until the receiver is ready to receive data.
Example:
>>> #sending process
>>> theoutput = Output()
>>> Process(16,
... #This instruction will stall the process until data is available
... theoutput.write(123),
... #This instruction will not be run for 1 second
... #..
... )
Process(...
>>> #receiving process
>>> target_variable = Variable(0)
>>> count = Variable(0)
>>> Process(16,
... #wait for 1 second
... count.set(1000),
... While(count,
... count.set(count-1),
... WaitUs(),
... ),
... #get some data
... theoutput.read(target_variable),
... )
Process(...
Data is stored and manipulated within a process using Variables. A Variable may only be accessed by one process. When a Variable an initial value must be supplied. A variable will be reset to its initial value before any process instructions are executed. A Variable may be assigned a value using the set method. The set method accepts an expression as its argument.
It is important to understand that a Variable object created like this:
a = Variable(12)
is different from a normal Python variable created like this:
a = 12
The key is to understand that a Variable will exist in the target Chip, and may be assigned and referenced as the Process executes. A Python variable can exist only in the Python environment, and not in a Chip. While a Python variable may be converted into a constant in the target Chip, a Process has no way to change its value when it executes.
Variables and Constants are the most basic form of expressions. More complex expressions can be formed by combining Constants, Variables and other expressions using following unary operators:
~
and the folowing binary operators:
+, -, *, //, %, &, |, ^, <<, >>, ==, !=, <, <=, >, >=
The function Not evaluates to the logical negation of each data item equivalent to ==0. The function abs evaluates to the magnitude of each data item.
If one of the operands of a binary operator is not an expression, the Chips library will attempt to convert this operand into an integer. If the conversion is successful, a Constant object will be created using the integer value. The Constant object will be used in place of the non-expression operand. This allows constructs such as a = 47+Constant(10) to be used as a shorthand for a = Constant(47)+Constant(10) or count.set(Constant(15)+3*2 to be used as a shorthand for count.set(Constant(15)+Constant(6). Of course a=1+1 still yields the integer 2 rather than an expression.
Note
The divide // operator in Chips works differently then the divide operator in Python. While a floor division in Python rounds to -infinite, in Chips division rounds to 0. Thus -3//2 rounds to -2 in Python, it rounds to -1 in Chips. This should be more familiar to users of C, C++ and VHDL. The same also applies to the modulo % operator.
An expression within a process will always inherit the data width in bits of the Process in which it is evaluated. A Stream expression such as Repeater(255) + 1 will automatically yield a 10-bit Stream so that the value 256 can be represented. A similar expression Constant(255)+1 will give an 9-bit result in a 9-bit process yielding the value -1. If the same expression is evaluated in a 10-bit process, the result will be 256.
The operator precedence is inherited from the Python language. The following table summarizes the operator precedences, from lowest precedence (least binding) to highest precedence (most binding). Operators in the same row have the same precedence.
Operator | Description |
---|---|
==, !=, <, <=, >, >= | Comparisons |
Bitwise OR | |
^ | Bitwise XOR |
& | Bitwise AND |
<<, >> | Shifts |
+, - | Addition and subtraction |
*, //, % | multiplication, division and modulo |
~ | bitwise NOT |
Not, abs | logical NOT, absolute |
A Variable is used within a Process to store data. A Variable can be used in only one Process. If you need to communicate with another Process you must use a stream.
A Variable accepts a single argument, the initial value. A Variable will be reset to the initial value when a simulation, or actual device is reset.
A Variable can be assigned an expression using the set method.
A VariableArray is an array of variables that can be accessed from within a single Process.
When a VariableArray is created, it accepts a single argument, the size.
A VariableArray can be written to using the write method, the write method accepts two arguments, an expression indicating the address to write to, and an expression indicating the data to write.
A VariableArray can be read to using the read method, the read method accepts a single argument, an expression indicating the address to read from. The read method returns an expression that evaluates to the value contained at *address.
Example:
>>> from chips import *
>>> def reverse(stream, number_of_items):
... """Read number_of_items from stream, and reverse them."""
... temp = Variable(0)
... index = Variable(0)
... reversed_stream = Output()
... data_store = VariableArray(number_of_items)
... Process(8,
... index.set(0),
... While(index < number_of_items,
... stream.read(temp),
... data_store.write(index, temp),
... index.set(index+1),
... ),
... index.set(number_of_items - 1),
... While(index >= 0,
... reversed_stream.write(data_store.read(index)),
... index.set(index-1),
... ),
... )
...
... return reversed_stream
>>> c = Chip(
... Console(
... Printer(
... reverse(Sequence(0, 1, 2, 3), 4)
... ),
... ),
... )
>>> c.reset()
>>> c.execute(1000)
3
2
1
0
Streams are a fundamental component of the Chips library.
A Stream Expression can be formed by combining Streams or Stream Expressions with the following unary operators:
~
and the folowing binary operators:
+, -, *, //, %, &, |, ^, <<, >>, ==, !=, <, <=, >, >=
The function Not yields the logical negation of each data item equivalent to ==0. The function abs yields the magnitude of each data item.
Each data item in the resulting Stream Expression will be evaluated by removing a data item from each of the operand streams, and applying the operator function to these data items.
Generally speaking a Stream Expression will have enough bits to contain any possible result without any arithmetic overflow. The one exception to this is the left shift operator where the result is always truncated to the size of the left hand operand. Stream expressions may be explicitly truncated or sign extended using the Resizer.
If one of the operands of a binary operator is not a Stream, Python Streams will attempt to convert this operand into an integer. If the conversion is successful, a Repeater stream will be created using the integer value. The repeater stream will be used in place of the non-stream operand. This allows constructs such as a = 47+InPort(12, 8) to be used as a shorthand for a = Repeater(47)+InPort("in", 8) or count = Counter(1, 10, 1)+3*2 to be used as a shorthand for count = Counter(1, 10, 1)+Repeater(5). Of course a=1+1 still yields the integer 2 rather than a stream.
Note
The divide // operator in Chips works differently then the divide operator in Python. While a floor division in Python rounds to -infinite, in Chips division rounds to 0. Thus -3//2 rounds to -2 in Python, it rounds to -1 in Chips. This should be more familiar to users of C, C++ and VHDL. The same also applies to the modulo % operator.
The operators provided in the Python Streams library are summarised in the table below. The bit width field specifies how many bits are used for the result based on the number of bits in the left and right hand operands.
Operator | Function | Data Width (bits) |
---|---|---|
abs | Logical Not | argument |
Not | Logical Not | 1 |
~ | Bitwise not | right |
+ | Signed Add | max(left, right) + 1 |
- | Signed Subtract | max(left, right) + 1 |
* | Signed Multiply | left + right |
// | Signed Floor Division | max(left, right) + 1 |
% | Signed Modulo | max(left, right) |
& | Bitwise AND | max(left, right) |
| | Bitwise OR | max(left, right) |
^ | Bitwise XOR | max(left, right) |
<< | Arithmetic Left Shift | left |
>> | Arithmetic Right Shift | left |
== | Equality Comparison | 1 |
!= | Inequality Comparison | 1 |
< | Signed Less Than Comparison | 1 |
<= | Signed Less Than or Equal Comparison | 1 |
> | Signed Greater Than Comparison | 1 |
>= | Signed Greater Than Comparison | 1 |
The operator precedence is inherited from the python language. The following table summarizes the operator precedences, from lowest precedence (least binding) to highest precedence (most binding). Operators in the same row have the same precedence.
Operator | Description |
---|---|
==, !=, <, <=, >, >= | Comparisons |
Bitwise OR | |
^ | Bitwise XOR |
& | Bitwise AND |
<<, >> | Shifts |
+, - | Addition and subtraction |
*, //, % | multiplication, division and modulo |
~ | bitwise NOT |
Not, abs | logical NOT, absolute |
An Array is a stream yields values from a writeable lookup table.
Like a Lookup, an Array looks up each data item in the address_in stream, and yields the value in the lookup table. In an Array, the lookup table is set up dynamically using data items from the address_in and data_in streams. An Array is equivalent to a Random Access Memory (RAM) with independent read, and write ports.
A Lookup accepts address_in, data_in and address_out arguments as source streams. The depth argument specifies the size of the lookup table.
Example:
>>> def video_raster_stream(width, height, row_stream, col_stream,
... intensity):
...
... pixel_clock = Counter(0, width*height, 1)
...
... pixstream = Array(
... address_in = (row_stream * width) + col_stream,
... data_in = intensity,
... address_out = pixel_clock,
... depth = width * height,
... )
...
... return pixstream
>>> pixstream = video_raster_stream(
... 64,
... 64,
... Repeater(32),
... Counter(0, 63, 1),
... Repeater(255),
... )
A Stream which yields numbers from start to stop in step increments.
A Counter is a versatile, and commonly used construct in device design, they can be used to number samples, index memories and so on.
Example:
>>> from chips import *
>>> c=Chip(
... Console(
... Printer(
... Counter(0, 10, 2) #creates a 4 bit stream
... )
... )
... )
>>> c.reset()
>>> c.execute(100)
0
2
4
6
8
10
0
...
>>> c=Chip(
... Console(
... Printer(
... Counter(10, 0, -2) #creates a 4 bit stream
... )
... )
... )
>>> c.reset()
>>> c.execute(100)
10
8
6
4
2
0
10
...
A Decoupler removes stream handshaking.
Usually, data is transfered though streams using blocking transfers. When a process writes to a stream, execution will be halted until the receiving process reads the data. While this behaviour greatly simplifies the design of parallel processes, sometimes Non-blocking transfers are needed. When a data item is written to a Decoupler, it is stored. When a Decoupler is read from, the value of the last stored value is yielded. Neither the sending or the receiving process ever blocks. This also means that the number of data items written into the Decoupler and the number read out do not have to be the same.
A Decoupler accepts only one argument, the source stream.
Example:
>>> from chips import *
>>> def time_stamp_data(data_stream):
...
... us_time = Output()
... time = Variable(0)
... Process(8,
... Loop(
... WaitUs(),
... time.set(time + 1),
... us_time.write(time),
... ),
... )
...
... output_stream = Output()
... temp = Variable(0)
... Process(8,
... Loop(
... data_stream.read(temp),
... output_stream.write(temp),
... us_time.read(temp),
... output_stream.write(temp),
... ),
... )
...
... return output_stream
>>> time_stamped_stream = time_stamp_data(SerialIn())
A Fifo stores a buffer of data items.
A Fifo contains a fixed size buffer of objects obtained from the source stream. A Fifo yields the data items in the same order in which they were stored.
The first argument to a Fifo, is the source stream, the depth argument determines the size of the Fifo buffer.
Example:
>>> from chips import *
>>> def scope(ADC_stream, trigger_level, buffer_depth):
... temp = Variable(0)
... count = Variable(0)
... buffer = Output()
...
... Process(16,
... Loop(
... ADC_stream.read(temp),
... If(temp > trigger_level,
... buffer.write(temp),
... count.set(buffer_depth - 1),
... While(count,
... ADC_stream.read(temp),
... buffer.write(temp),
... count.set(count-1),
... ),
... ),
... ),
... )
...
... return Printer(Fifo(buffer, buffer_depth))
...
>>> test_signal = Sequence(0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 5, 5, 5, 5)
>>> c = Chip(Console(scope(test_signal, 0, 5)))
>>> c.reset()
>>> c.execute(100)
1
2
3
4
5
A HexPrinter turns data into hexadecimal ASCII characters.
Each each data item is turned into the ASCII representation of its hexadecimal value, terminated with a newline character. Each character then forms a data item in the HexPrinter stream.
A HexPrinter accepts a single argument, the source stream. A HexPrinter stream is always 8 bits wide.
Example:
>>> from chips import *
>>> #print the numbers 0x0-0x10 to the console repeatedly
>>> c=Chip(
... Console(
... HexPrinter(
... Counter(0x0, 0x10, 1),
... ),
... ),
... )
>>> c.reset()
>>> c.execute(1000)
0
1
2
3
4
5
6
7
8
9
a
b
...
A device input port stream.
An InPort allows a port pins of the target device to be used as a data stream. There is no handshaking on the input port. The port pins are sampled at the point when data is transfered by the stream. When implemented in VHDL, the InPort provides double registers on the port pins to synchronise data to the local clock domain.
Since it is not possible to determine the width of the stream in bits automatically, this must be specified using the bits argument.
The name parameter allows a string to be associated with the input port. In a VHDL implementation, name will be used as the port name in the top level entity.
Example:
>>> from chips import *
>>> dip_switches = InPort("dip_switches", 8)
>>> s = Chip(SerialOut(Printer(dip_switches)))
A Lookup is a stream yields values from a read-only look up table.
For each data item in the source stream, a Lookup will yield the addressed value in the lookup table. A Lookup is basically a Read Only Memory(ROM) with the source stream forming the address, and the Lookup itself forming the data output.
Example:
>>> from chips import *
>>> def binary_2_gray(input_stream):
... return Lookup(input_stream, 0, 1, 3, 2, 6, 7, 5, 4)
>>> c = Chip(
... Console(
... Printer(binary_2_gray(Counter(0, 7, 1)))
... )
... )
>>> c.reset()
>>> c.execute(100)
0
1
3
2
6
7
5
4
0
...
The first argument to a Lookup is the source stream, all additional arguments form the lookup table. If you want to use a Python sequence object such as a tuple or a list to form the lookup table use the following syntax:
>>> my_list = [0, 1, 3, 2, 6, 7, 5, 4]
... my_sequence = Lookup(Counter(0, 7, 1), *my_list)
An Output is a stream that can be written to by a process.
Any stream can be read from by a process. Only an Output stream can be written to by a process. A process can be written to by using the read method. The read method accepts one argument, an expression to write.
Example:
>>> from chips import *
>>> def tee(input_stream):
... output_stream_1 = Output()
... output_stream_2 = Output()
... temp = Variable(0)
... Process(input_stream.get_bits(),
... Loop(
... input_stream.read(temp),
... output_stream_1.write(temp),
... output_stream_2.write(temp),
... )
... )
... return output_stream_1, output_stream_2
>>> os_1, os_2 = tee(Counter(1, 3, 1))
>>> c = Chip(
... Console(
... Printer(os_1),
... ),
... Console(
... Printer(os_2),
... ),
... )
>>> c.reset()
>>> c.execute(100)
1
1
2
2
3
3
...
A Printer turns data into decimal ASCII characters.
Each each data item is turned into the ASCII representation of its decimal value, terminated with a newline character. Each character then forms a data item in the Printer stream.
A Printer accepts a single argument, the source stream. A Printer stream is always 8 bits wide.
Example:
>>> from chips import *
>>> #print the numbers 0-10 to the console repeatedly
>>> c=Chip(
... Console(
... Printer(
... Counter(0, 10, 1),
... ),
... ),
... )
>>> c.reset()
>>> c.execute(100)
0
1
2
3
4
...
A stream which repeatedly yields the specified value.
The Repeater stream is one of the most fundamental streams available.
The width of the stream in bits is calculated automatically. The smallest number of bits that can represent value in twos-complement format will be used.
Examples:
>>> from chips import *
>>> c=Chip(
... Console(
... Printer(
... Repeater(5) #creates a 4 bit stream
... )
... )
... )
>>> c.reset()
>>> c.execute(100)
5
5
5
...
>>> c=Chip(
... Console(
... Printer(
... Repeater(10) #creates a 5 bit stream
... )
... )
... )
>>> c.reset()
>>> c.execute(100)
10
10
10
...
>>> c=Chip(
... Console(
... Printer(
... #This is shorthand for: Repeater(5)*Repeater(2)
... Repeater(5)*2
... )
... )
... )
>>> c.reset()
>>> c.execute(100)
10
10
10
...
A Resizer changes the width, in bits, of the source stream.
The Resizer takes two arguments, the source stream, and the width in bits. The Resizer will truncate data if it is reducing the width, ans sign extend if it is increasing the width.
Example:
>>> from chips import *
>>> a = InPort(name="din", bits=8) #a has a width of 8 bits
>>> a.get_bits()
8
>>> b = a + 1 #b has a width of 9 bits
>>> b.get_bits()
9
>>> c = Resizer(b, 8) #c is truncated to 8 bits
>>> c.get_bits()
8
>>> Chip(OutPort(c, name="dout"))
Chip(...
A Scanner converts a stream of decimal ASCII into their integer value.
Numeric characters separated by non-numeric characters are interpreted as numbers. As it is not possible to determine the maximum value of a Scanner stream at compile time, the width of the stream must be specified using the bits parameter.
The Scanner stream accepts two inputs, the source stream and the number of bits.
Example:
>>> from chips import *
>>> #multiply by two and echo
>>> c = Chip(
... Console(
... Printer(
... Scanner(Sequence(*map(ord, "10 20 30 ")), 8)*2,
... ),
... ),
... )
>>> c.reset()
>>> c.execute(1000) # doctest: +ELLIPSIS
20
40
60
20
...
A Sequence stream yields each of its arguments in turn repeatedly.
A Sequence accepts any number of arguments. The bit width of a sequence is determined automatically, using the number of bits necessary to represent the argument with the largest magnitude. A Sequence allows Python sequences to be used within a Chips simulation using the Sequence(*python_sequence) syntax.
Example:
>>> from chips import *
>>> c = Chip(
... Console(
... Sequence(*map(ord, "hello world\n")),
... )
... )
>>> c.reset()
>>> c.execute(50)
hello world
hello world
hello world
...
A SerialIn yields data from a serial UART port.
SerialIn yields one data item from the serial input port for each character read from the source stream. The stream is always 8 bits wide.
A SerialIn accepts an optional name argument which is used as the name for the serial RX line in generated VHDL. The clock rate of the target device in MHz can be specified using the clock_rate argument. The baud rate of the serial input can be specified using the baud_rate argument.
Example:
>>> from chips import *
>>> #echo typed characters
>>> c = Chip(SerialOut(SerialIn()))
A Stream that allows a Python iterable to be used as a stream.
A Stimulus stream allows a transparent method to pass data from the Python environment into the simulation environment. The sequence object is set at run time using the set_simulation_data() method. The sequence object can be any iterable Python sequence such as a list, tuple, or even a generator.
Example:
>>> from chips import *
>>> stimulus = Stimulus(8)
>>> c = Chip(Console(Printer(stimulus)))
>>> def count():
... i=0
... while True:
... yield i
... i+=1
...
>>> stimulus.set_simulation_data(count())
>>> c.reset()
>>> c.execute(100)
0
1
2
...
Sinks are a fundamental component of the Chips library.
An Asserter causes an exception if any data in the source stream is zero.
An Asserter is particularly useful in automated tests, as it causes a simulation to fail is a condition is not met. In generated VHDL code, an asserter is represented by a VHDL assert statement. In practice this means that an Asserter will function correctly in a VHDL simulation, but will have no effect when synthesized.
The Asserter sink accepts a source stream argument, a.
Example:
>>> from chips import *
>>> a = Sequence(1, 2, 3, 4)
>>> c = Chip(Asserter((a+1) == Sequence(2, 3, 4, 5)))
Look at the Chips test suite for more examples of the Asserter being used for automated testing.
A Console outputs data to the simulation console.
Console stores characters for output to the console in a buffer. When an end of line character is seen, the buffer is written to the console. A Console interprets a stream of numbers as ASCII characters. The source stream must be 8 bits wide. The source stream could be truncated to 8 bits using a Resizer, but it is usually more convenient to use a Printer as the source stream. The will allow a stream of any width to be represented as a decimal string.
A Console accepts a source stream argument a.
Example:
>>> from chips import *
>>> #convert string into a sequence of characters
>>> hello_world = tuple((ord(i) for i in "hello world\n"))
>>> my_chip = Chip(
... Console(
... Sequence(*hello_world),
... )
... )
An OutPort sink outputs a stream of data to I/O port pins.
No handshaking is performed on the output port, data will appear at the time when the source stream transfers data.
An output port take two arguments, the source stream a and a string name. Name is used as the port name in generated VHDL.
Example:
>>> from chips import *
>>> dip_switches = InPort("dip_switches", 8)
>>> led_array = OutPort(dip_switches, "led_array")
>>> s = Chip(led_array)
A Response sink allows data to be transfered into Python.
As a simulation is run, the Response sink accumulates data. After a simulation is run, you can retrieve a python iterable using the get_simulation_data method. Using a Response sink allows you to seamlessly integrate your Chips simulation into a wider Python simulation. This works for simulations using an external simulator as well, in this case you also need to pass the code generation plugin to get_simulation_data.
A Response sink accepts a single stream argument as its source.
Example:
>>> from streams import *
>>> import PIL.Image #You need the Python Imaging Library for this
>>> def image_processor():
... #black -> white
... return Counter(0, 63, 1)*4
>>> response = Response(image_processor())
>>> chip = Chip(response)
>>> chip.reset()
>>> chip.execute(100000)
>>> image_data = list(response.get_simulation_data())
>>> image_data = image_data[:(64*64)-1]
>>> im = PIL.Image.new("L", (64, 64))
>>> im.putdata(image_data)
>>> im.show()
A SerialOut outputs data to a serial UART port.
SerialOut outputs one character to the serial output port for each item of data in the source stream. At present only 8 data bits are supported, so the source stream must be 8 bits wide. The source stream could be truncated to 8 bits using a Resizer, but it is usually more convenient to use a Printer as the source stream. The will allow a stream of any width to be represented as a decimal string.
A SerialOut accepts a source stream argument a. An optional name argument is used as the name for the serial TX line in generated VHDL. The clock rate of the target device in MHz can be specified using the clock_rate argument. The baud rate of the serial output can be specified using the baud_rate argument.
Example:
>>> from chips import *
>>> #convert string into a sequence of characters
>>> hello_world = map(ord, "hello world\n")
>>> my_chip = Chip(
... SerialOut(
... Sequence(*hello_world),
... )
... )
The instructions provided here form the basis of the software that can be run inside Processes.
The Block statement allows instructions to be nested into a single statement. Using a Block allows a group of instructions to be stored as a single object. A block accepts a single argument, instructions, a Python Sequence of instructions
Example:
>>> from chips import *
>>> a = Variable(0)
>>> b = Variable(1)
>>> c = Variable(2)
>>> initialise = Block((a.set(0), b.set(0), c.set(0)))
>>> Process(8,
... initialise,
... a.set(a+1), b.set(b+1), c.set(c+1),
... )
Process(...
The Break statement causes the flow of control to immediately exit the loop.
Example:
#equivalent to a While loop
Loop(
If(Not(condition),
Break(),
),
#do stuff here
),
Example:
#equivalent to a DoWhile loop
Loop(
#do stuff here
If(Not(condition),
Break(),
),
),
The Continue statement causes the flow of control to immediately jump to the next iteration of the containing loop.
Example:
>>> from chips import *
>>> in_stream = Counter(0, 100, 1)
>>> out_stream = Output()
>>> a = Variable(0)
>>> #allow only even numbers
>>> Process(12,
... Loop(
... in_stream.read(a),
... If(a&1,
... Continue(),
... ),
... out_stream.write(a),
... ),
... )
Process(...
>>> c = Chip(Console(Printer(out_stream)))
>>> c.reset()
>>> c.execute(100)
0
2
4
6
8
...
A loop in which one iteration will be executed each time the condition is false. The condition is tested after each loop iteration.
Equivalent to:
Loop(
instructions,
If(condition, Break()),
)
A loop in which one iteration will be executed each time the condition is true. The condition is tested after each loop iteration.
Equivalent to:
Loop(
instructions,
If(Not(condition), Break()),
)
The If statement conditionally executes instructions.
The condition of the If branch is evaluated, followed by the condition of each of the optional Elif branches. If one of the conditions evaluates to non-zero then the corresponding instructions will be executed. If the If condition, and all of the Elif conditions evaluate to zero, then the instructions in the optional Else branch will be evaluated.
Example:
If(condition,
#do something
).Elif(condition,
#do something else
).Else(
#if all else fails do this
)
The Loop statement executes instructions repeatedly.
A Loop can be exited using the Break instruction. A Continue instruction causes the remainder of instructions in the loop to be skipped. Execution then repeats from the beginning of the Loop.
Example:
>>> from chips import *
>>> #filter values over 50 out of a stream
>>> in_stream = Sequence(10, 20, 30, 40, 50, 60, 70, 80, 90)
>>> out_stream = Output()
>>> a = Variable(0)
>>> Process(8,
... Loop(
... in_stream.read(a),
... If(a > 50, Continue()),
... out_stream.write(a),
... )
... )
Process(...
>>> c = Chip(
... Console(
... Printer(out_stream)
... )
... )
>>> c.reset()
>>> c.execute(100)
10
20
30
40
50
10
...
Example:
>>> from chips import *
>>> #initialise an array
>>> myarray = VariableArray(100)
>>> index = Variable(0)
>>> Loop(
... If(index == 100,
... Break(),
... ),
... myarray.write(index, 0),
... )
Loop(...
The Print instruction write an integer to a stream in decimal ASCII format.
Print will not add any white space or line ends (in contrast to the Printer) The Print instruction accepts two arguments, the destination stream, which must be an Output stream, and a numeric expression, exp. An optional third argument specifies the minimum number of digits to print (leading 0 characters are added).
Example:
>>> #multiply by 2 and echo
>>> temp = Variable(0)
>>> inp = Sequence(*map(ord, "1 2 3 "))
>>> out_stream = Output()
>>> p=Process(8,
... Loop(
... Scan(inp, temp),
... out_stream.write(temp*2),
... )
... )
>>> c = Chip(Console(Printer(out_stream)))
>>> c.reset()
>>> c.execute(1000)
2
4
6
2
...
The Scan instruction reads an integer value from a stream of decimal ASCII characters.
Numeric characters separated by non-numeric characters are interpreted as numbers. If Scan encounters a number that is too large to represent in a process, the result is undefined.
The Scan accepts two arguments, the source stream and a destination variable.
Example:
>>> from chips import *
>>> #multiply by 2 and echo
>>> temp = Variable(0)
>>> inp = Sequence(*map(ord, "1 2 3 "))
>>> out = Output()
>>> p=Process(8,
... Loop(
... Scan(inp, temp),
... out.write(temp*2),
... )
... )
>>> c = Chip(Console(Printer(out)))
>>> c.reset()
>>> c.execute(1000) # doctest: +ELLIPSIS
2
4
6
...
A loop in which one iteration will be executed each time the condition is false. The condition is tested before each loop iteration.
Equivalent to:
Loop(
If(condition, Break()),
instructions,
)
The Value statement gives a value to the surrounding Evaluate construct.
An Evaluate expression allows a block of statements to be used as an expression. When a Value is encountered, the supplied expression becomes the value of the whole evaluate statement.
Example:
>>> from chips import *
>>> #provide a And expression similar to Pythons and expression
>>> def LogicalAnd(a, b):
... return Evaluate(
... If(a,
... Value(b),
... ).Else(
... Value(0),
... )
... )
>>> check = Output()
>>> Process(8,
... If(LogicalAnd(1, 4),
... check.write(-1),#true
... ).Else(
... check.write(0),#false
... )
... )
Process(...
>>> c = Chip(Asserter(check))
>>> c.reset()
>>> c.execute(100)
WaitUs causes execution to halt until the next tick of the microsecond timer.
In practice, this means that the process is stalled for less than 1 microsecond. This behaviour is useful when implementing a real-time counter function because the execution time of statements does not affect the time between WaitUs statements (Providing the statements do not take more than 1 microsecond to execute of course!).
Example:
>>> from chips import *
>>> seconds = Variable(0)
>>> count = Variable(0)
>>> out_stream = Output()
>>> Process(12,
... seconds.set(0),
... Loop(
... count.set(1000),
... While(count,
... WaitUs(),
... count.set(count-1),
... ),
... seconds.set(seconds + 1),
... out_stream.write(seconds),
... ),
... )
Process(...
A loop in which one iteration will be executed each time the condition is true. The condition is tested before each loop iteration.
Equivalent to:
Loop(
If(Not(condition), Break()),
instructions,
)