PyHSL#
PyHSL is a high-level language for constructing HSL sequences. PyHSL is Python with a set of support classes and functions, and the “compilation” of a PyHSL program is simply execution of the main()
function provided in the source file. That execution produces one or more HSL bytecode sequences, packaged into a single output file in HSLC format.
Usage#
The PyHSL compiler is named hslcompile.py
. The following is a summary of its options:
usage: hslcompile.py [-h] [-i source-file] [-o output-file] [-d output-directory] [-D parameter=value]
[-l {debug,info,warning,error,critical}]
options:
-h, --help show this help message and exit
-i source-file, --input source-file
source file to compile
-o output-file, --output output-file
.hslc output file
-H output-directory, --hslb_directory output-directory
output directory for .hslb files
-D parameter=value, --define parameter=value
define a configuration parameter
-l {debug,info,warning,error,critical}, --loglevel {debug,info,warning,error,critical}
logging level
Command-Line Parameters#
--input <source-file>
or-i <source-file>
This required parameter specifies the source file, which by convention has the extension
.hsl
. Only one source file can be specified, although that Python module is free to import functions and other definitions from other files.--output <output-file>
or-o <output-file>
This optional parameter specifies the output file, which should have the extension
.hslc
. This file uses the HSLC format, which contains multiple HSL bytecode sequences, each of which is labeled with the sequence name provided in the source file.The default output filename is
out.hslc
.--hslb_directory <output-directory>
or-H <output-directory>
This optional parameter specifies an output directory for files in HSLB format. An HSLB file consists of a small header followed by the bytecode for a single sequence. Each file is named based on the sequence name in the source; for example, a sequence named “Init” produces a file called
Init.hslb
. These files are intended to be used only for development, because they are easy to work with but lack many features of the HSLC format.If this option is not specified, no HSLB files are produced.
--define <parameter=value>
or-D <parameter=value>
This optional parameter can be provided multiple times. Each invocation adds a configuration parameter that can be used by the PyHSL source file. The
ph_params()
utility function returns the configuration parameters as a Python dictionary. (See the example in Utility Functions.)--loglevel <level>
or-l <level>
This optional parameter specifies the PyHSL log level. The default is
warning
.
Program Structure#
Every PyHSL source file defines a function named main()
that generates the HSL sequences. This function is typically divided into three sections:
Constant definitions, such as symbolic names for register offsets or lists of
(offset, data)
pairs to be written to an I2C device. (These can also be defined beforemain()
.)Creation of PyHSL objects representing I2C devices, GPIO pins, and so on. (See the next section for information on these objects.)
Definition of sequences. Each sequence contains method invocations on PyHSL objects to produce specific hardware operations.
The following example illustrates this structure:
def main():
# Section 1: constant definitions
POWER_REG = 0x1234
STATUS_REG_0 = 0x6000
# Section 2: creation of PyHSL objects
sensor = I2CDevice(0x42, 16, 16, 'sensor') # device at address 0x42 with 16-bit offsets and 16-bit registers
# Section 3: definition of sequences
with Sequence('Init') as seq:
sensor.write(POWER_REG, 0xAA) # write 0xAA to reg[0x1234]
seq.delay(100) # pause 100 μs before proceeding
seq.annotate('Begin poll') # (no effect except for possibly being logged)
sensor.poll(STATUS_REG_0, 0xABBA, 0xFFFF, 10000, 5) # 5 retries with 10000 μs between retries
sensor.readVerify(0x6002, 0xB0B0, 0xF0F0) # fail if (reg[0x6002] & 0xF0F0) != 0xB0B0
with Sequence('Deinit') as seq:
sensor.write(POWER_REG, 0x00)
One essential feature of HSL is its error handling. If an HSL sequence encounters an error, it terminates execution immediately and returns error information. This applies to both explicit error detection (for example, if the poll()
operation times out in the preceding code) and unexpected execution errors (for example, if an I2C bus transaction fails).
PyHSL Objects#
All HSL operations are generated by method calls on various PyHSL objects.
I2CDevice#
An I2CDevice
object represents a single device on an I2C bus. The following example creates an I2CDevice
object:
sensor = I2CDevice(0x42, 16, 8, 'xyzSensor')
0x42
is the device’s I2C address.16
is the number of bits used to specify an offset for this device. Currently, this value must be either 8 or 16.8
is the number of bits in a register for this device. Currently, this value must be either 8 or 16.'xyzSensor'
is the name of this device (used primarily for logging and error reporting).
Call PyHSL methods on an I2CDevice
object to generate HSL operations, such as the following examples:
sensor.write(0x1234, 0xAA) # write 0xAA to reg[0x1234]
sensor.poll(0x480, 0xABBA, 0xFFFF, 10000, 5) # wait for reg[0x480] to become 0xABBA; 5 retries with 10000 μs between retries
sensor.readVerify(0x6002, 0xB0B0, 0xF0F0) # fail if (reg[0x6002] & 0xF0F0) != 0xB0B0
The valid range for many parameters is determined by the offset width and data width parameters used when constructing the I2CDevice
object. For example, the sensor object shown earlier has an offset width of 16 (2 bytes) and a data width of 8 (1 byte). These values mean that all offset parameters to I2CDevice
methods must be in the range [0, 0xFFFF]
, and all parameters containing register data must be in [0, 0xFF]
. Furthermore, commands that write or read I2C registers will write or read a single byte on the I2C bus for this device. (Several commands have “stream” forms that ignore the data width and read or write a specified number of consecutive bytes.)
I2C devices encode multibyte register values in big-endian form. HSL takes care of this; if you are specifying a write to a register on a device with a data width of 16, simply use the native 16-bit value you want to write instead of manually swapping bytes because your CPU is little-endian.
Sequence#
A Sequence
object defines an entire HSL sequence. To define a new sequence, use a with
block like this:
with Sequence(name) as seq:
<operations on other PyHSL objects>
name
is the name of this sequence. The name must be unique in this source file.seq
is a variable containing the newSequence
object.
PyHSL operations inside the with
block add HSL operations to the current sequence. When the with
block terminates successfully, the sequence is finalized and added to the list of sequences that is written to the output file.
The PyHSL methods defined on Sequence
objects generate HSL operations that are not tied to a particular device. The most commonly used are delay()
, which pauses execution for a specified interval, and annotate()
, which inserts a comment into the stream. See the reference for details.
Sequence
objects have the following constraints:
HSL-generating methods on other objects fail if not done in the context of a
Sequence
(that is, inside awith Sequence
block).Sequences cannot be nested.
Sequence names must be unique across the entire module.
Utility Functions#
ph_params()#
This function returns a dictionary of configuration parameters as specified by the --define parameter=value
command-line option.
For example, if the compiler had been invoked like this—
hslcompile.py --define module_version=c2 --define use_broadcast=true (other options...)
—then ph_params()
returns the Python dictionary { 'module_version' : 'c2', 'use_broadcast' : 'true' }
.
All keys and values in the dictionary are strings; values formatted as integers, booleans, and so on are not interpreted. The option --define the_answer=42
results in { 'the_answer' : '42' }
.
ph_logger()#
This function returns the default logger for PyHSL. Its log level is initialized to the level specified by the --loglevel
parameter to hslcompile.py
.
ph_set_sequence_hooks()#
This function lets the author specify hook functions to be called at the beginning and end of every sequence definition:
ph_set_sequence_hooks(before_hook, after_hook)
After this call, the before_hook function is called immediately after the new sequence is created (before the instructions in the with
block are executed), and the after_hook function is called after the last instruction in the with
block, but before the final bytecode is generated for the sequence. This technique allows common processing (including generation of HSL operations) to be done at the beginning or end of every sequence.
Either or both of the before_hook and after_hook parameters can be None
, resulting in no call to that hook function.
Examples#
Configuration Parameters#
A single HSL source file can generate multiple variants of its sequences by using conditional logic. PyHSL allows the compiler invocation to provide configuration parameters to select these variations. The following example demonstrates using a single configuration parameter:
def main():
SENSOR_REG_STREAM_CTL = 0x100
SENSOR_REG_STATUS_0 = 0x280
sensor = I2CDevice(0x42, 16, 16, 'imx123')
# Obtain the sensor revision from the 'rev' configuration parameter
config_params = ph_params() # get the dictionary of configuration parameters
sensor_rev = config_params['rev']
with Sequence('StartStreaming'):
# The C2 revision of this sensor needs to wait on bit 2 of the status register before starting streaming
if sensor_rev == 'c2':
sensor.poll(SENSOR_REG_STATUS_0, expectedValue=0x4, mask=0x4, intervalInUsec=100, retries=10)
sensor.write(SENSOR_REG_STREAM_CTL, 0x80)
If this code is compiled with --define rev=c2
on the command line, the resulting StartStreaming
sequence performs a poll operation before writing SENSOR_REG_STREAM_CTL
. Be aware that these conditionals are evaluated at compile time, not at runtime. Depending on the value of the rev
configuration parameter, this code generates different sequences. If this were C or C++, these conditional operations would be like #if
or #ifdef
—evaluated at compile time—rather than if
statements. HSL does not have runtime conditionals, branching, or looping.
Sequence Hooks#
As described in the ph_set_sequence_hooks() section earlier, sequence hooks allow execution of common code at the start and finish of every sequence. The following example uses the before_hook function to poll a status register for a ready bit at the beginning of every sequence:
def main():
SENSOR_REG_STATUS_0 = 0x280
sensor = I2CDevice(0x42, 16, 16, 'imx123')
# Define operations to happen at the beginning of every sequence
def before_hook():
sensor.poll(SENSOR_REG_STATUS_0, expectedValue=0x8, mask=0x8, intervalInUsec=50, retries=10)
# Install the hook function
ph_set_sequence_hooks(before_hook, None)
with Sequence('Init'):
# This sequence begins with the poll operation specified in before_hook()
# (Other operations go here)
Reference#
I2CDevice Methods#
Constructor#
Create a new I2CDevice
object.
I2CDevice(<address>, <offset_width>, <data_width>, <name>, auto_retry=False, ten_bit_address=False)
<address>
is the device’s I2C address.<offset_width>
is the number of bits used to specify an offset for this device. Currently, this value must be either 8 or 16.<data_width>
is the number of bits in a register for this device. Currently, this value must be either 8 or 16.<name>
is a name for this device (used primarily for logging and error reporting).auto_retry
specifies whether failed I2C operations should always be retried.ten_bit_address
specifies whether this device uses ten-bit addressing. (Seven-bit addressing is the default.)
poll#
Poll an I2C register until its value reaches an expected value or the poll times out. If the poll times out, fail the entire HSL sequence.
poll(<offset>, <expectedValue>, <mask>, <intervalInUsec>, <retries>)
<offset>
is the offset of the register being polled.<expectedValue>
is the expected value of the register.<mask>
is the bitmask to apply for the check.<intervalInUsec>
is the interval between retries.<retries>
is the number of times to retry the check.
The operation is equivalent to the following code:
while retries >= 0:
registerValue = i2cReadRegister(offset)
if (registerValue & mask) == expectedValue:
return
retries -= 1
fail_sequence()
readDiscard#
Read an I2C register and discard the result.
readDiscard(<offset>)
<offset>
is the offset of the register being read.
This method is typically used only when the purpose of the read is to induce a side effect. It has the same effect as calling readVerify(offset, 0, 0)
.
readVerify#
Read an I2C register and verify that its value is equal to the expected value. If it is not equal, fail the entire HSL sequence.
readVerify(<offset>, <expectedValue>, <mask>)
<offset>
is the offset of the register being read.<expectedValue>
is the expected value of the register.<mask>
is the bitmask to apply for the check.
The operation is equivalent to the following code:
registerValue = i2cReadRegister(offset)
if (registerValue & mask) != expectedValue:
fail_sequence()
readVerifyStream#
Read a series of bytes from I2C and verify that they are equal to the array of expected byte values. If any are not equal, fail the entire HSL sequence.
readVerifyStream(<startOffset>, <expectedValuesList>)
<startOffset>
is the offset of the first byte read from the device.<expectedValuesList>
is a Python sequence (list or tuple) containing byte values.
The operation is equivalent to the following code:
for i in range(len(expectedValuesList)):
registerValue = i2cReadByte(offset + i)
if registerValue != expectedValuesList[i]:
fail_sequence()
write#
Write values to I2C. This method has a variety of parameter lists, described in the following subsections.
write(offset, data)#
Write one or two bytes of <data>
to the device at the specified <offset>
. If the data width specified at device creation was 8, one byte is written; if it was 16, two bytes are written.
write((offset1, data1), (offset2, data2), …)#
Write a series of registers in the device. This method is equivalent to calling write(<offset>, <data>)
multiple times:
write(<offset1>, <data1>)
write(<offset2>, <data2>)
...
write(offset, (byte0, byte1, …))#
Write all bytes in the second parameter to the device, in order, beginning at the specified <offset>
.
writeMasked#
Write a masked subset of the bits in an I2C register. This method performs a read-modify-write operation.
writeMasked(<offset>, <data>, <mask>)
<offset>
is the offset of the register being modified.<data>
is the value being written.<mask>
specifies the bits in the register to modify.
This method is equivalent to the following code:
oldValue = i2cReadRegister(offset)
newValue = (data & mask) | (oldValue & ~mask)
i2cWriteRegister(offset, newValue)
Sequence Methods#
Constructor#
Create a new Sequence
object.
Sequence(<name>, <max-blob-size>)
<name>
is the name of this sequence. It must be unique among all sequence names in this source file.<max-blob-size>
(optional) is the maximum number of bytes of the HSL bytecode generated for this sequence. It defaults to 0x10000.
annotate#
Insert an annotation into the HSL stream. An annotation is simply a comment; it has no effect on HSL execution (other than perhaps being logged or otherwise displayed).
annotate(<comment>)
<comment>
is a comment string.
delay#
Delay HSL execution by the specified number of microseconds.
delay(<delay-in-μs>)
<delay-in-μs>
is the number of microseconds to delay.