Controlling the DP0POL switching matrix with daisy chained MAX4820

Aus DL8RDS Wiki
Wechseln zu: Navigation, Suche

1 Background

DP0POL / DP0GVN is an amateur radio station on Neumayer3 station in Antarctica, hosting a unique WSPR station. As I have described in my article Red Pitaya Based WSPR Beacon for Antarctica DP0GVN, how the Red Pitaya platform of the first version was built, we now envisage another version 2:

It will be a 19" rack housing two (or possiby even four) Red Pitaya platforms. The reason is that the standard WSPR / FT8 software is only capale of processing EIGHT bands. However, we would want to monitor all shortwave bands, from long wave over the uneven bands up to 30 MHz.

Besides that, Antarctica is a rather tough place: The Antarctic winter brings us permanent darkness with temperatures possibly down to -50°C and since the antenna field is about two kilometers away from the main station, the SDR platforms will be literally out of reach for human beings. It is like a station on another planet. I guess, living conditions on planet Mars are more comfortable than in the Antarctic winter.

It will be simply impossible to diagnose antenna faults, and in case of a defective Red Pitaya we want to be able to simply switch one Red Pitaya from one antenna to another.

This all will be feasible through a switching matrix built by Helmut Berka DL2MAJ.

The matrix therefore employs a special chip: The MAX4820.

Maybe you have read my article about power switching with bistable latching relays: 8 Port Passive PoE Injector, Managed and Fused. This project is using a combination of I2C controlled PCF7485 16bit serial extensions to steer 8 relays. However, the drive power is too weak to control the relays directly. So we still require a darlington relay driver chip ULN2803 to drive the 5V relays there.

In the Antarctica project the number of relays is a little bigger: The switching matrix holds a total of 28 relays and thus leaves little space for I2C and driver chips. Besides that, I2c has the major disadvantage that each I2C chip normally only has three jumpers fur a selectable bus address space of 2³ and thus a total of 8 chips of the same type on the same bus. Even though it could be enough data lines with 16 bit * 8 ICs, the effort is still quite complex.

Helmut chose another approach: The MAX4820 can be daisy chained and thus requires some very different approach, but you can set many, many more relays with it. In fact, you're not really limited at all, except through a rather complex digital handling technique.

According to the datasheet, the MAX4820 is compatible with the Microwire and/or SPI protocol. I found this statement still rather tricky, because pure SPI does not support the RESET data line, but instead you must take care of a reset signal yourself.

As a consequence, I decided against the implementation directly on a Raspberry Pi with my GPIOs, also given that the timing of the according signals was a little tricky. Instead, I chose an Arduino as the immediate controller platform, which now offers a USB standard interface and thus can be employed together with any arbitrary main computer.

This approach was also a response to the fact that there is no document on the web whatsoever, employing the keywords "MAX4820" together wirh "Raspberry" and "Python".

For this reason the Arduino drives my MAX4820 chain with all its control data lines and the Raspberry hosts the Python program that generates the integers over the serial connection that will again set the MAC ICs accordingly.

2 MAX4820

Here is the datasheet, first of all:

https://datasheets.maximintegrated.com/en/ds/MAX4820-MAX4821.pdf

Read it carefully!

The MAX4820 needs four main data lines:

  • Serial Data (DIN). Sometimes also named "SDA".
  • Serial Clock (SCL).
  • Chip Select (CS): Drive CS low to select the device. When CS is low, data at DIN is clocked into the 8-bit shift register on SCLK’s rising edge. Drive CS from low to high to latch the data to the registers and activate the appropriate relays.
  • Reset (RESET): Reset Input. Drive RESET low to clear all latches and registers (all outputs are turned off). RESET overrides all other inputs. If RESET and SET are pulledlow at the same time, then RESET takes precedence

The DOUT connects to the DIN of the next IC of the chain. Note that the prior chip eats up the first eight data bits and leaves nothing to its successor but what comes after those eight bits:

  • Data Out (DOUT): Serial-Data Output. DOUT is the output of the 8-bit shift register. This output can be used to daisy chain multiple MAX4820s. The data at DOUT appears synchronous to SCLK’s falling edge.

Do not confuse the usage of SCL and SDA with I2C even though the naming is identical, this is no I2C at all. The MAX does not employ any addresses. There is really NO addressing whatsoever.

Here is how the interface is described in the datasheet:

The serial interface consists of an 8-bit shift register
and parallel latch controlled by SCLK and CS. The
input to the shift register is an 8-bit word. Each data bit
controls one of the eight outputs, with the most signifi-
cant bit (D7) corresponding to OUT8 and the least significant 
bit (D0) corresponding to OUT1 (see Table 1).
When CS is low (device is selected), data at DIN  is
clocked into the shift register synchronously with SCLK’s rising 
edge. Driving CS from low to high latches the data in the shift 
register to the parallel latch. 
DOUT is the output of the shift register. Data appears
on DOUT synchronously with SCLK’s falling edge and
is identical to the data at DIN delayed by eight clock
cycles. When shifting the input data, D7 is the first bit in
and out of the shift register. While CS is low, the switches always 
remain in their previous state. Drive CS high after 8 bits of 
data have been shifted in to update the output state and  
inhibit further data from entering the shift register. When  
CS is  high, transitions at DIN and SCLK have no effect  
on the output, and the first input bit (D7) is present at DOUT.
If the number of data bits entered while CS is low is
greater or less than 8, the shift register contains only
the last 8 data bits, regardless of when they were
entered.

Here is how I implemented the interface:

  • The standard period is 50 microseconds. Every time span will be a multiple of 50 microseconds.
  • It makes sense to start with the RESET line going LO in order to set all the latches to ZERO disregarding their previous state. The RESET line can go back to HI after 5 times the standard timespan.
  • Then we will start with Chip Select. We pull it to LO in order to enable programming. CS stays LO as long as there is programing activity on the DIN line.
  • Then we start a loop:
    • We write the due data bit, HI or LO as requested.
    • After writing, we produce a rising flank and a falling flank. Since the data bit will be shifted into the according register with the rising flank, we must ensure to have the data bit set slightly before the clock comes around.
  • After the end of the bits, there are no more clock cycles either.
  • We rise CS to HI in order to activate the latching. This is the moment when the relays will be switched.
  • Then we spend a long time of 50 Milliseconds in order to grant sufficient time to the relays to get the switching done.
  • Finally we do another round of RESET for 5*standard timespans to make sure every bit is cleared again.

This will leave our relays in the situation that the coils still operate under power. Remember that we want to switch RF signals, so operating an inductive element in immediate proximity of a RF signal line is no good idea at all, we will need to remove power from all the coils.

The procedure is exactly the same as above, yet without any data bits. The Data In line can be left to LO and the rest of the procedure is carried out just alike.

Given that OUT1 and OUT2 are antagonists, as OUT3/OUT4, OUT5/OUT6 and OUT7/OUT8, it should be forbidden to set them at the same time. I will not implement any safety procedures in the Arduino code, because I think that the control interface should so clever as to do that at the other side. I follow the idea to keep the static part of the implementation as minimal as possible in order to allow as few incorrectible faults as possible.

3 Measurement

I am proud owner of a R&S HAMEG HMO-3024 mixed signals oscilloscope.

This device is capable of storing a vast number of samples, across all digital channels.

I was using the logic analysis part together with the manual trigger subsystem, where the trigger voltage was set to 3,3V and the logic trigger on the according channels that would start ahead, notably the RESET signal going LO.

4 Arduino Code

Here is my implementation:

#!/usr/bin/python

# Control for the switching matrix of the DB0GVN beacon

# (c) 2017,2018 Markus Heller M.A. DL8RDS <heller@relix.de>

import sys
import serial
import os
import time

ic5 = ['REL1-SHORT50','REL2-SHORT50','REL3-SHORT50','REL4-SHORT50']
ic4 = ['REL4-RP2B','REL4-RP2A','REL4-RP1B','REL4-RP1A']
ic3 = ['REL3-RP2B','REL3-RP2A','REL3-RP1B','REL3-RP1A']
ic2 = ['REL2-RP2B','REL2-RP2A','REL2-RP1B','REL2-RP1A']
ic1 = ['REL1-RP2B','REL1-RP2A','REL1-RP1B','REL1-RP1A']
ic7 = ['REL_GND-SHORT50','REL_50-SHORT50','REL_VNA-TXB1','REL_VNA-TXA1']
ic6 = ['REL_RP2A-MWSP2','REL_RP1A-MWSP2','','']
allRelays = ic5 + ic4 + ic3 + ic2 + ic1 + ic7 + ic6

class relais:

    '''
    Diese Klasse bildet ein Relais ab.
    Sie speichert den Schaltzustand und ermoeglicht das Auslesen.
    '''

    def __init__(self, name):
        self.name = name
        self.state = False

    def setOn(self):
        self.state = True

    def setOff(self):
        self.state = False

    def getState(self):
        return self.state

    def getName(self):
        return self.name

class max4820:

    '''
    Diese Klasse bildet einen MAX4820 mit seinen zugeordneten Relais ab.
    Fokus ist auf dem Setzen des jeweiligen Relaiszustandes.
    '''

    def __init__(self, name):
        self.name = name
        self.relaisList = []
        self.relaisIndex = {}

    def getName(self):
        return self.name

    def addRelaisList(self, rellist):
        for name in rellist:
            r = relais(name)
            self.relaisList.append(r)
            self.relaisIndex[name] = r

    def getStateByName(self, name):
        if self.relaisIndex.has_key(name):
            return self.relaisIndex[name].getState()

    def setOnByName(self, name):
        if self.relaisIndex.has_key(name):
            return self.relaisIndex[name].setOn()

    def setOffByName(self, name):
        if self.relaisIndex.has_key(name):
            return self.relaisIndex[name].setOff()

    def getStates(self):
        return [n.getState() for n in self.relaisList]

    def twoLine(self, state):
        if state:
            return '10'
        else:
            return '01'

    def getBinStates(self):
        return str(''.join([self.twoLine(n.getState()) for n in self.relaisList]))

    def getIntState(self):
        return str(int(''.join([self.twoLine(n.getState()) for n in self.relaisList]), 2))

class matrix:

    '''
    Diese Klasse bildet die Matrixplatine ab. 
    Fokus ist auf der Reihenfolge der ICs und die Reihenfolge der Relais
    '''

    def __init__(self):
        self.icList = []
        self.relayDict = {}
        self.icDict = {}

        self.allRelays = allRelays

        self.addMAX4820('ic5', ic5)
        self.addMAX4820('ic4', ic4)
        self.addMAX4820('ic3', ic3)
        self.addMAX4820('ic2', ic2)
        self.addMAX4820('ic1', ic1)
        self.addMAX4820('ic7', ic7)
        self.addMAX4820('ic6', ic6)

    def relayExists(self, name):
        if name in self.allRelays:
            return True
        return False

    def setRel(self, relName, state):
        self.relayDict[relName] = state

    def addMAX4820(self, name, relList):
        self.icList.append(name)
        m = max4820(name)
        m.addRelaisList(relList)
        self.icDict[name] = m
        for r in relList:
            self.relayDict[r] = m

    def setOnByRelName(self, name):
        self.relayDict[name].setOnByName(name)

    def setOffByRelName(self, name):
        self.relayDict[name].setOffByName(name)

    def getBitstring(self):
        # zur Diagnostik auf Bit-Ebene
        return str(''.join([self.icDict[x].getBinStates() + "." for x in self.icList])[:-1])

    def getIntegerArray(self):
        # Ausgabe des Integer-Arrays zur Uebergabe an den Arduino
        return ' '.join([self.icDict[x].getIntState()  for x in self.icList])

    def getCharArray(self):
        return ''.join([chr(int(self.icDict[x].getIntState()))  for x in self.icList])

class matrixlogik:

    '''
    Diese Klasse bildet die Anwendungslogik ab. 
    Wenn ein bestimmtes Relais geschaltet ist, duerfen bestimmte andere Relais nicht geschaltet sein.
    '''

    def __init__(self):
        self.matrix = matrix()

    def setOnByRelName(self, name):
        if not self.matrix.relayExists(name):
            print "Relay unknown: >" + name
            sys.exit(1)

        print "Relais >" + name + "< ON"  
        self.matrix.setOnByRelName(name)
        if name == 'REL_50-SHORT50':
            self.matrix.setOffByRelName('REL_GND-SHORT50')

        if name == 'REL_VNA-TXA1':
            self.matrix.setOffByRelName('REL_VNA-TXB1')
        if name == 'REL_VNA-TXB1':
            self.matrix.setOffByRelName('REL_VNA-TXA1')

        if name == 'REL_RP2A-MWSP2':
            self.matrix.setOffByRelName('REL_RP1A-MWSP2')
        if name == 'REL_RP1A-MWSP2':
            self.matrix.setOffByRelName('REL_RP2A-MWSP2')

    def setOffByRelName(self, name):
        print "Relais >" + name + "< OFF" 
        self.matrix.setOnByRelName(name)

    def getBitstring(self):
        return self.matrix.getBitstring()

    def getIntegerArray(self):
        return self.matrix.getIntegerArray()

    def getCharArray(self):
        return self.matrix.getCharArray()

def senddata(input):

    def interaction(input):
        ser.write(input + '\r')
        ser.flush()
        time.sleep(1)
        out = ''
        while ser.inWaiting() > 0:
            out += ser.read(1)
        return out[len(input)+2:]

    def singlecommand(input):

        test = 0
        while 1 :
            out = interaction("")
            print out
            if out.endswith(">> "):
                break
            if test > 2:
                print "Serial Port Communications FAIL"
                sys.exit(1)
            time.sleep(1)

        out = interaction(input)
        if not "OK" in out:
            print "Serial Port Communications FAIL"
            sys.exit(1)
        else:
            print "Serial Port Communications OK"
        return out

    # if offline (for debugging), then bail out now
    if options.offline:
        return input

    if not os.access(options.serialport, os.W_OK):
        print "Serial port not accessable >" + options.serialport
        sys.exit(1)
    else:
        print "Serial port access OK >" + options.serialport

    ser = serial.Serial(
        port=options.serialport,
        baudrate=9600,
        bytesize=serial.EIGHTBITS,
        parity=serial.PARITY_NONE,
        stopbits=serial.STOPBITS_ONE
    )

    ser.isOpen()

    out = singlecommand(input)

    if options.verbose:
        print out

    ser.close()
    exit()

def handle_bitstring(bitstring):

    # parse in groups of 8 bits
    bitgroups = [bitstring[i:i+8] for i in range(0, len(bitstring), 8)]

    # bitgroup into byte
    outstring = ''.join([chr(int(bg[::-1], 2)) for bg in bitgroups])
    senddata(outstring)

def handle_relays(args):

    logik = matrixlogik()

    for r in args:
        logik.setOnByRelName(r)
    if options.verbose:
        print logik.getIntegerArray()
        print logik.getCharArray()

    senddata(logik.getCharArray())


if __name__ == "__main__":

    from optparse import OptionParser, OptionGroup

    desc  = """This program is the interface to the Arduino logic controller of the switching matrix."""

    usage = "usage: %prog [options] rel1 rel2 ..."

    parser = OptionParser(description=desc, usage=usage)

    group = OptionGroup(parser, "Possible relay names are", ' '.join(allRelays))
    parser.add_option_group(group)

    parser.add_option("-s", "--serialport",
                  dest="serialport",
                  action="store",
                  default="/dev/ttyACM0",
                  nargs=1,
                  help="serial device for the matrix. Default: /dev/ttyACM0")

    parser.add_option("-o", "--offline",
                  dest="offline",
                  action="store_true",
                  default=False,
                  help="Do not try to connect to serial port")

    parser.add_option("-b", "--bitstring",
                  dest="bitstring",
                  action="store",
                  nargs=1,
                  default=False,
                  help="Bitstring, if you know what you are doing :-)")

    parser.add_option("-v", "--verbose",
                  dest="verbose",
                  action="store_true",
                  default=False,
                  help="Be verbose. Return debugging data.")

    (options, args) = parser.parse_args() 

    if options.bitstring:
        handle_bitstring(options.bitstring)
        sys.exit(0)

    if len(args) == 0:
        print "Error: no relays given"
        sys.exit(1)

    handle_relays(args)

4.1 Interaction

Usage: polmatrix.py [options] rel1 rel2 ...

This program is the interface to the Arduino logic controller of the switching
matrix.

Options:
  -h, --help            show this help message and exit
  -s SERIALPORT, --serialport=SERIALPORT
                        serial device for the matrix. Default: /dev/ttyACM0
  -o, --offline         Do not try to connect to serial port
  -b BITSTRING, --bitstring=BITSTRING
                        Bitstring, if you know what you are doing :-)
  -v, --verbose         Be verbose. Return debugging data.

  Possible relay names are:
    REL1-SHORT50 REL2-SHORT50 REL3-SHORT50 REL4-SHORT50 REL4-RP2B
    REL4-RP2A REL4-RP1B REL4-RP1A REL3-RP2B REL3-RP2A REL3-RP1B REL3-RP1A
    REL2-RP2B REL2-RP2A REL2-RP1B REL2-RP1A REL1-RP2B REL1-RP2A REL1-RP1B
    REL1-RP1A REL_GND-SHORT50 REL_50-SHORT50 REL_VNA-TXB1 REL_VNA-TXA1
    REL_RP2A-MWSP2 REL_RP1A-MWSP2

5 Images

2018-10-10-matrix1.jpg

2018-10-10-matrix2.jpg

2018-10-10-matrix3.jpg