Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Bringing a 45 year old CPU to life with Elixir and Phoenix LiveView - ElixirConf 2021

Bringing a 45 year old CPU to life with Elixir and Phoenix LiveView - ElixirConf 2021

Do you know how a CPU works? How does the CPU know when to add two numbers or grab something from a memory location? What exactly are all those 1s and 0s doing?

In this presentation, I’ll break down how I learned about assembly language programming, machine code, digital electronics, and the exciting early days of computing by writing a 6502 CPU emulator in Elixir. By utilizing Elixir’s amazing pattern matching abilities and memory-safe operations, we can watch a virtual 6502 CPU step through each instruction it encounters. Furthermore, with the magic of Phoenix LiveView, we can take it a step further by displaying the entire memory stack and disassembler in a web browser – including interacting with virtual hardware!

The 6502 CPU was first introduced in 1975 and eventually made its way into many very popular and influential early computers such as the Apple II, Commodore 64, Atari 2600 (and other families), BBC Micro, and even the Nintendo Entertainment System. With a history that strong and old, you may think that its best days are behind it. However, according to the manufacturer, hundreds of millions of 6502 CPUs are produced and shipped every year. It has a relatively small but powerful instruction set, making it a great beginner’s introduction into bare-bones and/or retro computing.

-----------------

Geoffrey Lessel has been learning Elixir since 2014 and has spoken at multiple conferences over the years, including 2 previous ElixirConf’s and a Lonestar ElixirConf. He is the author of Phoenix in Action and loves experimenting with new and creative ways to use Elixir.

Geoffrey Lessel

October 13, 2021
Tweet

More Decks by Geoffrey Lessel

Other Decks in Technology

Transcript

  1. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel
  2. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 RESULTS
  3. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 Uhhhhh… why?
  4. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 6502 OVERVIEW
  5. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 • 8-bit processor • Introduced in 1975 • 1/6th the price of the major competing processors • Used in many popular computing platforms in the 1980s 6502 OVERVIEW
  6. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021
  7. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021
  8. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021
  9. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021
  10. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021
  11. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 • 8-bit processor • Introduced in 1975 • 1/6th the price of the major competing processors • Used in many popular computing platforms in the 1980s • Still produced in the 100s of millions of units per year • Estimated total volume of 5-10 billion units • Still a great hobby CPU 6502 OVERVIEW
  12. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 ARCHITECTURE
  13. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 • A — 8-bit accumulator register • X — 8-bit index register • Y — 8-bit index register • P — 8-bit status register containing 7 1-bit status fl ags • PC — 16-bit program counter ARCHITECTURE – REGISTERS
  14. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 • 256-byte stack hardwired to memory addresses 
 0x0100—0x01FF • Able to access 65,536 locations • Memory allocation (ROM, RAM, I/O, etc.) left up the computer designer - Exceptions: 
 0x0100—0x01FF for memory stack 
 0xFFFA—0xFFFF for interrupt and reset vectors ARCHITECTURE – STACK & MEMORY
  15. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 MEMORY MAPS
  16. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 • The same CPU can be used in vastly di ff erent computers because of memory maps • Speci fi es the split of RAM, ROM, I/O, etc. MEMORY MAPS
  17. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 Apple IIe
  18. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 Apple IIe
  19. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 TALK LIKE A COMPUTER
  20. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 Binary 
 0 1
  21. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 Hexadecimal 
 0 1 2 3 4 5 6 7
 8 9 A B C D E F
  22. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 13 8 4 2 1 1 1 0 1 0xD
  23. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 180 128 64 32 16 8 4 2 1 1 0 1 1 0 1 0 0 0xB4
  24. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 62,954 3276816384 8192 4096 2048 1024 512 256 128 64 32 16 8 4 2 1 1 1 1 1 0 1 0 1 1 1 1 0 1 0 1 0 0xF5EA
  25. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 ASSEMBLY
  26. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 LOAD 241 INTO THE ACCUMULATOR LDA #241 0xA9 0xF1 10101001 11110001
  27. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021
  28. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 TO THE CODE!
  29. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 • Handles the 6502 unit • Contains registers, program count, and processor fl ags Ex6502.CPU @reset_vector 0xFFFC @flags %{c: 0, z: 1, i: 2, d: 3, b: 4, v: 6, n: 7} defstruct a: 0, x: 0, y: 0, sp: 0xFF, p: 0, pc: @reset_vector
  30. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 • Contains internals 
 beyond the 6502 itself - Memory - Buses - Connections - Running state Ex6502.Computer defstruct break: false, cpu: %Ex6502.CPU{}, cycles: 0, memory: [], address_bus: 0xFFFF, data_bus: 0xFF, running: false, io_subscribers: MapSet.new()
  31. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 Ex6502.Computer def init(opts \\ []) do memory = opts[:memory] || with {:ok, memory} <- Memory.init(0xFFFF) do memory end %Computer{cpu: CPU.init(), memory: memory} |> reset() end
  32. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 • Fetch the next instruction • Decode the instruction • Execute the instruction Ex6502.Computer – The Cycles def step(%Computer{} = c) do c |> Map.put(:break, false) |> put_pc_on_address_bus() |> handle_interrupt_location() |> update_data_bus_from_address_bus() |> step_pc() |> CPU.execute_instruction() end
  33. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 Ex6502.Computer – Fetch defp handle_interrupt_location(%Computer{cpu: %{pc: pc}} = c) when pc in [0xFFFC, 0xFFFE] do new_pc = resolve_address(c.memory, pc) c |> Map.put(:break, true) |> Map.put(:cpu, %{c.cpu | pc: new_pc}) end defp handle_interrupt_location(%Computer{} = c), do: c
  34. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 Ex6502.Computer – Fetch defp update_data_bus_from_address_bus(%Computer{address_bus: address, memory: memory} = c) do Map.put(c, :data_bus, Memory.get(memory, address)) end
  35. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 Ex6502.Computer – Fetch def step(%Computer{} = c) do c |> Map.put(:break, false) |> put_pc_on_address_bus() |> handle_interrupt_location() |> update_data_bus_from_address_bus() |> step_pc() |> CPU.execute_instruction() end
  36. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 Ex6502.CPU – Decode & Execute def execute_instruction(%Computer{break: true} = c), do: c def execute_instruction(%Computer{} = c) do Ex6502.CPU.Executor.execute(c) end
  37. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 Ex6502.CPU.Executor – Decode & Execute @andcodes [0x29, 0x2D, 0x3D, 0x39, 0x25, 0x35, 0x32, 0x21, 0x31] def execute(%Computer{data_bus: opcode} = c) when opcode in @andcodes, do: Executor.AND.execute(c)
  38. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 Ex6502.CPU.Executor.AND – Decode & Execute @moduledoc """ Transfer to the adder and perform a bit-by-bit AND operation ## Operation A ^ M -> A ## Table AND | AND Memory with Accumulator ================================================ A AND M -> A N V - B D I Z C + + addressing assembler opc bytes cycles ------------------------------------------------ immediate AND #oper 29 2 2 zeropage AND oper 25 2 3 zeropage,X AND oper,X 35 2 4 absolute AND oper 2D 3 4 absolute,X AND oper,X 3D 3 4* absolute,Y AND oper,Y 39 3 4* (indirect,X) AND (oper,X) 21 2 6 (indirect),Y AND (oper),Y 31 2 5* ## Flags - Zero: 1 if result is zero; 0 otherwise - Negative: 1 if bit 7 of result is 1; 0 otherwise
  39. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 Ex6502.CPU.Executor.AND.execute/1 # AND #$nn Immediate $29 def execute(%Computer{data_bus: 0x29} = c) do with %Computer{data_bus: value} = c <- Computer.put_next_byte_on_data_bus(c), result <- value &&& c.cpu.a do c |> CPU.set(:a, result) |> CPU.set_flags([:n, :z], :a) end end
  40. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 Ex6502.Computer.put_next_byte_on_data_bus/1 def put_next_byte_on_data_bus(%Computer{cpu: cpu, memory: memory} = c) do c |> Map.put(:data_bus, Memory.get(memory, cpu.pc)) |> step_pc() end
  41. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 Ex6502.CPU.Executor.AND.execute/1 # AND $(nn),Y zero page indirect y-indexed $31 def execute(%Computer{data_bus: 0x31} = c) do with c <- Computer.put_zero_page_on_address_bus(c), %Computer{data_bus: value} <- Memory.indirect(c, c.cpu.y), result <- value &&& c.cpu.a do c |> CPU.set(:a, result) |> CPU.set_flags([:n, :z], :a) end end
  42. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 Ex6502.Computer.put_zero_page_on_address_bus/1 def put_zero_page_on_address_bus(%Computer{} = c, index \\ 0) do c |> Map.put(:address_bus, Memory.get(c.memory, c.cpu.pc) + index) |> step_pc() end
  43. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 Ex6502.Memory def indirect(%Computer{} = c, index \\ 0) do address = c.memory |> Computer.resolve_address(c.address_bus) c |> Map.put(:address_bus, address + index) |> absolute() end def absolute(%Computer{address_bus: location, memory: memory} = c, index \\ 0) do Map.put(c, :data_bus, get(memory, location + index)) end
  44. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 ComputerServer
  45. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 web6502
  46. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 • Build a more complete computer • Monitor what is happening with memory • Practice assembly language programming • Emulate hardware input/output • See it all live with Phoenix LiveView! WEB6502
  47. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 Web6502Web.PageLive def mount(_params, _session, socket) do {:ok, c} = Computer.init() |> Computer.load_file(”./examples/elixirconf”) |> Computer.reset() |> ComputerServer.start_link() ComputerServer.subscribe(c, self()) ComputerServer.io_subscribe(c, self(), 0xFFF0..0xFFF0) computer = ComputerServer.get_computer(c) {:ok, assign(socket, computer: computer,
  48. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 Web6502Web.PageLive @input_addr 0xFFF1 def handle_event("handle-input", %{"key" => key}, socket) when key in @ascii_list do key_value = :binary.decode_unsigned(key) ComputerServer.send_io(socket.assigns.computer_pid, @input_addr, key_value) {:noreply, socket} end
  49. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 Web6502Web.PageLive @output_addr 0xFFF0 def handle_info({:io_change, @output_addr, value}, socket) do <<char::binary>> = <<value>> {:noreply, assign(socket, computer_out: socket.assigns.computer_out <> char)} end
  50. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 DEMO
  51. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 • Make it run at a correct, de fi ned speed (e.g. 1MHz) • Cycle-correct execution (i.e. each cycle takes a distinct amount of time and each instruction takes a speci fi c number of cycles • More full computer system examples (Apple II? Commodore 64? NES?) TODO
  52. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 • pagetable.com/c64ref/6502 • masswerk.at/6502/6502_instruction_set.html • archive.org (search for 6502) • Ben Eater: www.youtube.com/playlist? list=PLowKtXNTBypGqImE405J2565dvjafglHU REFERENCES
  53. Bringing a 45-year old CPU to life with Elixir and

    LiveView Geo ff rey Lessel • @geolessel • ElixirConf 2021 PhoenixInAction.com 
 coupon code: ctwelixir21 Twitter/Github: @geolessel github.com/geolessel/ex6502 Slack/Elixir Forum: @geo