目录
前言
起因是我有一个Xbox手柄,我想用ESP32去连接它,然后去控制一些东西。起初这个项目我是用ESP-IDF编写的,参考了官方的 bluetooth/esp_hid_host 示例,作了一些修改,最后我的ESP32设备成功地连接上了我的Xbox手柄,并能够读取各类数据(诸如摇杆、按键),但这不是今天的主题。
在后续的调试中,反复的修改、烧写、以及龟速的串口实在让我有些烦躁了,为了暂时摆脱他们,我决定用MicroPython先绘制出项目的大致架构,因为我可以直接用串口去执行Python代码,而无需烧写进Flash。
但不巧的是,官方的MicroPython固件只提供了BLE的驱动,而我的Xbox手柄是一个Classic BT设备。这似乎是个死局?幸好MicroPython是一个开源项目。
环境
- MCU:ESP-WROOM-32E 4MB Flash
- PC:Ubuntu 24.04 Linux 6.8.8
- IDE:CLion Nova 2024.2 EAP
- ESP-IDF:v5.1.4
- MicroPython:v1.24
前期准备
克隆MicroPython代码库
第一步当然是从托管平台克隆MicroPython的代码仓库:
git clone https://github.com/micropython/micropython
cd micropython/
编译子模块
进入文件夹后,我们需要初始化MicroPython的一些子模块,然后编译它的交叉编译工具
git submodule update --init --recursive
make -C mpy-cross
需要指出的是,我们对MicroPython代码的主要魔改对象在 micropython/ports 中,这些是与特定MCU相关的代码,剩下的一些解释器,内置库之类的,一般没必要去改。
然后,我们进入 ports/esp32 文件夹,编译一些子模块
cd ports/esp32
make submodules
此时, ports/esp32 目录就相当于一个ESP-IDF项目,因为ESP-IDF就是基于CMake管理项目的,我们自然就可以用CLion去打开它。
ESP-IDF配置
安装ESP-IDF编译环境的过程就不再赘述了,如果你正确安装了ESP-IDF,在终端上执行 “get_idf” 后,你应该看到类似下面的输出
Done! You can now compile ESP-IDF projects.
Go to the project directory and run:
idf.py build
但是不要急着按他说地做,我们必须先进行一些配置。
执行 idf.py -D MICROPY_BOARD=ESP32_GENERIC menuconfig
idf.py -D MICROPY_BOARD=ESP32_GENERIC menuconfig
-D选项指定了MICROPY_BOARD变量为ESP32_GENERIC,因为我使用的MCU是ESP-WROOM-32E,如果你使用了其他的MCU,就看看ports/*/boards的中是否有你使用的MCU对应的型号,如果有就改成那个,如果没有…那恐怕你得自己编写驱动了。这也不失为一种挑战
进入menuconfig菜单后,由于我要使用BT Classis功能,所以我要做的事就是把官方的sdkconfig中被关掉的BT Classis功能功能打开:
(高亮的地方原本是NimBLE - BLE only)
修改完我们需要的配置之后,可以试着执行 idf.py build 如果没有出错,就可以进行下一步
代码的定制
理论上来说,现在我们已经可以修改代码,然后再编译出自己的固件了。不如我们先试着把MicroPython启动时的报幕文字改成我们自己的名字?不错的主意。
修改报幕文字
用自己喜欢的IDE打开MicroPython项目后,我们打开 /ports/esp32/main.c
如果你和我一样,使用了CLion,那最好注意一下不要用右上角那个锤子去编译烧写,而是在终端中输入命令,并且CMake的编译输出路径要设置为build(与idf.py使用的目录同名)。
无论如何,现在让我们关注下面的代码
void app_main(void) {
// Hook for a board to run code at start up.
// This defaults to initialising NVS.
MICROPY_BOARD_STARTUP();
// Create and transfer control to the MicroPython task.
xTaskCreatePinnedToCore(mp_task, "mp_task", MICROPY_TASK_STACK_SIZE / sizeof(StackType_t), NULL, MP_TASK_PRIORITY, &mp_main_task_handle, MP_TASK_COREID);
}
MicroPython在ESP-IDF中是作为一个Task被执行的,并且运行在核心1。接下来我们进入mp_task
_Noreturn void mp_task(void *pvParameter) {
// ...
soft_reset:
// initialise the stack pointer for the main thread
mp_stack_set_top((void *)sp);
mp_stack_set_limit(MICROPY_TASK_STACK_SIZE - MP_TASK_STACK_LIMIT_MARGIN);
gc_init(mp_task_heap, mp_task_heap + MICROPY_GC_INITIAL_HEAP_SIZE);
mp_init();
mp_obj_list_append(mp_sys_path, MP_OBJ_NEW_QSTR(MP_QSTR__slash_lib));
readline_init0();
MP_STATE_PORT(native_code_pointers) = MP_OBJ_NULL;
// initialise peripherals
machine_pins_init();
#if MICROPY_PY_MACHINE_I2S
machine_i2s_init0();
#endif
// run boot-up scripts
pyexec_frozen_module("_boot.py", false);
int ret = pyexec_file_if_exists("boot.py");
if (ret & PYEXEC_FORCED_EXIT) {
goto soft_reset_exit;
}
if (pyexec_mode_kind == PYEXEC_MODE_FRIENDLY_REPL && ret != 0) {
int ret = pyexec_file_if_exists("main.py");
if (ret & PYEXEC_FORCED_EXIT) {
goto soft_reset_exit;
}
}
for (;;) {
if (pyexec_mode_kind == PYEXEC_MODE_RAW_REPL) {
vprintf_like_t vprintf_log = esp_log_set_vprintf(vprintf_null);
if (pyexec_raw_repl() != 0) {
break;
}
esp_log_set_vprintf(vprintf_log);
} else {
if (pyexec_friendly_repl() != 0) {
break;
}
}
}
// ...
}
这里只截取了mp_task的一部分,因为太长了。顺带一提,那个_Noreturn 前缀是我加上去的,否则CLion会把这一整段代码都画上黄色的下划线。
在上面那一大段代码中,我们着重关心这一段
for (;;) {
if