7.4 KiB

Open PicoBlaze Assembler Unit Test package

This python 3 package makes it easy for you to write PSM(4) Assembly - even test-first! Simply write a unit test for it in python, extending from OpbTestCase. It's easily integratable into your CI environment by leveraging the output of python -m unittest.

Example test case:

from opbtest import OpbTestCase

class MyTestCase(OpbTestCase):
    def test_add_registers_counts_reg1_and_reg2_into_reg3(self):
        assert_that = self.load_file("functions.psm4")\
            .setregs({"s0": 1, "s1": 2})\

Given the following Assembly:

jump main
proc add_registers(s0 is one, s1 is two, s5 is result) {
  load result, 0
    add result, 1
    sub one, 1
    jump NZ, loop1
    add result, 1
    sub two, 1
    jump NZ, loop2
  ; this should not be executed
  load s5, 42
  load sD, FF

That's it! Load your Assembly file, apply setup (mocks, inputs), and verify expectations.

opbtest relies on opbasm and opbsim to compile and evaluate your code. Both executables should be in your $PATH. The simulator will behave different compared to the actual hardware, so be aware that a set of green tests will not guarantee your code to work when deployed.


Use pip: pip install opbtest


  1. Python 3.x. Tested and written in Python 3.6
  2. The command opbsam should be installed. (See above)
  3. The command opbsim should be installed. (See above)

API Documentation

Start by subclassing from OpbTestCase instead of unittest's usual TestCase (you can still use all methods defined here, it's subclassed too.) Your IDE should recognize methods starting with test_ as a unit test. For more information, consult the Python 3 unittest framework documentation.

Loading your Assembly

Base class: OpbTestCase

The following methods are possible to bootstrap your Assembly:

  • use load_file(filename). This does not evaluate yet, until you call execute(), which returns an OpbTestAssertions instance.
  • use execute_file(filename). This immediately evaluates by reading the file, returning an OpbTestAssertions instance.
  • use execute_psm(asmstring). This immediately evaluates the given string, returning an OpbTestAssertions instance.

For example, inline Assembly:

    def test_basic_output_assertion(self):
        psm = """load s2, 5
                 output s2, 1
                 output sD, FF"""

        assert_that = self.execute_psm(psm)
        assert_that.ports(["1", "FF"]).contains([5, 0])


Base class: OpbTestAssertions

The following can be asserted:

  • reg(), regs(), rega(), regsa(), regb(), regsb(). Registers. .reg("s0").contains(1). Max 2x16. No bank specified defaults to bank "a".
  • port() and ports(). Output Ports. .port(0).contains(1). Max 16x16
  • scratchpad() and scratchpads(). Scratchpad. .scratchpad(0).contains(1) Max 4x16

Everything can be chained together, or be asserted in one line:

    def test_basic_scratchpad_assertion(self):
        psm = """load s0, 1
                 store s0, (s0)
                 load s0, 2
                 store s0, (s0)
                 load s0, 3
                 store s0, (s0)
                 output sD, FF"""

        assert_that = self.execute_psm(psm)
        assert_that.scratchpads(["0", "1", "2", "3"]).contains([0, 1, 2, 3])

Notice the difference: scratchpads() will accept an array, and contains() will accept the same length in values. You can set the scratchpad/output port/... index as an int (0, 1, ...) or as a str in hex values ("0", "1F", "FF", ...)

Assertion output: if for instance the output ports do not match, the following message will appear in your test output window:

AssertionError: Output dos not contain expected values: output ports_out 3 should contain 0 (hex: 00) but instead contains 12 (hex: 0C)

Since it's possible to assert hex values (as a str) or number values (as an int), the test output message always shows both.

It is possible to write assert_that.ports([1]).contains(["0A"]) and assert_that.ports([1]).contains([10]). Both assert the same thing and contain the same expectation values.

Testing only one procedure

If you don't want the whole thing to be executed, you can still use opbtest, and call testproc(name):

    def test_proc2_testproc_does_not_execute_rest_of_psm(self):
        assert_that = self.load_file("functions.psm4").testproc("proc2").execute()
        assert_that.regs(["s2", "s4"]).contains([0, 42])

given the following contents of functions.psm4:

use_stack(sF, 0x3F)

jump main

proc proc1(s0 is result := 1) {
    load result, 42
    call proc2

proc proc2(s4 is test) {
    load test, 42

    load s2, 11
    add s2, 1

    output sD, FF

Only testing proc1, and nothing more, is usually tricky in Assembly because of the jump statement. opbtest will inject code into your Assembly to jump to the procedure to test, execute that, and jump to an exit label. That ensures no other state will be modified.


If you explicitly do not want a certain procedure to be called, you can do so by calling mockproc(procname). This will replace all call procname statements with dummy statements, hence never actually executing the procedure.

You can replace your own statements with replace(statement_to_replace, statement_to_replace_with).

Injecting register values

Before calling execute(), you can preload register values using setreg(). For instance, .setregs({"s5": 2, "s6": 3}) will preload register s5 with value 2 and register s6 with value 3. Psm statements like output s5, 0 will load 2 into output port 0, because register s5 is preloaded.

Injecting input values

Before calling execute(), you can preload input port values using mockinput(). For instance, .mockinput(0, 4) will preload input port 0 with value 4. Psm statements like input s0, 0 will load 4 into register s4. opbtest acutally replaces the statement with load s0, 4, so no actual input statements will be processed.

Debugging Tests

Sometimes, a failing test does not clearly indicate the underlying problem. The Assembly source file is not the same source code as the code that is simulated to get to this result. For example, when mocking, parts are replaced, and when registers are setup, extra loads are done. If you want to take a look at the Assembly file to be compiled and interpreted, add the following to your setUp testcase:

    def setUp(self):

This will leave the generated Assembly files in the test directory after test execution:

  1. tmp_[timestamp].psm4
  2. tmp_[timestamp].gen.psm - PicoBlaze macro expanded instructionset
  3. tmp_[timestamp].fmt, .log metafiles
  4. tmp_[timestamp].mem - binary.

Run the assembler yourself:

  1. compiling: opbasm --6 -c file.psm4 (or --3 for PicoBlaze 3)
  2. simulating: opbsim -v -m:file.mem --pb6 (or --pb3 for PicoBlaze 3)

For more information about the commandline flags, see the Open PicoBlaze Assembler documentation.