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