Finding a Pulse

November 17, 2018

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:

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:

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.