diff --git a/docs/lua-modules/mcp23017.md b/docs/lua-modules/mcp23017.md new file mode 100644 index 0000000000..35bf91e84f --- /dev/null +++ b/docs/lua-modules/mcp23017.md @@ -0,0 +1,230 @@ +# Lua MCP23017 Module for NodeMCU / ESP8266 + +| Since | Origin / Contributor | Maintainer | Source | +| :----- | :-------------------- | :---------- | :------ | +| 2020-04-10 | [Marcel P.](https://github.com/plomi-net) | [Marcel P.](https://github.com/plomi-net) | [mcp23017.lua](../../lua_modules/mcp23017/mcp23017.lua) | + + +This Lua module provides access to the MCP23017 module. + +The [MCP23017](http://ww1.microchip.com/downloads/en/devicedoc/20001952c.pdf) is a port expander and provides 16 channels for inputs and outputs. +Up to 8 devices (128 channels) are possible by the configurable address (A0 - A2). + +Due to the 16 channels, 2 bytes are required for switching outputs or reading input signals. These are A and B. +A single pin can be set or a whole byte. + +The numbering of the individual pins starts at 0 and ends with 7. +The numbers are for each register GPIO A and GPIO B. + + +!!! important + The module requires `i2c` and `bit` C module built into firmware. + + +### Require +```lua +mcp = require "mcp23017" +``` + +## Example Script +The example script can be found [here](../../lua_examples/mcp23017/mcp23017_example.lua) + +## setup() +Configures the address of the module and tests the connection to the i2c bus. +The i2c id is required for an existing i2c interface, alternatively the sda and scl pins can be specified. +Then this function will establish the connection. +Automatically resets the device state (see `mcp23017:reset()`) + +#### Syntax +`mcp23017:setup(address, i2c_id)` + +#### Parameter +- `address` address for MCP23017, default: 0x20 (should be between 0x20 and 0x27) +- `i2c_id` id for the i2c bus connection (i2c.setup separately) + +#### Return +`true` if device found, otherwise `false`. + +#### possible Errors +- `MCP23017 device on address not found` +- `MCP23017 address is out of range` + +#### Example +```lua +local mcp23017 = require "mcp23017" + +local address = 0x20 +local cSCL = 1 +local cSDA = 2 +local i2c_instance = 0 + +-- setup i2c bus and create instance for mcp23017 (assigned to mcp) +i2c.setup(i2c_instance, cSDA, cSCL, i2c.SLOW) +local mcp = mcp23017(address, i2c_instance) +``` + +## setMode() +Set the mode of a single channel. This can be OUTPUT or INPUT. + +#### Syntax +`mcp23017:setMode(register, pin, mode)` + +#### Parameter +- `register` the side of channels (GPA or GPB) +- `pin` the number to be set for the channel (0-15) +- `mode` the mode for the channel. This can be `mcp23017.INPUT` or `mcp23017.OUTPUT` + +#### Return +`true`, in case of error `nil`. + +#### Example +```lua +-- set pin 7 and 8 to output (GPA7 and GPB0) and GPB1 to input +mcp:setMode(mcp23017.GPA, 7, mcp23017.OUTPUT) +mcp:setMode(mcp23017.GPB, 0, mcp23017.OUTPUT) +mcp:setMode(mcp23017.GPB, 1, mcp23017.INPUT) +``` + +## setPin() +Set the state of a single channel. This can be HIGH or LOW. + +#### Syntax +`mcp23017:setMode(register, pin, state)` + +#### Parameter +- `register` the side of channels (GPA or GPB) +- `pin` the number to be set for the channel (0-15) +- `state` the state for the channel. This can be `mcp23017.HIGH` or `mcp23017.LOW` + +#### Return +`true`, in case of error `nil`. + +#### Example +```lua +-- set pin 7 to high (GPA7) +mcp:setPin(mcp23017.GPA, 7, mcp23017.HIGH) +-- set pin 8 to low (GPB0) +mcp:setPin(mcp23017.GPB, 0, mcp23017.LOW) +``` + +## getPinState() +get the state for a single channel. This can be HIGH or LOW. + +#### Syntax +`mcp23017:getPinState(register, pin)` + +#### Parameter +- `register` the side of channels (GPA or GPB) +- `pin` the number for which a state is to be queried (0-15) + +#### Return +`true` for HIGH, `false` for LOW, in case of error `nil`. + +#### Example +```lua +-- get the state for pin 9 (GPB1) +print(mcp:getPinState(mcp23017.GPB, 1)) +``` + +## reset() +By calling this function, a safe state is established. +All channels are set to input. +This function can be used for a panic program. + +#### Syntax +`mcp23017:reset()` + +#### Parameter +None + +#### Return +None + +#### Example +```lua +-- reset the mcp23017 to startup defaults +mcp:reset() +``` + +## setInternalPullUp() +Enable or disable the internal pullup resistors. + +#### Syntax +`mcp23017:setInternalPullUp(register, byte)` + +#### Parameter +- `register` the side of channels (GPA or GPB) +- `byte` byte to set the pullup resistors + +#### Return +None + +#### Example +```lua +-- enable all pullup resistors for GPA +print(mcp:setInternalPullUp(mcp23017.GPA, 0xFF)) +-- disable all pullup resistors for GPA +print(mcp:setInternalPullUp(mcp23017.GPA, 0x00)) +``` + +## writeIODIR() +Setup the mode of the channels with a whole byte. + + +#### Syntax +`mcp23017:writeIODIR(register, byte)` + +#### Parameter +- `register` the side of channels (GPA or GPB) +- `byte` byte to set the mode for all channels for this register + +#### Return +None + +#### Example +```lua +-- set all GPA to input +print(mcp:writeIODIR(mcp23017.GPA, 0xFF)) +-- set all GPA to output +print(mcp:writeIODIR(mcp23017.GPA, 0x00)) +``` + +## writeGPIO() +Setup the output state of the channels with a whole byte. + +#### Syntax +`mcp23017:writeGPIO(register, byte)` + +#### Parameter +- `register` the side of channels (GPA or GPB) +- `byte` byte to set the state for all channels for this register + +#### Return +None + +#### Example +```lua +-- set all GPA to HIGH +print(mcp:writeGPIO(mcp23017.GPA, 0xFF)) +-- set all GPA to LOW +print(mcp:writeGPIO(mcp23017.GPA, 0x00)) +``` + +## readGPIO() +Read the input states of the channels with a whole byte. + +#### Syntax +`mcp23017:readGPIO(register)` + +#### Parameter +- `register` the side of channels (GPA or GPB) + +#### Return +byte with states + +#### Example +```lua +-- get states for GPA +print(mcp:readGPIO(mcp23017.GPA)) +``` + diff --git a/lua_examples/mcp23017/mcp23017_example.lua b/lua_examples/mcp23017/mcp23017_example.lua new file mode 100644 index 0000000000..122f279c6d --- /dev/null +++ b/lua_examples/mcp23017/mcp23017_example.lua @@ -0,0 +1,110 @@ +--[[ + + This example demonstrates how to use the different functions of the mcp23017 lua module. + + @author Marcel P. | Plomi.net + @github https://github.com/plomi-net + + @version 1.0.0 + +]] + + + + + +--[[ + + initialize and setup + +]] + +-- initialize module +local mcp23017 = require "mcp23017" + +-- set the address for MCP23017 +local address = 0x20 + +-- SCL pin = 1 = D1 / GPIO 5 (ESP8266) +local cSCL = 1 + +-- SDA pin = 2 = D2 / GPIO4 (ESP8266) +local cSDA = 2 + +local i2cId = 0 + +-- setup i2c bus and create instance for mcp23017 (assigned to mcp) +i2c.setup(i2cId, cSDA, cSCL, i2c.SLOW) +local mcp = mcp23017(address, i2cId) + + +--[[ + + set output and input channels + +]] + +-- set pin 7 and 8 to output (GPA7 and GPB0) and GPB1 to input +mcp:setMode(mcp.GPA, 7, mcp.OUTPUT) +mcp:setMode(mcp.GPB, 0, mcp.OUTPUT) +mcp:setMode(mcp.GPB, 1, mcp.INPUT) + + + +--[[ + + set output channels to high and low + +]] + +-- set pin 7 to high (GPA7) +mcp:setPin(mcp.GPA, 7, mcp.HIGH) +-- set pin 8 to low (GPB0) +mcp:setPin(mcp.GPB, 0, mcp.LOW) + + + + +--[[ + + toggle pin 6 channel state every second (blinking) + +]] + +local currentPin = 6 +local currentState = false + +mcp:setMode(mcp.GPA, currentPin, mcp.OUTPUT) + +tmr.create():alarm(1000, tmr.ALARM_AUTO, function() + if currentState == true then + -- print("set to low") + mcp:setPin(mcp.GPA, currentPin, mcp.LOW) + currentState = false + else + -- print("set to high") + mcp:setPin(mcp.GPA, currentPin, mcp.HIGH) + currentState = true + end +end) + + + + + +--[[ + + read input channels and display every 7 seconds + +]] + +-- read input register +tmr.create():alarm(7000, tmr.ALARM_AUTO, function() + local a = mcp:readGPIO(mcp.GPA) + print(" ") + print("GPIO A input states: " .. a) + + local b = mcp:readGPIO(mcp.GPB) + print("GPIO B input states: " .. b) + print(" ") +end) diff --git a/lua_modules/mcp23017/README.md b/lua_modules/mcp23017/README.md new file mode 100644 index 0000000000..f6d6d8089c --- /dev/null +++ b/lua_modules/mcp23017/README.md @@ -0,0 +1,3 @@ +# MCP23017 Module + +Documentation for this Lua module is available in the [mcp23017.md](../../docs/lua-modules/mcp23017.md) file and in the [Official NodeMCU Documentation](https://nodemcu.readthedocs.io/) in `Lua Modules` section. diff --git a/lua_modules/mcp23017/mcp23017.lua b/lua_modules/mcp23017/mcp23017.lua new file mode 100644 index 0000000000..38eb4ec876 --- /dev/null +++ b/lua_modules/mcp23017/mcp23017.lua @@ -0,0 +1,289 @@ +--[[ + + This Lua module provides access to the MCP23017 module. + + The MCP23017 is a port expander and provides 16 channels for inputs and outputs. + Up to 8 devices (128 channels) are possible by the configurable address (A0 - A2 Pins). + + Due to the 16 channels, 2 bytes are required for switching outputs or reading input signals. These are A and B. + A single pin can be set or a whole byte. The numbering of the individual pins starts at 0 and ends with 15. + The numbers up to 7 are on register GPIO A and from 8 to 15 on GPIO B. + + The module requires `i2c` and `bit` C module built into firmware. + + + @author Marcel P. | Plomi.net + @github https://github.com/plomi-net + + @version 1.0.0 + +]] + +local i2c, string = i2c, string +local issetBit = bit.isset +local setBit = bit.set +local clearBit = bit.clear + +-- MCP23017_ADDRESS = 0x20 -- (0x20 to 0x27) +-- MCP23017_SDA = 2 -- D2/GPIO 4 +-- MCP23017_SCL = 1 -- D1/GPIO 5 + + +-- metatable +local mcp23017 = {} +mcp23017.__index = mcp23017 + +-- registers (not used registers are commented out) +local MCP23017_IODIRA = 0x00 +local MCP23017_IODIRB = 0x01 +local MCP23017_DEFVALA = 0x06 +local MCP23017_DEFVALB = 0x07 +local MCP23017_GPIOA = 0x12 +local MCP23017_GPIOB = 0x13 + +--[[ +local MCP23017_IPOLA = 0x02 +local MCP23017_IPOLB = 0x03 +local MCP23017_GPINTENA = 0x04 +local MCP23017_GPINTENB = 0x05 +local MCP23017_DEFVALA = 0x06 +local MCP23017_DEFVALB = 0x07 +local MCP23017_INTCONA = 0x08 +local MCP23017_INTCONB = 0x09 +local MCP23017_IOCON = 0x0A +local MCP23017_IOCON2 = 0x0B +local MCP23017_GPPUA = 0x0C +local MCP23017_GPPUB = 0x0D +local MCP23017_INTFA = 0x0E +local MCP23017_INTFB = 0x0F + +local MCP23017_INTCAPA = 0x10 +local MCP23017_INTCAPB = 0x11 +local MCP23017_OLATA = 0x14 +local MCP23017_OLATB = 0x15 +]] + +-- check device is available on address +function mcp23017:checkDevice() + i2c.start(self.i2cId) + local response = i2c.address(self.i2cId, self.address, i2c.TRANSMITTER) + i2c.stop(self.i2cId) + if response ~= true then + print("MCP23017 device on " .. string.format('0x%02X', self.address) .. " not found") + end + return response +end + +-- check device address (0x20 to 0x27) +function mcp23017:checkAddress() + local addr = tonumber(self.address) + if (addr > 31 and addr < 40) then + return true + else + print("MCP23017 address is out of range") + return false + end +end + +-- write byte +function mcp23017:writeByte(registerAddr, val) + i2c.start(self.i2cId) + i2c.address(self.i2cId, self.address, i2c.TRANSMITTER) + i2c.write(self.i2cId, registerAddr) + i2c.write(self.i2cId, val) + i2c.stop(self.i2cId) +end + +-- read byte +function mcp23017:readByte(registerAddr) + i2c.start(self.i2cId) + i2c.address(self.i2cId, self.address, i2c.TRANSMITTER) + i2c.write(self.i2cId, registerAddr) + i2c.stop(self.i2cId) + i2c.start(self.i2cId) + i2c.address(self.i2cId, self.address, i2c.RECEIVER) + local data = i2c.read(self.i2cId, 1) + i2c.stop(self.i2cId) + return string.byte(data) +end + +-- get IO dir register +function mcp23017:getDirRegisterAddr(bReg) + if bReg == self.GPB then + return MCP23017_IODIRB + else + return MCP23017_IODIRA + end +end + +-- get GPIO register address +function mcp23017:getGPIORegisterAddr(bReg) + if bReg == self.GPB then + return MCP23017_GPIOB + else + return MCP23017_GPIOA + end +end + +-- check pin is in range +function mcp23017:checkPinIsInRange(pin) + if pin > 7 or pin < 0 then + print("The pin must be between 0 and 7") + return nil + end + return pin +end + +-- setup internal pullup +function mcp23017:setInternalPullUp(bReg, iByte) + if bReg == self.GPB then + self:writeByte(MCP23017_DEFVALB, iByte) + else + self:writeByte(MCP23017_DEFVALA, iByte) + end +end + +-- set default GPIO mode +function mcp23017:setDefaultMode(bReg, iByte) + self:writeByte(self:getDirRegisterAddr(bReg), iByte) +end + +function mcp23017:numberToBool(val) + if val == 1 or val == true or val == '1' then + return true + else + return false + end +end + +-- reset gpio mode +function mcp23017:reset() + -- all to input + self:setDefaultMode(self.GPA, 0xFF) + self:setDefaultMode(self.GPB, 0xFF) +end + +-- setup device +function mcp23017:setup(address, i2cId) + + self.address = string.format('0x%02X', address) + self.i2cId = i2cId + if (self:checkAddress() ~= true) or (self:checkDevice() ~= true) then + self.deviceOk = false + return 0 + else + self.deviceOk = true + self:reset() + return 1 + end +end + +-- set mode for a pin +function mcp23017:setMode(bReg, pin, mode) + if self.deviceOk == false then + return nil + end + + local inReq = self:getDirRegisterAddr(bReg) + local inPin = self:checkPinIsInRange(pin) + local response = self:readByte(inReq) + local newState + + if self:numberToBool(mode) == self.OUTPUT then + newState = clearBit(response, inPin) + else + newState = setBit(response, inPin) + end + + self:writeByte(inReq, newState) + return true +end + +-- set pin to low or high +function mcp23017:setPin(bReg, pin, state) + if self.deviceOk == false then + return nil + end + + local inReq = self:getGPIORegisterAddr(bReg) + local inPin = self:checkPinIsInRange(pin) + local response = self:readByte(inReq) + local newState + + if self:numberToBool(state) == self.HIGH then + newState = setBit(response, inPin) + else + newState = clearBit(response, inPin) + end + + self:writeByte(inReq, newState) + return true +end + +-- read pin input +function mcp23017:getPinState(bReg, pin) + if self.deviceOk == false then + return nil + end + + local inReq = self:getGPIORegisterAddr(bReg) + local inPin = self:checkPinIsInRange(pin) + local response = self:readByte(inReq) + return issetBit(response, inPin) +end + +function mcp23017:writeToRegister(registerAddr, newByte) + if self.deviceOk == false then + return nil + end + return self:writeByte(registerAddr, newByte) +end + +function mcp23017:readFromRegister(registerAddr) + if self.deviceOk == false then + return nil + end + return self:readByte(registerAddr) +end + +function mcp23017:writeIODIR(bReg, newByte) + if self.deviceOk == false then + return nil + end + return self:writeToRegister(self:getDirRegisterAddr(bReg), newByte) +end + +function mcp23017:writeGPIO(bReg, newByte) + if self.deviceOk == false then + return nil + end + return self:writeToRegister(self:getGPIORegisterAddr(bReg), newByte) +end + +function mcp23017:readGPIO(bReg) + if self.deviceOk == false then + return nil + end + return self:readFromRegister(self:getGPIORegisterAddr(bReg)) +end + +return function(address, i2cId) + local self = {} + setmetatable(self, mcp23017) + + -- defaults + self.deviceOK = false + self.i2cId = 0 + self.address = nil + self.OUTPUT = false + self.INPUT = true + self.GPA = false + self.GPB = true + self.HIGH = true + self.LOW = false + + self:setup(address, i2cId) + return self +end + + diff --git a/mkdocs.yml b/mkdocs.yml index ff40812943..ae49be4f4a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -57,6 +57,7 @@ pages: - 'liquidcrystal': 'lua-modules/liquidcrystal.md' - 'lm92': 'lua-modules/lm92.md' - 'mcp23008': 'lua-modules/mcp23008.md' + - 'mcp23017': 'lua-modules/mcp23017.md' - 'redis': 'lua-modules/redis.md' - 'telnet': 'lua-modules/telnet.md' - 'yeelink': 'lua-modules/yeelink.md'