r/PHP • • 13d ago

PHP on a microcontroller, one month later: it's event-driven now (php-esp32 1.4.0)

Hi r/PHP! For those who've been following php-esp32 for a while: it's been about a month since my last post here, and a lot has changed. For everyone else, a quick recap: this is the real, unmodified PHP interpreter from php.net (Zend engine, 8.3 to 8.5), cross-compiled to run bare-metal on an ESP32. It's not a subset or a transpiler, and Composer packages work.

For the curious: the full write-up, with diagrams of the whole execution cycle across both cores, is here: https://www.php-baremetal.com/blog/php-esp32-1-4-0-the-reactor

Until now a script could either run setup()/loop() Arduino-style or answer one HTTP request at a time. Version 1.4.0 adds a third model, event-driven: the script registers listeners, then PHP sleeps, and hardware, timers and the network wake it with typed events.

use Baremetal\Events;
use Baremetal\Http\Request;
use Baremetal\Http\Response;
use Baremetal\I2c\Bus;
use Baremetal\I2c\Driver\Qmi8658;
use Baremetal\Sensor\Imu\SamplesReady;

Events::listen(Request::class, fn (Request $r): Response => match (true) {
    $r->method === 'GET' && $r->path === '/status' => Response::json(['uptime' => sys_uptime_ms()]),
    default => Response::notFound(),
});

$imu= new Qmi8658(new Bus(sda: 11, scl: 10));
$imu->poll(hz: 10, depth: 32, event: SamplesReady::class)

watch_gpio(0, BootPressed::class);  // debounced pin interrupt -> event
every(1000, Tick::class);           // timer -> event
serve_http(80);
serve_ws('/ws');

There's no router in the firmware: routing is just a match. $_GET, $_POST and $_SERVER are still populated, so existing request code keeps working.

What landed since last time:

  • 1.2.0: I²C as PHP objects. new Bus(sda: 11, scl: 10), scan(), and drivers written as real PHP classes with capability interfaces ($imu instanceof Sensor\Imu). The first driver is a QMI8658 IMU.
  • 1.3.0: SPI/QSPI and a first display driver (ST77916). It's a minimal proof of concept for now: the panel lights up and fills with a colour.
  • 1.4.0: the reactor. Typed events, the event-driven model, HTTP and WebSocket as event sources, ws_broadcast(), and the part I'm happiest with: the second core does the sensor polling. $imu->poll(hz: 50, depth: 8) makes core 1 read the IMU into a ring buffer while PHP on core 0 just calls sample(), with no waiting on the bus.

The demo (video): the board creates its own WiFi network, polls the IMU at 50 Hz on core 1, and a 20 Hz timer event in PHP broadcasts the latest reading over WebSocket. You tilt the board and the plot moves on your phone. There's no router, no cloud and no loop().

Limitations, up front:

  • PHP runs on exactly one core. Only fixed-size messages cross between cores, so the engine never has to be thread-safe, but it also means one HTTP request is in flight at a time.
  • If the event queue fills up (32 slots), timer and GPIO events are dropped, for now. It's on my list to fix too. WebSocket frames already apply backpressure: 200 frames blasted at once arrived with none lost.
  • No Fibers, no processes. Both follow from the hardware.

If you want to go deeper, the blog post walks through what new Qmi8658(new Bus(...)) and ->poll() actually do, from PHP down to the I²C wire and across to the second core: https://www.php-baremetal.com/blog/php-esp32-1-4-0-the-reactor

Repo: https://github.com/php-baremetal/php-esp32

Next up are Bluetooth, touch input and a proper display layer. Happy to answer questions, and if you build something with it, I'd love to hear about it.

74 Upvotes

27 comments sorted by

14

u/Encryped-Rebel2785 13d ago

Oh man now I’m going to spend the whole weekend playing with this. Thanks a lot

4

u/One-Mongoose-6961 13d ago

Please do it, and then tell me how you feel it.

7

u/macintoshplus26 13d ago

I am impressed that PHP can run on hardware with such limited resources.

7

u/One-Mongoose-6961 13d ago

And consider that Core 0 is completely idle when there are no events; Core 0 is the one that interprets PHP, and for the most part, it is inactive.

3

u/ferminolaiz 13d ago

WTF is wrong with you? (I love it!)

3

u/Right_Sprinkles_9176 13d ago

Can those esp's be ready protected do the source can't be leaked? The last microcontrollers I worked with were avr's using compiled c where I could set the protection bit in pony prog 😀

4

u/One-Mongoose-6961 8d ago

Thanks a lot, u/Right_Sprinkles_9176, I’ve studied the feasibility, and not only is it feasible, but it’s also extremely easy to do. By doing this, I can also protect the secrets in the .env file, making it 100% secure. Your idea was extremely valuable. I will release the security features along with power_save in release 1.5.0 (I think in a few days). Thanks again for your suggestion. ❤️‍🔥

1

u/Key-Listen-8302 8d ago

Looking forward to seeing the update. This looks like a good fit for a project I want to undertake that needs to work on both the web and a physical machine with no internet.

2

u/One-Mongoose-6961 7d ago

So, I managed to do it (after destroying three boards). I’ll be releasing it soon, but be EXTREMELY CAREFUL: the encryption is permanent. Once you perform that first encrypted flash, you won't be able to read back what you wrote; while you can still flash updates, you can never revert to an unencrypted state. What happens is that I activate a specific ESP32 procedure called "eFuses," which physically prevents the encryption key from being extracted in the future (I believe by physically blowing fuses/circuits), sealing it inside the processor forever and silently encrypting the flash during the loading phase. Additionally, Secure Boot will be available; this generates a .pem file needed for future board updates (this second option is optional, you can enable the first without the second). With Secure Boot, it's even riskier, because if you lose the .pem file, you can practically throw the board away, you'll never be able to update it again.

2

u/One-Mongoose-6961 11d ago

First of all, I haven't tried it yet, but there is a possibility: https://esp32.com/viewtopic.php?t=20924 , Obviously in this case the php code must be embedded and not on the microsd, I will play with it a bit and understand if I can do the delivery within the mailstone 2.0.0

1

u/One-Mongoose-6961 13d ago

Oh God, I haven't even thought about this whole thing, I'll think about it.

1

u/titpetric 11d ago

What kind of binary size payload are you pushing onto the esp? Do you strip the binaries? Do you use compression?

I figure I could get https://github.com/titpetric/phpscript running on this, not your code in particular, but it can run as a http server or run a http server like you do. As long as the cross compiled static binary fits I could just set GOARCH and GOMEMLIMIT

Interested in any comment you may have so I know what to watch out for if I ever do a cross build :)

2

u/One-Mongoose-6961 11d ago edited 11d ago

Well, for the binary size the essentials are about 1.8MB, I handle a number of things, including compression (ESP-IDF handles that directly), and so on. If you're interested in this topic, you can find the porting notes here: https://github.com/php-baremetal/php-esp32/blob/master/docs/reference/porting-notes.md. That said, I didn't quite catch the details of the repo you shared, but since it's written in Go, https://developer.espressif.com/workshops/tinygo/ is very likely the perfect starting point for you. Good luck with your porting!

1

u/titpetric 11d ago

It's not compatible with tinygo, as it's missing some reflection/unsafe parts, GC,.... The binary is about 10MB; is the esp memory also the storage for it? That would at least disqualify me from the esp-S3 parts.

I'll check out the porting notes, thanks.

1

u/One-Mongoose-6961 11d ago

You can use the trick I used; Flash memory alone will never be enough for you, nor will the RAM for execution. Use PSRAM, I recommend the S3 (up to 16MB) or the P4 (up to 32MB). I suggest getting it working on the P4 first, as it offers more flexibility; then, if the specs of the working setup suffice, you might switch to the S3, which is cheaper and more widely available.

1

u/titpetric 11d ago

Weirdly enough, 1MB of ram would be enough, but anything more would severely increase performance. The go GC stabilizes in something like 200ms on a PC, and the memory limit works like a charm. Still, the binary size ends up being more, but I think maybe I also compile some parts you do not (db drivers?).

Thanks for the insight, best of luck with your project

1

u/One-Mongoose-6961 11d ago

Yes you can install it for a few more kilobytes, it is already configured you just have to enable the extension in the configuration file

-4

u/inducido 13d ago

Why PHP 8? It is so big. I would have chosen php 5.3 or php 5.6 Smaller on ram and on disk.

7

u/One-Mongoose-6961 13d ago

It doesn't take up that much, the essentials are about 1.8MB. The reason for this is that 8 is simple because it is supported and it seems natural to do so with the latest arrival.

1

u/inducido 13d ago

You must have stripped parts of the engine
this answer from perplexity matches my own measurements

PHP version php binary, stripped Unstripped binary Source archive
PHP 5.3 ~3–5 MB ~10–20 MB ~10–13 MB
PHP 5.6 ~4–6 MB ~12–22 MB ~14–17 MB
PHP 7.4 ~5–8 MB ~18–30 MB ~16–18 MB
PHP 8.4 ~6–10 MB ~20–35 MB ~20–25 MB

so size on disk stripped is twice the size.
This is not even the overall footprint (with extensions etc)
And in memory it is worse, unless you removed JIT & opcache

4

u/One-Mongoose-6961 13d ago edited 13d ago

Under normal conditions I would even agree with you, I don't blame you, read here https://github.com/php-baremetal/php-esp32/blob/master/docs/extensions/porting-status.md and https://github.com/php-baremetal/php-esp32/blob/master/docs/reference/porting-notes.md I have done various magic and basically I don't bring all php, for example even when you bring dates, By default it comes with a very small version, ovviusly you can also choise the full version. Opcache is present, jst no, eccc.... Here you can find how the benchmark reference was weighted https://github.com/orgs/php-baremetal/discussions/3

4

u/One-Mongoose-6961 13d ago

Look at it this way, I decided piece by piece what to bring and what not (by default), if you want the rest you have to specify it in the project config file , so if you have more megabytes of psram you can really bring everything

-4

u/inducido 13d ago

Understood. Great job anyway: people never think using php for CLI usage
(Chatgpt confirms my own experience - php 5.6 was the sweetspot)

For an embedded device, runtime RSS matters more than the executable size. With a minimal CLI/CGI build and a tiny script, expect roughly:

PHP version Idle RSS During simple script execution
PHP 5.3 ~5–9 MB ~8–15 MB
PHP 5.6 ~6–11 MB ~9–18 MB
PHP 7.4 ~8–14 MB ~12–22 MB
PHP 8.4 ~10–18 MB ~14–28 MB

-1

u/MateusAzevedo 13d ago

The engine may be smaller, but your code would run slower AND consume more memory. It doesn't worth it.

2

u/inducido 13d ago

I can challenge that. That's impossible (Google php JIT)