如何把你的C++代码集成到MicroPython

首先按照我之前发的《如何通过PlatformIO编译ESP32的MicroPython固件》尝试用PlatformIO烧一遍固件。成功了再往下看。

接下来进入正题,如何把你在PlatformIO中写的C++代码集成进MicroPython。

这里以ArduSensorPlatform为例。ArduSensorPlatform的代码这里就不提供了,会看这篇文章的大概率能看到相关代码。我们的目标是直接在Python中对Host的通道进行Read和Write。

我写了一个很简陋的TestPort作为示例Sensor,有两个成员变量sample1和sample2,Read读取这两个变量,write修改这两个变量,代码如下:

#include "TestPort.h"

TestPort::TestPort(float _sample1, float _sample2) {
    sample1 = _sample1;
    sample2 = _sample2;
}

TestPort::~TestPort() {
    
}

int TestPort::GetChannelNum() {
    return 2;
}

int TestPort::Update() {
    return 0;
}

float TestPort::Read(int channelNo) {
    if (channelNo == 0) {
        return sample1;
    } else if (channelNo == 1) {
        return sample2;
    }
    return -1.0;
}

int TestPort::Write(int channelNo, float value, bool isAsync)
{
    if (channelNo == 0) {
        sample1 = value;
    } else if (channelNo == 1) {
        sample2 = value;
    } else {
        return 1;
    }
    return 0;
}

然后在PlatformIO中新建HostWrapper.hHostWrapper.cpp,cpp文件的代码等会儿再放出来:

// HostWrapper.h
#ifndef _HOSTWRAPPER_H
#define _HOSTWRAPPER_H

#ifdef __cplusplus
extern "C" {
#endif

struct tagHost {
    void *host;
};

extern struct tagHost mp_ardu_host;

float ArduHostRead(int channelNo);
float ArduHostRead2(int sensorNo, int channelNo);
void ArduHostWrite(int channelNo, float value);
void ArduHostWrite2(int sensorNo, int channelNo, float value);
void ArduHostUpdate();

#ifdef __cplusplus
}
#endif

#endif

同时在MicroPython repo中,在port/esp32下新建一个host.c文件,然后在port上一级文件夹的extmod目录下新建host.cHostWrapper.h文件,把上面这个头文件的代码复制进HostWrapper.h。两个host.c的代码相同,代码如下:

#include "py/obj.h"
#include "py/runtime.h"
#include "py/builtin.h"
#include "extmod/HostWrapper.h"

STATIC mp_obj_t host_read(mp_obj_t channelNo_obj) {
    int channelNo = mp_obj_get_int(channelNo_obj);
    float value = ArduHostRead(channelNo);
    return mp_obj_new_float(value);
}

STATIC MP_DEFINE_CONST_FUN_OBJ_1(host_read_obj, host_read);

STATIC mp_obj_t host_read_2(mp_obj_t sensorNo_obj, mp_obj_t channelNo_obj) {
    int sensorNo = mp_obj_get_int(sensorNo_obj);
    int channelNo = mp_obj_get_int(channelNo_obj);
    float value = ArduHostRead2(sensorNo, channelNo);
    return mp_obj_new_float(value);
}

STATIC MP_DEFINE_CONST_FUN_OBJ_2(host_read_2_obj, host_read_2);

STATIC mp_obj_t host_write(mp_obj_t channelNo_obj, mp_obj_t value_obj) {
    int channelNo = mp_obj_get_int(channelNo_obj);
    float value = mp_obj_get_float(value_obj);
    ArduHostWrite(channelNo, value);
    return mp_const_none;
}

STATIC MP_DEFINE_CONST_FUN_OBJ_2(host_write_obj, host_write);

STATIC mp_obj_t host_write_2(mp_obj_t sensorNo_obj, mp_obj_t channelNo_obj, mp_obj_t value_obj) {
    int sensorNo = mp_obj_get_int(sensorNo_obj);
    int channelNo = mp_obj_get_int(channelNo_obj);
    float value = mp_obj_get_float(value_obj);
    ArduHostWrite2(sensorNo, channelNo, value);
    return mp_const_none;
}

STATIC MP_DEFINE_CONST_FUN_OBJ_3(host_write_2_obj, host_write_2);

STATIC mp_obj_t host_update() {
    ArduHostUpdate();
    return mp_const_none;
}

STATIC MP_DEFINE_CONST_FUN_OBJ_0(host_update_obj, host_update);

STATIC const mp_rom_map_elem_t host_module_globals_table[] = {
    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_host) },
    { MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&host_read_obj) },
    { MP_ROM_QSTR(MP_QSTR_read2), MP_ROM_PTR(&host_read_2_obj) },
    { MP_ROM_QSTR(MP_QSTR_write), MP_ROM_PTR(&host_write_obj) },
    { MP_ROM_QSTR(MP_QSTR_write2), MP_ROM_PTR(&host_write_2_obj) },
    { MP_ROM_QSTR(MP_QSTR_update), MP_ROM_PTR(&host_update_obj) },
};

STATIC MP_DEFINE_CONST_DICT(host_module_globals, host_module_globals_table);

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

// MP_REGISTER_MODULE(MP_QSTR_host, host_user_cmodule, MODULE_HOST_ENABLED);

这里就不解释代码了,自行查阅py/obj.cpy/obj.h

然后在port/esp32/mpconfigport.h中添加两行代码,这里要善用自己常用的文本编辑器的搜索功能:

extern const struct _mp_obj_module_t esp_module;
extern const struct _mp_obj_module_t esp32_module;
extern const struct _mp_obj_module_t utime_module;
extern const struct _mp_obj_module_t uos_module;
extern const struct _mp_obj_module_t mp_module_usocket;
extern const struct _mp_obj_module_t mp_module_machine;
extern const struct _mp_obj_module_t mp_module_network;
extern const struct _mp_obj_module_t mp_module_onewire;
// added line
extern const struct _mp_obj_module_t mp_module_host;

上面added line下是第一行

以及

// added line
extern const struct _mp_obj_module_t mp_module_host;

#define MICROPY_PORT_BUILTIN_MODULES \
    { MP_OBJ_NEW_QSTR(MP_QSTR_esp), (mp_obj_t)&esp_module }, \
    { MP_OBJ_NEW_QSTR(MP_QSTR_esp32), (mp_obj_t)&esp32_module }, \
    { MP_OBJ_NEW_QSTR(MP_QSTR_utime), (mp_obj_t)&utime_module }, \
    { MP_OBJ_NEW_QSTR(MP_QSTR_uos), (mp_obj_t)&uos_module }, \
    { MP_OBJ_NEW_QSTR(MP_QSTR_usocket), (mp_obj_t)&mp_module_usocket }, \
    { MP_OBJ_NEW_QSTR(MP_QSTR_machine), (mp_obj_t)&mp_module_machine }, \
    { MP_OBJ_NEW_QSTR(MP_QSTR_network), (mp_obj_t)&mp_module_network }, \
    { MP_OBJ_NEW_QSTR(MP_QSTR__onewire), (mp_obj_t)&mp_module_onewire }, \
    { MP_OBJ_NEW_QSTR(MP_QSTR_host), (mp_obj_t)&mp_module_host }, \

上面这部分的最后一行。

这些改动也要在pio的mpconfigport.h中对应地进行改动。

然后再Makefile中添加我们的host.c

SRC_C = \
	main.c \
	uart.c \
        …………
	host.c\
	$(SRC_MOD)

然后就是照着我开头提到的那篇帖子里的那样,执行make以及make staticlib这两个命令,app_main()的名字别忘了改。生成.a文件后把库文件复制到相应目录下。

接着回到pio project,main.c改成main.cpp,代码再做一些修改

#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
// #include "lib/utils/pyexec.h"

#include "lwip/err.h"
#include "lwip/sys.h"

#include <stdio.h>
#include <stdlib.h>
#include "driver/gpio.h"

#include "Host.h"
#include "HostWrapper.h"
#include "SensorBase.h"
#include "TestPort.h"

// #include "mpHelper.h"

#define GPIO_OUTPUT_IO_0 2

#define GPIO_OUTPUT_PIN_SEL (1ULL << GPIO_OUTPUT_IO_0)

extern "C" {
    void app_main();
    #include "mpHelper.h"
    #include "lib/utils/pyexec.h"  
}

struct tagHost mp_ardu_host;

void app_main()
{
    // Initialize obj
    mp_ardu_host.host = new Host(2);
    SensorBase *sensor = new TestPort(2.0, 1.5);
    static_cast<Host *>(mp_ardu_host.host)->AddSensor(sensor);
    sensor = new TestPort(3.5, 4.0);
    static_cast<Host *>(mp_ardu_host.host)->AddSensor(sensor);
    static_cast<Host *>(mp_ardu_host.host)->Start();
    static_cast<Host *>(mp_ardu_host.host)->Update();    

    //Initialize NVS
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
    {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    mp_start(); 

    pyexec_friendly_repl();

    gc_sweep_all();
    mp_deinit();
    fflush(stdout);
}

然后是HostWrapper.c的代码

#include "HostWrapper.h"
#include "Host.h"

#ifdef __cplusplus
extern "C" {
#endif

float ArduHostRead(int channelNo)
{
    return static_cast<Host *>(mp_ardu_host.host)->Read(channelNo);
}

float ArduHostRead2(int sensorNo, int channelNo)
{
    return static_cast<Host *>(mp_ardu_host.host)->Read(sensorNo, channelNo);
}

void ArduHostWrite(int channelNo, float value)
{
    static_cast<Host *>(mp_ardu_host.host)->Write(channelNo, value);
}

void ArduHostWrite2(int sensorNo, int channelNo, float value)
{
    if (!static_cast<Host *>(mp_ardu_host.host)->IsChannelWrittable[sensorNo][channelNo])
    {
        static_cast<Host *>(mp_ardu_host.host)->IsChannelWrittable[sensorNo][channelNo] = 1;
    }
    static_cast<Host *>(mp_ardu_host.host)->Write(sensorNo, channelNo, value);
}

void ArduHostUpdate()
{
    static_cast<Host *>(mp_ardu_host.host)->Update();
}

#ifdef __cplusplus
}
#endif

这里我做的工作是在HostWrapper.h中定义了一个结构体类型,成员为一个指针,然后在main.cpp中声明了一个结构体变量并把C++对象指针赋给这个成员指针,在HostWrapper.cpp中extern这个变量从而调用它,这样能够在符合C的规范的条件下调用这个对象的成员函数。代码比较丑陋,希望你能给出更优美的代码。

ArduHostWrite中我没有访问IsChannelWrittable是因为有点麻烦,等下就不用那个函数做演示了。

main.cpp中我添加了两个Sensor,第一个Sensor的sample1和sample2分别为2.0, 1.5,第二个Sensor的sample1和2分别为3.5和4.0。

然后就是Build和Upload

以下是MicroPython示例

>>> import host
>>> host.read(0)
2.0
>>> host.read2(1, 1)
4.0
>>> host.write2(0, 1, 6.3)
>>> host.write2(1, 0, 7.2)
>>> host.update()
>>> host.update()
>>> host.read(1)
6.3
>>> host.read(2)
7.2

大概就是这样。花几分钟写的,可能有纰漏,今天先摸了明天再改。


本文章使用limfx的vscode插件快速发布