Emulating Debugging

Emulating Debugging

In the emulator we have a whole host of tools to do introspection of our system’s state. In real silicon we don’t have those same methods available. If we’re going to build something that works identically to the silicon then it makes sense to try and implement the opcodes that support that first.

So, today we’ll start to implement a very minimalistic implementation of serial in/out smartpins that we can use to interface to our debugging shim.

What is a smartpin anyways?

Historically, the original Parallax Propeller (P1) has been known for its approach of not having dedicated peripherals. Instead, you would use cogs to bit-bang protocols and such. This meant that when it came time to choose “which propeller” to use for a project you could always just choose the 1 propeller IC instead of having to browse through perhaps hundreds of different other uC to look for your specific hardware requirements (2 serial, 1 uart… etc etc…).

The P2 has perhaps turned all this on its head by making every pin a “smart-pin”, which means that you can STILL do bit-banging with GPIO like before, but every pin has the hardware available to turn into any of those dedicated hardware interfaces. So, 32 USB interfaces, 64 Serial Ports, 64 ADC (Analog/Digital Converter) etc etc…

So how do we emulate this?

Well - since we’re running in the beam we’re going to create a smartpin process and then have the cogs communicate the configuration and data to that process via messages. We’ll just implement serial in/out and start by having the data go via the network so they can be “hooked up” in much the same way that as you’d hook up two pins on a chip.

So, what opcodes are we implementing?

Here’s our test code, aka: “smartpin_serial_turnaround.spin2”

' 10M baud 8-bit serial demo

dat             org

                hubset  #$FF            'set clock to 80MHz

                wrpin   pm_tx,  #0      'set asynchronous tx mode in smart pin 0
                wxpin   bitper, #0      'set tx bit period

                wrpin   pm_rx,  #1      'set asynchronous rx mode in smart pin 1
                wxpin   bitper, #1      'set rx bit period

                mov     dira,##$FF03    'enable pins 15..8 and smart pins 1..0

                wypin   #0,#0           'send initial byte to tx pin

.loop   '       waitx   #200            'uncomment for delay between bytes

.full           testp   #0      wc      'wait for buffer empty on tx pin
        if_nc   jmp     #.full

                wypin   x,#0            'send next byte to tx pin

                incmod  x,#$FF          'increment byte

.recv           testp   #1      wc      'wait for smart pin 1 to signal rx data received
        if_nc   jmp     #.recv

                rdpin   y,#1            'get data from rx pin
                shr     y,#32-8

                setbyte outa,y,#1       'write rx data to to pins 15..8

                outnot  #1              'make scope trigger on pin 1 (since smart pin 1 is
                outnot  #1              '...reading pin 0, pin 1 is still usable for normal output)

                jmp     #.loop          'loop


pm_tx           long    %0000_0000_000_0000000000000_01_11110_0 'async tx mode, output enabled for smart output
pm_rx           long    %0111_0000_000_0000000000000_01_11111_0 'async rx mode, output enabled for normal output, inputs pin 0

bitper          long    8<<16 + 7       'number of clocks per bit period, 3..65536, 8-bit words
x               long    0
y               long    0

Compile!

[red@apophenia:~/projects/p2gcc/p2asm_src/verify]$  fastspin -2 -e -o smartpin_serial_turnaround.eeprom  smartpin_serial_turnaround.spin2
Propeller Spin/PASM Compiler 'FastSpin' (c) 2011-2018 Total Spectrum Software Inc.
Version 3.9.9-beta- Compiled on: Jan  1 1970
smartpin_serial_turnaround.spin2
smartpin_serial_turnaround.p2asm
Done.
Program size is 128 bytes

and run!! (and fail!)

[red@evil:~/projects/p2_dasm]$ iex -S mix
Erlang/OTP 20 [erts-9.3.3.3] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:10] [hipe] [kernel-poll:false]

Interactive Elixir (1.7.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> {:ok, pid} = P2Dasm.Hub.Worker.start_link("smartpin_serial_turnaround.eeprom")
{:ok, #PID<0.147.0>}
iex(2)> P2Dasm.Hub.Worker.stepClock(pid)
:tick
iex(3)>
23:38:44.631 [error] GenServer :cog0 terminating
** (FunctionClauseError) no function clause matching in P2Dasm.Cog.decode_instr/2
    (p2_dasm) lib/p2_dasm/cog.ex:2: P2Dasm.Cog.decode_instr(&P2Dasm.Cog.Disassembler.dis_instr/1, <<253, 101, 254, 0>>)
    (p2_dasm) lib/p2_dasm/cog/worker.ex:40: P2Dasm.Cog.Worker.fetch_execute/1
    (p2_dasm) lib/p2_dasm/cog/worker.ex:33: P2Dasm.Cog.Worker.handle_info/2
    (stdlib) gen_server.erl:616: :gen_server.try_dispatch/4
    (stdlib) gen_server.erl:686: :gen_server.handle_msg/6
    (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Last message: {:tick, 0}
State: %{id: :cog0, lut: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...>>, pc: 0, reg: <<0, 254, 101, 253, 0, 40, 4, 252, 0, 44, 20, 252, 1, 42, 4, 252, 1, 44, 20, 252, 127, 0, 0, 255, 3, 245, 7, 246, 0, 0, 44, 252, 64, 0, 116, 253, 248, 255, 159, 61, 0, 46, 36, 252, 255, 46, ...>>}

(perhaps it’s time for a convenience method to help us identify unknown methods…)

Add:

require Logger

to the top of P2Dasm.Cog, and the following function at the bottom as our fall-through. It’s designed to just give us a bitmap of the illegal instruction and kill the cog/hub since any futher processing would be inconsistent with reality.

  ### Unimplemented Instruction:
  def decode_instr(function, <<con::size(4), instr::size(7), czi::size(3), source::size(9), destination::size(9)>>) do
    ExPrintf.sprintf("IllegalInstr: %04b %07b %03b %09b %09b", [con, instr, czi, source, destination])
    |> Logger.error

    {:error, :illegal_instruction}
  end

Now it looks like this:

iex(2)> P2Dasm.Hub.Worker.stepClock(pid)
:tick
iex(3)>
23:50:15.154 [error] IllegalInstr: 1111 1101011 001 011111111 000000000

From the documentation:

EEEE 1101011 00L DDDDDDDDD 000000000 HUBSET {#}D      Set hub configuration to D

Converting this pattern into a function:

##                           EEEE            1101011             00           L           DDDDDDDDD     000000000 HUBSET {#}D
def decode_instr(function, <<con::size(4), 0b1101011::size(7), 0b00::size(2), l::size(1), d::size(9), 0b000000000::size(9)>>), do: function.(%{instr: :HUBSET, con: con, l: l, d: d})

Hopefully the pattern is now fairly clear, we use the decode_instr() function to break up the bitmap into constituent parts and then kick the can to the function that’s passed. In our current example we disassemble and execute - let’s implement:

def dis_instr(%{instr: :HUBSET, l: 1, d: d}), do: "HUBSET  #$#{ExPrintf.sprintf("0x%03x", [d])}"

As far as implementing the emulation of this command, it sets the clockspeed. As we don’t have any concept of clockspeed in the emulator yet - we’re just going to NOP until we find something that HUBSET sets that we care about:

def exe_instr(%{instr: :HUBSET,  l: 1, d: d}, cogstate), do: Map.put(cogstate, :pc, cogstate.pc+1)

and test!

[red@evil:~/projects/p2_dasm]$ iex -S mix
Erlang/OTP 20 [erts-9.3.3.3] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:10] [hipe] [kernel-poll:false]

Interactive Elixir (1.7.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> {:ok, pid} = P2Dasm.Hub.Worker.start_link("smartpin_serial_turnaround.eeprom")
{:ok, #PID<0.147.0>}
iex(2)> P2Dasm.Hub.Worker.stepClock(pid)
:tick
cnt: 0: pc: 0 -> HUBSET  #$0x0ff
iex(3)> P2Dasm.Hub.Worker.stepClock(pid)

00:17:17.526 [error] IllegalInstr: 1111 1100000 001 000010100 000000000
:tick

IllegalInstr: 1111 1100000 001 000010100 000000000

(Now we’re on a roll) ;-)

EEEE 1100000 0LI DDDDDDDDD SSSSSSSSS    WRPIN {#}D,{#}S

From the documentation:

Smart pins have four 32-bin registers inside of them:

  mode - smart pin mode, as well as low-level I/O pin mode (write-only)
  X    - mode-specific parameter (write-only)
  Y    - mode-specific parameter (write-only)
  Z    - mode-specific result (read-only)

These four registers are written and read via the following 2-clock 
instructions, in which S/# is used to select the pin number (0..63)
and D/# is the 32-bit data conduit

  WRPIN  D/#, S/#    - Set smartpin S/# mode to D/#, ack pin
  WXPIN  D/#, S/#    - Set smartpin S/# parameter to D/#, ack pin
  WYPIN  D/#, S/#    - Set smartpin S/# parameter to D/#, ack pin
  RDPIN  D, S/# {WC} - Set smartpin S/# result Z into D, flag to C, ack pin
  RQPIN  D, S/# {WC} - Set smartpin S/# result Z into D, flag to C, don't ack
  AKPIN  S/#         - Acknowledge pin S/#

The Smartpin process

All of the instructions above are dependent on the mode that the smartpin is set to so let’s assume for now that we should never see any of those commands to set parameters until we see the mode set. As such, let’s assume that the WRPIN opcode to set the mode is the instruction we should take to start our smartpin process.

Where should the process live? Well - since a smartpin can be addressed by any cog once started, it makes sense for the smartpin process to by controlled by the Hub process. So, to implement the emulation - we need to have the cog issue syncronous commands to the hub to pass-through to the smartpin.

Let’s start with the decoding(!)

## EEEE 1100000 0LI DDDDDDDDD SSSSSSSSS    WRPIN {#}D,{#}S
def decode_instr(function, <<con::size(4), 0b1100000::size(7), 0b0::size(1), l::size(1), i::size(1), d::size(9), s::size(9)>>), do: function.(%{instr: :WRPIN, con: con, l: l, i: i, d: d, s: s})

… disassemble:

def dis_instr(%{instr: :WRPIN, con: con, l: 0, i: 1, d: d, s: s}), do: "WRPIN 0x#{ExPrintf.sprintf("%03x", [d])}, #$#{ExPrintf.sprintf("%03x", [s])}"

Let’s take a closer look at this output and the original source:

iex(3)>  P2Dasm.Hub.Worker.stepClock(pid)
cnt: 1: pc: 1 -> WRPIN 0x014, #$000
:tick

vs

wrpin   pm_tx,  #0      'set asynchronous tx mode in smart pin 0
<snip>
pm_tx           long    %0000_0000_000_0000000000000_01_11110_0 'async tx mode, output enabled for smart output
pm_rx           long    %0111_0000_000_0000000000000_01_11111_0 'async rx mode, output enabled for normal output, inputs pin 0

vs

0004 fc042800              wrpin   $14, #0
<snip>
0050 0000007c _ret_        ror     0, $7c
0054 7000007e if_nz_or_nc  ror     0, $7e

In the original instruction pm_tx refers to a location in cog memory (which the assembler inlined to be at address 0x14 in cog memory). This s the location in cog memory which is word aligned. The disassembler notates the addresses in bytes so we need to multiply by 4 and display in hex to get the correct location:

iex(10)> ExPrintf.printf("%04x\n", [(0x14*4)])
0050

So the real value of the parameter that will be send to configure the smartpin is actually 0x0000007c. We need to be able to get this value from cog memory so we need a function to read this memory. Let’s put that in P2Dasm.Cog.Worker for now:

def cmem(cs, addr) when (addr < 0x200), do: :binary.part(cs.reg, {(addr*4), 4})
def cmem(cs, addr), do: :binary.part(cs.lut, {(addr*4), 4})

… and let’s give ourselves some temporary instrumentation:

  def exe_instr(%{instr: :WRPIN, con: con, l: 0, i: 1, d: d, s: s}, cogstate) do
    ## D/# = %AAAA_BBBB_FFF_PPPPPPPPPPPPP_TT_MMMM_0
    <<endian::little-size(32)>> = P2Dasm.Cog.Worker.cmem(cogstate, d)
    <<a::size(4), b::size(4), f::size(3), p::size(13), t::size(2), m::size(5), 0::size(1)>> = <<endian::size(32)>>
    "AAAA_BBBB_FFF_PPPPPPPPPPPPP_TT_MMMMM_0" |> Logger.debug
    ExPrintf.sprintf("%04b %04b %03b %013b %02b %05b %01b", [a,b,f,p,t,m,0]) |> Logger.debug
    cogstate
iex(3)> P2Dasm.Hub.Worker.stepClock(pid)
cnt: 1: pc: 1 -> WRPIN 0x014, #$000
03:23:03.269 [debug] AAAA_BBBB_FFF_PPPPPPPPPPPPP_TT_MMMMM_0
03:23:03.269 [debug] 0000 0000 000 0000000000000 01 11110 0

From the documentation, each of these fields have specific meaning.

%AAAA   - 0xxx = true
%BBBB   - 0xxx = true
%FFF    - 000  = A, B
%PPPPPPPPPPPPP = %0000CIOHHHLLL
               
%C      - 0       = NOT clocked IO
%I      - 0       = NOT invert IN output
%O      - 0       = NOT invert OUT input
%HHH    - 000     = drive high, other (float when driven high)
%LLL    - 000     = drive low, other (float when driven low)

%TT     - 01      = OUT drives output
%MMMMM  - 11110 * = async serial transmit (baudrate)
* OUT signal overridden

Now we just need to notify the hub process to start our smartpin process. But how do we find our hub process?

Who’s your daddy?

We need to change the function that starts the cog so it stores who its parent hub is.

Modify start_link in P2Dasm.Cog.Worker as follows:

  def start_link(cogid, cogmem) do
    GenServer.start_link(__MODULE__, %{id: cogid, reg: cogmem, hubpid: self()}, name: cogid)
  end

NB: Remember that the start_link function is run IN the Hub process… it’s the init runs in the cog. Therefore the self() function returns the HUB pid.

Now that’s stored we can make the call to the Hub process. NOTE - we’re going to use a call instead of cast as we need to be sure that the smartpin process is created before we start to send it settings:

{:ok, smartpinpid} = GenServer.call(cogstate.hubpid, {:smartpinstart, [a,b,f,p,t,m,0], s})
Logger.debug("Smartpin PID for pin #{s} -> #{inspect(smartpinpid)}")

… inside the Hub Worker:

def handle_call({:smartpinstart, [a,b,f,p,1,30,0], s}, _from, state) do
  {:ok, pid} = P2Dasm.Smartpin.AsyncSerialTx.start_link({:smartpinstart, [a,b,f,p,1,30,0], s})
  {:reply, {:ok, pid}, state}
  end

… and our template Smartpin.AsyncSerialTx:

defmodule P2Dasm.Smartpin.AsyncSerialTx do
  use GenServer

  def start_link({:smartpinstart, [a,b,f,p,1,30,0], s}) do
    GenServer.start_link(__MODULE__, %{pin: s})
  end

  def init(state) do
    {:ok, state}
  end
end

Final thought of the day

As you may have noticed I’m starting to paint with very much broader strokes within these posts. The reality is that with 300-400 instructions to build it’s a massive undertaking to attempt to document every line in written form. If you have questions - feel free to reach other either via twitter (@noidd) or on the parallax forums: http://forums.parallax.com/discussion/169145/de-lurk-my-p2-project-plans

This commit can be found at reference:

commit 139161877388b954417b66e401c437823d8add83
Author: Redvers Davies <red@infect.me>
Date:   Thu Nov 22 04:09:57 2018 +0000

    Added WRPIN & initial Smartpin for Async Serial TX