gobot.io/x/gobot/v2@v2.1.0/drivers/i2c/mcp23017_driver.go (about)

     1  package i2c
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"strings"
     7  
     8  	"gobot.io/x/gobot/v2"
     9  )
    10  
    11  // default address for device when a2/a1/a0 pins are all tied to ground
    12  // please consider special handling for MCP23S17
    13  const mcp23017DefaultAddress = 0x20
    14  
    15  const mcp23017Debug = false // toggle debugging information
    16  
    17  // port contains all the registers for the device.
    18  type port struct {
    19  	IODIR   uint8 // I/O direction register: 0=output / 1=input
    20  	IPOL    uint8 // input polarity register: 0=normal polarity / 1=inversed
    21  	GPINTEN uint8 // interrupt on change control register: 0=disabled / 1=enabled
    22  	DEFVAL  uint8 // default compare register for interrupt on change
    23  	INTCON  uint8 // interrupt control register: bit set to 0= use defval bit value to compare pin value/ bit set to 1= pin value compared to previous pin value
    24  	IOCON   uint8 // configuration register
    25  	GPPU    uint8 // pull-up resistor configuration register: 0=enabled / 1=disabled
    26  	INTF    uint8 // interrupt flag register: 0=no interrupt / 1=pin caused interrupt
    27  	INTCAP  uint8 // interrupt capture register, captures pin values during interrupt: 0=logic low / 1=logic high
    28  	GPIO    uint8 // port register, reading from this register reads the port
    29  	OLAT    uint8 // output latch register, write modifies the pins: 0=logic low / 1=logic high
    30  }
    31  
    32  // A bank is made up of PortA and PortB pins.
    33  // Port B pins are on the left side of the chip (starting with pin 1), while port A pins are on the right side.
    34  type bank struct {
    35  	portA port
    36  	portB port
    37  }
    38  
    39  // mcp23017Config contains the device configuration for the IOCON register.
    40  // These fields should only be set with values 0 or 1.
    41  type mcp23017Config struct {
    42  	bank   uint8
    43  	mirror uint8
    44  	seqop  uint8
    45  	disslw uint8
    46  	haen   uint8
    47  	odr    uint8
    48  	intpol uint8
    49  }
    50  
    51  type mcp23017Behavior struct {
    52  	forceRefresh bool
    53  	autoIODirOff bool
    54  }
    55  
    56  // MCP23017Driver contains the driver configuration parameters.
    57  type MCP23017Driver struct {
    58  	*Driver
    59  	mcpConf  mcp23017Config
    60  	mcpBehav mcp23017Behavior
    61  	gobot.Eventer
    62  }
    63  
    64  // NewMCP23017Driver creates a new Gobot Driver to the MCP23017 i2c port expander.
    65  // Params:
    66  //
    67  //	c Connector - the Adaptor to use with this Driver
    68  //
    69  // Optional params:
    70  //
    71  //	i2c.WithBus(int):	bus to use with this driver
    72  //	i2c.WithAddress(int):	address to use with this driver
    73  //	i2c.WithMCP23017Bank(int):	MCP23017 bank to use with this driver
    74  //	i2c.WithMCP23017Mirror(int):	MCP23017 mirror to use with this driver
    75  //	i2c.WithMCP23017Seqop(int):	MCP23017 seqop to use with this driver
    76  //	i2c.WithMCP23017Disslw(int):	MCP23017 disslw to use with this driver
    77  //	i2c.WithMCP23017Haen(int):	MCP23017 haen to use with this driver
    78  //	i2c.WithMCP23017Odr(int):	MCP23017 odr to use with this driver
    79  //	i2c.WithMCP23017Intpol(int):	MCP23017 intpol to use with this driver
    80  func NewMCP23017Driver(c Connector, options ...func(Config)) *MCP23017Driver {
    81  	d := &MCP23017Driver{
    82  		Driver:  NewDriver(c, "MCP23017", mcp23017DefaultAddress),
    83  		mcpConf: mcp23017Config{},
    84  		Eventer: gobot.NewEventer(),
    85  	}
    86  	d.afterStart = d.initialize
    87  
    88  	for _, option := range options {
    89  		option(d)
    90  	}
    91  
    92  	d.AddCommand("WriteGPIO", func(params map[string]interface{}) interface{} {
    93  		pin := params["pin"].(uint8)
    94  		port := params["port"].(string)
    95  		val := params["val"].(uint8)
    96  		err := d.WriteGPIO(pin, port, val)
    97  		return map[string]interface{}{"err": err}
    98  	})
    99  
   100  	d.AddCommand("ReadGPIO", func(params map[string]interface{}) interface{} {
   101  		pin := params["pin"].(uint8)
   102  		port := params["port"].(string)
   103  		val, err := d.ReadGPIO(pin, port)
   104  		return map[string]interface{}{"val": val, "err": err}
   105  	})
   106  
   107  	return d
   108  }
   109  
   110  // WithMCP23017Bank option sets the MCP23017Driver bank option
   111  func WithMCP23017Bank(val uint8) func(Config) {
   112  	return func(c Config) {
   113  		d, ok := c.(*MCP23017Driver)
   114  		if ok {
   115  			d.mcpConf.bank = val
   116  		} else if mcp23017Debug {
   117  			log.Printf("trying to set bank for non-MCP23017Driver %v", c)
   118  		}
   119  	}
   120  }
   121  
   122  // WithMCP23017Mirror option sets the MCP23017Driver mirror option
   123  func WithMCP23017Mirror(val uint8) func(Config) {
   124  	return func(c Config) {
   125  		d, ok := c.(*MCP23017Driver)
   126  		if ok {
   127  			d.mcpConf.mirror = val
   128  		} else if mcp23017Debug {
   129  			log.Printf("Trying to set mirror for non-MCP23017Driver %v", c)
   130  		}
   131  	}
   132  }
   133  
   134  // WithMCP23017Seqop option sets the MCP23017Driver seqop option
   135  func WithMCP23017Seqop(val uint8) func(Config) {
   136  	return func(c Config) {
   137  		d, ok := c.(*MCP23017Driver)
   138  		if ok {
   139  			d.mcpConf.seqop = val
   140  		} else if mcp23017Debug {
   141  			log.Printf("Trying to set seqop for non-MCP23017Driver %v", c)
   142  		}
   143  	}
   144  }
   145  
   146  // WithMCP23017Disslw option sets the MCP23017Driver disslw option
   147  func WithMCP23017Disslw(val uint8) func(Config) {
   148  	return func(c Config) {
   149  		d, ok := c.(*MCP23017Driver)
   150  		if ok {
   151  			d.mcpConf.disslw = val
   152  		} else if mcp23017Debug {
   153  			log.Printf("Trying to set disslw for non-MCP23017Driver %v", c)
   154  		}
   155  	}
   156  }
   157  
   158  // WithMCP23017Haen option sets the MCP23017Driver haen option
   159  // This feature is only available for MCP23S17, because address pins are always enabled on the MCP23017.
   160  func WithMCP23017Haen(val uint8) func(Config) {
   161  	return func(c Config) {
   162  		d, ok := c.(*MCP23017Driver)
   163  		if ok {
   164  			d.mcpConf.haen = val
   165  		} else if mcp23017Debug {
   166  			log.Printf("Trying to set haen for non-MCP23017Driver %v", c)
   167  		}
   168  	}
   169  }
   170  
   171  // WithMCP23017Odr option sets the MCP23017Driver odr option
   172  func WithMCP23017Odr(val uint8) func(Config) {
   173  	return func(c Config) {
   174  		d, ok := c.(*MCP23017Driver)
   175  		if ok {
   176  			d.mcpConf.odr = val
   177  		} else if mcp23017Debug {
   178  			log.Printf("Trying to set odr for non-MCP23017Driver %v", c)
   179  		}
   180  	}
   181  }
   182  
   183  // WithMCP23017Intpol option sets the MCP23017Driver intpol option
   184  func WithMCP23017Intpol(val uint8) func(Config) {
   185  	return func(c Config) {
   186  		d, ok := c.(*MCP23017Driver)
   187  		if ok {
   188  			d.mcpConf.intpol = val
   189  		} else if mcp23017Debug {
   190  			log.Printf("Trying to set intpol for non-MCP23017Driver %v", c)
   191  		}
   192  	}
   193  }
   194  
   195  // WithMCP23017ForceWrite option modifies the MCP23017Driver forceRefresh option
   196  // Setting to true (1) will force refresh operation to register, although there is no change.
   197  // Normally this is not needed, so default is off (0).
   198  // When there is something flaky, there is a small chance to stabilize by setting this flag to true.
   199  // However, setting this flag to true slows down each IO operation up to 100%.
   200  func WithMCP23017ForceRefresh(val uint8) func(Config) {
   201  	return func(c Config) {
   202  		d, ok := c.(*MCP23017Driver)
   203  		if ok {
   204  			d.mcpBehav.forceRefresh = val > 0
   205  		} else if mcp23017Debug {
   206  			log.Printf("Trying to set forceRefresh for non-MCP23017Driver %v", c)
   207  		}
   208  	}
   209  }
   210  
   211  // WithMCP23017AutoIODirOff option modifies the MCP23017Driver autoIODirOff option
   212  // Set IO direction at each read or write operation ensures the correct direction, which is the the default setting.
   213  // Most hardware is configured statically, so this can avoided by setting the direction using SetPinMode(),
   214  // e.g. in the start up sequence. If this way is taken, the automatic set of direction at each call can
   215  // be safely deactivated with this flag (set to true, 1).
   216  // This will speedup each WriteGPIO by 50% and each ReadGPIO by 60%.
   217  func WithMCP23017AutoIODirOff(val uint8) func(Config) {
   218  	return func(c Config) {
   219  		d, ok := c.(*MCP23017Driver)
   220  		if ok {
   221  			d.mcpBehav.autoIODirOff = val > 0
   222  		} else if mcp23017Debug {
   223  			log.Printf("Trying to set autoIODirOff for non-MCP23017Driver %v", c)
   224  		}
   225  	}
   226  }
   227  
   228  // SetPinMode set pin mode of a given pin immediately, based on the value:
   229  // val = 0 output
   230  // val = 1 input
   231  func (m *MCP23017Driver) SetPinMode(pin uint8, portStr string, val uint8) (err error) {
   232  	m.mutex.Lock()
   233  	defer m.mutex.Unlock()
   234  
   235  	selectedPort := m.getPort(portStr)
   236  	// Set IODIR register bit for given pin to an output/input.
   237  	if err = m.write(selectedPort.IODIR, uint8(pin), bitState(val)); err != nil {
   238  		return
   239  	}
   240  	return
   241  }
   242  
   243  // SetPullUp sets the pull up state of a given pin immediately, based on the value:
   244  // val = 1 pull up enabled.
   245  // val = 0 pull up disabled.
   246  func (m *MCP23017Driver) SetPullUp(pin uint8, portStr string, val uint8) error {
   247  	m.mutex.Lock()
   248  	defer m.mutex.Unlock()
   249  
   250  	selectedPort := m.getPort(portStr)
   251  	return m.write(selectedPort.GPPU, pin, bitState(val))
   252  }
   253  
   254  // SetGPIOPolarity will change a given pin's polarity immediately, based on the value:
   255  // val = 1 opposite logic state of the input pin.
   256  // val = 0 same logic state of the input pin.
   257  func (m *MCP23017Driver) SetGPIOPolarity(pin uint8, portStr string, val uint8) (err error) {
   258  	m.mutex.Lock()
   259  	defer m.mutex.Unlock()
   260  
   261  	selectedPort := m.getPort(portStr)
   262  	return m.write(selectedPort.IPOL, pin, bitState(val))
   263  }
   264  
   265  // WriteGPIO writes a value to a gpio pin (0-7) and a port (A or B).
   266  func (m *MCP23017Driver) WriteGPIO(pin uint8, portStr string, val uint8) (err error) {
   267  	m.mutex.Lock()
   268  	defer m.mutex.Unlock()
   269  
   270  	selectedPort := m.getPort(portStr)
   271  	if !m.mcpBehav.autoIODirOff {
   272  		// Set IODIR register bit for given pin to an output by clearing bit.
   273  		// can't call SetPinMode() because mutex will cause deadlock
   274  		if err = m.write(selectedPort.IODIR, uint8(pin), clear); err != nil {
   275  			return err
   276  		}
   277  	}
   278  	// write value to OLAT register bit
   279  	err = m.write(selectedPort.OLAT, pin, bitState(val))
   280  	if err != nil {
   281  		return err
   282  	}
   283  	return nil
   284  }
   285  
   286  // ReadGPIO reads a value from a given gpio pin (0-7) and a port (A or B).
   287  func (m *MCP23017Driver) ReadGPIO(pin uint8, portStr string) (val uint8, err error) {
   288  	m.mutex.Lock()
   289  	defer m.mutex.Unlock()
   290  
   291  	selectedPort := m.getPort(portStr)
   292  	if !m.mcpBehav.autoIODirOff {
   293  		// Set IODIR register bit for given pin to an input by set bit.
   294  		// can't call SetPinMode() because mutex will cause deadlock
   295  		if err = m.write(selectedPort.IODIR, uint8(pin), set); err != nil {
   296  			return 0, err
   297  		}
   298  	}
   299  	val, err = m.read(selectedPort.GPIO)
   300  	if err != nil {
   301  		return val, err
   302  	}
   303  	val = 1 << uint8(pin) & val
   304  	if val > 1 {
   305  		val = 1
   306  	}
   307  	return val, nil
   308  }
   309  
   310  func (m *MCP23017Driver) initialize() (err error) {
   311  	// Set IOCON register with MCP23017 configuration.
   312  	ioconReg := m.getPort("A").IOCON // IOCON address is the same for Port A or B.
   313  	ioconVal := m.mcpConf.getUint8Value()
   314  	if _, err := m.connection.Write([]uint8{ioconReg, ioconVal}); err != nil {
   315  		return err
   316  	}
   317  	return
   318  }
   319  
   320  // write gets the value of the passed in register, and then sets the bit specified
   321  // by the pin to the given state.
   322  func (m *MCP23017Driver) write(reg uint8, pin uint8, state bitState) (err error) {
   323  	valOrg, err := m.read(reg)
   324  	if err != nil {
   325  		return fmt.Errorf("MCP write-read: %v", err)
   326  	}
   327  
   328  	var val uint8
   329  	if state == clear {
   330  		val = clearBit(valOrg, pin)
   331  	} else {
   332  		val = setBit(valOrg, pin)
   333  	}
   334  
   335  	if val != valOrg || m.mcpBehav.forceRefresh {
   336  		if mcp23017Debug {
   337  			log.Printf("write done: MCP forceRefresh: %t, address: 0x%X, register: 0x%X, name: %s, value: 0x%X\n",
   338  				m.mcpBehav.forceRefresh, m.GetAddressOrDefault(mcp23017DefaultAddress), reg, m.getRegName(reg), val)
   339  		}
   340  		if err = m.connection.WriteByteData(reg, val); err != nil {
   341  			return fmt.Errorf("MCP write-WriteByteData(reg=%d,val=%d): %v", reg, val, err)
   342  		}
   343  	} else {
   344  		if mcp23017Debug {
   345  			log.Printf("write skipped: MCP forceRefresh: %t, address: 0x%X, register: 0x%X, name: %s, value: 0x%X\n",
   346  				m.mcpBehav.forceRefresh, m.GetAddressOrDefault(mcp23017DefaultAddress), reg, m.getRegName(reg), val)
   347  		}
   348  	}
   349  	return nil
   350  }
   351  
   352  // read get the data from a given register
   353  // it is mainly a wrapper to create additional debug messages, when activated
   354  func (m *MCP23017Driver) read(reg uint8) (val uint8, err error) {
   355  	val, err = m.connection.ReadByteData(reg)
   356  	if err != nil {
   357  		return val, fmt.Errorf("MCP write-ReadByteData(reg=%d): %v", reg, err)
   358  	}
   359  	if mcp23017Debug {
   360  		log.Printf("reading done: MCP autoIODirOff: %t, address: 0x%X, register:0x%X, name: %s, value: 0x%X\n",
   361  			m.mcpBehav.autoIODirOff, m.GetAddressOrDefault(mcp23017DefaultAddress), reg, m.getRegName(reg), val)
   362  	}
   363  	return val, nil
   364  }
   365  
   366  // getPort return the port (A or B) given a string and the bank.
   367  // Port A is the default if an incorrect or no port is specified.
   368  func (m *MCP23017Driver) getPort(portStr string) (selectedPort port) {
   369  	portStr = strings.ToUpper(portStr)
   370  	switch {
   371  	case portStr == "A":
   372  		return mcp23017GetBank(m.mcpConf.bank).portA
   373  	case portStr == "B":
   374  		return mcp23017GetBank(m.mcpConf.bank).portB
   375  	default:
   376  		return mcp23017GetBank(m.mcpConf.bank).portA
   377  	}
   378  }
   379  
   380  // getUint8Value returns the configuration data as a packed value.
   381  func (mc *mcp23017Config) getUint8Value() uint8 {
   382  	return mc.bank<<7 | mc.mirror<<6 | mc.seqop<<5 | mc.disslw<<4 | mc.haen<<3 | mc.odr<<2 | mc.intpol<<1
   383  }
   384  
   385  // getRegName returns the name of the given register related to the configured bank
   386  // and can be used to write nice debug messages
   387  func (m *MCP23017Driver) getRegName(reg uint8) string {
   388  	b := mcp23017GetBank(m.mcpConf.bank)
   389  	portStr := "A"
   390  	regStr := "unknown"
   391  
   392  	for i := 1; i <= 2; i++ {
   393  		if regStr == "unknown" {
   394  			p := b.portA
   395  			if i == 2 {
   396  				p = b.portB
   397  				portStr = "B"
   398  			}
   399  			switch reg {
   400  			case p.IODIR:
   401  				regStr = "IODIR"
   402  			case p.IPOL:
   403  				regStr = "IPOL"
   404  			case p.GPINTEN:
   405  				regStr = "GPINTEN"
   406  			case p.DEFVAL:
   407  				regStr = "DEFVAL"
   408  			case p.INTCON:
   409  				regStr = "INTCON"
   410  			case p.IOCON:
   411  				regStr = "IOCON"
   412  			case p.GPPU:
   413  				regStr = "GPPU"
   414  			case p.INTF:
   415  				regStr = "INTF"
   416  			case p.INTCAP:
   417  				regStr = "INTCAP"
   418  			case p.GPIO:
   419  				regStr = "GPIO"
   420  			case p.OLAT:
   421  				regStr = "OLAT"
   422  			}
   423  		}
   424  	}
   425  
   426  	return fmt.Sprintf("%s_%s", regStr, portStr)
   427  }
   428  
   429  // mcp23017GetBank returns a bank's PortA and PortB registers given a bank number (0/1).
   430  func mcp23017GetBank(bnk uint8) bank {
   431  	if bnk == 0 {
   432  		return bank{portA: port{0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0x10, 0x12, 0x14}, portB: port{0x01, 0x03, 0x05, 0x07, 0x09, 0x0B, 0x0D, 0x0F, 0x11, 0x13, 0x15}}
   433  	}
   434  	return bank{portA: port{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A}, portB: port{0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A}}
   435  }