狠活:ESP32-S3-N16R8 运行 Typecho CMS !封面图

狠活:ESP32-S3-N16R8 运行 Typecho CMS !

前几天我的 ESP32-S3-N16R8 不是到了嘛,尝试搞了点常规的操作,我正在想还可以干啥比较有意思的,毕竟这玩意本质上就是一个计算机。
然后,我脑子里面出现了一个疯狂的想法——让 ESP32-S3-N16R8 运行 Typecho
当时和 ChatGPT 讨论的时候,只是试探性玩笑地问了一句:

我有一个疯狂的想法:能在这玩意上面运行 typecho 吗?

没想到的是,他居然回复了:

你这个想法居然不是完全不可能!而且我查到了一个相当惊人的项目:已经有人把真正的 PHP 解释器移植到了 ESP32-S3 上。

而这个项目,正是 php-esp32,最重要的是,这个项目支持两个 ESP32 的开发板:ESP32-S3 和 ESP32-P4 ,而我买的 ESP32-S3-N16R8 正好就在支持列表里面!果然多花的 10 块钱没有白花,这次可以整狠活了
还有什么好说的?直接开整!

备份开发板

因为我还是希望保留原厂的 MicroPython 环境进行后续的学习,所以我打算对开发板进行备份,整完了活之后再刷回去。有了上一篇文章时的入门基础,现在操作就熟悉多了。
开始备份:

python -m esptool \
  --chip esp32s3 \
  --port /dev/ttyACM0 \
  --baud 460800 \
  read_flash 0x0 0x1000000 esp32s3_original_16mb.bin

这里建议进行第二次备份以防万一,备份完成后校验一下两个文件的哈希:

python -m esptool \
  --chip esp32s3 \
  --port /dev/ttyACM0 \
  --baud 460800 \
  read_flash 0x0 0x1000000 esp32s3_verify_16mb.bin

cmp esp32s3_original_16mb.bin esp32s3_verify_16mb.bin

不过有一说一,这个备份真的慢,16MB 的数据传输了差不多10分钟,感觉回到了上古计算机时代。

准备 PHP 编译环境

这里最好是使用 ext4 或 Btrfs 等 Linux 文件系统,以防万一 ESP-IDF 的符号链接、文件权限和编译执行遇到问题。
创建一个你喜欢的项目文件夹名称,进入项目文件夹,创建工具目录:

mkdir -p tools toolchains

下载 phpflash:

curl -fL \
  https://github.com/php-baremetal/flash-tool/releases/latest/download/phpflash-linux-amd64 \
  -o tools/phpflash

chmod +x tools/phpflash

./tools/phpflash --version

这是 php-esp32 官方推荐的构建管理工具,负责创建工程、调用 ESP-IDF 编译、烧录和串口监控。

安装 ESP-IDF 和 PHP 移植源码:

export IDF_TOOLS_PATH="$PWD/toolchains/.espressif"

./tools/phpflash system-setup \
  --idf-path "$PWD/toolchains/esp-idf" \
  --php-esp32-path "$PWD/toolchains/php-esp32"

这会下载乐鑫 ESP-IDF、ESP32 交叉编译器,以及 php-esp32 的源码,并获取官方 PHP 源码进行移植构建准备。这套编译环境大概会占据 7GB ~ 8GB 的磁盘空间,请注意一下可用空间够不够(应该还是有这点空间的吧?)。

创建局部的项目环境变量,新建 env.sh 文件,内容如下:

export ROOT="/run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32"

export IDF_TOOLS_PATH="$ROOT/toolchains/.espressif"
export IDF_PATH="$ROOT/toolchains/esp-idf"
export PHP_ESP32_DIR="$ROOT/toolchains/php-esp32"

echo "PHP-on-ESP32 environment loaded."

/run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32 记得替换成你实际的项目文件夹路径,后续导入开发环境只需要:

source env.sh

尝试编译第一个 PHP 固件

饭要一口一口吃,测试也要一步一步来。第一步需要先验证这套开发环境是否有效,我们先不要放 Typecho,第一个目标应该是:
让 ESP32-S3 上电后执行真正的 PHP 代码,并通过串口输出 Hello World。
创建项目目录:

./tools/phpflash init php-hello

按照下面的方式进行选择:

Project name [php-hello]: 
Chip:
  1) ESP32-P4 (esp32-p4)
  2) ESP32-S3 (esp32-s3)
choice [1]: 2
Board preset:
  1) Espressif ESP32-S3-DevKitC-1 (N16R8) (espressif-esp32-s3-devkitc-1-n16r8)
  2) Espressif ESP32-S3-DevKitC-1 (N8R2) (espressif-esp32-s3-devkitc-1-n8r2)
  3) Espressif ESP32-S3-DevKitC-1 (N8R8) (espressif-esp32-s3-devkitc-1-n8r8)
  4) ESP32-S3 (octal PSRAM, generic) (generic)
  5) ESP32-S3 (quad PSRAM, generic) (generic)
  6) Waveshare ESP32-S3-ETH (waveshare-esp32-s3-eth)
  7) Waveshare ESP32-S3-Touch-LCD-1.85B (waveshare-esp32-s3-touch-lcd-1.85b)
  8) Waveshare ESP32-S3-Zero (waveshare-esp32-s3-zero)
  9) Waveshare ESP32-S3-Zero (N8R8) (waveshare-esp32-s3-zero-n8r8)
choice [1]: 1
Storage type:
  1) embedded
choice [1]: 
Project type:
  1) init-loop
  2) web-server
  3) event-driven
choice [1]: 1
PHP version:
  1) 8.3.32
  2) 8.3.33
  3) 8.4.24
  4) 8.4.25 (default)
  5) 8.5.10
  6) 8.5.9
choice [4]: 
Optional extensions (comma-separated numbers, empty = none):
  1) date — Real ext/date: the DateTime class and the date/time API (a UTC stub covers the core when off).
  2) ctype — Character-class checks (ctype_alpha, ctype_digit, ...).
  3) filter — filter_var() validation and sanitization.
  4) tokenizer — The PHP tokenizer: token_get_all(), token_name(), the PhpToken class and the T_* constants, over the engine's own lexer. Wanted by frameworks and static-analysis tools.
  5) session — Sessions: session_start(), $_SESSION, session_id(), and save handlers -- the files handler (point session.save_path at a writable dir like the microSD) and user handlers (session_set_save_handler).
  6) opcache — Zend OPcache (no JIT): compiled bytecode is cached so each request skips re-tokenizing/compiling the sources -- a big win for frameworks where the per-request cost is compilation. Default is file-cache on the microSD (bytecode on the card, full PSRAM free for the request); see the in_memory setting.
  7) mbstring — Multibyte strings (mb_strlen/substr/convert/detect/...). Built without mb_ereg unless the onig setting is on.
  8) openssl — The openssl_* functions. Default is a small mbedTLS-backed subset (symmetric AES: openssl_encrypt/decrypt, cipher_iv_length, random_pseudo_bytes) -- enough for e.g. Laravel's Encrypter. The `full` setting builds the real ext/openssl instead (RSA/X.509/...). See docs/openssl.md.
  9) sqlite — SQLite: read/write .db files. Default API is PDO (ext/pdo + pdo_sqlite); the 'sqlite3' type selects the SQLite3-class API instead (~45 KB smaller, no PDO).
  10) led — Addressable LEDs from PHP: Baremetal\Led\Driver\* (WS2812, SK6812) + the Output\Led capability. Pin and pixel count are passed at construction.
  11) wifi — WiFi from PHP: scan/join a network (STA) and create one (SoftAP). WiFi-capable SoCs only (not the ESP32-P4).
  12) i2c — I2C from PHP: Baremetal\I2c\Bus and \Device (scan/probe/read/write/readReg/writeReg), with a driver framework. Pins are passed at construction.
  13) spi — SPI/QSPI from PHP: Baremetal\Spi\Bus and \Device (transfer/write/read), plus the Output\Display capability and framebuffer panel drivers. Pins are passed at construction.
  14) web — HTTP server from PHP in the event-driven model: Baremetal\Http\Request/\Response + serve_http(). Requests arrive as Request events; the handler returns a Response. Needs a board with a network.
choices: 
Serial port (empty = autodetect) []: 
Starter PHP:
  1) hello
  2) blink
choice [1]: 
created php-hello/php-baremetal.config.toml

Next: cd php-hello && phpflash build

下一步,编译!

cd php-hello
../tools/phpflash build

等待编译完成后,烧录进 ESP32:

../tools/phpflash flash -p /dev/ttyACM0

注意检查不要让别的程序占用 TTY ,TTY 只能被一个程序占用,如果有别的程序占用,烧录会报错。
烧录完成后,连接 TTY 查看启动信息:

../tools/phpflash monitor -p /dev/ttyACM0

看到启动信息如下,说明就成功了:

I (24) boot: ESP-IDF v5.5.5 2nd stage bootloader
I (25) boot: compile time Oct 31 2026 12:23:36
I (25) boot: Multicore bootloader
I (25) boot: chip revision: v0.2
I (28) boot: efuse block revision: v1.4
I (31) boot.esp32s3: Boot SPI Speed : 80MHz
I (35) boot.esp32s3: SPI Mode       : DIO
I (39) boot.esp32s3: SPI Flash Size : 16MB
I (43) boot: Enabling RNG early entropy source...
I (47) boot: Partition Table:
I (50) boot: ## Label            Usage          Type ST Offset   Length
I (56) boot:  0 nvs              WiFi data        01 02 00009000 00006000
I (63) boot:  1 phy_init         RF data          01 01 0000f000 00001000
I (69) boot:  2 factory          factory app      00 00 00010000 00fd0000
I (76) boot:  3 storage          Unknown data     01 81 00fe0000 00020000
I (82) boot: End of partition table
I (86) esp_image: segment 0: paddr=00010020 vaddr=3c1f0020 size=bfa48h (784968) map
I (236) esp_image: segment 1: paddr=000cfa70 vaddr=3fc94b00 size=005a8h (  1448) load
I (237) esp_image: segment 2: paddr=000d0020 vaddr=42000020 size=1ef420h (2028576) map
I (610) esp_image: segment 3: paddr=002bf448 vaddr=3fc950a8 size=048ach ( 18604) load
I (614) esp_image: segment 4: paddr=002c3cfc vaddr=40374000 size=10ac0h ( 68288) load
I (630) esp_image: segment 5: paddr=002d47c4 vaddr=50000000 size=00020h (    32) load
I (637) boot: Loaded app from partition at offset 0x10000
I (637) boot: Disabling RNG early entropy source...
I (648) octal_psram: vendor id    : 0x0d (AP)
I (648) octal_psram: dev id       : 0x02 (generation 3)
I (649) octal_psram: density      : 0x03 (64 Mbit)
I (651) octal_psram: good-die     : 0x01 (Pass)
I (655) octal_psram: Latency      : 0x01 (Fixed)
I (659) octal_psram: VCC          : 0x01 (3V)
I (663) octal_psram: SRF          : 0x01 (Fast Refresh)
I (668) octal_psram: BurstType    : 0x01 (Hybrid Wrap)
I (673) octal_psram: BurstLen     : 0x01 (32 Byte)
I (678) octal_psram: Readlatency  : 0x02 (10 cycles@Fixed)
I (683) octal_psram: DriveStrength: 0x00 (1/1)
I (687) MSPI Timing: Enter psram timing tuning
I (692) esp_psram: Found 8MB PSRAM device
I (695) esp_psram: Speed: 80MHz
I (698) cpu_start: Multicore app
I (1133) esp_psram: SPI SRAM memory test OK
I (1142) cpu_start: GPIO 44 and 43 are used as console UART I/O pins
I (1142) cpu_start: Pro cpu start user code
I (1142) cpu_start: cpu freq: 160000000 Hz
I (1144) app_init: Application information:
I (1148) app_init: Project name:     php-esp32
I (1152) app_init: App version:      v1.5.0-9-g4ffc4a0-dirty
I (1158) app_init: Compile time:     Oct 31 2026 12:23:21
I (1163) app_init: ELF file SHA256:  f27c58e03...
I (1167) app_init: ESP-IDF:          v5.5.5
I (1171) efuse_init: Min chip rev:     v0.0
I (1175) efuse_init: Max chip rev:     v0.99 
I (1179) efuse_init: Chip rev:         v0.2
I (1183) heap_init: Initializing. RAM available for dynamic allocation:
I (1189) heap_init: At 3FCB15C0 len 00038150 (224 KiB): RAM
I (1195) heap_init: At 3FCE9710 len 00005724 (21 KiB): RAM
I (1200) heap_init: At 3FCF0000 len 00008000 (32 KiB): DRAM
I (1205) heap_init: At 600FE000 len 00001FE8 (7 KiB): RTCRAM
I (1211) esp_psram: Adding pool of 8192K of PSRAM memory to heap allocator
I (1218) spi_flash: detected chip: boya
I (1221) spi_flash: flash io: dio
I (1224) sleep_gpio: Configure to isolate all GPIO pins in sleep state
I (1230) sleep_gpio: Enable automatic switching of GPIO sleep configuration
I (1237) main_task: Started on CPU0
I (1257) esp_psram: Reserving pool of 32K of internal memory for DMA/internal allocations
I (1257) main_task: Calling app_main()
I (1257) php-esp32: starting PHP runtime
I (1267) php-esp32: php_task pinned to core 0
I (1277) php-esp32: site filesystem: 84 KiB total, 68 KiB free
I (1277) php-esp32: writable embedded FAT+WL mounted at /app (128 KiB partition)
I (1277) php-esp32: embedded source mounted at /app
I (1287) php-esp32: php_embed_init()...
PHP 8.4.25 on ESP32-S3
--- /app/index.php ---
Hello from PHP on ESP32
--- end ---
I (1517) main_task: Returned from app_main()
I (1567) php-esp32: done -- heap free: 8594727 bytes

这其中,Hello from PHP on ESP32 就是我们要的结果了。

让固件自动连WIFI

确认了 PHP 固件可以正常运行,接下来创建另一个项目,测试 WIFI 连接和网页能力:

./tools/phpflash init php-web

创建的过程如下:

Project name [php-web]: 
Chip:
  1) ESP32-P4 (esp32-p4)
  2) ESP32-S3 (esp32-s3)
choice [1]: 2
Board preset:
  1) Espressif ESP32-S3-DevKitC-1 (N16R8) (espressif-esp32-s3-devkitc-1-n16r8)
  2) Espressif ESP32-S3-DevKitC-1 (N8R2) (espressif-esp32-s3-devkitc-1-n8r2)
  3) Espressif ESP32-S3-DevKitC-1 (N8R8) (espressif-esp32-s3-devkitc-1-n8r8)
  4) ESP32-S3 (octal PSRAM, generic) (generic)
  5) ESP32-S3 (quad PSRAM, generic) (generic)
  6) Waveshare ESP32-S3-ETH (waveshare-esp32-s3-eth)
  7) Waveshare ESP32-S3-Touch-LCD-1.85B (waveshare-esp32-s3-touch-lcd-1.85b)
  8) Waveshare ESP32-S3-Zero (waveshare-esp32-s3-zero)
  9) Waveshare ESP32-S3-Zero (N8R8) (waveshare-esp32-s3-zero-n8r8)
choice [1]: 1
Storage type:
  1) embedded
choice [1]: 1
Project type:
  1) init-loop
  2) web-server
  3) event-driven
choice [1]: 2
PHP version:
  1) 8.3.32
  2) 8.3.33
  3) 8.4.24
  4) 8.4.25 (default)
  5) 8.5.10
  6) 8.5.9
choice [4]: 4
Optional extensions (comma-separated numbers, empty = none):
  1) date — Real ext/date: the DateTime class and the date/time API (a UTC stub covers the core when off).
  2) ctype — Character-class checks (ctype_alpha, ctype_digit, ...).
  3) filter — filter_var() validation and sanitization.
  4) tokenizer — The PHP tokenizer: token_get_all(), token_name(), the PhpToken class and the T_* constants, over the engine's own lexer. Wanted by frameworks and static-analysis tools.
  5) session — Sessions: session_start(), $_SESSION, session_id(), and save handlers -- the files handler (point session.save_path at a writable dir like the microSD) and user handlers (session_set_save_handler).
  6) opcache — Zend OPcache (no JIT): compiled bytecode is cached so each request skips re-tokenizing/compiling the sources -- a big win for frameworks where the per-request cost is compilation. Default is file-cache on the microSD (bytecode on the card, full PSRAM free for the request); see the in_memory setting.
  7) mbstring — Multibyte strings (mb_strlen/substr/convert/detect/...). Built without mb_ereg unless the onig setting is on.
  8) openssl — The openssl_* functions. Default is a small mbedTLS-backed subset (symmetric AES: openssl_encrypt/decrypt, cipher_iv_length, random_pseudo_bytes) -- enough for e.g. Laravel's Encrypter. The `full` setting builds the real ext/openssl instead (RSA/X.509/...). See docs/openssl.md.
  9) sqlite — SQLite: read/write .db files. Default API is PDO (ext/pdo + pdo_sqlite); the 'sqlite3' type selects the SQLite3-class API instead (~45 KB smaller, no PDO).
  10) led — Addressable LEDs from PHP: Baremetal\Led\Driver\* (WS2812, SK6812) + the Output\Led capability. Pin and pixel count are passed at construction.
  11) wifi — WiFi from PHP: scan/join a network (STA) and create one (SoftAP). WiFi-capable SoCs only (not the ESP32-P4).
  12) i2c — I2C from PHP: Baremetal\I2c\Bus and \Device (scan/probe/read/write/readReg/writeReg), with a driver framework. Pins are passed at construction.
  13) spi — SPI/QSPI from PHP: Baremetal\Spi\Bus and \Device (transfer/write/read), plus the Output\Display capability and framebuffer panel drivers. Pins are passed at construction.
  14) web — HTTP server from PHP in the event-driven model: Baremetal\Http\Request/\Response + serve_http(). Requests arrive as Request events; the handler returns a Response. Needs a board with a network.
choices: 11
Serial port (empty = autodetect) []: 
Starter PHP:
  1) hello
  2) blink
choice [1]: 1
created php-web/php-baremetal.config.toml

Next: cd php-web && phpflash build

创建完成后,先别急着编译,需要进行一些配置。
首先,打开 php-web/php-baremetal.config.toml ,增加下面的内容:

[web-server]
init = "init.php"

然后,在 php-web/project-src 目录,创建 init.php:

<?php

echo "Starting Wi-Fi...\n";

if (!function_exists('wifi_available') || !wifi_available()) {
    echo "ERROR: Wi-Fi extension unavailable!\n";
    return;
}

$ssid = getenv('WIFI_SSID');
$password = getenv('WIFI_PASSWORD');

if (!$ssid) {
    echo "ERROR: WIFI_SSID missing!\n";
    return;
}

if (wifi_connect($ssid, $password ?: null)) {
    echo "Wi-Fi connected!\n";
    echo "IP: " . wifi_ip() . "\n";
} else {
    echo "Wi-Fi connection failed!\n";
}

这个 init.php 会在启动的时候执行一次,进行 WIFI 的连接操作,对应地,我们还要写入 WIFI 的名称和密码的配置,才能正确链接。
创建 php-web\.env 文件,写入你的 WIFI 名称和密码:

WIFI_SSID=MikuNet
WIFI_PASSWORD=www.nekopara.uk

搞定后,编译:

cd php-web
../tools/phpflash build

一样的方法,烧录进 ESP32:

../tools/phpflash flash -p /dev/ttyACM0

烧录完成后,一样的,连接 TTY 查看启动信息:

../tools/phpflash monitor -p /dev/ttyACM0

看到启动信息有连接 WIFI 信息的输出,说明就成功了:

I (24) boot: ESP-IDF v5.5.5 2nd stage bootloader
I (25) boot: compile time Oct 31 2026 13:20:54
I (25) boot: Multicore bootloader
I (25) boot: chip revision: v0.2
I (28) boot: efuse block revision: v1.4
I (31) boot.esp32s3: Boot SPI Speed : 80MHz
I (35) boot.esp32s3: SPI Mode       : DIO
I (39) boot.esp32s3: SPI Flash Size : 16MB
I (43) boot: Enabling RNG early entropy source...
I (47) boot: Partition Table:
I (50) boot: ## Label            Usage          Type ST Offset   Length
I (56) boot:  0 nvs              WiFi data        01 02 00009000 00006000
I (63) boot:  1 phy_init         RF data          01 01 0000f000 00001000
I (69) boot:  2 factory          factory app      00 00 00010000 00fd0000
I (76) boot:  3 storage          Unknown data     01 81 00fe0000 00020000
I (82) boot: End of partition table
I (86) esp_image: segment 0: paddr=00010020 vaddr=3c260020 size=d105ch (856156) map
I (249) esp_image: segment 1: paddr=000e1084 vaddr=3fc9c500 size=06e88h ( 28296) load
I (255) esp_image: segment 2: paddr=000e7f14 vaddr=40374000 size=08104h ( 33028) load
I (263) esp_image: segment 3: paddr=000f0020 vaddr=42000020 size=252c64h (2436196) map
I (708) esp_image: segment 4: paddr=00342c8c vaddr=4037c104 size=1034ch ( 66380) load
I (723) esp_image: segment 5: paddr=00352fe0 vaddr=50000000 size=00020h (    32) load
I (733) boot: Loaded app from partition at offset 0x10000
I (734) boot: Disabling RNG early entropy source...
I (744) octal_psram: vendor id    : 0x0d (AP)
I (744) octal_psram: dev id       : 0x02 (generation 3)
I (744) octal_psram: density      : 0x03 (64 Mbit)
I (746) octal_psram: good-die     : 0x01 (Pass)
I (751) octal_psram: Latency      : 0x01 (Fixed)
I (755) octal_psram: VCC          : 0x01 (3V)
I (759) octal_psram: SRF          : 0x01 (Fast Refresh)
I (764) octal_psram: BurstType    : 0x01 (Hybrid Wrap)
I (769) octal_psram: BurstLen     : 0x01 (32 Byte)
I (773) octal_psram: Readlatency  : 0x02 (10 cycles@Fixed)
I (779) octal_psram: DriveStrength: 0x00 (1/1)
I (783) MSPI Timing: Enter psram timing tuning
I (788) esp_psram: Found 8MB PSRAM device
I (791) esp_psram: Speed: 80MHz
I (794) cpu_start: Multicore app
I (1229) esp_psram: SPI SRAM memory test OK
I (1238) cpu_start: GPIO 44 and 43 are used as console UART I/O pins
I (1238) cpu_start: Pro cpu start user code
I (1238) cpu_start: cpu freq: 160000000 Hz
I (1240) app_init: Application information:
I (1244) app_init: Project name:     php-esp32
I (1248) app_init: App version:      v1.5.0-9-g4ffc4a0-dirty
I (1254) app_init: Compile time:     Oct 31 2026 13:20:39
I (1259) app_init: ELF file SHA256:  6ce5e4f41...
I (1263) app_init: ESP-IDF:          v5.5.5
I (1267) efuse_init: Min chip rev:     v0.0
I (1271) efuse_init: Max chip rev:     v0.99 
I (1275) efuse_init: Chip rev:         v0.2
I (1279) heap_init: Initializing. RAM available for dynamic allocation:
I (1285) heap_init: At 3FCC17C0 len 00027F50 (159 KiB): RAM
I (1291) heap_init: At 3FCE9710 len 00005724 (21 KiB): RAM
I (1296) heap_init: At 3FCF0000 len 00008000 (32 KiB): DRAM
I (1301) heap_init: At 600FE000 len 00001FE8 (7 KiB): RTCRAM
I (1307) esp_psram: Adding pool of 8192K of PSRAM memory to heap allocator
I (1314) spi_flash: detected chip: boya
I (1317) spi_flash: flash io: dio
I (1320) sleep_gpio: Configure to isolate all GPIO pins in sleep state
I (1326) sleep_gpio: Enable automatic switching of GPIO sleep configuration
I (1333) main_task: Started on CPU0
I (1353) esp_psram: Reserving pool of 32K of internal memory for DMA/internal allocations
I (1353) main_task: Calling app_main()
I (1353) php-esp32: starting PHP runtime
I (1363) php-esp32: php_task pinned to core 0
I (1373) php-esp32: site filesystem: 84 KiB total, 64 KiB free
I (1373) php-esp32: writable embedded FAT+WL mounted at /app (128 KiB partition)
I (1373) php-esp32: embedded source mounted at /app
I (1383) php-esp32: applied 2 env var(s) from .env
I (1383) php-esp32: php_embed_init()...
PHP 8.4.25 on ESP32-S3
--- web-server init: /app/init.php ---
Starting Wi-Fi...
I (1563) pp: pp rom version: e7ae62f
I (1563) net80211: net80211 rom version: e7ae62f
I (1563) main_task: Returned from app_main()
I (1573) wifi:wifi driver task: 3fcee70c, prio:23, stack:6656, core=0
I (1583) wifi:wifi firmware version: b9f67df
I (1583) wifi:wifi certification version: v7.0
I (1583) wifi:config NVS flash: enabled
I (1583) wifi:config nano formatting: disabled
I (1583) wifi:Init data frame dynamic rx buffer num: 32
I (1593) wifi:Init static rx mgmt buffer num: 5
I (1593) wifi:Init management short buffer num: 32
I (1603) wifi:Init dynamic tx buffer num: 32
I (1603) wifi:Init static tx FG buffer num: 2
I (1613) wifi:Init static rx buffer size: 1600
I (1613) wifi:Init static rx buffer num: 10
I (1613) wifi:Init dynamic rx buffer num: 32
I (1623) wifi_init: rx ba win: 6
I (1623) wifi_init: accept mbox: 6
I (1623) wifi_init: tcpip mbox: 32
I (1633) wifi_init: udp mbox: 6
I (1633) wifi_init: tcp mbox: 6
I (1633) wifi_init: tcp tx win: 5760
I (1643) wifi_init: tcp rx win: 5760
I (1643) wifi_init: tcp mss: 1440
I (1643) wifi_init: WiFi IRAM OP enabled
I (1653) wifi_init: WiFi RX IRAM OP enabled
I (1653) phy_init: phy_version 712,87e8c20e,Apr 13 2026,18:51:10
I (1703) wifi:mode : sta (1c:29:04:3d:e4:dc)
I (1703) wifi:enable tsf
I (4113) wifi:new:<6,0>, old:<1,0>, ap:<255,255>, sta:<6,0>, prof:1, snd_ch_cfg:0x0
I (4113) wifi:state: init -> auth (0xb0)
I (4253) wifi:state: auth -> assoc (0x0)
I (4263) wifi:state: assoc -> run (0x10)
I (4273) wifi:connected with MikuNet, aid = 1, channel 6, BW20, bssid = 24:cf:24:da:9b:23
I (4273) wifi:security: WPA2-PSK, phy: bgn, rssi: -25, cipher(pairwise:0x3, group:0x3), pmf:0
I (4283) wifi:pm start, type: 1

I (4283) wifi:dp: 1, bi: 102400, li: 3, scale listen interval from 307200 us to 307200 us
I (4293) wifi:set rx beacon pti, rx_bcn_pti: 0, bcn_timeout: 25000, mt_pti: 0, mt_time: 10000
I (4353) wifi:dp: 2, bi: 102400, li: 4, scale listen interval from 307200 us to 409600 us
I (4353) wifi:AP's beacon interval = 102400 us, DTIM period = 2
I (4803) wifi:<ba-add>idx:0 (ifx:0, 24:cf:24:da:9b:23), tid:0, ssn:0, winSize:64
I (4803) wifi:<ba-add>idx:1 (ifx:0, 24:cf:24:da:9b:23), tid:6, ssn:2, winSize:64
I (5813) esp_netif_handlers: sta ip: 192.168.8.166, mask: 255.255.255.0, gw: 192.168.8.1
Wi-Fi connected!
IP: 192.168.8.166
--- web-server init done ---
I (5813) php-esp32: httpd pinned to core 1 (php_task on core 0)
I (5813) php-esp32: web-server model: serving /app/index.php over HTTP on :80

这里可以看到,WIFI 成功连接,IP 地址是 192.168.8.166!
打开浏览器,输入 ESP32 的 IP 地址,能看到文本:Hello from PHP on ESP32 说明操作成功。

接下来就是把 Typecho 塞进去了,但是事情没有我想象中那么简单。。。

编译 Typecho PHP 固件

在原有的基础上,复制一份已经成功验证的 Web 服务器配置,用来配置 Typecho 固件:

mkdir -p ../php-typecho/project-src

cp php-baremetal.config.toml ../php-typecho/
cp .env ../php-typecho/
cp project-src/init.php ../php-typecho/project-src/

然后,把 Typecho 解压到 php-typecho/project-src 目录下。
接下来,编辑 php-baremetal.config.toml 文件,增加 Typecho 需要的 PHP 扩展:

[extensions.sqlite]
enabled = true

[extensions.mbstring]
enabled = true

[extensions.ctype]
enabled = true

[extensions.filter]
enabled = true

[extensions.date]
enabled = true

[extensions.session]
enabled = true

[extensions.tokenizer]
enabled = true

尝试编译:

../tools/phpflash build

结果报错:

[15/1396] cd /run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32/php-typecho/build/compiled && /run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32/toolchains/.espressif/python_env/idf5.5_py3.13_env/bin/python /run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32/toolchains/esp-idf/components/fatfs/wl_fatfsgen.py /run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32/php-typecho/project-src --long_name_support --use_default_datetime --partition_size 0x270000 --output_file /run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32/php-typecho/build/compiled/storage.bin --sector_size 4096
FAILED: CMakeFiles/fatfs_storage_bin /run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32/php-typecho/build/compiled/CMakeFiles/fatfs_storage_bin 
cd /run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32/php-typecho/build/compiled && /run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32/toolchains/.espressif/python_env/idf5.5_py3.13_env/bin/python /run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32/toolchains/esp-idf/components/fatfs/wl_fatfsgen.py /run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32/php-typecho/project-src --long_name_support --use_default_datetime --partition_size 0x270000 --output_file /run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32/php-typecho/build/compiled/storage.bin --sector_size 4096
Traceback (most recent call last):
  File "/run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32/toolchains/esp-idf/components/fatfs/wl_fatfsgen.py", line 247, in <module>
    wl_fatfs.plain_fatfs.generate(args.input_directory)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
  File "/run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32/toolchains/esp-idf/components/fatfs/fatfsgen.py", line 221, in generate
    self._generate_partition_from_folder(folder_name, folder_path=path_to_folder, is_dir=True)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32/toolchains/esp-idf/components/fatfs/fatfsgen.py", line 30, in wrapper
    func(self, *args, **kwargs)
    ~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File "/run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32/toolchains/esp-idf/components/fatfs/fatfsgen.py", line 214, in _generate_partition_from_folder
    self._generate_partition_from_folder(os.path.join(lower_path, path), folder_path=folder_path)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32/toolchains/esp-idf/components/fatfs/fatfsgen.py", line 30, in wrapper
    func(self, *args, **kwargs)
    ~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File "/run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32/toolchains/esp-idf/components/fatfs/fatfsgen.py", line 214, in _generate_partition_from_folder
    self._generate_partition_from_folder(os.path.join(lower_path, path), folder_path=folder_path)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32/toolchains/esp-idf/components/fatfs/fatfsgen.py", line 30, in wrapper
    func(self, *args, **kwargs)
    ~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File "/run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32/toolchains/esp-idf/components/fatfs/fatfsgen.py", line 214, in _generate_partition_from_folder
    self._generate_partition_from_folder(os.path.join(lower_path, path), folder_path=folder_path)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32/toolchains/esp-idf/components/fatfs/fatfsgen.py", line 30, in wrapper
    func(self, *args, **kwargs)
    ~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File "/run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32/toolchains/esp-idf/components/fatfs/fatfsgen.py", line 204, in _generate_partition_from_folder
    self.write_content(split_path[1:], content)
    ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32/toolchains/esp-idf/components/fatfs/fatfsgen.py", line 30, in wrapper
    func(self, *args, **kwargs)
    ~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File "/run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32/toolchains/esp-idf/components/fatfs/fatfsgen.py", line 152, in write_content
    self.root_directory.write_to_file(path_from_root, content)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32/toolchains/esp-idf/components/fatfs/fatfs_utils/fs_object.py", line 340, in write_to_file
    self.fat.allocate_chain(entity_to_write.first_cluster, clusters_cnt)
    ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32/toolchains/esp-idf/components/fatfs/fatfs_utils/fat.py", line 97, in allocate_chain
    free_cluster = self.find_free_cluster()
  File "/run/media/chocola/3T-DATA02/Projects/ESP32/Typecho_on_ESP32/toolchains/esp-idf/components/fatfs/fatfs_utils/fat.py", line 83, in find_free_cluster
    raise NoFreeClusterException('No free cluster available!')
fatfs_utils.exceptions.NoFreeClusterException: No free cluster available!

询问 ChatGPT 得知,原因是 Typecho 的文件太多,自动生成的 FAT 文件系统镜像容量不足,导致编译失败。
修改 php-baremetal.config.toml 调整文件系统大小,找到:

[esp32.storage]
microsd = false   # embedded: also mount a microSD for writable data (off = no SD support compiled)
reserve_kb = 0   # extra KB in the flash `storage` partition on top of the source (0 = just fit it)

改成:

microsd = false   # embedded: also mount a microSD for writable data (off = no SD support compiled)
reserve_kb = 2048   # extra KB in the flash `storage` partition on top of the source (0 = just fit it)

再次尝试编译:

../tools/phpflash build

这次就编译成功了,依旧刷写:

../tools/phpflash flash -p /dev/ttyACM0

但是,打开浏览器,并不是我想要的 Typecho 安装界面,而是一行字:

Missing Config File

那看来得让 Typecho 适配一下 ESP32 的 PHP 环境了。

Typecho 适配 ESP32 PHP

改动其实不算多,修改了 5 个文件,修改的版本是 Typecho 1.3.0。

index.php

首先是index.php,将:

if (!defined('__TYPECHO_ROOT_DIR__') && !@include_once 'config.inc.php') {
    file_exists('./install.php') ? header('Location: install.php') : print('Missing Config File');
    exit;
}

改为:

$path = parse_url(
    $_SERVER['REQUEST_URI'] ?? '/',
    PHP_URL_PATH
) ?: '/';

// 将安装请求转交给 Typecho 安装器
if ($path === '/install.php') {
    require __DIR__ . '/install.php';
    return;
}

// 未安装时跳转到安装页面
$config = __DIR__ . '/config.inc.php';

if (!defined('__TYPECHO_ROOT_DIR__')) {
    if (!is_file($config)) {
        header('Location: /install.php', true, 302);
        return;
    }

    require_once $config;
}

改动情况:

  • 用 parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) 显式解析请求路径,把 /install.php 显式 require 后 return,避免进入路由分发。
  • 移除 @include_once 'config.inc.php' 的静默加载,改用 is_file($config) 显式判断后再 require_once;未安装时发送 302 跳转到 /install.php。

原因:

  • ESP32 嵌入式 SAPI 的 REQUEST_URI 处理与标准 PHP-CGI/Apache 不同,需显式解析路径才能正确区分安装请求与正常请求。
  • @include_once 的静默失败在嵌入式环境下错误诊断困难,改用显式 is_file 判断更可靠。

install.php

然后是install.php,需要修改四个地方:

1.安装表单 AJAX 增加 dataType: 'json'
原版:

            $.ajax({
                url: form.attr('action'),
                processData: false,
                contentType: false,
                type: 'POST',
                data: new FormData(this),
                success: function (data) {

适配版:

            $.ajax({
                url: form.attr('action'),
                   processData: false,
                   contentType: false,
                   type: 'POST',
                   dataType: 'json',
                   data: new FormData(this),
                   success: function (data) {

改动情况: 给安装表单的 jQuery AJAX 显式声明 dataType: 'json'。
原因: 配合下文 Response.php 的 Content-Type 修复,确保 jQuery 把安装器返回的 JSON 正确解析为对象而非字符串。

2.安装适配器脚本改用绝对路径
原版:

                <?php require_once './install/' . $type . '.php'; ?>

适配版:

                <?php require_once __TYPECHO_ROOT_DIR__ . '/install/' . $type . '.php'; ?>

改动情况: require_once 的基准从相对路径 ./install/ 改为基于 __TYPECHO_ROOT_DIR__ 的绝对路径。
原因: 嵌入式环境的当前工作目录(CWD)不一定是站点根,相对路径 ./install/... 可能找不到文件。

3.install_step_2_perform 建表阶段包裹 SQLite 事务
原版:

    // init db structure
    try {
        $scripts = file_get_contents(__TYPECHO_ROOT_DIR__ . '/install/' . $type . '.sql');
        $scripts = str_replace('typecho_', $config['dbPrefix'], $scripts);
        // ... 逐条执行建表 SQL ...
    }

    install_success(3);
}

适配版:

    // init db structure
    try {
        if ($type === 'SQLite') {
            $installDb->query('BEGIN IMMEDIATE', \Typecho\Db::WRITE);
        }
        $scripts = file_get_contents(__TYPECHO_ROOT_DIR__ . '/install/' . $type . '.sql');
        $scripts = str_replace('typecho_', $config['dbPrefix'], $scripts);
        // ... 逐条执行建表 SQL ...
    }

    if ($type === 'SQLite') {
        $installDb->query('COMMIT', \Typecho\Db::WRITE);
    }
    install_success(3);
}

改动情况: 当数据库类型为 SQLite 时,在执行建表 SQL 前发 BEGIN IMMEDIATE,全部建表完成后发 COMMIT。

4.install_step_3_perform 初始数据写入阶段包裹 SQLite 事务
原版:

    try {
        // write user
        $hasher = new \Utils\PasswordHash(8, true);
        $installDb->query(/* ... 写入 user/contents/options 等初始数据 ... */);

        foreach ($options as $key => $value) {
            // ...
            $installDb->insert('table.options')->rows(['name' => $key, 'user' => 0, 'value' => $value])
            );
        }
    } catch (\Typecho\Db\Exception $e) {
        install_raise_error($e->getMessage());
    }

适配版:

    try {
        // Commit the initial site data together, reducing Flash erase/sync cycles.
        $sqlite = install_get_db_type(install_get_current_db_driver()) === 'SQLite';
        if ($sqlite) {
            $installDb->query('BEGIN IMMEDIATE', \Typecho\Db::WRITE);
        }
        // write user
        $hasher = new \Utils\PasswordHash(8, true);
        $installDb->query(/* ... 写入 user/contents/options 等初始数据 ... */);

        foreach ($options as $key => $value) {
            // ...
            $installDb->insert('table.options')->rows(['name' => $key, 'user' => 0, 'value' => $value])
            );
        }
        if ($sqlite) {
            $installDb->query('COMMIT', \Typecho\Db::WRITE);
        }
    } catch (\Typecho\Db\Exception $e) {
        install_raise_error($e->getMessage());
    }

改动情况: 初始站点数据(用户、首篇文章、站点选项等)的批量写入同样用 BEGIN IMMEDIATE / COMMIT 包成单个事务。
原因: ESP32 用 Flash/FATFS 存储数据库文件,每次写入都会触发 Flash 擦除/同步周期。原版逐条建表 + 逐条插数据会产生几十次独立写入;合并为单个事务后只在事务提交时同步一次,大幅减少 Flash 磨损和安装耗时。BEGIN IMMEDIATE 比 BEGIN DEFERRED 更早获取写锁,避免中途遇到锁升级失败。

Common.php

var/Typecho/Common.php:Common::slug(生成 URL 友好别名)中 Unicode 单词匹配的回退分支。
原版:

            mb_regex_encoding('UTF-8');
            mb_ereg_search_init($str, "[\w" . preg_quote('_-') . "]+");
            $result = mb_ereg_search();
            $return = '';

            if ($result) {
                $regs = mb_ereg_search_getregs();
                $pos = 0;
                do {
                    $return .= ($pos > 0 ? '-' : '') . $regs[0];
                    $pos++;
                } while ($regs = mb_ereg_search_regs());
            }

            $str = trim($return, '-_');

适配版:

            if (function_exists('mb_ereg_search_init')) {
                mb_regex_encoding('UTF-8');
                mb_ereg_search_init($str, "[\w" . preg_quote('_-') . "]+");
                $result = mb_ereg_search();
                $return = '';

                if ($result) {
                    $regs = mb_ereg_search_getregs();
                    $pos = 0;
                    do {
                        $return .= ($pos > 0 ? '-' : '') . $regs[0];
                        $pos++;
                    } while ($regs = mb_ereg_search_regs());
                }
            } else {
                // ESP32 keeps mbstring without the separate Oniguruma engine.
                // PCRE already supports UTF-8 Unicode word characters.
                preg_match_all('/[\p{L}\p{N}\p{M}\p{Pc}-]+/u', $str, $matches);
                $return = implode('-', $matches[0]);
            }

            $str = trim($return, '-_');

改动情况:

  • 用 function_exists('mb_ereg_search_init') 守卫原逻辑;
  • 函数不存在时改用 PCRE 回退:preg_match_all('/[\p{L}\p{N}\p{M}\p{Pc}-]+/u', $str, $matches) 后用 - 连接。

原因: ESP32 固件里的 PHP 启用了 mbstring 扩展,但没有链接独立的 Oniguruma 正则引擎(mb_ereg 系列依赖它)。PCRE 在 /u UTF-8 模式下已能通过 Unicode 属性类(\p{L} 字母、\p{N} 数字、\p{M} 附加符号、\p{Pc} 连接符)匹配原版 [\w_-]+ 的语义,作为零依赖回退实现。

SQLite.php

var/Typecho/Db/Adapter/Pdo/SQLite.php:SQLite PDO 适配器 init() 方法中 DSN 构造与连接管理。
原版:

class SQLite extends Pdo
{
    use SQLiteTrait;

    /**
     * 判断适配器是否可用
     * ...
     */
    public function init(Config $config): \PDO
    {
        $pdo = new \PDO("sqlite:{$config->file}");
        $this->isSQLite2 = version_compare($pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '3.0.0', '<');
        return $pdo;
    }

适配版:

class SQLite extends Pdo
{
    use SQLiteTrait;

    /** Reuse each file handle for reads and writes on the serialized ESP32 SAPI. */
    private array $esp32Connections = [];

    /**
     * 判断适配器是否可用
     * ...
     */
    public function init(Config $config): \PDO
    {
        $embedded = function_exists('wifi_available');
        if ($embedded && isset($this->esp32Connections[$config->file])) {
            return $this->esp32Connections[$config->file];
        }
        // ESP-IDF FATFS implements POSIX lock calls as no-ops. SQLite's Unix
        // lock probing otherwise waits for a nonexistent competing process.
        // The firmware serializes all PHP requests on a single task.
        $dsn = $embedded
            ? "sqlite:file:{$config->file}?nolock=1"
            : "sqlite:{$config->file}";
        $pdo = new \PDO($dsn);
        $this->isSQLite2 = version_compare($pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '3.0.0', '<');
        if ($embedded) {
            $this->esp32Connections[$config->file] = $pdo;
        }
        return $pdo;
    }

改动情况:

  • 新增私有属性 $esp32Connections = [],按文件名缓存 PDO 句柄。
  • 通过 function_exists('wifi_available') 检测是否运行在 ESP32 嵌入式环境。
  • 嵌入式环境下:命中缓存直接复用句柄;否则用 sqlite:file:<path>?nolock=1 DSN 新建并缓存。
  • 非嵌入式环境保持原 sqlite:<path> 行为。

原因:

  • nolock=1:ESP-IDF 的 FATFS 把 POSIX 文件锁调用(fcntl F_SETLK)实现为空操作。SQLite 默认的 Unix 锁探测逻辑会等待一个并不存在的竞争进程,导致请求挂起。固件本身在单一任务上串行处理所有 PHP 请求,不存在并发,文件锁无意义,直接禁用。
  • 句柄复用:嵌入式环境资源受限,且 Flash 上的数据库文件打开/关闭成本高。串行执行模型保证复用句柄是安全的,避免每次请求重复打开文件。

Response.php

var/Typecho/Response.php HTTP 响应头发送:Response::send 中 header 循环对 Content-Type 的强制覆盖。
原版:

        header('HTTP/1.1 ' . $this->status . ' ' . self::HTTP_CODE[$this->status], true, $this->status);

        // set header
        foreach ($this->headers as $name => $value) {
            if (!in_array(strtolower($name), $sentHeaders)) {
                header($name . ': ' . $value);
            }
        }

适配版:

        header('HTTP/1.1 ' . $this->status . ' ' . self::HTTP_CODE[$this->status], true, $this->status);

        // TYPECHO_ESP32_HTTP_PATCH_V1: the declared response MIME type wins.
        // Some embedded PHP SAPIs already expose a default Content-Type in
        // headers_list(); keeping that default breaks jQuery JSON responses.
        // Preserve the existing behavior for every other header.
        foreach ($this->headers as $name => $value) {
            if (strtolower($name) === 'content-type') {
                header($name . ': ' . $value, true);
            } elseif (!in_array(strtolower($name), $sentHeaders, true)) {
                header($name . ': ' . $value);
            }
        }

改动情况:

  • 对 content-type(小写比较)强制使用 header($name.': '.$value, true)(第三参数 true 表示替换同名 header),不再走"已发送则跳过"逻辑。
  • 其他 header 保留原去重逻辑,in_array 同时加了严格比较 true。
  • 加了标记注释 TYPECHO_ESP32_HTTP_PATCH_V1 便于后续审查。

原因: 嵌入式 PHP SAPI 在请求开始时可能已经默认注入了一个 Content-Type(如 text/html)到 headers_list() 中。原版逻辑会把它当作"已发送"而跳过 Typecho 自己声明的 Content-Type,导致 jQuery AJAX 收到错误的 MIME 类型、JSON 解析失败。强制覆盖 Content-Type 让 Typecho 声明的 MIME(如 application/json)生效。其他 header 不受影响,保留原去重逻辑。


改动完成了再尝试编译一版固件看看能不能正常工作,确实打开了 Typecho 的安装界面,但是点击下一步会提示:error
浏览器抓包发现会发送一个 POST 请求,返回一个 JSON:

{"success":1,"message":2,"config":null}

但是没有进行跳转。
01.png
ChatGPT 建议我尝试一下在 URL 后面加上 ?step=2 强制跳转下一步,确实可以到下一步,但是点击开始安装依然 error:
02.png
依旧是一个 POST 请求,返回 JSON:

{"success":1,"message":2,"config":null}

而且两次的 POST 状态码都是 302 ,算了吧,直接让 Codex 修。不过到这里,我已经看到了希望的曙光。

修补 php-esp32

后续我是让 Codex 继续推进这个项目了,不过在前面的折腾中,我也意识到了一个问题:似乎设置 reserve_kb = 2048 不足以充分利用空间,而且好像这个空间默认还是只读的,这对于 Typecho 来说并不合适——因为 Typecho 初始化要创建 config.inc.php 以及 .db 数据库文件,而且后续可能还有附件上传。
于是我就让 Codex 帮忙实现可用空间最大化,并且让网站目录变得可以读写,做了下面的工作:

这次改动都在 toolchains/php-esp32/ 里,共修改 4 个文件,分两类:一类是把嵌入式存储从只读改成可写(构建脚本 + boot.c),一类是让 HTTP 服务器具备真正 Web 服务器该有的路由、安全边界和请求语义(web_server.c)。

CMakeLists.txt

storage 分区的 FAT 镜像生成器要换掉,把:

    fatfs_create_rawflash_image(storage "${PHP_EMBED_SRC}" FLASH_IN_PROJECT)

改为:

    fatfs_create_spiflash_image(storage "${PHP_EMBED_SRC}" FLASH_IN_PROJECT)

改动情况: rawflash 生成只读裸 FAT 镜像,spiflash 生成带磨损均衡的可写 FAT 镜像。
原因: 只读镜像没法让 Typecho 写数据库和上传文件;换成 spiflash 才能配合下文 esp_vfs_fat_spiflash_mount_rw_wl 在 Flash 上持久写入。

gen-partitions.cmake

cmake/gen-partitions.cmake 的 app 分区前的预留空间从硬编码改成按启动模式动态计算,把:

# nvs + phy + alignment; secure boot pushes the app to 0x20000 = 128 K, so 128 K is the safe floor).
set(_pre_k 128)

改为:

# nvs + phy + alignment). Normal boot starts at 0x10000; secure boot reserves
# 0x20000. Use the actual prefix so embedded storage can reach the end of flash.
set(_pre_k 64)
if(PHP_SECURE)
    set(_pre_k 128)
endif()

改动情况: _pre_k 从固定 128 KiB 改为普通启动 64 KiB、安全启动 128 KiB。
原因: 原版一律按安全启动预留,普通启动下白白浪费 64 KiB。按实际前置偏移计算后,storage 分区能多占到 Flash 末尾的 64 KiB。

boot.c

main/boot.c 改动最多,主要是把 /app 从只读挂载改成可写 FAT+WL,再补上 Typecho 需要的 ini 默认值。

1.mount_embedded() 改用可写 WL 挂载
原版:

/* Mount the optional embedded PHP source: a read-only FAT image in the internal
 * 'storage' partition. Absent on microSD-only firmware (the partition may not exist, or
 * exist but hold no image) -- in which case this returns false and we run from the SD. */
static bool s_app_mounted;

static bool mount_embedded(void)
{
    const esp_partition_t *p = esp_partition_find_first(
        ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_FAT, "storage");
    if (!p) {
        return false;
    }
    esp_vfs_fat_mount_config_t cfg = { .max_files = 5 };
    if (esp_vfs_fat_spiflash_mount_ro(APP_MOUNT_POINT, "storage", &cfg) != ESP_OK) {
        return false;
    }
    s_app_mounted = true;
    return true;
}

适配版:

/* Mount the optional embedded PHP source as a writable FAT filesystem with wear
 * levelling on the internal 'storage' partition.
 *
 * WARNING: The partition must be provisioned with fatfs_create_spiflash_image().
 * The old fatfs_create_rawflash_image() is incompatible with this mount method.
 * Keep auto-format DISABLED: a failed mount must never silently delete Typecho data.
 */
static bool s_app_mounted;
static wl_handle_t s_app_wl = WL_INVALID_HANDLE;

static bool mount_embedded(void)
{
    const esp_partition_t *p = esp_partition_find_first(
        ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_FAT, "storage");
    if (!p) {
        ESP_LOGW(TAG, "no embedded 'storage' partition found");
        return false;
    }

    const esp_vfs_fat_mount_config_t cfg = {
        .format_if_mount_failed = false, /* Do NOT erase the user's DB/uploads. */
        .max_files = 16,                 /* PHP + SQLite may open several files. */
        .allocation_unit_size = 4096,
    };

    esp_err_t err = esp_vfs_fat_spiflash_mount_rw_wl(
        APP_MOUNT_POINT, "storage", &cfg, &s_app_wl);
    if (err != ESP_OK) {
        ESP_LOGE(TAG,
                 "failed to mount writable /app (%s): check storage image / WL format",
                 esp_err_to_name(err));
        s_app_wl = WL_INVALID_HANDLE;
        return false;
    }

    s_app_mounted = true;
    uint64_t total_bytes = 0, free_bytes = 0;
    if (esp_vfs_fat_info(APP_MOUNT_POINT, &total_bytes, &free_bytes) == ESP_OK) {
        ESP_LOGI(TAG, "site filesystem: %lu KiB total, %lu KiB free",
                 (unsigned long)(total_bytes / 1024), (unsigned long)(free_bytes / 1024));
    }
    ESP_LOGI(TAG, "writable embedded FAT+WL mounted at %s (%lu KiB partition)",
             APP_MOUNT_POINT, (unsigned long)(p->size / 1024));
    return true;
}

改动情况: 挂载 API 从 esp_vfs_fat_spiflash_mount_ro 换成 esp_vfs_fat_spiflash_mount_rw_wl,新增 wl_handle_t 句柄;format_if_mount_failed = false,max_files 从 5 提到 16,allocation_unit_size = 4096;挂载成功后打印总容量和可用容量。
原因: Typecho 要往 /app 写 SQLite 数据库、usr/uploads 和会话文件,只读挂载根本跑不起来。format_if_mount_failed=false 是关键安全开关——自动格式化会静默擦掉用户的数据库和上传,绝对不能开。max_files=16 容纳 PHP 同时打开多个模板、插件和数据库文件。

2.新增 typecho ini 默认值钩子

/* TYPECHO_ESP32_HTTP_PATCH_V1: set writable upload/session temp locations.
 * These PHP_INI_SYSTEM values must be seeded before php_embed_init(). */
static void typecho_ini_default(HashTable *ht, const char *key, const char *val)
{
    zval z;
    ZVAL_NEW_STR(&z, zend_string_init(val, strlen(val), 1));
    zend_hash_str_update(ht, key, strlen(key), &z);
}

static void typecho_ini_defaults(HashTable *ht)
{
    typecho_ini_default(ht, "upload_tmp_dir", APP_MOUNT_POINT "/usr/tmp");
    typecho_ini_default(ht, "sys_temp_dir", APP_MOUNT_POINT "/usr/tmp");
    typecho_ini_default(ht, "session.save_path", APP_MOUNT_POINT "/usr/sessions");
    typecho_ini_default(ht, "upload_max_filesize", "512K");
    typecho_ini_default(ht, "post_max_size", "768K");
#ifdef PHP_EXT_OPCACHE_ENABLED
    opcache_ini_defaults(ht);
#endif
}

改动情况: 新增 typecho_ini_defaults 钩子,注入 5 个 INI 默认值,并复用 opcache_ini_defaults。
原因: 嵌入式 SAPI 没有 php.ini,上传和会话的临时目录默认指向不可写位置,Typecho 后台上传和登录会话全都会失败。这里把 upload_tmp_dir/sys_temp_dir/session.save_path 全部指到可写的 /app/usr/tmp 与 /app/usr/sessions,上传和 POST 上限收到 512K/768K 以匹配 PSRAM 缓冲能力。

3.boot_php_runtime() 创建临时目录并安装 ini 钩子
原版:

    php_embed_module.ub_write = esp_ub_write;

适配版:

    /* Create session and upload temp dirs on the writable Flash mount. */
    if (s_app_mounted) {
        mkdir(APP_MOUNT_POINT "/usr", 0755);
        mkdir(APP_MOUNT_POINT "/usr/tmp", 0755);
        mkdir(APP_MOUNT_POINT "/usr/sessions", 0755);
    }
    php_embed_module.ub_write = esp_ub_write;
    php_embed_module.ini_defaults = typecho_ini_defaults;

改动情况: /app 挂载后先 mkdir 出 usr、usr/tmp、usr/sessions 三个目录,再把 ini_defaults 钩子设为 typecho_ini_defaults。
原因: INI 里指向的目录必须真实存在,PHP 上传和会话才不会报错;钩子必须在 php_embed_init 读取 INI 之前装好。另外两个 OPcache 分支里的 opcache_ini_defaults 也一并换成了 typecho_ini_defaults(后者内部会调 opcache_ini_defaults),让上传/会话 INI 和 OPcache INI 在同一次钩子调用里一起生效。

4.卸载改为可写 WL 卸载
原版:

    if (s_app_mounted) {
        esp_vfs_fat_spiflash_unmount_ro(APP_MOUNT_POINT, "storage");
    }

适配版:

    if (s_app_mounted) {
        esp_err_t err = esp_vfs_fat_spiflash_unmount_rw_wl(APP_MOUNT_POINT, s_app_wl);
        if (err != ESP_OK) {
            ESP_LOGE(TAG, "failed to unmount writable /app: %s", esp_err_to_name(err));
        } else {
            s_app_mounted = false;
            s_app_wl = WL_INVALID_HANDLE;
        }
    }

改动情况: 卸载 API 配合挂载改为 esp_vfs_fat_spiflash_unmount_rw_wl,传入 WL 句柄并处理错误、复位状态。
原因: 挂载和卸载必须成对使用同一族 API,否则 WL 句柄泄漏。

web_server.c

main/models/web_server.c 这个文件改的是 HTTP 服务器本身。原版基本是把请求一股脑丢给 index.php,静态文件无差别地直出,对 Typecho 来说既跑不了安装器/后台子页面,又有把数据库和配置文件直接下载下来的风险。改动围绕路由分发和安全边界展开。

1.按路由选择入口脚本
原版:

static char s_docroot[256];
    php_register_variable("SCRIPT_NAME", "/index.php", arr);
    php_register_variable("PHP_SELF", "/index.php", arr);
    php_register_variable("SCRIPT_FILENAME", (char *) s_web_script, arr);

适配版:

static char s_docroot[256];
/* TYPECHO_ESP32_HTTP_PATCH_V1: per-request script selection. Only trusted routes are executable. */
static char s_route_script[512];
static char s_route_name[1024];
    php_register_variable("SCRIPT_NAME", s_route_name, arr);
    php_register_variable("PHP_SELF", s_route_name, arr);
    php_register_variable("SCRIPT_FILENAME", s_route_script, arr);

并新增 web_select_script(),默认路由到 index.php,仅当请求是 /install.php 或 /admin/<单段>.php(无子目录、无 ../\、文件真实存在)时才切到对应文件:

static void web_select_script(void)
{
    snprintf(s_route_script, sizeof(s_route_script), "%s", s_web_script);
    snprintf(s_route_name, sizeof(s_route_name), "/index.php");
    const char *path = s_req.path;
    if (!strcmp(path, "/admin/")) path = "/admin/index.php";
    bool is_install = !strcmp(path, "/install.php");
    bool is_admin = !strncmp(path, "/admin/", 7) && path[7] != '\0'
                 && strchr(path + 7, '/') == NULL;
    size_t n = strlen(path);
    if (!is_install && !(is_admin && n >= 4 && !strcasecmp(path + n - 4, ".php"))) return;
    if (strstr(path, "..") || strchr(path, '\\')) return;
    char candidate[sizeof(s_route_script)];
    int written = snprintf(candidate, sizeof(candidate), "%s%s", s_docroot, path);
    if (written <= 0 || written >= (int) sizeof(candidate)) return;
    struct stat st;
    if (stat(candidate, &st) != 0 || !S_ISREG(st.st_mode)) return;
    snprintf(s_route_script, sizeof(s_route_script), "%s", candidate);
    snprintf(s_route_name, sizeof(s_route_name), "%s", path);
}

改动情况: SCRIPT_NAME/PHP_SELF/SCRIPT_FILENAME 从硬编码 /index.php 改为按请求选定的 s_route_name/s_route_script;web_select_script() 严格限定只有安装器和 admin/ 顶层 PHP 才直接执行,其余一律走 index.php。
原因: Typecho 安装器走 /install.php、后台走 /admin/index.php,硬编码 /index.php 会让这些入口拿不到正确的自身路径,重定向和相对资源链接全错乱;同时严格限定可执行面,禁止 /var/、嵌套插件 PHP、config.inc.php 被远程执行。

2.静态资源白名单与内部路径 403
原版: web_try_static() 直接按路径找文件直出,无任何过滤。
适配版: 新增 web_is_public_asset() 和 web_is_internal_path() 两个判定函数:

static bool web_is_public_asset(const char *path)
{
    if (!path || path[0] != '/' || strstr(path, "..") || strchr(path, '\\')) return false;
    if (!strcmp(path, "/favicon.ico") || !strcmp(path, "/robots.txt")) return true;
    static const char *const roots[] = {
        "/admin/css/", "/admin/js/", "/admin/img/", "/admin/images/", "/admin/fonts/",
        "/usr/themes/", "/usr/plugins/", "/usr/uploads/"
    };
    bool in_public_dir = false;
    for (size_t i = 0; i < sizeof(roots) / sizeof(roots[0]); i++) {
        if (!strncmp(path, roots[i], strlen(roots[i]))) {
            in_public_dir = true;
            break;
        }
    }
    if (!in_public_dir) return false;
    if (strstr(path, "/.")) return false;
    const char *dot = strrchr(path, '.');
    if (!dot) return false;
    /* Uploaded assets can be public, but never serve scripts or active SVG/JS there. */
    if (!strncmp(path, "/usr/uploads/", 13)) {
        static const char *const upload_exts[] = {
            ".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".pdf"
        };
        for (size_t i = 0; i < sizeof(upload_exts) / sizeof(upload_exts[0]); i++) {
            if (!strcasecmp(dot, upload_exts[i])) return true;
        }
        return false;
    }
    static const char *const exts[] = {
        ".css", ".js", ".mjs", ".map", ".png", ".jpg", ".jpeg", ".gif",
        ".webp", ".svg", ".ico", ".woff", ".woff2", ".ttf", ".eot",
        ".avif", ".webm", ".mp4", ".pdf"
    };
    for (size_t i = 0; i < sizeof(exts) / sizeof(exts[0]); i++) {
        if (!strcasecmp(dot, exts[i])) return true;
    }
    return false;
}

static bool web_is_internal_path(const char *path)
{
    if (!path || path[0] != '/') return true;
    if (strstr(path, "..") || strchr(path, '\\') || strstr(path, "/.")) return true;
    if (!strcmp(path, "/config.inc.php") || !strcmp(path, "/init.php")) return true;
    if (!strncmp(path, "/var/", 5) || !strncmp(path, "/install/", 9)) return true;
    if (!strncmp(path, "/usr/", 5) && !web_is_public_asset(path)) return true;
    return false;
}

web_try_static() 入口加白名单守卫,web_handle() 里对内部路径直接 403:

static bool web_try_static(httpd_req_t *req)
{
    const char *path = s_req.path;
    if (!web_is_public_asset(path)) return false;
    // ...
    web_split_uri();
    if (web_is_internal_path(s_req.path)) {
        httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "Forbidden");
        return ESP_OK;
    }
    web_select_script();

改动情况: web_is_public_asset 只放行 /admin/{css,js,img,...}/、/usr/themes/、/usr/plugins/、/usr/uploads/ 下指定扩展名的静态资源,/usr/uploads/ 仅放行图片/PDF;web_is_internal_path 把 config.inc.php、init.php、/var/、/install/ 以及 /usr/ 下非公开资源一律判为内部路径,请求直接返回 403。
原因: 原版会把 /app 下任意存在的文件送出去,意味着 SQLite 数据库、config.inc.php、备份文件都能被远程下载。白名单把可服务面收窄到主题/插件/上传里的静态资源,杜绝敏感文件泄露。

3.设置 server_context 启用 POST/cookie 解析
原版:

    SG(request_info).request_method = s_req.method_str;

适配版:

    /* A real server context enables SAPI POST parsing and read_cookies. Embed
     * leaves this NULL, which silently makes every form look empty. */
    SG(server_context) = s_web_req;
    SG(sapi_headers).http_response_code = 200;
    SG(request_info).request_method = s_req.method_str;

改动情况: 设置 SG(server_context) = s_web_req 并把响应码初始化为 200。
原因: embed SAPI 默认 server_context 为 NULL,导致 SAPI 的 POST 解析和 cookie 读取被跳过,Typecho 后台所有表单提交都是空数据。设上 httpd 请求句柄后 SAPI 才会真正读 body。

4.缺省 Content-Type 采用 SAPI 声明的 MIME
原版:

    if (!have_ctype) {
        httpd_resp_set_type(s_web_req, "text/html; charset=UTF-8");
    }

适配版:

    if (!have_ctype) {
        snprintf(s_ctype_hdr, sizeof(s_ctype_hdr), "%s",
            (h->mimetype && h->mimetype[0]) ? h->mimetype : "text/html; charset=UTF-8");
        httpd_resp_set_type(s_web_req, s_ctype_hdr);
    }

改动情况: PHP 没显式发 Content-Type 时,优先用 SAPI 记录的 h->mimetype,仍无则回退 text/html; charset=UTF-8。
原因: 和 PHP 侧 Response.php 的强制覆盖配合,确保 JSON 等响应的 MIME 正确传给浏览器/jQuery,而不是一律被当成 text/html。

5.POST body 限流
原版: 无 body 大小检查。
适配版:

    /* Limit request-body buffering in the 8 MiB PSRAM system. */
    if (req->content_len > 786432) {
        httpd_resp_set_status(req, "413 Payload Too Large");
        httpd_resp_set_type(req, "text/plain; charset=UTF-8");
        httpd_resp_send(req, "POST body exceeds 768 KiB", HTTPD_RESP_USE_STRLEN);
        return ESP_OK;
    }

改动情况: POST body 超过 768 KiB 直接返回 413。
原因: N16R8 只有 8 MiB PSRAM,放任大 body 上传会直接 OOM。768 KiB 和 ini 里的 post_max_size 对齐。

6.异常日志钩子 + 改由 php_request_shutdown 发头
原版:

static void web_serve_one(void)
{
    s_web_len = 0;
    web_prepare_request();
    if (php_request_startup() == SUCCESS) {
        zend_try {
            run_php_file(s_web_script);
        } zend_catch {
        } zend_end_try();
        if (!SG(headers_sent)) {
            sapi_send_headers();
        }
        php_request_shutdown(NULL);
    }
}

适配版:

/* Keep framework-caught PHP errors visible on the serial console. */
static void web_log_exception(zend_object *exception)
{
    zval rv;
    zval *message = zend_read_property(exception->ce, exception,
                                      "message", sizeof("message") - 1, true, &rv);
    if (message && Z_TYPE_P(message) == IS_STRING) {
        ESP_LOGE(TAG, "%s: %s", ZSTR_VAL(exception->ce->name), Z_STRVAL_P(message));
    }
}

static void web_serve_one(void)
{
    s_web_len = 0;
    zend_throw_exception_hook = web_log_exception;
    web_prepare_request();
    if (php_request_startup() == SUCCESS) {
        zend_try {
            run_php_file(s_route_script);
        } zend_catch {
        } zend_end_try();
        /* Shutdown first flushes user output buffers. Typecho sets its final status,
         * MIME type and cookies in an ob_start callback; sending headers before
         * that callback freezes the defaults and breaks redirects / AJAX / login.
         * php_output_deactivate also sends headers for an empty response. */
        php_request_shutdown(NULL);
    }
}

改动情况: 新增 web_log_exception 装到 zend_throw_exception_hook,把框架捕获的异常打到串口;执行的脚本从 s_web_script 改为 s_route_script;删掉请求尾手动调的 sapi_send_headers(),改由 php_request_shutdown 触发。
原因: Typecho 在 ob_start 回调里才设置最终的状态码、MIME 和 Cookie,提前调 sapi_send_headers 会把默认值冻结,破坏重定向、AJAX 和登录——这正是之前安装器 POST 一直返回 302 却不跳转的根由。交给 php_request_shutdown 在输出缓冲冲刷后发送才正确。异常钩子则是为了调试:框架 try/catch 吞掉的错误 otherwise 在串口上一点痕迹都不留。

7.启动 SNTP 时间同步

    /* Typecho timestamps and expiring login cookies need wall-clock time after
     * every power cycle. The init script has connected Wi-Fi at this point. */
    esp_sntp_setoperatingmode(SNTP_OPMODE_POLL);
    esp_sntp_setservername(0, "ntp.aliyun.com");
    esp_sntp_init();

改动情况: Web 服务器启动时向 ntp.aliyun.com 轮询同步系统时间。
原因: ESP32 每次上电 RTC 不保留墙钟时间,Typecho 的文章时间戳和登录 Cookie 过期判断都依赖它;Wi-Fi 连上后同步一次 NTP 即可。


这些改动到位之后,Typecho 终于能完整跑完安装、登录后台、发文章了。不过还有个收尾问题:reserve_kb 到底该设多少?下面单独算一下。

调整 reserve_kb

reserve_kb = 8320 是怎么算出来的:板子是 ESP32-S3-N16R8,N16R8 即 16 MiB Flash、8 MiB PSRAM。Flash 一共 16384 KiB,要塞下四部分:前置区(bootloader + 分区表)、固定分区(nvs + phy_init)、factory(固件本体)、storage(Typecho 站点目录)。
构建时 cmake/gen-partitions.cmake 会按下面这条链推 storage 分区大小:

storage_k = ceil_to_64K( 源码占用 + 64K FAT 开销 + reserve_kb ) / 1024
factory_k = ( 16384 - 64 - storage_k - 0 ) 向下对齐到 64K

其中 64 是普通启动的前置偏移 0x10000,phpstore 没启用所以是 0。源码占用按 4 KB 簇向上取整累加 project-src/ 下所有文件,本次构建实测是 2441216 B(约 2384 KiB)。这些数字在构建日志里都打出来了:

-- php-esp32: embedded 'storage' partition = 10816K (source ~2441216 B occupancy + 64K FAT + 8320K reserve)

最终生成的分区表 php-typecho/build/partitions.gen.csv:

# Generated by cmake/gen-partitions.cmake -- do not edit; regenerated each build.
# Name,     Type, SubType,  Offset,   Size
nvs,        data, nvs,      ,         24K
phy_init,   data, phy,      ,         4K
factory,    app,  factory,  ,         5440K
storage,    data, fat,      ,         10816K,  

所以 reserve_kb = 8320 的推算思路是:16 MiB Flash 里给固件 factory 留 5440 KiB(PHP 引擎 + ESP-IDF + web server 实测够用且有余量),前置和固定分区共约 92 KiB,剩下全给 storage,对齐到 64 KB 后是 10816 KiB;再从 storage 里扣掉源码占用 2384 KiB 和 64 KiB FAT 开销,剩下的就是额外预留:

10816 − 2384 − 64 − 48(对齐填充) = 8320 KiB

这 8320 KiB(约 8.1 MiB)就是 storage 分区里真正给 Typecho 写数据库、上传附件和会话文件用的可写空间。把它写进 php-typecho/php-baremetal.config.toml:

[esp32.storage]
microsd = false
reserve_kb = 8320   # extra KB in the flash `storage` partition on top of the source (0 = just fit it)

如果以后源码体积变了,重跑一次构建看日志里的 occupancy,再用 目标 storage 大小 − 源码占用 − 64K FAT 反推一下就能重算 reserve_kb。

压缩 Typecho 代码仓库

修改后的 Typecho 文件夹依然有 1.9MB,而在寸土寸金的 16MB Flash 上面,0.1MB 也至关重要。Typecho 本身代码就有不少的注释,更进一步,我们还可以执行压缩。利用 PHP 自带的 php_strip_whitespace() 就可以完成。
首先进行 PHP 压缩:

#!/bin/bash

SRC="./project-src"
DST="./project-src-min"

mkdir -p "$DST"

find "$SRC" -type f -print0 | while IFS= read -r -d '' file; do
    relative="${file#"$SRC"/}"
    target="$DST/$relative"

    mkdir -p "$(dirname "$target")"

    case "$file" in
        *.php)
            php -w "$file" > "$target"
            ;;
        *)
            cp -p "$file" "$target"
            ;;
    esac
done

这时文件夹大小已经从 1.9MB 变成了 1.5MB,核算后设置 reserve_kb = 8744。

编译,烧录!Typecho 启动!

编译固件:

../tools/phpflash build --clean

然后烧录固件:

../tools/phpflash flash -p /dev/ttyACM0

打开了浏览器进行 Typecho 的安装:
03.png
04.png
05.png
06.png
07.png
成功了!!!太疯狂了!!!我居然在 ESP32 上面运行起来了 Typecho !!!


我想,对于 ESP32-S3-N16R8 来说,每次访问请求都是一次酣畅淋漓的性能释放吧,毕竟每次点击刷新都要等五六秒,有点像华为畅享的 麒麟710 强行带鸿蒙系统的卡顿感()
你永远不知道你的网上邻居在用什么奇葩玩意运行网站

评论区 (0)