(More Info) |
The M5Stamp-PICO is a very small and affordable WiFi and Bluetooth enabled module.
I am considering it for replacement in my many WiFi enabled projects where I have been using a WiFi-only module.
Those projects have typically used a conventional microcontroller to perform the basic functionality, and WiFi access is obtained through a WiFi to serial (UART) module. I have been using the C215 module from USR with satisfaction. It is very small, inexpensive, draws very little power which is great for battery powered application and performs well. The C215 has been listed as obsolete on the vendor's web site for over a year, even though I have been able to buy about 100 pieces directly from the factory since. Continued availability is definitely questionable so a replacement is needed.
Having both WiFi and Bluetooth in a single module is attractive though, and it may alleviate one issue I have with the WiFi-to-UART modules like the C215, namely all communication with the module occurs across the single UART (in-band signaling). In order to communicate with the module itself (for configuration or status), you have to interrupt the flow of data and place the module in "command" mode, which is quite inconvenient.
Using a programmable module like the M5Stamp-PICO would allow me to dedicate the UART for data and use the I2C port for command and control (such as obtaining the IP address, switching from Bluetooth to WiFi, etc...), a potentially much more flexible solution.
Alternately, in spite of the low pin count of the module, the processor used in the M5Stamp-PICO is a 32 bit dual core ESP32 chip running at 240MHz with ample Flash and RAM. It is not a slouch by any means and therefore it may well suffice for many low pin count applications, which would alleviate the need for a separate microcontroller for some projects.
My experiments so far indicate that the ESP32-PICO-D4 used in the M5Stamp-PICO is considerably faster in processing web pages than the ESP8266.
There is a variety of development tool options. I chose to try the Arduino IDE since I do have a bit of experience and software for that environment, even though as with most Arduino projects, libraries are not always compatible with your intended target and some tweaking is usually expected.
Configuring the Arduino IDE for use with this chip is beyond the scope of this article, but the jest of it is that you copy this link into the File->Preferences-> Additional Boards Manager URLs box:
(that will take a while), make sure you have a driver for your USB-Serial adapter and then pick your board into Tools->Board
The fine people at M5Stack have a Github repository with a good bit of software for their chips so that's what I got to get started. Note that while most Arduino boards have a single LED that can be used in a pinch to verify that the chip is running (and many examples or Arduino code make use of that LED), the M5Stamp-PICO has a multi-color LED that is accessed using the Adafruit Neopixel library.
This module being relatively new as this is being written (November 2021), there is not a lot of design information available for it yet other than the Github link above, but that is growing daily, so I am not proposing other links for now, I am sure Google will be of help.
I experimented with several of the WiFi examples with varying success (not all examples are for the PICO and some libraries have been updated in a way that makes them incompatible with examples relying on previous versions, not unusual in the Arduino world) but eventually wanted to build a device that would connect to my network using a static IP address and offer a web page that I could use to change the color of the LED.
There are a couple of examples that almost do that but those examples do not indicate the state of the LED. If you are controlling lights locally that's OK, but if you want to control the lights remotely, or control another device that does not visually indicate if it's on or off, that is not good enough, you need an indication of the state of the device back on the web page.
While there are a few different ways to do that, I personally like Ajax (Asynchronous Javascript and XML). I do have an example of an Ajax driven page running on this Apache server. Websockets is another alternative but it is newer and not as widely supported for now.
So this is my Ajax Demo for the M5Stamp-PICO:
/*
Connect to an access point using Static IP
Create a web page allowing control and providing
status of the LED using AJAX
This code is Public domain - 2021 Didier Juges KO4BB
Libraries may have more restrictive copyrights.
*/
#include <WiFi.h>
#include <WiFiClient.h>
#include <Adafruit_NeoPixel.h>
// How many leds in your strip?
#define NUM_LEDS 1
#define DATA_PIN 27
Adafruit_NeoPixel pixels( NUM_LEDS, DATA_PIN, NEO_GRB + NEO_KHZ800 );
const char* ssid = "xxxxxxx";
const char* password = "xxxxxxxxxxx";
IPAddress local_IP( 192, 168, 1, 8 );
IPAddress gateway( 192, 168, 1, 1 );
IPAddress subnet( 255, 255, 255, 0 );
IPAddress primaryDNS( 192, 168, 1, 1 ); //optional
IPAddress secondaryDNS(8, 8, 4, 4); //optional
WiFiServer server( 80 );
char color[6];
void setup(){
Serial.begin( 115200 );
if( !WiFi.config( local_IP, gateway, subnet, primaryDNS, secondaryDNS )){
Serial.println( "STA Failed to configure" );
}
pixels.begin();
pixels.clear();
pixels.setPixelColor( 0, pixels.Color( 255, 0, 0 )); // red
pixels.show();
delay( 500 );
Serial.print( "Connecting to " );
Serial.println( ssid );
WiFi.begin( ssid, password );
while( WiFi.status() != WL_CONNECTED ){
delay( 500 );
Serial.print( "." );
}
pixels.setPixelColor( 0, pixels.Color( 0, 255, 0 )); // green
pixels.show();
delay( 500 );
Serial.println( "" );
Serial.println( "WiFi connected!" );
Serial.print( "IP address: " );
Serial.println( WiFi.localIP() );
Serial.print( "ESP Mac Address: " );
Serial.println( WiFi.macAddress() );
Serial.print( "Subnet Mask: " );
Serial.println( WiFi.subnetMask() );
Serial.print( "Gateway IP: " );
Serial.println( WiFi.gatewayIP() );
Serial.print( "DNS: " );
Serial.println( WiFi.dnsIP() );
server.begin();
Serial.println( "Server started" )
pixels.setPixelColor( 0, pixels.Color( 0, 0, 255 )); // blue
pixels.show();
delay( 500 );
strcpy( color, "blue" );
}
void loop(){
bool page_sent=false;
WiFiClient client = server.available(); // listen for incoming clients
if( client ){ // if you get a client,
Serial.println( "New Client." ); // print a message out the serial port
page_sent = false; // only send response once
String currentLine = ""; // make a String to hold incoming data from the client
while( client.connected() ){ // loop while the client's connected
if( client.available() ){ // if there's bytes to read from the client,
char c = client.read(); // read a byte, then
Serial.write( c ); // print it out the serial monitor
if( c == '\n' ){ // if the byte is a newline character
// if the current line is blank, you got two newline characters in a row.
// that's the end of the client HTTP request, so send a response:
if( currentLine.length() == 0 ){
if( page_sent == false ){
Serial.println( "Sending page..." );
SendPage( &client );
}
// break out of the while loop:
break;
}else{ // if you got a newline, then clear currentLine:
currentLine = "";
}
}else if( c != '\r' ){ // if you got anything else but a carriage return character,
currentLine += c; // add it to the end of the currentLine
}
// Check to see if the client request was "GET /green", "GET /red", "GET /blue" or "GET /color":
if( currentLine.startsWith( "GET /red" )){
pixels.setPixelColor( 0, pixels.Color( 255, 0, 0 )); // red
pixels.show();
Serial.println( "\nChanging color to red" );
strcpy( color, "red" );
currentLine = " "; // length must be > 0
SendColor( color, &client );
page_sent = true;
}else if( currentLine.startsWith( "GET /green" )){
pixels.setPixelColor( 0, pixels.Color( 0, 255, 0 )); // green
pixels.show();
Serial.println( "\nChanging color to green" );
strcpy( color, "green" );
currentLine = " "; // length must be > 0
SendColor( color, &client );
page_sent = true;
}else if( currentLine.startsWith( "GET /blue" )){
pixels.setPixelColor( 0, pixels.Color( 0, 0, 255 )); // blue
pixels.show();
Serial.println( "\nChanging color to blue" );
strcpy( color, "blue" );
currentLine = " "; // length must be > 0
SendColor( color, &client );
page_sent = true;
}else if( currentLine.startsWith( "GET /color" )){
Serial.println( "\nRequesting current color" );
currentLine = " "; // length must be > 0
SendColor( color, &client );
page_sent = true;
}
}
}
// close the connection:
client.stop();
Serial.println( "Client Disconnected." );
}
}
void SendColor( char *col, WiFiClient *client ){
client->println( "HTTP/1.1 200 OK" );
client->println( "Content-type:text/plain" ); // plain text response, not html or xml
client->println();
// the contents of the HTTP response follow the header:
client->print( "color=" );
client->println( col );
// The HTTP response ends with another blank line:
client->println();
}
void SendPage( WiFiClient *client ){
// HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
// and a content-type so the client knows what's coming, then a blank line:
client->println( "HTTP/1.1 200 OK" );
client->println( "Content-type:text/html" );
client->println();
// the contents of the HTTP response follow the header:
client->print( "<html>\n<head>\n<script type=\"text/javascript\">\n" );
client->print( "var color=\"blue\";\n" );
client->print( "function load(){\n" );
client->print( " getColor();\n" );
client->print( " Interval=setInterval(getColor,2000);\n}\n" );
client->print( "function btn(col){\n" );
client->print( " getDocAndCallFunc(col,putColor,col);\n}\n" );
client->print( "function putColor(arg,col){\n" ); // arg = "", col = "col=green"
//client->print( " alert( arg + \",\" + col );\n" );
client->print( " var field = new Array();\n" );
client->print( " field = col.split('=');\n" );
client->print( " document.getElementById('color').innerHTML=field[1]\n}\n" );
client->print( "function getColor(){\n" );
client->print( " getDocAndCallFunc('color',putColor,'');\n}\n" );
client->print( "function getDocAndCallFunc(doc,func,arg){\n" );
client->print( " var xmlhttp;\n" );
client->print( " xmlhttp = new XMLHttpRequest();\n" );
client->print( " xmlhttp.onreadystatechange = function(){\n" );
client->print( " if( xmlhttp.readyState == 4 && xmlhttp.status == 200 ){\n" );
client->print( " func( arg, xmlhttp.responseText );}}\n" );
client->print( " var cmd;\n" );
client->print( " if( doc.indexOf('?') > -1 )\n" );
client->print( " cmd = doc+\"&id=\"+Math.random();\n" );
client->print( " //cmd = doc;\n" );
client->print( " else\n" );
client->print( " cmd = doc+\"?id=\"+Math.random();\n" );
client->print( " //cmd = doc;\n" );
client->print( " xmlhttp.open( \"GET\", cmd, true );\n" );
client->print( " xmlhttp.send();\n}\n" );
client->print( "</script>\n</head>\n" );
client->print( "<body onload=\"load()\">\n" );
client->print( "<center><h1>M5Stamp-PICO</h1><h2>WiFiClientAjaxDemo</h2><h3>Didier Juges</h3></center>\n" );
client->print( "<font face=\"Arial\" size=+3><table align=center><th span=2><form action=\"index.html\" method=get>\n" );
client->print( "<button type=button onclick=\"btn('green')\">Green</button>\n" );
client->print( "<button type=button onclick=\"btn('red')\">Red</button>\n" );
client->print( "<button type=button onclick=\"btn('blue')\">Blue</button>\n" );
client->print( "</form></th>\n" );
client->print( "<tr><td align=right>Color now is</td><td align=left><div id=\"color\"></div></td></tr></table>\n" );
client->print( "</font></body></html>" );
// The HTTP response ends with another blank line:
client->println();
}
The code can be broken down in two parts. The first part (the setup() and loop() functions) is code that describes the functionality running on the M5Stamp-PICO itself. The second part (the SendPage() function) describes Javascript code that will run in the browser and the html markup that describes the web page itself.
To configure the Arduino IDE for use with the M5Stamp-PICO, check the manual.
This is the web page produced by the demo code above:
This implementation allows multiple computers (or mobile devices) to access the web page concurrently and to have the LED status accurately reflected as it is changed, even on the browsers that are not being actively used. This is done by this Javascript statement:
Interval=setInterval(getColor,2000);
Note that since this is not using https, Chrome was complaining that the site was unsecure. Other browsers would most likely complain as well. On my Android 11 phone, I had to go in the settings to allow http traffic before the page would load.
The factory recommended way is to buy the ESP32 Downloader from the M5Stack store, but I have a quantity of CH340 based USB-serial adapters, so I built my own.
One small issue is that the M5Stamp-PICO does not have a Reset pin, so to restart the device before and after programming, you need to recycle power to the CPU. You can pull the EN pin low. This pin enables the 3.3V regulator, so when you bring it low, the CPU will reset, but that does not apparently clear the LED, so if you use the LED for debugging, it is annoying that it is not automatically cleared when grounding the EN pin. Note that the M5Stamp-PICO schematic shows the LED being powered from 3.3V but the datasheet for the SK6812 indicates 5V supply voltage, and the fact that the LED stays on when the EN pin is brought low seems to indicate that the LED is powered from 5V. Caveat emptor.
Since it is very convenient to power the device from the USB-Serial adapter, you can use a simple switch to recycle 5V power on the M5Stamp-PICO module itself, but the glitch caused by charging the capacitor in the module may lead to erratic behavior. I initially tried to add capacitance across the 5V and 3.3V buses of the CH340 board, but that did not do the job.
The solution is simpler, place a 1 to 5 ohm resistor in series with the switch. It causes a negligible (and tolerable) voltage drop during operation, and solves the problem.
To place the device in programming mode, close the BOOT switch, which connectsG0 and GND and press the POWER switch to recycle power. Note that the POWER switch is normlly closed.
When programmed, remove the jumper and recycle power to run your program.