Browse Source

first commit of GPIO expander

Philippe G 3 years ago
parent
commit
507c2c9755

+ 1 - 0
build-scripts/ESP32-A1S-sdkconfig.defaults

@@ -11,6 +11,7 @@ CONFIG_DISPLAY_CONFIG=""
 CONFIG_I2C_CONFIG=""
 CONFIG_SPI_CONFIG=""
 CONFIG_SET_GPIO=""
+CONFIG_GPIO_EXP_CONFIG=""
 CONFIG_ROTARY_ENCODER=""
 CONFIG_LED_GREEN_GPIO=-1
 CONFIG_LED_GREEN_GPIO_LEVEL=1

+ 1 - 0
build-scripts/I2S-4MFlash-sdkconfig.defaults

@@ -12,6 +12,7 @@ CONFIG_DISPLAY_CONFIG=""
 CONFIG_I2C_CONFIG=""
 CONFIG_SPI_CONFIG=""
 CONFIG_SET_GPIO=""
+CONFIG_GPIO_EXP_CONFIG=""
 CONFIG_ROTARY_ENCODER=""
 CONFIG_LED_GREEN_GPIO=-1
 CONFIG_LED_GREEN_GPIO_LEVEL=1

+ 1 - 0
build-scripts/SqueezeAmp-sdkconfig.defaults

@@ -16,6 +16,7 @@ CONFIG_DISPLAY_CONFIG=""
 CONFIG_I2C_CONFIG=""
 CONFIG_SPI_CONFIG=""
 CONFIG_SET_GPIO=""
+CONFIG_GPIO_EXP_CONFIG=""
 CONFIG_ROTARY_ENCODER=""
 CONFIG_LED_GREEN_GPIO=12
 CONFIG_LED_GREEN_GPIO_LEVEL=0

+ 29 - 0
components/services/accessors.c

@@ -482,6 +482,35 @@ const i2c_config_t * config_i2c_get(int * i2c_port) {
 	return &i2c;
 }
 
+/****************************************************************************************
+ * Get IO expander config structure from config string
+ */
+const gpio_exp_config_t* config_gpio_exp_get(void) {
+	char *nvs_item, *p;
+	static gpio_exp_config_t config = {
+		.intr = -1,
+		.count = 8,	
+	};
+	config.phy.port = i2c_system_port;
+
+	nvs_item = config_alloc_get(NVS_TYPE_STR, "gpio_exp_config");
+	if (!nvs_item) return NULL;
+	
+	if ((p = strcasestr(nvs_item, "addr")) != NULL) config.phy.addr = atoi(strchr(p, '=') + 1);
+	if ((p = strcasestr(nvs_item, "intr")) != NULL) config.intr = atoi(strchr(p, '=') + 1);
+	if ((p = strcasestr(nvs_item, "base")) != NULL) config.base = atoi(strchr(p, '=') + 1);
+	if ((p = strcasestr(nvs_item, "count")) != NULL) config.count = atoi(strchr(p, '=') + 1);
+	if ((p = strcasestr(nvs_item, "model")) != NULL) sscanf(p, "%*[^=]=%31[^,]", config.model);
+	if ((p = strcasestr(nvs_item, "port")) != NULL) {
+		char port[8] = "";
+		sscanf(p, "%*[^=]=%7[^,]", port);
+		if (strcasestr(port, "dac")) config.phy.port = 0;
+	}	
+
+	free(nvs_item);
+	return &config;
+}	
+
 /****************************************************************************************
  * 
  */

+ 4 - 0
components/services/accessors.h

@@ -12,8 +12,11 @@
 #include "driver/i2c.h"
 #include "driver/i2s.h"
 #include "driver/spi_master.h"
+#include "gpio_exp.h"
+
 extern const char *i2c_name_type;
 extern const char *spi_name_type;
+
 typedef struct {
 	int width;
 	int height;
@@ -92,6 +95,7 @@ esp_err_t 					config_i2s_set(const i2s_platform_config_t * config, const char *
 esp_err_t 					config_spi_set(const spi_bus_config_t * config, int host, int dc);
 const i2c_config_t * 		config_i2c_get(int * i2c_port);
 const spi_bus_config_t * 	config_spi_get(spi_host_device_t * spi_host);
+const gpio_exp_config_t *   config_gpio_exp_get(void);
 void 						parse_set_GPIO(void (*cb)(int gpio, char *value));
 const i2s_platform_config_t * 	config_dac_get();
 const i2s_platform_config_t * 	config_spdif_get( );

+ 121 - 60
components/services/buttons.c

@@ -21,6 +21,7 @@
 #include "esp_task.h"
 #include "driver/gpio.h"
 #include "driver/rmt.h"
+#include "gpio_exp.h"
 #include "buttons.h"
 #include "rotary_encoder.h"
 #include "globdefs.h"
@@ -68,10 +69,12 @@ static EXT_RAM_ATTR struct {
 	infrared_handler handler;
 } infrared;
 
-static xQueueHandle button_evt_queue;
+static QueueHandle_t button_queue, button_exp_queue;
+static TimerHandle_t button_exp_timer;
 static QueueSetHandle_t common_queue_set;
 
 static void buttons_task(void* arg);
+static void buttons_handler(struct button_s *button, int level);
 
 /****************************************************************************************
  * Start task needed by button,s rotaty and infrared
@@ -87,7 +90,7 @@ static void common_task_init(void) {
  }	
 
 /****************************************************************************************
- * GPIO low-level handler
+ * GPIO low-level ISR handler
  */
 static void IRAM_ATTR gpio_isr_handler(void* arg)
 {
@@ -103,23 +106,37 @@ static void IRAM_ATTR gpio_isr_handler(void* arg)
 /****************************************************************************************
  * Buttons debounce/longpress timer
  */
-static void buttons_timer( TimerHandle_t xTimer ) {
+static void buttons_timer_handler( TimerHandle_t xTimer ) {
 	struct button_s *button = (struct button_s*) pvTimerGetTimerID (xTimer);
+	buttons_handler(button, gpio_get_level(button->gpio));
+}
 
-	button->level = gpio_get_level(button->gpio);
-	if (button->shifter && button->shifter->type == button->shifter->level) button->shifter->shifting = true;
+/****************************************************************************************
+ * GPIO expander low-level ISR handler
+ */
+static BaseType_t IRAM_ATTR gpio_exp_isr_handler(void* arg)
+{
+	BaseType_t woken = pdFALSE;
+	xTimerResetFromISR((TimerHandle_t) arg, &woken);
+	return woken;
+}
 
-	if (button->long_press && !button->long_timer && button->level == button->type) {
-		// detect a long press, so hold event generation
-		ESP_LOGD(TAG, "setting long timer gpio:%u level:%u", button->gpio, button->level);
-		xTimerChangePeriod(xTimer, button->long_press / portTICK_RATE_MS, 0);
-		button->long_timer = true;
-	} else {
-		// send a button pressed/released event (content is copied in queue)
-		ESP_LOGD(TAG, "sending event for gpio:%u level:%u", button->gpio, button->level);
-		// queue will have a copy of button's context
-		xQueueSend(button_evt_queue, button, 0);
-		button->long_timer = false;
+/****************************************************************************************
+ * Buttons expander debounce timer
+ */
+static void buttons_exp_timer_handler( TimerHandle_t xTimer ) {
+	struct gpio_exp_s *expander = (struct gpio_exp_s*) pvTimerGetTimerID (xTimer);
+	xQueueSend(button_exp_queue, &expander, 0);
+	ESP_LOGI(TAG, "Button expander base %u debounced", gpio_exp_base(expander));
+}
+
+/****************************************************************************************
+ * Buttons expander enumerator
+ */
+static void buttons_exp_enumerator(int gpio, int level, struct gpio_exp_s *expander) {
+	for (int i = 0; i < n_buttons; i++) if (buttons[i].gpio == gpio) {
+		buttons_handler(buttons + i, level);
+		return;
 	}
 }
 
@@ -134,11 +151,33 @@ static void buttons_polling( TimerHandle_t xTimer ) {
 	
 		if (level != polled_gpio[i].level) {
 			polled_gpio[i].level = level;
-			buttons_timer(polled_gpio[i].button->timer);
+			buttons_handler(polled_gpio[i].button, level);
 		}	
 	}	
 }
 
+/****************************************************************************************
+ * Buttons timer handler for press/longpress
+ */
+static void buttons_handler(struct button_s *button, int level) {
+	button->level = level;
+
+	if (button->shifter && button->shifter->type == button->shifter->level) button->shifter->shifting = true;
+
+	if (button->long_press && !button->long_timer && button->level == button->type) {
+		// detect a long press, so hold event generation
+		ESP_LOGD(TAG, "setting long timer gpio:%u level:%u", button->gpio, button->level);
+		xTimerChangePeriod(button->timer, button->long_press / portTICK_RATE_MS, 0);
+		button->long_timer = true;
+	} else {
+		// send a button pressed/released event (content is copied in queue)
+		ESP_LOGD(TAG, "sending event for gpio:%u level:%u", button->gpio, button->level);
+		// queue will have a copy of button's context
+		xQueueSend(button_queue, button, 0);
+		button->long_timer = false;
+	}
+}
+
 /****************************************************************************************
  * Tasks that calls the appropriate functions when buttons are pressed
  */
@@ -151,13 +190,13 @@ static void buttons_task(void* arg) {
 		// wait on button, rotary and infrared queues 
 		if ((xActivatedMember = xQueueSelectFromSet( common_queue_set, portMAX_DELAY )) == NULL) continue;
 		
-		if (xActivatedMember == button_evt_queue) {
+		if (xActivatedMember == button_queue) {
 			struct button_s button;
 			button_event_e event;
 			button_press_e press;
 			
 			// received a button event
-			xQueueReceive(button_evt_queue, &button, 0);
+			xQueueReceive(button_queue, &button, 0);
 
 			event = (button.level == button.type) ? BUTTON_PRESSED : BUTTON_RELEASED;		
 
@@ -176,21 +215,29 @@ static void buttons_task(void* arg) {
 				if (event == BUTTON_RELEASED) {
 					// early release of a long-press button, send press/release
 					if (!button.shifting) {
-						(*button.handler)(button.client, BUTTON_PRESSED, press, false);		
-						(*button.handler)(button.client, BUTTON_RELEASED, press, false);		
+						button.handler(button.client, BUTTON_PRESSED, press, false);		
+						button.handler(button.client, BUTTON_RELEASED, press, false);		
 					}
 					// button is a copy, so need to go to real context
 					button.self->shifting = false;
 				} else if (!button.shifting) {
 					// normal long press and not shifting so don't discard
-					(*button.handler)(button.client, BUTTON_PRESSED, press, true);
+					button.handler(button.client, BUTTON_PRESSED, press, true);
 				}  
 			} else {
 				// normal press/release of a button or release of a long-press button
-				if (!button.shifting) (*button.handler)(button.client, event, press, button.long_press);
+				if (!button.shifting) button.handler(button.client, event, press, button.long_press);
 				// button is a copy, so need to go to real context
 				button.self->shifting = false;
 			}
+		} else if (xActivatedMember == button_exp_queue) {
+			struct gpio_exp_s *expander;
+			/* 
+			we are not there yet, this is just a notice of a debounce, we need to enumerate 
+			GPIOs and let buttons_handler take care of longpress & al
+			*/
+			xQueueReceive(button_exp_queue, &expander, 0);
+			gpio_exp_enumerate(buttons_exp_enumerator, expander);
 		} else if (xActivatedMember == rotary.queue) {
 			rotary_encoder_event_t event = { 0 };
 			
@@ -225,9 +272,9 @@ void button_create(void *client, int gpio, int type, bool pull, int debounce, bu
 	ESP_LOGI(TAG, "Creating button using GPIO %u, type %u, pull-up/down %u, long press %u shifter %d", gpio, type, pull, long_press, shifter_gpio);
 
 	if (!n_buttons) {
-		button_evt_queue = xQueueCreate(BUTTON_QUEUE_LEN, sizeof(struct button_s));
+		button_queue = xQueueCreate(BUTTON_QUEUE_LEN, sizeof(struct button_s));
 		common_task_init();
-		xQueueAddToSet( button_evt_queue, common_queue_set );
+		xQueueAddToSet( button_queue, common_queue_set );
 	}
 	
 	// just in case this structure is allocated in a future release
@@ -241,7 +288,7 @@ void button_create(void *client, int gpio, int type, bool pull, int debounce, bu
 	buttons[n_buttons].long_press = long_press;
 	buttons[n_buttons].shifter_gpio = shifter_gpio;
 	buttons[n_buttons].type = type;
-	buttons[n_buttons].timer = xTimerCreate("buttonTimer", buttons[n_buttons].debounce / portTICK_RATE_MS, pdFALSE, (void *) &buttons[n_buttons], buttons_timer);
+	buttons[n_buttons].timer = xTimerCreate("buttonTimer", buttons[n_buttons].debounce / portTICK_RATE_MS, pdFALSE, (void *) &buttons[n_buttons], buttons_timer_handler);
 	buttons[n_buttons].self = buttons + n_buttons;
 
 	for (int i = 0; i < n_buttons; i++) {
@@ -258,45 +305,59 @@ void button_create(void *client, int gpio, int type, bool pull, int debounce, bu
 		}	
 	}
 
-	gpio_pad_select_gpio(gpio);
-	gpio_set_direction(gpio, GPIO_MODE_INPUT);
-
-	// we need any edge detection
-	gpio_set_intr_type(gpio, GPIO_INTR_ANYEDGE);
-
-	// do we need pullup or pulldown
-	if (pull) {
-		if (GPIO_IS_VALID_OUTPUT_GPIO(gpio)) {
-			if (type == BUTTON_LOW) gpio_set_pull_mode(gpio, GPIO_PULLUP_ONLY);
-			else gpio_set_pull_mode(gpio, GPIO_PULLDOWN_ONLY);
-		} else {	
-			ESP_LOGW(TAG, "cannot set pull up/down for gpio %u", gpio);
+	// creation is different is this is a native or an expanded GPIO
+	if (gpio < GPIO_EXP_BASE_MIN) {
+		gpio_pad_select_gpio(gpio);
+		gpio_set_direction(gpio, GPIO_MODE_INPUT);
+
+		// do we need pullup or pulldown
+		if (pull) {
+			if (GPIO_IS_VALID_OUTPUT_GPIO(gpio)) {
+				if (type == BUTTON_LOW) gpio_set_pull_mode(gpio, GPIO_PULLUP_ONLY);
+				else gpio_set_pull_mode(gpio, GPIO_PULLDOWN_ONLY);
+			} else {	
+				ESP_LOGW(TAG, "cannot set pull up/down for gpio %u", gpio);
+			}
 		}
-	}
 	
-	// and initialize level ...
-	buttons[n_buttons].level = gpio_get_level(gpio);
+		// and initialize level ...
+		buttons[n_buttons].level = gpio_get_level(gpio);
 	
-	// nasty ESP32 bug: fire-up constantly INT on GPIO 36/39 if ADC1, AMP, Hall used which WiFi does when PS is activated
-	for (int i = 0; polled_gpio[i].gpio != -1; i++) if (polled_gpio[i].gpio == gpio) {
-		if (!polled_timer) {
-			polled_timer = xTimerCreate("buttonsPolling", 100 / portTICK_RATE_MS, pdTRUE, polled_gpio, buttons_polling);		
-			xTimerStart(polled_timer, portMAX_DELAY);
-		}	
+		// nasty ESP32 bug: fire-up constantly INT on GPIO 36/39 if ADC1, AMP, Hall used which WiFi does when PS is activated
+		for (int i = 0; polled_gpio[i].gpio != -1; i++) if (polled_gpio[i].gpio == gpio) {
+			if (!polled_timer) {
+				polled_timer = xTimerCreate("buttonsPolling", 100 / portTICK_RATE_MS, pdTRUE, polled_gpio, buttons_polling);		
+				xTimerStart(polled_timer, portMAX_DELAY);
+			}	
 		
-		polled_gpio[i].button = buttons + n_buttons;					
-		polled_gpio[i].level = gpio_get_level(gpio);
-		ESP_LOGW(TAG, "creating polled gpio %u, level %u", gpio, polled_gpio[i].level);		
+			polled_gpio[i].button = buttons + n_buttons;					
+			polled_gpio[i].level = gpio_get_level(gpio);
+			ESP_LOGW(TAG, "creating polled gpio %u, level %u", gpio, polled_gpio[i].level);		
 		
-		gpio = -1;
-		break;
-	}
+			gpio = -1;
+			break;
+		}
 	
-	// only create timers and ISR is this is not a polled gpio
-	if (gpio != -1) {
-		gpio_isr_handler_add(gpio, gpio_isr_handler, (void*) &buttons[n_buttons]);
-		gpio_intr_enable(gpio);
-	}	
+		// only create ISR if this is not a polled gpio
+		if (gpio != -1) {
+			// we need any edge detection
+			gpio_set_intr_type(gpio, GPIO_INTR_ANYEDGE);
+			gpio_isr_handler_add(gpio, gpio_isr_handler, (void*) &buttons[n_buttons]);
+			gpio_intr_enable(gpio);
+		}	
+	} else {
+		// set GPIO as an ouptut and acquire value
+		struct gpio_exp_s *expander = gpio_exp_set_direction(gpio, GPIO_MODE_INPUT, NULL);
+		buttons[n_buttons].level = gpio_exp_get_level(gpio, 0, expander);
+
+		// create queue and timer for GPIO expander
+		if (!button_exp_queue && expander) {
+			button_exp_queue = xQueueCreate(BUTTON_QUEUE_LEN, sizeof(struct gpio_exp_s*));
+			button_exp_timer = xTimerCreate("button_expander", pdMS_TO_TICKS(DEBOUNCE), pdFALSE, expander, buttons_exp_timer_handler);		
+			xQueueAddToSet( button_exp_queue, common_queue_set );
+			gpio_exp_add_isr(gpio_exp_isr_handler, button_exp_timer, expander);
+		}	
+	}
 
 	n_buttons++;
 }	
@@ -363,7 +424,7 @@ void *button_remap(void *client, int gpio, button_handler handler, int long_pres
 }
 
 /****************************************************************************************
- * Create rotary encoder
+ * Rotary encoder handler
  */
 static void rotary_button_handler(void *id, button_event_e event, button_press_e mode, bool long_press) {
 	ESP_LOGI(TAG, "Rotary push-button %d", event);

+ 347 - 0
components/services/gpio_exp.c

@@ -0,0 +1,347 @@
+/* GDS Example
+
+   This example code is in the Public Domain (or CC0 licensed, at your option.)
+
+   Unless required by applicable law or agreed to in writing, this
+   software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
+   CONDITIONS OF ANY KIND, either express or implied.
+*/
+#include <string.h>
+#include <stdlib.h>
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "esp_log.h"
+#include "driver/gpio.h"
+#include "driver/i2c.h"
+#include "gpio_exp.h"
+
+static const char TAG[] = "gpio expander";
+
+static void   IRAM_ATTR intr_isr_handler(void* arg);
+static struct gpio_exp_s* find_expander(struct gpio_exp_s *expander, int *gpio);
+
+static void   pca9535_set_direction(union gpio_exp_phy_u *phy, uint32_t r_mask, uint32_t w_mask);
+static int    pca9535_read(union gpio_exp_phy_u *phy);
+
+static esp_err_t i2c_write_byte(uint8_t i2c_port, uint8_t i2c_addr, uint8_t reg, uint8_t val);
+static uint8_t   i2c_read_byte(uint8_t i2c_port, uint8_t i2c_addr, uint8_t reg);
+static uint16_t  i2c_read_word(uint8_t i2c_port, uint8_t i2c_addr, uint8_t reg);
+static esp_err_t i2c_write_word(uint8_t i2c_port, uint8_t i2c_addr, uint8_t reg, uint16_t data);
+
+static const struct gpio_exp_model_s {
+	char *model;
+	gpio_int_type_t trigger;
+	void (*set_direction)(union gpio_exp_phy_u *phy, uint32_t r_mask, uint32_t mask);
+	int (*read)(union gpio_exp_phy_u *phy);
+} registered[] = {
+	{ .model = "pca9535",
+	  .trigger = GPIO_INTR_NEGEDGE, 
+	  .set_direction = pca9535_set_direction,
+	  .read = pca9535_read, }
+};
+
+static uint8_t n_expanders;
+
+static EXT_RAM_ATTR struct gpio_exp_s {
+	uint32_t first, last;
+	union gpio_exp_phy_u phy;
+	uint32_t shadow;
+	TickType_t age;
+	uint32_t r_mask, w_mask;
+	struct {
+		gpio_exp_isr handler;
+		void *arg;
+	} isr[4];
+	struct gpio_exp_model_s const *model;
+} expanders[4];
+
+/******************************************************************************
+ * Retrieve base from an expander reference
+ */
+uint32_t gpio_exp_base(struct gpio_exp_s *expander) { 
+	return expander->first; 
+}
+
+/******************************************************************************
+ * Retrieve reference from a GPIO
+ */
+struct gpio_exp_s *gpio_exp_expander(int gpio) { 
+	int _gpio = gpio;
+	return find_expander(NULL, &_gpio);
+}
+
+/******************************************************************************
+ * Create an I2C expander
+ */
+struct gpio_exp_s* gpio_exp_create(const gpio_exp_config_t *config) {
+	struct gpio_exp_s *expander = expanders + n_expanders;
+	
+	if (config->base < GPIO_EXP_BASE_MIN || n_expanders == sizeof(expanders)/sizeof(struct gpio_exp_s)) {
+		ESP_LOGE(TAG, "Base %d GPIO must be > %d for %s or too many expanders %d", config->base, GPIO_EXP_BASE_MIN, config->model, n_expanders);
+		return NULL;
+	}
+
+	// See if we know that model (expanders is zero-initialized)
+	for (int i = 0; !expander->model && i < sizeof(registered)/sizeof(struct gpio_exp_model_s); i++) {
+		if (strcasestr(config->model, registered[i].model)) expander->model = registered + i;
+    }
+
+	// well... try again
+	if (!expander->model) {
+		ESP_LOGE(TAG,"Unknown GPIO expansion chip %s", config->model);
+		return NULL;
+	}
+		
+	n_expanders++;
+	expander->first = config->base;
+	expander->last = config->base + config->count - 1;
+	memcpy(&expander->phy, &config->phy, sizeof(union gpio_exp_phy_u));
+
+	// set interrupt if possible
+	if (config->intr > 0) {
+		gpio_pad_select_gpio(config->intr);
+		gpio_set_direction(config->intr, GPIO_MODE_INPUT);
+
+		switch (expander->model->trigger) {
+		case GPIO_INTR_NEGEDGE:
+		case GPIO_INTR_LOW_LEVEL:
+			gpio_set_pull_mode(config->intr, GPIO_PULLUP_ONLY);
+			break;
+		case GPIO_INTR_POSEDGE:
+		case GPIO_INTR_HIGH_LEVEL:
+			gpio_set_pull_mode(config->intr, GPIO_PULLDOWN_ONLY);
+			break;
+		default:	
+			gpio_set_pull_mode(config->intr, GPIO_PULLUP_PULLDOWN);
+			break;
+		}	
+		
+		gpio_set_intr_type(config->intr, expander->model->trigger);		
+		gpio_isr_handler_add(config->intr, intr_isr_handler, expander);
+		gpio_intr_enable(config->intr);						
+	}
+	
+	ESP_LOGI(TAG, "Create GPIO expander at base %u with INT %u at @%x", config->base, config->intr, config->phy.addr);
+	return expander;
+}
+
+/******************************************************************************
+ * Add ISR handler
+ */
+bool gpio_exp_add_isr(gpio_exp_isr isr, void *arg, struct gpio_exp_s *expander) {
+	for (int i = 0; i < sizeof(expander->isr)/sizeof(*expander->isr); i++) {
+		if (!expander->isr[i].handler) {
+			expander->isr[i].handler = isr;
+			expander->isr[i].arg = arg;
+			ESP_LOGI(TAG, "Added new ISR for expander base %d", expander->first);
+			return true;
+		}
+	}
+
+	ESP_LOGE(TAG, "No room left to add new ISR");
+	return false;
+}
+
+/******************************************************************************
+ * Set GPIO direction
+ */
+struct gpio_exp_s* gpio_exp_set_direction(int gpio, gpio_mode_t mode, struct gpio_exp_s *expander) {
+	if ((expander = find_expander(expander, &gpio)) == NULL) return NULL;
+
+	if (mode == GPIO_MODE_INPUT) {
+		expander->r_mask |= 1 << gpio;
+		expander->age = ~xTaskGetTickCount();
+	} else {
+		expander->w_mask |= 1 << gpio;
+	}
+	
+	if (expander->r_mask & expander->w_mask) {
+		ESP_LOGE(TAG, "GPIO %d on expander base %u can't be r/w", gpio, expander->first);
+		return false;
+	}
+	
+	// most expanders want unconfigured GPIO to be set to output
+	if (expander->model->set_direction) expander->model->set_direction(&expander->phy, expander->r_mask, expander->w_mask);
+	
+	return expander;
+}	
+
+/******************************************************************************
+ * Get GPIO level with cache
+ */
+int gpio_exp_get_level(int gpio, uint32_t age, struct gpio_exp_s *expander) {
+	if ((expander = find_expander(expander, &gpio)) == NULL) return false;
+	
+	uint32_t now = xTaskGetTickCount();
+	
+	if (now - expander->age >= pdMS_TO_TICKS(age)) {
+		expander->shadow = expander->model->read(&expander->phy);
+		expander->age = now;
+	}
+	
+	return expander->shadow & (1 << gpio) ? 1 : 0;
+}
+
+/******************************************************************************
+ * Enumerate modified GPIO
+ */
+void gpio_exp_enumerate(gpio_exp_enumerator enumerator, struct gpio_exp_s *expander) {
+	uint32_t value = expander->model->read(&expander->phy) ^ expander->shadow;
+	uint8_t clz;
+
+	// memorize newly read value and just update if requested
+	expander->shadow ^= value;
+	if (!enumerator) return;
+	
+	// now we have a bitmap of all modified GPIO since last call
+	for (int gpio = 0; value; value <<= (clz + 1)) {
+		clz = __builtin_clz(value);
+		gpio += clz;
+		enumerator(expander->first + 31 - gpio, (expander->shadow >> (31 - gpio)) & 0x01, expander);
+	}	
+}
+
+/****************************************************************************************
+ * Find the expander related to base
+ */
+static struct gpio_exp_s* find_expander(struct gpio_exp_s *expander, int *gpio) {
+	for (int i = 0; !expander && i < n_expanders; i++) {
+		if (*gpio >= expanders[i].first && *gpio <= expanders[i].last) expander = expanders + i;
+	}
+	
+	// normalize GPIO number
+	if (expander && *gpio >= expanders->first) *gpio -= expanders->first;
+	
+	return expander;
+}
+
+/****************************************************************************************
+ * Configure unused GPIO to output
+ */
+static void pca9535_set_direction(union gpio_exp_phy_u *phy, uint32_t r_mask, uint32_t w_mask) {
+	esp_err_t err = i2c_write_word(phy->port, phy->addr, 0x06, r_mask);
+	ESP_LOGD(TAG, "PCA9535 set direction %x %d", r_mask, err);
+}
+
+/****************************************************************************************
+ * Configure unused GPIO to output
+ */
+static int pca9535_read(union gpio_exp_phy_u *phy) {
+	ESP_LOGD(TAG, "PCA9535 read @%d", phy->addr);
+	return i2c_read_word(phy->port, phy->addr, 0x0);
+}
+
+/****************************************************************************************
+ * INTR low-level handler
+ */
+static void IRAM_ATTR intr_isr_handler(void* arg)
+{
+	struct gpio_exp_s *expander = (struct gpio_exp_s*) arg;
+	BaseType_t woken = pdFALSE;
+	
+	for (int i = 0; i < sizeof(expander->isr)/sizeof(*expander->isr); i++) {
+		if (expander->isr[i].handler) woken |= expander->isr[i].handler(expander->isr[i].arg);
+	}
+
+	if (woken) portYIELD_FROM_ISR();
+
+	ESP_EARLY_LOGD(TAG, "INTR for expander %u", expander->first);
+}
+
+/****************************************************************************************
+ * 
+ */
+static esp_err_t i2c_write_byte(uint8_t i2c_port, uint8_t i2c_addr, uint8_t reg, uint8_t val) {
+    i2c_cmd_handle_t cmd = i2c_cmd_link_create();
+    i2c_master_start(cmd);
+	
+	i2c_master_write_byte(cmd, (i2c_addr << 1) | I2C_MASTER_WRITE, I2C_MASTER_NACK);
+	i2c_master_write_byte(cmd, reg, I2C_MASTER_NACK);
+	i2c_master_write_byte(cmd, val, I2C_MASTER_NACK);
+	
+	i2c_master_stop(cmd);
+    esp_err_t ret = i2c_master_cmd_begin(i2c_port, cmd, 100 / portTICK_RATE_MS);
+    i2c_cmd_link_delete(cmd);
+	
+	if (ret != ESP_OK) {
+		ESP_LOGW(TAG, "I2C write failed");
+	}
+	
+    return ret;
+}
+
+/****************************************************************************************
+ * I2C read one byte
+ */
+static uint8_t i2c_read_byte(uint8_t i2c_port, uint8_t i2c_addr, uint8_t reg) {
+	uint8_t data = 0xff;
+	
+	i2c_cmd_handle_t cmd = i2c_cmd_link_create();
+    i2c_master_start(cmd);
+    
+	i2c_master_write_byte(cmd, (i2c_addr << 1) | I2C_MASTER_WRITE, I2C_MASTER_NACK);
+	i2c_master_write_byte(cmd, reg, I2C_MASTER_NACK);
+
+	i2c_master_start(cmd);			
+	i2c_master_write_byte(cmd, (i2c_addr << 1) | I2C_MASTER_READ, I2C_MASTER_NACK);
+	i2c_master_read_byte(cmd, &data, I2C_MASTER_NACK);
+	
+    i2c_master_stop(cmd);
+	esp_err_t ret = i2c_master_cmd_begin(i2c_port, cmd, 100 / portTICK_RATE_MS);
+	i2c_cmd_link_delete(cmd);
+	
+	if (ret != ESP_OK) {
+		ESP_LOGW(TAG, "I2C read failed");
+	}
+	
+	return data;
+}
+
+/****************************************************************************************
+ * I2C read 16 bits word
+ */
+static uint16_t i2c_read_word(uint8_t i2c_port, uint8_t i2c_addr, uint8_t reg) {
+	uint16_t data = 0xffff;
+	
+	i2c_cmd_handle_t cmd = i2c_cmd_link_create();
+    i2c_master_start(cmd);
+	
+    i2c_master_write_byte(cmd, (i2c_addr << 1) | I2C_MASTER_WRITE, I2C_MASTER_NACK);
+    i2c_master_write_byte(cmd, reg, I2C_MASTER_NACK);
+	
+    i2c_master_start(cmd);
+    i2c_master_write_byte(cmd, (i2c_addr << 1) | I2C_MASTER_READ, I2C_MASTER_NACK);
+    i2c_master_read(cmd, (uint8_t*) &data, 2, I2C_MASTER_NACK);
+	
+    i2c_master_stop(cmd);
+    esp_err_t ret = i2c_master_cmd_begin(i2c_port, cmd, 100 / portTICK_RATE_MS);
+    i2c_cmd_link_delete(cmd);
+	
+	if (ret != ESP_OK) {
+		ESP_LOGW(TAG, "I2C read failed");
+	}
+
+	return data;
+}
+
+/****************************************************************************************
+ * I2C write 16 bits word
+ */
+static esp_err_t i2c_write_word(uint8_t i2c_port, uint8_t i2c_addr, uint8_t reg, uint16_t data) {
+	i2c_cmd_handle_t cmd = i2c_cmd_link_create();
+    i2c_master_start(cmd);
+	
+	i2c_master_write_byte(cmd, (i2c_addr << 1) | I2C_MASTER_WRITE, I2C_MASTER_NACK);
+	i2c_master_write_byte(cmd, reg, I2C_MASTER_NACK);
+	i2c_master_write(cmd, (uint8_t*) &data, 2, I2C_MASTER_NACK);
+    
+	i2c_master_stop(cmd);
+	esp_err_t ret = i2c_master_cmd_begin(i2c_port, cmd, 100 / portTICK_RATE_MS);
+    i2c_cmd_link_delete(cmd);
+	
+	if (ret != ESP_OK) {
+		ESP_LOGW(TAG, "I2C write failed");
+	}
+	
+    return ret;
+}

+ 49 - 0
components/services/gpio_exp.h

@@ -0,0 +1,49 @@
+/* GDS Example
+
+   This example code is in the Public Domain (or CC0 licensed, at your option.)
+
+   Unless required by applicable law or agreed to in writing, this
+   software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
+   CONDITIONS OF ANY KIND, either express or implied.
+*/
+#pragma once
+
+#include <stdint.h>
+#include "freertos/FreeRTOS.h"
+#include "driver/gpio.h"
+
+#define GPIO_EXP_BASE_MIN	100
+
+struct gpio_exp_s;
+
+typedef struct {
+	char model[32];
+	uint8_t intr;
+	uint8_t count;
+	uint32_t base;
+	union gpio_exp_phy_u {
+		struct {
+			uint8_t addr, port;
+		};
+		struct {
+			uint8_t cs_pin;
+		};
+	} phy;	
+} gpio_exp_config_t;
+
+typedef void 	   (*gpio_exp_enumerator)(int gpio, int level, struct gpio_exp_s *expander);
+typedef BaseType_t (*gpio_exp_isr)(void *arg);
+
+// set <intr> to -1 and <queue> to NULL if there is no interrupt
+struct gpio_exp_s* gpio_exp_create(const gpio_exp_config_t *config);
+bool               gpio_exp_add_isr(gpio_exp_isr isr, void *arg, struct gpio_exp_s *expander);
+uint32_t           gpio_exp_base(struct gpio_exp_s *expander);
+struct gpio_exp_s* gpio_exp_expander(int gpio);
+
+/* For all functions below when <expander> is provided, GPIO's can be numbered from 0. If <expander>
+   is NULL, then GPIO must start from base */
+struct gpio_exp_s* gpio_exp_set_direction(int gpio, gpio_mode_t mode, struct gpio_exp_s *expander);
+int                gpio_exp_get_level(int gpio, uint32_t age, struct gpio_exp_s *expander);
+/* This can be called to enumerate modified GPIO since last read. Note that <enumerator>
+   can be NULL to initialize all GPIOs */
+void               gpio_exp_enumerate(gpio_exp_enumerator enumerator, struct gpio_exp_s *expander);

+ 4 - 0
components/services/services.c

@@ -100,6 +100,10 @@ void services_init(void) {
 		ESP_LOGW(TAG, "no SPI configured");
 	}	
 
+	// create GPIO expander
+	const gpio_exp_config_t * gpio_exp_config = config_gpio_exp_get();
+	if (gpio_exp_config) gpio_exp_create(gpio_exp_config);
+
 	// system-wide PWM timer configuration
 	ledc_timer_config_t pwm_timer = {
 		.duty_resolution = LEDC_TIMER_13_BIT, 

+ 8 - 0
main/Kconfig.projbuild

@@ -88,6 +88,9 @@ menu "Squeezelite-ESP32"
 			string
 			default "bck=33,ws=25,do=15" if SQUEEZEAMP
 			default	""
+		config GPIO_EXP_CONFIG		
+			string
+			default	""
 		config SPI_CONFIG
 			string
 			default "dc=27,data=19,clk=18" if TWATCH2020		
@@ -333,6 +336,11 @@ menu "Squeezelite-ESP32"
 			help
 				Set GPIO for rotary encoder (quadrature phase). See README on SqueezeESP32 project's GitHub for more details
 				A=<gpio>,B=<gpio>[,SW=gpio>[[,knobonly[=<ms>]|[,volume][,longpress]]			
+		config GPIO_EXP_CONFIG
+			string "GPIO expander configuration"
+			help
+				Set parameters of GPIO extender
+				model=<model>[,addr=<addr>][,base=<100..N>][,count=<0..32>][,intr=<gpio>][,port=dac|system]				
 	endmenu
 	menu "LED configuration"
 		visible if !SQUEEZEAMP && !TWATCH2020

+ 4 - 1
main/esp_app_main.c

@@ -390,12 +390,15 @@ void register_default_nvs(){
 	
 	ESP_LOGD(TAG,"Registering default value for key %s", "dac_config");
 	config_set_default(NVS_TYPE_STR, "dac_config", "", 0);
-	//todo: add dac_config for known targets
+	
 	ESP_LOGD(TAG,"Registering default value for key %s", "dac_controlset");
 	config_set_default(NVS_TYPE_STR, "dac_controlset", "", 0);
 	
 	ESP_LOGD(TAG,"Registering default value for key %s", "jack_mutes_amp");
 	config_set_default(NVS_TYPE_STR, "jack_mutes_amp", "n", 0);
+
+	ESP_LOGD(TAG,"Registering default value for key %s", "gpio_exp_config");
+	config_set_default(NVS_TYPE_STR, "gpio_exp_config", CONFIG_GPIO_EXP_CONFIG, 0);
 	
 	ESP_LOGD(TAG,"Registering default value for key %s", "bat_config");
 	config_set_default(NVS_TYPE_STR, "bat_config", "", 0);