{"@attributes":{"version":"2.0"},"channel":{"title":"rcn at work","link":"https:\/\/blogs.igalia.com\/rcn","description":"rcn's articles about work and software","comment":[{},{},{},{},{}],"item":[{"title":"First steps with Zephyr","link":"https:\/\/blogs.igalia.com\/rcn\/posts\/20250807-first_steps_with_zephyr\/index.html","author":{"name":"rcn"},"description":{"p":["I recently started playing around\n          with ,\n          reading about it and doing some experiments, and I figured\n          I'd rather jot down my impressions and findings so that the me\n          in the future, who'll have no recollection of ever doing this,\n          can come back to it as a reference. And if it's helpful for\n          anybody else, that's a nice bonus.","It's been a really long time since I last dove into embedded\n          programming for low-powered hardware and things have changed\n          quite a bit, positively, both in terms of hardware\n          availability for professionals and hobbyists and in the\n          software options. Back in the day, most of the open source\n          embedded OSs I tried\n          felt like toy operating systems: enough for simple\n          applications but not really suitable for more complex systems\n          (eg. not having a proper preemptive scheduler is a serious\n          limitation). In the proprietary side things looked better and\n          there were many more options but, of course, those weren't\n          freely available.","Nowadays, Zephyr has filled that gap in the open source\n          embedded OSs field,\n          even becoming the  OS to use, something like\n          a \"Linux for embedded\": it feels like a full-fledged OS, it's\n          feature rich, flexible and scalable, it has an enormous\n          traction in embedded, it's widely supported by many of the big\n          names in the industry and it has plenty of available\n          documentation, resources and a thriving community. Currently,\n          if you need to pick an OS for embedded platforms, unless\n          you're targetting very minimal hardware (8\/16bit\n          microcontrollers), it's a no brainer.","One of the most interesting qualities of Zephyr is its\n          flexibility: the base system is lean and has a small\n          footprint, and at the same time it's easy to grow a\n          Zephyr-based firmware for more complex applications thanks to\n          the variety of supported features. These are some of them:","Find more information and details in\n          the .","Now let's move on and get some actual hands on experience\n          with Zephyr. The first thing we'll do is to set up a basic\n          development environment so we can start writing some\n          experiments and testing them. It's a good idea to keep a\n          browser tab open on\n          the , so we can reference them when needed or search for\n          more detailed info.","The development environment is set up and contained within a\n          python venv. The Zephyr project provides\n          the \n          command line tool to carry out all the setup and build\n          steps.","The basic tool requirements in Linux are CMake, Python3 and\n          the device tree compiler. Assuming they are installed and\n          available, we can then set up a development environment like\n          this:","Some basic nomenclature: the \n          directory is known as a west \"workspace\". Inside it,\n          the  directory contains the repo of Zephyr\n          itself.","Next step is to install the Zephyr SDK, ie. the toolchains\n          and other host tools. I found this step a bit troublesome and\n          it could have better defaults. By default it will install all\n          the available SDKs (many of which we won't need) and then all\n          the host tools (which we may not need either). Also, in my\n          setup, the script that install the host tools fails with a\n          buffer overflow, so instead of relying on it to install the\n          host tools (in my case I only needed qemu) I installed it\n          myself. This has some drawbacks: we might be missing some\n          features that are in the custom qemu binaries provided by the\n          SDK, and  won't be able to run our apps on\n          qemu automatically, we'll have to do that ourselves. Not ideal\n          but not a dealbreaker either, I could figure it out and run\n          that myself just fine.","So I recommend to install the SDK interactively so we can\n          select the toolchains we want and whether we want to install\n          the host tools or not (in my case I didn't):","For the initial tests I'm targetting riscv64 on qemu, we'll\n          pick up other targets later. In my case, since the host tools\n          installation failed on my setup, I needed to\n          provide  myself, you probably\n          won't have to do that.","Now, to see if everything is set up correctly, we can try to\n          build the simplest example program there\n          is: . To build it\n          for  we can use \n          like this:","Where  tells  to do a\n          pristine build ,ie. build everything every time. We may not\n          need that necessarily but for now it's a safe flag to use.","Then, to run the app in qemu, the standard way is to\n          do , but if we didn't install\n          the Zephyr host tools we'll need to run qemu ourselves:",{"b":"Architecture-specific note","code":["qemu-system-riscv64","-bios\n          none"],"a":{"@attributes":{"href":"#fn3","id":"ref3"},"sup":"3"}},"The  repo contains an example\n            application that we can use as a reference for a workspace\n            application (ie. an application that lives in the\n            `zephyrproject` workspace we created earlier). Although we\n            can use it as a reference, I didn't have a good experience\n            with it\n          \n          ,\n          so I recommend to start from scratch or to take the\n          example applications in the \n          directory as templates as needed.","To create a new application, we simply have to make a\n          directory for it in the workspace dir and write a minimum set of\n          required files:",{"code":["CMakeLists.txt","main.c"]},"where  is the name of the\n          application.  is meant to contain\n          application-specific config options and will be empty for\n          now.  is optional.","Assuming the code in  is correct, we can\n          then build the application for a specific target with:","where  is the directory containing the\n          application files listed above. Note that \n          uses CMake under the hood, so the build will be based on\n          whatever build system CMake uses\n          (apparently,  by default), so many of these\n          operations can also be done at a lower level using the\n          underlying build system commands (not recommended).","Zephyr supports building applications for different target\n          types or abstractions. While the end goal will normally be to\n          have a firmare running on a SoC, for debugging purposes, for\n          testing or simply to carry out most of the development without\n          relying on hardware, we can target qemu to run the application\n          on an emulated environment, or we can even build the app as a\n          native binary to run on the development machine.","The differences between targets can be abstracted through\n          proper use of APIs and device tree definitions so, in theory,\n          the same application (with certain limitations) can be\n          seamlessly built for different targets without modifications,\n          and the build process takes care of doing the right thing\n          depending on the target.","As an example, let's build and run\n          the  sample program in three different\n          targets with different architectures: \n          (x86_64 with emulated devices), qemu (Risc-V64 with full system\n          emulation) and a real board,\n          a  (ARM Cortex-M33).","Before starting, let's clean up any previous builds:","Now, to build and run the application as a native binary:","For Risc-V64 on qemu:","For the Raspberry Pi Pico 2W:","In this case, flashing and checking the console output are\n          board-specific steps. Assuming the flashing process worked, if\n          we connect to the board UART0, we can see the output\n          message:","Note that the application prints that line like this:","The output of printf will be sent through the\n          target  device, however it's\n          defined in its device tree. So,\n          for :","Which will eventually print to stdout\n          (see \n          and ). For\n          :","and\n          from :","For\n        the :","and\n        from :","This shows we can easily build our applications using\n          hardware abstractions and have them working on different\n          platforms using the same code and build environment.","Now that we're set and ready to work and the environment is\n          all set up, we can start doing more interesting things. In a\n          follow-up post I'll show a concrete example of an application\n          that showcases most of the features\n          listed ."],"h3":["Noteworthy features","Getting started","What's next?"],"ul":{"li":["\n            Feature-rich\n            kernel : for a small operating system, the amount of\n            core services available is quite remarkable. Most of the\n            usual tools for general application development are there:\n            thread-based runtime with preemptive and cooperative\n            scheduling, multiple synchronization and IPC mechanisms,\n            basic memory management functions, asynchronous and\n            event-based programming support, task management, etc.\n          ","\n            SMP support.\n          ","\n            Extensive core library: including common data structures,\n            shell support and a POSIX compatibility layer.\n          ","\n            Out-of-the-box hardware support for\n            a .\n          ","\n            Logging and tracing: simple but capable facilities with\n            support for different backends, easy to adapt to the\n            hardware and application needs.\n          ",{"a":["Native\n            simulation target","device\n            emulation"]},{"a":"Device\n              tree support"},{"a":"Configurable\n              scheduler"},"\n            Memory protection support\n            and  on supported architectures.\n          ","\n            Powerful and easy to\n            use .\n          "]},"h4":["Development environment setup","Starting a new application","Building for different targets"],"pre":[{"code":"\npython3 -m venv zephyrproject\/.venv\n. zephyrproject\/.venv\/bin\/activate\n\n# Now inside the venv\n\npip install west\nwest init zephyrproject\ncd zephyrproject\nwest update\n\nwest zephyr-export\nwest packages pip --install\n        "},{"code":"\ncd zephyr\nwest sdk install -i\n        "},{"code":"\nwest build -p always -b qemu_riscv64 samples\/hello_world\n        "},{"code":"\nqemu-system-riscv64 -nographic -machine virt -bios none -m 256 -net none \\\n    -pidfile qemu.pid -chardev stdio,id=con,mux=on -serial chardev:con \\\n    -mon chardev=con,mode=readline -icount shift=6,align=off,sleep=off \\\n    -rtc clock=vm \\\n    -kernel zephyr\/build\/zephyr\/zephyr.elf\n\n*** Booting Zephyr OS build v4.1.0-6569-gf4a0beb2b7b1 ***\nHello World! qemu_riscv64\/qemu_virt_riscv64\n        "},{"code":"\n.\n\u251c\u2500\u2500 CMakeLists.txt\n\u251c\u2500\u2500 prj.conf\n\u251c\u2500\u2500 README.rst\n\u2514\u2500\u2500 src\n    \u2514\u2500\u2500 main.c\n        "},{"code":"\ncmake_minimum_required(VERSION 3.20.0)\n\nfind_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})\nproject(test_app)\n\ntarget_sources(app PRIVATE src\/main.c)\n        "},{"code":"\nwest build -p always -b <target> <app_name>\n        "},{"code":"\nwest build -t clean\n        "},{"code":"\nwest build -p always -b native_sim\/native\/64 zephyr\/samples\/hello_world\n[... omitted build output]\n\n.\/build\/zephyr\/zephyr.exe \n*** Booting Zephyr OS build v4.1.0-6569-gf4a0beb2b7b1 ***\nHello World! native_sim\/native\/64\n        "},{"code":"\nwest build -t clean\nwest build -p always -b qemu_riscv64 zephyr\/samples\/hello_world\n[... omitted build output]\n\nwest build -t run\n*** Booting Zephyr OS build v4.1.0-6569-gf4a0beb2b7b1 ***\nHello World! qemu_riscv64\/qemu_virt_riscv64\n        "},{"code":"\nwest build -t clean\nwest build -p always -b rpi_pico2\/rp2350a\/m33 zephyr\/samples\/hello_world\n[... omitted build output]\n\nwest flash -r uf2\n        "},{"code":"\n*** Booting Zephyr OS build v4.1.0-6569-gf4a0beb2b7b1 ***\nHello World! rpi_pico2\/rp2350a\/m33\n        "},{"code":"\n#include <stdio.h>\n\nint main(void)\n{\n\tprintf(\"Hello World! %s\\n\", CONFIG_BOARD_TARGET);\n\n\treturn 0;\n}\n        "},{"code":"\n\/ {\n[...]\n\tchosen {\n\t\tzephyr,console = &uart0;\n[...]\n\tuart0: uart {\n\t\tstatus = \"okay\";\n\t\tcompatible = \"zephyr,native-pty-uart\";\n\t\t\/* Dummy current-speed entry to comply with serial\n\t\t * DTS binding\n\t\t *\/\n\t\tcurrent-speed = <0>;\n\t};\n        "},{"code":"\n\/ {\n\tchosen {\n\t\tzephyr,console = &uart0;\n[...]\n\n&uart0 {\n\tstatus = \"okay\";\n};\n        "},{"code":"\nuart0: uart@10000000 {\n\tinterrupts = < 0x0a 1 >;\n\tinterrupt-parent = < &plic >;\n\tclock-frequency = < 0x384000 >;\n\treg = < 0x10000000 0x100 >;\n\tcompatible = \"ns16550\";\n\treg-shift = < 0 >;\n};\n        "},{"code":"\n\/ {\n\tchosen {\n[...]\n\t\tzephyr,console = &uart0;\n\n[...]\n\n&uart0 {\n\tcurrent-speed = <115200>;\n\tstatus = \"okay\";\n\tpinctrl-0 = <&uart0_default>;\n\tpinctrl-names = \"default\";\n};\n        "},{"code":"\nuart0: uart@40070000 {\n\tcompatible = \"raspberrypi,pico-uart\", \"arm,pl011\";\n\treg = <0x40070000 DT_SIZE_K(4)>;\n\tclocks = <&clocks RPI_PICO_CLKID_CLK_PERI>;\n\tresets = <&reset RPI_PICO_RESETS_RESET_UART0>;\n\tinterrupts = <33 RPI_PICO_DEFAULT_IRQ_PRIORITY>;\n\tinterrupt-names = \"uart0\";\n\tstatus = \"disabled\";\n};\n        "}],"comment":{},"div":{"@attributes":{"class":"footnotes"},"p":["1: Most of them are generally labelled as RTOSs,\n            although the \"RT\" there is used rather\n            loosely.","2: ThreadX is now an option too, having become\n            open source recently. It brings certain features that are\n            more common in proprietary systems, such as security\n            certifications, and it looks like it was designed in a more\n            focused way. In contrast, it lacks the ecosystem and other\n            perks of open source projects (ease of adoption, rapid\n            community-based growth).","3: ."]}},"pubDate":"Thu, 7 Aug 2025 14:00:00 GMT+1"},{"title":"First steps with Zephyr (II)","link":"https:\/\/blogs.igalia.com\/rcn\/posts\/20250813-first_steps_with_zephyr_ii\/index.html","author":{"name":"rcn"},"description":{"p":["In\n          the  we set up a Zephyr development environment and\n          checked that we could build applications for multiple\n          different targets. In this one we'll work on a sample\n          application that we can use to showcase a few Zephyr features\n          and as a template for other applications with a similar\n          workflow.","We'll simulate a real work scenario and develop a firmware\n          for a hardware board (in this example it'll be\n          a ) and we'll set up a development workflow that\n          supports the  target, so we can do most\n          of the programming and software prototyping on a simulated\n          environment without having to rely on the\n          hardware.\n          \n          \n          Then, after the prototyping is done we can deploy and test the\n          firmare on the real board. We'll see how we can do a simple\n          behavioral model of some of the devices we'll use in the final\n          hardware setup and how we can leverage this workflow to\n          unit-test and refine the firmware.","This post is a walkthrough of the whole application. You can\n          find the code .","The application we'll build and run on the Raspberry Pi Pico\n          2W will basically just listen for a button press. When the\n          button is pressed the app will enqueue some work to be done by\n          a processing thread and the result will be published via I2C\n          for a controller to request. At the same time, it will\n          configure two serial consoles, one for message logging and\n          another one for a command shell that can be used for testing\n          and debugging.","These are the main features we'll cover with this experiment:","Besides the target board and the development machine, we'll\n          be using a Linux-based development board that we can use to\n          communicate with the Zephyr board via I2C. Anything will do\n          here, I used a very\n          old  that I had lying around.","The only additional peripheral we'll need is a physical\n          button connected to a couple of board pins. If we don't have\n          any, a jumper cable and a steady pulse will also\n          work. Optionally, to take full advantage of the two serial\n          ports, a USB - TTL UART converter will be useful. Here's how the\n          full setup looks like:","For additional info on how to set up the Linux-based\n          Raspberry Pi, see the  at the\n          end.","Before we start coding we need to know how we'll structure\n          the application. There are certain conventions and file\n          structure that the build system expects to find under certain\n          scenarios. This is how we'll structure the application\n          (test_rpi):","Some of the files there we already know from\n          the : \n          and . All the application code will be in\n          the  directory, and we can structure it as we\n          want as long as we tell the build system about the files we\n          want to compile. For this application, the main code will be\n          in ,  will contain\n          the code of the processing thread, and \n          will keep everything related to the device emulation for\n          the  target and will be compiled only\n          when we build for that target. We describe this to the build\n          system through the contents of :","In  we'll put the general Zephyr\n          configuration options for this application. Note that inside\n          the  directory there are two additional\n          .conf files. These are target-specific options that will be\n          merged to the common ones in  depending\n          on the target we choose to build for.","Normally, most of the options we'll put in the .conf files\n          will be already defined, but we can also define\n          application-specific config options that we can later\n          reference in the .conf files and the code. We can define them\n          in the application-specific Kconfig file. The build system\n          will it pick up as the main Kconfig file if it exists. For\n          this application we'll define one additional config option\n          that we'll use to configure the log level for the program, so\n          this is how Kconfig will look like:","Here we're simply prepending a config option before all\n          the rest of the main Zephyr Kconfig file. We'll see how to\n          use this option later.","Finally, the  directory also contains\n          target-specific overlay files. These are regular device tree\n          overlays which are normally used to configure the\n          hardware. More about that in a while.","The application flow is structured in two main threads: the\n          \n          and an additional processing thread that does its work\n          separately. The main thread runs the application entry point\n          (the  function) and does all the software\n          and device set up. Normally it doesn't need to do anything\n          more, we can use it to start other threads and have them do\n          the rest of the work while the main thread sits idle, but in\n          this case we're doing some work with it instead of creating an\n          additional thread for that. Regarding the processing thread,\n          we can think of it as \"application code\" that runs on its\n          own and provides a simple interface to interact with the rest\n          of the system.","Once the main thread has finished all the initialization\n        process (creating threads, setting up callbacks, configuring\n        devices, etc.) it sits in an infinite loop waiting for messages\n        in a message queue. These messages are sent by the processing\n        thread, which also runs in a loop waiting for messages in\n        another queue. The messages to the processing thread are sent, as\n        a result of a button press, by the GPIO ISR callback registered\n        (actually, by the bottom half triggered by it and run by a\n        ). Ignoring the I2C part for now, this is how the\n        application flow would look like:","Once the button press is detected, the GPIO ISR calls a\n          callback we registered in the main setup code. The callback\n          defers the work (1) through a workqueue (we'll see why later),\n          which sends some data to the processing thread (2). The data\n          it'll send is just an integer: the current uptime in\n          seconds. The processing thread will then do some processing\n          using that data (convert it to a string) and will send the\n          processed data to the main thread (3). Let's take a look at\n          the code that does all this.","As we mentioned, the main thread will be responsible for,\n        among other tasks, spawning other threads. In our example it\n          will create only one additional thread.","We'll see what the  function does\n          in a while. For now, notice we're passing two message queues,\n          one for input and one for output, as parameters for that\n          function. These will be used as the interface to connect the\n          processing thread to the rest of the firmware.","Zephyr's device tree support greatly simplifies device\n          handling and makes it really easy to parameterize and handle\n          device operations in an abstract way. In this example, we\n          define and reference the GPIO for the button in our setup\n          using a platform-independent device tree node:","This looks for a \"button-gpios\" property in\n          the  in the device tree of the target platform and\n          initializes\n          a \n          property containing the GPIO pin information defined in the\n          device tree. Note that this initialization and the check for\n          the \"zephyr,user\" node are static and happen at compile time\n          so, if the node isn't found, the error will be caught by the\n          build process.","This is how the node is defined for the Raspberry Pi Pico\n          2W:","This defines the GPIO to be used as the second GPIO from\n          , it'll be set up with an internal pull-up resistor and\n          will be active-low. See\n          the  for details on the specification format. In\n          the board, that GPIO is routed to pin 4:","Now we'll use\n          the  to configure the GPIO as defined and to add a callback\n          that will run when the button is pressed:","We're configuring the pin as an input and then we're enabling\n          interrupts for it when it goes to logical level \"high\". In\n          this case, since we defined it as active-low, the interrupt\n          will be triggered when the pin transitions from the stable\n          pulled-up voltage to ground.","Finally, we're initializing and adding a callback function\n          that will be called by the ISR when it detects that this GPIO\n          goes active. We'll use this callback to start an action from\n          a user event. The specific interrupt handling is done\n          by the target-specific device driver and we don't have to worry about\n          that, our code can remain device-independent.",{"strong":"NOTE","a":["input\n        subsystem","this"]},"What we want to do in the callback is to send a message to\n          the processing thread. The communication input channel to the\n          thread is the  message queue, and the data\n          we'll send is a simple 32-bit integer with the number of\n          uptime seconds. But before doing that, we'll first de-bounce\n          the button press using a simple idea: to schedule the message\n          delivery to a\n          :","That way, every unwanted oscillation will cause a\n          re-scheduling of the message delivery (replacing any prior\n          scheduling).  will eventually\n          read the GPIO status and send the message.","As I mentioned earlier, the interface with the processing\n          thread consists on two message queues, one for input and one for\n          output. These are defined statically with\n          the  macro:","Both queues have space to hold only one message each. For the\n          input queue (the one we'll use to send messages to the\n          processing thread), each message will be one 32-bit\n          integer. The messages of the output queue (the one the\n          processing thread will use to send messages) are 8 bytes\n          long.","Once the main thread is done initializing everything, it'll\n          stay in an infinite loop waiting for messages from the\n          processing thread. The processing thread will also run a loop\n          waiting for incoming messages in the input queue, which are\n          sent by the button callback, as we saw earlier, so the message\n          queues will be used both for transferring data and for\n          synchronization. Since the code running in the processing\n          thread is so small, I'll paste it here in its entirety:","Now that we have a way to interact with the program by\n          inputting an external event (a button press), we'll add a way\n          for it to communicate with the outside world: we're going to\n          turn our device into a\n          IC \n          that will listen for command requests from a controller and\n          send data back to it. In our setup, the controller will be\n          Linux-based Raspberry Pi, see the diagram in\n          the  section above\n          for details on how the boards are connected.","In order to define an IC target we first need a\n          suitable device defined in the device tree. To abstract the\n          actual target-dependent device, we'll define and use an alias\n          for it that we can redefine for every supported target. For\n          instance, for the Raspberry Pi Pico 2W we define this alias in\n          its device tree overlay:","Where  is originally  like this:","and then :","So now in the code we can reference\n          the  alias to load the device info and\n          initialize it:","To register the device as a target, we'll use\n          the \n          function, which takes the loaded device tree device and an\n          IC target configuration () containing the IC\n          address we choose for it and a set of callbacks for all the\n          possible events. It's in these callbacks where we'll define the\n          target's functionality:","Each of those callbacks will be called as a response from an\n          event started by the controller. Depending on how we want to\n          define the target we'll need to code the callbacks to react\n          appropriately to the controller requests. For this application\n          we'll define a register that the controller can read to get a\n          timestamp (the firmware uptime in seconds) from the last time\n          the button was pressed. The number will be received as an\n          8-byte ASCII string.","If the controller is the Linux-based Raspberry Pi, we can use\n          the \n          to poll the target and read from it:","We basically want the device to react when the controller\n          sends a write request (to select the register and prepare the\n          data), when it sends a read request (to send the data bytes\n          back to the controller) and when it sends a stop\n          condition.","To handle the data to be sent, the IC callback\n          functions manage an internal buffer that will hold the string\n          data to send to the controller, and we'll load this buffer\n          with the contents of a source buffer that's updated every time\n          the main thread receives data from the processing thread (a\n          double-buffer scheme). Then, when we program an IC\n          transfer we walk this internal buffer sending each byte to the\n          controller as we receive read requests. When the transfer\n          finishes or is aborted, we reload the buffer and rewind it for\n          the next transfer:","The application logic is done at this point, and we were\n          careful to write it in a platform-agnostic way. As mentioned\n          earlier, all the target-specific details are abstracted away\n          by the device tree and the Zephyr APIs. Although we're\n          developing with a real deployment board in mind, it's very\n          useful to be able to develop and test using a behavioral model\n          of the hardware that we can program to behave as close to the\n          real hardware as we need and that we can run on our\n          development machine without the cost and restrictions of the\n          real hardware.","To do this, we'll rely on the  board, which implements the core OS\n          services on top of a POSIX compatibility layer, and we'll add\n          code to simulate the button press and the IC\n          requests.","We'll use\n        the \n        driver as a base for our emulated\n        button. The  device tree already defines\n        an  for this:","So we can define the GPIO to use for our button in the\n          native_sim board overlay:","We'll model the button press as a four-phase event consisting\n          on an initial status change caused by the press, then a\n          semi-random rebound phase, then a phase of signal\n          stabilization after the rebounds stop, and finally a button\n          release. Using the  API it'll look like\n          this:","The driver will take care of checking if the state changes\n          , depending on the GPIO configuration,\n        and will trigger the registered callback that we defined\n        earlier.","As with the button emulator, we'll rely on an existing\n          emulated device driver for\n          this: . Again,\n          the device tree for the target already defines the node we\n          need:","So we can define a machine-independent alias that we can\n          reference in the code:","The events we need to emulate are the requests sent by the\n          controller: READ start, WRITE start and STOP. We can define\n          these based on\n          the  which will, in this case, use\n          the  to simulate the transfer. As in the\n          GPIO emulation case, this will trigger the appropriate\n          callbacks. The implementation of our controller requests looks\n          like this:","Now we can define a complete request for an \"uptime read\"\n          operation in terms of these primitives:","Ok, so now that we have implemented all the emulated\n          operations we needed, we need a way to trigger them on the\n          emulated\n          environment. The  is tremendously useful for cases like this.","The shell module in Zephyr has a lot of useful features that\n          we can use for debugging. It's quite extensive and talking\n          about it in detail is out of the scope of this post, but I'll\n          show how simple it is to add a few custom commands to trigger\n          the button presses and the IC controller requests\n          from a console. In fact, for our purposes, the whole thing is\n          as simple as this:","We'll enable these commands only when building for\n          the  board. With the configuration\n          provided, once we run the application we'll have the log\n          output in stdout and the shell UART connected to a pseudotty,\n          so we can access it in a separate terminal and run these\n          commands while we see the output in the terminal where we ran\n          the application:","To simulate a button press (ie. capture the current\n        uptime):","And the log output should print the enabled debug\n          messages:","If we now simulate an IC uptime command request\n          we should get the captured uptime as a string:","We can check the log to see how the IC callbacks\n          ran:","This is the process I followed to set up a Linux system on a\n          Raspberry Pi (very old, model 1 B). There are plenty of\n          instructions for this on the Web, and you can probably just\n          pick up a pre-packaged and\n          pre-configured  and get done with it faster, so I'm adding this here\n          for completeness and because I want to have a finer grained\n          control of what I put into it.","The only harware requirement is an SD card with two\n          partitions: a small (~50MB) FAT32 boot partition and the rest\n          of the space for the rootfs partition, which I formatted as\n          ext4. The boot partition should contain a specific set of\n          configuration files and binary blobs, as well as the kernel\n          that we'll build and the appropriate device tree binary. See\n          the  for more information on the boot partition contents\n          and  for the binary blobs. For this board, the minimum\n          files needed are:","And, optionally but very recommended:","In practice, pretty much all Linux setups will also have\n        these files. For our case we'll need to add one additional\n        config entry to the  file in order to\n          enable the I2C bus:","Once we have the boot partition populated with the basic\n          required files (minus the kernel and dtb files), the two main\n          ingredients we need to build now are the kernel image and the\n          root filesystem.","Main\n          reference: ","There's nothing non-standard about how we'll generate this\n          kernel image, so you can search the Web for references on how\n          the process works if you need to. The only things to take into\n          account is that we'll pick\n          the  instead of a vanilla mainline kernel. I also\n          recommend getting the \n          cross-toolchain\n          from .","After installing the toolchain and cloning the repo, we just\n          have to run the usual commands to configure the kernel, build\n          the image, the device tree binaries, the modules and have the\n          modules installed in a specific directory, but first we'll\n          add some extra config options:","We'll need to add at least ext4 builtin support so that the\n          kernel can mount the rootfs, and I2C support for our\n          experiments, so we need to edit , add\n          these:","And run the  target. Then we can\n          proceed with the rest of the build steps:","Now we need to copy the kernel and the dtbs to the boot partition of the\n          sd card:","(we really only need the dtb for this particular board, but\n          anyway).","There are many ways to do this, but I normally use the classic\n          \n          to build Debian rootfss. Since I don't always know which\n          packages I'll need to install ahead of time, the strategy I\n          follow is to build a minimal image with the bare minimum\n          requirements and then boot it either on a virtual machine or\n          in the final target and do the rest of the installation and\n          setup there. So for the initial setup I'll only include the\n          openssh-server package:","Now we'll copy the kernel modules to the rootfs. From the kernel directory, and\n          based on the build instructions above:","If your distro provides qemu static binaries (eg. Debian:\n          qemu-user-static), it's a good idea to copy the qemu binary to the\n          rootfs so we can mount it locally and run apt-get on it:","Otherwise, we can boot a kernel on qemu and load the rootfs there to\n          continue the installation. Next we'll create and populate the filesystem\n          image, then we can boot it on qemu for additional tweaks or\n          dump it into the rootfs partition of the SD card:","To copy the rootfs to the SD card:","(Substitute  for the sd card rootfs\n          partition in your system).","At this point, if we need to do any extra configuration steps\n          we can either:","Here are some of the changes I made. First, network\n          configuration. I'm setting up a dedicated point-to-point\n          Ethernet link between the development machine (a Linux laptop)\n          and the Raspberry Pi, with fixed IPs. That means I'll use a\n          separate subnet for this minimal LAN and that the laptop will\n          forward traffic between the Ethernet nic and the WLAN\n          interface that's connected to the Internet. In the rootfs I\n          added a file\n          () with the\n          following contents:","Where 192.168.2.101 is the address of the board NIC and\n          192.168.2.100 is the one of the Eth NIC in my laptop. Then,\n          assuming we have access to the serial console of the board and\n          we logged in as root, we need to\n          enable :","Additionally, we need to edit the ssh server configuration to\n          allow login as root. We can do this by\n          setting \n          in .","In the development machine, I configured the traffic\n          forwarding to the WLAN interface:","Once all the configuration is done we should be able to log\n          in as root via ssh:","In order to issue I2C requests to the Zephyr board, we'll\n          need to load the i2c-dev module at boot time and install the\n          i2c-tools in the Raspberry Pi:"],"h3":["Application description","Hardware setup","Setting up the application files","Main application architecture","Thread creation","GPIO handling","Thread synchronization and messaging","IC target implementation","Device emulation","Shell commands","Appendix: Linux set up on the Raspberry Pi"],"ul":[{"li":["Support for multiple targets.","Target-specific build and hardware configuration.","Logging.","Multiple console output.","Zephyr shell with custom commands.","Device emulation.","GPIO handling.","I2C target handling.","Thread synchronization and message-passing.","Deferred work (bottom halves)."]},{"li":["\n            bootcode.bin: the second-stage bootloader, loaded by the\n            first-stage bootloader in the BCM2835 ROM. Run by the GPU.\n          ","start.elf: GPU firmware, starts the ARM CPU.","fixup.dat: needed by start.elf. Used to configure the SDRAM.","kernel.img: this is the kernel image we'll build.","dtb files and overlays."]},{"li":["config.txt: bootloader configuration.","cmdline.txt: kernel command-line parameters."]},{"li":["Mount the SD card and make the changes there.","Boot the filesystem image in qemu with a suitable kernel\n            and make the changes in a live system, then dump the\n            changes into the SD card again.","Boot the board and make the changes there directly. For\n            this we'll need to access the board serial console through\n            its UART pins."]}],"pre":["\n   +--------------------------+\n   |                          |    Eth\n   |      Raspberry Pi        |---------------+\n   |                          |               |\n   +--------------------------+               |\n      6    5   3                              |\n      |    |   |                              |\n      |   I2C I2C       \/                     |\n     GND  SCL SDA    __\/ __                   |\n      |    |   |    |     GND                 |\n      |    |   |    |      |                  |\n      18   7   6    4     38                  |\n   +--------------------------+            +-------------+\n   |                          |    USB     | Development |\n   |   Raspberry Pi Pico 2W   |------------|   machine   |\n   |                          |            +-------------+\n   +--------------------------+                |\n          13      12      11                   |\n           |      |       |                    |\n          GND   UART1    UART1                 |\n           |     RX       TX                   |\n           |      |       |                    |\n          +-----------------+     USB          |\n          |  USB - UART TTL |------------------+\n          |    converter    |\n          +-----------------+\n",{"code":"\ntest_rpi\n\u251c\u2500\u2500 boards\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 native_sim_64.conf\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 native_sim_64.overlay\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 rpi_pico2_rp2350a_m33.conf\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 rpi_pico2_rp2350a_m33.overlay\n\u251c\u2500\u2500 CMakeLists.txt\n\u251c\u2500\u2500 Kconfig\n\u251c\u2500\u2500 prj.conf\n\u251c\u2500\u2500 README.rst\n\u2514\u2500\u2500 src\n    \u251c\u2500\u2500 common.h\n    \u251c\u2500\u2500 emul.c\n    \u251c\u2500\u2500 main.c\n    \u2514\u2500\u2500 processing.c"},{"code":"\ncmake_minimum_required(VERSION 3.20.0)\n\nfind_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})\nproject(test_rpi)\n\ntarget_sources(app PRIVATE src\/main.c src\/processing.c)\ntarget_sources_ifdef(CONFIG_BOARD_NATIVE_SIM app PRIVATE src\/emul.c)"},{"code":"\nconfig TEST_RPI_LOG_LEVEL\n\tint \"Default log level for test_rpi\"\n\tdefault 4\n\nsource \"Kconfig.zephyr\""},{"code":"\n    Main thread    Processing thread    Workqueue thread     GPIO ISR\n        |                  |                    |                |\n        |                  |                    |<--------------| |\n        |                  |<------------------| |           (1) |\n        |                 | |               (2) |                |\n        |<----------------| |                   |                |\n       | |             (3) |                    |                |\n        |                  |                    |                |"},{"code":"\n#include <zephyr\/kernel.h>\n\n#define THREAD_STACKSIZE\t2048\n#define THREAD_PRIORITY\t\t10\n\nK_THREAD_STACK_DEFINE(processing_stack, THREAD_STACKSIZE);\nstruct k_thread processing_thread;\n\nint main(void)\n{\n\t[...]\n\n\t\/* Thread initialization *\/\n\tk_thread_create(&processing_thread, processing_stack,\n\t\t\tTHREAD_STACKSIZE, data_process,\n\t\t\t&in_msgq, &out_msgq, NULL,\n\t\t\tTHREAD_PRIORITY, 0, K_FOREVER);\n\tk_thread_name_set(&processing_thread, \"processing\");\n\tk_thread_start(&processing_thread);"},{"code":"\n#define ZEPHYR_USER_NODE DT_PATH(zephyr_user)\nconst struct gpio_dt_spec button = GPIO_DT_SPEC_GET_OR(\n\tZEPHYR_USER_NODE, button_gpios, {0});\n"},{"code":"\n\/ {\n\n[...]\n\n\tzephyr,user {\n\t\tbutton-gpios = <&gpio0 2 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;\n\t};\n};"},{"code":"\nif (!gpio_is_ready_dt(&button)) {\n\tLOG_ERR(\"Error: button device %s is not ready\",\n\t       button.port->name);\n\treturn 0;\n}\nret = gpio_pin_configure_dt(&button, GPIO_INPUT);\nif (ret != 0) {\n\tLOG_ERR(\"Error %d: failed to configure %s pin %d\",\n\t       ret, button.port->name, button.pin);\n\treturn 0;\n}\nret = gpio_pin_interrupt_configure_dt(&button,\n                                      GPIO_INT_EDGE_TO_ACTIVE);\nif (ret != 0) {\n\tLOG_ERR(\"Error %d: failed to configure interrupt on %s pin %d\",\n\t\tret, button.port->name, button.pin);\n\treturn 0;\n}\ngpio_init_callback(&button_cb_data, button_pressed, BIT(button.pin));\ngpio_add_callback(button.port, &button_cb_data);"},{"code":"\n\/*\n * Deferred irq work triggered by the GPIO IRQ callback\n * (button_pressed). This should run some time after the ISR, at which\n * point the button press should be stable after the initial bouncing.\n *\n * Checks the button status and sends the current system uptime in\n * seconds through in_msgq if the the button is still pressed.\n *\/\nstatic void debounce_expired(struct k_work *work)\n{\n\tunsigned int data = k_uptime_seconds();\n\tARG_UNUSED(work);\n\n\tif (gpio_pin_get_dt(&button))\n\t\tk_msgq_put(&in_msgq, &data, K_NO_WAIT);\n}\n\nstatic K_WORK_DELAYABLE_DEFINE(debounce_work, debounce_expired);\n\n\/*\n * Callback function for the button GPIO IRQ.\n * De-bounces the button press by scheduling the processing into a\n * workqueue.\n *\/\nvoid button_pressed(const struct device *dev, struct gpio_callback *cb,\n\t\t    uint32_t pins)\n{\n\tk_work_reschedule(&debounce_work, K_MSEC(30));\n}"},{"code":"\n#define PROC_MSG_SIZE\t\t8\n\nK_MSGQ_DEFINE(in_msgq, sizeof(int), 1, 1);\nK_MSGQ_DEFINE(out_msgq, PROC_MSG_SIZE, 1, 1);"},{"code":"\nstatic char data_out[PROC_MSG_SIZE];\n\n\/*\n * Receives a message on the message queue passed in p1, does some\n * processing on the data received and sends a response on the message\n * queue passed in p2.\n *\/\nvoid data_process(void *p1, void *p2, void *p3)\n{\n\tstruct k_msgq *inq = p1;\n\tstruct k_msgq *outq = p2;\n\tARG_UNUSED(p3);\n\n\twhile (1) {\n\t\tunsigned int data;\n\n\t\tk_msgq_get(inq, &data, K_FOREVER);\n\t\tLOG_DBG(\"Received: %d\", data);\n\n\t\t\/* Data processing: convert integer to string *\/\n\t\tsnprintf(data_out, sizeof(data_out), \"%d\", data);\n\n\t\tk_msgq_put(outq, data_out, K_NO_WAIT);\n\t}\n}"},{"code":"\n\/ {\n\t[...]\n\n\taliases {\n                i2ctarget = &i2c0;\n\t};"},{"code":"\ni2c0: i2c@40090000 {\n\tcompatible = \"raspberrypi,pico-i2c\", \"snps,designware-i2c\";\n\t#address-cells = <1>;\n\t#size-cells = <0>;\n\treg = <0x40090000 DT_SIZE_K(4)>;\n\tresets = <&reset RPI_PICO_RESETS_RESET_I2C0>;\n\tclocks = <&clocks RPI_PICO_CLKID_CLK_SYS>;\n\tinterrupts = <36 RPI_PICO_DEFAULT_IRQ_PRIORITY>;\n\tinterrupt-names = \"i2c0\";\n\tstatus = \"disabled\";\n};"},{"code":"\n&i2c0 {\n\tclock-frequency = <I2C_BITRATE_STANDARD>;\n\tstatus = \"okay\";\n\tpinctrl-0 = <&i2c0_default>;\n\tpinctrl-names = \"default\";\n};"},{"code":"\n\/*\n * Get I2C device configuration from the devicetree i2ctarget alias.\n * Check node availability at buid time.\n *\/\n#define I2C_NODE\tDT_ALIAS(i2ctarget)\n#if !DT_NODE_HAS_STATUS_OKAY(I2C_NODE)\n#error \"Unsupported board: i2ctarget devicetree alias is not defined\"\n#endif\nconst struct device *i2c_target = DEVICE_DT_GET(I2C_NODE);"},{"code":"\n#define I2C_ADDR\t\t0x60\n\n[...]\n\nstatic struct i2c_target_callbacks target_callbacks = {\n\t.write_requested = write_requested_cb,\n\t.write_received = write_received_cb,\n\t.read_requested = read_requested_cb,\n\t.read_processed = read_processed_cb,\n\t.stop = stop_cb,\n};\n\n[...]\n\nint main(void)\n{\n\tstruct i2c_target_config target_cfg = {\n\t\t.address = I2C_ADDR,\n\t\t.callbacks = &target_callbacks,\n\t};\n\n\tif (i2c_target_register(i2c_target, &target_cfg) < 0) {\n\t\tLOG_ERR(\"Failed to register target\");\n\t\treturn -1;\n\t}"},{"code":"\n# Scan the I2C bus:\n$ i2cdetect -y 0\n     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f\n00:                         -- -- -- -- -- -- -- -- \n10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- \n20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- \n30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- \n40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- \n50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- \n60: 60 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- \n70: -- -- -- -- -- -- -- --\n\n# I2C bus 0: issue command 0 (read uptime) on device 0x60:\n# - Send byte 0 to device with address 0x60\n# - Read back 8 bytes\n$ i2ctransfer -y 0 w1@0x60 0 r8\n0x36 0x33 0x00 0x00 0x00 0x00 0x00 0x00\n"},{"code":"\ntypedef enum {\n\tI2C_REG_UPTIME,\n\tI2C_REG_NOT_SUPPORTED,\n\n\tI2C_REG_DEFAULT = I2C_REG_UPTIME\n} i2c_register_t;\n\n\/* I2C data structures *\/\nstatic char i2cbuffer[PROC_MSG_SIZE];\nstatic int i2cidx = -1;\nstatic i2c_register_t i2creg = I2C_REG_DEFAULT;\n\n[...]\n\n\/*\n * Callback called on a write request from the controller.\n *\/\nint write_requested_cb(struct i2c_target_config *config)\n{\n\tLOG_DBG(\"I2C WRITE start\");\n\treturn 0;\n}\n\n\/*\n * Callback called when a byte was received on an ongoing write request\n * from the controller.\n *\/\nint write_received_cb(struct i2c_target_config *config, uint8_t val)\n{\n\tLOG_DBG(\"I2C WRITE: 0x%02x\", val);\n\ti2creg = val;\n\tif (val == I2C_REG_UPTIME)\n\t\ti2cidx = -1;\n\n\treturn 0;\n}\n\n\/*\n * Callback called on a read request from the controller.\n * If it's a first read, load the output buffer contents from the\n * current contents of the source data buffer (str_data).\n *\n * The data byte sent to the controller is pointed to by val.\n * Returns:\n *   0 if there's additional data to send\n *   -ENOMEM if the byte sent is the end of the data transfer\n *   -EIO if the selected register isn't supported\n *\/\nint read_requested_cb(struct i2c_target_config *config, uint8_t *val)\n{\n\tif (i2creg != I2C_REG_UPTIME)\n\t\treturn -EIO;\n\n\tLOG_DBG(\"I2C READ started. i2cidx: %d\", i2cidx);\n\tif (i2cidx < 0) {\n\t\t\/* Copy source buffer to the i2c output buffer *\/\n\t\tk_mutex_lock(&str_data_mutex, K_FOREVER);\n\t\tstrncpy(i2cbuffer, str_data, PROC_MSG_SIZE);\n\t\tk_mutex_unlock(&str_data_mutex);\n\t}\n\ti2cidx++;\n\tif (i2cidx == PROC_MSG_SIZE) {\n\t\ti2cidx = -1;\n\t\treturn -ENOMEM;\n\t}\n\t*val = i2cbuffer[i2cidx];\n\tLOG_DBG(\"I2C READ send: 0x%02x\", *val);\n\n\treturn 0;\n}\n\n\/*\n * Callback called on a continued read request from the\n * controller. We're implementing repeated start semantics, so this will\n * always return -ENOMEM to signal that a new START request is needed.\n *\/\nint read_processed_cb(struct i2c_target_config *config, uint8_t *val)\n{\n\tLOG_DBG(\"I2C READ continued\");\n\treturn -ENOMEM;\n}\n\n\/*\n * Callback called on a stop request from the controller. Rewinds the\n * index of the i2c data buffer to prepare for the next send.\n *\/\nint stop_cb(struct i2c_target_config *config)\n{\n\ti2cidx = -1;\n\tLOG_DBG(\"I2C STOP\");\n\treturn 0;\n}"},{"code":"\nint main(void)\n{\n\t[...]\n\n\twhile (1) {\n\t\tchar buffer[PROC_MSG_SIZE];\n\n\t\tk_msgq_get(&out_msgq, buffer, K_FOREVER);\n\t\tLOG_DBG(\"Received: %s\", buffer);\n\t\tk_mutex_lock(&str_data_mutex, K_FOREVER);\n\t\tstrncpy(str_data, buffer, PROC_MSG_SIZE);\n\t\tk_mutex_unlock(&str_data_mutex);\n\t}"},{"code":"\ngpio0: gpio_emul {\n\tstatus = \"okay\";\n\tcompatible = \"zephyr,gpio-emul\";\n\trising-edge;\n\tfalling-edge;\n\thigh-level;\n\tlow-level;\n\tgpio-controller;\n\t#gpio-cells = <2>;\n};"},{"code":"\n\/ {\n\t[...]\n\n\tzephyr,user {\n\t\tbutton-gpios = <&gpio0 0 GPIO_ACTIVE_HIGH>;\n\t};\n};"},{"code":"\n\/*\n * Emulates a button press with bouncing.\n *\/\nstatic void button_press(void)\n{\n\tconst struct device *dev = device_get_binding(button.port->name);\n\tint n_bounces = sys_rand8_get() % 10;\n\tint state = 1;\n\tint i;\n\n\t\/* Press *\/\n\tgpio_emul_input_set(dev, 0, state);\n\t\/* Bouncing *\/\n\tfor (i = 0; i < n_bounces; i++) {\n\t\tstate = state ? 0: 1;\n\t\tk_busy_wait(1000 * (sys_rand8_get() % 10));\n\t\tgpio_emul_input_set(dev, 0, state);\n\t}\n\t\/* Stabilization *\/\n\tgpio_emul_input_set(dev, 0, 1);\n\tk_busy_wait(100000);\n\t\/* Release *\/\n\tgpio_emul_input_set(dev, 0, 0);\n}"},{"code":"\ni2c0: i2c@100 {\n\tstatus = \"okay\";\n\tcompatible = \"zephyr,i2c-emul-controller\";\n\tclock-frequency = <I2C_BITRATE_STANDARD>;\n\t#address-cells = <1>;\n\t#size-cells = <0>;\n\t#forward-cells = <1>;\n\treg = <0x100 4>;\n};"},{"code":"\n\/ {\n\taliases {\n\t\ti2ctarget = &i2c0;\n\t};"},{"code":"\n\/*\n * A real controller may want to continue reading after the first\n * received byte. We're implementing repeated-start semantics so we'll\n * only be sending one byte per transfer, but we need to allocate space\n * for an extra byte to process the possible additional read request.\n *\/\nstatic uint8_t emul_read_buf[2];\n\n\/*\n * Emulates a single I2C READ START request from a controller.\n *\/\nstatic uint8_t *i2c_emul_read(void)\n{\n\tstruct i2c_msg msg;\n\tint ret;\n\n\tmsg.buf = emul_read_buf;\n\tmsg.len = sizeof(emul_read_buf);\n\tmsg.flags = I2C_MSG_RESTART | I2C_MSG_READ;\n\tret = i2c_transfer(i2c_target, &msg, 1, I2C_ADDR);\n\tif (ret == -EIO)\n\t\treturn NULL;\n\n\treturn emul_read_buf;\n}\n\nstatic void i2c_emul_write(uint8_t *data, int len)\n{\n\tstruct i2c_msg msg;\n\n\t\/*\n\t * NOTE: It's not explicitly said anywhere that msg.buf can be\n\t * NULL even if msg.len is 0. The behavior may be\n\t * driver-specific and prone to change so we're being safe here\n\t * by using a 1-byte buffer.\n\t *\/\n\tmsg.buf = data;\n\tmsg.len = len;\n\tmsg.flags = I2C_MSG_WRITE;\n\ti2c_transfer(i2c_target, &msg, 1, I2C_ADDR);\n}\n\n\/*\n * Emulates an explicit I2C STOP sent from a controller.\n *\/\nstatic void i2c_emul_stop(void)\n{\n\tstruct i2c_msg msg;\n\tuint8_t buf = 0;\n\n\t\/*\n\t * NOTE: It's not explicitly said anywhere that msg.buf can be\n\t * NULL even if msg.len is 0. The behavior may be\n\t * driver-specific and prone to change so we're being safe here\n\t * by using a 1-byte buffer.\n\t *\/\n\tmsg.buf = &buf;\n\tmsg.len = 0;\n\tmsg.flags = I2C_MSG_WRITE | I2C_MSG_STOP;\n\ti2c_transfer(i2c_target, &msg, 1, I2C_ADDR);\n}"},{"code":"\n\/*\n * Emulates an I2C \"UPTIME\" command request from a controller using\n * repeated start.\n *\/\nstatic void i2c_emul_uptime(const struct shell *sh, size_t argc, char **argv)\n{\n\tuint8_t buffer[PROC_MSG_SIZE] = {0};\n\ti2c_register_t reg = I2C_REG_UPTIME;\n\tint i;\n\n\ti2c_emul_write((uint8_t *)&reg, 1);\n\tfor (i = 0; i < PROC_MSG_SIZE; i++) {\n\t\tuint8_t *b = i2c_emul_read();\n\t\tif (b == NULL)\n\t\t\tbreak;\n\t\tbuffer[i] = *b;\n\t}\n\ti2c_emul_stop();\n\n\tif (i == PROC_MSG_SIZE) {\n\t\tshell_print(sh, \"%s\", buffer);\n\t} else {\n\t\tshell_print(sh, \"Transfer error\");\n\t}\n}"},{"code":"\nSHELL_CMD_REGISTER(buttonpress, NULL, \"Simulates a button press\", button_press);\nSHELL_CMD_REGISTER(i2cread, NULL, \"Simulates an I2C read request\", i2c_emul_read);\nSHELL_CMD_REGISTER(i2cuptime, NULL, \"Simulates an I2C uptime request\", i2c_emul_uptime);\nSHELL_CMD_REGISTER(i2cstop, NULL, \"Simulates an I2C stop request\", i2c_emul_stop);"},{"code":"\n$ .\/build\/zephyr\/zephyr.exe\nWARNING: Using a test - not safe - entropy source\nuart connected to pseudotty: \/dev\/pts\/16\n*** Booting Zephyr OS build v4.1.0-6569-gf4a0beb2b7b1 ***\n\n# In another terminal\n$ screen \/dev\/pts\/16\n\nuart:~$\nuart:~$ help\nPlease press the <Tab> button to see all available commands.\nYou can also use the <Tab> button to prompt or auto-complete all commands or its subcommands.\nYou can try to call commands with <-h> or <--help> parameter for more information.\n\nShell supports following meta-keys:\n  Ctrl + (a key from: abcdefklnpuw)\n  Alt  + (a key from: bf)\nPlease refer to shell documentation for more details.\n\nAvailable commands:\n  buttonpress  : Simulates a button press\n  clear        : Clear screen.\n  device       : Device commands\n  devmem       : Read\/write physical memory\n                 Usage:\n                 Read memory at address with optional width:\n                 devmem <address> [<width>]\n                 Write memory at address with mandatory width and value:\n                 devmem <address> <width> <value>\n  help         : Prints the help message.\n  history      : Command history.\n  i2cread      : Simulates an I2C read request\n  i2cstop      : Simulates an I2C stop request\n  i2cuptime    : Simulates an I2C uptime request\n  kernel       : Kernel commands\n  rem          : Ignore lines beginning with 'rem '\n  resize       : Console gets terminal screen size or assumes default in case\n                 the readout fails. It must be executed after each terminal\n                 width change to ensure correct text display.\n  retval       : Print return value of most recent command\n  shell        : Useful, not Unix-like shell commands."},{"code":"\nuart:~$ buttonpress"},{"code":"\n[00:00:06.300,000] <dbg> test_rpi: data_process: Received: 6\n[00:00:06.300,000] <dbg> test_rpi: main: Received: 6"},{"code":"\nuart:~$ i2cuptime \n6"},{"code":"\n[00:01:29.400,000] <dbg> test_rpi: write_requested_cb: I2C WRITE start\n[00:01:29.400,000] <dbg> test_rpi: write_received_cb: I2C WRITE: 0x00\n[00:01:29.400,000] <dbg> test_rpi: stop_cb: I2C STOP\n[00:01:29.400,000] <dbg> test_rpi: read_requested_cb: I2C READ started. i2cidx: -1\n[00:01:29.400,000] <dbg> test_rpi: read_requested_cb: I2C READ send: 0x36\n[00:01:29.400,000] <dbg> test_rpi: read_processed_cb: I2C READ continued\n[00:01:29.400,000] <dbg> test_rpi: read_requested_cb: I2C READ started. i2cidx: 0\n[00:01:29.400,000] <dbg> test_rpi: read_requested_cb: I2C READ send: 0x00\n[00:01:29.400,000] <dbg> test_rpi: read_processed_cb: I2C READ continued\n[00:01:29.400,000] <dbg> test_rpi: read_requested_cb: I2C READ started. i2cidx: 1\n[00:01:29.400,000] <dbg> test_rpi: read_requested_cb: I2C READ send: 0x00\n[00:01:29.400,000] <dbg> test_rpi: read_processed_cb: I2C READ continued\n[00:01:29.400,000] <dbg> test_rpi: read_requested_cb: I2C READ started. i2cidx: 2\n[00:01:29.400,000] <dbg> test_rpi: read_requested_cb: I2C READ send: 0x00\n[00:01:29.400,000] <dbg> test_rpi: read_processed_cb: I2C READ continued\n[00:01:29.400,000] <dbg> test_rpi: read_requested_cb: I2C READ started. i2cidx: 3\n[00:01:29.400,000] <dbg> test_rpi: read_requested_cb: I2C READ send: 0x00\n[00:01:29.400,000] <dbg> test_rpi: read_processed_cb: I2C READ continued\n[00:01:29.400,000] <dbg> test_rpi: read_requested_cb: I2C READ started. i2cidx: 4\n[00:01:29.400,000] <dbg> test_rpi: read_requested_cb: I2C READ send: 0x00\n[00:01:29.400,000] <dbg> test_rpi: read_processed_cb: I2C READ continued\n[00:01:29.400,000] <dbg> test_rpi: read_requested_cb: I2C READ started. i2cidx: 5\n[00:01:29.400,000] <dbg> test_rpi: read_requested_cb: I2C READ send: 0x00\n[00:01:29.400,000] <dbg> test_rpi: read_processed_cb: I2C READ continued\n[00:01:29.400,000] <dbg> test_rpi: read_requested_cb: I2C READ started. i2cidx: 6\n[00:01:29.400,000] <dbg> test_rpi: read_requested_cb: I2C READ send: 0x00\n[00:01:29.400,000] <dbg> test_rpi: read_processed_cb: I2C READ continued\n[00:01:29.400,000] <dbg> test_rpi: stop_cb: I2C STOP"},{"code":"\ndtparam=i2c_arm=on"},{"code":"\ncd kernel_dir\nKERNEL=kernel\nmake ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- bcmrpi_defconfig"},{"code":"\nCONFIG_EXT4_FS=y\nCONFIG_I2C=y"},{"code":"\nmake ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- olddefconfig\nmake ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- zImage modules dtbs -j$(nproc)\nmkdir modules\nmake ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- INSTALL_MOD_PATH=.\/modules modules_install"},{"code":"\ncp arch\/arm\/boot\/zImage \/path_to_boot_partition_mountpoint\/kernel.img\ncp arch\/arm\/boot\/dts\/broadcom\/*.dtb \/path_to_boot_partition_mountpoint\nmkdir \/path_to_boot_partition_mountpoint\/overlays\ncp arch\/arm\/boot\/dts\/overlays\/*.dtb \/path_to_boot_partition_mountpoint\/overlays\n"},{"code":"\nmkdir bookworm_armel_raspi\nsudo debootstrap --arch armel --include=openssh-server bookworm \\\n        bookworm_armel_raspi http:\/\/deb.debian.org\/debian\n\n# Remove the root password\nsudo sed -i '\/^root\/ { s\/:x:\/::\/ }' bookworm_armel_raspi\/etc\/passwd\n\n# Create a pair of ssh keys and install them to allow passwordless\n# ssh logins\ncd ~\/.ssh\nssh-keygen -f raspi\nsudo mkdir bookworm_armel_raspi\/root\/.ssh\ncat raspi.pub | sudo tee bookworm_armel_raspi\/root\/.ssh\/authorized_keys\n"},{"code":"\ncd kernel_dir\nsudo cp -fr modules\/lib\/modules \/path_to_rootfs_mountpoint\/lib\n"},{"code":"\nsudo cp \/usr\/bin\/qemu-arm-static bookworm_armel_raspi\/usr\/bin\n"},{"code":"\n# Make rootfs image\nfallocate -l 2G bookworm_armel_raspi.img\nsudo mkfs -t ext4 bookworm_armel_raspi.img\nsudo mkdir \/mnt\/rootfs\nsudo mount -o loop bookworm_armel_raspi.img \/mnt\/rootfs\/\nsudo cp -a bookworm_armel_raspi\/* \/mnt\/rootfs\/\nsudo umount \/mnt\/rootfs\n"},{"code":"\nsudo dd if=bookworm_armel_raspi.img of=\/dev\/sda2 bs=4M"},{"code":"\n[Match]\nName=en*\n\n[Network]\nAddress=192.168.2.101\/24\nGateway=192.168.2.100\nDNS=1.1.1.1"},{"code":"\nsystemctl enable systemd-networkd"},{"code":"\nsudo sysctl -w net.ipv4.ip_forward=1\nsudo pptables -t nat -A POSTROUTING -o <wlan_interface> -j MASQUERADE"},{"code":"\nssh -i ~\/.ssh\/raspi root@192.168.2.101"},{"code":"\napt-get install i2c-tools\necho \"ic2-dev\" >> \/etc\/modules"}],"div":[{"@attributes":{"align":"center"},"br":{},"a":{"@attributes":{"href":"https:\/\/datasheets.raspberrypi.com\/picow\/pico-2-w-datasheet.pdf"},"img":{"@attributes":{"src":"https:\/\/blogs.igalia.com\/rcn\/posts\/20250813-first_steps_with_zephyr_ii\/Raspberry-Pi-Pico-2-W-Pinout.webp"}}}},{"@attributes":{"class":"footnotes"},"p":["1: Although in this case the thread is a regular\n          kernel thread and runs on the same memory space as the rest of\n          the code, so there's no memory protection. See\n          the  page in the docs for more\n          details.","2: As a reference, for the Raspberry Pi Pico 2W,\n            \n            is where the ISR is registered\n            for \n            GPIO devices,\n            and \n            is the ISR that checks the pin status and triggers the\n            registered callbacks.","3:  in my setup."]}],"h4":["Emulating a button press","Emulating an IC controller","Building a Linux kernel for the Raspberry Pi","Setting up a Debian rootfs"],"comment":{}},"pubDate":"Wed, 13 Aug 2025 14:00:00 GMT+1"},{"title":"First steps with Zephyr (III)","link":"https:\/\/blogs.igalia.com\/rcn\/posts\/20250904-first_steps_with_zephyr_iii\/index.html","author":{"name":"rcn"},"description":{"p":["In previous installments of this post series about Zephyr we\n        had\n        an , and then we went through\n        a  that showcased some of its features. If\n        you didn't read those, I heartily recommend you go through them\n        before continuing with this one. If you did already, welcome\n        back. In this post we'll see how to add support for a new device\n        in Zephyr.","As we've been doing so far, we'll use a Raspberry Pi Pico 2W\n        for our experiments. As of today (September 2nd, 2025), most of\n        the devices in the RP2350\n        SoC ,\n        but . One of them is the\n        inter-processor mailbox that allows both ARM Cortex-M33\n        cores to communicate\n        and synchronize with each other. This opens some interesting\n        possibilities, since the SoC contains two cores but only one is\n        supported in Zephyr due to the architectural characteristics of\n        this type of SoC. It'd\n        be nice to be able to use that second core for other things: a\n        bare-metal application, a second Zephyr instance or something\n        else, and the way to start the second core involves the use of\n        the inter-processor mailbox.","Throughout the post we will reference our main source material\n        for this task:\n        the , so make sure to keep it at hand.","The processor subsystem block in the RP2350 contains a\n        Single-cycle IO subsystem (SIO) that defines a set of\n        peripherals that require low-latency and deterministic access\n        from the processors. One of these peripherals is a pair of\n        inter-processor FIFOs that allow passing data, messages or\n        events between the two cores (section 3.1.5 in [1]).","The implementation and programmer's model for these is very\n          simple:","That's basically it. The mailbox writing, reading, setup\n        and status check are done through an also simple register\n        interface that's thoroughly described in sections 3.1.5 and\n        3.1.11 of the datasheet.","The typical use case scenario of this peripheral may be an\n        application distributed in two different computing entities (one\n        in each core) cooperating and communicating with each other: one\n        core running the main application logic in an OS while the other\n        performs computations triggered and specified by the former. For\n        instance, a modem\/bridge device that runs the PHY logic in one\n        core and a bridge loop in the other as a bare metal program,\n        piping packets between network interfaces and a shared\n        memory. The mailbox is one of the peripherals that make it\n        possible for these independent cores to talk to each other.","But, as I mentioned earlier, in the RP2350 the mailbox has\n        another key use case: after reset, Core 1 remains asleep until\n        woken by Core 0. The process to wake up and run Core 1 involves\n        both cores going through a state machine coordinated by passing\n        messages over the mailbox (see [1], section 5.3).","Zephyr has more than one API that fits this type of hardware:\n        there's\n        the , which models a generic multi-channel mailbox that\n        can be used for signalling and messaging, and\n        the , which seems a bit more specific and higher-level,\n        in the sense that it provides an API that's further away from\n        the hardware. For this particular case, our driver could use\n        either of these, but, as an exercise, I'm choosing to use the\n        generic MBOX interface, which we can then use as a backend for\n        the \n        driver (a thin IPM API wrapper over an MBOX driver) so we\n        can use the peripheral with the IPM API for free. This is also a\n        simple example of driver composition.","The MBOX API defines functions to send a message, configure\n        the device and check its status, register a callback handler for\n        incoming messages and get the number of channels. That's\n        what we need to implement, but first let's start with the basic\n        foundation for the driver: defining the hardware.","As we know, Zephyr uses device tree definitions extensively\n        to configure the hardware and to query hardware details and\n        parameters from the drivers, so the first thing we'll do is to\n        model the peripheral\n        into .","In this case, the mailbox peripheral is part of the SIO\n        block, which isn't defined in the device tree, so we'll start by\n        adding this block as a placeholder for the mailbox and leave it\n        there in case anyone needs to add support for any of the other\n        SIO peripherals in the future. We only need to define its\n        address range mapping according to the info in the datasheet:","We also need to define a minimal device tree binding for it,\n          which can be extended later as needed\n          ():","Now we can define the mailbox as a peripheral inside the SIO\n        block. We'll create a device tree binding for it that will be\n        based on\n        the  and that we can extend as needed. To define the\n        mailbox device, we only need to specify the IRQ number it\n        uses, a name for the interrupt and the number of \"items\"\n        (channels) to expect in a mailbox specifier, ie. when we\n        reference the device in another part of the device tree through\n        a phandle. In this case we won't need any channel\n        specification, since a CPU core only handles one mailbox\n        channel:","The binding looks like this:","Now that we have defined the hardware in the device tree, we\n        can start writing the driver. We'll put the source code next to\n        the rest of the mailbox drivers,\n        in , and we'll create a\n        Kconfig file for it ()\n        to define a custom config option that will let us enable or\n          disable the driver in our firmware build:","Now, to make the build system aware of our driver, we need to\n        add it to the appropriate  file\n          ():","And source our new Kconfig file in the :","Finally, we're ready to write the driver. The work here can\n        basically be divided into three parts: the driver structure setup\n        according to the MBOX API, the scaffolding needed to have our\n        driver correctly plugged into the device tree definitions by the\n        build system (according to\n        the ), and the actual interfacing with the\n        hardware. We'll skip over most of the hardware-specific details,\n        though, and focus on the driver structure.","First, we will create a device object using one of the macros\n        of\n        the . There are many ways to do this, but, in rough\n        terms, what these macros do is to create the object from a\n        device tree node identifier and set it up for boot time\n        initialization. As part of the object attributes, we provide\n        things like an init function, a pointer to the device's private\n        data if needed, the device initialization level and a pointer to\n        the device's API structure. It's fairly common to\n        use \n        for this and loop over the different instances of the device in\n        the SoC with a macro\n        like ,\n        so we'll use it here as well, even if we have only one instance\n        to initialize:","Note that this\n        macro \n        specifying the driver's compatible string with\n          the  macro:","In the device's API struct, we define the functions the driver\n          will use to implement the API primitives. In this case:","The init function, ,\n        referenced in the  macro\n        call above, simply needs to set the device in a known state and\n        initialize the interrupt handler appropriately (but we're not\n          enabling interrupts yet):","Where  is the interrupt\n        handler.","The implementation of the MBOX API functions is really\n        simple. For the  function, we need to check\n        that the FIFO isn't full, that the message to send has the\n          appropriate size and then write it in the FIFO:","Note that the API lets us pass a \n          parameter to the call, but we don't need it.","The  and \n        calls are trivial: for the first one we simply need to return\n        the maximum message size we can write to the FIFO (4 bytes), for\n          the second we'll always return 1 channel:","The function to implement the  call\n          will just enable or disable the mailbox interrupt depending on\n          a parameter:","Finally, the function for the \n        call will store a pointer to a callback function for processing\n        incoming messages in the device's private data struct:","Once interrupts are enabled, the interrupt handler will call\n        that callback every time this core receives anything from the\n          other one:","The  functions scattered over the code\n        are helper functions that access the memory-mapped device\n          registers. This is, of course, completely\n        hardware-specific. For example:","Done, we should now be able to build and use the driver\n        if we enable the  config option in our\n          firmware build.","As I mentioned earlier, Zephyr provides a more convenient API\n        for inter-processor messaging based on this type of\n        devices. Fortunately, one of the drivers that implement that API\n        is a generic wrapper over an MBOX API driver like this one, so we\n        can use our driver as a backend for\n        the  driver simply by adding a new\n        device to the device tree:","This defines an IPM device that takes two existing mailbox channels\n        and uses them for receiving and sending data. Note that, since\n        our mailbox only has one channel from the point of view of each\n        core, both \"rx\" and \"tx\" channels point to the same mailbox,\n          which implements the send and receive primitives appropriately.","If we did everything right, now we should be able to signal\n        events and send data from one core to another. That'd require\n        both cores to be running, and, at boot time, only Core 0 is. So\n        let's see if we can get Core 1 to run, which is, in fact, the\n          most basic test of the mailbox we can do.","To do that in the easiest way possible, we can go back to the\n        most basic sample program there is,\n        the , which, in this board, should print a periodic\n        message through the UART:","To wake up Core 1, we need to send a sequence of inputs from\n        Core 0 using the mailbox and check at each step in the sequence\n        that Core 1 received and acknowledged the data by sending it\n          back. The data we need to send is (all 4-byte words):","in that order.","To send the data from Core 0 we need to instantiate an IPM\n        device, which we'll define in the device-tree first as an alias\n        for the IPM node we created before:","Once we enable the IPM driver in the firmware configuration\n        (), we can use the device like\n        this:","To send data we use , to receive data\n        we'll register a callback that will be called every time Core 1\n        sends anything. In order to process the sequence handshake one\n        step at a time we can use a message queue to send the received\n        data from the IPM callback to the main thread:","The last elements to add are the actual Core 1 code, as well\n        as its stack and vector table. For the code, we can use a basic\n          infinite loop that will send a message to Core 0 every now and\n        then:","For the stack, we can just allocate a chunk of memory (it\n        won't be used anyway) and for the vector table we can do the\n        same and use an empty dummy table (because it won't be used\n        either):","And the code to handle the handshake would look like\n        this:","You can find the complete\n        example .","So, finally we can build the example and check if Core 1\n          comes to life:","Here's the UART output:","That's it! We just added support for a new device and we\n        \"unlocked\" a new functionality for this board. I'll probably\n        take a break from Zephyr experiments for a while, so I don't\n        know if there'll be a part IV of this series anytime soon. In\n          any case, I hope you enjoyed it and found it useful. Happy\n        hacking!"],"h3":["The inter-processor mailbox peripheral","Inter-processor mailbox support in Zephyr","Hardware definition","Driver set up and code","Using the driver as an IPM backend","Testing the driver","References"],"ul":[{"li":["\n            A mailbox is a pair of FIFOs that are 32 bits wide and\n            four entries deep.\n          ","\n            One of the FIFOs can only be written by Core\n            0 and read by Core 1; the other can only be written by Core 1\n            and read by Core 0.\n          ","\n            The SIO block has an IRQ output for each core to notify the\n            core that it has received data in its FIFO. This interrupt\n            is mapped to the same IRQ number (25) on each core.\n          ","\n            A core can write to its outgoing FIFO as long as it's\n            not full.\n          "]},{"li":["0.","0.","1.","A pointer to the vector table for Core 1.","Core 1 stack pointer.","Core 1 initial program counter (ie. a pointer to its entry function)."]},{"li":"\n            [1]: \n          "}],"b":"NOTE:","a":"mailbox\n        objects in the kernel","pre":[{"code":"\nsio: sio@d0000000 {\n\tcompatible = \"raspberrypi,pico-sio\";\n\treg = <0xd0000000 DT_SIZE_K(80)>;\n};"},{"code":"\ndescription: Raspberry Pi Pico SIO\n\ncompatible: \"raspberrypi,pico-sio\"\n\ninclude: base.yaml"},{"code":"\nsio: sio@d0000000 {\n\tcompatible = \"raspberrypi,pico-sio\";\n\treg = <0xd0000000 DT_SIZE_K(80)>;\n\n\tmbox: mbox {\n\t\tcompatible = \"raspberrypi,pico-mbox\";\n\t\tinterrupts = <25 RPI_PICO_DEFAULT_IRQ_PRIORITY>;\n\t\tinterrupt-names = \"mbox0\";\n\t\tfifo-depth = <4>;\n\t\t#mbox-cells = <0>;\n\t\tstatus = \"okay\";\n\t};\n};"},{"code":"\ndescription: Raspberry Pi Pico interprocessor mailbox\n\ncompatible: \"raspberrypi,pico-mbox\"\n\ninclude: [base.yaml, mailbox-controller.yaml]\n\nproperties:\n  fifo-depth:\n    type: int\n    description: number of entries that the mailbox FIFO can hold\n    required: true"},{"code":"\nconfig MBOX_RPI_PICO\n\tbool \"Inter-processor mailbox driver for the RP2350\/RP2040 SoCs\"\n\tdefault y\n\tdepends on DT_HAS_RASPBERRYPI_PICO_MBOX_ENABLED\n\thelp\n\t  Raspberry Pi Pico mailbox driver based on the RP2350\/RP2040\n\t  inter-processor FIFOs."},{"code":"\nzephyr_library_sources_ifdef(CONFIG_MBOX_RPI_PICO   mbox_rpi_pico.c)"},{"code":"\nsource \"drivers\/mbox\/Kconfig.rpi_pico\""},{"code":"\nDEVICE_DT_INST_DEFINE(\n\t0,\n\trpi_pico_mbox_init,\n\tNULL,\n\t&rpi_pico_mbox_data,\n\tNULL,\n\tPOST_KERNEL,\n\tCONFIG_MBOX_INIT_PRIORITY,\n\t&rpi_pico_mbox_driver_api);"},{"code":"\n#define DT_DRV_COMPAT raspberrypi_pico_mbox"},{"code":"\nstatic DEVICE_API(mbox, rpi_pico_mbox_driver_api) = {\n\t.send = rpi_pico_mbox_send,\n\t.register_callback = rpi_pico_mbox_register_callback,\n\t.mtu_get = rpi_pico_mbox_mtu_get,\n\t.max_channels_get = rpi_pico_mbox_max_channels_get,\n\t.set_enabled = rpi_pico_mbox_set_enabled,\n};"},{"code":"\n#define MAILBOX_DEV_NAME mbox0\n\nstatic int rpi_pico_mbox_init(const struct device *dev)\n{\n\tARG_UNUSED(dev);\n\n\tLOG_DBG(\"Initial FIFO status: 0x%x\", sio_hw->fifo_st);\n\tLOG_DBG(\"FIFO depth: %d\", DT_INST_PROP(0, fifo_depth));\n\n\t\/* Disable the device interrupt. *\/\n\tirq_disable(DT_INST_IRQ_BY_NAME(0, MAILBOX_DEV_NAME, irq));\n\n\t\/* Set the device in a stable state. *\/\n\tfifo_drain();\n\tfifo_clear_status();\n\tLOG_DBG(\"FIFO status after setup: 0x%x\", sio_hw->fifo_st);\n\n\t\/* Initialize the interrupt handler. *\/\n\tIRQ_CONNECT(DT_INST_IRQ_BY_NAME(0, MAILBOX_DEV_NAME, irq),\n\t\tDT_INST_IRQ_BY_NAME(0, MAILBOX_DEV_NAME, priority),\n\t\trpi_pico_mbox_isr, DEVICE_DT_INST_GET(0), 0);\n\n\treturn 0;\n}"},{"code":"\nstatic int rpi_pico_mbox_send(const struct device *dev,\n\t\t\tuint32_t channel, const struct mbox_msg *msg)\n{\n\tARG_UNUSED(dev);\n\tARG_UNUSED(channel);\n\n\tif (!fifo_write_ready()) {\n\t\treturn -EBUSY;\n\t}\n\tif (msg->size > MAILBOX_MBOX_SIZE) {\n\t\treturn -EMSGSIZE;\n\t}\n\tLOG_DBG(\"CPU %d: send IP data: %d\", sio_hw->cpuid, *((int *)msg->data));\n\tsio_hw->fifo_wr = *((uint32_t *)(msg->data));\n\tsev();\n\n\treturn 0;\n}"},{"code":"\n#define MAILBOX_MBOX_SIZE sizeof(uint32_t)\n\nstatic int rpi_pico_mbox_mtu_get(const struct device *dev)\n{\n\tARG_UNUSED(dev);\n\n\treturn MAILBOX_MBOX_SIZE;\n}\n\nstatic uint32_t rpi_pico_mbox_max_channels_get(const struct device *dev)\n{\n\tARG_UNUSED(dev);\n\n\t\/* Only one channel per CPU supported. *\/\n\treturn 1;\n}"},{"code":"\nstatic int rpi_pico_mbox_set_enabled(const struct device *dev,\n\t\t\t\tuint32_t channel, bool enable)\n{\n\tARG_UNUSED(dev);\n\tARG_UNUSED(channel);\n\n\tif (enable) {\n\t\tirq_enable(DT_INST_IRQ_BY_NAME(0, MAILBOX_DEV_NAME, irq));\n\t} else {\n\t\tirq_disable(DT_INST_IRQ_BY_NAME(0, MAILBOX_DEV_NAME, irq));\n\t}\n\n\treturn 0;\n}"},{"code":"\nstruct rpi_pico_mailbox_data {\n\tconst struct device *dev;\n\tmbox_callback_t cb;\n\tvoid *user_data;\n};\n\nstatic int rpi_pico_mbox_register_callback(const struct device *dev,\n\t\t\t\t\tuint32_t channel,\n\t\t\t\t\tmbox_callback_t cb,\n\t\t\t\t\tvoid *user_data)\n{\n\tARG_UNUSED(channel);\n\n\tstruct rpi_pico_mailbox_data *data = dev->data;\n\tuint32_t key;\n\n\tkey = irq_lock();\n\tdata->cb = cb;\n\tdata->user_data = user_data;\n\tirq_unlock(key);\n\n\treturn 0;\n}"},{"code":"\nstatic void rpi_pico_mbox_isr(const struct device *dev)\n{\n\tstruct rpi_pico_mailbox_data *data = dev->data;\n\n\t\/*\n\t * Ignore the interrupt if it was triggered by anything that's\n\t * not a FIFO receive event.\n\t *\n\t * NOTE: the interrupt seems to be triggered when it's first\n\t * enabled even when the FIFO is empty.\n\t *\/\n\tif (!fifo_read_valid()) {\n\t\tLOG_DBG(\"Interrupt received on empty FIFO: ignored.\");\n\t\treturn;\n\t}\n\n\tif (data->cb != NULL) {\n\t\tuint32_t d = sio_hw->fifo_rd;\n\t\tstruct mbox_msg msg = {\n\t\t\t.data = &d,\n\t\t\t.size = sizeof(d)};\n\t\tdata->cb(dev, 0, data->user_data, &msg);\n\t}\n\tfifo_drain();\n}"},{"code":"\n\/*\n * Returns true if the read FIFO has data available, ie. sent by the\n * other core. Returns false otherwise.\n *\/\nstatic inline bool fifo_read_valid(void)\n{\n\treturn sio_hw->fifo_st & SIO_FIFO_ST_VLD_BITS;\n}\n\n\/*\n * Discard any data in the read FIFO.\n *\/\nstatic inline void fifo_drain(void)\n{\n\twhile (fifo_read_valid()) {\n\t\t(void)sio_hw->fifo_rd;\n\t}\n}"},{"code":"\nipc: ipc {\n\tcompatible = \"zephyr,mbox-ipm\";\n\tmboxes = <&mbox>, <&mbox>;\n\tmbox-names = \"tx\", \"rx\";\n\tstatus = \"okay\";\n};"},{"code":"\n*** Booting Zephyr OS build v4.2.0-1643-g31c9e2ca8903 ***\nLED state: OFF\nLED state: ON\nLED state: OFF\nLED state: ON\n..."},{"code":"\n\/ {\n\tchosen {\n\t\tzephyr,ipc = &ipc;\n\t};"},{"code":"\nstatic const struct device *const ipm_handle =\n\tDEVICE_DT_GET(DT_CHOSEN(zephyr_ipc));\n\nint main(void)\n{\n\t...\n\n\tif (!device_is_ready(ipm_handle)) {\n\t\tprintf(\"IPM device is not ready\\n\");\n\t\treturn 0;\n\t}"},{"code":"\nK_MSGQ_DEFINE(ip_msgq, sizeof(int), 4, 1);\n\nstatic void platform_ipm_callback(const struct device *dev, void *context,\n\t\t\t\t  uint32_t id, volatile void *data)\n{\n\tprintf(\"Message received from mbox %d: 0x%0x\\n\", id, *(int *)data);\n\tk_msgq_put(&ip_msgq, (const void *)data, K_NO_WAIT);\n}\n\nint main(void)\n{\n\t...\n\n\tipm_register_callback(ipm_handle, platform_ipm_callback, NULL);\n\tret = ipm_set_enabled(ipm_handle, 1);\n\tif (ret) {\n\t\tprintf(\"ipm_set_enabled failed\\n\");\n\t\treturn 0;\n\t}"},{"code":"\nstatic inline void busy_wait(int loops)\n{\n\tint i;\n\n\tfor (i = 0; i < loops; i++)\n\t\t__asm__ volatile(\"nop\");\n}\n\n#include <hardware\/structs\/sio.h>\nstatic void core1_entry()\n{\n\tint i = 0;\n\n\twhile (1) {\n\t\tbusy_wait(20000000);\n\t\tsio_hw->fifo_wr = i++;\n\t}\n}"},{"code":"\n#define CORE1_STACK_SIZE 256\nchar core1_stack[CORE1_STACK_SIZE];\nuint32_t vector_table[16];"},{"code":"\nvoid start_core1(void)\n{\n\tuint32_t cmd[] = {\n\t\t0, 0, 1,\n\t\t(uintptr_t)vector_table,\n\t\t(uintptr_t)&core1_stack[CORE1_STACK_SIZE - 1],\n\t\t(uintptr_t)core1_entry};\n\n\tint i = 0;\n\twhile (i < sizeof(cmd) \/ sizeof(cmd[0])) {\n\t\tint recv;\n\n\t\tprintf(\"Sending to Core 1: 0x%0x (i = %d)\\n\", cmd[i], i);\n\t\tipm_send(ipm_handle, 0, 0, &cmd[i], sizeof (cmd[i]));\n\t\tk_msgq_get(&ip_msgq, &recv, K_FOREVER);\n\t\tprintf(\"Data received: 0x%0x\\n\", recv);\n\t\ti = cmd[i] == recv ? i + 1 : 0;\n\t}\n}"},{"code":"\nwest build -p always -b rpi_pico2\/rp2350a\/m33 zephyr\/samples\/basic\/blinky_two_cores\nwest flash -r uf2"},{"code":"\n*** Booting Zephyr OS build v4.2.0-1643-g31c9e2ca8903 ***\nSending to Core 1: 0x0 (i = 0)\nMessage received from mbox 0: 0x0\nData received: 0x0\nSending to Core 1: 0x0 (i = 1)\nMessage received from mbox 0: 0x0\nData received: 0x0\nSending to Core 1: 0x1 (i = 2)\nMessage received from mbox 0: 0x1\nData received: 0x1\nSending to Core 1: 0x20000220 (i = 3)\nMessage received from mbox 0: 0x20000220\nData received: 0x20000220\nSending to Core 1: 0x200003f7 (i = 4)\nMessage received from mbox 0: 0x200003f7\nData received: 0x200003f7\nSending to Core 1: 0x10000905 (i = 5)\nMessage received from mbox 0: 0x10000905\nData received: 0x10000905\nLED state: OFF\nMessage received from mbox 0: 0x0\nLED state: ON\nMessage received from mbox 0: 0x1\nMessage received from mbox 0: 0x2\nLED state: OFF\nMessage received from mbox 0: 0x3\nMessage received from mbox 0: 0x4\nLED state: ON\nMessage received from mbox 0: 0x5\nMessage received from mbox 0: 0x6\nLED state: OFF\nMessage received from mbox 0: 0x7\nMessage received from mbox 0: 0x8"}],"div":{"@attributes":{"class":"footnotes"},"p":["1: Or both Hazard3 RISC-V cores, but we won't get\n          into that.","2: Zephyr supports SMP, but the ARM Cortex-M33\n          configuration in the RP2350 isn't built for symmetric\n          multi-processing. Both cores are independent and have no cache\n          coherence, for instance. Since these cores are meant for\n          small embedded devices rather than powerful computing\n          devices, the existence of multiple cores is meant to allow\n          different independent applications (or OSs) running in\n          parallel, cooperating and sharing the\n          hardware.","3: There's an additional instance of the mailbox\n          with its own interrupt as part of the non-secure SIO block\n          (see [1], section 3.1.1), but we won't get into that\n          either."]}},"pubDate":"Thu, 4 Sep 2025 14:00:00 GMT+1"},{"title":"Why don't we do a demo? Part 1: the plan","link":"https:\/\/blogs.igalia.com\/rcn\/posts\/20260317-why_dont_we_do_a_demo_part_1\/index.html","author":{"name":"rcn"},"description":{"h3":["Introduction","Problem 1: the idea","Solution","Problem 2: selecting the hardware","Solution","Problem 3: practical limitations, redefining the idea","Solution","Problem 4: initial planning","Solution"],"p":[{"a":"Some\n        time ago"},"Initially, I had no further intentions beyond playing around\n          a bit, gaining enough know-how to undertake typical embedded\n          software projects and doing the occasional upstream\n          contribution here and there, until\n          a \n          told me \"Now that you've spent some time with Zephyr, what do\n          you think about doing a demo about it?\". Not a bad idea. The\n          goal is to have something to show at conferences and that\n          showcases Zephyr's possibilities using a simple\n          application.","At work, I'm not a specialist. What I do most of the time is\n          basically one thing, and it typically doesn't fit in a\n          specific field, area, or team: I solve problems . So this is an example of how to\n          solve a single-sentence problem (\"Let's do a demo\") using\n          whatever means necessary, involving software, hardware,\n          planning, design, logistics, decision making and\n          improvisation. It's also a personal expression of the\n          importance, meaning and value of human work.","The following is a non-exhaustive list of the problems faced\n          along the way and the solutions found.","The starting point is just a phrase: \"Why don't we do a\n          demo?\", and a deadline. Nothing more. The amount of\n          possibilities alone can already be an obstacle if we can't\n          find a way to limit the solution space. Obviously, we'll find\n          limitations and constraints down the road that will shape the\n          final solution but, right now, everything is uncertainty.","What we want to show in the demo is the possibilities offered\n          by Zephyr for embedded development using 100% open source\n          software, how we can undertake complex application development\n          with Zephyr, and show a variety of development cases within\n          the same application.","There are may approaches to a technical demo. However, having\n          been to conferences with demo booths, it's clear that the live\n          and interactive demos are the ones that gather the most\n          attention of the general public by a large margin. Regardless\n          of the technical merits displayed in the demo, people are\n          drawn to things they can touch, blinking lights, sounds, video\n          games.","So a hard requirement since the beginning was that the demo\n          should be interactive. Fortunately, the nature of the\n          technology behind it lends itself to that easily, although\n          I've seen many Zephyr-based demos that were rather static and\n          only for display. The intention here is to allow the public to\n          actually use it.","Another important thing to take into account is that\n          widespread or hot technologies and buzzwords will be more\n          attractive than obscure or niche terms. Fortunately, I'm\n          building the demo from scratch, so I get to decide what to\n          show. In this case, I picked up\n          \n          as a base technology. Not world-changing, but familiar enough to everyone.","The goal, then, is to develop a hardware\/software solution\n          using Zephyr and its BLE stack, allowing interaction from the\n          public and incorporating some way to display real-time\n          information about it. The initial idea is to have small\n          battery-powered devices in the demo booth and track their\n          position using trilateration based on any available\n          distance-measurement mechanism available in BLE devices, and\n          have a central device that displays the position of the\n          devices in real time.","Now that I settled on an initial idea, even if it's in a very\n          rough and sketchy form, with no further technical details, I\n          can start experimenting with the options. The first step is to\n          do some research about the hardware and software possibilities\n          to reach our goal, pick up some evaluation boards and start\n          sketching ideas to have a better understanding of the\n          feasibility of what I want to achieve and the limitations I\n          can find (time, software\/hardware constraints, skills,\n          etc.)","A good option for BLE-based applications is to use some of\n          the Nordic development kits. They're easy to source and\n          inexpensive. Besides, the recent nRF54L15 SoC\n          supports , which promises precise distance estimations\n          between devices. Just what I'm looking for.","I'll need two types of devices for this: one of them needs to\n          be small (wearable size, if possible) and battery-powered. The\n          other type will be at a fixed location and can have a cabled\n          power supply. The idea is to have three devices at fixed\n          locations in the booth measuring the distance to a number of\n          battery-powered devices that will be moving. Then, a central\n          device will collect the distance information from the metering\n          devices and use it to calculate the position of each\n          battery-powered device.\n\n          This central device will need to have some way to display the\n          position of the devices in some kind of graphical interface,\n          so I need to search for a device that can connect to the\n          metering devices, that is well supported in Zephyr and that\n          can support some kind of display out-of-the-box.","With all these requirements in mind I came up with this list\n        of devices:","All the hardware is already supported in Zephyr, so that\n          should eliminate a lot of the initial friction and save us\n          time.","After some initial experiments with the hardware, running\n          sample BLE applications and getting familiar with the\n          ecosystem, I found out that, while the nRF54L15 hardware\n          supports Bluetooth channel sounding, the Zephyr BLE stack\n          still doesn't support it, so in order to use it I'd need to\n          use\n          Nordic's  instead of the upstream Zephyr controller,\n          together with the\n           stack.","This is a problem because a key feature of this demo should\n          be that it's done using 100% open source code and, preferably,\n          upstream Zephyr code.","Another, even bigger obstacle, is that it's not clear that\n          collecting distance data from multiple sources simultaneously\n          for trilateration, and doing it for multiple peripherals at\n          the same time, is practically viable. I couldn't find any\n          examples or documentation on it, and I could be entering\n          uncharted territory. Considering that we have a deadline for\n          this, I'd rather find an alternative.","The immediate solution is to find a less audacious idea to\n          develop using the same hardware that I already have, keeping\n          it interactive but simpler, and keeping the same goals.","The idea I finally settled on is an extension of the typical\n          BLE peripheral -- central application, where the peripheral\n          publishes some services and the central device connects to it\n          and issues\n          \n          reads and writes to the peripheral characteristics, but adding\n          a multi-level network topology instead of a simple star\n          network, and adding real-time remote display and control of\n          the devices using a graphical interface. So we'd have three\n          device types: the battery-powered peripherals, which will\n          provide the basic services, then the controller devices, which\n          will connect to the peripherals to control them remotely, and\n          then a console device which will connect to the controllers\n          and can show and control the devices remotely using a\n          graphical interface.","Zephyr supports BLE Mesh already, but we'd lose part of the\n          challenge of implementing the networking routing ourselves, so\n          I'm keeping things more interesting by implementing a custom\n          tree topology that provides us with finer grained control, and\n          which can be tailored to a specific application use case.","This means that the controller device will need to act both\n          as a BLE central and peripheral device simultaneously, while\n          the peripheral devices will act only as peripherals and the\n          console will be only a central.","With the development boards at hand, I can start designing\n          and developing the firmwares for the three board types,\n          including testing and documentation. The other certain thing I\n          have right now is a deadline: the conference where we want to\n          show the demo. Now I need to draw a rough plan with concrete\n          dates.","Considering that I'll surely find a few bad surprises down\n          the road and that there'll be uncertainty and problems that I\n          can't yet anticipate, since it's the first time we're doing a\n          demo with these characteristics, I set myself a personal hard\n          deadline: one month before the real hard deadline. Ideally,\n          the firmware should be all done and thoroughly tested one\n          month before that, so that'd leave two full months for\n          additional preparations and for sorting out whichever\n          last-minute obstacles I could find in the end.","Of course, all of this rough planning is based purely on\n          intuition. I could fall into the trap of wanting to plan\n          everything beforehand and write a well-specified roadmap of\n          everything that needs to be done in minute detail, but I'd be\n          setting myself up for failure from the start, since 90% of the\n          work ahead is a big question mark. I'm defining everything as\n          we go, and in cases like this it's much more reasonable to\n          plan and work based on different principles:","Doing this as a one-person-army has both pros and cons. Fear\n          and uncertainty are something you have to shoulder on your\n          own, but you're also free to take whatever decision you need\n          whenever you need.","So, now we're ready to start developing. A rough milestones\n          sketch for the firmware development could be:","Testing and simulation should be a part of every\n          milestone.","In the next post we'll go through the firmware development\n          part of the project."],"div":[{"@attributes":{"align":"center"},"br":{},"img":{"@attributes":{"src":"https:\/\/blogs.igalia.com\/rcn\/posts\/20260317-why_dont_we_do_a_demo_part_1\/idea_sketch.jpg"}}},{"@attributes":{"align":"center"},"br":{},"img":{"@attributes":{"src":"https:\/\/blogs.igalia.com\/rcn\/posts\/20260317-why_dont_we_do_a_demo_part_1\/demo_idea.png"}}},{"@attributes":{"class":"footnotes"},"p":"1: I like to think that's a specialty,\n          though. Maybe one day that'll be a role in the\n          company."}],"ul":[{"li":["\n            For the battery-powered\n            devices: , based on the nRF54L15 SoC.\n          ","\n            For the metering\n            devices:  boards from Nordic, also based on the nRF54L15.\n          ","\n            For the central\n            device:  board, which supports a serial SPI touchscreen like\n            .\n          "]},{"li":["\n            Define reasonable and achievable milestones and iterate\n            based on them.\n          ","\n            Iterate fast and as many times as needed.\n          ","\n            Re-draw the plan after an iteration if needed.\n          ","\n            Be ready to improvise.\n          ",{"a":"Improve\n              incrementally"}]},{"li":["\n            Base application for the peripheral: board setup and hardware\n            handling.\n          ","\n            Base application for the controller device: board setup and\n            hardware handling.\n          ","\n            Basic peripheral-central BLE application using the\n            peripheral and controller devices.\n          ","\n            Base application for the console device: board setup and\n            hardware handling.\n          ","\n            Make the controller device work as both a BLE peripheral and\n            central device.\n          ","\n            Incorporate the console device to the peripheral +\n            controller application.\n          ","\n            Graphical interface design and implementation.\n          "]}]},"pubDate":"Tue, 17 Mar 2026 08:00:00 GMT+1"},{"title":"Why don't we do a demo? Part 2: software development","link":"https:\/\/blogs.igalia.com\/rcn\/posts\/20260330-why_dont_we_do_a_demo_part_2\/index.html","author":{"name":"rcn"},"description":{"p":["In  of this series I talked about the beginning of this\n          story and laid out the plan. In this post we'll start the\n          actual work, beginning with the software part.","I'll start with the most basic device: the peripheral. It\n          will provide a simple BLE service to allow toggling the board\n          LED remotely and displaying its current status.","The  are a good starting point for the firmware\n          skeleton. The XIAO nRF54L15 is also well supported in Zephyr,\n          so defining a custom BLE service and operating the on-board\n          LED is not a challenge. A minimal sketch firmware with the\n          basic functionality can be done reasonably quickly starting\n          from scratch. To test the BLE service we can use a smartphone\n          and .","I probably don't need to go all the trouble of doing a custom\n          BLE service and characteristic for this, but it's an exercise\n          I'll need to do at some point, and it has the added bonus of\n          giving us full freedom to define the functionalities we\n          want.","For the BLE services and characteristics, I picked up a\n          random\n          128-bit  generated\n          with .","This should be good enough for now, we'll surely need to\n        complicate it later.","The user LED in the XIAO nRF54L15 turns off\n          with  and on with . Not a problem if we only want to toggle it instead\n          of setting a specific value, but not ideal, since we also want\n          to keep track of its current state and report it.","This one's easy. According to the\n          ,\n          this LED is active low, but\n          the  defines it as active\n          high. .","In the BLE central-peripheral architecture proposed, the\n          peripheral will work as an autonomous device that provides a\n          service but does no other action except when requested by the\n          user through a button press. Other than that, it'll sit there\n          waiting for requests from the central (the controller device\n          in our case), which will be the one governing the bulk of the\n          application and, more importantly, managing the connection and\n          doing the necessary actions to establish and monitor it.","Some of the tasks under the responsibility of the controller\n          are:","We need a way to model this behavior into the controller so\n          we can integrate these tasks with the rest of the firmware\n          gracefully.","I'll abstract the list of tasks above in a simple state\n          machine that will run in a separate thread taking care of\n          handling the connections, running the necessary actions as\n          response to specific events, interacting with the rest of the\n          firmware and reacting to the actions triggered by the user via\n          the board buttons or by external sources.","That way, the main thread will set up the hardware and the\n          necessary software subsystems, and the state machine will keep\n          track of most of the BLE-related tasks and of the connected\n          devices.","So, when the initialization is done, the main thread will\n          start the state machine thread and then wait for events such\n          as button presses, managing and restarting common services,\n          while the state machine works on its own.","For our purposes we'll only need three states:","I can reuse most of this architecture as the basis for the\n          console device as well, since it'll be a central device to the\n          controllers (remember the controllers are both central and\n          peripheral BLE devices at the same time), so I can start\n          sketching the console firmware as well as a generic central\n          device.","We need a way for the controller to interact with the\n          connected peripherals, and in the controller boards (nRF54L15\n          DK) we have as user-facing devices four LEDs and four\n          buttons. The operations we'll need to perform are:","The most useful thing we could do with the board LEDs is to\n          replicate the status of the peripheral LEDs. That way we could\n          have a real-time overview of the state of the connected\n          peripherals at all times.","The downside of this is that the board only has four LEDs, so\n          if I want to show the status of the connected peripherals at a\n          glance, I'm limited to four of them. And it'd be good to keep\n          one LED to show the status of the controller itself, so lets\n          start by limiting the amount of simultaneously connected\n          peripherals to three.","Now, about the buttons, I'm going to need a way to perform at\n          least three actions: scanning, disconnecting and toggling, and\n          I'll probably need to make room for additional actions down\n          the road.","One option is to assign one button to each peripheral \"slot\",\n          so I could use button 0 to perform an action on slot 0, button\n          1 for slot 1, etc. In this case, I'd need to encode multiple\n          actions on the same button: scanning and toggling at least.","A different approach is to use one or two buttons to select\n          the active slot, and then the action buttons would operate on\n          the selected slot. I feel like this method could be easier to\n          adapt in case I need to add additional functionalities later,\n          so this is what I'll do:","I'll also need a way to tell which one is the selected\n          slot. Since I'm using the LEDs to represent the slots, an easy\n          way to do this is by briefly blink the LED of the currently\n          active slot when we use buttons 0 or 2 to cycle through the\n          slots. Additionally, I can use the same method to encode\n          whether the slot contains a connected peripheral or not, since\n          I'm using a static LED to show the status of the peripheral\n          LED (i.e. we can't tell from a LED that's off if the connected\n          peripheral has its LED off or of there's no peripheral\n          connected at all): when cycling through the slots selecting\n          the active one, the LED can do a short blink cycle to\n          represent a disconnected slot and a long blink cycle to\n          represent a connected one.","During development, it's very inconvenient to run all the\n          firmware changes we do on real hardware, even if these boards\n          can be flashed very fast. And for debugging and testing,\n          relying on the hardware is overkill most of the time, even if\n          we have direct access to a serial console and we have plenty\n          of tracing possibilities. I'd need a better way to test our\n          changes.","Fortunately, Zephyr includes\n          a  that allows to build a firmware as a native\n          binary that I can run on the development machine using\n          emulated devices. For my purposes, the\n           even let me simulate the specific SoC used in\n          the boards, including most of the SoC hardware, and run the\n          firmware natively in\n           to\n          simulate real BLE usage.","This offers many advantages over testing on hardware:","Ideally, what I'd like is to configure the environment so\n          that I can selectively build and test the firmware on the\n          simulator, or build a release firmware for the real\n          hardware. A way to do this is to keep two separate project\n          config files, create the necessary device tree overlay files\n          for the different target boards (real and simulated) and\n          compile certain parts of the firmware conditionally, so that I\n          can enable test code and emulated devices only on the\n          simulator build and I can keep hardware-dependent code only\n          for the release build:","Code compiled conditionally for the simulator looks like\n        this:","From now on, I can do most of the development on the\n          simulator, and once things are the way I want I can test them\n          on the real hardware.","While the peripheral devices can be powered via USB, just the\n          same as the bigger boards, the demo would be both more\n          realistic and more diverse if we used batteries for them. The\n          XIAO nRF54L15 is prepared for that and\n          has  and the necessary hardware to manage a LiPo\n          battery. I need to provide the batteries and add the\n          appropriate battery leads to the boards, though.","Any suitable LiPo battery will do, but I'll search for\n          batteries with an appropriate dimensions and capacity for this\n          application.","I found this bundle containing five batteries and a charger,\n          which should be good enough for our purposes: we can have up\n          to 5 battery-powered peripherals and a convenient way to\n          recharge the batteries if they're easy to detach from the\n          devices.","The battery connectors are Molex 51005, so I'll also need to\n          source a bunch of male and female leads. The pads are big\n          enough to solder the leads to them with a conventional pen\n          solder:","The XIAO nRF54L15 seems very flaky. In particular, after\n          flashing it sometimes the device crashes and Zephyr reports a\n          bus data error in the serial console. It seems to be random,\n          it happens only after flashing some builds and it also seems\n          to depend on timing.","Even worse, when battery-powered, the board won't boot. When\n          powered via USB, though, it will boot, and then I can plug in\n          the battery, unplug the USB cable and the board will keep on\n          running.","After some investigation and tests, it looks like the crashes\n          are related to the logging through the UART console. Why, I\n          don't know. The kind of crashes I'm seeing right during\n          booting are bus faults, and the first things I'd check for are\n          null pointer dereferences and stack overflows, but in this\n          case I'm not even getting a valid PC in the error\n          report. Besides, there are a few signs that this will be hard\n          to pinpoint:","All of these hint that there's some flakiness involved in the\n          XIAO nRF54L15, particularly related to either power\n          management, flashing or the use of the builtin USB for UART\n          output.","Judging by some issues raised in\n          the , it looks like the USB-based SWD circuitry could be\n          the cause of these problems. Regarding the problems booting\n          when battery-powered, after asking about it in the forums, I\n          got a\n          \n          explaining the reason: when logging is enabled, the TX line\n          back-feeds and powers up the USB-UART chip, causing a brownout\n          and a shutdown\/reboot.","The most reasonable fix or workaround for all of this is to\n          simply disable all logging and UART usage when the board is\n          battery-powered. In\n          order to do this, I created another build type that will be\n          used for \"production\" releases. For the non-production builds\n          (the ones I'll use for development and debugging) I'll keep\n          logging disabled with the possibility of enabling it through\n          shell commands. That'll reduce the chances of crashing the\n          system at boot time.","We can take advantage of the builtin web server capabilities\n          provided by Zephyr for the console board. Since it'll be\n          governing the application and monitoring \/ controlling the\n          connected devices, we'll need a user interface to manage\n          it. Implementing it in the form of a web interface should be\n          easy enough, and it'd give us a lot of freedom to design the\n          interface. The idea would be to connect the console board to a\n          client (a laptop, for instance) using a point-to-point\n          Ethernet link and have the client access the web page served\n          by the console board.","The problem is that the board doesn't have an Ethernet\n        interface.","Everything's not lost, though. The board doesn't have an\n          Ethernet interface but it has a general USB interface besides\n          the one used for flashing and debugging. And, fortunately, the\n          USB stack in Zephyr\n          supports  (Ethernet-over-USB) and we even have an\n          \n          of the web server running on the same board we're using for\n          the console device, so setting it up shouldn't be too much of\n          an issue.","I can run the sample code on the board and check that it\n          works, I can connect to it and see the web page published by\n          the web server. Integrating the basic code into our sketchy\n          console firmware is mostly painless, although I'm publishing\n          only a placeholder web page. For now, that's good enough. I'll\n          see what we can do with it later.","In the next post we'll continue through the rest of the\n          software development part of the project."],"h3":["Problem 5: base peripheral device","Solution","Problem 6: unexpected LED behavior","Solution","Problem 7: modeling the behavior of the central device","Solution","Problem 8: designing the UX for the controller device","Solution","Problem 9: simulation and testing","Solution","Problem 10: battery-powered peripheral setup","Solution","Problem 11: hardware unreliability","Solution","Problem 12: network connectivity in the console device","Solution"],"pre":[{"code":"\/* LED service UUID: 46239800-1bed5-4c51-a215-9251faaae809 *\/\n#define LED_SERVICE_UUID_VAL \\\n\tBT_UUID_128_ENCODE(0x46239800, 0x1bed5, 0x4c51, 0xa215, 0x9251faaae809)\n\nstatic struct bt_uuid_128 led_svc_uuid =\n\tBT_UUID_INIT_128(LED_SERVICE_UUID_VAL);\n\n\/* Characteristic UUID: 46239801-1bed5-4c51-a215-9251faaae809 *\/\nstatic struct bt_uuid_128 led_char_uuid = BT_UUID_INIT_128(\n\tBT_UUID_128_ENCODE(0x46239801, 0x1bed5, 0x4c51, 0xa215, 0x9251faaae809));\n\n\/* Characteristic UUID: 46239802-1bed5-4c51-a215-9251faaae809 *\/\nstatic struct bt_uuid_128 led_indication_char_uuid = BT_UUID_INIT_128(\n\tBT_UUID_128_ENCODE(0x46239802, 0x1bed5, 0x4c51, 0xa215, 0x9251faaae809));\n\n[...]\n\nBT_GATT_SERVICE_DEFINE(led_svc,\n\tBT_GATT_PRIMARY_SERVICE(&led_svc_uuid),\n\tBT_GATT_CHARACTERISTIC(&led_char_uuid.uuid,\n\t\t\tBT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE,\n\t\t\tBT_GATT_PERM_READ | BT_GATT_PERM_WRITE,\n\t\t\tread_led_state, write_led_state, &led_state),\n\tBT_GATT_CHARACTERISTIC(&led_indication_char_uuid.uuid,\n\t\t\tBT_GATT_CHRC_INDICATE,\n\t\t\tBT_GATT_PERM_READ | BT_GATT_PERM_WRITE,\n\t\t\tNULL, NULL, NULL),\n\tBT_GATT_CCC(led_ccc_changed,\n\t\tBT_GATT_PERM_READ | BT_GATT_PERM_WRITE),\n);"},{"code":"\/*\n * LED state characteristic read callback.\n *\/\nstatic ssize_t read_led_state(struct bt_conn *conn,\n                             const struct bt_gatt_attr *attr, void *buf,\n                             uint16_t len, uint16_t offset) {\n\tconst uint8_t *val = attr->user_data;\n\treturn bt_gatt_attr_read(conn, attr, buf, len, offset, val,\n\t\t\t\tsizeof(*val));\n}\n\n\/*\n * LED state characteristic write callback.\n * A write to this characteristic will trigger a LED toggle, the data\n * sent is irrelevant so we can just ignore it.\n *\/\nstatic ssize_t write_led_state(struct bt_conn *conn,\n                              const struct bt_gatt_attr *attr, const void *buf,\n                              uint16_t len, uint16_t offset, uint8_t flags) {\n\tARG_UNUSED(conn);\n\tARG_UNUSED(attr);\n\tARG_UNUSED(buf);\n\tARG_UNUSED(offset);\n\tARG_UNUSED(flags);\n\n\t\/*\n\t * Ignore received data (dummy): *((uint8_t *)buf)\n\t * and override (toggle) the led_state here as a side-effect.\n\t *\/\n\tLOG_DBG(\"LED toggle received: %d -> %d\", led_state, led_state ? 0 : 1);\n\tled_state = led_state ? 0 : 1;\n\tgpio_pin_set_dt(&led, led_state);\n\tgpio_pin_set_dt(&led_board, led_state);\n\tif (led_indication_enabled)\n\t\tk_work_schedule(&led_indicate_work, K_NO_WAIT);\n\n\treturn len;\n}\n\n\/*\n * LED indication Client Characteristic Configuration callback.\n *\/\nstatic void led_ccc_changed(const struct bt_gatt_attr *attr, uint16_t value)\n{\n\tARG_UNUSED(attr);\n\n\tled_indication_enabled = (value == BT_GATT_CCC_INDICATE);\n\tLOG_DBG(\"Indication %s\", led_indication_enabled ? \"enabled\" : \"disabled\");\n}"},{"code":"\u251c\u2500\u2500 boards\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 nrf52_bsim.conf\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 nrf52_bsim.overlay\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 nrf54l15bsim_nrf54l15_cpuapp.conf\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 nrf54l15bsim_nrf54l15_cpuapp.overlay\n\u251c\u2500\u2500 build.sh\n\u251c\u2500\u2500 CMakeLists.txt\n\u251c\u2500\u2500 flash.sh\n\u251c\u2500\u2500 Kconfig\n\u251c\u2500\u2500 prj.conf\n\u251c\u2500\u2500 prj_sim.conf\n\u251c\u2500\u2500 sim_bin\n\u251c\u2500\u2500 sim_build.sh\n\u251c\u2500\u2500 sim_run.sh\n\u2514\u2500\u2500 src\n    \u251c\u2500\u2500 common.h\n    \u251c\u2500\u2500 emul.c\n    \u251c\u2500\u2500 emul.h\n    \u251c\u2500\u2500 main.c\n    \u251c\u2500\u2500 peripheral_mgmt.c\n    \u251c\u2500\u2500 peripheral_mgmt.h\n    \u251c\u2500\u2500 sim_test.c\n    \u251c\u2500\u2500 sm.c\n    \u2514\u2500\u2500 sm.h"},{"code":"[...]\nint main(void)\n{\n\tstatic struct gpio_callback button_cb_data;\n\tint log_sources = log_src_cnt_get(0);\n\tint ret;\n\tint i;\n\n#ifdef CONFIG_BOARD_NRF52_BSIM\n\t\/* Set all logging to INFO level by default *\/\n\tfor (i = 0; i < log_sources; i++) {\n\t\tlog_filter_set(NULL, 0, i, LOG_LEVEL_INF);\n\t}\n\tint id = log_source_id_get(\"controller__main\");\n\tlog_filter_set(NULL, 0, id, LOG_LEVEL_DBG);\n#else\n\t\/* Disable all logging by default *\/\n\tfor (i = 0; i < log_sources; i++) {\n\t\tlog_filter_set(NULL, 0, i, LOG_LEVEL_NONE);\n\t}\n#endif"}],"code":["read_led_state","write_led_state","led_ccc_changed"],"ul":[{"li":["\n            Scanning for peripherals.\n          ","\n            Connecting to peripherals.\n          ","\n            Service discovery.\n          ","\n            Keep track of the connected devices.\n          ","\n            Handle disconnection requests and lost connections.\n          "]},{"li":[{"b":"Event listen"},{"b":"Scan"},{"b":"Discover"}]},{"li":["\n            Scan for peripherals.\n          ","\n            Disconnect from a connected peripheral.\n          ","\n            Toggle the LED of a connected peripheral.\n          ","\n            Check the status of the peripherals.\n          "]},{"li":["\n            Button 0: select the next slot as the \"active slot\".\n          ","\n            Button 1: \"action button\", trigger an action on the\n            peripheral connected in the active slot. For now, the action\n            will be to toggle the LED.\n          ","\n            Button 2: select the previous slot as the \"active slot\".\n          ","\n            Button 3: disconnect the peripheral in the active slot, if\n            any, and start scanning on it.\n          "]},{"li":["\n            Faster development cycles.\n          ","\n            Easier debugging of runtime errors.\n          ","\n            Triggering of specific corner cases programmatically.\n          "]},{"li":["\n            Altering the logging does cause different results.\n          ","\n            Different builds and flashings of the same firmware\n            sometimes crash and sometimes don't.\n          ","\n            It doesn't seem related to the size of the logging stack.\n          ","\n            Deferred vs immediate logging causes different results.\n          ","\n            It doesn't fail on the simulator.\n          ","\n            It seems related to timing.\n          ","\n            There's a big randomness factor.\n          ","\n            The same firmware on the same SoC but on a different board\n            design (nRF54L15 DK) works fine.\n          "]}],"div":[{"@attributes":{"align":"center"},"br":{},"img":{"@attributes":{"src":"https:\/\/blogs.igalia.com\/rcn\/posts\/20260330-why_dont_we_do_a_demo_part_2\/sm.png"}}},{"@attributes":{"align":"center"},"br":{},"img":{"@attributes":{"src":"https:\/\/blogs.igalia.com\/rcn\/posts\/20260330-why_dont_we_do_a_demo_part_2\/nrf54l15dk_leds_buttons.jpg"}}},{"@attributes":{"align":"center"},"br":{},"video":{"@attributes":{"width":"320","height":"240","controls":"controls"},"source":{"@attributes":{"src":"https:\/\/blogs.igalia.com\/rcn\/posts\/20260330-why_dont_we_do_a_demo_part_2\/slot_cycle.webm","type":"video\/webm"}}}},{"@attributes":{"align":"center"},"br":{},"img":{"@attributes":{"src":"https:\/\/blogs.igalia.com\/rcn\/posts\/20260330-why_dont_we_do_a_demo_part_2\/batteries_charger.jpg"}}},{"@attributes":{"align":"center"},"br":{},"img":{"@attributes":{"src":"https:\/\/blogs.igalia.com\/rcn\/posts\/20260330-why_dont_we_do_a_demo_part_2\/peripheral_and_battery.jpg"}}},{"@attributes":{"class":"footnotes"},"p":"1: This is now  in the "}]},"pubDate":"Mon, 30 Mar 2026 13:00:00 GMT+1"}]}}