Building a Hub

November 17, 2018

Building a Hub

At the center of the P2 is the Hub - it’s responsible for the initial booting, a whole bunch of signal routing, starting and stopping Cogs, controlling shared memory, etc etc…

P2 Overview

In order to implement the Hub, let’s set ourselves some basic requirements to meet for today:

Implementing a minimal Hub

Much like building the minimal Cog, we can build the Hub in an almost identical way using a GenServer:

defmodule P2Dasm.Hub.Worker do
  use GenServer

  def start_link(eepromfilename) do
    GenServer.start(__MODULE__, %{eepromfilename: eepromfilename})
  end

  def init(state = %{eepromfilename: eepromfilename}) do
    {eepromcontents, eepromlength} = read_eeprom(eepromfilename)
    IO.inspect({eepromcontents, eepromlength})

    {:ok, state}
  end

  def read_eeprom(eepromfilename) do
    {:ok, filepid} = File.open(eepromfilename, [:binary, :read])

    eepromcontents =
    IO.binread(filepid, :all)
    |> :binary.bin_to_list

    eepromlength =
    Enum.count(eepromcontents)

    %{eepromcontents: eepromcontents, eepromlength: eepromlength}
  end
end


Populating the first 1024 bytes of Hub RAM

The simplest way to acheive this would seem to be:

def defineHubRAM({eepromcontents, eepromlength}) when eepromlength > 1024 do 
  defineHubRAM({Enum.take(eepromcontents,1024),1024})
end
def defineHubRAM({eepromcontents, eepromlength}) do 
  eepromcontents ++ List.duplicate(0, (512*1024)-eepromlength)
end

So… let’s add this to what we have thus far and add hub memory to our state!

defmodule P2Dasm.Hub.Worker do
  use GenServer

  def start_link(eepromfilename) do
    GenServer.start(__MODULE__, %{eepromfilename: eepromfilename})
  end

  def init(state = %{eepromfilename: eepromfilename}) do
    hubdata = read_eeprom(eepromfilename)
              |> defineHubRAM()

    newstate = Map.put(state, :hubram, hubdata)
    {:ok, newstate}
  end

  def read_eeprom(eepromfilename) do
    {:ok, filepid} = File.open(eepromfilename, [:binary, :read])

    eepromcontents =
    IO.binread(filepid, :all)
    |> :binary.bin_to_list

    eepromlength =
    Enum.count(eepromcontents)

    {eepromcontents, eepromlength}
  end

  def defineHubRAM({eepromcontents, eepromlength}) when eepromlength > 1024 do 
    defineHubRAM({Enum.take(eepromcontents,1024),1024})
  end
  def defineHubRAM({eepromcontents, eepromlength}) do 
    eepromcontents ++ List.duplicate(0, (512*1024)-eepromlength)
  end

end

Let’s compile up some P2 ASM to test this with:

dat
        orgh    0
        org

start   nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        jmp #start
[nix-shell:~/projects/p2gcc]$ fastspin -2 -o nopalooza.eeprom -e nopalooza.spin2
Propeller Spin/PASM Compiler 'FastSpin' (c) 2011-2018 Total Spectrum Software Inc.
Version 3.9.9-beta- Compiled on: Jan  1 1970
nopalooza.spin2
nopalooza.p2asm
Done.
Program size is 64 bytes

[nix-shell:~/projects/p2gcc]$ od -x nopalooza.eeprom
0000000 0000 0000 0000 0000 0000 0000 0000 0000
*
0000040 ffdc fd9f 0000 0000 0000 0000 0000 0000
0000060 0000 0000 0000 0000 0000 0000 0000 0000
0000100

[nix-shell:~/projects/p2gcc]$ ./bin/p2dump -hub 400 -dis -data nopalooza.eeprom 0000 00000000              nop
0004 00000000              nop
0008 00000000              nop
000c 00000000              nop
0010 00000000              nop
0014 00000000              nop
0018 00000000              nop
001c 00000000              nop
0020 fd9fffdc              jmp     #$-8

fastspin is an assembler for the P2. od is a UNIX command that shows us binary content. p2dump is a disassembler for the P2.

The disassembler produces assembly code that fairly represents the assembly we fed the assembler. One should always verify your tooling :-)

Two things should jump out at us before we start:

Well, no.

You can see that the output of the disassembler is showing that every instruction increments the counter by 4 bytes (ie, one long) If you remember from a few posts ago, we reference the HUB using bytes, and COG memory in Longs. (Confusing, I know)

Remember that this code is going to execute in the COG, from COG memory so that means that the JMP reference needs to be in Longs, not in bytes. So, -0x20 / 4 is, -8 as expected.

Let’s test what we have so far!

[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]

Compiling 1 file (.ex)
Interactive Elixir (1.7.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> {:ok, pid} = P2Dasm.Hub.Worker.start_link("nopalooza.eeprom")
{:ok, #PID<0.118.0>}
iex(2)> :sys.get_state(pid)
%{
  eepromfilename: "nopalooza.eeprom",
  hubram: [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, 220, 255, 159, 253, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
   0, 0, ...]
}
iex(3)> :sys.get_state(pid).hubram
[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, 220, 255, 159, 253, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
 ...]
iex(4)> :sys.get_state(pid).hubram |> Enum.count
524288
iex(5)> :sys.get_state(pid).hubram |> Enum.count |> div(1024)
512

… showing that we now have 512k worth of state in our Hub process.

Copy those 256 longs into COG memory

Before we can get 256 longs into COG memory, we have to have a COG to insert into. Before we do that, let’s remind ourselves how we start a cog with our current interface:

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

As of right now, the only argument we pass is an identifier. In theory we could leave it like this and then create a call that would copy the data into COG RAM, but the danger in this is that the cog may recieve a clock-tick before receiving the data to populate RAM - a race condition which could cause us headaches.

Given this, let’s change P2Dasm.Cog.Worker to receive an identifier AND 256 longs to populate the memory. We just need to modify the start_link and init functions as follows:

defmodule P2Dasm.Cog.Worker do
  use GenServer

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

  def init(state) do
    newstate =
    state
    |> Map.put(:lut, genmem())
    |> Map.put(:pc, 0)

    {:ok, newstate}
  end

Now we can get the Hub to spawn its first cog. We should also store the pid of the cog too for future reference. Modify the init function as follows:

  def init(state = %{eepromfilename: eepromfilename}) do
    hubdata = read_eeprom(eepromfilename)
              |> defineHubRAM()

    copytocog = Enum.take(hubdata, 1024) # 512longs
                |> :erlang.list_to_binary

    {ok, cog0pid} = P2Dasm.Cog.Worker.start_link(:cog0, copytocog)
    cogs = %{"0": cog0pid}

    newstate =  state
             |> Map.put(:hubram, hubdata)
             |> Map.put(:cogs, cogs)

    {:ok, newstate}
  end

We should now be able to test it, “soup-to-nuts” as it were:

[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]
Compiling 1 file (.ex)

Interactive Elixir (1.7.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> {:ok, pid} = P2Dasm.Hub.Worker.start_link("nopalooza.eeprom")
{:ok, #PID<0.118.0>}
iex(2)> :sys.get_state(pid)
%{
  cogs: %{"0": #PID<0.120.0>},
  eepromfilename: "nopalooza.eeprom",
  hubram: [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, 220, 255, 159, 253, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
   0, ...]
}
iex(3)> cogpid = :sys.get_state(pid).cogs."0"
#PID<0.120.0>
iex(4)> :sys.get_state(cogpid)
%{
  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, 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, 220, 255, 159, 253, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
   ...>>
}

Confirmed! Our firmware is now in the cog image… so we should now we able to execute it!

iex(5)> send(cogpid, :tick)
:tick
pc: 0 -> NOP
iex(6)> send(cogpid, :tick)
pc: 1 -> NOP
:tick
iex(7)> send(cogpid, :tick)
pc: 2 -> NOP
:tick
iex(8)> send(cogpid, :tick)
pc: 3 -> NOP
:tick
iex(9)> send(cogpid, :tick)
pc: 4 -> NOP
:tick
iex(10)> send(cogpid, :tick)
pc: 5 -> NOP
:tick
iex(11)> send(cogpid, :tick)
pc: 6 -> NOP
:tick
iex(12)> send(cogpid, :tick)
pc: 7 -> NOP
:tick
iex(13)> send(cogpid, :tick)
:tick
iex(14)>
20:12:02.168 [error] GenServer :cog0 terminating
** (FunctionClauseError) no function clause matching in P2Dasm.Sandbox.decode_instr/2
    (p2_dasm) lib/p2_dasm/sandbox.ex:2: P2Dasm.Sandbox.decode_instr(&P2Dasm.Sandbox.dis_instr/1, <<220, 255, 159, 253>>)
    (p2_dasm) lib/p2_dasm/cog/worker.ex:38: P2Dasm.Cog.Worker.fetch_execute/1
    (p2_dasm) lib/p2_dasm/cog/worker.ex:32: 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
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: 8, reg: <<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, 220, 255, 159, 253, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...>>}

BOOM! Of course our cog crashes because we’ve not implemented the JMP instruction yet.

Let’s do that next time, enjoy!