Compare commits

...

2 Commits

Author SHA1 Message Date
Jonathan Bell
e255c6e1fa bugfixes 2025-12-03 10:40:08 +00:00
Jonathan Bell
d90fb9c529 Add remote_gpio feature and helper app
This provides a framework for tunneling arbitrary GPIO ops through the
USB control pipe.

NOTE: this implementation does not respect probe pin assignments, and
destructive operations can be performed which will require a power cycle
to undo.

It is intended as a proof-of concept for experimentation.

The helper requires libudev-dev and libusb-1.0-dev to be installed on
the host.
2025-12-02 13:41:52 +00:00
6 changed files with 485 additions and 3 deletions

View File

@@ -23,6 +23,7 @@ add_executable(debugprobe
src/sw_dp_pio.c
src/tusb_edpt_handler.c
src/autobaud.c
src/remote_gpio.c
)
target_sources(debugprobe PRIVATE

8
include/TODO.txt Normal file
View File

@@ -0,0 +1,8 @@
TODO - add CMake package to build probe_gpio_api applet
In the meanwhile, this oneliner builds on the host platform:
gcc -static -Os probe_gpio_api.c -lusb-1.0 -ludev
requires libusb-1.0-dev and libudev-dev

244
include/probe_gpio_api.c Normal file
View File

@@ -0,0 +1,244 @@
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <errno.h>
#include <string.h>
#include <stdbool.h>
#include <libusb-1.0/libusb.h>
#include "remote_gpio.h"
#define VENDOR_ID 0x2E8A // Raspberry Pi
#define PRODUCT_ID 0x000c // Debugprobe
#define BCDD_VER 0x0103
#define NR_GPIOS 28 // Limit to RP2040/2350A for now
#define GPIO_ALL 0xff
#define GET_STATE BMREQUEST_GPIO_GET
#define SET_STATE BMREQUEST_GPIO_SET
#define GPIO_OUT 1
#define GPIO_IN 0
static const char usage[] = {
"Use: \n"
" probe-gpio <get> [GPIO]\n"
" probe-gpio <set> <GPIO> <OPTIONS>\n"
"\n"
" get: retrieve GPIO state\n"
" if 'get' is specified with no further arguments, then the state of all gpios is returned.\n"
"\n"
" set: GPIO must be specified. One at a time, unlike raspi-gpio.\n"
" OPTIONS: one of:\n"
" op - drive GPIO output\n"
" ip - drive GPIO input\n"
" dl - drive low\n"
" dh - drive high\n"
" pu - pull-up\n"
" pd - pull-down\n"
" pn - pull none\n"
};
const char* pull_state[] = {
"NONE",
"UP",
"DOWN",
"KEEPER"
};
const char* dir_state[] = {
"INPUT",
"OUTPUT",
};
static void print_gpio_state(int gpio, int level, int function, unsigned int dir, unsigned int pull)
{
fprintf(stdout, "GPIO %d: level=%d function=%d dir=%s pull=%s\n",
gpio, level, function,
dir <= 1 ? dir_state[dir] : "UNK",
pull <= 3 ? pull_state[pull] : "UNK"
);
};
int main(int argc, char **argv)
{
int rc, i = 0;
struct libusb_device_handle *devh = NULL;
struct libusb_device **dlist = NULL;
struct libusb_context *ctx = NULL;
struct libusb_device_descriptor desc = {0};
int op;
int gpio;
int set_fn = -1;
unsigned char dummy[4];
unsigned char data[4] = {};
uint32_t glevel, gfunction, gdir, gpull;
if (argc < 2 || argc > 4) {
fprintf(stderr, "Need an operation to do.\n");
fprintf(stderr, "%s", usage);
return -1;
}
if (strcmp(argv[1], "get") == 0) {
op = BMREQUEST_GPIO_GET;
if (argc == 2)
gpio = GPIO_ALL;
else {
gpio = strtol(argv[2], NULL, 10);
if (gpio < 0 || gpio > 28) {
fprintf(stderr, "gpio out of range: %d\n", gpio);
return errno;
}
}
} else if (strcmp(argv[1], "set") == 0) {
op = BMREQUEST_GPIO_SET;
if (argc != 4) {
fprintf(stderr, "need to specify gpio and operation\n");
fprintf(stderr, "%s", usage);
return -1;
}
gpio = strtol(argv[2], NULL, 10);
if (gpio < 0 || gpio > 29) {
fprintf(stderr, "gpio out of range: %d\n", gpio);
return errno;
}
if (strcmp(argv[3], "op") == 0) {
set_fn = GPIO_SET_DIR;
data[0] = GPIO_OUT;
} else if (strcmp(argv[3], "ip") == 0) {
set_fn = GPIO_SET_DIR;
data[0] = GPIO_IN;
} else if (strcmp(argv[3], "dl") == 0) {
set_fn = GPIO_PUT;
data[0] = 0;
} else if (strcmp(argv[3], "dh") == 0) {
set_fn = GPIO_PUT;
data[0] = 1;
} else if (strcmp(argv[3], "pu") == 0) {
set_fn = GPIO_SET_PULLS;
data[0] = 1;
} else if (strcmp(argv[3], "pd") == 0) {
set_fn = GPIO_SET_PULLS;
data[0] = 1 << 1;
} else if (strcmp(argv[3], "pn") == 0) {
set_fn = GPIO_SET_PULLS;
data[0] = 0;
} else {
fprintf(stderr, "invalid operation specified\n");
fprintf(stderr, "%s", usage);
return -1;
}
} else {
fprintf(stderr, "Operation must be <set> or <get>\n");
fprintf(stderr, "%s", usage);
return -1;
}
/* Initialize libusb
*/
rc = libusb_init(&ctx);
if (rc < 0) {
fprintf(stderr, "Error initializing libusb: %s\n", libusb_error_name(rc));
exit(rc);
}
/* Set debugging output to max level.
*/
#if LIBUSB_API_VERSION >= 0x01000106
libusb_set_option(ctx, LIBUSB_OPTION_LOG_LEVEL, 3);
#else
libusb_set_debug(ctx, 3);
#endif
rc = libusb_get_device_list(ctx, &dlist);
if (rc < 0) {
fprintf(stderr, "Error retrieving device list: %s\n", libusb_error_name(i));
exit(rc);
}
/* Look for a specific device and open it.
*/
for (i = 0; i < rc; i++) {
libusb_device *device = dlist[i];
libusb_get_device_descriptor(device, &desc);
if (desc.idVendor == VENDOR_ID && desc.idProduct == PRODUCT_ID) {
//if(desc.bcdDevice == BCDD_VER) {
//printf(stderr, "Found a Debug Probe version %04x - using it\n", desc.bcdDevice);
break;
// } else {
//fprintf(stderr, "Found a Debug Probe version %04x - incompatible\n", desc.bcdDevice);
// }
}
}
if (i == rc) {
fprintf(stderr, "Error: no compatible Debug Probes found - wrong fw?\n");
goto out;
}
rc = libusb_open(dlist[i], &devh);
if (rc < 0) {
fprintf(stderr, "Error: can't open device at address %u: %s\n", libusb_get_device_address(dlist[i]), libusb_error_name(rc));
goto out;
}
libusb_free_device_list(dlist, 1);
/* Does the it do remote gpio? */
rc = libusb_control_transfer(devh, BMREQUEST_GPIO_GET, CTRL_REMOTE_GPIO_REQ, GPIO_GET_FUNCTION,
0, (unsigned char *)&dummy, sizeof(dummy), 0);
if (rc < 0) {
fprintf(stderr, "Error: probe doesn't understand REMOTE_GPIO access - wrong fw?\n");
goto out;
}
rc = 0;
if (op == BMREQUEST_GPIO_GET) {
if (gpio == GPIO_ALL) {
for (i = 0; i < NR_GPIOS; i++) {
rc = libusb_control_transfer(devh, op, CTRL_REMOTE_GPIO_REQ, GPIO_GET,
i, (unsigned char *)&glevel, sizeof(glevel), 3000); if (rc < 0) goto out;
rc = libusb_control_transfer(devh, op, CTRL_REMOTE_GPIO_REQ, GPIO_GET_FUNCTION,
i, (unsigned char *)&gfunction, sizeof(gfunction), 3000); if (rc < 0) goto out;
rc = libusb_control_transfer(devh, op, CTRL_REMOTE_GPIO_REQ, GPIO_GET_DIR,
i, (unsigned char *)&gdir, sizeof(gdir), 3000); if (rc < 0) goto out;
rc = libusb_control_transfer(devh, op, CTRL_REMOTE_GPIO_REQ, GPIO_GET_PULLS,
i, (unsigned char *)&gpull, sizeof(gpull), 3000); if (rc < 0) goto out;
print_gpio_state(i, glevel, gfunction, gdir, gpull);
}
} else {
rc = libusb_control_transfer(devh, op, CTRL_REMOTE_GPIO_REQ, GPIO_GET,
gpio, (unsigned char *)&glevel, sizeof(glevel), 3000); if (rc < 0) goto out;
rc = libusb_control_transfer(devh, op, CTRL_REMOTE_GPIO_REQ, GPIO_GET_FUNCTION,
gpio, (unsigned char *)&gfunction, sizeof(gfunction), 3000); if (rc < 0) goto out;
rc = libusb_control_transfer(devh, op, CTRL_REMOTE_GPIO_REQ, GPIO_GET_DIR,
gpio, (unsigned char *)&gdir, sizeof(gdir), 3000); if (rc < 0) goto out;
rc = libusb_control_transfer(devh, op, CTRL_REMOTE_GPIO_REQ, GPIO_GET_PULLS,
gpio, (unsigned char *)&gpull, sizeof(gpull), 3000); if (rc < 0) goto out;
print_gpio_state(gpio, glevel, gfunction, gdir, gpull);
}
} else if (op == BMREQUEST_GPIO_SET) {
// Need to init a gpio before SIO can do anything useful to it. This clobbers output enable, so avoid glitches
rc = libusb_control_transfer(devh, BMREQUEST_GPIO_GET, CTRL_REMOTE_GPIO_REQ, GPIO_GET_FUNCTION,
gpio, (unsigned char *)dummy, 4, 300); if (rc < 0) goto out;
if (dummy[0] != 5) {
rc = libusb_control_transfer(devh, op, CTRL_REMOTE_GPIO_REQ, GPIO_INIT,
gpio, (unsigned char *)data, 4, 3000); if (rc < 0) goto out;
//printf("fn was %d, setting init\n", gfunction);
}
rc = libusb_control_transfer(devh, op, CTRL_REMOTE_GPIO_REQ, set_fn,
gpio, (unsigned char *)data, 4, 3000); if (rc < 0) goto out;
}
libusb_release_interface(devh, 0);
out:
if (rc < 0) {
fprintf(stderr, "libusb error: %s\n", libusb_error_name(rc));
}
if (devh)
libusb_close(devh);
libusb_exit(NULL);
return rc < 0 ? rc : 0;
}

64
include/remote_gpio.h Executable file
View File

@@ -0,0 +1,64 @@
#ifndef _REMOTE_GPIO_H_
#define _REMOTE_GPIO_H_
#ifdef PICO_SDK_VERSION_MAJOR
#include "tusb.h"
bool gpio_remote_req(uint8_t rhport, uint8_t stage, tusb_control_request_t const * request);
#endif
/* Control interface definitions for exposing pico-sdk's GPIO API.
* bmRequestType.direction - SET calls are Host to Device (0), GET calls are Device to Host (1)
* bmRequestType.type = Vendor (2)
* bmRequestType.recipient = Device (0)
* (i.e. bmRequestType = 0x40 or 0xc0.)
* bRequest = CTRL_REMOTE_GPIO_REQ (0x02)
* wValue - GPIO API function indexed by enum here. Note that there are fewer get_ calls defined than set_ calls.
* wIndex - for functions operating on a single GPIO, the GPIO number. For functions operating on all GPIOs in bulk (e.g gpio_get_all or gpio_dir_out_masked), set to zero.
* wLength = 4. For SET calls, provide a le32 word of either the 2nd argument to a function call for a specific GPIO, or a mask of GPIOs to operate on in bulk.
* Note: for GPIO_INIT and GPIO_DEINIT, the data stage still happens but is ignored.
* For GET calls, returns a le32 of either the function call result, or the results of a bulk operation as a bitmask.
**/
#define CTRL_REMOTE_GPIO_REQ 0x2
#define BMREQUEST_GPIO_SET 0x40
#define BMREQUEST_GPIO_GET 0xc0
typedef enum {
GPIO_GET_FUNCTION,
GPIO_GET_PULLS,
GPIO_GET_INPUT_ENABLED,
GPIO_GET_INPUT_HYST_ENABLED,
GPIO_GET_SLEW_RATE,
GPIO_GET_DRIVE_STRENGTH,
GPIO_GET,
GPIO_GET_ALL,
GPIO_GET_OUT_LEVEL,
GPIO_GET_DIR,
GPIO_GET_MAX,
} gpio_get_fns;
typedef enum {
GPIO_SET_FUNCTION,
GPIO_SET_PULLS,
GPIO_SET_INPUT_ENABLED,
GPIO_SET_INPUT_HYST_ENABLED,
GPIO_SET_SLEW_RATE,
GPIO_SET_DRIVE_STRENGTH,
GPIO_PUT, // No idea why this isn't called gpio_set in the sdk
GPIO_PUT_ALL,
GPIO_SET_MASK,
GPIO_CLR_MASK,
GPIO_XOR_MASK,
GPIO_SET_DIR_OUT_MASKED,
GPIO_SET_DIR_IN_MASKED,
GPIO_SET_DIR_ALL_BITS,
GPIO_SET_DIR,
GPIO_INIT,
GPIO_INIT_MASK,
GPIO_DEINIT,
GPIO_SET_MAX,
} gpio_set_fns;
#endif /* _REMOTE_GPIO_H_ */

View File

@@ -46,6 +46,7 @@
#include "tusb_edpt_handler.h"
#include "DAP.h"
#include "hardware/structs/usb.h"
#include "remote_gpio.h"
// UART0 for debugprobe debug
// UART1 for debugprobe to target device
@@ -194,8 +195,9 @@ extern uint8_t const desc_ms_os_20[];
bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const * request)
{
// nothing to with DATA & ACK stage
if (stage != CONTROL_STAGE_SETUP) return true;
// nothing to with Status stage
if (stage == CONTROL_STAGE_ACK)
return true;
switch (request->bmRequestType_bit.type)
{
@@ -203,6 +205,8 @@ bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_requ
switch (request->bRequest)
{
case 1:
if (stage != CONTROL_STAGE_SETUP)
return true;
if ( request->wIndex == 7 )
{
// Get Microsoft OS 2.0 compatible descriptor
@@ -214,7 +218,8 @@ bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_requ
{
return false;
}
case CTRL_REMOTE_GPIO_REQ:
return gpio_remote_req(rhport, stage, request);
default: break;
}
break;

160
src/remote_gpio.c Executable file
View File

@@ -0,0 +1,160 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2023 Raspberry Pi Ltd
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
#include <pico/stdlib.h>
#include <stdio.h>
#include <string.h>
#include "hardware/gpio.h"
#include "probe_config.h"
#include "remote_gpio.h"
/* tinyUSB gives you multiple callbacks, one per stage */
static uint32_t data;
bool gpio_remote_req(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request)
{
/* Is the host paying attention ? */
if (request->bmRequestType_bit.recipient != TUSB_REQ_RCPT_DEVICE)
return false;
probe_info("remote_req wValue=0x%02x wIndex=0x%02x wLength=0x%02x dir=%d\n", request->wValue, request->wIndex, request->wLength, request->bmRequestType_bit.direction);
if (request->wIndex >= NUM_BANK0_GPIOS)
return false;
switch (request->bmRequestType_bit.direction)
{
case TUSB_DIR_IN:
{
if(stage != CONTROL_STAGE_SETUP)
return true;
if (request->wLength != 4)
return false;
switch (request->wValue) {
case GPIO_GET_FUNCTION:
data = gpio_get_function(request->wIndex);
break;
case GPIO_GET_PULLS:
data = (gpio_is_pulled_up(request->wIndex) ? 0x1 : 0);
data |= (gpio_is_pulled_down(request->wIndex) ? 0x2 : 0);
break;
case GPIO_GET_INPUT_HYST_ENABLED:
data = gpio_is_input_hysteresis_enabled(request->wIndex);
break;
case GPIO_GET_SLEW_RATE:
data = gpio_get_slew_rate(request->wIndex);
break;
case GPIO_GET_DRIVE_STRENGTH:
data = gpio_get_drive_strength(request->wIndex);
break;
case GPIO_GET:
data = gpio_get(request->wIndex);
break;
case GPIO_GET_ALL:
data = gpio_get_all();
break;
case GPIO_GET_OUT_LEVEL:
data = gpio_get_out_level(request->wIndex);
break;
case GPIO_GET_DIR:
data = gpio_get_dir(request->wIndex);
break;
default:
return false;
break;
}
probe_info("ctrl IN data 0x%08lx\n", data);
return tud_control_xfer(rhport, request, &data, sizeof(data));
}
case TUSB_DIR_OUT:
{
/* Data phase should "always" happen */
if (stage == CONTROL_STAGE_SETUP) {
return tud_control_xfer(rhport, request, &data, sizeof(data));
} else if (stage == CONTROL_STAGE_DATA) {
probe_info("data stage2 data= %08lx\n", data);
switch (request->wValue) {
case GPIO_SET_FUNCTION:
gpio_set_function(request->wIndex, data);
break;
case GPIO_SET_PULLS:
gpio_set_pulls(request->wIndex, !!(data & 0x1), !!(data & 0x2));
break;
case GPIO_SET_INPUT_ENABLED:
gpio_set_input_enabled(request->wIndex, data);
break;
case GPIO_SET_INPUT_HYST_ENABLED:
gpio_set_input_hysteresis_enabled(request->wIndex, data);
break;
case GPIO_SET_SLEW_RATE:
gpio_set_slew_rate(request->wIndex, data);
break;
case GPIO_SET_DRIVE_STRENGTH:
gpio_set_drive_strength(request->wIndex, data);
break;
case GPIO_PUT:
gpio_put(request->wIndex, !!data);
break;
case GPIO_PUT_ALL:
gpio_put_all(data);
break;
case GPIO_SET_MASK:
gpio_set_mask(data);
break;
case GPIO_CLR_MASK:
gpio_clr_mask(data);
break;
case GPIO_XOR_MASK:
gpio_xor_mask(data);
break;
case GPIO_SET_DIR_OUT_MASKED:
gpio_set_dir_out_masked(data);
break;
case GPIO_SET_DIR_IN_MASKED:
gpio_set_dir_in_masked(data);
break;
case GPIO_SET_DIR_ALL_BITS:
gpio_set_dir_all_bits(data);
break;
case GPIO_SET_DIR:
gpio_set_dir(request->wIndex, !!data);
break;
case GPIO_INIT:
gpio_init(request->wIndex);
break;
case GPIO_INIT_MASK:
gpio_init_mask(data);
break;
case GPIO_DEINIT:
gpio_deinit(request->wIndex);
break;
default:
return false;
}
return true;
} else {
return true;
}
}
}
return false;
}