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_I2C_CONFIG=""
 CONFIG_SPI_CONFIG=""
 CONFIG_SPI_CONFIG=""
 CONFIG_SET_GPIO=""
 CONFIG_SET_GPIO=""
+CONFIG_GPIO_EXP_CONFIG=""
 CONFIG_ROTARY_ENCODER=""
 CONFIG_ROTARY_ENCODER=""
 CONFIG_LED_GREEN_GPIO=-1
 CONFIG_LED_GREEN_GPIO=-1
 CONFIG_LED_GREEN_GPIO_LEVEL=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_I2C_CONFIG=""
 CONFIG_SPI_CONFIG=""
 CONFIG_SPI_CONFIG=""
 CONFIG_SET_GPIO=""
 CONFIG_SET_GPIO=""
+CONFIG_GPIO_EXP_CONFIG=""
 CONFIG_ROTARY_ENCODER=""
 CONFIG_ROTARY_ENCODER=""
 CONFIG_LED_GREEN_GPIO=-1
 CONFIG_LED_GREEN_GPIO=-1
 CONFIG_LED_GREEN_GPIO_LEVEL=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_I2C_CONFIG=""
 CONFIG_SPI_CONFIG=""
 CONFIG_SPI_CONFIG=""
 CONFIG_SET_GPIO=""
 CONFIG_SET_GPIO=""
+CONFIG_GPIO_EXP_CONFIG=""
 CONFIG_ROTARY_ENCODER=""
 CONFIG_ROTARY_ENCODER=""
 CONFIG_LED_GREEN_GPIO=12
 CONFIG_LED_GREEN_GPIO=12
 CONFIG_LED_GREEN_GPIO_LEVEL=0
 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;
 	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/i2c.h"
 #include "driver/i2s.h"
 #include "driver/i2s.h"
 #include "driver/spi_master.h"
 #include "driver/spi_master.h"
+#include "gpio_exp.h"
+
 extern const char *i2c_name_type;
 extern const char *i2c_name_type;
 extern const char *spi_name_type;
 extern const char *spi_name_type;
+
 typedef struct {
 typedef struct {
 	int width;
 	int width;
 	int height;
 	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);
 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 i2c_config_t * 		config_i2c_get(int * i2c_port);
 const spi_bus_config_t * 	config_spi_get(spi_host_device_t * spi_host);
 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));
 void 						parse_set_GPIO(void (*cb)(int gpio, char *value));
 const i2s_platform_config_t * 	config_dac_get();
 const i2s_platform_config_t * 	config_dac_get();
 const i2s_platform_config_t * 	config_spdif_get( );
 const i2s_platform_config_t * 	config_spdif_get( );

+ 121 - 60
components/services/buttons.c

@@ -21,6 +21,7 @@
 #include "esp_task.h"
 #include "esp_task.h"
 #include "driver/gpio.h"
 #include "driver/gpio.h"
 #include "driver/rmt.h"
 #include "driver/rmt.h"
+#include "gpio_exp.h"
 #include "buttons.h"
 #include "buttons.h"
 #include "rotary_encoder.h"
 #include "rotary_encoder.h"
 #include "globdefs.h"
 #include "globdefs.h"
@@ -68,10 +69,12 @@ static EXT_RAM_ATTR struct {
 	infrared_handler handler;
 	infrared_handler handler;
 } infrared;
 } 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 QueueSetHandle_t common_queue_set;
 
 
 static void buttons_task(void* arg);
 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
  * 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)
 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
  * 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);
 	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) {
 		if (level != polled_gpio[i].level) {
 			polled_gpio[i].level = 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
  * 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 
 		// wait on button, rotary and infrared queues 
 		if ((xActivatedMember = xQueueSelectFromSet( common_queue_set, portMAX_DELAY )) == NULL) continue;
 		if ((xActivatedMember = xQueueSelectFromSet( common_queue_set, portMAX_DELAY )) == NULL) continue;
 		
 		
-		if (xActivatedMember == button_evt_queue) {
+		if (xActivatedMember == button_queue) {
 			struct button_s button;
 			struct button_s button;
 			button_event_e event;
 			button_event_e event;
 			button_press_e press;
 			button_press_e press;
 			
 			
 			// received a button event
 			// received a button event
-			xQueueReceive(button_evt_queue, &button, 0);
+			xQueueReceive(button_queue, &button, 0);
 
 
 			event = (button.level == button.type) ? BUTTON_PRESSED : BUTTON_RELEASED;		
 			event = (button.level == button.type) ? BUTTON_PRESSED : BUTTON_RELEASED;		
 
 
@@ -176,21 +215,29 @@ static void buttons_task(void* arg) {
 				if (event == BUTTON_RELEASED) {
 				if (event == BUTTON_RELEASED) {
 					// early release of a long-press button, send press/release
 					// early release of a long-press button, send press/release
 					if (!button.shifting) {
 					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 is a copy, so need to go to real context
 					button.self->shifting = false;
 					button.self->shifting = false;
 				} else if (!button.shifting) {
 				} else if (!button.shifting) {
 					// normal long press and not shifting so don't discard
 					// 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 {
 			} else {
 				// normal press/release of a button or release of a long-press button
 				// 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 is a copy, so need to go to real context
 				button.self->shifting = false;
 				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) {
 		} else if (xActivatedMember == rotary.queue) {
 			rotary_encoder_event_t event = { 0 };
 			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);
 	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) {
 	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();
 		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
 	// 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].long_press = long_press;
 	buttons[n_buttons].shifter_gpio = shifter_gpio;
 	buttons[n_buttons].shifter_gpio = shifter_gpio;
 	buttons[n_buttons].type = type;
 	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;
 	buttons[n_buttons].self = buttons + n_buttons;
 
 
 	for (int i = 0; i < n_buttons; i++) {
 	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++;
 	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) {
 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);
 	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");
 		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
 	// system-wide PWM timer configuration
 	ledc_timer_config_t pwm_timer = {
 	ledc_timer_config_t pwm_timer = {
 		.duty_resolution = LEDC_TIMER_13_BIT, 
 		.duty_resolution = LEDC_TIMER_13_BIT, 

+ 8 - 0
main/Kconfig.projbuild

@@ -88,6 +88,9 @@ menu "Squeezelite-ESP32"
 			string
 			string
 			default "bck=33,ws=25,do=15" if SQUEEZEAMP
 			default "bck=33,ws=25,do=15" if SQUEEZEAMP
 			default	""
 			default	""
+		config GPIO_EXP_CONFIG		
+			string
+			default	""
 		config SPI_CONFIG
 		config SPI_CONFIG
 			string
 			string
 			default "dc=27,data=19,clk=18" if TWATCH2020		
 			default "dc=27,data=19,clk=18" if TWATCH2020		
@@ -333,6 +336,11 @@ menu "Squeezelite-ESP32"
 			help
 			help
 				Set GPIO for rotary encoder (quadrature phase). See README on SqueezeESP32 project's GitHub for more details
 				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]]			
 				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
 	endmenu
 	menu "LED configuration"
 	menu "LED configuration"
 		visible if !SQUEEZEAMP && !TWATCH2020
 		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");
 	ESP_LOGD(TAG,"Registering default value for key %s", "dac_config");
 	config_set_default(NVS_TYPE_STR, "dac_config", "", 0);
 	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");
 	ESP_LOGD(TAG,"Registering default value for key %s", "dac_controlset");
 	config_set_default(NVS_TYPE_STR, "dac_controlset", "", 0);
 	config_set_default(NVS_TYPE_STR, "dac_controlset", "", 0);
 	
 	
 	ESP_LOGD(TAG,"Registering default value for key %s", "jack_mutes_amp");
 	ESP_LOGD(TAG,"Registering default value for key %s", "jack_mutes_amp");
 	config_set_default(NVS_TYPE_STR, "jack_mutes_amp", "n", 0);
 	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");
 	ESP_LOGD(TAG,"Registering default value for key %s", "bat_config");
 	config_set_default(NVS_TYPE_STR, "bat_config", "", 0);
 	config_set_default(NVS_TYPE_STR, "bat_config", "", 0);