diff --git a/documentation.json b/documentation.json index 711f7d2..f318501 100644 --- a/documentation.json +++ b/documentation.json @@ -42,6 +42,8 @@ }, "Logic" : { "Luacontroller" : "mesecons_luacontroller/doc/luacontroller", + "FPGA" : "mesecons_fpga/doc/fpga", + "FPGA Programmer" : "mesecons_fpga/doc/programmer", "Torch" : "mesecons_torch/doc/torch", "Delayer" : "mesecons_delayer/doc/delayer", "Gates" : { diff --git a/mesecons_microcontroller/textures/jeija_microcontroller_LED_A.png b/mesecons/textures/jeija_microcontroller_LED_A.png similarity index 100% rename from mesecons_microcontroller/textures/jeija_microcontroller_LED_A.png rename to mesecons/textures/jeija_microcontroller_LED_A.png diff --git a/mesecons_microcontroller/textures/jeija_microcontroller_LED_B.png b/mesecons/textures/jeija_microcontroller_LED_B.png similarity index 100% rename from mesecons_microcontroller/textures/jeija_microcontroller_LED_B.png rename to mesecons/textures/jeija_microcontroller_LED_B.png diff --git a/mesecons_microcontroller/textures/jeija_microcontroller_LED_C.png b/mesecons/textures/jeija_microcontroller_LED_C.png similarity index 100% rename from mesecons_microcontroller/textures/jeija_microcontroller_LED_C.png rename to mesecons/textures/jeija_microcontroller_LED_C.png diff --git a/mesecons_microcontroller/textures/jeija_microcontroller_LED_D.png b/mesecons/textures/jeija_microcontroller_LED_D.png similarity index 100% rename from mesecons_microcontroller/textures/jeija_microcontroller_LED_D.png rename to mesecons/textures/jeija_microcontroller_LED_D.png diff --git a/mesecons_fpga/depends.txt b/mesecons_fpga/depends.txt new file mode 100644 index 0000000..acaa924 --- /dev/null +++ b/mesecons_fpga/depends.txt @@ -0,0 +1 @@ +mesecons diff --git a/mesecons_fpga/doc/fpga/description.html b/mesecons_fpga/doc/fpga/description.html new file mode 100644 index 0000000..be6bd65 --- /dev/null +++ b/mesecons_fpga/doc/fpga/description.html @@ -0,0 +1,5 @@ +FPGAs can be used to chain multiple logic gates together in a compact manner. +They come with 4 I/O ports and 10 internal registers, +which can then be connected with eachother to form logic circuits.
+Supported gate types: AND, OR, NOT, XOR, NAND, XNOR, Buffer (=)
+I/O ports: A B C D; Registers: numbered 0 to 9 diff --git a/mesecons_fpga/doc/fpga/preview.png b/mesecons_fpga/doc/fpga/preview.png new file mode 100644 index 0000000..c156321 Binary files /dev/null and b/mesecons_fpga/doc/fpga/preview.png differ diff --git a/mesecons_fpga/doc/fpga/recipe.png b/mesecons_fpga/doc/fpga/recipe.png new file mode 100644 index 0000000..1140bfa Binary files /dev/null and b/mesecons_fpga/doc/fpga/recipe.png differ diff --git a/mesecons_fpga/doc/programmer/description.html b/mesecons_fpga/doc/programmer/description.html new file mode 100644 index 0000000..39e2374 --- /dev/null +++ b/mesecons_fpga/doc/programmer/description.html @@ -0,0 +1,3 @@ +The FPGA programmer can be used to copy gate configurations from one FPGA to another.
+Shift+Right-Click an FPGA to read its configuration and "remember" it. +Left-click (punch) FPGAs to write the saved configuration to them. diff --git a/mesecons_fpga/doc/programmer/preview.png b/mesecons_fpga/doc/programmer/preview.png new file mode 100644 index 0000000..7437d39 Binary files /dev/null and b/mesecons_fpga/doc/programmer/preview.png differ diff --git a/mesecons_fpga/doc/programmer/recipe.png b/mesecons_fpga/doc/programmer/recipe.png new file mode 100644 index 0000000..778ec5f Binary files /dev/null and b/mesecons_fpga/doc/programmer/recipe.png differ diff --git a/mesecons_fpga/init.lua b/mesecons_fpga/init.lua new file mode 100644 index 0000000..03beafc --- /dev/null +++ b/mesecons_fpga/init.lua @@ -0,0 +1,374 @@ +local plg = {} +plg.rules = {} + +local lcore = dofile(minetest.get_modpath(minetest.get_current_modname()) .. "/logic.lua") +dofile(minetest.get_modpath(minetest.get_current_modname()) .. "/tool.lua")(plg) + + +plg.register_nodes = function(template) + -- each loop is for one of the 4 IO ports + for a = 0, 1 do + for b = 0, 1 do + for c = 0, 1 do + for d = 0, 1 do + local ndef = table.copy(template) + local nodename = "mesecons_fpga:fpga" + .. tostring(d) .. tostring(c) .. tostring(b) .. tostring(a) + + -- build top texture string + local texture = "jeija_fpga_top.png" + if a == 1 then texture = texture .. "^jeija_microcontroller_LED_A.png" end + if b == 1 then texture = texture .. "^jeija_microcontroller_LED_B.png" end + if c == 1 then texture = texture .. "^jeija_microcontroller_LED_C.png" end + if d == 1 then texture = texture .. "^jeija_microcontroller_LED_D.png" end + ndef.tiles[1] = texture + ndef.inventory_image = texture + + if (a + b + c + d) > 0 then + ndef.groups["not_in_creative_inventory"] = 1 + end + + -- interaction with mesecons (input / output) + local rules_out = {} + if a == 1 then table.insert(rules_out, {x = -1, y = 0, z = 0}) end + if b == 1 then table.insert(rules_out, {x = 0, y = 0, z = 1}) end + if c == 1 then table.insert(rules_out, {x = 1, y = 0, z = 0}) end + if d == 1 then table.insert(rules_out, {x = 0, y = 0, z = -1}) end + plg.rules[nodename] = rules_out + + local rules_in = {} + if a == 0 then table.insert(rules_in, {x = -1, y = 0, z = 0}) end + if b == 0 then table.insert(rules_in, {x = 0, y = 0, z = 1}) end + if c == 0 then table.insert(rules_in, {x = 1, y = 0, z = 0}) end + if d == 0 then table.insert(rules_in, {x = 0, y = 0, z = -1}) end + ndef.mesecons.effector.rules = rules_in + + if (a + b + c + d) > 0 then + ndef.mesecons.receptor = { + state = mesecon.state.on, + rules = rules_out, + } + end + + minetest.register_node(nodename, ndef) + end + end + end + end +end + +plg.register_nodes({ + description = "FPGA", + drawtype = "nodebox", + tiles = { + "", -- replaced later + "jeija_microcontroller_bottom.png", + "jeija_fpga_sides.png", + "jeija_fpga_sides.png", + "jeija_fpga_sides.png", + "jeija_fpga_sides.png" + }, + inventory_image = "", -- replaced later + sunlight_propagates = true, + paramtype = "light", + walkable = true, + groups = {dig_immediate = 2, mesecon = 3}, + drop = "mesecons_fpga:fpga0000", + selection_box = { + type = "fixed", + fixed = { -8/16, -8/16, -8/16, 8/16, -5/16, 8/16 }, + }, + node_box = { + type = "fixed", + fixed = { + { -8/16, -8/16, -8/16, 8/16, -7/16, 8/16 }, -- bottom slab + { -5/16, -7/16, -5/16, 5/16, -6/16, 5/16 }, -- circuit board + { -3/16, -6/16, -3/16, 3/16, -5/16, 3/16 }, -- IC + } + }, + on_construct = function(pos) + local meta = minetest.get_meta(pos) + local is = { {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {} } + + meta:set_string("instr", lcore.serialize(is)) + meta:set_int("valid", 0) + meta:set_string("formspec", plg.to_formspec_string(is)) + meta:set_string("infotext", "FPGA") + end, + on_receive_fields = function(pos, formname, fields, sender) + if fields.program == nil then return end -- we only care when the user clicks "Program" + local meta = minetest.get_meta(pos) + local is = plg.from_formspec_fields(fields) + + meta:set_string("instr", lcore.serialize(is)) + plg.update_formspec(pos, is) + end, + sounds = default.node_sound_stone_defaults(), + mesecons = { + effector = { + rules = {}, -- replaced later + action_change = function(pos, node, rule, newstate) + plg.ports_changed(pos, rule, newstate) + plg.update(pos) + end + } + }, + after_dig_node = function(pos, node) + mesecon.receptor_off(pos, plg.rules[node.name]) + end, +}) + + +plg.to_formspec_string = function(is) + local function dropdown_op(x, y, name, val) + local s = "dropdown[" .. tostring(x) .. "," .. tostring(y) .. ";" + .. "0.75,0.5;" .. name .. ";" -- the height seems to be ignored? + s = s .. " ,A,B,C,D,0,1,2,3,4,5,6,7,8,9;" + if val == nil then + s = s .. "0" -- actually selects no field at all + elseif val.type == "io" then + local mapping = { + ["A"] = 1, + ["B"] = 2, + ["C"] = 3, + ["D"] = 4, + } + s = s .. tostring(1 + mapping[val.port]) + else -- "reg" + s = s .. tostring(6 + val.n) + end + return s .. "]" + end + local function dropdown_action(x, y, name, val) + local s = "dropdown[" .. tostring(x) .. "," .. tostring(y) .. ";" + .. "1.125,0.5;" .. name .. ";" -- the height seems to be ignored? + s = s .. " , AND, OR, NOT, XOR,NAND, =,XNOR;" + if val == nil then + return s .. "0]" -- actually selects no field at all + end + local mapping = { + ["and"] = 1, + ["or"] = 2, + ["not"] = 3, + ["xor"] = 4, + ["nand"] = 5, + ["buf"] = 6, + ["xnor"] = 7, + } + return s .. tostring(1 + mapping[val]) .. "]" + end + local s = "size[9,9]".. + "label[3.4,-0.15;FPGA gate configuration]".. + "button_exit[7,7.5;2,2.5;program;Program]".. + "box[4.2,0.5;0.03,7;#ffffff]".. + "label[0.25,0.25;op. 1]".. + "label[1.0,0.25;gate type]".. + "label[2.125,0.25;op. 2]".. + "label[3.15,0.25;dest]".. + "label[4.5,0.25;op. 1]".. + "label[5.25,0.25;gate type]".. + "label[6.375,0.25;op. 2]".. + "label[7.4,0.25;dest]" + local x = 1 - 0.75 + local y = 1 - 0.25 + for i = 1, 14 do + local cur = is[i] + s = s .. dropdown_op (x , y, tostring(i).."op1", cur.op1) + s = s .. dropdown_action(x+0.75 , y, tostring(i).."act", cur.action) + s = s .. dropdown_op (x+1.875, y, tostring(i).."op2", cur.op2) + s = s .. "label[" .. tostring(x+2.625) .. "," .. tostring(y+0.1) .. "; ->]" + s = s .. dropdown_op (x+2.9 , y, tostring(i).."dst", cur.dst) + y = y + 1 + + if i == 7 then + x = 4.5 + y = 1 - 0.25 + end + end + return s +end + +plg.from_formspec_fields = function(fields) + local function read_op(s) + if s == nil or s == " " then + return nil + elseif s == "A" or s == "B" or s == "C" or s == "D" then + return {type = "io", port = s} + else + return {type = "reg", n = tonumber(s)} + end + end + local function read_action(s) + if s == nil or s == " " then + return nil + end + local mapping = { + [" AND"] = "and", + [" OR"] = "or", + [" NOT"] = "not", + [" XOR"] = "xor", + ["NAND"] = "nand", + [" ="] = "buf", + ["XNOR"] = "xnor", + } + return mapping[s] + end + local is = {} + for i = 1, 14 do + local cur = {} + cur.op1 = read_op(fields[tonumber(i) .. "op1"]) + cur.action = read_action(fields[tonumber(i) .. "act"]) + cur.op2 = read_op(fields[tonumber(i) .. "op2"]) + cur.dst = read_op(fields[tonumber(i) .. "dst"]) + is[#is + 1] = cur + end + return is +end + +plg.update_formspec = function(pos, is) + if type(is) == "string" then -- serialized string + is = lcore.deserialize(is) + end + local meta = minetest.get_meta(pos) + local form = plg.to_formspec_string(is) + + local err = lcore.validate(is) + if err == nil then + meta:set_int("valid", 1) + meta:set_string("infotext", "FPGA (functional)") + else + meta:set_int("valid", 0) + meta:set_string("infotext", "FPGA") + local fmsg = minetest.colorize("#ff0000", minetest.formspec_escape(err.msg)) + form = form .. plg.red_box_around(err.i) .. + "label[0.25,8.25;The gate configuration is erroneous in the marked area:]".. + "label[0.25,8.5;" .. fmsg .. "]" + end + + meta:set_string("formspec", form) + + -- reset ports and run programmed logic + plg.setports(pos, false, false, false, false) + plg.update(pos) +end + +plg.red_box_around = function(i) + local x, y + if i > 7 then + x = 4.5 + y = 0.75 + (i - 8) + else + x = 0.25 + y = 0.75 + (i - 1) + end + return string.format("box[%f,%f;3.8,0.8;#ff0000]", x-0.1, y-0.05) +end + + +plg.update = function(pos) + local meta = minetest.get_meta(pos) + if meta:get_int("valid") ~= 1 then + return + end + + local is = lcore.deserialize(meta:get_string("instr")) + local A, B, C, D = plg.getports(pos) + A, B, C, D = lcore.interpret(is, A, B, C, D) + plg.setports(pos, A, B, C, D) +end + +plg.ports_changed = function(pos, rule, newstate) + if rule == nil then return end + local meta = minetest.get_meta(pos) + local states + + local s = meta:get_string("portstates") + if s == nil then + states = {false, false, false, false} + else + states = { + s:sub(1, 1) == "1", + s:sub(2, 2) == "1", + s:sub(3, 3) == "1", + s:sub(4, 4) == "1", + } + end + + -- trick to transform rules (see register_node) into port number + local portno = ({4, 1, nil, 3, 2})[3 + rule.x + 2*rule.z] + states[portno] = (newstate == "on") + + meta:set_string("portstates", + (states[1] and "1" or "0") .. (states[2] and "1" or "0") .. + (states[3] and "1" or "0") .. (states[4] and "1" or "0") + ) +end + +plg.getports = function(pos) -- gets merged states of INPUT & OUTPUT + local sin, sout + + local s = minetest.get_meta(pos):get_string("portstates") + if s == nil then + sin = {false, false, false, false} + else + sin = { + s:sub(1, 1) == "1", + s:sub(2, 2) == "1", + s:sub(3, 3) == "1", + s:sub(4, 4) == "1", + } + end + + local name = minetest.get_node(pos).name + assert(name:find("mesecons_fpga:fpga") == 1) + local off = #"mesecons_fpga:fpga" + sout = { + name:sub(off+4, off+4) == "1", + name:sub(off+3, off+3) == "1", + name:sub(off+2, off+2) == "1", + name:sub(off+1, off+1) == "1", + } + + return unpack({ + sin[1] or sout[1], + sin[2] or sout[2], + sin[3] or sout[3], + sin[4] or sout[4], + }) +end + +plg.setports = function(pos, A, B, C, D) -- sets states of OUTPUT + local base = "mesecons_fpga:fpga" + + local name = base + .. (D and "1" or "0") .. (C and "1" or "0") + .. (B and "1" or "0") .. (A and "1" or "0") + minetest.swap_node(pos, {name = name, param2 = minetest.get_node(pos).param2}) + + if A ~= nil then + local ru = plg.rules[base .. "0001"] + if A then mesecon.receptor_on(pos, ru) else mesecon.receptor_off(pos, ru) end + end + if B ~= nil then + local ru = plg.rules[base .. "0010"] + if B then mesecon.receptor_on(pos, ru) else mesecon.receptor_off(pos, ru) end + end + if C ~= nil then + local ru = plg.rules[base .. "0100"] + if C then mesecon.receptor_on(pos, ru) else mesecon.receptor_off(pos, ru) end + end + if D ~= nil then + local ru = plg.rules[base .. "1000"] + if D then mesecon.receptor_on(pos, ru) else mesecon.receptor_off(pos, ru) end + end +end + + +minetest.register_craft({ + output = "mesecons_fpga:fpga0000 2", + recipe = { + {'group:mesecon_conductor_craftable', 'group:mesecon_conductor_craftable'}, + {'mesecons_materials:silicon', 'mesecons_materials:silicon'}, + {'group:mesecon_conductor_craftable', 'group:mesecon_conductor_craftable'}, + } +}) diff --git a/mesecons_fpga/logic.lua b/mesecons_fpga/logic.lua new file mode 100644 index 0000000..3dca154 --- /dev/null +++ b/mesecons_fpga/logic.lua @@ -0,0 +1,210 @@ +local lg = {} + +-- (de)serialize +lg.serialize = function(t) + local function _op(t) + if t == nil then + return " " + elseif t.type == "io" then + return t.port + else -- t.type == "reg" + return tostring(t.n) + end + end + local function _action(s) + if s == nil then + return " " + end + local mapping = { + ["and"] = "&", + ["or"] = "|", + ["not"] = "~", + ["xor"] = "^", + ["nand"] = "?", --dunno + ["buf"] = "_", + ["xnor"] = "=", + } + return mapping[s] + end + + local s = "" + for i = 1, 14 do + local cur = t[i] + if next(cur) ~= nil then + s = s .. _op(cur.op1) .. _action(cur.action) .. _op(cur.op2) .. _op(cur.dst) + end + s = s .. "/" + end + return s +end + +lg.deserialize = function(s) + local function _op(c) + if c == "A" or c == "B" or c == "C" or c == "D" then + return {type = "io", port = c} + elseif c == " " then + return nil + else + return {type = "reg", n = tonumber(c)} + end + end + local function _action(c) + local mapping = { + ["&"] = "and", + ["|"] = "or", + ["~"] = "not", + ["^"] = "xor", + ["?"] = "nand", + ["_"] = "buf", + ["="] = "xnor", + [" "] = nil, + } + return mapping[c] + end + + local ret = {} + for part in s:gmatch("(.-)/") do + local parsed + if part == "" then + parsed = {} + else + parsed = { + action = _action( part:sub(2,2) ), + op1 = _op( part:sub(1,1) ), + op2 = _op( part:sub(3,3) ), + dst = _op( part:sub(4,4) ), + } + end + ret[#ret + 1] = parsed + end + -- More than 14 instructions (write to all 10 regs + 4 outputs) + -- will not pass the write-once requirement of the validator + assert(#ret == 14) + return ret +end + +-- validation +lg.validate_single = function(t, i) + local function is_reg_written_to(t, n, max) + for i = 1, max-1 do + if next(t[i]) ~= nil + and t[i].dst and t[i].dst.type == "reg" + and t[i].dst.n == n then + return true + end + end + return false + end + local function compare_op(t1, t2, allow_same_io) + if t1 == nil or t2 == nil then + return false + elseif t1.type ~= t2.type then + return false + end + if t1.type == "reg" and t1.n == t2.n then + return true + elseif t1.type == "io" and t1.port == t2.port then + return not allow_same_io + end + return false + end + local elem = t[i] + -- check for completeness + if elem.action == nil then + return {i = i, msg = "Gate type required"} + elseif elem.action == "not" or elem.action == "buf" then + if elem.op1 ~= nil or elem.op2 == nil or elem.dst == nil then + return {i = i, msg = "Second operand (only) and destination required"} + end + else + if elem.op1 == nil or elem.op2 == nil or elem.dst == nil then + return {i = i, msg = "Operands and destination required"} + end + end + -- check whether operands/destination are identical + if compare_op(elem.op1, elem.op2) then + return {i = i, msg = "Operands cannot be identical"} + end + if compare_op(elem.op1, elem.dst, true) or compare_op(elem.op2, elem.dst, true) then + return {i = i, msg = "Destination and operands must be different"} + end + -- check whether operands point to defined registers + if elem.op1 ~= nil and elem.op1.type == "reg" + and not is_reg_written_to(t, elem.op1.n, i) then + return {i = i, msg = "First operand is undefined register"} + end + if elem.op2.type == "reg" and not is_reg_written_to(t, elem.op2.n, i) then + return {i = i, msg = "Second operand is undefined register"} + end + -- check whether destination points to undefined register + if elem.dst.type == "reg" and is_reg_written_to(t, elem.dst.n, i) then + return {i = i, msg = "Destination is already used register"} + end + + return nil +end + +lg.validate = function(t) + for i = 1, 14 do + if next(t[i]) ~= nil then + local r = lg.validate_single(t, i) + if r ~= nil then + return r + end + end + end + return nil +end + +-- interpreter +lg.interpret = function(t, a, b, c, d) + local function _action(s, v1, v2) + if s == "and" then + return v1 and v2 + elseif s == "or" then + return v1 or v2 + elseif s == "not" then + return not v2 + elseif s == "xor" then + return v1 ~= v2 + elseif s == "nand" then + return not (v1 and v2) + elseif s == "buf" then + return v2 + else -- s == "xnor" + return v1 == v2 + end + end + local function _op(t, regs, io_in) + if t.type == "reg" then + return regs[t.n] + else -- t.type == "io" + return io_in[t.port] + end + end + + local io_in = {A=a, B=b, C=c, D=d} + local regs = {} + local io_out = {} + for i = 1, 14 do + local cur = t[i] + if next(cur) ~= nil then + local v1, v2 + if cur.op1 ~= nil then + v1 = _op(cur.op1, regs, io_in) + end + v2 = _op(cur.op2, regs, io_in) + + local result = _action(cur.action, v1, v2) + + if cur.dst.type == "reg" then + regs[cur.dst.n] = result + else -- cur.dst.type == "io" + io_out[cur.dst.port] = result + end + end + end + return io_out.A, io_out.B, io_out.C, io_out.D +end + +return lg diff --git a/mesecons_fpga/textures/jeija_fpga_programmer.png b/mesecons_fpga/textures/jeija_fpga_programmer.png new file mode 100644 index 0000000..9c0ba8f Binary files /dev/null and b/mesecons_fpga/textures/jeija_fpga_programmer.png differ diff --git a/mesecons_fpga/textures/jeija_fpga_sides.png b/mesecons_fpga/textures/jeija_fpga_sides.png new file mode 100644 index 0000000..e2d8e15 Binary files /dev/null and b/mesecons_fpga/textures/jeija_fpga_sides.png differ diff --git a/mesecons_fpga/textures/jeija_fpga_top.png b/mesecons_fpga/textures/jeija_fpga_top.png new file mode 100644 index 0000000..eaf1a1c Binary files /dev/null and b/mesecons_fpga/textures/jeija_fpga_top.png differ diff --git a/mesecons_fpga/tool.lua b/mesecons_fpga/tool.lua new file mode 100644 index 0000000..26ab49e --- /dev/null +++ b/mesecons_fpga/tool.lua @@ -0,0 +1,62 @@ +return function(plg) + + +minetest.register_tool("mesecons_fpga:programmer", { + description = "FPGA Programmer", + inventory_image = "jeija_fpga_programmer.png", + stack_max = 1, + on_place = function(itemstack, placer, pointed_thing) + if pointed_thing.type ~= "node" then + return itemstack + end + + local pos = pointed_thing.under + if minetest.get_node(pos).name:find("mesecons_fpga:fpga") ~= 1 then + return itemstack + end + + local meta = minetest.get_meta(pos) + if meta:get_string("instr") == "//////////////" then + minetest.chat_send_player(placer:get_player_name(), "This FPGA is unprogrammed.") + return itemstack + end + itemstack:set_metadata(meta:get_string("instr")) + minetest.chat_send_player(placer:get_player_name(), "FPGA gate configuration was successfully copied!") + + return itemstack + end, + on_use = function(itemstack, user, pointed_thing) + if pointed_thing.type ~= "node" then + return itemstack + end + + local pos = pointed_thing.under + if minetest.get_node(pos).name:find("mesecons_fpga:fpga") ~= 1 then + return itemstack + end + + local imeta = itemstack:get_metadata() + if imeta == "" then + minetest.chat_send_player(user:get_player_name(), "Use shift+right-click to copy a gate configuration first.") + return itemstack + end + + local meta = minetest.get_meta(pos) + meta:set_string("instr", imeta) + plg.update_formspec(pos, imeta) + minetest.chat_send_player(user:get_player_name(), "Gate configuration was successfully written to FPGA!") + + return itemstack + end +}) + +minetest.register_craft({ + output = "mesecons_fpga:programmer", + recipe = { + {'group:mesecon_conductor_craftable'}, + {'mesecons_materials:silicon'}, + } +}) + + +end