earlephilhower / arduino-pico

Raspberry Pi Pico Arduino core, for all RP2040 and RP2350 boards
GNU Lesser General Public License v2.1
1.99k stars 412 forks source link

How to detect difference between Pico and Pico W? #849

Closed jhmaloney closed 1 year ago

jhmaloney commented 2 years ago

This is a question, not an issue.

Is there a way to detect the difference between a Pico (i.e. a generic RP2040 board) and a Pico W, either when the board is in BOOTSEL mode or at startup time? Alternatively, is it possible to create a "universal .uf2" file that can contain code for both variations?

I'm the lead developer for MicroBlocks, a blocks programming language aimed at students and teachers who are just getting started with microcontrollers. I'd like to make the installation process for the Pico/PicoW as simple and automatic as possible.

One way to do that would be to have the firmware installer detect the board type (in BOOTSEL mode) so it could copy the appropriate .uf2 file to the board.

An alternate solution would be to create a universal .uf2 file, as the Micro:bit Educational Foundation did to allow a single .uf2 file to include binaries for both the micro:bit v1 and the micro:bit v2 boards. That would require that the installer built into the boot ROM could read such universal .uf2 files. (some possibly useful info here: https://forums.raspberrypi.com/viewtopic.php?t=335280)

A third solution would be to always include to WiFi support code, but have the program detect the presence or absence of the WiFI chip and adapt at power up. While this solution would include unused WiFi code on non-WiFi boards it would be okay to "waste" Flash storage space for the sake of simplifying the installation process for beginners.

Suggestions?

earlephilhower commented 2 years ago

That's an interesting question and one I've been wondering myself but not dug into.

FWI, re: the universal binary UF2, I think they were talking about the flash and not something like this. The ROM bootloader is a very simple UF2 programmer and there's no logic in it to allow something like that. https://github.com/raspberrypi/pico-bootrom . What I think they meant was that every flash chip has its own BOOT2 bootloader that's called by the ROM to set up the flash interface. It seems every flash mfgr. has their own protocol for non-1-bit, 1-directional SPI, and there are also different timing requirements even in SPI mode. So AIUI a "universal" binary would just include the (very slow) SPI/4 boot2.S file and work everywhere.

The Pico chip and flash themselves are absolutely identical W and non-W, so it's up to the cyw43_init() call to fail and report back up the stack. Right now I believe it hangs in the case of missing CYW43 chip, but I have not examined it in GDB to see if it's my code or the upstream wifi driver.

Let me see if I can wire up a GDB session and see if there's a simple way to catch the failing init call...

jhmaloney commented 2 years ago

re: the universal binary UF2

As I understand it, the data blocks in the .uf2 can have a "family_ID" field and the boot loader is supposed to ignore blocks that don't match its own family ID. That allows one to create multiplatform .uf2 files by merging the .uf2 files for different families. For that work, however, the bootloaders for the Pico and the Pico W would need to be filtering on different family ID's. Unfortunately, the master file at https://github.com/microsoft/uf2/blob/master/utils/uf2families.json only lists one entry for the RP2040 right now, so I'm guessing that the boot loader does not make any distinction between Pico W and other RP2040 boards.

Let me see if I can wire up a GDB session and see if there's a simple way to catch the failing init call...

Thanks! No rush; I have a temporary workaround (just ask the user), but I'm looking for a better long-term solution. If it is possible to catch the failing call, it would be great if there were a way to check if it succeeded (i.e. that the board is Pico W). That's needed in order to enable/disable the WiFi API and to handle the user LED correctly on the two boards.

earlephilhower commented 2 years ago

or that work, however, the bootloaders for the Pico and the Pico W would need to be filtering on different family ID's.

The ROM itself needs to do this filtering, and there is no "Pico W" ROM, it's just a standard RP2040 chip with an onboard peripheral that the ROM knows nothing about. So, even updating the UF2 family generator isn't going to help here. :(

A quick check w/GDB shows the CYW43 low level driver (from the SDK) is stuck waiting for a DMA completion that will never happen in the case when you try to start WiFi on a board w/o the chip, so it looks like some low-level hacking to make this work. It's in the queue but not going to happen quickly, sorry!

jhmaloney commented 2 years ago

The ROM itself needs to do this filtering, and there is no "Pico W" ROM, it's just a standard RP2040 chip

That's what I thought; thanks for confirming. That also also means that there's no way to tell the difference between a Pico W and any other RP2040 board by looking at files on the virtual Flash drive (RPI-RP2), a trick I've sometimes used for other boards.

That just leaves the third option, have the program detect the presence or absence of the WiFI chip at power up and adapt.

it looks like some low-level hacking to make this work. It's in the queue but not going to happen quickly

No problem, and no rush! Thanks for considering it.

Also, many thanks for adding support for the Pico W so quickly. I was surprised at how easy it was to get the WiFi code we had for ESP boards up and running on the Pico W. Everything I've tried has worked right away, including joining a network, making HTTP requests, and UDP. Kudos!

NuclearPhoenixx commented 1 year ago

Just found this (arguably old) issue and thought I maybe have a bit of a hacky solution to your problem that might still help you.

On the "normal" Raspberry Pi Pico, GPIO_24 is used to detect the presence of a USB connection (digital 0 or 1). However, the same pin GPIO_24 on the Pico W is used for OP/IP wireless SPI data/IRQ and not the USB connection. So, theoretically, you could check if this pin GPIO_24 is pulled high on your very first start-up, just after the upload of your sketch is completed. Since there has to be a USB connection for uploading, this pin should be high every time you flash the device. Then you can save a variable to your flash that reminds the device what type of Pico it is...

Pico: Screenshot 2022-12-29 at 19-36-22 Raspberry Pi Pico Datasheet - pico-datasheet pdf

Pico W: Screenshot 2022-12-29 at 19-36-16 Raspberry Pi Pico W Datasheet - pico-w-datasheet pdf

There are some other pins that are changed between the two versions, so something similar to this might be a solution too. It's pretty complicated, heck you'd need LittleFS or the "EEPROM" compatability lib just for this, but it's the only way you could differentiate it in hardware without looking for the wireless chip, I think.

jhmaloney commented 1 year ago

Any progress on this issue?

sabas1080 commented 1 year ago

Why not identify it by the manufacturer's USB PID?

PICO W USB_VID = 0x239A USB_PID = 0x8120

PICO

USB_VID = 0x239A USB_PID = 0x80F4

Regards

earlephilhower commented 1 year ago

Those USB IDs are stored in the actual code, so aren't useful here where the program is actually trying to find out which one of those IDs to use for the Sw USB interface (and whether or not to init WiFi)...

NuclearPhoenixx commented 1 year ago

Those USB IDs are stored in the actual code, so aren't useful here where the program is actually trying to find out which one of those IDs to use for the Sw USB interface (and whether or not to init WiFi)...

You could still check it from the programmer side and choose which sketch to upload, couldn't you? I.e. one for the Pico and one for the Pico W.

jhmaloney commented 1 year ago

Thanks for your suggestions sabas1080 and Phoenix1747. Here's a bit more context, in case you are interested.

This fix would help K-12 educators and students who are just getting started with microcontrollers.

I'd like to have a single universal .uf2 file that they can install on either a Pico or a Pico W. That means the binary has to detect which board it is running on at power up. If it is on a Pico W, it should initialize the CYW43 chip, otherwise it should not.

A binary for a plain Pico runs on a Pico W. You don't get WiFi, of course, and the default user LED pin is incorrect, but it runs. However, if you install a Pico W binary on a normal Pico then it hangs on power up trying to initialize the non-existent CYW43 chip.

Earle understands this problem. As he suggested, one solution would be to modify cyw43_init() to return with an error rather than hanging, allowing the boot sequence to proceed. Another solution would be detect the board type and only call cyw43_init() if it is a Pico W.

MicroBlocks is currently asking users to select the board type from a menu when installing firmware on Pico boards. That works if they make the right selection. Unfortunately, some users select Pico W when they don't actually have a Pico W, which doesn't work. It would be a better user experience if we didn't need to ask them to choose. We could always install the universal binary and it would work on either board.

dlkeng commented 1 year ago

This is how I programmatically determine whether the code is running on a Pico or a Pico W. It is kind of ad-hoc and depends on the different way the Pico and Pico W hardware implement the VSYS monitoring input on the GP29 pin.

In use, at startup, a call is made to the CheckPicoBoard() function which then sets the global PicoIsW variable to true or false for use anytime the program needs to determine which board it's running on.

#define GPIO25_PIN      (25u)
#define GPIO29_PIN      (29u)

bool PicoIsW = false;

// ad-hoc way to determine if is Pico or is Pico W
void CheckPicoBoard(void)
{
    enum gpio_function pin25_func;
    enum gpio_function pin29_func;
    uint pin25_dir;
    uint pin29_dir;

    // remember pin directions
    pin25_dir = gpio_get_dir(GPIO25_PIN);
    pin29_dir = gpio_get_dir(GPIO29_PIN);

    // remember pin functions
    pin25_func = gpio_get_function(GPIO25_PIN);
    pin29_func = gpio_get_function(GPIO25_PIN);

    // activate pins
    gpio_init(GPIO25_PIN);
    gpio_init(GPIO29_PIN);

    // activate VSYS input
    gpio_set_dir(GPIO25_PIN, GPIO_OUT);
    gpio_put(GPIO25_PIN, 1);

    gpio_set_dir(GPIO29_PIN, GPIO_IN);
    if (gpio_get(GPIO29_PIN))   // should be high (sort-of, read ADC value better) on both Pico and Pico W
    {
        gpio_put(GPIO25_PIN, 0);        // disable VSYS input on Pico W
        if (!gpio_get(GPIO29_PIN))      // should be low from pull-down on Pico W
        {
            PicoIsW = true;             // is Pico W
        }
        else
        {
            PicoIsW = false;            // is Pico
        }
    }

    // restore pin functions
    gpio_set_function(GPIO25_PIN, pin25_func);
    gpio_set_function(GPIO29_PIN, pin29_func);

    // restore pin directions
    gpio_set_dir(GPIO25_PIN, pin25_dir);
    gpio_set_dir(GPIO29_PIN, pin29_dir);
}
earlephilhower commented 1 year ago

@jhmaloney have you given the above routine a test? If it's good I can include it in the PicoW variant and run it automatically on startup, and expose the boolean through a rp2040.isPicoW() function...

jhmaloney commented 1 year ago

I'll give it a try next week and let you know.

earlephilhower commented 1 year ago

Another option. There's a (new?) PDF from The RPI folks with a section called "2.4: Which HW am I running on?" https://datasheets.raspberrypi.com/picow/connecting-to-the-internet-with-pico-w.pdf


#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/gpio.h"
#include "hardware/adc.h"

int main() {
 stdio_init_all();
 adc_init();
 adc_gpio_init(29);
 adc_select_input(3);
 const float conversion_factor = 3.3f / (1 << 12);
 uint16_t result = adc_read();
 printf("ADC3 value: 0x%03x, voltage: %f V\n", result, result * conversion_factor);

 gpio_init(25);
 gpio_set_dir(25, GPIO_IN);
 uint value = gpio_get(25);
 printf("GP25 value: %i", value);
 }

in a project directory gives this for Raspberry Pi Pico W,

ADC3 value: 0x01c, voltage: 0.022559 V
GP25 value: 0

and this for an original Raspberry Pi Pico board,

ADC3 value: 0x2cd, voltage: 0.577661 V
GP25 value: 0
jhmaloney commented 1 year ago

dlkeng's code did not work for me -- I got the same results on both Pico and Pico-W. I'm guessing that the voltage on pin 29 is in the indeterminate range for digital values so the current code might work on some boards but not others. Reading the analog value on that pin would probably fix the problem.

In any case, the code from RPI folks in "2.4: Which HW am I running on?" gives very different analog values for the Pico and Pico-W so that looks like the way to go.

I assume the test would need to be done before calling cyw43_init() since cyw43_init () hangs on Pico boards that don't have the CYW43 chip, right?

dlkeng commented 1 year ago

My original code appeared to work on the two Pico's and the two Pico W's I have. However, I did note in a comment on reading the GPIO29 input that "read ADC value better".

Therefore, based on the reported results, I have changed reading the GPIO29 value from digital to analog using the ADC. Note that saving and restoring the pins configuration and ADC reset states is possibly optional.

#include "hardware/resets.h"
#include "hardware/gpio.h"
#include "hardware/adc.h"

#define GPIO25_PIN      (25u)
#define GPIO29_PIN      (29u)
#define GPIO29_ADC_CHAN (3)

bool PicoIsW = false;

// ad-hoc way to determine if is Pico or is Pico W
void CheckPicoBoard(void)
{
    enum gpio_function pin25_func;
    enum gpio_function pin29_func;
    uint pin25_dir;
    uint pin29_dir;
    uint adc_reset;
    uint16_t result;

    // remember pin directions
    pin25_dir = gpio_get_dir(GPIO25_PIN);
    pin29_dir = gpio_get_dir(GPIO29_PIN);

    // remember pin functions
    pin25_func = gpio_get_function(GPIO25_PIN);
    pin29_func = gpio_get_function(GPIO25_PIN);

    // remember ADC reset state
    adc_reset = resets_hw->reset & RESETS_RESET_ADC_BITS;

    // activate pins
    gpio_init(GPIO25_PIN);

    // initialize ADC peripheral
    adc_init();

    // Make sure ADC GPIO is high-impedance, no pullups etc.
    adc_gpio_init(GPIO29_PIN);
    // Select ADC input 3 (GPIO29_PIN)
    adc_select_input(GPIO29_ADC_CHAN);

    // activate VSYS input
    gpio_set_dir(GPIO25_PIN, GPIO_OUT);
    gpio_put(GPIO25_PIN, 1);

    result = adc_read();
    if (result > 0x200)   // should be high on both Pico and Pico W
    {
        gpio_put(GPIO25_PIN, 0);        // disable VSYS input on Pico W
        result = adc_read();
        if (result < 0x100)             // should be low from pull-down on Pico W
        {
            PicoIsW = true;             // is Pico W
        }
        else
        {
            PicoIsW = false;            // is Pico
        }
    }

    // restore ADC reset state
    reset_block(adc_reset);

    // restore pin functions
    gpio_set_function(GPIO25_PIN, pin25_func);
    gpio_set_function(GPIO29_PIN, pin29_func);

    // restore pin directions
    gpio_set_dir(GPIO25_PIN, pin25_dir);
    gpio_set_dir(GPIO29_PIN, pin29_dir);
}
earlephilhower commented 1 year ago

@dlkeng why are you pulling GP25 high? Is that safe in the case the board is non-USB powered? The RPI code actually just reads it (so should be safe at all times) and if I understand the doc correctly the logic is:

GP(25)==0 && ADC(29)<0x100 => PICOW
GP(25)==0 && ADC(29)>=0x100 => PICO
GP(25)=1 => can't say, take your chances :shrug: 
earlephilhower commented 1 year ago

I just ran a binary built with #1204 on Pico and PicoW and it seems to report properly.

It also adjusts the LED_DEFAULT pin, too (since they differ Pico/PicoW) so a blink.ino built for the PicoW will work properly on a Pico.

Can you please give it a go, @jhmaloney and @dlkeng ?

(The new call, in the docs, is rp2040.isPicoW() and can be called by anything. On non-PicoW compiles it always returns false, on PicoW builds it returns the guessed value.)

earlephilhower commented 1 year ago

Here's a blink.ino which should work on either board (compiled for PicoW) blink.zip

jhmaloney commented 1 year ago

This is perfect! Blink works on both pico and pico-W board and really nice that you made LED_DEFAULT work for both boards.

This allow MicroBlocks (and others) to distribute a single binary that works on both Pico and Pico-W board. That's really nice!

When will the fix become available in PlatformIO? No rush...

earlephilhower commented 1 year ago

Thanks for the feedback!

I should have a release this weekend, so probably sometime middle of next week. You can also always point to the git repo directly to pull it in before then.

jhmaloney commented 1 year ago

Thanks @dlkeng for the code that led to a solution.

ladyada commented 1 year ago

popping in here to request that instead of LED_DEFAULT, please use LED_BUILTIN - its the 'official' name of the built in LED on arduino boards/cores

https://github.com/search?q=org%3Aarduino%20LED_BUILTIN&type=code

dlkeng commented 1 year ago

@earlephilhower, in response to "why are you pulling GP25 high? Is that safe in the case the board is non-USB powered?"

GPIO25 on both the Pico and Pico W are generally used as a digital output to control things. On the Pico, GPIO25 drives the onboard LED via a resistor. On the Pico W, GPIO25 has two functions:

  1. participate in the CYW43 communications,
  2. manage the ability to monitor the VSYS voltage via an FET (see: Reading VSYS on Pico W).

As such, it should be safe to drive GPIO25 as long as no WiFi communications are going on with the CYW43. In my case I was using it to ensure the Pico vs Pico W boards by driving GPIO25 high to sense the VSYS voltage and then drive GPIO25 low to disconnect monitoring of the VSYS voltage, to more actively determine the Pico vs the Pico W difference. In the case of a Pico, this will momentarily turn the onboard LED on and then off with the monitored voltage from GPIO29/ADC3 not changing. In the case of the Pico W, when GPIO25 is high, the voltage from GPIO29/ADC3 will be at a higher level (about VSYS / 3) and when GPIO25 is low, the voltage from GPIO29/ADC will be at a lower level (close to 0 volts).

In the case of only monitoring the state of the GPIO25 signal, on the Pico, it should always be low due to the resistor and LED to ground. On the Pico W, it appears to be taking advantage of the defaultly-enabled pull-down on the signal, which will turn off the VSYS FET so that a low will also be seen (if it is a high, then something is wrong with the CYW43 somehow driving that signal).

As to the board being non-USB powered (i.e. Pico board VSYS pin 39 powered or Pico board VBUS pin 40 powered), that should not have any relevance as in any case VSYS is common - USB or VSYS or VBUS pin. If the Pico board 3V3_EN pin 37 is externally pulled low, then the RP2040 will not be running.

As to the supplied "blink.zip", I too can confirm that it appears to run correctly on the Pico's and Pico W's that I have.

As an observation, the new rp2040.isPicoW() only detects a Pico W if it was compiled as a "Raspberry Pi Pico W" board but not if compiled as a "Raspberry Pi Pico" board. My preference would be that it would be nice if I compiled code as "Raspberry Pi Pico" board and ran it on a Pico W, I would like to detect that it was running on a Pico W instead of a Pico. I realize that probably complicates things quite a bit with the way it is currently implemented.

P.S. I think instead of LED_DEFAULT, you probably meant to say LED_PIN, which is mapped to LED_BUILTIN in "common.h".

earlephilhower commented 1 year ago

As an observation, the new rp2040.isPicoW() only detects a Pico W if it was compiled as a "Raspberry Pi Pico W" board but not if compiled as a "Raspberry Pi Pico" board. My preference would be that it would be nice if I compiled code as "Raspberry Pi Pico" board and ran it on a Pico W, I would like to detect that it was running on a Pico W instead of a Pico. I realize that probably complicates things quite a bit with the way it is currently implemented.

That's not possible w/o costing ~400KB of useless code (the WIFI FW and the TCP stuff) for all Pico projects, so it's not going to happen. The idea here is you build for the max, knowingly paying that price, but are able to also run on the minimum one (i.e. opposite idea).

I'll do an emergency fix and move the LED pin change right into the custom digitalWrite for the PicoW. 2.7.4 here we cone. :sob:

dlkeng commented 1 year ago

That's OK. I was not proposing to be able to utilize any of the Pico W enhancements - just recognize which board it was executing on. I can still do that with my routine without much extra code.

earlephilhower commented 1 year ago

The real pain is it takes 350KB of firmware to control the LED on the PicoW, even if you don't do any WiFi, since it's controlled by the 2nd ARM chip. If you're not using the WiFi at all, and no LED,. then you can just build for the Pico and be done with it, since the chip itself is 100% identical, only a few IO hookups being modified.

@ladyada yes, it works (and will work) with LED_BUILTIN. I was looking at Blink.ino which, for some reason, uses the Arduino LED_DEFAULT constant but we are setting LED_BUILTIN in the variant.

jhmaloney commented 1 year ago

I've been thinking about what @dlkeng wrote and studying the Pico-W schematic:

Screenshot 2023-02-18 at 3 41 55 PM

With the current implementation, it seems possible that, under certain unusual conditions, the voltage on pin 29 would be higher than the expected value on the Pico-W and the test might falsely report that the board was not a Pico-W.

This scenario might occur if the test were done while pin 25 was outputting high (e.g. during communications with the CYW43). The test code makes pin 25 an input, but I don't believe that doing that automatically turns on the pull-down resistor. Thus, there could be enough residual voltage on the gate of the FET to turn it on. That would pull the value on pin 29 up to 1/3 of VSYS.

In that scenario, a digital read of pin 25 would probably return "high", so the later code in the test function would assume that it was Pico-W. But there's a small chance that, due to leakage, the voltage could have dropped to "low" from the standpoint of the pin 25 yet still be high enough to turn on the FET.

I admit, this scenario seems unlikely.

Still, it might be more reliable to explicitly enable the pulldown on pin 25 with gpio_pull_down(25) before reading reading the analog value of pin 29.

You'd also need to record the pulldown state with gpio_is_pulled_down( 25) and and restore that state when done, so this would add three lines of code to your current test. But, in compensation, the code wouldn't actually need to check the value of pin 25 since it would be in a known state.

A variation would be to make pin 25 output low (briefly) while reading the value of pin 29.

This is definitely not urgent and it may not be needed at all -- I just wanted to think it through for my own satisfaction.