From ef353809eb3e2f18a53eb661a1f02ed997d99293 Mon Sep 17 00:00:00 2001 From: Marcel P <32096029+plomi-net@users.noreply.github.com> Date: Sun, 25 Oct 2020 12:48:34 +0100 Subject: [PATCH] Add mcp23017 Lua module (#3197) --- docs/lua-modules/mcp23017.md | 230 +++++++++++++++++++++ lua_examples/mcp23017/mcp23017_example.lua | 77 +++++++ lua_modules/mcp23017/README.md | 3 + lua_modules/mcp23017/mcp23017.lua | 185 +++++++++++++++++ mkdocs.yml | 1 + 5 files changed, 496 insertions(+) create mode 100644 docs/lua-modules/mcp23017.md create mode 100644 lua_examples/mcp23017/mcp23017_example.lua create mode 100644 lua_modules/mcp23017/README.md create mode 100644 lua_modules/mcp23017/mcp23017.lua diff --git a/docs/lua-modules/mcp23017.md b/docs/lua-modules/mcp23017.md new file mode 100644 index 00000000..0fc13369 --- /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 I²C I/O Expander. + +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 (remember to call i2c.setup before) + +#### 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(mcp.GPA, 7, mcp.OUTPUT) +mcp:setMode(mcp.GPB, 0, mcp.OUTPUT) +mcp:setMode(mcp.GPB, 1, mcp.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(mcp.GPA, 7, mcp.HIGH) +-- set pin 8 to low (GPB0) +mcp:setPin(mcp.GPB, 0, mcp.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(mcp.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(mcp.GPA, 0xFF)) +-- disable all pullup resistors for GPA +print(mcp:setInternalPullUp(mcp.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(mcp.GPA, 0xFF)) +-- set all GPA to output +print(mcp:writeIODIR(mcp.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(mcp.GPA, 0xFF)) +-- set all GPA to LOW +print(mcp:writeGPIO(mcp.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(mcp.GPA)) +``` + diff --git a/lua_examples/mcp23017/mcp23017_example.lua b/lua_examples/mcp23017/mcp23017_example.lua new file mode 100644 index 00000000..b01cb9d8 --- /dev/null +++ b/lua_examples/mcp23017/mcp23017_example.lua @@ -0,0 +1,77 @@ +--[[ + 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" +local address = 0x20 -- the address for MCP23017 (between 0x20 and 0x27) +local cSCL = 1 -- SCL pin = 1 = D1 / GPIO 5 (ESP8266) +local cSDA = 2 -- SDA pin = 2 = D2 / GPIO4 (ESP8266) +local i2cId = 0 -- i2c id +-- 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 currentReg = mcp.GPA +local currentState = false + +mcp:setMode(currentReg, currentPin, mcp.OUTPUT) +tmr.create():alarm(1000, tmr.ALARM_AUTO, function() + if currentState == true then + -- print("set to low") + mcp:setPin(currentReg, currentPin, mcp.LOW) + currentState = false + else + -- print("set to high") + mcp:setPin(currentReg, 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) + if a ~= nil then + print("GPIO A input states: " .. a) + else + print("GPIO A unreadable, check device") + end + + local b = mcp:readGPIO(mcp.GPB) + if b ~= nil then + print("GPIO B input states: " .. b) + else + print("GPIO B unreadable, check device") + end + print(" ") +end) diff --git a/lua_modules/mcp23017/README.md b/lua_modules/mcp23017/README.md new file mode 100644 index 00000000..f6d6d808 --- /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 00000000..47aedccf --- /dev/null +++ b/lua_modules/mcp23017/mcp23017.lua @@ -0,0 +1,185 @@ +--[[ + 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 7. + The numbers are for each register GPIO A and 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, issetBit, setBit, clearBit, error = + i2c, string, bit.isset, bit.set, bit.clear, error +local isINPUT, isGPB, isHIGH = true, true, true + +-- 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 +]] + +-- metatable +local mcp23017 = { + INPUT = isINPUT, + OUTPUT = not isINPUT, + GPA = not isGPB, + GPB = isGPB, + HIGH = isHIGH, + LOW = not isHIGH +} +mcp23017.__index = mcp23017 + +-- check device is available on address +local function checkDevice(address, i2cId) + i2c.start(i2cId) + local response = i2c.address(i2cId, address, i2c.TRANSMITTER) + i2c.stop(i2cId) + return response +end + +-- write byte +local function writeByte(address, i2cId, registerAddr, val) + i2c.start(i2cId) + i2c.address(i2cId, address, i2c.TRANSMITTER) + i2c.write(i2cId, registerAddr) + i2c.write(i2cId, val) + i2c.stop(i2cId) +end + +-- read byte +local function readByte(address, i2cId, registerAddr) + i2c.start(i2cId) + i2c.address(i2cId, address, i2c.TRANSMITTER) + i2c.write(i2cId, registerAddr) + i2c.stop(i2cId) + i2c.start(i2cId) + i2c.address(i2cId, address, i2c.RECEIVER) + local data = i2c.read(i2cId, 1) + i2c.stop(i2cId) + return string.byte(data) +end + +-- check pin is in range +local function checkPinIsInRange(pin) + if pin > 7 or pin < 0 then + error("MCP23017 the pin must be between 0 and 7") + end + return pin +end + +local function reset(address, i2cId) + writeByte(address, i2cId, MCP23017_IODIRA, 0xFF) + writeByte(address, i2cId, MCP23017_IODIRB, 0xFF) +end + +-- setup device +local function setup(address, i2cId) + + -- check device address (0x20 to 0x27) + if (address < 32 or address > 39) then + error("MCP23017 address is out of range") + end + + if (checkDevice(address, i2cId) ~= true) then + error("MCP23017 device on " .. string.format('0x%02X', address) .. " not found") + else + reset(address, i2cId) + return 1 + end +end + +return function(address, i2cId) + local self = setmetatable({}, mcp23017) + + if setup(address, i2cId) then + self.writeIODIR = function(sf, bReg, newByte) -- luacheck: no unused + writeByte(address, i2cId, + bReg == isGPB and MCP23017_IODIRB or MCP23017_IODIRA, + newByte) + end + + self.writeGPIO = function(sf, bReg, newByte) -- luacheck: no unused + writeByte(address, i2cId, + bReg == isGPB and MCP23017_GPIOB or MCP23017_GPIOA, newByte) + end + + self.readGPIO = function(sf, bReg) -- luacheck: no unused + return readByte(address, i2cId, -- upvals + bReg == isGPB and MCP23017_GPIOB or MCP23017_GPIOA) + end + + -- read pin input + self.getPinState = function(sf, bReg, pin) -- luacheck: no unused + return issetBit(readByte(address, i2cId, + bReg == isGPB and MCP23017_GPIOB or MCP23017_GPIOA), + checkPinIsInRange(pin)) + end + + -- set pin to low or high + self.setPin = function(sf, bReg, pin, state) -- luacheck: no unused + local inReq = bReg == isGPB and MCP23017_GPIOB or MCP23017_GPIOA + local inPin = checkPinIsInRange(pin) + local response = readByte(address, i2cId, inReq) + writeByte(address, i2cId, inReq, + state == isHIGH and setBit(response, inPin) or clearBit(response, inPin)) + return true + end + + -- set mode for a pin + self.setMode = function(sf, bReg, pin, mode) -- luacheck: no unused + local inReq = bReg == isGPB and MCP23017_IODIRB or MCP23017_IODIRA + local inPin = checkPinIsInRange(pin) + local response = readByte(address, i2cId, inReq) + writeByte(address, i2cId, inReq, + mode == isINPUT and setBit(response, inPin) or clearBit(response, inPin)) + return true + end + + -- reset gpio mode + self.reset = function(sf) -- luacheck: no unused + reset(address, i2cId) + end + + -- setup internal pullup + self.setInternalPullUp = function(sf, bReg, iByte) -- luacheck: no unused + writeByte(address, i2cId, + bReg == isGPB and MCP23017_DEFVALB or MCP23017_DEFVALA, iByte) + end + + return self + end + return nil +end + + diff --git a/mkdocs.yml b/mkdocs.yml index c266bf39..e7e4c428 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -62,6 +62,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'