From cda3dbcc75e8bd4983dd5f1af6c1cf808deb243c Mon Sep 17 00:00:00 2001 From: Thomas Kohler Date: Thu, 23 Jan 2025 18:18:14 +0100 Subject: [PATCH] zero: introduce adaptor for Radxa Zero (#1128) --- examples/zero_analog.go | 60 +++++ examples/zero_direct_pin.go | 80 ++++++ .../{tinkerboard_servo.go => zero_servo.go} | 15 +- .../{tinkerboard_yl40.go => zero_yl40.go} | 10 +- platforms/radxa/zero/LICENSE | 13 + platforms/radxa/zero/README.md | 254 ++++++++++++++++++ platforms/radxa/zero/adaptor.go | 157 +++++++++++ platforms/radxa/zero/adaptor_test.go | 224 +++++++++++++++ platforms/radxa/zero/doc.go | 7 + platforms/radxa/zero/pinmap.go | 62 +++++ 10 files changed, 870 insertions(+), 12 deletions(-) create mode 100644 examples/zero_analog.go create mode 100644 examples/zero_direct_pin.go rename examples/{tinkerboard_servo.go => zero_servo.go} (80%) rename examples/{tinkerboard_yl40.go => zero_yl40.go} (85%) create mode 100644 platforms/radxa/zero/LICENSE create mode 100644 platforms/radxa/zero/README.md create mode 100644 platforms/radxa/zero/adaptor.go create mode 100644 platforms/radxa/zero/adaptor_test.go create mode 100644 platforms/radxa/zero/doc.go create mode 100644 platforms/radxa/zero/pinmap.go diff --git a/examples/zero_analog.go b/examples/zero_analog.go new file mode 100644 index 000000000..de1d05369 --- /dev/null +++ b/examples/zero_analog.go @@ -0,0 +1,60 @@ +//go:build example +// +build example + +// +// Do not build by default. + +package main + +import ( + "fmt" + "log" + "time" + + "gobot.io/x/gobot/v2" + "gobot.io/x/gobot/v2/drivers/aio" + "gobot.io/x/gobot/v2/platforms/radxa/zero" +) + +// Wiring: +// PWR : 1, 17 (+3.3V, VCC), 2, 4 (+5V), 6, 9, 14, 20, 25, 30, 34, 39 (GND) +// ADC (max. 1.8V): header pin 15 is input for channel 1, pin 26 is input for channel 2 +func main() { + const ( + inPin0 = "15_mean" + inPin1 = "26" + inVoltageScale = 0.439453125 // see README.md of the platform + ) + + scaler := aio.AnalogSensorLinearScaler(0, 4095, 0, 1.8) + + adaptor := zero.NewAdaptor() + ana0 := aio.NewAnalogSensorDriver(adaptor, inPin0, aio.WithSensorScaler(scaler)) + ana1 := aio.NewAnalogSensorDriver(adaptor, inPin1) + + work := func() { + gobot.Every(500*time.Millisecond, func() { + v0, err := ana0.Read() + if err != nil { + log.Println(err) + } + + v1, err := ana1.Read() + if err != nil { + log.Println(err) + } + + fmt.Printf("%s: %1.3f V, %s: %2.0f (%4.0f mV)\n", inPin0, v0, inPin1, v1, v1*inVoltageScale) + }) + } + + robot := gobot.NewRobot("adcBot", + []gobot.Connection{adaptor}, + []gobot.Device{ana0, ana1}, + work, + ) + + if err := robot.Start(); err != nil { + panic(err) + } +} diff --git a/examples/zero_direct_pin.go b/examples/zero_direct_pin.go new file mode 100644 index 000000000..2696f7c8d --- /dev/null +++ b/examples/zero_direct_pin.go @@ -0,0 +1,80 @@ +//go:build example +// +build example + +// +// Do not build by default. + +package main + +import ( + "fmt" + "time" + + "gobot.io/x/gobot/v2" + "gobot.io/x/gobot/v2/drivers/gpio" + "gobot.io/x/gobot/v2/platforms/adaptors" + "gobot.io/x/gobot/v2/platforms/radxa/zero" +) + +// Wiring +// PWR : 1, 17 (+3.3V, VCC), 2, 4 (+5V), 6, 9, 14, 20, 25, 30, 34, 39 (GND) +// GPIO : header pin 24 is input, pin 32 used as normal output, pin 36 used as inverted output +// Button: the input pin is wired with a button to GND, the internal pull up resistor is used +// LED's: the output pins are wired to the cathode of the LED, the anode is wired with a resistor (70-130Ohm for 20mA) +// to VCC +// Expected behavior: always one LED is on, the other in opposite state, if button is pressed the state changes +func main() { + const ( + inPinNum = "24" + outPinNum = "32" + outPinInvertedNum = "36" + ) + // note: WithGpiosOpenDrain() is optional, if using WithGpiosOpenSource() the LED's will not light up + board := zero.NewAdaptor(adaptors.WithGpiosActiveLow(outPinInvertedNum), + adaptors.WithGpiosOpenDrain(outPinNum, outPinInvertedNum), adaptors.WithGpiosPullUp(inPinNum)) + + inPin := gpio.NewDirectPinDriver(board, inPinNum) + outPin := gpio.NewDirectPinDriver(board, outPinNum) + outPinInverted := gpio.NewDirectPinDriver(board, outPinInvertedNum) + + work := func() { + level := byte(1) + + gobot.Every(500*time.Millisecond, func() { + read, err := inPin.DigitalRead() + fmt.Printf("pin %s state is %d\n", inPinNum, read) + if err != nil { + fmt.Println(err) + if level == 1 { + level = 0 + } else { + level = 1 + } + } else { + level = byte(read) + } + + err = outPin.DigitalWrite(level) + fmt.Printf("pin %s is now %d\n", outPinNum, level) + if err != nil { + fmt.Println(err) + } + + err = outPinInverted.DigitalWrite(level) + fmt.Printf("pin %s is now not %d\n", outPinInvertedNum, level) + if err != nil { + fmt.Println(err) + } + }) + } + + robot := gobot.NewRobot("pinBot", + []gobot.Connection{board}, + []gobot.Device{inPin, outPin, outPinInverted}, + work, + ) + + if err := robot.Start(); err != nil { + panic(err) + } +} diff --git a/examples/tinkerboard_servo.go b/examples/zero_servo.go similarity index 80% rename from examples/tinkerboard_servo.go rename to examples/zero_servo.go index 1ea3e9987..ed4ddbfea 100644 --- a/examples/tinkerboard_servo.go +++ b/examples/zero_servo.go @@ -13,25 +13,26 @@ import ( "gobot.io/x/gobot/v2" "gobot.io/x/gobot/v2/drivers/gpio" "gobot.io/x/gobot/v2/platforms/adaptors" - "gobot.io/x/gobot/v2/platforms/asus/tinkerboard" + "gobot.io/x/gobot/v2/platforms/radxa/zero" ) // Wiring -// PWR Tinkerboard: 1 (+3.3V, VCC), 2(+5V), 6, 9, 14, 20 (GND) -// PWM Tinkerboard: header pin 33 (PWM2) or pin 32 (PWM3) +// PWR: 1, 17 (+3.3V, VCC), 2, 4 (+5V), 6, 9, 14, 20, 25, 30, 34, 39 (GND) +// PWM: header pin 18 (PWM_C), 40 (PWMAO_A) +// Servo SG90: red (+5V), brown (GND), orange (PWM) func main() { const ( - pwmPin = "32" + pwmPin = "18" wait = 3 * time.Second fiftyHzNanos = 20 * 1000 * 1000 // 50Hz = 0.02 sec = 20 ms ) // usually a frequency of 50Hz is used for servos, most servos have 0.5 ms..2.5 ms for 0-180°, // however the mapping can be changed with options: - adaptor := tinkerboard.NewAdaptor( + adaptor := zero.NewAdaptor( adaptors.WithPWMDefaultPeriodForPin(pwmPin, fiftyHzNanos), - adaptors.WithPWMServoDutyCycleRangeForPin(pwmPin, time.Millisecond, 2*time.Millisecond), - adaptors.WithPWMServoAngleRangeForPin(pwmPin, 0, 270), + adaptors.WithPWMServoDutyCycleRangeForPin(pwmPin, 500*time.Microsecond, 2500*time.Microsecond), + adaptors.WithPWMServoAngleRangeForPin(pwmPin, 0, 180), ) servo := gpio.NewServoDriver(adaptor, pwmPin) diff --git a/examples/tinkerboard_yl40.go b/examples/zero_yl40.go similarity index 85% rename from examples/tinkerboard_yl40.go rename to examples/zero_yl40.go index ea6504ed0..37546d859 100644 --- a/examples/tinkerboard_yl40.go +++ b/examples/zero_yl40.go @@ -13,19 +13,19 @@ import ( "gobot.io/x/gobot/v2" "gobot.io/x/gobot/v2/drivers/i2c" - "gobot.io/x/gobot/v2/platforms/asus/tinkerboard" + "gobot.io/x/gobot/v2/platforms/radxa/zero" ) func main() { // Wiring - // PWR Tinkerboard: 1 (+3.3V, VCC), 6, 9, 14, 20 (GND) - // I2C1 Tinkerboard: 3 (SDA), 5 (SCL) + // PWR : 1, 17 (+3.3V, VCC), 6, 9, 14, 20, 25, 30, 34, 39 (GND) + // I2C3: 3 (SDA), 5 (SCL), I2C1: 24/16 (SDA), 23/13 (SCL), I2C4: 7 (SDA), 11 (SCL) // YL-40 module: wire AOUT --> AIN2 for this example // // Note: temperature measurement is often buggy, because sensor is not properly grounded // fix it by soldering a small bridge to the adjacent ground pin of brightness sensor - board := tinkerboard.NewAdaptor() - yl := i2c.NewYL40Driver(board, i2c.WithBus(1)) + board := zero.NewAdaptor() + yl := i2c.NewYL40Driver(board, i2c.WithBus(3)) work := func() { // the LED light is visible above ~1.7V diff --git a/platforms/radxa/zero/LICENSE b/platforms/radxa/zero/LICENSE new file mode 100644 index 000000000..f4448d985 --- /dev/null +++ b/platforms/radxa/zero/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2025 The Hybrid Group + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/platforms/radxa/zero/README.md b/platforms/radxa/zero/README.md new file mode 100644 index 000000000..c78f11b72 --- /dev/null +++ b/platforms/radxa/zero/README.md @@ -0,0 +1,254 @@ +# Radxa Zero + +The Radxa Zero is a single board SoC computer based on the Amlogic S905Y2 arm64 processor. It has built-in +GPIO, I2C, PWM, SPI, 1-Wire and ADC interfaces. + +For more info about the Radxa Zero, go to [https://docs.radxa.com/en/zero/zero](https://docs.radxa.com/en/zero/zero). + +## How to Install + +Please refer to the main [README.md](https://github.com/hybridgroup/gobot/blob/release/README.md) + +Tested OS: + +* [dietPi](https://dietpi.com/downloads/images/DietPi_RadxaZero-ARMv8-Bookworm.img.xz) a minimal image with Debian Bookworm + +## Configuration steps for the OS + +### WLAN access + +There is no LAN network interface on the board, but WLAN is sufficient for our needs. After copy over the image to your +SD card you can modify the file dietpi.txt before plug in the card to [make it work on first boot](https://dietpi.com/docs/usage/#how-to-do-an-automatic-base-installation-at-first-boot-dietpi-automation). + +```txt adjust dietpi.txt to your needs +AUTO_SETUP_NET_WIFI_ENABLED=1 +AUTO_SETUP_NET_WIFI_COUNTRY_CODE=DE +``` + +Afterwards the WiFi login data needs to be provided (unencrypted file, will be removed after first boot): + +```txt adjust dietpi-wifi.txt +aWIFI_SSID[0]="" +aWIFI_KEY[0]="" +``` + +First login needs to be done with "root" or "dietpi", and you will start with a configuration procedure. + +```sh +ssh root@DietPi +``` + +### System access and configuration basics + +Please follow the instructions of the OS provider. A ssh access for (WLAN with dietpi user) is used in this guide. + +```sh +ssh dietpi@DietPi +``` + +### Enabling hardware drivers in general + +Not all drivers are enabled by default. You can have a look at the configuration file, to find out what is enabled at +your system: + +```sh +cat /boot/dietpiEnv.txt +``` + +Missing interfaces needs to be enabled by DT-overlays (drop "meson" prefix and file extension). + +```sh list available overlays +ls /boot/dtb/amlogic/overlay/ +``` + +Please read [this GPIO page](https://wiki.radxa.com/Zero/hardware/gpio.) for meaning of the different part of file name (e.g. "ao" vs. "ee"). +The [page about overlays](https://wiki.radxa.com/Device-tree-overlays#Meson_G12A_Available_Overlay_.28Radxa_Zero.29) will help you in addition to choose the right one. + +### enable I2C + +| device |SDA|SCL|DT overlay file name| +|----------|---|---|--------------------| +|/dev/i2c-1| 16| 13|meson-g12a-radxa-zero-i2c-ee-m1-gpiox-10-gpiox-11.dtbo| +|/dev/i2c-1| 24| 23|meson-g12a-radxa-zero-i2c-ee-m1-gpioh-6-gpioh-7.dtbo| +|/dev/i2c-3| 3| 5|meson-g12a-radxa-zero-i2c-ee-m3-gpioa-14-gpioa-15.dtbo| +|/dev/i2c-4| 7| 11|meson-g12a-radxa-zero-i2c-ao-m0-gpioao-2-gpioao-3.dtbo| + + +```sh /boot/dietpiEnv.txt example for i2c-1 +... +overlays=g12a-radxa-zero-i2c-ee-m1-gpioh-6-gpioh-7 +... +``` + +>The I2C device "/dev/i2c-3" was already enabled on dietPi after setup. + +### enable SPI + +```sh /boot/dietpiEnv.txt for SPI +... +overlays=g12a-radxa-zero-spi-spidev +... +``` + +>Most likely the overlay is currently defective - it contains "armbian" and only "disabled" spi devices. + +### enable PWM + +|pin | symbol | DT path | driver |DT overlay file name| +|--------|---------|---------------------------|--------------------|--------------------| +|32, PWMAO_C|pwm_AO_cd|/soc/bus@ff800000/pwm@2000 |meson-g12a-ao-pwm-cd|on by default| +|40, PWMAO_A|pwm_AO_ab|/soc/bus@ff800000/pwm@7000 |meson-g12a-ao-pwm-ab|meson-g12a-radxa-zero-pwmao-a-on-gpioao-11.dtbo| +|18, PWM_C |pwm_cd |/soc/bus@ffd00000/pwm@1a000|meson-g12a-ee-pwm |meson-g12a-radxa-zero-pwm-c-on-gpiox-8.dtbo| +|21, PWM_F |pwm_ef |/soc/bus@ffd00000/pwm@19000|meson-g12a-ee-pwm |on by default| + +>PWMAO_B (channel 1 of pwm_AO_ab) and PWM_D (channel 1 of pwm_cd) not wired. PWMAO_D (channel 1 of pwm_AO_cd) in use by +>"regulator-vddcpu". PWM_E (channel 0 of pwm_ef) in use by wifi32k. PWMAO_C and PWM_F not really working, see +>troubleshooting section. + +### enable 1-wire + +The contained overlays maybe not working, because compatible with gxbb (Amlogic Meson S905). At least for my Zero V1.51 +g12a (Amlogic Meson S905X2 and above) it does not work. + +```sh /boot/dietpiEnv.txt for 1-wire +dtc -I dtb -O dts /boot/dtb/amlogic/overlay/meson-w1AB-gpio.dtbo | grep amlogic +... +compatible = "amlogic,meson-gxbb"; +``` + +So create your own overlay as `/boot/overlay_user/meson-g12a-w1-gpioao-3.dts`... + +```sh +/dts-v1/; +/plugin/; + +/ { + compatible = "radxa,zero", "amlogic,g12a"; + + fragment@0 { + target-path = "/"; + + __overlay__ { + w1: onewire { + compatible = "w1-gpio"; + pinctrl-names = "default"; + /* GPIOAO_3=0x03, GPIOC_7=0x30 */ + /* GPIO_ACTIVE_HIGH=0, GPIO_ACTIVE_LOW=1 */ + /* GPIO_SINGLE_ENDED=2, GPIO_LINE_OPEN_DRAIN=4 */ + gpios = <0xffffffff 0x03 0x06>; + status = "okay"; + phandle = <0x01>; + }; + }; + }; + + __fixups__ { + /* gpio_ao or gpio for GPIOC_7 */ + gpio_ao = "/fragment@0/__overlay__/onewire:gpios:0"; + }; +}; +``` + +...compile it + +```sh +dtc -@ -O dtb -b 0 -o meson-g12a-w1-gpioao-3.dtbo meson-g12a-w1-gpioao-3.dts +``` + +... and add it as follows: + +```sh /boot/dietpiEnv.txt for 1-wire +... +user_overlays= meson-g12a-w1-gpioao-3 +... +``` + +## enable SAR ADC + +The 12-bit ADC is enabled by default. The voltage range is 0..1.8V. Raw values can be read with pin 15 (channel 1) or +pin 26 (channel 2). Additionally some internal values can be accessed, e.g. gnd and 1/4 vdd. For debugging purposes more +information is provided, e.g. each item provides a label: + +```sh +cat /sys/bus/platform/drivers/meson-saradc/ff809000.adc/iio:device0/in_voltage9_label +gnd +cat /sys/bus/platform/drivers/meson-saradc/ff809000.adc/iio:device0/in_voltage10_label +0.25vdd +``` +```sh +cat /sys/bus/platform/drivers/meson-saradc/ff809000.adc/iio:device0/calibbias +-4 +cat /sys/bus/platform/drivers/meson-saradc/ff809000.adc/iio:device0/calibscale +1.002447 +cat /sys/bus/platform/drivers/meson-saradc/ff809000.adc/iio:device0/in_voltage_scale +0.439453125 +cat /sys/bus/platform/drivers/meson-saradc/ff809000.adc/iio:device0/in_voltage9_raw +0 +cat /sys/bus/platform/drivers/meson-saradc/ff809000.adc/iio:device0/in_voltage10_raw +1023 +``` + +The `in_voltage_scale` (e.g. 0.439453125) is for calculation of "value = (raw + offset) * scale", value in millivolts. +The `calibbias` is the offset and `calibscale` the scale which is used for internal calibration, so we get an output +of 0..4095 for 0..1.8V input. + +>The channel 10 is a 1/4 of full range, but if the channel 2 is in saturation state (above 1.8V = 4095), it becomes more +>and more wrong. + +## How to Use + +The pin numbering used by your Gobot program should match the way your board is labeled right on the board itself. + +```go +r := zero.NewAdaptor() +led := gpio.NewLedDriver(r, "7") +``` + +## How to Connect + +### Compiling + +Compile your Gobot program on your workstation like this: + +```sh +GOARCH=arm64 GOOS=linux go build -o output/ examples/zero_blink.go +``` + +Once you have compiled your code, you can upload your program and execute it on the board from your workstation +using the `scp` and `ssh` commands like this: + +```sh +scp zero_blink dietpi@DietPi:~ +ssh -t dietpi@DietPi "./zero_blink" +``` + +## Troubleshooting + +### scp fails + +"bash: line 1: /usr/lib/sftp-server: No such file or directory" + +The dietPi has only a limited package set, so sftp-server is missing. + +```sh +sudo apt install openssh-sftp-server +``` + +### GPIO pin3, pin5 and pin7 not working like expected + +e.g. for pin3: +`cdev.Export(): cdev.reconfigure(gpiochip0-63)-c.RequestLine(63, [0 2000000000 2]): invalid argument` + +The pin does not support `adaptors.WithGpioDebounce(inPinNum, debounceTime)`. + +`cdev.Export(): cdev.reconfigure(gpiochip1-0)-c.RequestLine(0, [0 2]): invalid argument` + +The pin 8 is configured for UART. + +>Some pins have low power or have a strong pullup resistor, so the expected voltage drop is maybe not possible. +>Pins 7-27 and 11-28 are bridged. + +### PWMAO_C and PWM_F not really working + +If you run `cat /sys/kernel/debug/pwm` when those PWMs are used you can see, that it is working "internally". Most +likely the pin itself is not enabled by DT on PWM usage. Currently there is no fix provided for that. diff --git a/platforms/radxa/zero/adaptor.go b/platforms/radxa/zero/adaptor.go new file mode 100644 index 000000000..cc6b00c8c --- /dev/null +++ b/platforms/radxa/zero/adaptor.go @@ -0,0 +1,157 @@ +package zero + +import ( + "fmt" + "sync" + + multierror "github.com/hashicorp/go-multierror" + + "gobot.io/x/gobot/v2" + "gobot.io/x/gobot/v2/platforms/adaptors" + "gobot.io/x/gobot/v2/system" +) + +const ( + defaultI2cBusNumber = 3 + + defaultSpiBusNumber = 0 + defaultSpiChipNumber = 0 + defaultSpiMode = 0 + defaultSpiBitsNumber = 8 + defaultSpiMaxSpeed = 10000000 +) + +// Adaptor represents a Gobot Adaptor for the Radxa Zero +type Adaptor struct { + name string + sys *system.Accesser // used for unit tests only + mutex *sync.Mutex + *adaptors.AnalogPinsAdaptor + *adaptors.DigitalPinsAdaptor + *adaptors.PWMPinsAdaptor + *adaptors.I2cBusAdaptor + *adaptors.SpiBusAdaptor + *adaptors.OneWireBusAdaptor +} + +// NewAdaptor creates a Zero Adaptor +// +// Optional parameters: +// +// adaptors.WithGpioSysfsAccess(): use legacy sysfs driver instead of default character device driver +// adaptors.WithSpiGpioAccess(sclk, ncs, sdo, sdi): use GPIO's instead of /dev/spidev#.# +// adaptors.WithGpiosActiveLow(pin's): invert the pin behavior +// adaptors.WithGpiosPullUp/Down(pin's): sets the internal pull resistor +// adaptors.WithGpiosOpenDrain/Source(pin's): sets the output behavior +// adaptors.WithGpioEventOnFallingEdge/RaisingEdge/BothEdges(pin, handler): activate edge detection +// +// Optional parameters for PWM, see [adaptors.NewPWMPinsAdaptor] +func NewAdaptor(opts ...interface{}) *Adaptor { + sys := system.NewAccesser() + a := &Adaptor{ + name: gobot.DefaultName("Zero"), + sys: sys, + mutex: &sync.Mutex{}, + } + + var digitalPinsOpts []adaptors.DigitalPinsOptionApplier + var pwmPinsOpts []adaptors.PwmPinsOptionApplier + var spiBusOpts []adaptors.SpiBusOptionApplier + for _, opt := range opts { + switch o := opt.(type) { + case adaptors.DigitalPinsOptionApplier: + digitalPinsOpts = append(digitalPinsOpts, o) + case adaptors.PwmPinsOptionApplier: + pwmPinsOpts = append(pwmPinsOpts, o) + case adaptors.SpiBusOptionApplier: + spiBusOpts = append(spiBusOpts, o) + default: + panic(fmt.Sprintf("'%s' can not be applied on adaptor '%s'", opt, a.name)) + } + } + + analogPinTranslator := adaptors.NewAnalogPinTranslator(sys, analogPinDefinitions) + digitalPinTranslator := adaptors.NewDigitalPinTranslator(sys, gpioPinDefinitions) + pwmPinTranslator := adaptors.NewPWMPinTranslator(sys, pwmPinDefinitions) + // Valid bus numbers are [1,3,4] which corresponds to /dev/i2c-1, /dev/i2c-3, /dev/i2c-4 + // We don't support /dev/i2c-5 (DesignWare HDMI) + i2cBusNumberValidator := adaptors.NewBusNumberValidator([]int{1, 3, 4}) + // Valid bus numbers are [0,1] which corresponds to /dev/spidev0.x, /dev/spidev1.x + // x is the chip number <255 + spiBusNumberValidator := adaptors.NewBusNumberValidator([]int{0, 1}) + + a.AnalogPinsAdaptor = adaptors.NewAnalogPinsAdaptor(sys, analogPinTranslator.Translate) + a.DigitalPinsAdaptor = adaptors.NewDigitalPinsAdaptor(sys, digitalPinTranslator.Translate, digitalPinsOpts...) + a.PWMPinsAdaptor = adaptors.NewPWMPinsAdaptor(sys, pwmPinTranslator.Translate, pwmPinsOpts...) + a.I2cBusAdaptor = adaptors.NewI2cBusAdaptor(sys, i2cBusNumberValidator.Validate, defaultI2cBusNumber) + a.SpiBusAdaptor = adaptors.NewSpiBusAdaptor(sys, spiBusNumberValidator.Validate, defaultSpiBusNumber, + defaultSpiChipNumber, defaultSpiMode, defaultSpiBitsNumber, defaultSpiMaxSpeed, a.DigitalPinsAdaptor, spiBusOpts...) + // pin ?? needs to be activated by DT-overlay w1-gpio + a.OneWireBusAdaptor = adaptors.NewOneWireBusAdaptor(sys) + + return a +} + +// Name returns the name of the Adaptor +func (a *Adaptor) Name() string { return a.name } + +// SetName sets the name of the Adaptor +func (a *Adaptor) SetName(n string) { a.name = n } + +// Connect create new connection to board and pins. +func (a *Adaptor) Connect() error { + a.mutex.Lock() + defer a.mutex.Unlock() + + if err := a.OneWireBusAdaptor.Connect(); err != nil { + return err + } + + if err := a.SpiBusAdaptor.Connect(); err != nil { + return err + } + + if err := a.I2cBusAdaptor.Connect(); err != nil { + return err + } + + if err := a.AnalogPinsAdaptor.Connect(); err != nil { + return err + } + + if err := a.PWMPinsAdaptor.Connect(); err != nil { + return err + } + + return a.DigitalPinsAdaptor.Connect() +} + +// Finalize closes connection to board, pins and bus +func (a *Adaptor) Finalize() error { + a.mutex.Lock() + defer a.mutex.Unlock() + + err := a.DigitalPinsAdaptor.Finalize() + + if e := a.PWMPinsAdaptor.Finalize(); e != nil { + err = multierror.Append(err, e) + } + + if e := a.AnalogPinsAdaptor.Finalize(); e != nil { + err = multierror.Append(err, e) + } + + if e := a.I2cBusAdaptor.Finalize(); e != nil { + err = multierror.Append(err, e) + } + + if e := a.SpiBusAdaptor.Finalize(); e != nil { + err = multierror.Append(err, e) + } + + if e := a.OneWireBusAdaptor.Finalize(); e != nil { + err = multierror.Append(err, e) + } + + return err +} diff --git a/platforms/radxa/zero/adaptor_test.go b/platforms/radxa/zero/adaptor_test.go new file mode 100644 index 000000000..e21f59dcd --- /dev/null +++ b/platforms/radxa/zero/adaptor_test.go @@ -0,0 +1,224 @@ +package zero + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "gobot.io/x/gobot/v2" + "gobot.io/x/gobot/v2/drivers/aio" + "gobot.io/x/gobot/v2/drivers/gpio" + "gobot.io/x/gobot/v2/drivers/i2c" + "gobot.io/x/gobot/v2/platforms/adaptors" + "gobot.io/x/gobot/v2/system" +) + +const ( + pwmDir = "/sys/devices/platform/soc/ffd00000.bus/ffd1a000.pwm/pwm/pwmchip2/" //nolint:gosec // false positive + pwmExportPath = pwmDir + "export" + pwmUnexportPath = pwmDir + "unexport" + pwmPwmDir = pwmDir + "pwm0/" + pwmEnablePath = pwmPwmDir + "enable" + pwmPeriodPath = pwmPwmDir + "period" + pwmDutyCyclePath = pwmPwmDir + "duty_cycle" + pwmPolarityPath = pwmPwmDir + "polarity" + + pwmInvertedIdentifier = "inversed" +) + +var pwmMockPaths = []string{ + pwmExportPath, + pwmUnexportPath, + pwmEnablePath, + pwmPeriodPath, + pwmDutyCyclePath, + pwmPolarityPath, +} + +// make sure that this Adaptor fulfills all the required interfaces +var ( + _ gobot.Adaptor = (*Adaptor)(nil) + _ gobot.DigitalPinnerProvider = (*Adaptor)(nil) + _ gobot.PWMPinnerProvider = (*Adaptor)(nil) + _ gpio.DigitalReader = (*Adaptor)(nil) + _ gpio.DigitalWriter = (*Adaptor)(nil) + _ aio.AnalogReader = (*Adaptor)(nil) + _ i2c.Connector = (*Adaptor)(nil) +) + +func preparePwmFs(fs *system.MockFilesystem) { + fs.Files[pwmEnablePath].Contents = "0" + fs.Files[pwmPeriodPath].Contents = "0" + fs.Files[pwmDutyCyclePath].Contents = "0" + fs.Files[pwmPolarityPath].Contents = pwmInvertedIdentifier +} + +func initConnectedTestAdaptorWithMockedFilesystem(mockPaths []string) (*Adaptor, *system.MockFilesystem) { + a := initConnectedTestAdaptor() + fs := a.sys.UseMockFilesystem(mockPaths) + return a, fs +} + +func initConnectedTestAdaptor() *Adaptor { + a := NewAdaptor() + if err := a.Connect(); err != nil { + panic(err) + } + return a +} + +func TestNewAdaptor(t *testing.T) { + // arrange & act + a := NewAdaptor() + // assert + assert.IsType(t, &Adaptor{}, a) + assert.True(t, strings.HasPrefix(a.Name(), "Zero")) + assert.NotNil(t, a.sys) + assert.NotNil(t, a.mutex) + assert.NotNil(t, a.AnalogPinsAdaptor) + assert.NotNil(t, a.DigitalPinsAdaptor) + assert.NotNil(t, a.PWMPinsAdaptor) + assert.NotNil(t, a.I2cBusAdaptor) + assert.NotNil(t, a.SpiBusAdaptor) + assert.True(t, a.sys.HasDigitalPinCdevAccess()) + // act & assert + a.SetName("NewName") + assert.Equal(t, "NewName", a.Name()) +} + +func TestNewAdaptorWithOption(t *testing.T) { + // arrange & act + a := NewAdaptor(adaptors.WithGpiosActiveLow("1"), adaptors.WithGpioSysfsAccess()) + // assert + require.NoError(t, a.Connect()) + assert.True(t, a.sys.HasDigitalPinSysfsAccess()) +} + +func TestDigitalIO(t *testing.T) { + // some basic tests, further tests are done in "digitalpinsadaptor.go" + // arrange + a := initConnectedTestAdaptor() + dpa := a.sys.UseMockDigitalPinAccess() + require.True(t, a.sys.HasDigitalPinCdevAccess()) + // act & assert write + err := a.DigitalWrite("7", 1) + require.NoError(t, err) + assert.Equal(t, []int{1}, dpa.Written("gpiochip1", "3")) + // arrange, act & assert read + dpa.UseValues("gpiochip1", "1", []int{3}) + i, err := a.DigitalRead("10") + require.NoError(t, err) + assert.Equal(t, 3, i) + // act and assert unknown pin + require.ErrorContains(t, a.DigitalWrite("99", 1), "'99' is not a valid id for a digital pin") + // act and assert finalize + require.NoError(t, a.Finalize()) + assert.Equal(t, 0, dpa.Exported("gpiochip1", "3")) + assert.Equal(t, 0, dpa.Exported("gpiochip1", "1")) +} + +func TestDigitalIOSysfs(t *testing.T) { + // some basic tests, further tests are done in "digitalpinsadaptor.go" + // arrange + a := NewAdaptor(adaptors.WithGpioSysfsAccess()) + require.NoError(t, a.Connect()) + dpa := a.sys.UseMockDigitalPinAccess() + require.True(t, a.sys.HasDigitalPinSysfsAccess()) + // act & assert write + err := a.DigitalWrite("7", 1) + require.NoError(t, err) + assert.Equal(t, []int{1}, dpa.Written("", "415")) + // arrange, act & assert read + dpa.UseValues("", "413", []int{4}) + i, err := a.DigitalRead("10") + require.NoError(t, err) + assert.Equal(t, 4, i) + // act and assert unknown pin + require.ErrorContains(t, a.DigitalWrite("99", 1), "'99' is not a valid id for a digital pin") + // act and assert finalize + require.NoError(t, a.Finalize()) + assert.Equal(t, 0, dpa.Exported("", "415")) + assert.Equal(t, 0, dpa.Exported("", "413")) +} + +func TestAnalogRead(t *testing.T) { + mockPaths := []string{ + "/sys/class/thermal/thermal_zone0/temp", + } + + a, fs := initConnectedTestAdaptorWithMockedFilesystem(mockPaths) + + fs.Files["/sys/class/thermal/thermal_zone0/temp"].Contents = "567\n" + got, err := a.AnalogRead("cpu_thermal") + require.NoError(t, err) + assert.Equal(t, 567, got) + + _, err = a.AnalogRead("thermal_zone10") + require.ErrorContains(t, err, "'thermal_zone10' is not a valid id for an analog pin") + + fs.WithReadError = true + _, err = a.AnalogRead("cpu_thermal") + require.ErrorContains(t, err, "read error") + fs.WithReadError = false + + require.NoError(t, a.Finalize()) +} + +func TestFinalizeErrorAfterGPIO(t *testing.T) { + // arrange + a := initConnectedTestAdaptor() + dpa := a.sys.UseMockDigitalPinAccess() + require.True(t, a.sys.HasDigitalPinCdevAccess()) + require.NoError(t, a.DigitalWrite("7", 1)) + dpa.UseUnexportError("gpiochip1", "3") + // act + err := a.Finalize() + // assert + require.ErrorContains(t, err, "unexport error") +} + +func TestFinalizeErrorAfterPWM(t *testing.T) { + // indirect test for PWM.Finalize() is called for the adaptor + // arrange + a, fs := initConnectedTestAdaptorWithMockedFilesystem(pwmMockPaths) + preparePwmFs(fs) + require.NoError(t, a.PwmWrite("18", 1)) + fs.WithWriteError = true + // act + err := a.Finalize() + // assert + require.ErrorContains(t, err, "write error") +} + +func TestSpiDefaultValues(t *testing.T) { + a := NewAdaptor() + + assert.Equal(t, 0, a.SpiDefaultBusNumber()) + assert.Equal(t, 0, a.SpiDefaultChipNumber()) + assert.Equal(t, 0, a.SpiDefaultMode()) + assert.Equal(t, 8, a.SpiDefaultBitCount()) + assert.Equal(t, int64(10000000), a.SpiDefaultMaxSpeed()) +} + +func TestI2cDefaultBus(t *testing.T) { + a := NewAdaptor() + assert.Equal(t, 3, a.DefaultI2cBus()) +} + +func TestI2cFinalizeWithErrors(t *testing.T) { + // arrange + a := initConnectedTestAdaptor() + a.sys.UseMockSyscall() + fs := a.sys.UseMockFilesystem([]string{"/dev/i2c-4"}) + con, err := a.GetI2cConnection(0xff, 4) + require.NoError(t, err) + _, err = con.Write([]byte{0xbf}) + require.NoError(t, err) + fs.WithCloseError = true + // act + err = a.Finalize() + // assert + require.ErrorContains(t, err, "close error") +} diff --git a/platforms/radxa/zero/doc.go b/platforms/radxa/zero/doc.go new file mode 100644 index 000000000..79cdb7a5b --- /dev/null +++ b/platforms/radxa/zero/doc.go @@ -0,0 +1,7 @@ +/* +Package zero contains the Gobot adaptor for the Radxa Zero. + +For further information refer to the boards README: +https://github.com/hybridgroup/gobot/blob/release/platforms/radxa/zero/README.md +*/ +package zero // import "gobot.io/x/gobot/v2/platforms/radxa/zero" diff --git a/platforms/radxa/zero/pinmap.go b/platforms/radxa/zero/pinmap.go new file mode 100644 index 000000000..a0ca21616 --- /dev/null +++ b/platforms/radxa/zero/pinmap.go @@ -0,0 +1,62 @@ +package zero + +import "gobot.io/x/gobot/v2/platforms/adaptors" + +// tested with cdev on a Zero V1.51 board: dietPi Linux, OK: works, ?: unknown, NOK: not working +// IN: works only as input, PU: if used as input, external pullup resistor needed +// +//nolint:lll // ok here +var gpioPinDefinitions = adaptors.DigitalPinDefinitions{ + "8": {Sysfs: 412, Cdev: adaptors.CdevPin{Chip: 1, Line: 0}}, // GPIOAO_0_UART_AO_A_TXD - ? + "10": {Sysfs: 413, Cdev: adaptors.CdevPin{Chip: 1, Line: 1}}, // GPIOAO_1_UART_AO_A_RXD - OK + "11": {Sysfs: 414, Cdev: adaptors.CdevPin{Chip: 1, Line: 2}}, // GPIOAO_2_I2C_AO_M0_SCL_UART_AO_B_TX_I2C_AO_S0_SCL - OK + "28": {Sysfs: 414, Cdev: adaptors.CdevPin{Chip: 1, Line: 2}}, // GPIOAO_2_I2C_AO_M0_SCL_UART_AO_B_TX_I2C_AO_S0_SCL - OK + "7": {Sysfs: 415, Cdev: adaptors.CdevPin{Chip: 1, Line: 3}}, // GPIOAO_3_I2C_AO_M0_SDA_UART_AO_B_RX_I2C_AO_S0_SDA - OK + "27": {Sysfs: 415, Cdev: adaptors.CdevPin{Chip: 1, Line: 3}}, // GPIOAO_3_I2C_AO_M0_SDA_UART_AO_B_RX_I2C_AO_S0_SDA - OK + "32": {Sysfs: 416, Cdev: adaptors.CdevPin{Chip: 1, Line: 4}}, // GPIOAO_4_PWMAO_C - OK + "35": {Sysfs: 420, Cdev: adaptors.CdevPin{Chip: 1, Line: 8}}, // GPIOAO_8_UART_AO_B_TX - OK + "37": {Sysfs: 421, Cdev: adaptors.CdevPin{Chip: 1, Line: 9}}, // GPIOAO_9_UART_AO_B_RX - OK + "LED": {Sysfs: 422, Cdev: adaptors.CdevPin{Chip: 1, Line: 10}}, // GPIOAO_10_PWMAO_D - Wired to LED besides USB-C + "40": {Sysfs: 423, Cdev: adaptors.CdevPin{Chip: 1, Line: 11}}, // GPIOAO_11_PWMAO_A - OK + "19": {Sysfs: 447, Cdev: adaptors.CdevPin{Chip: 0, Line: 20}}, // GPIOH_4_UART_EE_C_RTS_SPI_B_MOSI - OK + "21": {Sysfs: 448, Cdev: adaptors.CdevPin{Chip: 0, Line: 21}}, // GPIOH_5_UART_EE_C_CTS_SPI_B_MISO_PWM_F - OK + "24": {Sysfs: 449, Cdev: adaptors.CdevPin{Chip: 0, Line: 22}}, // GPIOH_6_UART_EE_C_RX_SPI_B_SS0_I2C_EE_M1_SDA - OK + "23": {Sysfs: 450, Cdev: adaptors.CdevPin{Chip: 0, Line: 23}}, // GPIOH_7_UART_EE_C_TX_SPI_B_SCLK_I2C_EE_M1_SCL - OK + "36": {Sysfs: 451, Cdev: adaptors.CdevPin{Chip: 0, Line: 24}}, // GPIOH_8 - OK + "22": {Sysfs: 475, Cdev: adaptors.CdevPin{Chip: 0, Line: 48}}, // GPIOC_7 - OK + "3": {Sysfs: 490, Cdev: adaptors.CdevPin{Chip: 0, Line: 63}}, // GPIOA_14_I2C_EE_M3_SDA - OK (i2c-3) + "5": {Sysfs: 491, Cdev: adaptors.CdevPin{Chip: 0, Line: 64}}, // GPIOA_15_I2C_EE_M3_SCL - OK (i2-c3) + "18": {Sysfs: 500, Cdev: adaptors.CdevPin{Chip: 0, Line: 73}}, // GPIOX_8_SPI_A_MOSI_PWM_C_TDMA_D1 - OK + "12": {Sysfs: 501, Cdev: adaptors.CdevPin{Chip: 0, Line: 74}}, // GPIOX_9_SPI_A_MISO_TDMA_D0 - OK + "16": {Sysfs: 502, Cdev: adaptors.CdevPin{Chip: 0, Line: 75}}, // GPIOX_10_SPI_A_SS0_I2C_EE_M1_SDA_TDMA_FS - OK + "13": {Sysfs: 503, Cdev: adaptors.CdevPin{Chip: 0, Line: 76}}, // GPIOX_11_SPI_A_SCLK_I2C_EE_M1_SCL_TDMA_SCLK - OK +} + +var pwmPinDefinitions = adaptors.PWMPinDefinitions{ + // enabled by default, but pin seems to be not really enabled on pwm usage, channel 1 used by "regulator-vddcpu" + "32": {Dir: "/sys/devices/platform/soc/ff800000.bus/ff802000.pwm/pwm/", DirRegexp: "pwmchip[0|1|2|3|4]$", Channel: 0}, + "40": {Dir: "/sys/devices/platform/soc/ff800000.bus/ff807000.pwm/pwm/", DirRegexp: "pwmchip[0|1|2|3|4]$", Channel: 0}, + "18": {Dir: "/sys/devices/platform/soc/ffd00000.bus/ffd1a000.pwm/pwm/", DirRegexp: "pwmchip[0|1|2|3|4]$", Channel: 0}, + // enabled by default, but pin seems to be not really enabled on pwm usage, channel 0 used by "wifi32k" + "21": {Dir: "/sys/devices/platform/soc/ffd00000.bus/ffd19000.pwm/pwm/", DirRegexp: "pwmchip[0|1|2|3|4]$", Channel: 1}, +} + +var analogPinDefinitions = adaptors.AnalogPinDefinitions{ + // +/-273.200 °C need >=7 characters to read: +/-273200 millidegree Celsius + // names equals /sys/class/thermal/thermal_zone*/hwmon*/name + "cpu_thermal": {Path: "/sys/class/thermal/thermal_zone0/temp", W: false, ReadBufLen: 7}, + "ddr_thermal": {Path: "/sys/class/thermal/thermal_zone1/temp", W: false, ReadBufLen: 7}, + // 2 channel 12-bit SAR ADC, 0..4095, so 4 characters to read + "15": { + Path: "/sys/bus/platform/drivers/meson-saradc/ff809000.adc/iio:device0/in_voltage1_raw", W: false, ReadBufLen: 4, + }, + "15_mean": { + Path: "/sys/bus/platform/drivers/meson-saradc/ff809000.adc/iio:device0/in_voltage1_mean_raw", W: false, ReadBufLen: 4, + }, + "26": { + Path: "/sys/bus/platform/drivers/meson-saradc/ff809000.adc/iio:device0/in_voltage2_raw", W: false, ReadBufLen: 4, + }, + "26_mean": { + Path: "/sys/bus/platform/drivers/meson-saradc/ff809000.adc/iio:device0/in_voltage2_mean_raw", W: false, ReadBufLen: 4, + }, +}