记录一次简单的MicroPython源码定制过程

前言

起因是我有一个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 (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;
            }
        }
    }

pyexec_friendly_repl() 就是我们在控制台与MicroPython设备交互时,实际负责处理指令的函数,我们进入其中

int pyexec_friendly_repl(void) {
    vstr_t line;
    vstr_init(&line, 32);

friendly_repl_reset:
    mp_hal_stdout_tx_str(MICROPY_BANNER_NAME_AND_VERSION);
    mp_hal_stdout_tx_str("; " MICROPY_BANNER_MACHINE);
    mp_hal_stdout_tx_str("\r\n");
    #if MICROPY_PY_BUILTINS_HELP
    mp_hal_stdout_tx_str("Type \"help()\" for more information.\r\n");
    // ...

显然,我们找到了自己想要修改的东西,那不妨把 MICROPY_BANNER_NAME_AND_VERSION 改成自己的名字试试。

int pyexec_friendly_repl(void) {
    vstr_t line;
    vstr_init(&line, 32);

friendly_repl_reset:
    mp_hal_stdout_tx_str("Micropython Acite Customized Version v0.1");
    mp_hal_stdout_tx_str("; " MICROPY_BANNER_MACHINE);
    mp_hal_stdout_tx_str("\r\n");
    #if MICROPY_PY_BUILTINS_HELP
    mp_hal_stdout_tx_str("Type \"help()\" for more information.\r\n");
    // ...

做完这一切后,我们在终端输入命令:

idf.py -D MICROPY_BOARD=ESP32_GENERIC build
idf.py -D MICROPY_BOARD=ESP32_GENERIC flash

这会先编译我们修改后的固件,然后烧录进ESP32设备之中(如果你正确地连接了ESP32设备的话)。
随后,我们打开一个Python的REPL窗口:

在这里插入图片描述
很显然,我们成功了。

源文件的添加

随后,我们来做我们本来要做的事——让MicroPython可以连接我的Xbox手柄,并且能从Python代码中获取数据。
首先,我们把 bluetooth/esp_hid_host 示例中的 esp_hid_gap.cesp_hid_gap.h 文件复制到 ports/esp32 目录下。然后在 esp32_common.cmake 中添加对应的源文件:

list(APPEND MICROPY_SOURCE_PORT
...
    esp_hid_gap.c
...
)

此后如果在项目中引入的新的源文件,也请注意要在 esp32_common.cmake 中添加,我就不再赘述了。

编写可被Python代码调用的模块

如果想让我们编写的C代码可以被Python代码调用,我们需要把它们封装为一个模块,并遵守相应的规范。
首先,我们在 ports/esp32 目录下添加 modacite.c 文件,我们将会在这里编写我们的模块。

// modacite.c

#include <stdio.h>
#include "esp_flash.h"
#include "esp_log.h"
#include "py/runtime.h"
#include "py/mperrno.h"
#include "py/mphal.h"

static const mp_rom_map_elem_t acite_module_globals_table[] = {
        {MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_acite)},
};

static MP_DEFINE_CONST_DICT(acite_module_globals, acite_module_globals_table);

const mp_obj_module_t acite_module = {
        .base = { &mp_type_module },
        .globals = (mp_obj_dict_t *)&acite_module_globals,
};

MP_REGISTER_MODULE(MP_QSTR_acite, acite_module);

以上的代码定义了一个名为"acite"的模块(我是按照其他模块文件照葫芦画瓢画出来的,可能不符合代码规范)
随后,我们需要在这个模块中定义一个可以被调用的函数

// modacite.c

#include <stdio.h>
#include "esp_flash.h"
#include "esp_log.h"
#include "py/runtime.h"
#include "py/mperrno.h"
#include "py/mphal.h"

static mp_obj_t acite_test()	// 定义一个Test函数
{
    mp_print_str(MP_PYTHON_PRINTER, "Module Acite is Testing");
    return mp_const_none;
}

static MP_DEFINE_CONST_FUN_OBJ_0(acite_test_obj, acite_test);	// 定义相关的对象

static const mp_rom_map_elem_t acite_module_globals_table[] = {
        {MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_acite)},
        {MP_ROM_QSTR(MP_QSTR_Test), MP_ROM_PTR(&acite_test_obj)}	// 添加对象引用
};

static MP_DEFINE_CONST_DICT(acite_module_globals, acite_module_globals_table);

const mp_obj_module_t acite_module = {
        .base = { &mp_type_module },
        .globals = (mp_obj_dict_t *)&acite_module_globals,
};

MP_REGISTER_MODULE(MP_QSTR_acite, acite_module);

至此,我们就定义了一个名为 “acite” 的模块,这个模块中有一个函数 Test() 可被调用,让我们编译看看效果

在这里插入图片描述
Perfect!
值得一提的是,在定制MicroPython代码的时候不要用ESP_LOG之类的函数,MicroPython重定向了log的输出,所以我们什么也看不到。最好用下面的函数

int mp_print_str(const mp_print_t *print, const char *str);

添加新的任务

现在有一个新的问题,MicroPython作为一个任务运行在ESP32(FreeRTOS)中,那我们的蓝牙相关的代码在哪运行呢?直接和MicroPython的代码混在一起不是一个明智的选择,但是既然MicroPython是一个任务,我们是否可以再创建一个新的任务做我们想做的事?当然可以。

蓝牙相关代码

ports/esp32 下新建源文件 bt_hid_proc.c 用于处理我们的业务逻辑。


// #include ...

uint8_t bt_buffer[32];

void hidh_callback(void *handler_args, esp_event_base_t base, int32_t id, void *event_data)
{
    esp_hidh_event_t event = (esp_hidh_event_t)id;
    esp_hidh_event_data_t *param = (esp_hidh_event_data_t *)event_data;

    switch (event) {
        case ESP_HIDH_OPEN_EVENT: {
            if (param->open.status == ESP_OK) {
                const uint8_t *bda = esp_hidh_dev_bda_get(param->open.dev);
                mp_printf(MP_PYTHON_PRINTER, ESP_BD_ADDR_STR " OPEN: %s", ESP_BD_ADDR_HEX(bda), esp_hidh_dev_name_get(param->open.dev));
                esp_hidh_dev_dump(param->open.dev, stdout);
            } else {
                mp_printf(MP_PYTHON_PRINTER, " OPEN failed!");
            }
            break;
        }
        case ESP_HIDH_BATTERY_EVENT: {
            const uint8_t *bda = esp_hidh_dev_bda_get(param->battery.dev);
            mp_printf(MP_PYTHON_PRINTER, ESP_BD_ADDR_STR " BATTERY: %d%%", ESP_BD_ADDR_HEX(bda), param->battery.level);
            break;
        }
        case ESP_HIDH_INPUT_EVENT: {
            esp_hidh_dev_bda_get(param->input.dev);
            memcpy(bt_buffer, param->input.data, param->input.length);
            break;
        }
        case ESP_HIDH_FEATURE_EVENT: {
            const uint8_t *bda = esp_hidh_dev_bda_get(param->feature.dev);
            mp_printf(MP_PYTHON_PRINTER, ESP_BD_ADDR_STR " FEATURE: %8s, MAP: %2u, ID: %3u, Len: %d", ESP_BD_ADDR_HEX(bda),
                     esp_hid_usage_str(param->feature.usage), param->feature.map_index, param->feature.report_id,
                     param->feature.length);
            ESP_LOG_BUFFER_HEX("TAG", param->feature.data, param->feature.length);
            break;
        }
        case ESP_HIDH_CLOSE_EVENT: {
            const uint8_t *bda = esp_hidh_dev_bda_get(param->close.dev);
            mp_printf(MP_PYTHON_PRINTER, ESP_BD_ADDR_STR " CLOSE: %s", ESP_BD_ADDR_HEX(bda), esp_hidh_dev_name_get(param->close.dev));
            break;
        }
        default:
            mp_printf(MP_PYTHON_PRINTER, "EVENT: %d", event);
            break;
    }
}

#define SCAN_DURATION_SECONDS 5

void hid_demo_task(void *pvParameters)
{
    size_t results_len = 0;
    esp_hid_scan_result_t *results = NULL;
    mp_printf(MP_PYTHON_PRINTER, "SCAN...");
    esp_hid_scan(SCAN_DURATION_SECONDS, &results_len, &results);
    mp_printf(MP_PYTHON_PRINTER, "SCAN: %u results", results_len);
    if (results_len) {
        esp_hid_scan_result_t *r = results;
        esp_hid_scan_result_t *cr = NULL;
        while (r) {
            printf("  %s: " ESP_BD_ADDR_STR ", ", (r->transport == ESP_HID_TRANSPORT_BLE) ? "BLE" : "BT ", ESP_BD_ADDR_HEX(r->bda));
            printf("RSSI: %d, ", r->rssi);
            printf("USAGE: %s, ", esp_hid_usage_str(r->usage));

            if (r->transport == ESP_HID_TRANSPORT_BT) {
                cr = r;
                printf("COD: %s[", esp_hid_cod_major_str(r->bt.cod.major));
                esp_hid_cod_minor_print(r->bt.cod.minor, stdout);
                printf("] srv 0x%03x, ", r->bt.cod.service);
                print_uuid(&r->bt.uuid);
                printf(", ");
            }

            printf("NAME: %s ", r->name ? r->name : "");
            printf("\n");
            r = r->next;
        }
        if (cr) {
            //open the first result
            mp_printf(MP_PYTHON_PRINTER, "Try to Open " ESP_BD_ADDR_STR, ESP_BD_ADDR_HEX(cr->bda));
            esp_hidh_dev_open(cr->bda, ESP_HID_TRANSPORT_BT, cr->ble.addr_type);
        }
        //free the results
        esp_hid_scan_results_free(results);
    }
    vTaskDelete(NULL);
}

void bt_proc_main(void* param)
{
    mp_printf(MP_PYTHON_PRINTER, "setting hid gap, mode:%d", HID_HOST_MODE);
    ESP_ERROR_CHECK( esp_hid_gap_init(HID_HOST_MODE) );
    ESP_ERROR_CHECK( esp_ble_gattc_register_callback(esp_hidh_gattc_event_handler));
    
    esp_hidh_config_t config = {
            .callback = hidh_callback,
            .event_stack_size = 4096,
            .callback_arg = NULL,
    };
    ESP_ERROR_CHECK(esp_hidh_init(&config));

    xTaskCreate(&hid_demo_task, "hid_task", 4 * 1024, NULL, 2, NULL);

    vTaskDelete(NULL);
}

平平无奇,就是在官方的示例上改的,但不同的是,我们要把启动蓝牙搜索的部分编写为一个FreeRTOS的任务。
这个代码文件做的事情简单来说就是寻找一个HID设备去配对,然后把接收到的数据保存在uint8_t bt_buffer[32];中。
随后,我们在acite模块中添加函数:

static mp_obj_t acite_bt_connect();
static mp_obj_t acite_bt_get_data();

前者负责启动蓝牙任务,后者负责把读取到的数据交付到Python代码层中:编写完成后的样子是这样:

//
// Created by acite on 7/18/24.
//

#include <stdio.h>

#include "esp_flash.h"
#include "esp_log.h"

#include "py/runtime.h"
#include "py/mperrno.h"
#include "py/mphal.h"

extern void bt_proc_main(void* param);
extern uint8_t bt_buffer[32];

static mp_obj_t acite_test()
{
    mp_print_str(MP_PYTHON_PRINTER, "Module Acite is Testing");
    return mp_const_none;
}

static MP_DEFINE_CONST_FUN_OBJ_0(acite_test_obj, acite_test);

static mp_obj_t acite_bt_connect()
{

    TaskHandle_t bt_t;
    xTaskCreatePinnedToCore(bt_proc_main,
                            "bt_task",
                            4096,
                            NULL,
                            2,
                            &bt_t,
                            MP_TASK_COREID);
    return mp_const_none;
}

static MP_DEFINE_CONST_FUN_OBJ_0(acite_bt_connect_obj, acite_bt_connect);

static mp_obj_t acite_bt_get_data()
{
    mp_obj_tuple_t *tuple = MP_OBJ_TO_PTR(mp_obj_new_tuple(32, NULL));
    for (int i = 0; i < 32; i++) {
        tuple->items[i] = mp_obj_new_int(bt_buffer[i]);
    }
    return MP_OBJ_FROM_PTR(tuple);
}

static MP_DEFINE_CONST_FUN_OBJ_0(acite_bt_get_data_obj, acite_bt_get_data);

static const mp_rom_map_elem_t acite_module_globals_table[] = {
        {MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_acite)},
        {MP_ROM_QSTR(MP_QSTR_Test), MP_ROM_PTR(&acite_test_obj)},
        {MP_ROM_QSTR(MP_QSTR_BtConnect), MP_ROM_PTR(&acite_bt_connect_obj)},
        {MP_ROM_QSTR(MP_QSTR_BtGetData), MP_ROM_PTR(&acite_bt_get_data_obj)}
};

static MP_DEFINE_CONST_DICT(acite_module_globals, acite_module_globals_table);

const mp_obj_module_t acite_module = {
        .base = { &mp_type_module },
        .globals = (mp_obj_dict_t *)&acite_module_globals,
};

MP_REGISTER_MODULE(MP_QSTR_acite, acite_module);

在 acite_bt_get_data 函数中,我们读取了bt_buffer的内容,然后把它作为一个Tuple返回。

成果

现在,我们可以编译看一看效果了:

在这里插入图片描述

除了一些细节的问题之外,可以说完美。

总结

虽然是一次很简单的源码定制过程,但是这种过程可以唤起我们的一种意识:没有什么代码是高高在上,只可远观不可亵玩的。不要一味的寻找能满足自己需求的轮子,如果一个轮子距离你的需求就差一点,而他又是开源的——那就勇敢的去研究、修改它。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值