Picowoose: The Raspberry Pi Pico-W meets Mongoose
The Raspberry Pi Pico-W meets Mongoose; here's the picodriver explained, with a very small MQTT example.
This example application describes the way to adapt the George Robotics CYW43 driver, present in the Pico-SDK, to work with Cesanta's Mongoose. We are then able to use Mongoose internal TCP/IP stack (with TLS 1.3), instead of lwIP (and MbedTLS). Future improvements will let us run this along MycroPython... "Pycowoose"... would you be interested ?
The driver code is taken as is from Cesanta's Github repository.
The example code is based on their MQTT client examples.
How to build
Building for Linux does not have any special requirements; just requires having ARM GCC and CMake. This tutorial describes how to install these for your OS. The Makefile will need some tweaks for Windows users, as rm
is extensively used and I'm not friends with their shell(s).
- Clone this Github repository and change to the "picowoose" directory:
$ git clone https://github.com/scaprile/mongoose_apps.git $ cd picowoose
- then call make to build with your Wi-Fi credentials
$ make WIFI_SSID=mySSID WIFI_PASS=mypassword
- The Makefile will clone Pico-SDK 1.5.1 from Github and start the build process; be patient and wait for it to finish.
- Plug your board; boot your Pico in USB Mass Storage Mode by holding down the BOOTSEL button while you plug it into the USB port. Once it is connected, release that button; it should mount as a new USB disk in your computer. Now either manually copy or drag and drop the uf2 file we just built (it lies inside the build directory), into this new USB disk device.
- The Pico will flash this code and reboot, unmounting this disk and running the flashed code.
See it in action
The example code catches the event when the device gets an IP address from your DHCP server, then connects to an MQTT broker and sends a simple message. It also logs these two, so we can verify it is connecting by checking the log opening a terminal on the serial console. Run a serial port software (we use picocom; make sure you configure it at 115200bps). Device name is usually /dev/ttyACM0 in Linux.
ede 2 mongoose.c:5351:rx_dhcp_client Lease: 43200 sec (43203) ee1 2 mongoose.c:5126:onstatechange READY, IP: 192.168.5.244 ee2 2 mongoose.c:5127:onstatechange GW: 192.168.5.1 ee3 2 mongoose.c:5128:onstatechange MAC: 28:cd:c1:00:e3:f9 ee7 2 main.c:28:mif_fn Got IP 11b1 2 main.c:19:mqtt_fn PUBLISHED
Assuming we've been smart enough to honor causality and had an MQTT client already listening, we'll see something like this:
$ mosquitto_sub -h broker.hivemq.com -t embedded/related Pico-W is up and running !
How does it work ?
The Raspberry Pi Pico-W is a nice module with .1" pin spacing and castellated holes, so it can have .1" pins to be plugged to a protoboard, or easily soldered onto a PCB. Its heart is an RP2040, a Raspberry Pi-designed microcontroller based on an Arm Cortex-M0+ core and actually sporting two of those cores. This microcontroller has many interesting features, and this module is pretty similar to its elder sibling, the Pico, though it just adds an Infineon CYW43-family chip with Wi-Fi and Bluetooth support.
Mongoose is an event-driven framework that decouples us from a possible underlying networking stack and/or operating system, calling an event handler callback whenever there is an event of interest; for this to work, we need to periodically call its event manager, usually in an infinite loop. Inside this infinite loop we also call the Wi-FI chip driver.
Initialization code is a bit more involved that in other past articles, as we are using Mongoose built-in TCP/IP stack (and in "low-level mode") this time. Besides the usual event manager initialization, we need to initialize the TCP/IP stack first:
As we've already seen in a previous blog, an MQTT client will connect to a broker and stay connected; hence being able to subscribe to a topic, publish to some topics, and receive messages that have been published on its topic of interest (to which it has subscribed). This time, we will just publish a simple message on a specified topic so it can be received by all subscribed clients, for demonstration purposes.
The connection to the broker is done at initalization time, when we receive the READY
event on the interface handler. We are not passing neither user and password nor last-will message, this can be easily done setting the proper elements on the opts
structure.
This MQTT connection is a Mongoose connection, and as such it has an event handler. Once we are connected to the broker, we get called to our MQTT event handler function with an MG_EV_MQTT_OPEN
event. We want to publish a message, so we will call a Mongoose function to perform that action:
The connection process should be a bit more involved. We should thrive to keep the connection always alive, though in this case we just wanted to do an as-simple-as-possible example, focusing on how to interact with the radio driver. We'll address all those nice real-life situations once we merge this with Pythongoose and give birth to "Pycowoose", being able to have Mongoose and MicroPython on the Pico-W. That is not a difficult task, but there are some cleanup tasks that we would like to do first, along with possible upgrades to the 2.0 version of the Pico-SDK. Who knows... do you ?
Let's go deeper
"A dream within a dream ?"
We've seen that Mongoose can use a Berkeley Sockets interface to interact with an underlying networking stack. The Pico-SDK can use lwIP, and with newest SDKs also running on FreeRTOS, with MbedTLS to handle TLS. However, the traditional SDK used the RAW lwIP API, that is not compatible with the way Mongoose can work. So we use Mongoose's own TCP/IP stack.
Our code starts by initializing the TCP/IP stack. As Mongoose is internally an event-driven library, it will call our interface event handler when it has a usable IP address. At that point, we will call mg_mqtt_connect()
, Mongoose then parses the URL and obtains the IP address to connect to, using the TCP port indicated in the URL. Default port is 1883. It then initiates a TCP connection to that IP and port by sending a TCP SYN.
When the TCP connection establishes, the MQTT connection takes place. As we've explained on a previous blog, this involves an exchange of messages and possibly user authentication, depending on how the broker is setup. In this case, we don't do user authentication. Once the connection phase is completed, we are effectively connected to the broker. This is indicated by a specific MQTT control message; with the reception of this message, Mongoose fires a specific event that we catch in our MQTT event handler.
There, we will call mg_mqtt_pub()
, that will result in yet another MQTT message exchange. The purpose of publishing is application dependent, we are just demoing a driver so we just send a simple message to be received on some client also connected to the same broker and subscribed to the topic to which we are publishing.
MQTT is an application of the publish/subscribe systems architecture, born as a way to decouple realms of the whole application in order to solve less complex portions of the whole, one at a time. When it receives are publish message, the broker will get the topic, search its internal list, and deliver the message to every client that has subscribed to it (and is connected... should the message have the retain flag set, it would be stored and delivered on connect to new clients, but this is not the case now). We just started mosquitto_sub to do that, so the broker will gently publish the message to it.
Mongoose driver internals
Mongoose’s TCP/IP stack driver API is simple and elegant, it just requires writing a set of four functions (send, receive, interface is up, initialize) and there is a lock-free queue that can be used to decouple asynchronous environments like interrupt contexts.
The CYW43 driver requires periodic polling, this is done by calling cyw43_poll()
. This function then fires a callback for each outstanding frame; our callback queues those frames:
To send a frame, we just call cyw43_send_ethernet()
, driver function present in the Pico-SDK.
To check link state, we just call cyw43_wifi_link_status()
:
The driver initialization code is also simple, it takes care of starting the connection to the Wi-Fi network:
There are also some required callback functions that are called back by the CYW43 under those name-related circumstances, and a handy set of functions to either get a MAC address from the chip OTP memory, or generate one based on the Pico unique board-id.
What the future might bring
Newer SDKs have an interface to work with the CYW43 in one of three modes: polled, async in background, with FreeRTOS. In this implementation, we bypassed that scheme and went as baremetal as possible. This might not be feasible with Pico-SDK 2.0.0+, but, on the other hand, it seems that we'll have two extra possibilities: periodically running at a low priority IRQ (similar to how they are handlng USB stdio now), or run under FreeRTOS, which also means we could probably also run over lwIP and share the same MicroPython architecture that we can have on an ESP32. We'll see.
- Comments
- Write a Comment Select to add a comment
To post reply to a comment, click on the 'reply' button attached to each comment. To post a new comment (not a reply to a comment) check out the 'Write a Comment' tab at the top of the comments.
Please login (on the right) if you already have an account on this platform.
Otherwise, please use this form to register (free) an join one of the largest online community for Electrical/Embedded/DSP/FPGA/ML engineers: