I've been doing tests with the MCP23017 attached to a raspberry pi 3b+. I have concluded that the handling of data read by the MCP23017 using interrupts should always be done with the INTA and INTB NOT mirrored. In other words, the INTA and INTB interrupts should be handled separately. (Though the same callback routine can be used for both)
Here's why:
I am driving it with a raspberry pi zero w. I have 8 gpio ports on the pi-zero connected to 8 ports of the MCP23017 BANK A and another connected to BANK B. I have the starting GPIO output to ON or HIGH.
Using the python GPIO package, I can turn on OFF or more GPIO ports on the pi-zero and observe the effect on the 3b+.
On the 3b+, I have connected the INTA and INTB lines to two GPIO ports, (GPIO27 and 22, in my case).
I have set the all of the MCP23017 ports to input, and INTERRUPT-ON-CHANGE, and turned on the PULL-UPs for all of the MCP23017 GPIO pins on both banks. The Interrupt pins for INTA and INTB are set to Active-HIGH. INTA and INTB are NOT mirror allowing the two banks to change the interrupt pins independently.
When the program starts up, it sets a callback for the two GPIO ports 27 and 22. One is associated with BANK A and the other with BANK B. When an interrupt occurs, it means 1 or more bits in the corresponding bank is changed. The callbacks are written to complete as quickly as possible. No io is done in the callback routine in order to avoid the interrupt processing to be delayed. They capture which port has the interrupt (to know if it is BANKA or BANKB), the bits for the GPIO BANK, and the INTF , the interrupt Flag Register and add it to a thread-safe Queue
I have observed that if I keep the interrupts un-MIRRORed, then the pi-zero can change bits without any delay. Note that a particular pin cannot be toggled to quickly or the MCP23017 will not detect the change. However, different bits can be turned on quickly. If multiple bits are turned on at the same time (or close to the same time), the interrupt will happen for that bank and all of the changed bits will appear in the GPIO register. All of the bits changes will be accounted for.
However, if I set the interrupts to MIRRORed, (i.e "The INT pins are internally connected", or rather a change in either bank will cause the INT to be toggled) the program only works reliably when bit changes occur slowly enough.
PS, I have a connection between GPIO4 on the pi-zero and the GPIO4 on the 3b+ so that the pi-zero program can signal the 3b+ when to start and when to stop.
At the risk of having folks read other people's poorly documented code, here it all is. I'll go ahead because I find sample code very helpful.
I'll add a circuit diagram later.
Here is my program to handle the MCP23017:
Code: Select all
#!/usr/bin/env python3
import smbus
import sys
import RPi.GPIO as RPIGPIO
import time
import traceback
import queue
from datetime import datetime
from datetime import timedelta
bus = None
starttime = None
state3ime = None
state = None
eventqueue = queue.Queue()
userelay = False
usedebug = False
RELAY = 23 # relay to turn on 24vac
DEVICE_ADDRESS = 0x20 # MCP23017 address to use
# default addresses
BANKA = 0
BANKB = 1
NOTIFY = 2 # notification pin = TRUE means ready to send FALSE means done
IODIR = 0x00 # Pin direction register A
IPOL = 0x02 # Input polarity regsiter A
GPENTEN = 0x04 # Interrupt on change pins A
IOCON = 0x0a # Config register A Bank, Mirror, SEQOP, DISSLW, HAEN, ODR, TPOL, N/U
GPPU = 0x0c # 100k ohm pullups A
GPIO = 0x12 # GPIO pins
INTF = 0x0e # interupt flag register
# IOCON I/O Expander Configuration bits
iocBANK = 0x80 # if 1 registers are in different banks
iocMIRROR = 0x40 # if 1 INT pins are ORed togetther
iocSEQOP = 0x20 # if 1 address pointer does not increment
iocDISSLW = 0x10 # if 1 Slew rate disabled
iocHAEN = 0x08 # if 1 Hardware address enable MCP23S17 only
iocODR = 0x04 # if 1 open drain output, overrides INTPOL
iocINTPOL = 0x02 # if 1 Active-high (sets polarity of INT output pin
# map of gpioports given bank
GPIOINT = {
BANKA : 22, # gpio connected to port A interrupt line.
BANKB : 27, # gpio connected to port A interrupt line.
NOTIFY : 4, # notification from other side
}
# map of banks given gpioport
GPIOINTREV = {}
for bank, gpioport in GPIOINT.items():
GPIOINTREV[gpioport] = bank
MASKBITS = {
BANKA : 0x00FF, # BANKA are low order bits
BANKB : 0xFF00, # BANKB are high order bits
}
# put bits in BANKB byte and and also in BANKA
def bothBanks(bits):
bits = bits & 0xff
return (bits << 8) | bits
# these routines try to deal with the MCP23017 as a 16bit register rather than 2 8 bit registers
# intbits determine whether to read reg for bank A or B or both.
# returns 16 bit value with B in high order and A in low order
def readREG(port, intbits = 0xFFFF):
# need to separate bus reads because we don't want to clear an interupt we aren't handling
# intbits will be non-zero in top 8 and/or bottom 8
pinAdata = 0
pinBdata = 0
if (intbits >> 8) & 0xFF:
pinBdata = bus.read_byte_data(DEVICE_ADDRESS, port + BANKB)
if intbits & 0xFF:
pinAdata = bus.read_byte_data(DEVICE_ADDRESS, port + BANKA)
return (pinBdata << 8) | pinAdata
# intbits determine whether to set reg for bank A or B or both.
# sets B from high order and A from low order if any bit is set in the corresponding by
# int intbits
def writeREG(port, data, intbits = 0xFFFF):
# only write to the BANK we need to
if (intbits >> 8) & 0xFF:
bus.write_byte_data(DEVICE_ADDRESS, port + BANKB, (data >> 8) & 0xFF)
if intbits & 0xFF:
bus.write_byte_data(DEVICE_ADDRESS, port + BANKA, data & 0xFF)
return None
# read next item in the queue and print it out
# queue consists of a list per entry
def getQueueItem(eventqueue):
queueitem = eventqueue.get()
itemtime, gpiolevel, bank, intfBA, pinBA, gpioport = tuple(queueitem)
# get time difference object and calculater elapsed time string
mstime = itemtime - starttime
elapsed = "%d"%mstime.seconds + ".%06d"%mstime.microseconds
message = ""
# check to see if any bits changed other that those causing the interrupt
if (intfBA | pinBA) != intfBA:
message = ", intFA !~ pinBA"
print ("Elapsed=%s, gpiolevel=%d, bank=%d, intfBA = 0x%04x, pinBA = 0x%04x, gpioport=%d%s"%(elapsed, gpiolevel, bank, intfBA, pinBA, gpioport, message))
return queueitem
def my_callback(gpioport):
# Note: Multiple interupts can occur simultaneously
global bus
global state
global state3time
# we have the gpioport but need the bank
bank = GPIOINTREV[gpioport]
nowtim = datetime.utcnow()
gpiolevel = RPIGPIO.input(GPIOINT[bank])
if bank == NOTIFY:
intfBA = 0x0000
pinBAbits = 0x0000
if gpiolevel == 0:
state = 1
else:
state = 3
state3time = nowtim
else:
maskBits = MASKBITS[bank]
intfBA = readREG(INTF, maskBits) # findout which pin changed
# maskBits = intfBA
if gpiolevel: # if port 25 == 1
if usedebug:
print("level up clear interrupt")
pinBAbits = readREG(GPIO, maskBits) # get the BANK's pin value and clear interrupt
if usedebug:
print("----------level up bank=%d, infBA= 0x%04x, pinBA= 0x%04x" %(bank, intfBA, pinBAbits))
else: # if port 25 != 1
if usedebug:
print("level dn clear interrupt")
pinBAbits = readREG(GPIO, maskBits) # get the BANK's pin value and clear interrupt
if usedebug:
print("----------level dn bank=%d, infBA= 0x%04x, pinBA= 0x%04x" %(bank, intfBA, pinBAbits))
eventqueue.put([nowtim, gpiolevel, bank, intfBA, pinBAbits, gpioport])
return
def init():
global bus
starttime = datetime.utcnow()
bus = smbus.SMBus(1) # 0 = /dev/i2c-0 (port I2C0), 1 = /dev/i2c-1 (port I2C1)
# port A & B all in input bits
writeREG(IODIR, 0xFFFF)
# reverse input values. This is because A3700 is low when upper threshold is detected
writeREG(IPOL, 0xFFFF)
# Activate all internal pullup resistors at Port A & B for output:
writeREG(GPPU, 0xFFFF)
# disable interupts on change
writeREG(GPENTEN, 0x0000)
# clear any previous interrupts
writeREG(GPIO, 0x0000)
pinBAbits = readREG(GPIO)
# I/O Expander config register. Set INT pins NOT internally connected, Interupt polarity Active-high
# for both A & B
writeREG(IOCON, bothBanks(iocINTPOL)) # | iocMIRROR))
# enable interupts on change
writeREG(GPENTEN, 0xFFFF)
time.sleep(1)
RPIGPIO.setwarnings(False)
RPIGPIO.setmode(RPIGPIO.BCM)
RPIGPIO.setup(RELAY, RPIGPIO.OUT) # set GPIO24 as output
RPIGPIO.output(RELAY, False)
# set up all interrupt
for bank, thisGPIOINT in GPIOINT.items():
RPIGPIO.setup(thisGPIOINT, RPIGPIO.IN, pull_up_down=RPIGPIO.PUD_UP) # set interrupt from MCP23017
# when a changing edge is detected on OPTO1 port, regardless of whatever
# else is happening in the program, the function my_callback will be run
cbType = RPIGPIO.RISING
if thisGPIOINT == GPIOINT[NOTIFY]: # want both rising and falling for NOTIFY
cbType = RPIGPIO.BOTH
RPIGPIO.add_event_detect(thisGPIOINT, cbType, callback=my_callback)
# RPIGPIO.add_event_detect(GPIOINT, RPIGPIO.BOTH, callback=my_callback)
# RPIGPIO.add_event_detect(GPIOINT, RPIGPIO.FALLING, callback=my_callback)
pass
def main():
global bus
global starttime
global state # 0 = not started, 1 = on, 2 = off, 3= zero reached
global state3time
sleeptime = 0.1 # 5ms
print("start=========================================================")
init()
starttime = datetime.utcnow()
state = 0
if userelay:
print ("----------- Relay on")
RPIGPIO.output(RELAY, True)
print("ON, state = " + str(state))
state3time = None
while state < 3:
elapsedtime = datetime.utcnow() - starttime
if userelay and elapsedtime.seconds > 4 and state != 2:
print ("----------- Relay off")
RPIGPIO.output(RELAY, False)
state = 2
print("OFF, state = " + str(state))
time.sleep(sleeptime)
while not eventqueue.empty():
queueitem = getQueueItem(eventqueue)
if state == 0:
continue
if False and queueitem[3] == 0x8000 and queueitem[4] == 0x0000:
print("End detected.")
state3time = datetime.utcnow()
state = 3
# drain queue in case other events came in after end event
while (datetime.utcnow()- state3time).seconds < 2:
time.sleep(sleeptime)
while not eventqueue.empty():
queueitem = getQueueItem(eventqueue)
pass
# while (True):
# pin = RPIGPIO.input(GPIOINT)
# print( "pin = %i"%pin)
# time.sleep(1)
#
pass
if __name__ =='__main__':
try:
main()
except KeyboardInterrupt:
pass
except:
traceback.print_exc()
pass
RPIGPIO.cleanup()
print ("Done")
And here is my code to handle the pi-zero simulation driver
Code: Select all
#!/usr/bin/env python3
## on esp
import sys
import RPi.GPIO as GPIO
import time
from datetime import datetime
from datetime import timedelta
from time import sleep # this lets us have a time delay (see line 12)
import traceback
# table of connections from simulator rpi zero
# to MCP23017 on driven raspberry pi 3b+
# layout designed so wires follow sequentially on both MCP23017 and raspberry pi header
# uses only generic rpi GPIO pins, skipping rpi pins useful for other things (SPI, I2C, txd/rxd
MCPRESET = [ {
"name" : "Reset MCP", # name of connection
"color" : "green", # wire color
"mcp" : "0x0000", # NOT MCP line
"gpio" : 4 # BCM GPIO number
},
]
MCPRESET0 = MCPRESET[0]
lines = [
{
"name" : "BankA 0", # name of connection
"color" : "white", # wire color
"mcp" : "0x0001", # bit setting for ((GPIO-B << 8) | GPIO-A) on MCP20137
"gpio" : 21 # BCM GPIO number
},
{
"name" : "BankA 1",
"color" : "black",
"mcp" : "0x0002",
"gpio" : 20
},
{
"name" : "BankA 2",
"color" : "brown",
"mcp" : "0x0004",
"gpio" : 16
},
{
"name" : "BankA 3",
"color" : "red",
"mcp" : "0x0008",
"gpio" : 12
},
{
"name" : "BankA 4",
"color" : "orange",
"mcp" : "0x0010",
"gpio" : 25
},
{
"name" : "BankA 5",
"color" : "yellow",
"mcp" : "0x0020",
"gpio" : 24
},
{
"name" : "BankA 6",
"color" : "green",
"mcp" : "0x0040",
"gpio" : 23
},
{
"name" : "BankA 7",
"color" : "blue",
"mcp" : "0x0080",
"gpio" : 18
},
{
"name" : "BankB 0",
"color" : "black",
"mcp" : "0x0100",
"gpio" : 17
},
{
"name" : "BankB 1",
"color" : "white",
"mcp" : "0x0200",
"gpio" : 27
},
{
"name" : "BankB 2",
"color" : "gray",
"mcp" : "0x0400",
"gpio" : 22
},
{
"name" : "BankB 3",
"color" : "violet",
"mcp" : "0x0800",
"gpio" : 5
},
{
"name" : "BankB 4",
"color" : "blue",
"mcp" : "0x1000",
"gpio" : 6
},
{
"name" : "BankB 5",
"color" : "green",
"mcp" : "0x2000",
"gpio" : 13
},
{
"name" : "BankB 6",
"color" : "yellow",
"mcp" : "0x4000",
"gpio" : 19
},
{
"name" : "BankB 7",
"color" : "orange",
"mcp" : "0x8000",
"gpio" : 26
},
]
#port init
def init():
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
for line in MCPRESET + lines:
print ("Setup %s" % line["name"])
GPIO.setup(line["gpio"], GPIO.OUT)
GPIO.output(line["gpio"], True)
pass
def main():
init()
sleep(1)
slice = 1
GPIO.output(MCPRESET0["gpio"], False)
sleep(1)
for line in lines: # [slice : slice+1]:
# print("----------- On: %s, gpio=%d, mcp=%s"%(line["name"], line["gpio"], line["mcp"]))
GPIO.output(line["gpio"], False)
# sleep(0.5)
sleep(2)
for line in lines: # [slice : slice+1]:
# for line in lines: # [slice : slice+1]:
# print("Off: %s, gpio=%d, mcp=%s"%(line["name"], line["gpio"], line["mcp"]))
GPIO.output(line["gpio"], True)
# sleep(1)
# sleep(1)
sleep(2)
GPIO.output(MCPRESET0["gpio"], True)
if __name__ =='__main__':
try:
main()
except KeyboardInterrupt:
pass
except:
traceback.print_exc()
pass
GPIO.cleanup()