Volt Docs

Language

Program structure

The parts of a Volt program, what each one is for, and when each runs.

A Volt program is one file, main.volt. It describes the hardware, then the behaviour. Here is one with most of the parts:

main.volt
hardware {
    pin led: Output = 2
}

var count: int = 0

func double(x: int) -> int {
    return x * 2
}

every 500ms {
    led.toggle()
    count = double(count) + 1
}

setup {
    log("boot", "ready")
}

The parts

PartWhat it’s for
hardwareThe pins, devices and buses, and what’s wired where. See Hardware.
let, varValues the whole program shares. Globals always have a starting value.
enum, structYour own types. See Types and variables.
storageValues kept in flash, so they survive a restart. See Storage.
channelA queue for passing values between tasks. See Channels.
funcA function you can call from anywhere.
everyA block that runs on a period: every 500ms { … }. See Timing and tasks.
onA block that runs when a condition becomes true.
taskA group of every blocks with state of their own.
machineA state machine. See State machines.
setupRuns once, when the board starts, before anything else.

The parts can come in any order. The formatter (volt fmt, or Format in the IDE) puts setup last.

The order things run in

  1. The board starts and the hardware is set up: every pin gets its mode, every device is started.
  2. Globals and storage values are loaded.
  3. setup runs, once, to the end.
  4. Every every block, task and state machine starts, each on its own schedule, as below.

There’s no main loop. If you’re used to Arduino, setup is the same idea; instead of loop() with delays, each thing that should happen regularly gets its own every block.

Running side by side

A Volt program usually has several every blocks, each with its own rhythm. They seem to run at the same time, but a microcontroller does one thing at a time. The blocks take turns, and each turn is so short that, to you, they all happen together.

The timeline under this program shows it the way a video editor shows clips. Each lane is one block, and each clip is one turn. The playhead moves in real time, so the clips light up when the board would run them.

main.volt
hardware {
    pin led: Output = 2
    pin button: PullUpInput = 23
    pin fan: Output = 4
    pin probe: Analog = 34
}

every 500ms {
    led.toggle()
}

every 100ms {
    if button.is_pressed() { fan.on() }
}

every 1s {
    let raw: int = probe.read()
    log("probe", "{raw}")
}
0.00s
0ms500ms1s1.5s2s
every 500msled.toggle()
every 100msif button.is_pressed() { fan.on() }
every 1slet raw: int = probe.read()

Read it like this:

  • The lanes don’t wait for each other. The 100 ms lane keeps its beat whatever the others are doing. You never write the timing yourself: no delay, no counting loops. The compiler builds the scheduler that wakes each block when its next turn is due.
  • A clip is short. Toggling an LED or reading a pin takes a few microseconds, not milliseconds, so the clips are drawn wider than they really are. Between clips the chip has nothing to do; the scheduler lets it idle.
  • When two lanes are due together, as at 0s and 1s here, their turns run one after the other, a few microseconds apart. A block always finishes its turn before the next one starts, so two blocks never trip over a shared variable halfway through.
  • A turn has to end. A block that loops for a long time holds up every lane. Anything that genuinely waits, like Wi-Fi, MQTT or HTTP, is moved by the compiler onto its own task, so the waiting happens off this timeline.
  • strict blocks cut in. An every 1ms strict block runs from a hardware timer and interrupts whatever is running, so it keeps exact time. That’s why it has to be short and can’t wait for anything.

Try it. Press Check, then change every 100ms to every 250ms, or add a block of your own. The timeline redraws as you type.

Functions

Functions take typed parameters and return a typed value:

main.volt
func average(a: float, b: float) -> float {
    return (a + b) / 2.0
}

func announce(name: string) {
    log("hello", "hi {name}")
}

setup {
    announce("Volt")
    let mid: float = average(1.0, 3.0)
    log("math", "mid={mid}")
}

A function with no -> returns nothing.

Comments and layout

// comments to the end of the line, and /* … */ can span lines. There are no semicolons: one statement per line, and blocks are wrapped in { }.