Finding a Pulse
Microcontrollers are driven by a clock signal, we saw simulated that earlier by send()ing our cog a :tick message. Here’s our list of requirements for today’s post:
- The Hub contains a counter called CNT. It increments by one every clock tick.
- Have the HUB generate the :tick and send the value of CNT with it so that all cogs have the same value of CNT at the same time.
- Implement our compiled JMP instruction.
Implement CNT, the counter
In the init function, we add a field in our state for the counter, cnt. Adding a single line to the newstate expression to set it to zero, like so:
newstate = state
|> Map.put(:hubram, hubdata)
|> Map.put(:cogs, cogs)
|> Map.put(:cnt, 0)
{:ok, newstate}
Getting the Hub to tick(!)
Next, we need to have the hub generate a tick message to send to all running cogs. Before we do that, let’s think about the API.
On the front of many microcomputers, they have the ability to stop and step the system clock. Let’s simulate that with the following simple API:
P2Dasm.Hub.Worker.stepClock(pid)
P2Dasm.Hub.Worker.startClock(pid)
P2Dasm.Hub.Worker.stopClock(pid)
P2Dasm.Hub.Worker.stepClock(pid)
Stepping the clock is the most fundamental function:
- Send a tick to every running Cog with the current value of the counter.
- Increment the Counter
Rememeber though that when you execute the stepClock function that’s likely going to be running in your shell process. As such, you need to make that function send a message to the Hub process to execute our tick function. This means we need to write two functions. One for the API, the other that actually runs inside the Hub and does the real work.
Note - those experiences with OTP may wonder why I’m using send() here instead of a cast or call, the reason will become apparent shortly.
## The API call
def stepClock(pid) do
send(pid, :tick)
end
## Runs inside the Hub.Worker process:
def handle_info(:tick, state = %{cogs: cogs, cnt: cnt}) do
Map.values(cogs) # All the Cog pids
|> Enum.map(&(send(&1, {:tick, cnt}))) # Send tick to all
newstate = Map.put(state, :cnt, cnt+1)
{:ok, newstate}
end
You’ll note that I’ve also changed the format of the incoming :tick message from what the cog process expects, let’s fix that now:
def handle_info(:tick, state), do: {:noreply, fetch_execute(state)}
becomes:
def handle_info({:tick, cnt}, state) do
newstate = Map.put(state, :cnt, cnt)
{:noreply, fetch_execute(newstate)}
end
while we’re at it, let’s add the value of CNT to our debug output for fun.
IO.puts("pc: #{state.pc} -> #{textinstr}")
becomes:
IO.puts("cnt: #{state.cnt}: pc: #{state.pc} -> #{textinstr}")
Let’s test it!
[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)>
nil
iex(2)> {:ok, pid} = P2Dasm.Hub.Worker.start_link("nopalooza.eeprom")
{:ok, #PID<0.119.0>}
iex(3)> P2Dasm.Hub.Worker.stepClock(pid)
:tick
cnt: 0: pc: 0 -> NOP
iex(4)> P2Dasm.Hub.Worker.stepClock(pid)
cnt: 1: pc: 1 -> NOP
:tick
iex(5)> P2Dasm.Hub.Worker.stepClock(pid)
:tick
cnt: 2: pc: 2 -> NOP
iex(6)> P2Dasm.Hub.Worker.stepClock(pid)
cnt: 3: pc: 3 -> NOP
:tick
Boogie!
P2Dasm.Hub.Worker.startClock(pid)
Assuming we don’t want to manually execute every tick in our emulator, let’s make the startClock(pid) function cause the Hub to issue a clockTick every second until the stopClock(pid) function is called.
What we don’t do
Pseudocode:
while (true) do
stepClock(pid)
end
Why? … because in doing so it becomes impossible for the hub to receive any messages. We have to find a way to do this in such a way as we always go back to the GenServer Mainloop. Of course, there’s a simple way to do it. We can set timers into the future to receive messages. The messages come in via _info, (which is why we used send / handle_info() above).
When we create this re-occuring timer erlang gives us a reference for it so we can cancel it in the future. That will come in handy when it comes to stopClock(pid)
# API Call
def startClock(pid) do
GenServer.cast(pid, :startClock)
end
## Runs inside the Hub.Worker process
def handle_cast(:startClock, state) do
# Set a re-occuring 1 second timer
{:ok, timerref} = :timer.send_interval(1000, self(), :tick)
newstate = Map.put(state, :clockTimerRef, timerref)
{:noreply, newstate}
end
Let’s test what we have thus far (it will still crash on the JMP):
[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)> P2Dasm.Hub.Worker.stepClock(pid)
:tick
cnt: 0: pc: 0 -> NOP
iex(3)> P2Dasm.Hub.Worker.startClock(pid)
:ok
cnt: 1: pc: 1 -> NOP
cnt: 2: pc: 2 -> NOP
cnt: 3: pc: 3 -> NOP
cnt: 4: pc: 4 -> NOP
cnt: 5: pc: 5 -> NOP
cnt: 6: pc: 6 -> NOP
cnt: 7: pc: 7 -> NOP
iex(4)>
00:42:04.494 [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:41: P2Dasm.Cog.Worker.fetch_execute/1
(p2_dasm) lib/p2_dasm/cog/worker.ex:34: 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, 8}
State: %{cnt: 7, 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, ...>>, 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, ...>>}
Be still my beating heart!… because our hub has one instead ;-)
P2Dasm.Hub.Worker.stop(pid)
# API Call
def stopClock(pid) do
GenServer.cast(pid, :stopClock)
end
## Runs inside the Hub.Worker process
def handle_cast(:stopClock, state = %{clockTimerRef: timerref}) do
:timer.cancel(timerref)
newstate = Map.delete(state, :clockTimerRef)
{:noreply, newstate}
end
Let’s 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]
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)> P2Dasm.Hub.Worker.startClock(pid)
:ok
cnt: 0: pc: 0 -> NOP
cnt: 1: pc: 1 -> NOP
cnt: 2: pc: 2 -> NOP
cnt: 3: pc: 3 -> NOP
cnt: 4: pc: 4 -> NOP
iex(3)> P2Dasm.Hub.Worker.stopClock(pid)
:ok
iex(4)> P2Dasm.Hub.Worker.stepClock(pid)
cnt: 5: pc: 5 -> NOP
:tick
Success! We’re able to start, stop, and step our processor.
Implement our compiled JMP instruction.
Before we do that, let’s remove the instruction decoding, decompiling, and emulation from the Sanbox module and put them in P2Dasm.Cog, P2Dasm.Cog.Disassembler, and P2Dasm.Cog.Emulator (and remove the “YIKES” instruction):
defmodule P2Dasm.Cog do
def decode_instr(function, <<0b00000000000000000000000000000000::size(32)>>),do: function.(:NOP)
end
defmodule P2Dasm.Cog.Disassembler do
def dis_instr(:NOP), do: "NOP"
end
defmodule P2Dasm.Cog.Emulator do
def exe_instr(:NOP, cogstate), do: Map.put(cogstate, :pc, cogstate.pc+1)
end
… and change all the references to the Sandbox module in the CogWorker, all in the fetch_execute function:
def fetch_execute(state) do
<<instr_32bits::size(32)>> = fetch_instruction_word(state)
# Get the textual version of ASM command and display with pc
textinstr = P2Dasm.Cog.decode_instr(&P2Dasm.Cog.Disassembler.dis_instr/1, <<instr_32bits::size(32)>>)
IO.puts("cnt: #{state.cnt}: pc: #{state.pc} -> #{textinstr}")
function = fn(instr) -> P2Dasm.Cog.Emulator.exe_instr(instr, state) end
P2Dasm.Cog.decode_instr(function, <<instr_32bits::size(32)>>)
end
So, let’s take a look at the JMP instruction that caused our cog to crash:
01:09:33.450 [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, <<220, 255, 159, 253>>)
We’re going to want to look at how that looks in binary, so let’s add a utility module to our project that replicates the functionality of (s)printf et al.
Edit the mix.exs file as follows:
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:exprintf, "~> 0.2.1"}
]
end
… and run mix deps.get
[red@evil:~/projects/p2_dasm]$ mix deps.get
Resolving Hex dependencies...
Dependency resolution completed:
exprintf 0.2.1
* Getting exprintf (Hex package)
Checking package (https://repo.hex.pm/tarballs/exprintf-0.2.1.tar)
Fetched package
Now we can do these binary translations in our shell:
[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]
==> exprintf
Compiling 1 file (.ex)
Generated exprintf app
==> p2_dasm
Compiling 7 files (.ex)
Generated p2_dasm app
Interactive Elixir (1.7.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> <<decodeme::little-size(32)>> = <<220, 255, 159, 253>>
<<220, 255, 159, 253>>
iex(2)> ExPrintf.printf("%032b\n", [decodeme])
11111101100111111111111111011100
:ok
Let’s break that up into our 4 / 7 / 3 / 9 / 9 format and find the match in the documentation
1111 1101100 111 111111111 111011100
matches:
EEEE 1101100 RAA AAAAAAAAA AAAAAAAAA JMP #A
1111 1101100 111 111111111 111011100
#A is a signed value, so let’s see what it resolves to:
iex(65)> <<value::signed-size(20)>> = <<0b11111111111111011100::size(20)>>
<<255, 253, 12::size(4)>>
iex(66)> value
-36
Why -36 instead of the -8 that we expected from our decompiler?
As we are going relative cog-to-cog, the disassembler divided the number by four and subtracted one. Why did it subtract the one? I suspect because the processor increments PC before the next instruction.
So, let’s write our match function first:
## EEEE 1101100 R AAAAAAAAAAAAAAAAAAAA (Relative JMP)
def decode_instr(function, <<con::size(4), 0b1101100::size(7), 1::size(1), a::signed-size(20)>>),do: function.(%{instr: :JMP, r: 1, a: a})
… and our disassembly function:
def dis_instr(%{instr: :JMP, r: 1, a: a}), do: "JMP #$#{div(a,4)+1}"
… and our emulator instruction:
def exe_instr(%{instr: :JMP, r: 1, a: a}, cogstate), do: Map.put(cogstate, :pc, (cogstate.pc+(div(a,4)+1)))
… there is one more change we have to do, if you look at the output of the binary hex dump and the output of the disassembler, you may notice something interesting:
0000040 ffdc fd9f 0000 0000 0000 0000 0000 0000
vs
fd9fffdc
ffdc fd9f
vs
fd9f ffdc
This issue was alluded too earlier when we were decoding the instructions. We need to convert endianness before we pass it to the decode_instr function. Why didn’t it crash before? 00000000 is the exact same value in both little and big endian.
Modify the function fetch_execute in CogWorker by adding little- to the instruction decoder:
<<instr_32bits::little-size(32)>> = fetch_instruction_word(state)>>
Now, let’s test it!
[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 3 files (.ex)
Interactive Elixir (1.7.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> {:ok, pid} = P2Dasm.Hub.Worker.start
startClock/1 start_link/1
iex(1)> {:ok, pid} = P2Dasm.Hub.Worker.start_link("nopalooza.eeprom")
{:ok, #PID<0.166.0>}
iex(2)> P2Dasm.Hub.Worker.startClock(pid)
:ok
cnt: 0: pc: 0 -> NOP
cnt: 1: pc: 1 -> NOP
cnt: 2: pc: 2 -> NOP
cnt: 3: pc: 3 -> NOP
cnt: 4: pc: 4 -> NOP
cnt: 5: pc: 5 -> NOP
cnt: 6: pc: 6 -> NOP
cnt: 7: pc: 7 -> NOP
cnt: 8: pc: 8 -> JMP #$-8
cnt: 9: pc: 0 -> NOP
cnt: 10: pc: 1 -> NOP
cnt: 11: pc: 2 -> NOP
cnt: 12: pc: 3 -> NOP
cnt: 13: pc: 4 -> NOP
cnt: 14: pc: 5 -> NOP
cnt: 15: pc: 6 -> NOP
cnt: 16: pc: 7 -> NOP
cnt: 17: pc: 8 -> JMP #$-8
cnt: 18: pc: 0 -> NOP
cnt: 19: pc: 1 -> NOP
cnt: 20: pc: 2 -> NOP
cnt: 21: pc: 3 -> NOP
cnt: 22: pc: 4 -> NOP
cnt: 23: pc: 5 -> NOP
cnt: 24: pc: 6 -> NOP
cnt: 25: pc: 7 -> NOP
cnt: 26: pc: 8 -> JMP #$-8
cnt: 27: pc: 0 -> NOP
iex(3)> P2Dasm.Hub.Worker.stopClock(pid)
:ok
Success! We now have the extremely vital nop/jmp application running in our emulator.