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…
In order to implement the Hub, let’s set ourselves some basic requirements to meet for today:
- It will allow reading and writing of the shared memory.
- It will load an eeprom image:
- Load the first 1024bytes (256 longs) into Hub Memory
- Copy those 256 longs into COG memory
- Start Cog0
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:
- Measure the length of the eeprom image.
- If > 1024, extract the first 1024 bytes and call again.
def defineHubRAM({eepromcontents, eepromlength}) when eepromlength > 1024 do
defineHubRAM({Enum.take(eepromcontents,1024),1024})
end
- If <= 1024, null-pad all the way to (512*1024)
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:
- The jmp #$-8 means “Decrement the PC by 8”.
- According to our dump, shouldn’t that be “Decrement by 0x20”?
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!