1
0
mirror of https://github.com/RIOT-OS/RIOT.git synced 2025-12-17 18:43:50 +01:00
RIOT/drivers/mcp23x17/mcp23x17.c

623 lines
18 KiB
C

/*
* Copyright (C) 2021 Gunar Schorcht
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @ingroup drivers_mcp23x17
* @brief Device driver implementation for Microchip MCP23x17 I/O expanders
* @author Gunar Schorcht <gunar@schorcht.net>
* @file
*/
#include <stdint.h>
#include "assert.h"
#include "periph/i2c.h"
#include "ztimer.h"
#include "mcp23x17.h"
#include "mcp23x17_regs.h"
#if IS_USED(MODULE_MCP23X17_IRQ)
#include "event/thread.h"
#endif
#define ENABLE_DEBUG 0
#include "debug.h"
#if ENABLE_DEBUG
#include <string.h>
#define DEBUG_DEV(m, d, ...) \
DEBUG("[mcp23x17] %s dev=%" PRIxPTR " addr=%02x: " m "\n", \
__func__, (unsigned int)d, d->params.addr, ## __VA_ARGS__)
#else /* ENABLE_DEBUG */
#define DEBUG_DEV(f, d, ...)
#endif /* ENABLE_DEBUG */
#define _ADDR (MCP23X17_BASE_ADDR + dev->params.addr)
#if IS_USED(MODULE_MCP23X17_SPI)
#define _SPI_DEV (dev->params.if_params.spi.dev)
#define _SPI_CS (dev->params.if_params.spi.cs)
#define _SPI_CLK (dev->params.if_params.spi.clk)
#endif
#if IS_USED(MODULE_MCP23X17_I2C)
#define _I2C_DEV (dev->params.if_params.i2c.dev)
#endif
#if IS_USED(MODULE_MCP23X17_IRQ)
#if IS_USED(MODULE_MCP23X17_IRQ_MEDIUM)
#define MCP23X17_EVENT_PRIO EVENT_PRIO_MEDIUM
#elif IS_USED(MODULE_MCP23X17_IRQ_HIGHEST)
#define MCP23X17_EVENT_PRIO EVENT_PRIO_HIGHEST
#endif
/* interrupt service routine for IRQs */
static void _irq_isr(void *arg);
/* declaration of IRQ handler function */
static void _irq_handler(event_t *event);
#endif /* MODULE_MCP23X17_IRQ */
/* forward declarations of internal functions */
static void _acquire(const mcp23x17_t *dev);
static void _release(const mcp23x17_t *dev);
static int _read(const mcp23x17_t *dev, uint8_t reg, uint8_t *data, size_t len);
static int _write(const mcp23x17_t *dev, uint8_t reg, const uint8_t *data, size_t len);
static int _update_pin(const mcp23x17_t *dev, uint8_t reg, gpio_t pin, int value);
/* static power on reset configuration */
static const uint8_t _reset_conf[] =
{
0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
int mcp23x17_init(mcp23x17_t *dev, const mcp23x17_params_t* params)
{
assert(dev);
assert(params);
dev->params = *params;
dev->od_pins = 0;
DEBUG_DEV("params %p", dev, params);
int res = MCP23X17_OK;
#if IS_USED(MODULE_MCP23X17_SPI)
if (params->if_params.type == MCP23X17_SPI) {
/* CS pin has to be defined and has to be initialized */
assert(gpio_is_valid(_SPI_CS));
if (spi_init_cs(_SPI_DEV, _SPI_CS) != SPI_OK) {
DEBUG_DEV("CS pin defined but could not be initialized\n", dev);
return -MCP23X17_ERROR_NO_DEV;
}
}
#endif
#if IS_USED(MODULE_MCP23X17_RESET)
/* GPIO pin for the RESET signal has to be define and initialized */
assert(gpio_is_valid(params->reset_pin));
/* initialize the low active RESET pin */
if (gpio_init(params->reset_pin, GPIO_OUT)) {
DEBUG_DEV("RESET pin defined but could not be initialized", dev);
return -MCP23X17_ERROR_RESET_PIN;
}
/* hardware reset impuls for at least 10 us is required, we use 1 ms */
gpio_clear(params->reset_pin);
ztimer_sleep(ZTIMER_MSEC, 1);
gpio_set(params->reset_pin);
#endif
#if IS_USED(MODULE_MCP23X17_IRQ)
/* GPIO pin for combined interrupt signal INTA/INTB has to be defined */
assert(gpio_is_valid(params->int_pin));
/* initialize the IRQ event object used for delaying interrupts */
dev->irq_event.event.handler = _irq_handler;
dev->irq_event.dev = dev;
for (unsigned i = 0; i < MCP23X17_GPIO_PIN_NUM; i++) {
dev->isr[i].cb = NULL;
dev->isr[i].arg = NULL;
}
/* GPIO for interrupt signal has to be initialized */
if (gpio_init_int(dev->params.int_pin, GPIO_IN, GPIO_FALLING,
_irq_isr, (void*)dev)) {
DEBUG_DEV("INT pin defined but could not be initialized", dev);
return -MCP23X17_ERROR_INT_PIN;
}
#endif /* MODULE_MCP23X17_IRQ */
_acquire(dev);
/*
* After power on reset or hardware reset, the default BANK mode is 0
* i.e., the A/B registers are paired and in the same bank with
* subsequent ascending addresses.
* Since the BANK mode is never changed, we can rely on the addressing
* scheme as define in mcp23x17_regs.h.
*/
uint8_t iocon; /* configuration register is the same for port A and B */
/* read the configuration registers to see whether device is reachable */
if (_read(dev, MCP23X17_REG_IOCONA, &iocon, 1) ||
_read(dev, MCP23X17_REG_IOCONB, &iocon, 1)) {
DEBUG_DEV("error reading IOCON registers", dev);
_release(dev);
return -MCP23X17_ERROR_NO_DEV;
}
/*
* After power on reset or hardware reset, the configuration of GPIOs is:
*
* - GPIO pins are defined as inputs
* - GPIO registers reflects the same logic state of the input pin
* - GPIO pin value is compared against previous pin value for
* interrupt-on-change
* - GPIO input pins are disabled for interrupts on change
* - GPIO pull-ups are disabled
* - GPIO default output values are 0
*
* If hardware reset is not used, we have to restore this configuration
* after system reboots
*/
res |= _write(dev, MCP23X17_REG_IODIR, _reset_conf, ARRAY_SIZE(_reset_conf));
#if IS_USED(MODULE_MCP23X17_IRQ)
/* INT is configured as push/pull and is active low */
iocon &= ~MCP23X17_IOCON_ODR;
iocon &= ~MCP23X17_IOCON_INTPOL;
/*
* Since we use only one pin for INTA and INTB signal, we have to use
* the MIRROR mode, i.e., an interrupt on either port will cause both
* interrupt pins to activate.
*/
iocon |= MCP23X17_IOCON_MIRROR;
/*
* Reset all interrupt-on-change control bits to 0, that is, pin values
* are compared against the previous value for interrupt-on-change.
* Disable all interrupts.
*
* Since this corresponds to the power on reset configuration written
* before, we have not execute it here.
*/
#if 0
uint8_t zero = 0;
res |= _write(dev, MCP23X17_REG_INTCONA, &zero, 1);
res |= _write(dev, MCP23X17_REG_INTCONB, &zero, 1);
res |= _write(dev, MCP23X17_REG_GPINTENA, &zero, 1);
res |= _write(dev, MCP23X17_REG_GPINTENB, &zero, 1);
#endif
#endif /* MODULE_MCP23X17_IRQ */
iocon &= ~MCP23X17_IOCON_SEQOP; /* sequential operation mode enabled */
iocon &= ~MCP23X17_IOCON_DISSLW; /* slew rate control enabled */
iocon |= MCP23X17_IOCON_HAEN; /* hardware addressing enabled */
/* write back configuration registers */
res |= _write(dev, MCP23X17_REG_IOCONA, &iocon, 1);
res |= _write(dev, MCP23X17_REG_IOCONB, &iocon, 1);
_release(dev);
return res;
}
int mcp23x17_gpio_init(mcp23x17_t *dev, gpio_t pin, gpio_mode_t mode)
{
assert(dev);
assert(pin < MCP23X17_GPIO_PIN_NUM);
DEBUG_DEV("pin %u, mode %d", dev, pin, mode);
uint8_t port = pin >> 3;
uint8_t pin_bit = 1 << (pin & 0x07);
uint8_t iodir;
uint8_t gppu;
uint8_t gpinten;
int res;
_acquire(dev);
/* read the I/O direction configuration and GPIO pull-up register */
if ((res = _read(dev, MCP23X17_REG_IODIR + port, &iodir, 1)) ||
(res = _read(dev, MCP23X17_REG_GPPU + port, &gppu, 1)) ||
(res = _read(dev, MCP23X17_REG_GPINTEN + port, &gpinten, 1))) {
DEBUG_DEV("error reading IODIR, GPPU or GPINTEN register", dev);
_release(dev);
return res;
}
/* set default configuration first */
iodir |= pin_bit; /* input pin */
gppu &= ~pin_bit; /* no pull-up */
gpinten &= ~pin_bit; /* interrupt disabled */
dev->od_pins &= ~(1 << pin); /* open-drain flag not set */
/* override only non default settings */
switch (mode) {
case GPIO_OUT: iodir &= ~pin_bit; /* change direction to output */
break;
case GPIO_OD_PU: gppu |= pin_bit; /* enable pull-up */
/* intentionally falls through */
case GPIO_OD: dev->od_pins |= (1 << pin); /* set open-drain flag */
_update_pin(dev, MCP23X17_REG_GPIO, pin, 0); /* clear pin */
break;
case GPIO_IN_PU: gppu |= pin_bit; /* enable pull-up */
/* intentionally falls through */
case GPIO_IN: break;
default: DEBUG_DEV("invalid pin mode", dev);
_release(dev);
return -MCP23X17_ERROR_INV_MODE;
}
/* write back the I/O direction configuration and GPIO pull-up register */
if ((res = _write(dev, MCP23X17_REG_GPINTEN + port, &gpinten, 1)) ||
(res = _write(dev, MCP23X17_REG_IODIR + port, &iodir, 1)) ||
(res = _write(dev, MCP23X17_REG_GPPU + port, &gppu, 1))) {
DEBUG_DEV("error writing IODIR, GPPU or GPINTEN register", dev);
_release(dev);
return res;
}
_release(dev);
return MCP23X17_OK;
}
int mcp23x17_gpio_read(mcp23x17_t *dev, gpio_t pin)
{
assert(dev);
assert(pin < MCP23X17_GPIO_PIN_NUM);
DEBUG_DEV("pin %u", dev, pin);
uint8_t port = pin >> 3;
pin -= (port << 3);
uint8_t gpio;
/* read the GPIO port register */
_acquire(dev);
int res = _read(dev, MCP23X17_REG_GPIO + port, &gpio, 1);
_release(dev);
if (res) {
DEBUG_DEV("error reading GPIO register", dev);
return res;
}
return (gpio & (1 << pin)) ? 1 : 0;
}
void mcp23x17_gpio_write(mcp23x17_t *dev, gpio_t pin, int value)
{
assert(dev);
assert(pin < MCP23X17_GPIO_PIN_NUM);
DEBUG_DEV("pin %u, value %d", dev, pin, value);
_acquire(dev);
/* check whether it is an emulated OD pin */
if (dev->od_pins & (1 << pin)) {
if (value) {
/* simply set direction to input */
_update_pin(dev, MCP23X17_REG_IODIR, pin, 1);
}
else {
/* set direction to output, was cleared during initialization */
_update_pin(dev, MCP23X17_REG_IODIR, pin, 0);
}
}
else {
_update_pin(dev, MCP23X17_REG_GPIO, pin, value);
}
_release(dev);
}
void mcp23x17_gpio_set(mcp23x17_t *dev, gpio_t pin)
{
mcp23x17_gpio_write(dev, pin, 1);
}
void mcp23x17_gpio_clear(mcp23x17_t *dev, gpio_t pin)
{
mcp23x17_gpio_write(dev, pin, 0);
}
void mcp23x17_gpio_toggle(mcp23x17_t *dev, gpio_t pin)
{
DEBUG_DEV("pin %u", dev, pin);
mcp23x17_gpio_write(dev, pin, mcp23x17_gpio_read(dev, pin) ? 0 : 1);
}
#if IS_USED(MODULE_MCP23X17_IRQ)
int mcp23x17_gpio_init_int(mcp23x17_t *dev, gpio_t pin,
gpio_mode_t mode,
gpio_flank_t flank,
gpio_cb_t isr,
void *arg)
{
assert(dev);
assert(pin < MCP23X17_GPIO_PIN_NUM);
assert(isr != NULL);
DEBUG_DEV("pin %u, mode %d, flank %d, isr %p, arg %p",
dev, pin, mode, flank, isr, arg);
/* initialize the pin */
int res = mcp23x17_gpio_init(dev, pin, mode);
if (res != MCP23X17_OK) {
return res;
}
switch (flank) {
case GPIO_FALLING:
case GPIO_RISING:
case GPIO_BOTH: dev->isr[pin].cb = isr;
dev->isr[pin].arg = arg;
dev->flank[pin] = flank;
break;
default: DEBUG_DEV("invalid flank %d for pin %d", dev, flank, pin);
return -MCP23X17_ERROR_INV_FLANK;
}
/* enable the interrupt */
mcp23x17_gpio_irq_enable(dev, pin);
return MCP23X17_OK;
}
void mcp23x17_gpio_irq_enable(mcp23x17_t *dev, gpio_t pin)
{
/* some parameter sanity checks */
assert(dev != NULL);
assert(pin < MCP23X17_GPIO_PIN_NUM);
DEBUG_DEV("pin %u", dev, pin);
_acquire(dev);
/* delete pending interrupts */
uint8_t regs[4];
/* read the GPIO port register */
if (_read(dev, MCP23X17_REG_INTF, regs, ARRAY_SIZE(regs))) {
DEBUG_DEV("error reading INTF and INTCAP registers", dev);
_release(dev);
return;
}
_update_pin(dev, MCP23X17_REG_GPINTEN, pin, 1);
_release(dev);
}
void mcp23x17_gpio_irq_disable(mcp23x17_t *dev, gpio_t pin)
{
/* some parameter sanity checks */
assert(dev != NULL);
assert(pin < MCP23X17_GPIO_PIN_NUM);
DEBUG_DEV("pin %u", dev, pin);
_acquire(dev);
_update_pin(dev, MCP23X17_REG_GPINTEN, pin, 0);
_release(dev);
}
/* interrupt service routine for IRQs */
static void _irq_isr(void *arg)
{
/* some parameter sanity checks */
assert(arg != NULL);
/* just indicate that an interrupt occurred and return */
event_post(MCP23X17_EVENT_PRIO, (event_t*)&((mcp23x17_t*)arg)->irq_event);
}
/* handle one IRQ event of device referenced by the event */
static void _irq_handler(event_t* event)
{
mcp23x17_irq_event_t* irq_event = (mcp23x17_irq_event_t*)event;
assert(irq_event != NULL);
assert(irq_event->dev);
mcp23x17_t *dev = irq_event->dev;
DEBUG_DEV("event %p", dev, event);
uint8_t regs[4];
/* read the GPIO port register */
_acquire(dev);
if (_read(dev, MCP23X17_REG_INTF, regs, ARRAY_SIZE(regs))) {
DEBUG_DEV("error reading INTF and INTCAP registers", dev);
_release(dev);
return;
}
_release(dev);
/* iterate over all pins to check whether ISR has to be called */
for (unsigned i = 0; i < MCP23X17_GPIO_PIN_NUM; i++) {
uint8_t port = i >> 3;
uint8_t pin_bit = 1 << (i & 0x07);
/* test whether interrupt flag is set and cb is defined for the pin */
if ((regs[port] & pin_bit) && dev->isr[i].cb != NULL) {
/* check the flank and the activated flank mode */
if (dev->flank[i] == GPIO_BOTH || /* no matter what flank */
((regs[2 + port] & pin_bit) == 0 && /* new value is 0 -> falling flank */
(dev->flank[i] == GPIO_FALLING)) ||
((regs[2 + port] & pin_bit) && /* new value is 1 -> rising flank */
(dev->flank[i] == GPIO_RISING))) {
/* call the ISR */
dev->isr[i].cb(dev->isr[i].arg);
}
}
}
}
#endif /* MODULE_MCP23X17_IRQ */
/* internal functions */
static void _acquire(const mcp23x17_t *dev)
{
#if IS_USED(MODULE_MCP23X17_SPI)
if (dev->params.if_params.type == MCP23X17_SPI) {
spi_acquire(_SPI_DEV, _SPI_CS, SPI_MODE_0, _SPI_CLK);
}
#endif
#if IS_USED(MODULE_MCP23X17_I2C)
if (dev->params.if_params.type == MCP23X17_I2C) {
i2c_acquire(_I2C_DEV);
}
#endif
}
static void _release(const mcp23x17_t *dev)
{
#if IS_USED(MODULE_MCP23X17_SPI)
if (dev->params.if_params.type == MCP23X17_SPI) {
spi_release(_SPI_DEV);
}
#endif
#if IS_USED(MODULE_MCP23X17_I2C)
if (dev->params.if_params.type == MCP23X17_I2C) {
i2c_release(_I2C_DEV);
}
#endif
}
static int _read(const mcp23x17_t *dev, uint8_t reg, uint8_t *data, size_t len)
{
DEBUG_DEV("reg=%02x data=%p len=%d", dev, reg, data, len);
int res = MCP23X17_OK;
#if IS_USED(MODULE_MCP23X17_SPI)
if (dev->params.if_params.type == MCP23X17_SPI) {
spi_transfer_byte(_SPI_DEV, _SPI_CS, true, (_ADDR << 1) | 1);
spi_transfer_byte(_SPI_DEV, _SPI_CS, true, reg);
spi_transfer_bytes(_SPI_DEV, _SPI_CS, false, NULL, data, len);
}
#endif
#if IS_USED(MODULE_MCP23X17_I2C)
if (dev->params.if_params.type == MCP23X17_I2C) {
int res = i2c_read_regs(_I2C_DEV, _ADDR, reg, data, len, 0);
if (res) {
DEBUG_DEV("I2C read error: %d (%s)", dev, res, strerror(res * -1));
return -MCP23X17_ERROR_I2C;
}
else {
return MCP23X17_OK;
}
}
#endif
if (ENABLE_DEBUG) {
printf("[mcp23x17] %s dev=%" PRIxPTR " addr=%02x reg=%02x read: ",
__func__, (uintptr_t)dev, dev->params.addr, reg);
for (uint8_t i = 1; i < len + 1; i++) {
printf("%02x ", data[i]);
}
printf("\n");
}
return res;
}
static int _write(const mcp23x17_t *dev, uint8_t reg,
const uint8_t *data, size_t len)
{
DEBUG_DEV("reg=%02x data=%p len=%d", dev, reg, data, len);
if (ENABLE_DEBUG) {
printf("[mcp23x17] %s dev=%" PRIxPTR " addr=%02x reg=%02x write: ",
__func__, (uintptr_t)dev, dev->params.addr, reg);
for (uint8_t i = 1; i < len + 1; i++) {
printf("%02x ", data[i]);
}
printf("\n");
}
#if IS_USED(MODULE_MCP23X17_SPI)
if (dev->params.if_params.type == MCP23X17_SPI) {
spi_transfer_byte(_SPI_DEV, _SPI_CS, true, (_ADDR << 1));
spi_transfer_byte(_SPI_DEV, _SPI_CS, true, reg);
spi_transfer_bytes(_SPI_DEV, _SPI_CS, false, data, NULL, len);
return MCP23X17_OK;
}
#endif
#if IS_USED(MODULE_MCP23X17_I2C)
if (dev->params.if_params.type == MCP23X17_I2C) {
int res = i2c_write_regs(_I2C_DEV, _ADDR, reg, data, len, 0);
if (res) {
DEBUG_DEV("I2C write error: %d (%s)", dev, res, strerror(res * -1));
return -MCP23X17_ERROR_I2C;
}
else {
return MCP23X17_OK;
}
}
#endif
return -MCP23X17_ERROR_NO_DEV;
}
static int _update_pin(const mcp23x17_t *dev, uint8_t reg, gpio_t pin, int value)
{
/* some parameter sanity checks */
assert(dev != NULL);
assert(pin < MCP23X17_GPIO_PIN_NUM);
DEBUG_DEV("pin %d, value %d, reg %02x", dev, pin, value, reg);
uint8_t data;
uint8_t port = pin >> 3;
pin -= (port << 3);
/* read the register */
int res = _read(dev, reg + port, &data, 1);
if (value) {
data |= (1 << pin);
}
else {
data &= ~(1 << pin);
}
/* write back the register */
res |= _write(dev, reg + port, &data, 1);
return res;
}