Skip to content

Machine API

Ron Evans edited this page Aug 13, 2019 · 19 revisions

There should be a clearly documented API for peripherals in TinyGo. Here is an initial attempt.

Tracking issue (for comments): issue 9

For reference:

Pins

type Pin struct {
    // depends on the platform
}

type PinMode ... // type depends on the platform

const (
    // values depend on the platform
    PinOutput PinMode = ...
    PinInput  PinMode = ...
)

// Pin configuration. More fields may be added in the future, but the zero
// value must do the expected thing - e.g. pull configuration must default to
// 'no pull'.
type PinConfig struct {
    Mode PinMode
}

// Initialize PWM on platforms that need such a thing.
func InitPWM()

// Configures pin mode and possibly other settings.
func (p Pin) Configure(config PinConfig)

// Sets the pin to high or low when configured as output.
func (p Pin) Set(high bool)

// Gets the current value of the pin. Pin must be configured as input pin.
func (p Pin) Get() bool

// Enables PWM with the given duty cycle. Pin must be configured as output pin.
func (p Pin) SetPWM(dutycycle uint16)

// Sets the frequency for this PWM. This may be a no-op on some platforms.
func (p Pin) SetPWMFrequency(...)

// Reads a number from the ADC. Pin must be configured as input pin.
func (p Pin) ReadADC() uint16

PWM uses a 16-bits number for the duty cycle where 0 means 0% and 0xffff means 100% (so that for example 0xc000 means 75% on). This is a 16 bits number so that the most precision can be reached on all platforms and PWM peripherals that implement less bits can simply ignore the lower bits (for example, use dutycycle / 256 for an 8-bits PWM).

ADC uses a 16-bits number for the same reason, and is also 0 for the lowest possible output and 0xffff for the highest possible output. This means that an ADC that is not 16 bits must scale the output to fit 16 bits (for example, a 12 bits PWM must multiply the output by 16).

Open issues:

  • How should a pin object be obtained?
  • How should PWM be stopped?
  • This assumes that PWM, ADC etc. all use the same system for configuring as input or output. Is this true on all platforms?

I2C

The I2C interface allows for sending/receiving data from the I2C buses on any particular hardware device. The only 2 required methods are Configure() and Tx(), but any given hardware device can add additional low-level functions depending on the implementation details for the platform.

// All fields are optional and may be left out on a particular platform.
type I2CConfig struct {
	Frequency uint32
	SCL       Pin
	SDA       Pin
}

var I2C0 = ... // implementation defined

// Not a real exported type, just here to serve as example.
type I2C interface {
    Configure(I2CConfig)
    Tx(addr uint16, w, r []byte) error
}

There are some convenience methods for I2C register reading/writing that are already implemented and add themselves to the I2C interface, as long as the target board supports the required methods specified above:

WriteRegister(address uint8, register uint8, data []byte) error
ReadRegister(address uint8, register uint8, data []byte) error

SPI

The SPI interface allows for synchronous write/reading of data from the SPI buses on any particular hardware device. The only 2 required methods are Configure() and Transfer(), but any given hardware device can add additional low-level functions depending on the implementation details for the platform.

// All fields are optional and may be left out on a particular platform.
type SPIConfig struct {
	Frequency uint32
	SCK       Pin
	MOSI      Pin
	MISO      Pin
	LSBFirst  bool
	Mode      uint8
}

var SPI0 = ... // implementation defined

// Not a real exported type, just here to serve as example.
type SPI interface {
    Configure(SPIConfig)
    Transfer(w, r byte) error
}

There is a convenience method for typical SPI operation that is already implemented and adds itself to the SPI interface, as long as the target board supports the required methods specified above:

Tx(w, r []byte) error

UART

The UART interface implements io.Reader and io.Writer. Apart from that, it also implements ReadByte() and WriteByte() as implemented in package "bufio".

This is the interface it should implement:

// UART configuration. Some fields may be unavailable on some platforms.
type UARTConfig struct {
    BaudRate uint32
    RX       Pin
    TX       Pin
    RTS      Pin
    CTS      Pin
}

// Not a real exported type.
// Operations may not always return an error when something goes wrong.
type UARTInterface interface {
    Configure(UARTConfig)
    io.ReadWriter
    ReadByte() (byte, error) // like bufio.Reader
    WriteByte(c byte) error  // like bufio.Writer
}

Not all platforms support all configuration options. In particular, this is also a valid UARTConfig struct:

type UARTConfig struct {
    BaudRate uint32 // the only required field
}

Currently the UART implements the "standard" configuration, which is 8 bits per byte, one stop bit, and no parity. More fields may be added in the future, but something like UARTConfig{BaudRate: 115200} must continue to work.

Specific instances are normally accessed as a global variable and have type machine.UART. Pseudocode for the "machine" module:

var (
    UART0 = &UART{...} // possibly with pins configured for the board
)

type UART struct {
    // Implementation defined. Probably one of:
    //  - empty, if there is only one UART on the given system
    //  - single field that is a pointer to the memory-mapped peripheral
}

I2S

I2S is for audio communication between chips. The I2S interface implements io.Reader, io.Writer, and io.Closer.

// All fields are optional and may not be required or used on a particular platform.
type I2SConfig struct {
    SCK               Pin
    WS                Pin
    SD                Pin
    Mode              I2SMode
    Standard          I2SStandard
    ClockSource       I2SClockSource
    DataFormat        I2SDataFormat
    AudioFrequency    uint32
    MasterClockOutput bool
}

// var I2S0 = ... // implementation defined

// Not a real exported type, just here to serve as example.
// Supports the io.ReadWriteCloser interface.
type I2SReadWriteCloser interface {
    Configure(I2SConfig)
    Read(p []uint32) (n int, err error)
    Write(p []uint32) (n int, err error)
    Close() error
}

DAC

DAC is for using a Digital to Analog Converter. The DAC interface implements io.Writer, and io.Closer.

// All fields are optional and may not be required or used on a particular platform.
type DACConfig struct {
    // TBD
}

// var DAC0 = ... // implementation defined
// var DAC1 = ...

// Not a real exported type, just here to serve as example.
// Supports the io.WriteCloser interface.
type DACWriteCloser interface {
    Configure(DACConfig)
    WriteByte(b byte) error
    Write(p []uint32) (n int, err error)
    Close() error
}