Skip to content

Commit 06eda15

Browse files
authored
Merge pull request #703 from songruo/feature/esp_io_expander_gpio_wrapper
feat(io_expander): add GPIO API wrapper for ESP IO Expander (BSP-764)
2 parents ad668c7 + 4890915 commit 06eda15

File tree

6 files changed

+391
-11
lines changed

6 files changed

+391
-11
lines changed
Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,21 @@
1-
idf_component_register(SRCS "esp_io_expander.c" INCLUDE_DIRS "include")
1+
if("${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}" VERSION_GREATER_EQUAL "5.3")
2+
set(PRIV_REQ "esp_driver_gpio")
3+
else()
4+
set(PRIV_REQ "driver")
5+
endif()
6+
7+
idf_component_register(
8+
SRCS "esp_io_expander.c"
9+
INCLUDE_DIRS "include"
10+
PRIV_REQUIRES ${PRIV_REQ}
11+
)
12+
13+
if(CONFIG_IO_EXPANDER_ENABLE_GPIO_API_WRAPPER)
14+
target_sources(${COMPONENT_LIB} PRIVATE "esp_io_expander_gpio_wrapper.c")
15+
16+
target_link_libraries(${COMPONENT_LIB} INTERFACE "-Wl,--wrap=gpio_set_level"
17+
"-Wl,--wrap=gpio_get_level"
18+
"-Wl,--wrap=gpio_set_direction"
19+
"-Wl,--wrap=gpio_set_pull_mode"
20+
)
21+
endif()
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
menu "ESP IO Expander"
2+
3+
config IO_EXPANDER_ENABLE_GPIO_API_WRAPPER
4+
bool "Enable GPIO API wrapper"
5+
default n
6+
help
7+
If enabled, ESP-IDF GPIO APIs can be used to control the IOs of IO expander.
8+
9+
endmenu

components/io_expander/esp_io_expander/README.md

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,62 @@
22

33
[![Component Registry](https://components.espressif.com/components/espressif/esp_io_expander/badge.svg)](https://components.espressif.com/components/espressif/esp_io_expander)
44

5-
This componnent is main esp_io_expander component which defines main functions and types for easy adding specific io expander chip component.
5+
This component is main esp_io_expander component which defines main functions and types for easy adding specific IO expander chip component.
66

77
## Supported features
88

99
### From v1.0
1010

11-
- [x] Set an IO's direction
12-
- [x] Get an IO's direction
13-
- [x] Set an IO's output level
14-
- [x] Get an IO's input level
15-
- [x] Show all IOs' status
11+
- Set an IO's direction
12+
- Get an IO's direction
13+
- Set an IO's output level
14+
- Get an IO's input level
15+
- Show all IOs' status
1616

1717
### From v1.1
1818

19-
- [x] Set an IO's output mode (Push Pull / Open Drain)
20-
- [x] Set an IO's Pull-up / Pull-down state
19+
- Set an IO's output mode (Push Pull / Open Drain)
20+
- Set an IO's Pull-up / Pull-down state
21+
22+
### From v1.2
23+
24+
- GPIO API wrapper to control IO expander pins via ESP-IDF `gpio_*` APIs
25+
26+
## GPIO API wrapper
27+
28+
When enabled, IO expander pins can be used like regular GPIOs through ESP-IDF's `gpio_*` APIs. This allows existing GPIO-based code to work with IO expanders with minimal changes.
29+
30+
- Enable in `menuconfig`: `IO_EXPANDER_ENABLE_GPIO_API_WRAPPER` (under `ESP IO Expander`).
31+
- Append a handler with a virtual GPIO start index using `esp_io_expander_gpio_wrapper_append_handler(handler, start_io_num)`. The `start_io_num` must be greater than or equal to `GPIO_NUM_MAX`. The handler's `io_count` pins will be mapped to the range `[start_io_num, start_io_num + io_count)`.
32+
- Remove a previously appended mapping with `esp_io_expander_gpio_wrapper_remove_handler(handler)`.
33+
34+
Supported wrapped APIs:
35+
- `gpio_set_level`
36+
- `gpio_get_level`
37+
- `gpio_set_direction` (supports `GPIO_MODE_INPUT`, `GPIO_MODE_OUTPUT`, `GPIO_MODE_OUTPUT_OD` if the chip supports high-Z write)
38+
- `gpio_set_pull_mode` (supports `GPIO_PULLUP_ONLY`, `GPIO_PULLDOWN_ONLY` if the chip supports pulldown select, and `GPIO_FLOATING`)
39+
40+
Example:
41+
42+
```c
43+
// 1) Enable IO_EXPANDER_ENABLE_GPIO_API_WRAPPER in menuconfig
44+
// 2) Create the IO expander handler via esp_io_expander_new_i2c_xxx(..., &ioexp)
45+
esp_io_expander_handle_t ioexp;
46+
ESP_ERROR_CHECK(esp_io_expander_new_i2c_xxx(/* i2c_port, i2c_addr, optional cfg */, &ioexp));
47+
// 3) Initialize your IO expander and obtain `esp_io_expander_handle_t ioexp`
48+
ESP_ERROR_CHECK(esp_io_expander_gpio_wrapper_append_handler(ioexp, GPIO_NUM_MAX));
49+
50+
gpio_num_t vgpio0 = (gpio_num_t)(GPIO_NUM_MAX + 0);
51+
gpio_set_direction(vgpio0, GPIO_MODE_OUTPUT);
52+
gpio_set_level(vgpio0, 1);
53+
54+
// When no longer needed:
55+
ESP_ERROR_CHECK(esp_io_expander_gpio_wrapper_remove_handler(ioexp));
56+
```
2157
2258
### Future
2359
24-
- [ ] Interrupt mode
60+
- Interrupt mode
2561
2662
2763
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
#include <stdlib.h>
8+
#include <stdbool.h>
9+
#include "esp_io_expander_gpio_wrapper.h"
10+
#include "freertos/FreeRTOS.h"
11+
#include "freertos/portmacro.h"
12+
#include "esp_heap_caps.h"
13+
#include "esp_err.h"
14+
#include "esp_log.h"
15+
#include "driver/gpio.h"
16+
#include "soc/gpio_num.h"
17+
18+
typedef struct ioexp_range_node {
19+
uint32_t start_num;
20+
uint32_t count;
21+
esp_io_expander_handle_t handler;
22+
struct ioexp_range_node *next;
23+
} ioexp_range_node_t;
24+
25+
static ioexp_range_node_t s_embedded_range_head = {
26+
.start_num = 0,
27+
.count = GPIO_NUM_MAX,
28+
.handler = NULL,
29+
.next = NULL,
30+
};
31+
static ioexp_range_node_t *s_ioexp_ranges = &s_embedded_range_head;
32+
static portMUX_TYPE s_ioexp_lock = portMUX_INITIALIZER_UNLOCKED;
33+
34+
static char *TAG = "io_expander_wrapper";
35+
36+
esp_err_t __real_gpio_set_level(gpio_num_t gpio_num, uint32_t level);
37+
int __real_gpio_get_level(gpio_num_t gpio_num);
38+
esp_err_t __real_gpio_set_direction(gpio_num_t gpio_num, gpio_mode_t mode);
39+
esp_err_t __real_gpio_set_pull_mode(gpio_num_t gpio_num, gpio_pull_mode_t pull);
40+
41+
static bool find_ioexp_for_num(uint32_t gpio_num, esp_io_expander_handle_t *out_handler, uint32_t *out_pin_mask)
42+
{
43+
bool found = false;
44+
portENTER_CRITICAL(&s_ioexp_lock);
45+
ioexp_range_node_t *node = s_ioexp_ranges;
46+
while (node) {
47+
uint32_t end = node->start_num + node->count;
48+
if (gpio_num >= node->start_num && gpio_num < end) {
49+
uint32_t bit_index = gpio_num - node->start_num;
50+
if (out_handler) {
51+
*out_handler = node->handler;
52+
}
53+
if (out_pin_mask) {
54+
*out_pin_mask = (1U << bit_index);
55+
}
56+
found = true;
57+
break;
58+
}
59+
node = node->next;
60+
}
61+
portEXIT_CRITICAL(&s_ioexp_lock);
62+
return found;
63+
}
64+
65+
esp_err_t esp_io_expander_gpio_wrapper_append_handler(esp_io_expander_handle_t handler, uint32_t start_io_num)
66+
{
67+
if (handler == NULL || start_io_num < GPIO_NUM_MAX) {
68+
return ESP_ERR_INVALID_ARG;
69+
}
70+
uint32_t cnt = handler->config.io_count;
71+
if (cnt == 0 || cnt > IO_COUNT_MAX) {
72+
return ESP_ERR_INVALID_ARG;
73+
}
74+
// [start, end)
75+
uint32_t start = start_io_num;
76+
uint32_t end = start_io_num + cnt;
77+
// Pre-allocate node before taking the lock to minimize time spent in critical section
78+
ioexp_range_node_t *node = (ioexp_range_node_t *)heap_caps_malloc(sizeof(ioexp_range_node_t),
79+
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
80+
if (node == NULL) {
81+
return ESP_ERR_NO_MEM;
82+
}
83+
esp_err_t err = ESP_OK;
84+
// Ensure no overlap and no duplicate handler with existing non-embedded nodes
85+
portENTER_CRITICAL(&s_ioexp_lock);
86+
for (ioexp_range_node_t *n = s_ioexp_ranges; n != NULL; n = n->next) {
87+
// Disallow appending the same handler more than once
88+
if (n->handler == handler) {
89+
err = ESP_ERR_INVALID_ARG;
90+
goto err;
91+
}
92+
if (n == &s_embedded_range_head) {
93+
continue; // embedded range is [0, GPIO_NUM_MAX), guaranteed disjoint by start_io_num >= GPIO_NUM_MAX
94+
}
95+
uint32_t n_start = n->start_num;
96+
uint32_t n_end = n->start_num + n->count;
97+
// Overlap if the intervals [start, end) and [n_start, n_end) intersect
98+
if ((start < n_end) && (n_start < end)) {
99+
err = ESP_ERR_INVALID_ARG;
100+
goto err;
101+
}
102+
}
103+
node->start_num = start_io_num;
104+
node->count = cnt;
105+
node->handler = handler;
106+
node->next = s_ioexp_ranges;
107+
s_ioexp_ranges = node;
108+
err:
109+
portEXIT_CRITICAL(&s_ioexp_lock);
110+
if (err != ESP_OK) {
111+
ESP_LOGE(TAG,
112+
"Either requested IO expander range overlaps with existing range or the same handler is already appended");
113+
free(node);
114+
}
115+
return err;
116+
}
117+
118+
esp_err_t esp_io_expander_gpio_wrapper_remove_handler(esp_io_expander_handle_t handler)
119+
{
120+
if (handler == NULL) {
121+
return ESP_ERR_INVALID_ARG;
122+
}
123+
ioexp_range_node_t *to_free = NULL;
124+
ioexp_range_node_t *prev = NULL;
125+
portENTER_CRITICAL(&s_ioexp_lock);
126+
ioexp_range_node_t *curr = s_ioexp_ranges;
127+
while (curr) {
128+
if (curr->handler == handler) {
129+
to_free = curr;
130+
if (prev) {
131+
prev->next = curr->next;
132+
} else {
133+
s_ioexp_ranges = curr->next;
134+
}
135+
curr = curr->next;
136+
break;
137+
}
138+
prev = curr;
139+
curr = curr->next;
140+
}
141+
portEXIT_CRITICAL(&s_ioexp_lock);
142+
if (to_free) {
143+
free(to_free);
144+
}
145+
return ESP_OK;
146+
}
147+
148+
esp_err_t __wrap_gpio_set_level(gpio_num_t gpio_num, uint32_t level)
149+
{
150+
if (gpio_num < GPIO_NUM_MAX) {
151+
// Call the ESP-IDF implementation for regular GPIOs
152+
return __real_gpio_set_level(gpio_num, level);
153+
}
154+
// Redirect GPIO set level calls to ESP IO Expander here
155+
esp_io_expander_handle_t handler = NULL;
156+
uint32_t pin_mask = 0;
157+
if (!find_ioexp_for_num((uint32_t)gpio_num, &handler, &pin_mask)) {
158+
ESP_LOGE(TAG, "GPIO %d is not assigned to any IO Expander", gpio_num);
159+
return ESP_ERR_INVALID_ARG;
160+
}
161+
return esp_io_expander_set_level(handler, pin_mask, (uint8_t)(level ? 1 : 0));
162+
}
163+
164+
int __wrap_gpio_get_level(gpio_num_t gpio_num)
165+
{
166+
if (gpio_num < GPIO_NUM_MAX) {
167+
// Call the ESP-IDF implementation for regular GPIOs
168+
return __real_gpio_get_level(gpio_num);
169+
}
170+
// Redirect GPIO get level calls to ESP IO Expander here
171+
esp_io_expander_handle_t handler = NULL;
172+
uint32_t pin_mask = 0;
173+
if (!find_ioexp_for_num((uint32_t)gpio_num, &handler, &pin_mask)) {
174+
ESP_LOGE(TAG, "GPIO %d is not assigned to any IO Expander", gpio_num);
175+
return -1; // Indicate error
176+
}
177+
uint32_t level_mask = 0;
178+
esp_err_t err = esp_io_expander_get_level(handler, pin_mask, &level_mask);
179+
if (err != ESP_OK) {
180+
return -1; // Indicate error
181+
}
182+
return (level_mask & pin_mask) ? 1 : 0;
183+
}
184+
185+
esp_err_t __wrap_gpio_set_direction(gpio_num_t gpio_num, gpio_mode_t mode)
186+
{
187+
if (gpio_num < GPIO_NUM_MAX) {
188+
// Call the ESP-IDF implementation for regular GPIOs
189+
return __real_gpio_set_direction(gpio_num, mode);
190+
}
191+
// Redirect GPIO set direction calls to ESP IO Expander here
192+
esp_io_expander_handle_t handler = NULL;
193+
uint32_t pin_mask = 0;
194+
if (!find_ioexp_for_num((uint32_t)gpio_num, &handler, &pin_mask)) {
195+
ESP_LOGE(TAG, "GPIO %d is not assigned to any IO Expander", gpio_num);
196+
return ESP_ERR_INVALID_ARG;
197+
}
198+
esp_io_expander_dir_t dir;
199+
esp_io_expander_output_mode_t out_mode = IO_EXPANDER_OUTPUT_MODE_PUSH_PULL;
200+
bool mode_valid = true;
201+
switch (mode) {
202+
case GPIO_MODE_INPUT:
203+
dir = IO_EXPANDER_INPUT;
204+
break;
205+
case GPIO_MODE_OUTPUT:
206+
dir = IO_EXPANDER_OUTPUT;
207+
out_mode = IO_EXPANDER_OUTPUT_MODE_PUSH_PULL;
208+
break;
209+
case GPIO_MODE_OUTPUT_OD:
210+
dir = IO_EXPANDER_OUTPUT;
211+
out_mode = IO_EXPANDER_OUTPUT_MODE_OPEN_DRAIN;
212+
if (!handler->write_highz_reg) {
213+
mode_valid = false;
214+
}
215+
break;
216+
default:
217+
mode_valid = false;
218+
}
219+
if (!mode_valid) {
220+
ESP_LOGE(TAG, "Unsupported GPIO mode %d for IO Expander GPIO %d", mode, gpio_num);
221+
return ESP_ERR_INVALID_ARG;
222+
}
223+
esp_err_t err = esp_io_expander_set_dir(handler, pin_mask, dir);
224+
if (err == ESP_OK && dir == IO_EXPANDER_OUTPUT && handler->write_highz_reg) {
225+
err = esp_io_expander_set_output_mode(handler, pin_mask, out_mode);
226+
}
227+
return err;
228+
}
229+
230+
esp_err_t __wrap_gpio_set_pull_mode(gpio_num_t gpio_num, gpio_pull_mode_t pull)
231+
{
232+
if (gpio_num < GPIO_NUM_MAX) {
233+
// Call the ESP-IDF implementation for regular GPIOs
234+
return __real_gpio_set_pull_mode(gpio_num, pull);
235+
}
236+
// Redirect GPIO set pull mode calls to ESP IO Expander here
237+
esp_io_expander_handle_t handler = NULL;
238+
uint32_t pin_mask = 0;
239+
if (!find_ioexp_for_num((uint32_t)gpio_num, &handler, &pin_mask)) {
240+
ESP_LOGE(TAG, "GPIO %d is not assigned to any IO Expander", gpio_num);
241+
return ESP_ERR_INVALID_ARG;
242+
}
243+
esp_io_expander_pullupdown_t pud = IO_EXPANDER_PULL_NONE;
244+
bool pud_valid = true;
245+
switch (pull) {
246+
case GPIO_PULLUP_ONLY:
247+
if (!handler->write_pullup_en_reg) {
248+
pud_valid = false;
249+
} else {
250+
pud = IO_EXPANDER_PULL_UP;
251+
}
252+
break;
253+
case GPIO_PULLDOWN_ONLY:
254+
if (!handler->write_pullup_en_reg || !handler->write_pullup_sel_reg) {
255+
pud_valid = false;
256+
} else {
257+
pud = IO_EXPANDER_PULL_DOWN;
258+
}
259+
break;
260+
case GPIO_FLOATING:
261+
pud = IO_EXPANDER_PULL_NONE;
262+
break;
263+
default:
264+
pud_valid = false;
265+
break;
266+
}
267+
if (!pud_valid) {
268+
ESP_LOGE(TAG, "Unsupported GPIO pull mode %d for IO Expander GPIO %d", pull, gpio_num);
269+
return ESP_ERR_INVALID_ARG;
270+
}
271+
return esp_io_expander_set_pullupdown(handler, pin_mask, pud);
272+
}

components/io_expander/esp_io_expander/idf_component.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
version: "1.1.0"
1+
version: "1.2.0"
22
description: ESP IO Expander - main component for using io expander chip
33
url: https://github.com/espressif/esp-bsp/tree/master/components/io_expander/esp_io_expander
44
dependencies:

0 commit comments

Comments
 (0)