Volt Docs

Language

Hardware

Declare pins, devices and buses in the hardware block, and the compiler checks each one against your chip.

Everything wired to the board is declared in one hardware block. A declaration gives the thing a name your code uses, and tells the compiler enough to check it.

Pins

A pin has a name, a mode and a GPIO number:

main.volt
hardware {
    pin led = 2
    pin relay: Output = 4
    pin button: PullUpInput = 23
    pin pot: Analog = 34
    pin buzzer: PWM = 18
    pin arm: Servo = 13
}

every 100ms {
    if button.is_pressed() {
        relay.on()
        buzzer.set_pwm(128)
    } else {
        relay.off()
        buzzer.set_pwm(0)
    }
    arm.move(pot.read() * 180 / 4095)
    led.toggle()
}

Try it. Press Check on the example, then change relay to GPIO 34. The compiler explains why it can’t, and offers the pins that can.

The mode can be left out, as with led above, when the way the pin is used makes it clear. Pin modes lists every mode and its methods.

What the compiler checks

Each pin is checked against the chip set in volt.toml. On an ESP32:

  • GPIO 6 to 11 are wired to the flash chip. Using one is an error: the board would crash.
  • GPIO 34 to 39 are input-only. They can’t be an Output, PWM or Servo.
  • GPIO 0, 2, 5, 12 and 15 decide how the chip boots. Using them gets a warning explaining what to watch for.
  • GPIO 1 and 3 are the USB serial port. Using them breaks the serial monitor and flashing.
  • Analog pins on ADC2 stop working while Wi-Fi is on, so a program that uses Wi-Fi gets a warning for them.
  • Two declarations on the same GPIO are an error.

When a pin can’t do what you ask, the error lists the pins that can, and offers to move it there.

Devices

A device is a part with a driver: a sensor, a display, a radio. Declare it by name and the compiler fills in the bus, the default address and the wiring:

main.volt
hardware {
    device env: BME280
}

every 2s {
    let t: float = env.temperature()
    log("env", "temp={t}")
}

Devices lists every part Volt has a driver for, with its methods and wiring.

Choosing the bus and address

To say exactly how a device is wired, declare the bus and attach the device to it with on, and give an I²C address with at:

main.volt
hardware {
    device lcd: LCD16x2 on i2c0 at 0x27
    device accel: ADXL345 on i2c0 at 0x53
    device gsm: SIM800L on modem_uart
    i2c i2c0(sda: 21, scl: 22)
    uart modem_uart(tx: 17, rx: 16, baud: 9600)
}

every 1s {
    lcd.set_cursor(0, 0)
    lcd.print("g={accel.magnitude()}")
}

The compiler checks that two devices on one bus don’t share an address, that the address is one the part supports, and that the bus pins are free.

Devices on plain pins

Parts wired to ordinary GPIOs take their pins, and any settings, as named arguments:

main.volt
hardware {
    device sonar: HCSR04(trig: 26, echo: 27)
    device strip: NeoPixel(pin: 32, leds: 30)
}

every 200ms {
    let cm: float = sonar.distance_cm()
    log("sonar", "cm={cm}")
}

Buses

BusDeclaration
I²Ci2c i2c0(sda: 21, scl: 22)
UARTuart modem(tx: 17, rx: 16, baud: 9600)
SPIDevices take their chip-select pin: device flash: W25Q(cs: 5)

A UART you declare can be used directly too: see Serial buses.