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 }