SPI on Orange Pi Zero

I had no need for SPI on my many Orange Pi Zero. Now I do, and thus enabling it is needed. Not hard, but not trivial either, which is why I document it here:

  1. Have a recent Armbian, e.g. https://www.armbian.com/orange-pi-zero/
  2. Run “sudo armbian-config”, System, Hardware, enable spi-spidev
  3. Reboot

In theory that should be it and you should now have a /dev/spidev0.0, however I had to edit /boot/armbianEnv.txt first:

verbosity=1
bootlogo=false
console=both
disp_mode=1920x1080p60
overlay_prefix=sun8i-h3
overlays=spi-spidev tve usbhost2 usbhost3
param_spidev_spi_bus=1
param_spidev_max_freq=100000000
rootdev=UUID=01234567-ffff-eeee-dddd-000000000001
rootfstype=ext4
usbstoragequirks=0x2537:0x1066:u,0x2537:0x1068:u

and add the two param_spidev_* lines. Once rebooted, I finally got /dev/spidev1.0

Note that the pins for spidev0 are not available on an Orange Pi Zero, while the pins for spidev1 are:

MicroSD Cards Performance

App Performance Class Spec Table

I read about this “A1” specification on microSD cards: it’s supposed to be “App Performance” and is technically a minimum read and write IOPS.

So I got one to find out how much of a difference it makes in an OrangePi Zero running Armbian.

Simple test:

  1. Prepare an existing microSD card (I-O Data, Class 4, 8GB)
    • apt clear
    • apt update
    • shutdown
  2. Copy that card on another PC
    • dd bs=128k if=/dev/sdb of=8g.img
  3. Copy that image to the A1 card (Kingston Canvas Select Plus 16GB)
    • dd bs=128k if=8g.img of=/dev/sdb
  4. Now do the following commands after booting each card

This was the result:

CommandOld cardNew card
ls -Rl /usr >/dev/null5.6″4.9″
apt update2’22”1’57”
apt upgrade -y24’10”10’2″
Linux commands using different microSD cards

I thought the “ls” will make a bigger difference, but I guess the filesystem and its caching does a good job. The “apt update” is mainly pulling data from the Internet. But the “apt upgrade” was impressive: 97 packets were updated in both cases and while it includes a lot of data pulling from the Internet, the main time is spend on unpacking and installing all those Debian packets. That’s reading and writing plenty files. And it shows.

I did not expect the difference to be that much. I’m sold and will replace any future microSD cards with A1 models (note A2 needs drivers for queuing and without driver support, they can be slower than A1 models).

BME280 and Node.js on OrangePi

I got some more sensors, like an Bosch BME280 which uses I²C bus. Neat as it can measure temperature, air pressure and humidity.

Using it on an OrangePi Zero is simple:

  • Enable the I2C-0 module: in /boot/armbianEnv.txt add “i2c0” (or use i2c1 or i2c2) to the overlay.
  • Create /etc/udev/rules.d/60-i2c-tools.rules with the following content:
KERNEL=="i2c-0" , GROUP="i2c", MODE="0660"
KERNEL=="i2c-[1-9]*", GROUP="i2c", MODE="0666"
  • Add yourself to the i2c group:
useradd -a -G i2c harald
  • Reboot

You now have an accessible /dev/i2c-0 (or i2c-1 or i2c-2). You can check you have a BME280 visible on the I2C bus:

harald@opz2:~$ i2cdetect 0
WARNING! This program can confuse your I2C bus, cause data loss and worse!
I will probe file /dev/i2c-0.
I will probe address range 0x03-0x77.
Continue? [Y/n] y
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- 76 --                         

0x76 is the default I2C address of a BME280 (0x77 being the alternative address).

Now read it via Node.js (package from here):

const BME280 = require('bme280-sensor');

// The BME280 constructor options are optional.
// 
const options = {
  i2cBusNo   : 0, // defaults to 1
  i2cAddress : 0x76 //BME280.BME280_DEFAULT_I2C_ADDRESS() // defaults to 0x77
};

const bme280 = new BME280(options);

// Read BME280 sensor data, repeat
//
const readSensorData = () => {
  bme280.readSensorData()
    .then((data) => {
      // temperature_C, pressure_hPa, and humidity are returned by default.
      // I'll also calculate some unit conversions for display purposes.
      //
      data.temperature_F = BME280.convertCelciusToFahrenheit(data.temperature_C);
      data.pressure_inHg = BME280.convertHectopascalToInchesOfMercury(data.pressure_hPa);
 
      console.log(`data = ${JSON.stringify(data, null, 2)}`);
      setTimeout(readSensorData, 2000);
    })
    .catch((err) => {
      console.log(`BME280 read error: ${err}`);
      setTimeout(readSensorData, 2000);
    });
};

// Initialize the BME280 sensor
//
bme280.init()
  .then(() => {
    console.log('BME280 initialization succeeded');
    readSensorData();
  })
  .catch((err) => console.error(`BME280 initialization failed: ${err} `));

And it works:

harald@opz2:~/js/bme280$ node bme280.js 
Found BMx280 chip ID 0x60 on bus i2c-0, address 0x76
BME280 initialization succeeded
data = {
  "temperature_C": 23.19,
  "humidity": 35.62696153399403,
  "pressure_hPa": 1003.0408627864141,
  "temperature_F": 73.742,
  "pressure_inHg": 29.619784150102433
}
[...]

Reading Temperatures from many DS18B20 via Node.js

The easiest way:

harald@opz2:~$ cat /sys/bus/w1/devices/28-*/hwmon/hwmon/temp1_input
21812
22375
23375

The problem is that each sensor takes about 800ms (750ms according to data sheet) conversion time. Making this better and reading them concurrently via Node.js:

const fs=require('fs');

const HWSENSORPREFIX='/sys/bus/w1/devices/';

async function getSensors() {
  let sensors=[];
  const dir = await fs.promises.opendir(`${HWSENSORPREFIX}`);
  for await (const dirent of dir) {
    if (dirent.name.slice(0,3)=='28-') {
        sensors.push(dirent.name);
    }
  }
  return sensors;
}

// Print temperature reading from a given 1-Wire sensor

async function printTemp(sensor) {
  const dir = await fs.promises.opendir(`${HWSENSORPREFIX}/${sensor}/hwmon`);
  for await (const dirent of dir) {
    if (dirent.name.slice(0, 5)=='hwmon') {
      let temp = parseFloat(await fs.promises.readFile(`${HWSENSORPREFIX}/${sensor}/hwmon/${dirent.name}/temp1_input`))/1000.0;
      console.log(`${dirent.name} (${sensor}): ${temp}`);
    }
  }
}

(async function () {
  try {
    let a=await getSensors();
    a.forEach(x => printTemp(x));
  } catch(e) {
    console.error(`Error: ${e}`);
  }
})();

That’s reading all in parallel (plus about 600ms startup time):

harald@opz2:~$ node read-temp.js
hwmon0 (28-80000004215e): 23.312
hwmon2 (28-0316884578ff): 21.812
hwmon1 (28-0316884fabff): 22.312

Adding sensors changes the time until all are read:

DS18B20 Countcatread-1w-temp.js
10.851.45
21.691.51
32.501.53
Time to read in [s] for 1 to 3 DS18B20 temperature sensors

OrangePi Zero + DS18B20

Making my OrangePi Zero useful (again) with updating it to the latest Armbian and connecting a 1-Wire DS18B20 temperature sensor to it.

OrangePi Zero on the left, DS18B20 on the right

Installing

Get a microSD card and install the latest Armbian image for your Zero. Boot and do the usual setup: set root password, create a new user, copy ssh keys etc.

Hardware Related Setup

Here the fun part starts. Define the features you want to use on the 26 pin header (see at the Sunxi wiki).

Edit /boot/armbianEnv.txt

Add the w1-gpio overlay and configure it to use PA07 (pin 12 on the 26 pin header) and enable pull-up (not really needed though as we need an external pull-up resistor anyway). Reboot when done. Other pins are ok too. PA07 works though.

overlay_prefix=sun8i-h3
overlays=usbhost2 usbhost3 w1-gpio
param_w1_pin=PA07
param_w1_pin_int_pullup=1

The name of the overlay are from /boot/dtb/overlay/.

Wire up everything

DS18B20 has 3 pins: GND, 3.3V and DATA. DATA goes to PA07. Don’t forget a 4.7k pull-up resistor between 3.3V and DATA.

Measure Temperature

If everything worked, you will have a temp1_input which has the temperature in °C * 1000:

harald@opz2:/boot$ modprobe w1_therm
harald@opz2:/boot$ lsmod | grep w1
w1_therm               16384  0
w1_gpio                16384  0
wire                   24576  2 w1_gpio,w1_therm
harald@opz2:/boot$ dmesg
[...]
[  142.370130] w1_master_driver w1_bus_master1: Attaching one wire slave 28.80000004215e crc 94
harald@opz2:/boot$ cat /sys/bus/w1/devices/28-80000004215e/hwmon/hwmon0/temp1_input
23812

2020-04-09 Quick update: Adding a 2nd DS18B20 was picked up automatically and now I have 2 sensors:

harald@opz2:~$ cat /sys/bus/w1/devices/28-80000004215e/hwmon/hwmon0/temp1_input
24187
harald@opz2:~$ cat /sys/bus/w1/devices/28-0316884fabff/hwmon/hwmon1/temp1_input 
24375