diff --git a/README.md b/README.md index 2e197ee..45ed224 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,111 @@ # Ratel -## Next-generation, zero-cost abstraction microconroller programming in Nim +### Next-generation, zero-cost abstraction microconroller programming in Nim -This README is a bit light at the moment, please visit -[the website](https://ratel.peterme.net) to see the getting started guide and -documentation! +## Getting started +Getting started with Ratel is fairly simply. First you need to have [Nim](https://nim-lang.org/) installed along with the toolchain for +the controller you want to compile for. This guide is written with the Arduino Uno in mind, so for that board this would be the AVR toolchain. If you +have written code for Arduino before chances are good you already have these tools installed. Once that is done you need to install +Ratel itself: + +``` +nimble install ratel +``` + +If your board is not included in the official distribution you need to grab support for that as well. You should be able to search for all Ratel +based packages in the [package directory](https://nimble.directory/search?query=ratel). In order to compile and build for your board you +will also need the toolchain to compile C code and upload that to your board as well. The details of this should be found with the board support +library. + +Once you have Ratel and your board support installed you need to set up your project. In this tutorial we'll be building for an Arduino Uno, but +this process is pretty much the same no matter the board. Simply create a `config.nims` file in your folder with the content: + +```nim +import boardConf +board "unor3" + +avr.any.gcc.path = "/usr/bin" +avr.any.gcc.exe = "avr-gcc" +avr.any.gcc.linkerexe = "avr-gcc" +``` + +This does a couple of things, starting with including the `boardConf` module from Ratel. Then it calls `board` which tells +Ratel that the board we want to build for is the Arduino Uno rev. 3. This will automatically include a set of sane defaults for compiling for this +micro-controller, along with some procedures that we'll get back to later. + +The last three lines are simply telling Nim where to find the specific compiler we need to use for the CPU/OS combination we're using. +These could also be placed in your [global configuration](https://nim-lang.org/docs/nimc.html#compiler-usage-configuration-files) +as they will only be applied when compiling for these platforms. + +## Writing our code +Now that our project is all set up we need to write some code, the sample from the front page is a good start. Simply save the following code in +a file with the `.nim` extension named the same as the folder it's in. If you're using Nim on the devel branch you can leave out the `main` proc and simply +have everything in the global scope. + +```nim +import board +import board / [times, serial, progmem] + +Serial.init(9600.Hz) +Serial.send p"Hello world\n" +Led.output() + +while true: + Led.high() + Serial.send p"Led is on\n" + delayMs(1000) + Led.low() + Serial.send p"Led is off\n" + delayMs(1000) +``` + +To write your own code have a browse through the documentation and check out any Ratel modules in Nimble. + +## Compiling and uploading +With the project set up and the code written it is time to compile. When we imported the board configuration earlier we got some tasks for doing +this loaded into our configuration. To run these we use the `ratel` binary. So to build simply run: + +``` +ratel build +``` + +If you missed the sentence earlier about putting your file in a folder of the same name this will fail, but fret not, simply pass the file to +build with the `-f` flag. This command should have created a binary file in the same folder with the same name as your file but without +the extension. In order to check how big the resulting binary is we can simply run: + +``` +ratel size +``` + +Or if you're of the curious kind: + +``` +ratel sizeDetails +``` + +The size breakdown you get from this should be familiar to you if you have done any kind of programming with Arduino, it's the same one which is +written out in the terminal before uploading. It should look something like this: + +``` +AVR Memory Usage +---------------- +Device: atmega328p + +Program: 298 bytes (0.9% Full) +(.text + .data + .bootloader) + +Data: 0 bytes (0.0% Full) +(.data + .bss + .noinit) +``` + +Now the final step of the process is to upload our code to the controller. You can of course do this manually with `avrdude` but Ratel +comes with a task for this as well: + +``` +ratel upload --device=/dev/ttyACM0 +``` + +For me the board is connected to the USB port at `/dev/ttyACM0` but this might be different for you. + +And that should be it! Your board should now be flashing an LED and printing to the serial terminal. To view the output you can either use the +serial terminal that comes with Arduino if you have that installed, or you can use a number of terminal applications. The easiest might even be to run +`tail -f /dev/ttyACM0`. diff --git a/ratel.nimble b/ratel.nimble index e635540..57aa861 100644 --- a/ratel.nimble +++ b/ratel.nimble @@ -1,12 +1,17 @@ # Package -version = "0.1.0" +version = "0.2.0" author = "Peter Munch-Ellingsen" description = "Nim for microcontrollers" license = "MIT" srcDir = "src" +installExt = @["nim"] +bin = @["ratel"] # Dependencies -requires "nim >= 1.6.2" +requires "nim >= 1.6.4" +requires "nimscripter" +requires "compiler" +requires "cligen" diff --git a/src/private/boards/teensy/boardConf.nim b/src/private/boards/teensy/boardConf.nim index 66181b3..80fbe15 100644 --- a/src/private/boards/teensy/boardConf.nim +++ b/src/private/boards/teensy/boardConf.nim @@ -1 +1,23 @@ include "../helpers/avrBoardConf.nim" + +proc build*(file: string) = + exec "nim c -d:danger --os:any " & file + +proc upload*(file: string) = + exec "avr-objcopy -O ihex -R .eeprom " & file & " " & file & ".hex" + exec "teensy-loader-cli --mcu=TEENSY2 -v -w " & file & ".hex" + exec "rm " & file & ".hex" + +proc size*(file: string) = + exec "avr-size -C --mcu=atmega32u4 " & file + +proc sizeDetails*(file: string) = + size(file) + echo ".data section:" + exec "avr-nm -S --size-sort " & file & " | grep \" [Dd] \" || echo \"empty\"" + echo "" + echo ".bss section:" + exec "avr-nm -S --size-sort " & file & " | grep \" [Bb] \" || echo \"empty\"" + echo "" + echo ".text section:" + exec "avr-nm -S --size-sort " & file & " | grep \" [Tt] \" || echo \"empty\"" diff --git a/src/private/boards/unor3/boardConf.nim b/src/private/boards/unor3/boardConf.nim index 66181b3..5d15cde 100644 --- a/src/private/boards/unor3/boardConf.nim +++ b/src/private/boards/unor3/boardConf.nim @@ -1 +1,21 @@ include "../helpers/avrBoardConf.nim" + +proc build*(file: string) = + exec "nim c -d:danger --os:any " & file + +proc upload*(device: string, file: string) = + exec "avrdude -F -V -c arduino -p atmega328p -P " & device & " -b 115200 -U flash:w:" & file & ":e" + +proc size*(file: string) = + exec "avr-size -C --mcu=atmega328p " & file + +proc sizeDetails*(file: string) = + size(file) + echo ".data section:" + exec "avr-nm -S --size-sort " & file & " | grep \" [Dd] \" || echo \"empty\"" + echo "" + echo ".bss section:" + exec "avr-nm -S --size-sort " & file & " | grep \" [Bb] \" || echo \"empty\"" + echo "" + echo ".text section:" + exec "avr-nm -S --size-sort " & file & " | grep \" [Tt] \" || echo \"empty\"" diff --git a/src/ratel.nim b/src/ratel.nim new file mode 100644 index 0000000..9118bda --- /dev/null +++ b/src/ratel.nim @@ -0,0 +1,75 @@ +import strutils, tables, os, options +import nimscripter, nimscripter / vmops +import cligen + +var + keys*: Table[string, string] + defines*: seq[string] + +proc switch*(key: string, value = "") = + if key == "define": + defines.add value.strip(chars = {'"'}) + else: + keys[key] = value.strip(chars = {'"'}) + +proc getSetting*(key: string): string = + keys[key] + +proc isDefined*(key: string): bool = + defines.contains key + +proc runCommand(command: string, arguments: varargs[string]) = + addVMops(tasks) + addCallable(tasks): + proc build(file: string) + proc getSize(file: string) + + exportCode(tasks): + template `--`(key, val: untyped) = + switch(strip(astToStr(key)), strip(astToStr(val))) + + template `--`(key: untyped) = + switch(strip(astToStr(key))) + + exportTo(tasks, switch, getSetting, isDefined) + + var searchPaths: seq[string] + # TODO: figure out paths on Windows + let nimblePath = getEnv("NIMBLE_PATH") + let nimblePaths = if nimblePath.len == 0: + @[getHomeDir() & "/.nimble/pkgs/", getHomeDir() & "/.nimble/pkgs2/", "/opt/nimble/pkgs/", "/opt/nimble/pkgs2/"] + else: + @[nimblePath] + for path in nimblePaths: + for kind, path in path.walkDir: + if kind == pcDir: searchPaths.add path + + var + addins = implNimscriptModule(tasks) + interpreter = loadScript("config.nims".NimScriptPath, addins, modules = @["strutils"], searchPaths = searchPaths) + + case arguments.len: + of 0: interpreter.get.invokeDynamic(command) + of 1: interpreter.get.invokeDynamic(command, arguments[0]) + of 2: interpreter.get.invokeDynamic(command, arguments[0], arguments[1], returnType = void) + of 3: interpreter.get.invokeDynamic(command, arguments[0], arguments[1], arguments[2], returnType = void) + of 4: interpreter.get.invokeDynamic(command, arguments[0], arguments[1], arguments[2], arguments[3], returnType = void) + else: interpreter.get.invokeDynamic(command, arguments) + +proc build(file = getCurrentDir().lastPathPart().addFileExt(".nim")) = + runCommand("build", file) + +proc upload(device = "autodetect", file = getCurrentDir().lastPathPart()) = + if device != "autodetect": + runCommand("upload", device, file) + else: + runCommand("upload", file) + +proc size(file = getCurrentDir().lastPathPart()) = + runCommand("size", file) + +proc sizeDetails(file = getCurrentDir().lastPathPart()) = + runCommand("sizeDetails", file) + +dispatchMulti([build], [upload], [size], [sizeDetails]) + diff --git a/website/gettingstarted.html b/website/gettingstarted.html index ac6b0dc..62b7c92 100644 --- a/website/gettingstarted.html +++ b/website/gettingstarted.html @@ -44,82 +44,60 @@

Getting started

will also need the toolchain to compile C code and upload that to your board as well. The details of this should be found with the board support library.

-

Once you have Ratel and your board support installed you need to set up your project. It's a little bit of manual work for the time being, but - this should improve in the future. First you need a configuration file to let - Nim know that we're building for a microcontroller. In this tutorial - we'll be building for an Arduino Uno, but these steps should be mostly the same no matter the board.

+

Once you have Ratel and your board support installed you need to set up your project. In this tutorial we'll be building for an Arduino Uno, but + this process is pretty much the same no matter the board. Simply create a config.nims file in your folder with the content:

-
cpu = "avr"
-os = "any"
-define = "noSignalHandler"
-
-gc = "arc"
-define = "useMalloc"
-
-opt = "size"
-exceptions = "goto"
-
-noMain
-define = "board:unor3"
+            
import boardConf
+board "unor3"
 
 avr.any.gcc.path = "/usr/bin"
 avr.any.gcc.exe = "avr-gcc"
 avr.any.gcc.linkerexe = "avr-gcc"
 
-

This does a couple of things, cpu = "avr" tells the compiler about our architecture, such as the size of a pointer. - os = "any" makes sure that Nim doesn't depend on any OS specific features and only relies on the standard - stdlib and stdio libraries. The define = "noSignalHandler" switch overrides Nims default - behaviour of attaching signal handlers to create better stack traces in the case of segfaults and such.

- -

gc = "arc" turns on the new automatic reference counting algorithm for the garbage collector. If you - don't use any garbage collected memory this won't generate any code, and even if you do it's nice and efficient. - define = "useMalloc" configures Nim to only use the standard C malloc and friends memory handling calls.

- -

opt = "size", and exceptions = "goto" are simply features of the Nim compiler which creates smaller code. - These are really optional.

- -

We will also define our own main procedure so we pass noMain to stop Nim from creating one. And then - the only part of this definition which is Ratel related, namely the define = "board:unor3" which tells Ratel that the - board we want to build for is the Arduino Uno rev. 3.

+

This does a couple of things, starting with including the boardConf module from Ratel. Then it calls board which tells + Ratel that the board we want to build for is the Arduino Uno rev. 3. This will automatically include a set of sane defaults for compiling for this + micro-controller, along with some procedures that we'll get back to later.

The last three lines are simply telling Nim where to find the specific compiler we need to use for the CPU/OS combination we're using. These could also be placed in your global configuration - as they will only be applied when compiling for these platforms

+ as they will only be applied when compiling for these platforms.

Writing our code

Now that our project is all set up we need to write some code, the sample from the front page is a good start. Simply save the following code in - a file with the .nim extension.

+ a file with the .nim extension named the same as the folder it's in.

import board
 import board / [times, serial, progmem]
 
-proc main(): cint {.exportc.} =
-  Serial.init(9600.Hz)
-  Serial.send p"Hello world\n"
-  Led.output()
+Serial.init(9600.Hz)
+Serial.send p"Hello world\n"
+Led.output()
 
-  while true:
-    Led.high()
-    Serial.send p"Led is on\n"
-    delayMs(1000)
-    Led.low()
-    Serial.send p"Led is off\n"
-    delayMs(1000)
+while true: + Led.high() + Serial.send p"Led is on\n" + delayMs(1000) + Led.low() + Serial.send p"Led is off\n" + delayMs(1000)

To write your own code have a browse through the documentation and check out any Ratel modules in Nimble.

Compiling and uploading

-

With the project set up and the code written it is time to compile. There is currently a bug which doesn't allow os = "any" to work - properly from within the configuration file we created earlier, and there isn't much use for building with debug symbols and runtime overflow checks - and such on this kind of controller. So to build our program simply run:

+

With the project set up and the code written it is time to compile. When we imported the board configuration earlier we got some tasks for doing + this loaded into our configuration. To run these we use the ratel binary. So to build simply run:

+ +
ratel build
+ +

If you missed the sentence earlier about putting your file in a folder of the same name this will fail, but fret not, simply pass the file to + build with the -f flag. This command should have created a binary file in the same folder with the same name as your file but without + the extension. In order to check how big the resulting binary is we can simply run:

-
nim c -d:danger --os:any <name of your file>
+
ratel size
-

This should have created a binary file in the same folder with the same name as your file but without the extension. The AVR toolchain comes with - a couple handy tools, such as avr-size which we can run to check the size our program will take on the controller. For the Arduino Uno - which uses the ATMega 328p controller the command for that would look like this:

+

Or if you're of the curious kind:

-
avr-size -C --mcu=atmega328p <name of your binary>
+
ratel sizeDetails

The size breakdown you get from this should be familiar to you if you have done any kind of programming with Arduino, it's the same one which is written out in the terminal before uploading. It should look something like this:

@@ -128,20 +106,18 @@

Compiling and uploading

---------------- Device: atmega328p -Program: 304 bytes (0.9% Full) +Program: 298 bytes (0.9% Full) (.text + .data + .bootloader) Data: 0 bytes (0.0% Full) (.data + .bss + .noinit) -

Now the final step of the process is to upload our code to the controller. This can be achieved by using another program installed by the - toolchain called avrdude. There are a lot of options that can be passed to this tool, and a lot of resources explaining what they all do, - so I won't go into detail about that here. Suffice to say that to upload our binary we can use a command like this one:

+

Now the final step of the process is to upload our code to the controller. You can of course do this manually with avrdude but Ratel + comes with a task for this as well:

-
avrdude -F -V -c arduino -p atmega328p -P /dev/ttyACM0 -b 115200 -U flash:w:<name of your binary>:e
+
ratel upload --device=/dev/ttyACM0
-

The only parameter you really should have to change here is the port at which your board is located. For me it's /dev/ttyACM0 but this - might be different for you.

+

For me the board is connected to the USB port at /dev/ttyACM0 but this might be different for you.

And that should be it! Your board should now be flashing an LED and printing to the serial terminal. To view the output you can either use the serial terminal that comes with Arduino if you have that installed, or you can use a number of terminal applications. The easiest might even be to run