├── .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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------