Getting WaveShare High-Precision AD/DA Expansion Board to work

I am trying to get the WaveShare High-Precision AD/DA Expansion Board to work in a project.

It seems I aam having difficulty getting the SPI communication with the WaveShare board to work. Currently, everything works until I try to send a byte to the WaveShare. board using SPI, then the container exits.

Following is my Docker.template file (I put single quotes infront of # to keep the lines from being bolded)

FROM balenalib/%%BALENA_MACHINE_NAME%%-python:3-stretch-run

'# enable container init system.
ENV INITSYSTEM on

'# use install_packages if you need to install dependencies,
'# for instance if you need git, just uncomment the line below.
'# RUN install_packages git
RUN pip install --upgrade pip
RUN apt-get update && apt-get install -yq --no-install-recommends build-essential rpi.gpio
RUN apt-get install python-dev
RUN apt-get install python-smbus
RUN apt-get install python-serial
RUN pip install spidev

RUN apt-get install python-imaging

'# Set our working directory
WORKDIR /usr/src/automated_testing_button_panel

'# Copy requirements.txt first for better cache on later pushes
COPY requirements.txt requirements.txt

'# pip install python deps from requirements.txt on the resin.io build server
RUN pip install -r requirements.txt

'# This will copy all files in our root to the working directory in the container
COPY . ./

'# Enable udevd so that plugged dynamic hardware devices show up in our container.
'#ENV UDEV=1

CMD modprobe i2c-dev
CMD modprobe i2c-bcm2708

'# main.py will run when container starts up on the device
CMD [“python”,"-u",“src/automated_testing_button_panel_main.py”]

And I have RPi.GPIO, pySerial, and wiringpi in my “requirements.txt” file

In “Define DT parameters” I have

“i2c_arm=on”,“spi=on”

The following page has WaveShare’s installation instructions, https://www.waveshare.com/wiki/Libraries_Installation_for_RPi

Thanks for your help.

Hi, do you have any logs detailing what is causing the container to exit as that would probably be the easiest place to start?

I import ADS1256 from pipyadc, then initialize it in my main with the following.

self.ads = ADS1256()

In the constructor for ADS1256 object the following reset() method is called to define its initial state (I added the print functions for debugging)

def reset(self):
    """Reset all registers except CLK0 and CLK1 bits
    to reset values and Polls for DRDY change / timeout afterwards.
    """
    print('1')
    self._chip_select()
    print('2')
    self._send_byte(CMD_RESET)
    print('3')
    self.wait_DRDY()
    # Release chip select and implement t_11 timeout
    print('4')
    self._chip_release()

Following is the relevant log output.

02.04.20 10:17:49 (-0400) automated_testing_button_panel Initialize ads_dac
02.04.20 10:17:49 (-0400) automated_testing_button_panel Before ADS1256 device reset
02.04.20 10:17:49 (-0400) automated_testing_button_panel 1
02.04.20 10:17:49 (-0400) automated_testing_button_panel 2
02.04.20 10:17:52 (-0400) Service exited ‘automated_testing_button_panel sha256:5830def359605aa4677362d4144a59ba8e61b93c488e05720934960a2ed7e62e’

So the service is exited when it attempts

self._send_byte(CMD_RESET)

and following is the definition for

def _send_byte(self, mybyte):
    # Sends a byte via the SPI bus
    # Workaround for single-byte transfers due to mutation of immutable
    # function argument. Thanks @JKR
    # wiringPiSPIDataRW() returns a Linux ioctl() error code.
    # We ignore that since we already checked for presence of the file
    # descriptor of the SPI device during initialisation.
    wp.wiringPiSPIDataRW(self.SPI_CHANNEL, "%s" % chr(mybyte&0xFF))

This is why I am thinking it is the setup of the SPI for controlling the WaveShare AD/DA board

It seems very strange that there’s no error message for your app when it stops. Are you sure the script is meant to continue? You’re not masking stdout/stderr/errors at all are you? I can try and get some more tips from some of our devices guys too.

Since the command “wait_DRDY()” is called after “_send_byte()” in “reset()”, to “Delays until the configured DRDY input pin is pulled to active logic low level by the ADS1256 hardware or until the DRDY_TIMEOUT in seconds has passed”, I am pretty sure the script is meant to continue.

I am not redirecting stdout/stderr/errors.

I also thought it was strange that there were no error messages before it exited, so I also tried putting a try…except around the _send_byte() command but don’t see any exceptions get thrown.

Hi @mdcraver

Looking over the code and log output you’ve pasted there’s unfortunately not much more that we can do without having access to the same hardware as you. The log message Service exited ... would indicate that the script exited with a zero exitcode (ie didn’t crash). Unfortunately we can’t really assist you debug your code, but please do remember to update us here if you find a solution as it may help other users working with the same library. Hopefully someone else on the forums will be able to assist with the issue.

Good luck!
James.

How could I perform all the library installation steps in the following using my Docker.template?

https://www.waveshare.com/wiki/Libraries_Installation_for_RPi

You can pretty much just put a RUN in front of the required shell commands for most instances, although the raspi-config stuff is done a little differently on balena. This doc will be of use: https://www.balena.io/docs/learn/develop/hardware/i2c-and-spi/#raspberry-pi-family

After working on this for a while, I found someone else who was having problems getting the SPI to work in docker. https://community.home-assistant.io/t/enable-spi-and-gpio-in-docker/145905

They said that they successfully replaced the wiringpi calls with the corresponding pigpio calls

I went through and replaced all the wiringpi calls with pigpio calls and created my own replacement function for the wiringpi microsecond delay method.

I am trying to run the pigpio daemon in a separate container.

However, it seems I am having problems successfully starting the pigpio daemon. When it runs I get the following message.

07.04.20 11:06:39 (-0400)  AD_test  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
07.04.20 11:06:39 (-0400)  AD_test  Can't connect to pigpio at soft(8888)
07.04.20 11:06:39 (-0400)  AD_test  
07.04.20 11:06:39 (-0400)  AD_test  Did you start the pigpio daemon? E.g. sudo pigpiod
07.04.20 11:06:39 (-0400)  AD_test  
07.04.20 11:06:39 (-0400)  AD_test  Did you specify the correct Pi host/port in the environment
07.04.20 11:06:39 (-0400)  AD_test  variables PIGPIO_ADDR/PIGPIO_PORT?
07.04.20 11:06:39 (-0400)  AD_test  E.g. export PIGPIO_ADDR=soft, export PIGPIO_PORT=8888
07.04.20 11:06:39 (-0400)  AD_test  
07.04.20 11:06:39 (-0400)  AD_test  Did you specify the correct Pi host/port in the
07.04.20 11:06:39 (-0400)  AD_test  pigpio.pi() function? E.g. pigpio.pi('soft', 8888)
07.04.20 11:06:39 (-0400)  AD_test  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
07.04.20 11:06:39 (-0400)  AD_test  Empty main loop

Thanks for your help getting pigpio to work.

Below are all my relevant settings and files


Screen Shot 2020-04-07 at 11.51.24 AM


Device Configuration

pigpio uses GPU memory, so with GPU memory set at less than 64MB I got the following error:

initMboxBlock: init mbox zaps failed

Screen Shot 2020-04-07 at 11.46.12 AM

Device Environment Variables

Screen Shot 2020-04-07 at 11.47.09 AM


Files

docker-compose.yml

version: '2.1'
services:
    pigpio:
        privileged: true
        restart: always
        build: ./rpi-pigpio
        ports:
          - "8888:8888"
        cap_add:
          - SYS_RAWIO
        devices:
          - "/dev/mem"
          - "/dev/vcio"
    AD_test:
        privileged: true
        build: ./AD_test
        depends_on:
            - pigpio

rpi_pigpio

Dockerfile

(from https://github.com/lachatak/rpi-pigpio/blob/master/Dockerfile)

FROM resin/rpi-raspbian:jessie
MAINTAINER Krisztian Lachata <krisztian.lachata@gmail.com>

RUN apt-get update && apt-get upgrade
RUN apt-get install build-essential daemontools git -y

WORKDIR /opt
RUN git config --global http.sslVerify false
RUN git clone https://github.com/joan2937/pigpio
WORKDIR /opt/pigpio
RUN make
RUN make install
RUN ln -s /usr/local/lib/libpigpio.so /usr/lib/libpigpio.so
RUN mkdir -p  /etc/svscan/pigpiod
RUN echo "#!/bin/bash\nif [ ! -f /var/run/pigpio.pid ]; then\n	echo 'Starting'\n	exec /opt/pigpio/pigpiod\nfi" > /etc/svscan/pigpiod/run
RUN chmod +x /etc/svscan/pigpiod/run

EXPOSE 8888

CMD ["/usr/bin/svscan", "/etc/svscan/"]

AD_test

Dockerfile.template

FROM balenalib/%%BALENA_MACHINE_NAME%%-python:3-stretch-run

# enable container init system.
ENV INITSYSTEM on

# use `install_packages` if you need to install dependencies,
# for instance if you need git, just uncomment the line below.
# RUN install_packages git
RUN pip install --upgrade pip
RUN apt-get update && apt-get install -yq --no-install-recommends build-essential rpi.gpio
RUN pip install pigpio
RUN apt-get install pigpio python-pigpio python3-pigpio

# Set our working directory
WORKDIR /usr/src/AD_test

# Copy requirements.txt first for better cache on later pushes
COPY requirements.txt requirements.txt

# pip install python deps from requirements.txt on the resin.io build server
RUN pip install -r requirements.txt

# This will copy all files in our root to the working  directory in the container
COPY . ./

# Enable udevd so that plugged dynamic hardware devices show up in our container.
ENV UDEV=1

# main.py will run when container starts up on the device
CMD ["python","-u","src/AD_test_main.py"]

requirements

RPi.GPIO
pySerial

AD_test_main.py

#!/usr/bin/python
# -*- coding: utf-8 -*-

import time

from ADS1256_definitions import *
from pipyadc import ADS1256



def main():
    print("Empty main loop")
    time.sleep(10)


if __name__ == "__main__":
    main()

pipyadc.py

(following is a reduced snippet with init and SPI read/write)

# -*- coding: utf-8 -*-
"""PiPyADC - Python module for interfacing Texas Instruments SPI
bus based analog-to-digital converters with the Raspberry Pi.

Currently only implemented class in this module is ADS1256 for the
ADS1255 and ADS1256 chips which are register- and command compatible.

Download: https://github.com/ul-gh/PiPyADC

Depends on WiringPi library, see:
https://github.com/WiringPi/WiringPi-Python

Uses code from: https://github.com/heathsd/PyADS1256

License: GNU LGPLv2.1, see:
https://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html

Ulrich Lukas, 2017-03-03
"""
import time

#import wiringpi as wp
import pigpio
#pi_gpio = pigpio.pi()
pi_gpio = pigpio.pi('soft', 8888)
import pigpio_delay

from ADS1256_definitions import *
import ADS1256_default_config

class ADS1256(object):
    """Python class for interfacing the ADS1256 and ADS1255 analog to
    digital converters with the Raspberry Pi.

    This is part of module PiPyADC
    Download: https://github.com/ul-gh/PiPyADC
    
    Default pin and settings configuration is for the Open Hardware
    "Waveshare High-Precision AD/DA Board"

    See file ADS1256_default_config.py for
    configuration settings and description.

    Register read/write access is implemented via Python class/instance
    properties. Commands are implemented as functions.

    See help(ADS1256) for usage of the properties and functions herein.

    See ADS1256_definitions.py for chip registers, flags and commands.
    
    Documentation source: Texas Instruments ADS1255/ADS1256
    datasheet SBAS288: http://www.ti.com/lit/ds/sbas288j/sbas288j.pdf
    """

   .
   .
   .

    # Constructor for the ADC object: Hardware pin configuration must be
    # set up at initialization phase and can not be changed later.
    # Register/Configuration Flag settings are initialized, but these
    # can be changed during runtime via class properties.
    # Default config is read from external file (module) import
    def __init__(self, conf=ADS1256_default_config):
        
        # Set up the wiringpi object to use physical pin numbers
        #wp.wiringPiSetupPhys()
        
        # Config and initialize the SPI and GPIO pins used by the ADC.
        # The following four entries are actively used by the code:
        self.SPI_CHANNEL  = conf.SPI_CHANNEL
        self.DRDY_PIN     = conf.DRDY_PIN
        self.CS_PIN       = conf.CS_PIN
        self.DRDY_TIMEOUT = conf.DRDY_TIMEOUT
        self.DRDY_DELAY   = conf.DRDY_DELAY
        
        #DA settings
        self.DA_CS_PIN = 16

        # Only one GPIO input:
        if conf.DRDY_PIN is not None:
            self.DRDY_PIN = conf.DRDY_PIN
            '''
            wp.pinMode(conf.DRDY_PIN,  wp.INPUT)
            '''
            pi_gpio.set_mode(conf.DRDY_PIN, pigpio.INPUT)

        # GPIO Outputs. Only the CS_PIN is currently actively used. ~RESET and
        # ~PDWN must be set to static logic HIGH level if not hardwired:
        for pin in (conf.CS_PIN,
                    conf.RESET_PIN,
                    conf.PDWN_PIN):
            if pin is not None:
                pi_gpio.pinMode(pin, pigpio.OUTPUT)
                '''
                wp.pinMode(pin, wp.OUTPUT)
                '''
                pi_gpio.write(pin, 1)
                '''
                wp.digitalWrite(pin, wp.HIGH)
                '''
        
        # Initialize the wiringpi SPI setup. Return value is the Linux file
        # descriptor for the SPI bus device:
        self.spi_handle = pi_gpio.spi_open(conf.SPI_CHANNEL, conf.SPI_FREQUENCY, conf.SPI_MODE)
        '''
        fd = wp.wiringPiSPISetupMode(
                conf.SPI_CHANNEL, conf.SPI_FREQUENCY, conf.SPI_MODE)
        if fd == -1:
        '''
        if (self.spi_handle == -1):
            raise IOError("ERROR: Could not access SPI device file")
            return False
        
        # ADS1255/ADS1256 command timing specifications. Do not change.
        # Delay between requesting data and reading the bus for
        # RDATA, RDATAC and RREG commands (datasheet: t_6 >= 50*CLKIN period).
        self._DATA_TIMEOUT_US = 1 + (50*1000000)/conf.CLKIN_FREQUENCY
        # Command-to-command timeout after SYNC and RDATAC
        # commands (datasheet: t11)
        self._SYNC_TIMEOUT_US = 1 + (24*1000000)/conf.CLKIN_FREQUENCY
        # See datasheet ADS1256: CS needs to remain low
        # for t_10 = 8*T_CLKIN after last SCLK falling edge of a command.
        # Because this delay is longer than timeout t_11 for the
        # RREG, WREG and RDATA commands of 4*T_CLKIN, we do not need
        # the extra t_11 timeout for these commands when using software
        # chip select selection and the _CS_TIMEOUT_US.
        self._CS_TIMEOUT_US   = 1 + (8*1000000)/conf.CLKIN_FREQUENCY
        # When using hardware/hard-wired chip select, still a command-
        # to command timeout of t_11 is needed as a minimum for the
        # RREG, WREG and RDATA commands.
        self._T_11_TIMEOUT_US   = 1 + (4*1000000)/conf.CLKIN_FREQUENCY

       
        # Initialise class properties
        self.v_ref         = conf.v_ref

        # At hardware initialisation, a settling time for the oscillator
        # is necessary before doing any register access.
        # This is approx. 30ms, according to the datasheet.
        time.sleep(0.03)
        self.wait_DRDY()
        
        print('Before ADS1256 device reset')
        # Device reset for defined initial state
        self.reset()

        # Configure ADC registers:
        # Status register not yet set, only variable written to avoid multiple
        # triggering of the AUTOCAL procedure by changing other register flags
        self._status       = conf.status
        # Class properties now configure registers via their setter functions
        self.mux           = conf.mux
        self.adcon         = conf.adcon
        self.drate         = conf.drate
        self.gpio          = conf.gpio
        self.status        = conf.status
        
        #configure DA cs pin mode
        pi_gpio.set_mode(self.DA_CS_PIN, pigpio.OUTPUT)
        '''
        wp.pinMode(self.DA_CS_PIN, wp.OUTPUT)
        '''
        
   .
   .
   .
   

    def _send_byte(self, mybyte):
        # Sends a byte via the SPI bus
        # Workaround for single-byte transfers due to mutation of immutable
        # function argument. Thanks @JKR
        # wiringPiSPIDataRW() returns a Linux ioctl() error code.
        # We ignore that since we already checked for presence of the file
        # descriptor of the SPI device during initialisation.
        '''
        wp.wiringPiSPIDataRW(self.SPI_CHANNEL, "%s" % chr(mybyte&0xFF))
        '''
        pi_gpio.spi_write(self.spi_handle, "%s" % chr(mybyte&0xFF))

    def _read_byte(self):
        # Returns a byte read via the SPI bus
        (nBytes, MISObyte) = pi_gpio.spi_read(self.spi_handle, 1)
        if (nBytes < 0):
            print('Error: _read_byte failed and gave error code %d', nBytes)
        '''
        MISObyte = wp.wiringPiSPIDataRW(self.SPI_CHANNEL, chr(0xFF))
        '''
        return ord(MISObyte[1])

PDFs of full files for reference.

pipyadc.py.pdf (70.3 KB) ADS1256_default_config.py.pdf (52.9 KB) ADS1256_definitions.py.pdf (21.6 KB) pigpio_delay.py.pdf (17.8 KB)

Hi, have you confirmed that the pigpio daemon is actually running its container? It appears from the logs you are unable to connect to it from the AD_test container so assuming it is running are you sure that you have provided the correct value for PIGPIO_ADDR. Assuming the pigpio daemon is running and this is a networking issue, there are more details on networking here https://www.balena.io/docs/learn/more/masterclasses/services-masterclass/#4-networking-types but you should be able to use the service name i.e. pigpio to connect to that running container.

I am not sure it is running since I get the same error if I don’t include the pigpio container. However, I am not sure how to figure out if the issue is the daemon isn’t running or if it is networking issue relating to connecting to the daemon.

I would imagine if the error is the same that it probably means that the daemon isn’t running. I’d try to execute it directly from the container (not using svscan) and monitor the logs from the dashboard. This should give you some information.

I moved away from using pigpio and instead refactored using SpiDev to perform the SPI functionality.

The refactor is done, but the SPI ins’t working. I scoped the SCK while getting the chip ID and I don’t see the clock signal.

I think the problem is that I am not getting the bcm2835 libraries installed.

What steps do I need to perform, including any needed DT parameters or overlays, in order to use the bcm2835 libraries?

After looking through the balena forums I tried “RUN modprobe bcm2835-412” and “RUN mdprobe v412_common” to no success.

Following is my Dockerfile.template:

FROM balenalib/%%BALENA_MACHINE_NAME%%-python:3-stretch-run

# enable container init system.
ENV INITSYSTEM on

# use `install_packages` if you need to install dependencies,
# for instance if you need git, just uncomment the line below.
# RUN install_packages git
RUN install_packages build-essential
RUN pip install --upgrade pip
RUN apt-get update && apt-get install -yq --no-install-recommends build-essential rpi.gpio
RUN pip install smbus2
RUN pip install spidev
#RUN modprobe bcm2835-v4l2
#RUN modprobe v4l2_common

# Set our working directory
WORKDIR /usr/src/automated_testing_button_panel

# Copy requirements.txt first for better cache on later pushes
COPY requirements.txt requirements.txt

# pip install python deps from requirements.txt on the resin.io build server
RUN pip install -r requirements.txt

# This will copy all files in our root to the working  directory in the container
COPY . ./

# Enable udevd so that plugged dynamic hardware devices show up in our container.
ENV UDEV=1

# main.py will run when container starts up on the device
CMD ["python","-u","src/automated_testing_button_panel_main.py"]

And my DT parameters and overlays.

Following is some additional information.

If I run

ls /dev/spidev*

I get

/dev/spidev0.0 /dev/spidev0.1

I initialize the SPI with the following, and it doesn’t give an error

SPI_FREQUENCY = 976563
SPI_MODE      = 1

# Initialize the wiringpi SPI setup. 
self.spi_handle = spidev.SpiDev()
# Open SPI device "/dev/spidev0.0".
self.spi_handle.open(0,0)
self.spi_handle.max_speed_hz = conf.SPI_FREQUENCY
self.spi_handle.mode = conf.SPI_MODE

if (self.spi_handle == -1):
    raise IOError("ERROR: Could not access SPI device file")

To read the Chip ID byte I use

chipId = self.spi_handle.readbytes(1)

I keep reading the Chip ID in a loop with a 100ms delay between calls, and when I probe the SCK pin with my oscilloscope I never see the clock, only noise.