├── .gitignore ├── extras ├── Documents │ ├── Media │ │ ├── Teensy.jpg │ │ ├── BlinkRed.png │ │ ├── BlinkGreen.png │ │ ├── NightlightOff.png │ │ ├── NightlightOn.png │ │ └── ArduinoLibrary.png │ └── README.md ├── Interactive │ ├── Interactive.fsproj │ └── Interactive.fs ├── Compiler │ ├── Compiler.fsproj │ ├── Interface.fs │ └── Bytecode.fs └── Brief.sln ├── library.properties ├── examples └── Brief │ └── Brief.ino ├── keywords.txt ├── README.md ├── LICENSE └── src ├── Brief.h └── Brief.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | **/bin/* 2 | **/obj/* 3 | **/.vs/* 4 | -------------------------------------------------------------------------------- /extras/Documents/Media/Teensy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshleyF/BriefEmbedded/HEAD/extras/Documents/Media/Teensy.jpg -------------------------------------------------------------------------------- /extras/Documents/Media/BlinkRed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshleyF/BriefEmbedded/HEAD/extras/Documents/Media/BlinkRed.png -------------------------------------------------------------------------------- /extras/Documents/Media/BlinkGreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshleyF/BriefEmbedded/HEAD/extras/Documents/Media/BlinkGreen.png -------------------------------------------------------------------------------- /extras/Documents/Media/NightlightOff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshleyF/BriefEmbedded/HEAD/extras/Documents/Media/NightlightOff.png -------------------------------------------------------------------------------- /extras/Documents/Media/NightlightOn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshleyF/BriefEmbedded/HEAD/extras/Documents/Media/NightlightOn.png -------------------------------------------------------------------------------- /extras/Documents/Media/ArduinoLibrary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshleyF/BriefEmbedded/HEAD/extras/Documents/Media/ArduinoLibrary.png -------------------------------------------------------------------------------- /extras/Interactive/Interactive.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /extras/Compiler/Compiler.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=Brief 2 | version=1.0.6 3 | author=AshleyF 4 | maintainer=AshleyF 5 | sentence=A scriptable firmware and protocol for interfacing hardware. 6 | paragraph=It is comprised of a VM – a tiny stack machine running on the MCU, Protocol – an extensible and composable set of commands and events, Language – a Forth-like interactive scripting language compiled for the VM, Interactive – console for interactive experimentation and development. 7 | category=Other 8 | url=https://github.com/AshleyF/BriefEmbedded 9 | architectures=* 10 | includes=Brief.h 11 | -------------------------------------------------------------------------------- /examples/Brief/Brief.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | // This is an example of how to extend the VM. 5 | // Add a void function taking no arguments. 6 | // Pop parameters from the stack and push return values. 7 | void delayMillis() 8 | { 9 | delay((int)brief::pop()); 10 | } 11 | 12 | void setup() 13 | { 14 | brief::setup(); 15 | 16 | // Bind extended function to a custom instruction (100) 17 | // In the interactive: 100 'delay instruction 18 | // In .NET: compiler.Instruction("delay", 100) 19 | brief::bind(100, delayMillis); 20 | } 21 | 22 | void loop() 23 | { 24 | brief::loop(); 25 | } 26 | -------------------------------------------------------------------------------- /keywords.txt: -------------------------------------------------------------------------------- 1 | # Syntax Coloring Map For Brief 2 | ####################################### 3 | 4 | ####################################### 5 | # Datatypes (KEYWORD1) 6 | ####################################### 7 | 8 | Brief KEYWORD1 9 | 10 | ####################################### 11 | # Methods and Functions (KEYWORD2) 12 | ####################################### 13 | 14 | setup KEYWORD2 15 | loop KEYWORD2 16 | memget KEYWORD2 17 | memset KEYWORD2 18 | bind KEYWORD2 19 | push KEYWORD2 20 | pop KEYWORD2 21 | error KEYWORD2 22 | exec KEYWORD2 23 | 24 | ####################################### 25 | # Constants (LITERAL1) 26 | ####################################### 27 | 28 | MEM_SIZE LITERAL1 29 | DATA_STACK_SIZE LITERAL1 30 | RETURN_STACK_SIZE LITERAL1 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Brief Embedded 2 | ============== 3 | 4 | Brief Embedded is a scriptable firmware and protocol for interfacing hardware with .NET. 5 | 6 | Here's a quick demo: https://youtu.be/M_iiSRowOFM 7 | 8 | It may be discovered and installed via the Arduino Library Manager. Just search for "brief" and click Install. 9 | 10 | ![Arduino Library installation](./extras/Documents/Media/ArduinoLibrary.png) 11 | 12 | Have fun! 13 | 14 | ---- 15 | 16 | [Full documentation here](extras/Documents/README.md). The code itself contains literate style documentation as well. 17 | 18 | This is a fork [from here](http://github.com/ashleyf/brief/tree/gh-pages/embedded), but simplified by removing Reflecta, IL translation and several instructions. This is the most active fork now-a-days. 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 AshleyF 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /extras/Brief.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31019.35 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Interactive", "Interactive\Interactive.fsproj", "{C797EB9A-C7A7-44E0-9B64-8059FCA78C24}" 7 | EndProject 8 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Brief", "Compiler\Compiler.fsproj", "{DAEDCED6-7D0B-458E-82D6-9A85E705E54C}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {C797EB9A-C7A7-44E0-9B64-8059FCA78C24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {C797EB9A-C7A7-44E0-9B64-8059FCA78C24}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {C797EB9A-C7A7-44E0-9B64-8059FCA78C24}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {C797EB9A-C7A7-44E0-9B64-8059FCA78C24}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {DAEDCED6-7D0B-458E-82D6-9A85E705E54C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {DAEDCED6-7D0B-458E-82D6-9A85E705E54C}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {DAEDCED6-7D0B-458E-82D6-9A85E705E54C}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {DAEDCED6-7D0B-458E-82D6-9A85E705E54C}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {1E60B039-3E19-4F47-88BA-65F69D764AC0} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /src/Brief.h: -------------------------------------------------------------------------------- 1 | /* Brief.h 2 | 3 | Scriptable firmware for interfacing hardware with the .NET libraries and for 4 | running real time control loops. */ 5 | 6 | #define __STDC_LIMIT_MACROS 7 | #include 8 | #include 9 | 10 | #ifndef BRIEF_H 11 | #define BRIEF_H 12 | 13 | #define MEM_SIZE 1024 // dictionary space 14 | #define DATA_STACK_SIZE 8 // evaluation stack elements (int32s) 15 | #define RETURN_STACK_SIZE 8 // return and locals stack elements (int32s) 16 | 17 | #define MAX_PRIMITIVES 128 // max number of primitive (7-bit) instructions 18 | #define MAX_INTERRUPTS 6 // max number of ISR words 19 | 20 | #define BOOT_EVENT_ID 0xFF // event sent upon 'setup' (not reset) 21 | #define VM_EVENT_ID 0xFE // event sent upon VM error 22 | 23 | #define VM_ERROR_RETURN_STACK_UNDERFLOW 0 24 | #define VM_ERROR_RETURN_STACK_OVERFLOW 1 25 | #define VM_ERROR_DATA_STACK_UNDERFLOW 2 26 | #define VM_ERROR_DATA_STACK_OVERFLOW 3 27 | #define VM_ERROR_OUT_OF_MEMORY 4 28 | 29 | namespace brief 30 | { 31 | /* The following setup() and loop() are expected to be added to the main *.ino 32 | as you will find in Brief.ino. */ 33 | 34 | void setup(); // initialize everything, bind primitives 35 | void loop(); // execute loop word (execute/define) 36 | 37 | /* The following allow peeking/poking the Brief dictionary */ 38 | 39 | uint8_t memget(int16_t address); // fetch with bounds checking 40 | void memset(int16_t address, uint8_t value); // store with bounds checking 41 | // TODO: expose `here`, along with appending call, return, literal, etc. 42 | 43 | /* The following get/set the program counter; allowing for deeply integrated external instructions 44 | that fetch operands or do self-modification */ 45 | 46 | int16_t pget(); 47 | void pset(int16_t pp); 48 | 49 | /* The following are meant for those wanting to bind their own functions into the Brief system. 50 | New functions can be bound to the instruction table, they can push/pop data to interact with 51 | other Brief instructions, and they may emit errors up to the PC. */ 52 | 53 | void bind(uint8_t i, void (*f)()); // add function to instruction table 54 | void push(int16_t x); // push data to evaluation stack 55 | int16_t pop(); // pop data from evaluation stack 56 | void error(uint8_t code); // error events 57 | 58 | /* If, for some reason, you want to manually execute Brief bytecode in memory */ 59 | 60 | void exec(int16_t address); // execute code at given address 61 | } 62 | 63 | #endif // BRIEF_H -------------------------------------------------------------------------------- /extras/Compiler/Interface.fs: -------------------------------------------------------------------------------- 1 | namespace Brief 2 | 3 | open System 4 | open System.IO.Ports 5 | open System.Threading 6 | open Bytecode 7 | 8 | (* Below is a class meant to be used in C#-land. As such we give a little object wrapper with 9 | overloaded methods hiding the option types and such. It holds onto an instance the dictionary, 10 | the current address and the sequence of pending bytecode to be sent down to the MCU. This 11 | makes many of the methods simply curried versions of existing functions with these captured. 12 | In fact, it's interesting to think of objects as really a set of partially-applied functions? :) 13 | 14 | Each of the Eager* methods return a pair of byte arrays (byte[] * byte[] tuple) representing 15 | the dependent definitions needing to be sent down and the bytecode for the compiled/translated 16 | code. 17 | 18 | Each of the Lazy* methods return a Lazy which can be reified (as if eager) later with 19 | the Reify method. 20 | 21 | The Define methods have the side effect of adding lazy definitions to the dictionary. These may 22 | later be reified implicitly and returned as definitions to send down when depending code is 23 | reified. *) 24 | 25 | open System.Reflection 26 | 27 | type Compiler() = 28 | let dict = ref [] 29 | let address = ref 0 30 | let pending = ref Seq.empty 31 | 32 | let getPending () = 33 | let p = !pending |> Array.ofSeq 34 | pending := Seq.empty; p 35 | 36 | let token (memb : MemberInfo) = Some (memb.Module.FullyQualifiedName, memb.MetadataToken) 37 | 38 | do initDictionary dict address pending 39 | 40 | member x.Reset() = dict := []; address := 0; pending := Seq.empty; initDictionary dict address pending 41 | 42 | member x.EagerCompile(source) = eagerCompile dict source, getPending () 43 | member x.EagerAssemble(ast) = eagerAssemble dict ast, getPending () 44 | 45 | member x.LazyCompile(source) = lazyCompile dict source address pending 46 | member x.LazyAssemble(ast) = lazyAssemble dict ast address pending 47 | 48 | member x.Reify(lazycode : Lazy) = lazycode.Force(), getPending () 49 | 50 | member x.Define(word, code) = define dict None word None code 51 | member x.Define(word, brief, code) = define dict (Some brief) word None code 52 | member x.Define(word, memb, code) = define dict None word (token memb ) code 53 | member x.Define(word, brief, meth, code) = define dict (Some brief) word (token meth ) code 54 | 55 | member x.Instruction(word, code) = define dict None word None (lazy ([|code|])) 56 | 57 | member x.Address = !address 58 | 59 | member x.Disassemble(bytecode) = 60 | bytecode 61 | |> disassembleBrief dict 62 | |> printBrief dict 63 | |> List.map ((+) " ") |> List.reduce (+) 64 | 65 | type Communication(eventFn : Action, traceFn: Action) = 66 | let (serial : SerialPort option ref) = ref None 67 | let rec readEvents () = 68 | let event message = if eventFn <> null then eventFn.Invoke(message) 69 | match !serial with 70 | | Some port -> 71 | if port.IsOpen && port.BytesToRead > 0 then 72 | let len = port.ReadByte() 73 | let id = port.ReadByte() |> byte 74 | let data = Array.create len 0uy 75 | port.Read(data, 0, len) |> ignore 76 | let toInt d = 77 | match Array.length d with 78 | | 0 -> 0s 79 | | 1 -> d.[0] |> sbyte |> int16 80 | | 2 -> (int16 d.[0] <<< 8) ||| int16 d.[1] 81 | | _ -> failwith "Invalid event data." 82 | match id with 83 | | id when id = 0xF0uy -> data |> toInt |> sprintf "%i" |> event 84 | | 0xFFuy -> event "Boot event" 85 | | 0xFEuy -> 86 | sprintf "VM Error: %s" 87 | (match data with 88 | | [|0uy|] -> "Return stack underflow" 89 | | [|1uy|] -> "Return stack overflow" 90 | | [|2uy|] -> "Data stack underflow" 91 | | [|3uy|] -> "Data stack overflow" 92 | | [|4uy|] -> "Out of memory" 93 | | _ -> "Unknown") |> event 94 | | _ -> sprintf "Event (%i): %A" id data |> event 95 | | None -> () 96 | Thread.Sleep(100) 97 | readEvents () 98 | let mutable (readThread: Thread) = null 99 | member x.Connect(com) = 100 | let port = new SerialPort(com, 19200) 101 | serial := Some port 102 | port.Open() 103 | port.DiscardInBuffer() 104 | port.DiscardOutBuffer() 105 | readThread <- new Thread(readEvents, IsBackground = true) 106 | readThread.Start() 107 | member x.Disconnect() = 108 | match !serial with 109 | | Some port -> 110 | port.Close() 111 | serial := None 112 | readThread.Abort() 113 | readThread <- null 114 | | None -> failwith "Not connected" 115 | member x.SendBytes(execute, bytecode) = 116 | let trace () = if traceFn <> null then traceFn.Invoke(execute, bytecode) 117 | match !serial with 118 | | Some port -> 119 | if bytecode.Length > 127 then failwith "Too much bytecode in single packet" 120 | trace () 121 | let header = (byte bytecode.Length ||| if execute then 0x80uy else 0uy) 122 | port.Write(Array.create 1 header, 0, 1) 123 | port.Write(bytecode, 0, bytecode.Length) 124 | port.BaseStream.Flush() 125 | | None -> failwith "Not connected to MCU." -------------------------------------------------------------------------------- /extras/Interactive/Interactive.fs: -------------------------------------------------------------------------------- 1 | (* This is a very basic interactive console for working with an MCU running the Brief firmware. 2 | This is extreemely useful for debugging and experimenting with new hardware; allowing interactive 3 | compilation and execution Brief code as well as adding of definitions to the dictionary. 4 | 5 | The full Brief language syntax is available. Additionally, there are several "compile-time" words 6 | such as connect/disconnect/reset, define/variable, and debug. 7 | 8 | We keep a compile-time stack of lexed/parsed nodes. This can include literals and quotations that 9 | end up being consumed by following compile-time words. Anything not consumed is assumed to be 10 | meant for runtime and is compiled. *) 11 | 12 | open System 13 | open System.IO 14 | open Bytecode 15 | open Brief 16 | 17 | let compiler = new Compiler() 18 | let traceMode = ref false // whether to spew trace info (bytecode, disassembly, etc.) 19 | let comm = 20 | new Communication( 21 | (fun e -> printfn "Event: %s" e), 22 | (fun execute bytecode -> 23 | if !traceMode then 24 | printfn "%s:%s\nBytecode (%i): %s\n" 25 | (if execute then "Execute" else "Define") 26 | (compiler.Disassemble(bytecode)) 27 | bytecode.Length 28 | (new String(bytecode |> Array.map (sprintf "%02x ") |> Seq.concat |> Array.ofSeq)))) 29 | 30 | (* Here we use the lexer/parser to process lines of Brief code. Most everything is handled by the 31 | compiler, but several words are intercepted here as "compile-time" words for the interactive: 32 | 33 | Connecting to, disconnecting from and resetting the MCU can be done as follows. You must connect 34 | to an MCU before executing anything or issuing definitions. The 'connect' word expects a 35 | quotation (on the compiler-stack) containing a single word specifying the COM port. This sounds 36 | strange to use undefined words such as "com16", but remember that this is consumed by the 37 | interactive at compile-time. There need not be any such words in the dictionary. Examples: 38 | 39 | 'com16 connect 40 | 'com8 conn 41 | 42 | disconnect 43 | 44 | reset 45 | 46 | Definitions are added by the following form: 47 | 48 | [foo bar] 'baz define 49 | 50 | That is, a compile-time quotation containing the definition, followed by a quotation containing 51 | a single (not necessarily defined word) usually abreviated with a tick, but [baz] is equally 52 | valid. These two compile-time arguments are followed by define or def. Examples: 53 | 54 | [dup *] 'square def 55 | [dup 0 < 'neg if] 'abs define 56 | 57 | Variables are really just defined words that push the address of a two-byte slot of memory taken 58 | from dictionary space. They are intended to then be used with the fetch (@) and store (!) words. 59 | Here we use a little trick of a definition containing simply [0]. This will compile to a 60 | quotation (which pushed the address of the contained code) containing a simple literal; the code 61 | for which happens to be two bytes. This two-byte value is used as writable memory. 62 | 63 | Variables take a compile-time single-word quotation giving the name. Examples: 64 | 65 | 'foo variable 66 | 'bar var 67 | 68 | Remember that these words now push the address of the two-byte slot. The can be used in 69 | combination with fetch (@) to retrieve the value of the variable: 70 | 71 | foo @ 72 | bar @ 73 | 74 | They can be used along with literals (or any calculated value already on the stack) and the 75 | store (!) word to set the value of the variable: 76 | 77 | 123 foo ! 78 | 0 analogRead bar ! 79 | 80 | Code may be loaded from a file with the load word. This accepts a single quotation containing 81 | the file path. It may be an absolute path or otherwise is relative to the working directory: 82 | 83 | 'foo.txt load 84 | 'c:\temp\test.txt load 85 | 86 | Commented lines may begin with backslash (\). 87 | 88 | Debugging mode may be toggled in which disassembly and raw bytecode are displayed. For example, 89 | the following interactive session defining and using words to turn on/off the built-in LED on 90 | the Teensy: 91 | 92 | > 'com16 conn 93 | > trace 94 | Debug mode: true 95 | 96 | > 11 output pinMode 97 | Execute: 11 1 pinMode 98 | Bytecode (5): 01 0b 01 01 3a 99 | 100 | You can see that "output" disassembles to a literal 1, and that a total of five bytes is sent 101 | down to the MCU. 102 | 103 | > [high 11 digitalWrite] 'ledOn def 104 | > [low 11 digitalWrite] 'ledOff def 105 | 106 | Then we define a pair of words to turn the LED on/off. Notice though that nothing at all is sent 107 | down to the MCU! The bytecode is lazily defined upon first use: 108 | 109 | > ledOn 110 | Define: -1 11 digitalWrite (return) 111 | Bytecode (6): 01 ff 01 0b 3c 00 112 | 113 | Execute: ledOn 114 | Bytecode: (2): 80 00 115 | 116 | >ledOff 117 | Define: 0 11 digitalWrite (return) 118 | Bytecode (6): 01 00 01 0b 3c 00 119 | 120 | Execute: ledOff 121 | Bytecode (2): 80 06 122 | 123 | The disassembly of the definitions shows the trailing "(return)" instruction and the fact that 124 | "high"/"low" are translated to literals -1/0. In the bytecode, 01 is apparently the lit8 125 | instruction followed by the values. The 3c bytecode is apparently digitalWrite and 00 is return. 126 | 127 | Upon executing "ledOn", we can see that the 6-byte definition is sent down followed by a 2-byte 128 | call to it. The same thing happens for "ledOff". Now the second time we execute these words: 129 | 130 | > ledOn 131 | Execute: ledOn 132 | Bytecode (2): 80 00 133 | 134 | > ledOff 135 | Execute: ledOff 136 | Bytecode (2): 80 06 137 | 138 | We can clearly see that only the 2-byte calls need to be send as the definitions are already in 139 | the dictionary at the MCU. All of these interesting mechanics and the raw and disassembled 140 | bytecode can be seen with tracing on. *) 141 | 142 | let rec rep line = 143 | let reset () = comm.SendBytes(true, compiler.EagerCompile("(reset)") |> fst) 144 | let p = line |> parse 145 | let rec rep' stack = function 146 | | Token tok :: t -> 147 | match tok with 148 | | "connect" | "conn" -> 149 | match stack with 150 | | [Quotation [Token com]] :: stack' -> 151 | printfn "Connecting to %s" com 152 | comm.Connect(com) 153 | reset () 154 | rep' stack' t 155 | | _ -> failwith "Malformed connect syntax - usage: '7 connect" 156 | | "disconnect" -> 157 | comm.Disconnect() 158 | rep' stack t 159 | | "reset" -> 160 | reset () 161 | compiler.Reset() 162 | rep' stack t 163 | | "define" | "def" -> 164 | match stack with 165 | | [Quotation [Token name]] :: [Quotation def] :: stack' -> 166 | compiler.Define(name, compiler.LazyAssemble(def)) 167 | rep' stack' t 168 | | _ -> failwith "Malformed definition syntax - usage: [foo bar] 'baz define" 169 | | "instruction" -> 170 | match stack with 171 | | [Quotation [Token name]] :: [Number code] :: stack' -> 172 | compiler.Instruction(name, byte code) 173 | rep' stack' t 174 | | _ -> failwith "Malformed instruction definition - usage: 123 'foo instruction" 175 | | "variable" | "var" -> 176 | match stack with 177 | | [Quotation [Token name]] :: stack' -> 178 | compiler.Define(name, compiler.LazyCompile("[(return)]")) 179 | rep' stack' t 180 | | _ -> failwith "Malformed variable syntax - usage: 'foo variable" 181 | | "load" -> 182 | match stack with 183 | | [Quotation [Token path]] :: stack' -> 184 | use file = File.OpenText path 185 | file.ReadToEnd().Split '\n' 186 | |> Array.iter (fun line -> 187 | printfn " %s" line 188 | rep line) 189 | rep' stack' t 190 | | _ -> failwith "Malformed load syntax - usage: 'foo.txt load" 191 | | "\\" -> rep' stack [] 192 | | "." -> rep' stack (Number (int16 0xF0uy) :: Token "event" :: t) 193 | | "prompt" -> 194 | Console.ReadLine() |> ignore 195 | rep' stack t 196 | | "trace" -> 197 | traceMode := not !traceMode 198 | printfn "Trace mode: %b" !traceMode 199 | rep' stack t 200 | | "memory" | "mem" -> 201 | printfn "Memory used: %i bytes" compiler.Address 202 | rep' stack t 203 | | "go" -> 204 | traceMode := true 205 | printfn "Trace mode: %b" !traceMode 206 | rep' stack [Quotation [Token "com4"]; Token "conn"; Quotation [Token "test.b"]; Token "load"] 207 | | "exit" -> 208 | comm.Disconnect() 209 | failwith "exit" 210 | | _ -> rep' ([Token tok] :: stack) t 211 | | node :: t -> rep' ([node] :: stack) t 212 | | [] -> List.rev stack 213 | let exe, def = compiler.EagerAssemble(rep' [] p |> List.concat) 214 | if def.Length > 0 then comm.SendBytes(false, def) 215 | if exe.Length > 0 then comm.SendBytes(true, (Array.concat [exe; [|0uy|]])) 216 | 217 | let rec repl () = 218 | printf "\n> " 219 | try 220 | Console.ReadLine() |> rep 221 | repl () 222 | with ex -> 223 | if ex.Message <> "exit" then 224 | printfn "Error: %s" ex.Message 225 | repl () 226 | 227 | printfn "Welcome to Brief" 228 | repl () 229 | -------------------------------------------------------------------------------- /extras/Compiler/Bytecode.fs: -------------------------------------------------------------------------------- 1 | module Bytecode 2 | 3 | open System 4 | 5 | (* Below is everything having to do with Brief bytecode, AST, assembly, disassembly and lexing, 6 | parsing and compiling from source. Also the host-side dictionary. 7 | 8 | Below are all of the base Brief instructions. Any user-defined functions will be (User byte). 9 | Notice that most of them have no operands; instead taking parameters from the stack. The 10 | exceptions are literals and Quote. A Word is a 16-bit subroutine address. The User 11 | type is for user-defined instructions. 12 | 13 | To understand the instruction set, refer to the VM implementation: 14 | 15 | Firmware\libraries\Brief\Brief.cpp/h *) 16 | 17 | type Instruction = 18 | | Literal of int16 // becomes lit8/16 19 | | Quote of byte 20 | | Return 21 | | EventHeader | EventBody8 | EventBody16 | EventFooter | Event 22 | | Fetch8 | Store8 23 | | Fetch16 | Store16 24 | | Add | Subtract | Multiply | Divide | Modulus 25 | | And | Or | ExclusiveOr 26 | | Shift 27 | | Equal | NotEqual | Greater | GreaterOrEqual | Less | LessOrEqual 28 | | Not 29 | | Negate 30 | | Increment | Decrement 31 | | Drop | Duplicate | Swap | Pick | Roll | Clear 32 | | Push | Pop | Peek 33 | | Forget 34 | | Call 35 | | Choice | If 36 | | LoopTicks 37 | | SetLoop | StopLoop 38 | | Reset 39 | | PinMode 40 | | DigitalWrite | DigitalRead 41 | | AnalogWrite | AnalogRead 42 | | AttachISR | DetachISR 43 | | Milliseconds 44 | | PulseIn 45 | | Word of int16 * string 46 | | User of byte // user defined instruction 47 | | NoOperation 48 | 49 | (* We maintain mappings from Word names, and optionally Brief instructions to bytecode. Definitions 50 | exist host-side (PC) initially. This is why Code is a Lazy. Only upon first use are 51 | they reified. We want to allow libraries of host-side definitions with a "pay as you go" model. 52 | At that point, long definitions are sent down to the MCU and the reified form becomes a two-byte 53 | call. The address of the call is specific to the MCU. Another reason for MCU-specificity is that 54 | bytecode values may change depending on the order in which they're bound. 55 | 56 | A "dictionary" is a simple Definition list. Various helper functions are provided to search the 57 | dictionary and to add new definitions. Defintions may shadow existing ones (last one 58 | defined becomes the first one found). 59 | 60 | Upon lookup, these definitions may be simply returned as is, which is what happens when they are 61 | very short. A call is two bytes, so there is no reason to add definitions at the MCU for bytecode 62 | sequences <= 2 bytes. This is the case for definitions mapped directly to primitives (e.g. 63 | PinMode, DigitalWrite, etc.) and is true for aliases to 'call's already defined or other very 64 | short definitions (e.g. [dup *] 'sq define) which are always inlined. See 'shrink' and 65 | 'lazyGenerate' below to understand how lazy thunks are created. 66 | 67 | If a definition is a longer sequence then it is sent down to the MCU as a definition and lookup 68 | returns a 2-byte call instead of the sequence. This call address is specific to the MCU on which 69 | it's defined, so lookup takes a dictionary instance owned by a particular MCU. The Brief compiler 70 | uses this mechanism to resolve word definitions. *) 71 | 72 | type Definition = { 73 | Brief : Instruction option // Brief instruction (optional) 74 | Word : string // Brief word name 75 | Code : Lazy } // on-demand code generator 76 | 77 | let find pred dict = List.tryFind pred (!dict) 78 | 79 | let findBrief brief = find (fun d -> d.Brief = Some brief) 80 | let findWord word = find (fun d -> d.Word = word) 81 | let findCode code = find (fun d -> d.Code.IsValueCreated && d.Code.Value = code) 82 | 83 | let codeToWord dict call = 84 | match findCode call dict with 85 | | Some def -> def.Word 86 | | None -> failwith "Unrecognized bytecode sequence." 87 | 88 | let define dict brief word token code = 89 | dict := 90 | { Brief = brief 91 | Word = word 92 | Code = code } :: !dict 93 | 94 | (* Below is the Brief assembler. Here we convert Brief instruction sequences to bytecode. It's a 95 | pretty straightforward process. Notice that Literals become either two or three bytes depending 96 | on the value. Future optimizations may include specific single-byte instructions for certain 97 | values. Words become two-byte calls with the high bit set. There is a Call instruction but this 98 | is for taking an address from the stack. Instead, this high-bit-scheme makes for very efficiently 99 | packed subroutine threaded code. 100 | 101 | In idiomatic Brief code, there are no branches. Instead we make use of quotations (the Quote 102 | instruction) and Choice and If for conditionals. This mechanism, along with subroutine calls, 103 | is all that is needed for a fully expressive language. *) 104 | 105 | let assembleBriefInstruction dict = function 106 | | Literal x -> 107 | if x >= -128s && x <= 127s then [1uy; byte x] // lit8 x 108 | else [2uy; x >>> 8 |> byte; byte x] // lit16 x 109 | | Quote x -> [3uy; byte x] 110 | | Word (x, _) -> [byte (x >>> 8) ||| 0x80uy; byte x] 111 | | NoOperation -> [] 112 | | User x -> [x] 113 | | brief -> 114 | match findBrief brief dict with 115 | | Some def -> def.Code.Force() |> List.ofArray 116 | | None -> failwith "Unrecognized Brief bytecode" 117 | 118 | let assembleBrief dict = List.map (assembleBriefInstruction dict) >> List.concat 119 | 120 | (* For debugging and diagnostics, it is often useful to convert raw bytecode back to a list of 121 | Brief instructions. For subroutine calls, we even look up the name in the dictionary. We also 122 | provide a simple pretty-printer. *) 123 | 124 | let disassembleBrief dict bytecode = 125 | let rec disassemble dis b = 126 | let recurse t d = disassemble (d :: dis) t 127 | let unpackInt16 a b = (int16 a <<< 8 ||| int16 b) 128 | match b with 129 | | 0uy :: t -> Return |> recurse t 130 | | 1uy :: x :: t -> Literal (x |> sbyte |> int16) |> recurse t 131 | | 2uy :: a :: b :: t -> Literal (unpackInt16 a b) |> recurse t 132 | | 3uy :: x :: t -> Quote (byte x) |> recurse t 133 | | a :: b :: t when a &&& 0x80uy <> 0uy -> // call 134 | let addr = unpackInt16 (a &&& 0x7Fuy) b 135 | let word = codeToWord dict [|a; b|] 136 | Word (addr, word) |> recurse t 137 | | bytecode :: t -> 138 | (match findCode [|bytecode|] dict with 139 | | Some def -> 140 | match def.Brief with 141 | | Some brief -> brief 142 | | None -> User bytecode 143 | | None -> User bytecode) |> recurse t 144 | | [] -> List.rev dis 145 | bytecode |> List.ofArray |> disassemble [] 146 | 147 | let printBrief dict b = // Brief to 'words' 148 | let rec print = function 149 | | Literal x -> sprintf "%i" x 150 | | Quote x -> sprintf "(quote %i)" x 151 | | Word (_, name) -> name 152 | | NoOperation -> failwith "NoOperation should not exist in assembled code" 153 | | User x -> 154 | match findCode [|x|] dict with 155 | | Some def -> def.Word 156 | | None -> sprintf "(user %i)" x 157 | | brief -> 158 | match findBrief brief dict with 159 | | Some def -> def.Word 160 | | None -> sprintf "(unknown %A)" brief 161 | b |> List.map print 162 | 163 | (* Below is everything needed to lex/parse/compile Brief source. 164 | 165 | The lexer is quite simple! For the most part, tokens are plainly whitespace separated. The 166 | exception to this is square brackets for quotations and a small bit of syntactic sugar allowing 167 | a tick mark to quote single tokens. Square brackets become separate tokens (even if not space 168 | separated) and ' followed by a token expands as if the token were surrounded by square brackets. 169 | 170 | For example: 171 | 172 | foo bar 123 173 | Becomes three tokens "foo", "bar", "123". 174 | 175 | foo [bar] 123 176 | Becomes five tokens "foo", "[", "bar", "]", "123". 177 | 178 | foo 'bar 123 179 | Becomes the same thing with 'bar expanding to "[", "bar", "]". *) 180 | 181 | let lex source = 182 | let rec lex' quote token source = seq { 183 | let emitToken token = seq { // emit word or [word] if quote 184 | let tokenToString = List.fold (fun s c -> c.ToString() + s) "" 185 | if List.length token > 0 then 186 | if quote then yield "[" 187 | yield tokenToString token 188 | if quote then yield "]" 189 | elif quote then failwith "Syntax error: Dangling tick" } 190 | match source with 191 | | c :: t when Char.IsWhiteSpace c -> // whitespace delimeted tokens 192 | yield! emitToken token 193 | yield! lex' false [] t 194 | | ('[' as c) :: t | (']' as c) :: t -> // brackets separate token 195 | if quote then failwith "Syntax error: '[ or ']" 196 | yield! emitToken token 197 | yield c.ToString() 198 | yield! lex' false [] t 199 | | '\'' :: t -> // quote next token: 'foo becomes [foo] for example 200 | if quote then failwith "Syntax error: ''" 201 | yield! emitToken token 202 | yield! lex' true [] t 203 | | c :: t -> yield! lex' quote (c :: token) t 204 | | [] -> yield! emitToken token } 205 | source |> List.ofSeq |> lex' false [] 206 | 207 | (* The parser takes a sequence of tokens (from the lexer) and gives them some semantic meaning. 208 | Square bracket surrounded tokens become a single Quotation node with the surrounded tokens 209 | parsed as a child Node list. Tokens which can be parsed as an Int16 become Numbers. Special 210 | syntax is allowed for literal subroutine call addresses in the form "(123)". 211 | 212 | Note that there is no guarantee that output from the pretty-printer can be "round tripped" 213 | through the lexer/parser. *) 214 | 215 | type Node = 216 | | Token of string 217 | | Address of int16 218 | | Number of int16 219 | | Quotation of Node list 220 | 221 | let parse source = 222 | let rec parse' nodes source = 223 | match source with 224 | | "[" :: t -> 225 | let q, t' = parse' [] t 226 | parse' (Quotation q :: nodes) t' 227 | | "]" :: t -> List.rev nodes, t 228 | | [] -> List.rev nodes, [] 229 | | token :: t -> 230 | let isNum, num = Int16.TryParse token 231 | let len = token.Length 232 | if isNum then parse' (Number num :: nodes) t // 123 becomes Number 233 | elif len >= 3 && token.[0] = '(' && token.[len - 1] = ')' then 234 | match token.Substring(1, token.Length - 2) |> Int16.TryParse with 235 | | true, addr -> parse' (Address addr :: nodes) t // (123) becomes Call 236 | | false, _ -> parse' (Token token :: nodes) t // not a call 237 | else parse' (Token token :: nodes) t // otherwise remains a Token 238 | source |> lex |> List.ofSeq |> parse' [] |> fst // TODO: unmatched brackets 239 | 240 | (* Below is the assembler, taking parsed syntax trees (Node) and producing the final bytecode. 241 | Tokens are looked up in the dictionary and are *eagerly* reified. Addresses and Numbers are 242 | assembled straightforwardly. Quotations have a special case when they contain a single Word. 243 | In this case, we emit the Word address directly rather than a Quote 1 Word Return; saving a few 244 | bytes and also making expressions like 'foo setLoop valid for immediate execution (otherwise 245 | you'd be setting a temporarily allocated anonymous quotation address as the loop word. *) 246 | 247 | let eagerAssemble dict parsed = 248 | let rec assemble' bytecode = function 249 | | Token tok :: t -> 250 | match findWord tok dict with 251 | | Some word -> 252 | let code = word.Code.Force() |> List.ofSeq 253 | assemble' (code :: bytecode) t 254 | | None -> sprintf "Unrecognized token: %s" tok |> failwith 255 | | Address addr :: t -> 256 | let call = [Word (int16 addr, "")] |> assembleBrief dict 257 | assemble' (call :: bytecode) t // TODO: address to name 258 | | Number n :: t -> assemble' (assembleBrief dict [Literal n] :: bytecode) t 259 | | Quotation quote :: t -> 260 | let q = assemble' [] quote 261 | match disassembleBrief dict q with 262 | | [Word (addr, _)] -> // special case for single secondary 263 | assemble' (assembleBrief dict [Literal addr] :: bytecode) t // emit address directly 264 | | _ -> 265 | let q' = assembleBrief dict [Quote (1 + Array.length q |> byte)] 266 | let ret = assembleBrief dict [Return] 267 | assemble' (ret :: (q' @ List.ofArray q) :: bytecode) t 268 | | [] -> bytecode |> List.rev |> List.concat |> Array.ofList 269 | assemble' [] parsed 270 | 271 | let eagerCompile dict = parse >> eagerAssemble dict 272 | 273 | (* The essence of subroutine threaded code is that long sequences of code are aggressively factored 274 | out into definitions and replaced with two-byte calls. Lazily generated definition are "shrunken" 275 | in this way upon first use. They are assumed to be sent down to the MCU as a definition at the 276 | given address (addr argument). 277 | 278 | There is no need to shrink code that is already only two bytes (not including the Return 279 | instruction). In this case we return it as is to be inlined. This allows small definitions such 280 | as aliases for individual Brief instructions, for 8-bit numbers or or tiny definitions such as 281 | [dup *] 'sq define to be made with no cost. Asside from this, the stack machine mechanics of the 282 | VM make subroutine calls extreemely light-weight so relentless factoring is highly encouraged. *) 283 | 284 | let shrink dict addr (code : byte array) = 285 | let ret = assembleBriefInstruction dict Return |> Array.ofList 286 | let len = code.Length 287 | if len = 0 then [||], addr, [||] // empty 288 | elif len <= 2 then code.[0..len-1], addr, [||] // inline 289 | else [|addr >>> 8 |> byte ||| 0x80uy; byte addr|], addr + len + 1, Array.append code ret 290 | 291 | (* We can't eagerly reify definitions because they may depend on other definitions that have yet to 292 | be shrunken (which implies sending them down to the MCU). We could easily cause a cascading effect 293 | in which many definitions suddenly need to be reified in order to know their addresses to embed as 294 | calls. 295 | 296 | To avoid this, as we've talked about in the dictionary mechanics above, we store bytecode in the 297 | dictionary as a Lazy. Forcing these lazy values causes compilation, assembly at that 298 | moment. We call the compiler/assembler/translator function a 'generator', a unit -> byte array 299 | function. *) 300 | 301 | let lazyGenerate dict generator address pending = lazy ( 302 | let code, addr, def = generator () |> shrink dict !address 303 | address := addr 304 | pending := Seq.append !pending def 305 | code) 306 | 307 | let lazyCompile dict source = lazyGenerate dict (fun () -> eagerCompile dict source) 308 | 309 | let lazyAssemble dict ast = lazyGenerate dict (fun () -> eagerAssemble dict ast) 310 | 311 | (* Below is a function to initialize a dictionary with mappings for all of the Brief primitives 312 | as well as a library of useful words which can be thought of as being part of the language. *) 313 | 314 | let initDictionary dict address pending = 315 | let defineBytecode (b, w, c) = define dict (Some b) w None (lazy [|byte c|]) 316 | List.iter defineBytecode 317 | [Return, "(return)", 0 // - (from return) 318 | EventHeader, "event{", 4 // id - 319 | EventBody8, "cdata", 5 // val - 320 | EventBody16, "data", 6 // val - 321 | EventFooter, "}event", 7 // - 322 | Event, "event", 8 // val id - 323 | Fetch8, "c@", 9 // addr - val 324 | Store8, "c!", 10 // val addr - 325 | Fetch16, "@", 11 // addr - val 326 | Store16, "!", 12 // val addr - 327 | Add, "+", 13 // y x - sum 328 | Subtract, "-", 14 // y x - diff 329 | Multiply, "*", 15 // y x - prod 330 | Divide, "/", 16 // y x - quot 331 | Modulus, "mod", 17 // y x - rem 332 | And, "and", 18 // y x - result 333 | Or, "or", 19 // y x - result 334 | ExclusiveOr, "xor", 20 // y x - result 335 | Shift, "shift", 21 // x bits - result 336 | Equal, "=", 22 // y x - pred 337 | NotEqual, "<>", 23 // y x - pred 338 | Greater, ">", 24 // y x - pred 339 | GreaterOrEqual, ">=", 25 // y x - pred 340 | Less, "<", 26 // y x - pred 341 | LessOrEqual, "<=", 27 // y x - pred 342 | Not, "not", 28 // x - result 343 | Negate, "neg", 29 // x - -x 344 | Increment, "1+", 30 // x - x+1 345 | Decrement, "1-", 31 // x - x-1 346 | Drop, "drop", 32 // x - 347 | Duplicate, "dup", 33 // x - x x 348 | Swap, "swap", 34 // y x - x y 349 | Pick, "pick", 35 // n - val 350 | Roll, "roll", 36 // n - 351 | Clear, "clear", 37 // - 352 | Push, "push", 38 // x - (to return) 353 | Pop, "pop", 39 // - x (from return) 354 | Peek, "peek", 40 // - x (from return) 355 | Forget, "forget", 41 // addr - 356 | Call, "call", 42 // addr - 357 | Choice, "choice", 43 // q p - 358 | If, "if", 44 // q - 359 | LoopTicks, "loopTicks", 45 // - 360 | SetLoop, "setLoop", 46 // addr - 361 | StopLoop, "stopLoop", 47 // - 362 | Reset, "(reset)", 48 // - 363 | PinMode, "pinMode", 49 // mode pin - 364 | DigitalRead, "digitalRead", 50 // pin - val 365 | DigitalWrite, "digitalWrite", 51 // val pin - 366 | AnalogRead, "analogRead", 52 // pin - val 367 | AnalogWrite, "analogWrite", 53 // val pin - 368 | AttachISR, "attachISR", 54 // addr i mode - 369 | DetachISR, "detachISR", 55 // i - 370 | Milliseconds, "milliseconds", 56 // - millis 371 | PulseIn, "pulseIn", 57] // val pin - duration 372 | 373 | let library (w, d) = lazyCompile dict d address pending |> define dict None w None 374 | List.iter library 375 | ["square" , "dup *" 376 | "cube" , "dup dup * *" 377 | "over" , "1 pick" 378 | "rot" , "2 roll" 379 | "-rot" , "rot rot" 380 | "nip" , "swap drop" 381 | "tuck" , "swap over" 382 | "abs" , "dup 0 < 'neg if" 383 | "2dup" , "over over" 384 | "min" , "2dup > 'swap if drop" 385 | "max" , "2dup < 'swap if drop" 386 | "nor" , "or not" 387 | "xnor" , "xor not" 388 | "+!" , "dup push @ + pop !" 389 | "-!" , "dup push @ swap - pop !" 390 | "clamp" , "dup neg rot max min" 391 | "sign" , "-1 max 1 min" // 1 clamp 392 | "true" , "-1" 393 | "high" , "-1" 394 | "on" , "-1" 395 | "false" , "0" 396 | "low" , "0" 397 | "off" , "0" 398 | "input" , "0" 399 | "output" , "1" 400 | "change" , "1" 401 | "falling" , "2" 402 | "rising" , "3" 403 | "lastms" , "'(return)" // variable 404 | "ms" , "milliseconds 32767 and" // wrapping at 32767 instead of going negative 405 | "ellapsed" , "ms lastms @ - abs" 406 | "resetEllapsed", "ms lastms !" 407 | "ontick" , "ellapsed <= [resetEllapsed call] 'drop choice" // e.g. [foo bar] 10 ontick 408 | "dip" , "swap push call pop" // abq-xb 409 | "when" , "[[] choice]" 410 | "unless" , "[[] swap choice]" 411 | "apply" , "[true swap when]" 412 | "sum" , "[0 [+] fold]" 413 | "2drop" , "[drop drop]" 414 | "3drop" , "[drop drop drop]" 415 | "neg" , "[0 swap -]" 416 | "abs" , "[dup 0 < [neg] when]" 417 | "nip" , "[swap drop]" 418 | "2nip" , "[[2drop] dip]" 419 | "over" , "[[dup] dip swap]" 420 | "2dup" , "[over over]" 421 | "pick" , "[[over] dip swap]" 422 | "3dup" , "[pick pick pick]" 423 | "dupd" , "[[dup] dip]" 424 | "swapd" , "[[swap] dip]" 425 | "rot" , "[swapd swap]" 426 | "-rot" , "[rot rot]" 427 | "2dip" , "[swap [dip] dip]" 428 | "3dip" , "[swap [2dip] dip]" 429 | "4dip" , "[swap [3dip] dip]" 430 | "keep" , "[dupd dip]" 431 | "2keep" , "[[2dup] dip 2dip]" 432 | "3keep" , "[[3dup] dip 3dip]" 433 | "bi" , "[[keep] dip apply]" 434 | "2bi" , "[[2keep] dip apply]" 435 | "3bi" , "[[3keep] dip apply]" 436 | "tri" , "[[keep] 2dip [keep] dip apply]" 437 | "2tri" , "[[2keep] 2dip [2keep] dip apply]" 438 | "3tri" , "[[3keep] 2dip [3keep] dip apply]" 439 | "bi*" , "[[dip] dip apply]" 440 | "2bi*" , "[[2dip] dip apply]" 441 | "tri*" , "[[2dip] 2dip [dip] dip apply]" 442 | "2tri*" , "[[4dip] 2dip [2dip] dip apply]" 443 | "bi@" , "[dup 2dip apply]" 444 | "2bi@" , "[dup 3dip apply]" 445 | "tri@" , "[dup 3dip dup 2dip apply]" 446 | "2tri@" , "[dup 4dip apply]" 447 | "both?" , "[bi@ and]" 448 | "either?" , "[bi@ or]" ] 449 | -------------------------------------------------------------------------------- /src/Brief.cpp: -------------------------------------------------------------------------------- 1 | #include "Brief.h" 2 | 3 | namespace brief 4 | { 5 | /* The Brief VM revolves around a pair of stacks and a block of memory serving as a dictionary of 6 | subroutines. 7 | 8 | The dictionary is typically 1Kb. This is where Brief byte code is stored and executed. While it 9 | can technically be used as general purpose memory, the intent is to treat it as a structured 10 | space for definitions; subroutines, variables, and the like, all contiguously packed. 11 | 12 | The two stacks are each eight elements of 16-bit signed integers. They are used to store data 13 | and addresses. They are connected in that elements can be popped from the top of one and pushed 14 | to the top of the other. 15 | 16 | One stack is used as a data stack; persisting values across instructions and subroutine calls. 17 | With very few exceptions, instructions get their operands only from the data stack. All 18 | parameter passing between subroutines is done via this stack. 19 | 20 | The other stack is used by the VM as a return stack. The program counter is pushed here before 21 | jumping into a subroutine and is popped to return. Be careful not to nest subroutines more than 22 | eight levels deep! Note that infinite tail recursion is possible none-the-less. */ 23 | 24 | // Memory (dictionary) 25 | 26 | void error(uint8_t code); // forward decl 27 | 28 | uint8_t memory[MEM_SIZE]; // dictionary (and local/arg space for IL semantics) 29 | 30 | uint8_t memget(int16_t address) // fetch with bounds checking 31 | { 32 | if (address < 0 || address >= MEM_SIZE) 33 | { 34 | error(VM_ERROR_OUT_OF_MEMORY); 35 | return 0; 36 | } 37 | 38 | return memory[address]; 39 | } 40 | 41 | void memset(int16_t address, uint8_t value) // store with bounds checking 42 | { 43 | if (address >= MEM_SIZE) 44 | { 45 | error(VM_ERROR_OUT_OF_MEMORY); 46 | } 47 | else 48 | { 49 | memory[address] = value; 50 | } 51 | } 52 | 53 | // Data stack 54 | 55 | int16_t dstack[DATA_STACK_SIZE]; // eval stack (and args in Brief semantics) 56 | 57 | int16_t* s = dstack; // data stack pointer 58 | 59 | void push(int16_t x) 60 | { 61 | if (s >= dstack + DATA_STACK_SIZE) 62 | { 63 | error(VM_ERROR_DATA_STACK_OVERFLOW); 64 | } 65 | else 66 | { 67 | *(++s) = x; 68 | } 69 | } 70 | 71 | int16_t pop() 72 | { 73 | if (s < dstack) 74 | { 75 | error(VM_ERROR_DATA_STACK_UNDERFLOW); 76 | return 0; 77 | } 78 | else 79 | { 80 | return *s--; 81 | } 82 | } 83 | 84 | // Return stack 85 | 86 | int16_t rstack[RETURN_STACK_SIZE]; // return stack (and locals in Brief) 87 | 88 | int16_t* r; // return stack pointer 89 | 90 | void rpush(int16_t x) 91 | { 92 | if (r >= rstack + RETURN_STACK_SIZE) 93 | { 94 | error(VM_ERROR_RETURN_STACK_OVERFLOW); 95 | } 96 | else 97 | { 98 | *(++r) = x; 99 | } 100 | } 101 | 102 | int16_t rpop() 103 | { 104 | if (r < rstack) 105 | { 106 | error(VM_ERROR_RETURN_STACK_UNDERFLOW); 107 | return 0; 108 | } 109 | else 110 | { 111 | return *r--; 112 | } 113 | } 114 | 115 | /* Brief instructions are single bytes with the high bit reset: 116 | 117 | 0xxxxxxx 118 | 119 | The lower seven bits become essentially an index into a function table. Each may consume 120 | and/or produce values on the data stack as well as having other side effects. Only three 121 | instructions manipulate the return stack. Two are `push` and `pop` which move values between the 122 | data and return stack. The third is (return); popping an address at which execution continues. 123 | 124 | It is extremely common to factor out redundant sequences of code into subroutines. The `call` 125 | instruction is not used for general subroutine calls. Instead, if the high bit is set then the 126 | following byte is taken and together (in little endian), with the high bit reset, they become 127 | an address to be called. 128 | 129 | 1xxxxxxxxxxxxxxx 130 | 131 | This allows 15-bit addressing to definitions in the dictionary. 132 | 133 | Upon calling, the VM pushes the current program counter to the return stack. There is a `return` 134 | instruction, used to terminate definitions, which pops the return stack to continue execution 135 | after the call. */ 136 | 137 | void (*instructions[MAX_PRIMITIVES])(); // instruction function table 138 | 139 | void bind(uint8_t i, void (*f)()) // add function to instruction table 140 | { 141 | instructions[i] = f; 142 | } 143 | 144 | int16_t p; // program counter (VM instruction pointer) 145 | 146 | int16_t pget() 147 | { 148 | return p; 149 | } 150 | 151 | void pset(int16_t pp) 152 | { 153 | p = pp; 154 | } 155 | 156 | void ret() // return instruction 157 | { 158 | p = rpop(); 159 | } 160 | 161 | void run() // run code at p 162 | { 163 | int16_t i; 164 | do 165 | { 166 | i = memget(p++); 167 | if ((i & 0x80) == 0) // instruction? 168 | { 169 | instructions[i](); // execute instruction 170 | } 171 | else // address to call 172 | { 173 | if (memget(p + 1) != 0) // not followed by return (TCO) 174 | rpush(p + 1); // return address 175 | p = ((i << 8) & 0x7F00) | memget(p); // jump 176 | } 177 | } while (p >= 0); // -1 pushed to return stack 178 | } 179 | 180 | void exec(int16_t address) // execute code at given address 181 | { 182 | r = rstack; // reset return stack 183 | rpush(-1); // causing `run()` to fall through upon completion 184 | p = address; 185 | run(); 186 | } 187 | 188 | /* As the dictionary is filled `here` points to the next available byte, while `last` points to the 189 | byte following the previously commited definition. This way the dictionary also acts as a scratch 190 | buffer; filled with event data or with "immediate mode" instructions, then rolled back to `last`. */ 191 | 192 | int16_t here; // dictionary 'here' pointer 193 | int16_t last; // last definition address 194 | 195 | /* Events are used to send unsolicited data up to the PC. Requests may cause events, but it is 196 | not a request/response model. That is, the event is always async and is not correlated with a 197 | particular request (at the protocol level). 198 | 199 | The payload is a zero- or single-byte identifier followed by an arbitrary number of data bytes. 200 | This is prefixed by a length header byte, indicating the length of the data (excluding ID). 201 | 202 | Length: 1 byte 203 | ID: 1 byte 204 | Data: n bytes (0, 1 or 2) 205 | 206 | Events may be considered simple signed scalar values generated by the event instruction. In this 207 | case the data bytes consist of 0-, 1- or 2-bytes depending on the value taken from the stack. 208 | The value 0 is transmitted as zero-length data and may be used when the ID alone is enough 209 | information to signal an event. Other values have various lengths: 210 | 211 | x = 0 0 bytes 212 | -128 >= x <= 127 1 byte 213 | otherwise 2 bytes 214 | 215 | Events may instead be hand packed records of data, such as a "heartbeat" of sensor data. This is 216 | produced using the `eventHeader` and `eventFooter` instructions. Event data may be included using 217 | `eventBody8`/`eventBody16`. */ 218 | 219 | int16_t eventBuffer = MEM_SIZE; // index into event buffer (reusing dictionary) 220 | 221 | void eventHeader() // pack event payload (ID from stack) 222 | { 223 | eventBuffer = here; // note: initially `MEM_SIZE` to cause OOM if body/footer without header 224 | memset(eventBuffer++, pop()); 225 | } 226 | 227 | void eventBody8() // append byte to packed event payload 228 | { 229 | memset(eventBuffer++, pop()); 230 | } 231 | 232 | void eventBody16() // append int16 to packed event payload 233 | { 234 | int16_t val = pop(); 235 | memset(eventBuffer++, val >> 8); 236 | memset(eventBuffer++, val); 237 | } 238 | 239 | void eventFooter() // send packed event 240 | { 241 | byte len = eventBuffer - here; 242 | Serial.write(len - 1); 243 | for (int16_t i = 0; i < len; i++) 244 | { 245 | Serial.write(memget(here + i)); 246 | } 247 | Serial.flush(); 248 | } 249 | 250 | void event(uint8_t id, int16_t val) // helper to send simple scaler events 251 | { 252 | push(id); 253 | eventHeader(); 254 | if (val != 0) 255 | { 256 | if (val >= INT8_MIN && val <= INT8_MAX) 257 | { 258 | push(val); 259 | eventBody8(); 260 | } 261 | else 262 | { 263 | push(val); 264 | eventBody16(); 265 | } 266 | } 267 | eventFooter(); 268 | } 269 | 270 | /* Several event IDs are used to notify the PC of VM activity and errors. Defined in Brief.h: 271 | 272 | ID Value Meaning 273 | 0xFF Reset None MCU reset 274 | 0xFE - VM 0 Return stack underflow 275 | 1 Return stack overflow 276 | 2 Data stack underflow 277 | 3 Data stack overflow 278 | 4 Indexed out of memory */ 279 | 280 | void error(uint8_t code) // error events 281 | { 282 | event(code, VM_EVENT_ID); 283 | } 284 | 285 | /* Below are the primitive Brief instructions; later bound in setup. All of these functions take no 286 | parameters and return nothing. Arguments and return values flow through the stack. */ 287 | 288 | void eventOp() // send event up to PC containing top stack value 289 | { 290 | int8_t id = pop(); 291 | int16_t val = pop(); 292 | event(id, val); 293 | } 294 | 295 | /* Memory `fetch`/`store` instructions. Fetches take an address from the stack and push back the 296 | contents of that address (within the dictionary). Stores take a value and an address from the 297 | stack and store the value to the address. */ 298 | 299 | inline int16_t mem16(int16_t address) // helper (not Brief instruction) 300 | { 301 | int16_t x = ((int16_t)memget(address)) << 8; 302 | return x | memget(address + 1); 303 | } 304 | 305 | void fetch8() 306 | { 307 | *s = memget(*s); 308 | } 309 | 310 | void store8() 311 | { 312 | memset(pop(), (uint8_t)pop()); 313 | } 314 | 315 | void fetch16() 316 | { 317 | int16_t a = *s; 318 | *s = mem16(a); 319 | } 320 | 321 | void store16() 322 | { 323 | int16_t a = pop(), v = pop(); 324 | memset(a, v >> 8); 325 | memset(a + 1, v); 326 | } 327 | 328 | /* Literal values are pushed to the stack by the `lit8`/`lit16` instructions. The values is a 329 | parameter to the instruction. Literals (as well as branches below) are one of the few 330 | instructions to actually have operands. This is done by consuming the bytes at the current 331 | program counter and advancing the counter to skip them for execution. */ 332 | 333 | void lit8() 334 | { 335 | push(memget(p++)); 336 | } 337 | 338 | void lit16() 339 | { 340 | push(mem16(p++)); p++; 341 | } 342 | 343 | /* Binary and unary ALU operations pop one or two values and push back one. These include basic 344 | arithmetic, bitwise operations, comparison, etc. 345 | 346 | The truth value used in Brief is all bits reset (-1) and so the bitwise `and`/`or`/`not` words 347 | serve equally well as logical operators. */ 348 | 349 | void add() 350 | { 351 | int16_t x = pop(); 352 | *s = *s + x; 353 | } 354 | 355 | void sub() 356 | { 357 | int16_t x = pop(); 358 | *s = *s - x; 359 | } 360 | 361 | void mul() 362 | { 363 | int16_t x = pop(); 364 | *s = *s * x; 365 | } 366 | 367 | void div() 368 | { 369 | int16_t x = pop(); 370 | *s = *s / x; 371 | } 372 | 373 | void mod() 374 | { 375 | int16_t x = pop(); 376 | *s = *s % x; 377 | } 378 | 379 | void andb() 380 | { 381 | int16_t x = pop(); 382 | *s = *s & x; 383 | } 384 | 385 | void orb() 386 | { 387 | int16_t x = pop(); 388 | *s = *s | x; 389 | } 390 | 391 | void xorb() 392 | { 393 | int16_t x = pop(); 394 | *s = *s ^ x; 395 | } 396 | 397 | void shift() 398 | { 399 | int16_t x = pop(); // negative values shift left, positive right 400 | if (x < 0) *s = *s << -x; 401 | else *s = *s >> x; 402 | } 403 | 404 | inline int16_t boolval(int16_t b) // helper (not Brief instruction) 405 | { 406 | // true is all bits on (works for bitwise and logical operations alike) 407 | return b ? 0xFFFF : 0; 408 | } 409 | 410 | void eq() 411 | { 412 | int16_t x = pop(); 413 | *s = boolval(*s == x); 414 | } 415 | 416 | void neq() 417 | { 418 | int16_t x = pop(); 419 | *s = boolval(*s != x); 420 | } 421 | 422 | void gt() 423 | { 424 | int16_t x = pop(); 425 | *s = boolval(*s > x); 426 | } 427 | 428 | void geq() 429 | { 430 | int16_t x = pop(); 431 | *s = boolval(*s >= x); 432 | } 433 | 434 | void lt() 435 | { 436 | int16_t x = pop(); 437 | *s = boolval(*s < x); 438 | } 439 | 440 | void leq() 441 | { 442 | int16_t x = pop(); 443 | *s = boolval(*s <= x); 444 | } 445 | 446 | void notb() 447 | { 448 | *s = ~(*s); 449 | } 450 | 451 | void neg() 452 | { 453 | *s = -(*s); 454 | } 455 | 456 | void inc() 457 | { 458 | *s = *s + 1; 459 | } 460 | 461 | void dec() 462 | { 463 | *s = *s - 1; 464 | } 465 | 466 | /* Stack manipulation instructions */ 467 | 468 | void drop() 469 | { 470 | s--; 471 | } 472 | 473 | void dup() 474 | { 475 | push(*s); 476 | } 477 | 478 | void swap() 479 | { 480 | int16_t t = *s; 481 | int16_t* n = s - 1; 482 | *s = *n; *n = t; 483 | } 484 | 485 | void pick() // nth item to top of stack 486 | { 487 | int16_t n = pop(); 488 | push(*(s - n)); 489 | } 490 | 491 | void roll() // top item slipped into nth position 492 | { 493 | int16_t n = pop(), t = *(s - n); 494 | int16_t* i; 495 | for (i = s - n; i < s; i++) 496 | { 497 | *i = *(i + 1); 498 | } 499 | *s = t; 500 | } 501 | 502 | void clr() // clear stack 503 | { 504 | s = dstack; 505 | } 506 | 507 | /* Moving items between data and return stack. The return stack is commonly also used to store data 508 | that is local to a subroutine. It is safe to push data here to be recovered after a subroutine 509 | call. It is not safe to use it for passing data between subroutines. That is what the data stack 510 | is for. Think of arguments vs. locals. The normal way of handling locals in Brief that need to 511 | survive a call and return from another word is to store them on the return stack. */ 512 | 513 | void pushr() 514 | { 515 | rpush(pop()); 516 | } 517 | 518 | void popr() 519 | { 520 | push(rpop()); 521 | } 522 | 523 | void peekr() 524 | { 525 | push(*r); 526 | } 527 | 528 | /* Dictionary manipulation instructions: 529 | 530 | The `forget` function is a Forthism for reverting to the address of a previously defined word; 531 | essentially forgetting it and any (potentially dependent words) defined thereafter. */ 532 | 533 | void forget() // revert dictionary pointer to TOS 534 | { 535 | int16_t i = pop(); 536 | if (i < here) here = i; // don't "remember" random memory! 537 | } 538 | 539 | /* A `call` instruction pops an address and calls it; pushing the current `p` as to return. */ 540 | 541 | void call() 542 | { 543 | rpush(p); 544 | p = pop(); 545 | } 546 | 547 | /* Quotations and `choice` need some explanation. The idea behind quotations is something like 548 | an anonymous lambda and is used with some nice syntax in the Brief language. The `quote` 549 | instruction precedes a sequence that is to be treated as an embedded definition 550 | essentially. It takes a length as an operand, pushes the address of the sequence of code 551 | following and then jumps over that code. 552 | 553 | The net result is that the sequence is not executed, but its address is left on the stack 554 | for future words to call as they see fit. 555 | 556 | One primitive that makes use of this is `choice` which is the idiomatic Brief conditional. 557 | It pops two addresses (likely from two quotations) along with a predicate value (likely the 558 | result of some comparison or logical operations). It then executes one or the other 559 | quotation depending on the predicate. 560 | 561 | Another primitive making use of quotations is `chooseIf` (called simply `if` in Brief) which 562 | pops a predicate and a single address; calling the address if non-zero. 563 | 564 | Many secondary words in Brief also use quotation such as `bi`, `tri`, `map`, `fold`, etc. 565 | which act as higher-order functions. */ 566 | 567 | void quote() 568 | { 569 | uint8_t len = memget(p++); 570 | push(p); // address of quotation 571 | p += len; // jump over 572 | } 573 | 574 | void choice() 575 | { 576 | int16_t f = pop(), t = pop(); 577 | rpush(p); 578 | p = pop() == 0 ? f : t; 579 | } 580 | 581 | void chooseIf() 582 | { 583 | int16_t t = pop(); 584 | if (pop() != 0) 585 | { 586 | rpush(p); 587 | p = t; 588 | } 589 | } 590 | 591 | void next() 592 | { 593 | int16_t count = rpop() - 1; 594 | int16_t rel = memget(p++); 595 | if (count > 0) 596 | { 597 | rpush(count); 598 | p -= (rel + 2); 599 | } 600 | } 601 | 602 | void nop() 603 | { 604 | } 605 | 606 | /* A Brief word (address) may be set to run in the main loop. Also, a loop counter is 607 | maintained for use by conditional logic (throttling for example). */ 608 | 609 | int16_t loopword = -1; // address of loop word 610 | 611 | int16_t loopIterations = 0; // number of iterations since 'setup' (wraps) 612 | 613 | void loopTicks() 614 | { 615 | push(loopIterations & 0x7FFF); 616 | } 617 | 618 | void setLoop() 619 | { 620 | loopIterations = 0; 621 | loopword = pop(); 622 | } 623 | 624 | void stopLoop() 625 | { 626 | loopword = -1; 627 | } 628 | 629 | /* Upon first connecting to a board, the PC will execute a reset so that assumptions about 630 | dictionary contents and such hold true. */ 631 | 632 | void resetBoard() // likely called initialy upon connecting from PC 633 | { 634 | clr(); 635 | here = last = 0; 636 | loopword = -1; 637 | loopIterations = 0; 638 | } 639 | 640 | /* Here begins all of the Arduino-specific instructions. 641 | 642 | Starting with basic setup and reading/write to GPIO pins. Note we treat `HIGH`/`LOW` values as 643 | Brief-style booleans (-1 or 0) to play well with the logical and conditional operations. */ 644 | 645 | void pinMode() 646 | { 647 | ::pinMode(pop(), pop()); 648 | } 649 | 650 | void digitalRead() 651 | { 652 | push(::digitalRead(pop()) ? -1 : 0); 653 | } 654 | 655 | void digitalWrite() 656 | { 657 | ::digitalWrite(pop(), pop() == 0 ? LOW : HIGH); 658 | } 659 | 660 | void analogRead() 661 | { 662 | push(::analogRead(pop())); 663 | } 664 | 665 | void analogWrite() 666 | { 667 | ::analogWrite(pop(), pop()); 668 | } 669 | 670 | /* I2C support comes from several instructions, essentially mapping composable, zero-operand 671 | instructions to functions in the Arduino library: 672 | 673 | http://arduino.cc/en/Reference/Wire 674 | 675 | Brief words (addresses/quotations) may be hooked to respond to Wire events. */ 676 | 677 | void wireBegin() 678 | { 679 | Wire.begin(); // join bus as master (slave not supported) 680 | } 681 | 682 | void wireRequestFrom() 683 | { 684 | Wire.requestFrom(pop(), pop()); 685 | } 686 | 687 | void wireAvailable() 688 | { 689 | push(Wire.available()); 690 | } 691 | 692 | void wireRead() 693 | { 694 | while (Wire.available() < 1); 695 | push(Wire.read()); 696 | } 697 | 698 | void wireBeginTransmission() 699 | { 700 | Wire.beginTransmission((uint8_t)pop()); 701 | } 702 | 703 | void wireWrite() 704 | { 705 | Wire.write((uint8_t)pop()); 706 | } 707 | 708 | void wireEndTransmission() 709 | { 710 | Wire.endTransmission(); 711 | } 712 | 713 | int16_t onReceiveWord = -1; 714 | 715 | void wireOnReceive(int16_t count) 716 | { 717 | if (onReceiveWord != -1) 718 | { 719 | push(count); 720 | exec(onReceiveWord); 721 | } 722 | } 723 | 724 | void wireSetOnReceive() 725 | { 726 | onReceiveWord = pop(); 727 | Wire.onReceive(wireOnReceive); 728 | } 729 | 730 | int16_t onRequestWord = -1; 731 | 732 | void wireOnRequest() 733 | { 734 | if (onRequestWord != -1) 735 | { 736 | exec(onRequestWord); 737 | } 738 | } 739 | 740 | void wireSetOnRequest() 741 | { 742 | onRequestWord = pop(); 743 | Wire.onRequest(wireOnRequest); 744 | } 745 | 746 | /* Brief word addresses (or quotations) may be set to run upon interrupts. For more info on 747 | the argument values and behavior, see: 748 | 749 | http://arduino.cc/en/Reference/AttachInterrupt 750 | 751 | We keep a mapping of up to MAX_INTERRUPTS (6) words. */ 752 | 753 | int16_t isrs[MAX_INTERRUPTS]; 754 | 755 | void interrupt(int16_t n) // helper (not Brief instruction) 756 | { 757 | int16_t w = isrs[n]; 758 | if (w != -1) exec(w); 759 | } 760 | 761 | void interrupt0() // helper (not Brief instruction) 762 | { 763 | interrupt(0); 764 | } 765 | 766 | void interrupt1() // helper (not Brief instruction) 767 | { 768 | interrupt(1); 769 | } 770 | 771 | void interrupt2() // helper (not Brief instruction) 772 | { 773 | interrupt(2); 774 | } 775 | 776 | void interrupt3() // helper (not Brief instruction) 777 | { 778 | interrupt(3); 779 | } 780 | 781 | void interrupt4() // helper (not Brief instruction) 782 | { 783 | interrupt(4); 784 | } 785 | 786 | void interrupt5() // helper (not Brief instruction) 787 | { 788 | interrupt(5); 789 | } 790 | 791 | void interrupt6() // helper (not Brief instruction) 792 | { 793 | interrupt(6); 794 | } 795 | 796 | void attachISR() 797 | { 798 | uint8_t mode = pop(); 799 | uint8_t interrupt = pop(); 800 | isrs[interrupt] = pop(); 801 | switch (interrupt) 802 | { 803 | case 0 : attachInterrupt(0, interrupt0, mode); 804 | case 1 : attachInterrupt(1, interrupt1, mode); 805 | case 2 : attachInterrupt(2, interrupt2, mode); 806 | case 3 : attachInterrupt(3, interrupt3, mode); 807 | case 4 : attachInterrupt(4, interrupt4, mode); 808 | case 5 : attachInterrupt(5, interrupt5, mode); 809 | case 6 : attachInterrupt(6, interrupt6, mode); 810 | } 811 | } 812 | 813 | void detachISR() 814 | { 815 | int interrupt = pop(); 816 | isrs[interrupt] = -1; 817 | detachInterrupt(interrupt); 818 | } 819 | 820 | /* A couple of stragglers... */ 821 | 822 | void milliseconds() 823 | { 824 | push(millis()); 825 | } 826 | 827 | void pulseIn() 828 | { 829 | push(::pulseIn(pop(), pop())); 830 | } 831 | 832 | /* The Brief VM needs to be hooked into the main setup and loop on the hosting project. 833 | A minimal *.ino would contain something like: 834 | 835 | #include 836 | 837 | void setup() 838 | { 839 | brief::setup(); 840 | } 841 | 842 | void loop() 843 | { 844 | brief::loop(); 845 | } 846 | 847 | Brief setup binds all of the instruction functions from above. After setup, the hosting 848 | project is free to bind its own custom functions as well! 849 | 850 | An example of this could be to add a `delayMillis` instruction. Such an instruction is not 851 | included in the VM to discourage blocking code, but you're free to add whatever you like: 852 | 853 | void delayMillis() 854 | { 855 | delay((int)brief::pop()); 856 | } 857 | 858 | void setup() 859 | { 860 | brief::setup(19200); 861 | brief::bind(100, delayMillis); 862 | } 863 | 864 | This adds the new instruction as opcode 100. You can then give it a name and tell the compiler 865 | about it with `compiler.Instruction("delay", 100)` in PC-side code or can tell the Brief 866 | interactive about it with `100 'delay instruction`. This is the extensibility story for Brief. 867 | 868 | Notice that custom instruction function may retrieve and return values via the 869 | `brief::pop()` and `brief::push()` functions, as well as raise errors with 870 | `brief::error(uint8_t code)`. */ 871 | 872 | void setup() 873 | { 874 | Serial.begin(19200); // assumed by interactive 875 | resetBoard(); 876 | 877 | bind(0, ret); 878 | bind(1, lit8); 879 | bind(2, lit16); 880 | bind(3, quote); 881 | bind(4, eventHeader); 882 | bind(5, eventBody8); 883 | bind(6, eventBody16); 884 | bind(7, eventFooter); 885 | bind(8, eventOp); 886 | bind(9, fetch8); 887 | bind(10, store8); 888 | bind(11, fetch16); 889 | bind(12, store16); 890 | bind(13, add); 891 | bind(14, sub); 892 | bind(15, mul); 893 | bind(16, div); 894 | bind(17, mod); 895 | bind(18, andb); 896 | bind(19, orb); 897 | bind(20, xorb); 898 | bind(21, shift); 899 | bind(22, eq); 900 | bind(23, neq); 901 | bind(24, gt); 902 | bind(25, geq); 903 | bind(26, lt); 904 | bind(27, leq); 905 | bind(28, notb); 906 | bind(29, neg); 907 | bind(30, inc); 908 | bind(31, dec); 909 | bind(32, drop); 910 | bind(33, dup); 911 | bind(34, swap); 912 | bind(35, pick); 913 | bind(36, roll); 914 | bind(37, clr); 915 | bind(38, pushr); 916 | bind(39, popr); 917 | bind(40, peekr); 918 | bind(41, forget); 919 | bind(42, call); 920 | bind(43, choice); 921 | bind(44, chooseIf); 922 | bind(45, loopTicks); 923 | bind(46, setLoop); 924 | bind(47, stopLoop); 925 | bind(48, resetBoard); 926 | bind(49, pinMode); 927 | bind(50, digitalRead); 928 | bind(51, digitalWrite); 929 | bind(52, analogRead); 930 | bind(53, analogWrite); 931 | bind(54, attachISR); 932 | bind(55, detachISR); 933 | bind(56, milliseconds); 934 | bind(57, pulseIn); 935 | bind(58, next); 936 | bind(59, nop); 937 | 938 | for (int16_t i = 0; i < MAX_INTERRUPTS; i++) 939 | { 940 | isrs[i] = -1; 941 | } 942 | 943 | event(BOOT_EVENT_ID, 0); // boot event 944 | } 945 | 946 | /* The payload from the PC to the MCU is in the form of Brief code. A header byte indicates 947 | the length and whether the code is to be executed immediately (0x00) or appended to the 948 | dictionary as a new definition (0x01). 949 | 950 | A dictionary pointer is maintained at the MCU. This pointer always references the first 951 | available free byte of dictionary space (beginning at address 0x0000). Each definition sent to 952 | the MCU is appended to the end of the dictionary and advances the pointer. The bottom end of the 953 | dictionary space is used for arguments (mainly for IL, not idiomatic Brief). 954 | 955 | If code is a definition then it is expected to already be terminated by a `return` instruction (if 956 | appropriate) and so we do nothing at all; just leave it in place and leave the `here` pointer 957 | alone. 958 | 959 | If code is to be executed immediately then a return instruction is appended and exec(...) is 960 | called on it. The dictionary pointer (`here`) is restored; reclaiming this memory. */ 961 | 962 | void loop() 963 | { 964 | if (Serial.available()) 965 | { 966 | int8_t b = Serial.read(); 967 | bool isExec = (b & 0x80) == 0x80; 968 | int8_t len = b & 0x7f; 969 | for (; len > 0; len--) 970 | { 971 | while(!Serial.available()); 972 | memset(here++, Serial.read()); 973 | } 974 | 975 | if (isExec) 976 | { 977 | memset(here++, 0); // ensure return 978 | here = last; 979 | exec(here); 980 | } 981 | else 982 | { 983 | last = here; 984 | } 985 | } 986 | 987 | if (loopword >= 0) 988 | { 989 | exec(loopword); 990 | loopIterations++; 991 | } 992 | } 993 | } 994 | -------------------------------------------------------------------------------- /extras/Documents/README.md: -------------------------------------------------------------------------------- 1 | A Brief Introduction 2 | ==== 3 | 4 | # Abstract 5 | 6 | Brief is a scriptable firmware and protocol for interfacing hardware with .NET libraries and for running real time control loops. 7 | 8 | ![Teensy](./Media/Teensy.jpg) 9 | 10 | # Introduction 11 | 12 | Brief is so easy to use that you may be entirely unaware of it. Perhaps the robotics hardware you’ve chosen for your project is using the Brief firmware and is exposed as a USB device under Windows with interfacing libraries already available. In this case you’ve already been working at a much higher level without a care in the world about microcontrollers and hardware interfacing. This is the intended beauty of the system. The introduction here is for those of you who, out of curiosity or need, want to dive in and learn the inner workings. We think you’ll find it very interesting and uniquely simple and powerful. 13 | 14 | It is comprised of the following: 15 | 16 | * VM – a tiny stack machine running on the MCU. 17 | * Protocol – an extensible and composable set of commands and events. 18 | * Language – a Forth-like interactive scripting language compiled for the VM. 19 | * Interactive – console for interactive experimentation and development. 20 | * Interface – from managed code (not in this fork, [but here](https://github.com/ashleyf/brief/tree/gh-pages/embedded)). 21 | * IL Translator - JIT compiler from CLR to Brief bytecode (again, not in this fork, [but here](https://github.com/ashleyf/brief/tree/gh-pages/embedded)). 22 | 23 | You will find that there is absolutely nothing off-limits in the Brief system. It purposely leaves the “wires exposed” so to speak. It is meant to be tinkered with at all levels. The interactive REPL is a wonderful way to experiment with new hardware. You can go much further (still without cracking open the firmware) by customizing the protocol with your own commands and events. You can customize the heartbeat payload. You can write your own driver libraries for new hardware. You can script the firmware with your own real time control logic. Finally, the firmware itself is open source and you’re free to extend it. You can easily do so in ways that continue to work seamlessly within the existing scriptable protocol, commands and event system. 24 | 25 | Brief is not a tool chain for programming standalone MCUs that are not connected to a PC. It is potentially possible to load byte code from flash or EEPROM rather than pushing down from a PC but this is not the intent. 26 | 27 | ## Demo 28 | 29 | Brief targets AVR and ARM MCUs with at least 16Kb of flash and 1Kb of SRAM. Here we’ll use the Teensy with an ATmega32U4; a nice chip with 32Kb flash, 2.5Kb SRAM and built-in USB. 30 | 31 | ### Setup 32 | 33 | If you’re lucky, your MCU is already running the Brief firmware. Simply connect it to the PC and note the COM port on which it’s communicating. Otherwise, you will need to flash it. 34 | 35 | #### Flashing Firmware 36 | 37 | We’re using the loader provided by PJRC: 38 | 39 | teensy_loader_cli -mmcu=atmega32u4 -w brief.hex 40 | 41 | You may also open the Brief.ino in the Arduino IDE (with TeensyDuino if you’re using that board) and Upload from there. 42 | 43 | #### Setup 44 | 45 | If you plan to modify the firmware (see Custom Instructions below) then you will need to setup your environment: 46 | 47 | 1. Install Arduino IDE 48 | 2. Install TeensyDuino 49 | 3. Install Brief library (via the Arduino Library Manager) 50 | 51 | ![Arduino Library installation](./Media/ArduinoLibrary.png) 52 | 53 | ### Interactive Console 54 | 55 | Once the firmware has been flashed, launch `Interactive.exe`, which is [built from this project](https://github.com/AshleyF/BriefEmbedded/tree/master/extras/Interactive). It is a .NET Core app and works on Linux, macOS and Windows. This is an interactive console giving you full control of the MCU. It can be used to experiment with and to even program it. 56 | 57 | Type the following (replacing 'com16 appropriately): 58 | 59 | 'com16 connect 60 | 61 | You may see in the photo above that we have a pair of LEDs on pins 0 and 1, as well as an IR sensor on pin 21 (see pinouts for the Teensy 2.0). To initialize a pin: 62 | 63 | output 0 pinMode 64 | 65 | To light up the green LED: 66 | 67 | high 0 digitalWrite 68 | 69 | The LED lights up! You can probably guess what the following does: 70 | 71 | low 0 digitalWrite 72 | 73 | If you’ve used the Arduino IDE then you recognize that what we’re doing is equivalent to `pinMode(0, OUTPUT)`, `digitalWrite(0, HIGH)`, etc. 74 | 75 | Reading sensors is equally easy: 76 | 77 | input 21 pinMode 78 | 21 analogRead 79 | 80 | This sets up the pin and reads the IR sensor, but where does the value go? It goes onto a stack on the MCU where it can be used by other commands. We can send values back to the host PC with something like: 81 | 82 | 123 event 83 | 84 | This emits the sensor value (which happens to be 49) as a PC-side event with an event ID of our choosing (123). The event command isn’t just for emitting sensor values. It will emit any value on top of the stack regardless of how it got there. This will make more sense momentarily. You see the following at the console. From the Robotics for Windows libraries this is surfaced as a regular .NET event. 85 | 86 | Event (id=123): 49 87 | 88 | If you hold your hand over the sensor and again issue: 89 | 90 | 21 analogRead 123 event 91 | 92 | You see a lower value: 93 | 94 | Event (id=123): 32 95 | 96 | Isn’t this nice? No edit/compile/flash development cycle. We can play with the sensor and interactively see the range of values we get under various conditions. Having an interactive console like this completely changes the way you work. 97 | 98 | Rather than sending values back to the host PC, you may also use them directly on the MCU. For example, a “nightlight” application which turns on the LED only when the room is dark: 99 | 100 | 21 analogRead 40 < [high 0 digitalWrite] [low 0 digitalWrite] choice 101 | 102 | We are starting to get into some complicated looking syntax (we’ll improve it below), but I’m sure you get the gist. Sequences of words within square brackets are quotations and can be thought of as anonymous lambdas pushed onto the stack. The choice word is a combinator that takes two quotations and executes one or the other of them depending on whether pin 21 was less than 40. We’re sampling pin 21 and, depending on whether it reads below 40, setting pin 0 high/low to turn on/off the LED. This sequence can be added to the main loop and run completely independently of the PC. 103 | 104 | # Overview 105 | 106 | Before diving into a very detailed bottom-up discussion (see Underview below), let’s look at a top-down overview. Here we will purposely introduce concepts without full explanation so as to cover a lot of ground quickly. This will give you a good feel for the system as a whole and will show you where we’re headed as we methodically build up from primitives later. 107 | 108 | As we’ve seen, Brief is stack VM used to facilitate a programmable protocol between the PC and MCU. The Robotics for Windows libraries use this to dynamically customize the protocol according to the attached hardware. It may also be used to run control loop logic on the MCU rather than incur the latency of sensor values up to the PC, feeding logic there which in turn pushes down actuator commands back down to the MCU. 109 | 110 | As seen in the demo, Brief is also a Forth-like language. Unless you’ve programmed in Forth before, surely the syntax looks completely foreign and somehow inverted. The truly surprising thing is that there is actually virtually no syntax at all. This is practically a direct representation of the semantics of the Brief VM. This language is used by library authors as an embedded DSL. The interactive console can also be invaluable to application writers when experimenting with new hardware. 111 | 112 | ## Stack Machines in General 113 | 114 | There are some very beautiful stack machines in hardware. Since the mid-80s though, register machines have clearly dominated and stack machines have receded into virtual machines such as the JVM, the CLR, the Ethereum VM (EVM) and the WebAssembly. 115 | 116 | In register machine instruction sets each operator comes packed with operands. An add instruction, for example, needs to know which registers and/or memory locations to sum. In a stack machine virtually all instructions take exactly zero operands. This makes the code extremely compact and also leads to abundant opportunities to factor out redundant sequences. 117 | 118 | The Brief addition instruction + compiles to just one byte. A sequence like 42 7 + produces 49 on the stack. The literals are indirectly left on the stack to be consumed. They are not operands tied to that instruction. They could have just as easily have been left there as the result of reading pins or by some previous arithmetic. 119 | 120 | In fact you can use Brief as an RPN calculator and this may be a good way to get used to the stack and to this reversed word ordering. If you’ve used an HP calculator you may already be familiar with RPN. Try this: 121 | 122 | 42 7 + 6 * 123 | 124 | It is equivalent to the infix expression (42 + 7) * 6. If what you really wanted was 42 + (7 * 6) then instead do `42 7 6 * +` or `7 6 * 42 +`. 125 | 126 | A benefit of RPN is the fact that you don’t have operator precedence or any need for parenthesis. 127 | 128 | There are commands for manipulating the stack to prepare arguments (`drop`ping, `dup`licating, `swap`ping, …), but they are used sparingly. 129 | 130 | ## Execution Model 131 | 132 | The incantations we entered are being broken into whitespace separated words. Words may be signed integer literals such as `0`, `-7`, `21`, `123`, `40`, ... They may represent constants such as `input`, `output`, `high`, `low`, … They may be Brief commands such as `pinMode`, `digitalWrite`, `analogRead`, `event`, ... Very rarely do they indicate syntactic structure. 133 | 134 | Words are executed from left to right. Some are nouns. Literals and constants are pushed onto the stack. So for example, `0 output` causes two values to be placed on the stack (`output` = `1`). Some are verbs and cause action to be taken by the MCU. The word `pinMode` consumes these two values and sets the mode. Some commands both consume and produce values on the stack. For example the phrase, `21 analogRead 123 event` will push a 21 and then `analogRead` will consume this as a parameter as well as push the pin’s analog value, then a 123 will be pushed. The tricky part is that this event ID and the analog pin value from earlier will be taken to signal an event back to the PC. 135 | 136 | This is how it goes; each word taking and/or leaving values on the stack. The concatenation of these words makes a useful expression. 137 | 138 | ## Defining New Words 139 | 140 | You may extend the built-in words with your own; defining new ones in terms of existing primitives or secondary words you’ve previously defined: 141 | 142 | '0 'green def 143 | 144 | This makes a word for the pin number to which our green LED is attached. The form for defining a word is `[…] 'foo def` as in: 145 | 146 | [1] 'red def 147 | 148 | Or, since the definition is a single word, it may be quoted with a tick (`'1 'red def`). Now we don’t have to hardcode these values and the purpose of our code is clearer. 149 | 150 | output green pinMode 151 | output red pinMode 152 | 153 | Definitions can be multi-word phrases. In fact, whenever you see a sequence of words used repeatedly it is a good candidate to be factored out. It may be going a little overboard but let’s factor out the phrase `output pinMode`. 154 | 155 | [output swap pinMode] 'outmode def 156 | 157 | It may seem strange to take a sequence expecting parameters and separate it into a new definition with the parameters missing. Our new `outmode` word expects a pin number on the stack and sets the mode. This is the elegance of the stack machine and of our syntax matching those semantics. 158 | 159 | If you have experience in functional programming you may already be comfortable with a point-free/tacit style and may think of words as being functions taking a stack and returning a stack. Then you can think of juxtaposition of words as composition of functions. The whitespace between words is the composition operator. Forth is all about composition. 160 | 161 | Back to our refactoring, the following is a little more succinct now: 162 | 163 | green outmode 164 | red outmode 165 | 166 | Let’s see what we can do about the “nightlight” example: 167 | 168 | 21 analogRead 40 < [high 0 digitalWrite] [low 0 digitalWrite] choice 169 | 170 | Let’s give a few of the phrases names: 171 | 172 | '21 'sensor def 173 | [analogRead 40 <] 'dark? def 174 | 175 | Now the same program can be written more clearly: 176 | 177 | sensor dark? 'high 'low choice green digitalWrite 178 | 179 | Getting even more tricky we may notice that the value for `high` and `low` is the same as the truth values (`true`/`false`) pushed by the `<` word. In this case, the phrase `[on] [off] choice` is almost completely redundant. We can just write the truth value directly: 180 | 181 | sensor dark? green digitalWrite 182 | 183 | This is how it goes, programming in Brief. Factoring and factoring, boiling down to essence. 184 | 185 | ## Defining a Vocabulary 186 | 187 | The style of programming in Forth is to treat it as a programmable programming language. Words layered on words, layered on words; raising the vocabulary up to your problem domain so that you can talk about it in its own terms. This is more of a language oriented approach in which the whole language bends toward your application domain rather than the traditional object oriented approach in which only the type system bends. This idea becomes clear later when we get into authoring defining words; words that define words – much like Lisp-style macros. 188 | 189 | To get just an ever so tiny glimpse, let’s define a language for controlling a pair of motors. Without getting into the details, the following works with the Sparkfun MonsterMoto controller board: 190 | 191 | [5 7 8] 'left def 192 | [6 4 9] 'right def 193 | 194 | [swap digitalWrite] 'pin def 195 | 'analogWrite 'power def 196 | 197 | [low pin high pin power] 'cw def 198 | [high pin low pin power] 'ccw def 199 | [high pin high pin 0 swap power] 'stop def 200 | 201 | The `left`/`right` words push pin numbers which, along with a power value expected on the stack, is used to cause clockwise (`cw`) or counter-clockwise (`ccw`) driving. 202 | 203 | We can further define: 204 | 205 | [dup left cw right cw] 'forward def 206 | [dup left ccw right ccw] 'backward def 207 | 208 | Now we can say phrases like: 209 | 210 | 255 left cw 211 | 100 right ccw 212 | 213 | 50 forward 214 | 10 backward 215 | 216 | left stop 217 | right stop 218 | 219 | The words work together to form essentially a little language for controlling the motors. 220 | 221 | ## Extending the Protocol 222 | 223 | Not only can definitions be thought of as extending the language, but they can be thought of as extending the protocol between the PC and MCU. 224 | 225 | These don’t just save typing. They save bytes over the wire. These definitions are being compiled and persisted in a dictionary on the MCU. They’re sent down once and then invoking them becomes a single instruction over the wire thereafter. 226 | 227 | ## Extending the Control Loop 228 | 229 | If we define our nightlight sequence as a new word: 230 | 231 | [sensor dark? green digitalWrite] 'nightlight def 232 | 233 | We can add it to the main control loop with `setLoop`: 234 | 235 | 'nightlight setLoop 236 | 237 | Now the PC is completely out of the loop (literally). The LED responds immediately as you can move your hand over the sensor and back away from the sensor. Stop the loop whenever you like with `stopLoop`. 238 | 239 | Have fun with it! 240 | 241 | [high swap digitalWrite] 'on def 242 | [low swap digitalWrite] 'off def 243 | 244 | [200 delay] 'pause def 245 | [red on green on pause red off green on pause] 'blink def 246 | 'blink setLoop 247 | 248 | Notice that we’re using a delay word to do a blocking pause. This isn’t a Brief primitive. It is a custom instruction added below in the Custom Instructions section. 249 | 250 | ## Triggered Events 251 | 252 | We can use this same mechanism to set up conditional events. Instead of the PC polling sensor values and reacting under certain conditions we can describe the conditions in Brief and have the MCU do the filtering and signal the PC. 253 | 254 | [sensor dark? [123 456 event] if] 'signalWhenDark def 255 | 'signalWhenDark setLoop 256 | 257 | In this way the sensor polling can happen at a hundreds of KHz frequency until it needs to report to the PC over USB. 258 | 259 | ## Custom Heartbeat 260 | 261 | We can use a loop word to provide an unsolicited stream of sensor data. 262 | 263 | [sensor analogRead 123 event] 'streamLightData def 264 | 265 | This may completely overwhelm the PC however. Instead you may want to throttle it to every 100th loop iteration or something similar: 266 | 267 | [loopTicks 100 mod 0 = [streamLightData] if] 'throttledData def 268 | 269 | We’ll get deeper into the words being used here, but if you want to pack multiple values into a single event (which comes in as an ID-tagged byte array at the PC) then you can do something like the following: 270 | 271 | [123 6 event{ 19 analogRead data 272 | 20 analogRead data 273 | 21 analogRead data }event] 'packedData def 274 | 275 | By the way, the multi-line formatting is just for readability. Any whitespace between words will do. 276 | 277 | A packed heartbeat can reasonably be expected to achieve frequencies approaching 1KHz if needed. 278 | 279 | ## Attaching Interrupts 280 | 281 | You can attach Brief words as interrupt routines with `attachInterrupt` which, like `setLoop`, expects the address of a word to attach. 282 | 283 | For example, to turn on the LED when interrupt zero signals a change you can simply say: 284 | 285 | [red on] 'ontrigger def 286 | 'ontrigger 0 change attachInterrupt 287 | 288 | To detach just say `0 detachInterrupt`. 289 | 290 | Other triggers include low, rising or falling pin values, timers, etc. 291 | 292 | # Underview 293 | 294 | So far we have been exploring Brief in a sparse top-down fashion. Now that you have the gist of the system and where we’re headed, here is a thorough bottom-up discovery of Brief. 295 | 296 | ## Brief VM 297 | 298 | The Brief VM is a stack machine executing single-byte instructions. There are some very beautiful stack machines in hardware, but since the mid-80s register machines have clearly dominated and stack machines have receded into virtual machines such as the JVM, the CLR, the Ethereum VM (EVM) and the WebAssembly. 299 | 300 | ### Two Stacks and a Dictionary 301 | 302 | The Brief VM revolves around a pair of stacks and a block of memory serving as a dictionary of subroutines. 303 | 304 | The dictionary is typically 1Kb. This is where Brief byte code is stored and executed. While it can technically be used as general purpose memory, the intent is to treat it as a structured space for definitions; subroutines, variables, and the like, all contiguously packed. 305 | 306 | The two stacks are each eight elements of 16-bit signed integers. They are used to store data and addresses. They are connected in that elements can be popped from the top of one and pushed to the top of the other. They are circular with no concept of overflow or underflow. When too many elements are pushed then the oldest are overwritten. When too many elements are popped they begin to repeat. Often elements can be abandoned on the stack rather than waste cycles removing them. 307 | 308 | One stack is used as a data stack; persisting values across instructions and subroutine calls. With very few exceptions, instructions get their operands only from the data stack. All parameter passing between subroutines is done via this stack. 309 | 310 | The other stack is used by the VM as a return stack. The program counter is pushed here before jumping into a subroutine and is popped to return. Be careful not to nest subroutines more than eight levels deep! It should be noted that infinite tail recursion is possible none the less (final calls become jumps and don't push a return address). 311 | 312 | The return stack is commonly also used to store data that is local to a subroutine. It is safe to push data here to be recovered after a subroutine call. It is not safe to use it for passing data between subroutines. That is what the data stack is for. Think of arguments vs. locals. 313 | 314 | ### Zero Operand Instructions 315 | 316 | In a register machine each operator comes packed with operands. An add instruction, for example, needs to know which registers and/or memory locations to sum. In a stack machine virtually all instructions take exactly zero operands. This makes the code extremely compact and more composable. Composability is the key. 317 | 318 | Brief instructions are single bytes with the high bit reset: 319 | 320 | | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | 321 | | --- | --- | --- | --- | --- | --- | --- | --- | 322 | | 0 | x | x | x | x | x | x | x | 323 | 324 | The lower seven bits become essentially an index into a function table. Each may consume and/or produce values on the data stack as well as having other side effects. Only three instructions manipulate the return stack. Two are `push` and `pop` which move values between the data and return stack. The third is `(return)` which consumes an address at which execution continues. 325 | 326 | ### Very Efficient Subroutines 327 | 328 | You will see that it is extremely common to factor out redundant sequences of code into subroutines. There is no “call” instruction. Instead, if the high bit is set then the following byte is taken together (in little endian), with the high bit reset, as an address to be called. 329 | 330 | | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | 331 | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | 332 | | 1 | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | 333 | 334 | This allows 15-bit addressing to definitions in the dictionary. 335 | 336 | Upon calling, the VM pushes the current program counter to the return stack. There is a `(return)` instruction, used to terminate definitions, which pops the return stack to continue execution after the call. 337 | 338 | ## Brief Protocol 339 | 340 | ### Code 341 | 342 | The payload from the PC to the MCU is in the form of Brief code. A leading bit of the first byte indicates whether the code is to be executed immediately (1) or appended to the dictionary as a new definition (0). 343 | 344 | A dictionary pointer is maintained at the MCU. This pointer always references the first available free byte of dictionary space (beginning at address 0x0000). Each definition sent to the MCU is appended to the end of the dictionary and advances the pointer. 345 | 346 | It is very likely that definitions end with a `(return)` instruction as the intent is to use the address at which the definition starts as the target of subsequent subroutine calls. 347 | 348 | Code sent for immediate execution is not persisted in the dictionary and instead is executed immediately. It is not necessary (though harmless) to end code send for immediate execution with a `(return)`. 349 | 350 | ### Events 351 | 352 | The payload to the PC contains events from the MCU. These are comprised of a length byte, followed by an ID byte, followed by data payload. The ID and payload are determined in Brief code send down to raise then event. A couple of IDs have special meaning (see below). 353 | 354 | To see bytecode, enter `trace` at the interactive prompt. 355 | 356 | ### Scalar Events 357 | 358 | Events may be considered simple signed scalar values generated by the event instruction. In this case the data bytes consist of 0-, 1-, 2-bytes depending on the value taken from the stack. The value 0 is transmitted as zero-length data and may be used when the ID alone is enough information to signal an event. Other values have various lengths: 359 | 360 | | Range | Bytes | 361 | | --- | --- | 362 | | x = 0 | 0 bytes | 363 | | -128 ≥ x ≤ 127 | 1 byte | 364 | | othrewise | 2 bytes | 365 | 366 | For example `42 123 event` will emit a single byte value 123 as event ID 42. 367 | 368 | ### Vector Events 369 | 370 | Events may instead be hand packed records of data, such as a heartbeat of sensor data. This is produced using the event{ and }event instructions (notice that the parenthesis are part of the word tokens). For example 42 event{ will transmit the sequence number for the framing protocol along with an event ID of 42. Event data may be included using data and cdata. For example 123 cdata 456 data transmits the single-byte value 123 followed by the two-byte value 456. Finally }event emits the checksum and the end byte for the framing protocol. The PC will need to know to expect a 3-byte data payload packed this way for event ID 42. 371 | 372 | An example heartbeat loop, reporting on a pair of analog pins, could be defined by: 373 | 374 | [42 event{ 20 analogRead data 21 analogRead data }event] setLoop 375 | 376 | This is to show the underlying Brief instructions for implementing events. Normally you only need to specify the packing and leave allocation of event IDs and wiring events to callbacks on the PC side to be handled for you by an instance of IMicrocontrollerHal. 377 | 378 | ### Reserved Event IDs 379 | 380 | Several event IDs are used by the MCU to notify the PC of protocol and VM activity. Normally you deal with these at the level of APIs on an instance of IMicrocontrollerHal, but this is build atop the same event system: 381 | 382 | | ID | Value | Meaning | 383 | | --- | --- | --- | 384 | | 0xFF – Reset | None | MCU reset | 385 | | 0xFD - VM | 0 | Return stack underflow | 386 | | | 1 | Return stack overflow | 387 | | | 2 | Data stack underflow | 388 | | | 3 | Data stack overflow | 389 | | | 4 | Indexed out of memory | 390 | 391 | ### Primitive Instructions 392 | 393 | #### Literals 394 | 395 | Literal values are pushed to the data stack with `lit8`/`lit16` instructions. These are followed by a 1- or 2-byte operand value as a parameter to the instruction. Literals (as well as branches below) are one of the few instructions to actually have operands. This is done by consuming the bytes at the current program counter and advancing the counter to skip them for execution. 396 | 397 | #### Branches 398 | 399 | Relative branching is accomplished by `branch`/`zbranch` instructions. Conditional and unconditional branching is done by relative offsets as a parameter to the instruction (following byte). These (like literals) are among the few instructions with operands; in this case to save code size by not requiring a preceeding literal. 400 | 401 | Notice that there is only the single conditional branch instruction. There are no 'branch if greater', 'branch if equal', etc. Instead the separate comparison instructions above are used as the preceding predicate. 402 | 403 | #### Quotations, Choice and If 404 | 405 | Quotations, `choice` and `if` need some explanation: The idea behind quotations is something like an anonymous lambda and is used with some nice syntax in the Brief language. The `(quote)` instruction precedes a sequence that is to be treated as an embedded definition essentially. It takes a length as an operand, pushes the address of the sequence of code following and then jumps over that code. The bytecode is expected to be terminated by a `(return)` and so the address on the stack is safe to be called. Quotations like these are used in combination with `choice`, `if`, `setLoop`, etc. 406 | 407 | The net result is that the sequence is not executed, but its address is left on the stack for future words to call as they see fit. 408 | 409 | One primitive that makes use of this is `choice` which is the idiomatic Brief conditional. It pops two addresses (likely from two quotations) along with a predicate value (likely the result of some comparison or logical operations). It then executes one or the other quotation depending on the predicate. 410 | 411 | Another primitive making use of quotations is `if` which pops a predicate and a single address; calling the address if non-zero. 412 | 413 | Many secondary words in Brief also use quotation such as `bi`, `tri`, `map`, `fold`, etc. which act as higher-order functions applying. 414 | 415 | #### Events 416 | 417 | Events may be considered simple signed scalar values generated by the event instruction. In this case the data bytes consist of 0-, 1- or 2-bytes depending on the value taken from the stack (see Events section below). 418 | 419 | Events may instead be hand packed records of data, such as a heartbeat of sensor data. This is produced using the `event{` and `}event` instructions along with `data` and `cdata` instructions between. 420 | 421 | #### Memory Fetch/Store 422 | 423 | Memory fetches take an address from the stack and push back the contents of that address (within the dictionary). Stores take a value and an address from the stack and store the value to the address. They come in single- and two-byte (little endian) variations. 424 | 425 | #### ALU Operations 426 | 427 | Binary and unary ALU operations pop one or two values and push back one. These include basic arithmetic, bitwise operations, comparison, etc. 428 | 429 | One interesting thing to note is that the truth values used in Brief are zero (0) for false as you’d expect but negative one (-1) for true. This is all bits on which unifies bitwise and logical operations. That is, there is a single set of `and`/`or`/`xor`/`not` instructions and they can be considered bitwise or logical as you wish. 430 | 431 | #### Return Stack Operations 432 | 433 | Aside from the usual data stack manipulation instructions (`drop`, `dup`, `swap`, `pick`, `roll`), there are several more for moving items between data and return stack. The instructions `pop`, `push`, `peek` each refer to the return stack (popping from return to data, pushing from data to return, …). The return stack is commonly also used to store data that is local to a subroutine. It is safe to push data here to be recovered after a subroutine call. It is not safe to use it for passing data between subroutines. That is what the data stack is for. Think of arguments vs. locals. 434 | 435 | #### Dictionary Manipulation 436 | 437 | The `forget` word is a Forthism for reverting to the address of a previously defined word; essentially forgetting it and any (potentially dependent words) defined thereafter. 438 | 439 | The `(alloc)`, `(free)`, `(tail)`, and `(local)`/`(local@)`/`(local!)` instructions are all to support IL translation. The CLR doesn't use the evaluation stack for parameter passing and local storage. For example, there are no stack manipulation instructions in IL except drop. Instead, IL code generally makes use of a per-method locals and arguments fetched and stored via instructions such as stloc/ldloc/starg/ldarg. (note: IL translation is not in this fork, [but here](https://github.com/ashleyf/brief/tree/gh-pages/embedded)). 440 | 441 | This is not a feature used in idiomatic Brief code but is here to make IL translation more straight forward. Each method allocated enough space for locals and args. Before returning (or earlier for TCO), this is freed. 442 | 443 | The normal way of handling locals in Brief that need to survive a call and return from another word is to store them on the return stack. The `(alloc)` instruction does this to persist the size of allocation to be used by `(free)`/`(tail)` later. Tail call optimization (that is, `.tail` in the IL) is handled by the `(tail)` instruction. This frees and pushes back a zero so that freeing later upon return has no further effect. 444 | 445 | Local and arg space is allocated from the bottom of dictionary space. The `(local)` instruction is used for args as well despite the name. It simply pushes the address of the nth slot. This address can then be used by the regular fetch and store instructions. Because 16-bit values are commonly used in translated IL, there are single-byte instructions for this. 446 | 447 | #### Arduino Integration 448 | 449 | Starting with basic setup and reading/write to GPIO pins. Note we treat `high`/`low` values as Brief-style booleans (-1 or 0) to play well with the logical and conditional operations. 450 | 451 | I2C support comes from several instructions, essentially mapping composable, zero-operand instructions to functions in the Arduino library: http://arduino.cc/en/Reference/Wire. Brief words (addresses/quotations) may be hooked to respond to Wire events. 452 | 453 | Brief word addresses (or quotations) may be set to run upon interrupts. For more info on the argument values and behavior, see: http://arduino.cc/en/Reference/AttachInterrupt. We keep a mapping of up to six words. 454 | 455 | Servo support also comes by simple mapping of composable, zero-operand instructions to Arduino library calls: http://arduino.cc/en/Reference/Servo. We keep up to 48 servo instances attached. 456 | 457 | #### Instruction Set 458 | 459 | Here is a complete list of primitive instructions. 460 | 461 | Note that the naming convention of surrounding in parenthesis means that the instruction is meant to be compiler-generated rather than used in source. 462 | 463 | The signature follows the Forth-style stack effect format of input - output. Square brackets mean the value comes as an operand rather than from the stack. Three instructions have effects on the return stack which are not shown in the signature – these are: (return), push and pop. 464 | 465 | | Bytecode | Name | Signature | Description | 466 | | --- | --- | --- | --- | 467 | | 0x00 | (return) | [address] - | Return from subroutine. | 468 | | 0x01 | (lit8) | [value] - | Push following byte as literal. | 469 | | 0x02 | (lit16) | [value] - | Push following two bytes as little endian literal. | 470 | | 0x03 | (branch) | [offset] - | Branch unconditionally by signed byte offset. | 471 | | 0x04 | (zbranch) | [offset] pred - | Branch if zero by signed byte offset. | 472 | | 0x05 | (quote) | [length] - | Push quotation address and jump over length. | 473 | | 0x06 | event{ |id - | Begin packing custom event payload. | 474 | | 0x07 | cdata | value - | Pack event payload 8-bit value. | 475 | | 0x08 | data | value - | Pack event payload 16-bit value (little endian). | 476 | | 0x09 |}event | - | Send event payload. | 477 | | 0x0A | event | id value - | Send single-value event payload. | 478 | | 0x0B | c@ | address - value | Fetch byte. | 479 | | 0x0C | c! | value address - | Store byte. | 480 | | 0x0D | @ | address - value | Fetch 16-bit value (little endian). | 481 | | 0x0E | ! | value address - | Store 16-bit value (little endian). | 482 | | 0x0F | + | y x - sum | Addition. | 483 | | 0x10 | - | y x - difference | Subtraction. | 484 | | 0x11 | * | y x - product | Multiplication. | 485 | | 0x12 | / | y x - quotient | Division. | 486 | | 0x13 | mod | y x - remainder | Modulus. | 487 | | 0x14 | and | y x - result | Bitwise/logical and. | 488 | | 0x15 | or | y x - result | Bitwise/logical or. | 489 | | 0x16 | xor | y x - result | Bitwise/logical xor. | 490 | | 0x17 | lsh | y x - result | Shift y left by x. | 491 | | 0x18 | rsh | y x - result | Shift y right by x. | 492 | | 0x19 | = | y x - result | Predicate y equals x. | 493 | | 0x1A | <> | y x - result | Predicate y not equal to x. | 494 | | 0x1B | > | y x - result | Predicate y greater than x. | 495 | | 0x1C | >= | y x - result | Predicate y greater than or equal to x. | 496 | | 0x1D | < | y x - result | Predicate y less than x. | 497 | | 0x1E | <= | y x - result | Predicate y less than or equal to x. | 498 | | 0x1F | not | x - result | Bitwise/logical not. | 499 | | 0x20 | neg | x - result | Negation. | 500 | | 0x21 | ++ | x - result | Increment. | 501 | | 0x22 | -- | x – result | Decrement. | 502 | | 0x23 | drop | x - | Drop top stack element. | 503 | | 0x24 | dup | x – x x | Duplicate top stack element. | 504 | | 0x25 | swap | y x - x y | Swap top two stack elements. | 505 | | 0x26 | pick | x - result | Duplicate xth stack element to top. | 506 | | 0x27 | roll | x - | Roll stack elements by x. | 507 | | 0x28 | clear | - | Clear stack. | 508 | | 0x29 | push | x - | Push top stack element to return stack. | 509 | | 0x2A | pop | - x | Pop top of return stack onto data stack. | 510 | | 0x2B | peek | - x | Duplicate top of return stack onto data stack. | 511 | | 0x2C | forget | address - | Revert dictionary to address. | 512 | | 0x2D | (alloc) | x - | Allocate x bytes, push x to return stack. | 513 | | 0x2E | (free) | - | Free n bytes (n taken from return stack). | 514 | | 0x2F | (tail) | - | Free n bytes (n taken from return stack and replaced with 0) | 515 | | 0x30 | (local) | index - address | Get address of local (to be used with regular fetch/store). | 516 | | 0x31 | (local@) | index - value | Fetch value of local (by index rather than address). | 517 | | 0x32 | (local!) | value index - | Store value to local (by index rather than address). | 518 | | 0x33 | (call) | address - | Call address (used internally by compiler). | 519 | | 0x34 | choice | pred p q - | Call one or the other quotation depending on predicate. | 520 | | 0x35 | if | pred q - | Call quotation depending on predicate. | 521 | | 0x36 | loopTicks | - x | Get number of loop iterations since reset. | 522 | | 0x37 | setLoop | q - | Set loop word. | 523 | | 0x38 | stopLoop | - | Reset loop word. | 524 | | 0x39 | (reset) | - | Reset MCU (clearing dictionary, stacks, etc.) | 525 | | 0x3A | pinMode | mode pin - | Set pin mode. | 526 | | 0x3B | digitalRead | pin - value | Read digital pin. | 527 | | 0x3C | digitalWrite | value pin - | Write to digital pin. | 528 | | 0x3D | analogRead | pin - value | Read analog pin. | 529 | | 0x3E | analogWrite | value pin - | Write to analog pin. | 530 | | 0x3F | wireBegin | - | Join I2C bus as master. | 531 | | 0x40 | wireRequestFrom | address count - | Request bytes from device at address. | 532 | | 0x41 | wireAvailable | - count | Get number of bytes available. | 533 | | 0x42 | wireRead | - value | Read byte from I2C device. | 534 | | 0x43 | wireBeginTransmission | reg - | Begin transmission to register. | 535 | | 0x44 | wireWrite | value - | Write byte to I2C device. | 536 | | 0x45 | wireEndTransmission | - | End transmission. | 537 | | 0x46 | wireSetOnReceive | q - | Set callback word. | 538 | | 0x47 | wireSetOnRequest | q - | Set callback word. | 539 | | 0x48 | attachISR | q interrupt mode - | Attach interrupt word. | 540 | | 0x49 | detachISR | interrupt - | Detach interrupt word. | 541 | | 0x4A | servoAttach | pin - | Attach servo on given pin. | 542 | | 0x4B | servoDetach | pin - | Detach servo on given pin. | 543 | | 0x4C | servoWriteMicros | value pin - | Write microseconds value to servo on given pin. | 544 | | 0x4D | milliseconds | - value | Get number of milliseconds since reset. | 545 | | 0x4E | pulseIn | mode pin - | Read a pulse on given pin. | 546 | 547 | ### Custom Instructions 548 | 549 | It’s likely that you can accomplish what you want without customizing the firmware; instead building from the primitives already available. However, if necessary you are free to do so. 550 | 551 | However, for example we can introducing a time delay instruction and to keep things simple we can just use the blocking `delay()` function provided by the Arduino libraries (this is normally a bad idea to block in the main loop rather than a state machine and this is why delay isn’t a Brief primitive). 552 | 553 | To introduce a new instruction in the VM we need only define a `void (*fn)()` function and bind it. Notice that instruction functions take and return no values. As we’ll soon see, Brief instructions take all of their arguments from the stack (`brief::pop()`) and leave their return values there as well (`brief::push(...)`). This is how instructions compose and conforming to this for our custom instruction will allow for composition with existing primitives. 554 | 555 | void delayMillis() 556 | { 557 | delay((int)brief::pop()); 558 | } 559 | 560 | Many of the primitives are really just thin wrappers such as this over Arduino library functionality. 561 | 562 | To bind this as a new instruction (say, bytecode 100) we add the following at the end of setup: 563 | 564 | brief::bind(100, delayMillis); 565 | 566 | That’s it. Very simple. 567 | 568 | Again though, as simple as this is, you are encouraged to build from existing primitives as much as possible. If you must add custom instructions then you are encouraged to add very low-level basic ones that may be composed into compound expressions. 569 | 570 | In the Custom Instructions section below we add an instruction to do a blocking delay. This isn’t part of the built-in primitives. There we give it bytecode 100. To use this raw instruction we will want to give it a friendly name in the interactive: 571 | 572 | 'nightlight setLoop 573 | --------------------------------------------------------------------------------