W800上手 Part.3 以跑马灯浅谈代码可移植性

W800上手 Part.3 实战开发跑马灯

1 新建工程并将语言更改为Cpp

先创建工程

mkdir W800 && cd W800
yoc init && yoc install -b v7.4.3 helloworld_w800
cd solutions/helloworld_w800
bear make -j

然后打开package.yaml,找到

source_file:
    - app/src/*.c

改为

source_file:
    - app/src/*.cpp

并在app/src中,将init.c和main.c分别改名为init.cpp和main.cpp

2 定义PWM接口类并实现PWM驱动类

在app/include中新建DriverInterface文件夹,并在文件夹中新建Pwm.hpp,我们在这里定义PWM接口类

class PwmInterface {
public:
    virtual void SetFrequency(unsigned int Frequency);
    virtual void PwmWrite(unsigned char Duty);
};

然后在app/include中新建W800Driver文件夹,并在文件夹中新建Pwm.hpp,我们在这里实现PWM驱动类(记得include接口类的头文件)

class PwmDriver : public PwmInterface {
private:
    unsigned char Duty = 0;
    unsigned int PwmChannel = 0;
    unsigned int Frequency = 1000;
    pwm_handle_t PwmHandle = NULL;
    unsigned int CalcPulseWidth() {
        unsigned int period = (unsigned int)(1e+6 / Frequency);
        unsigned int PulseWidth = (unsigned int)(Duty * period / 100);
        return PulseWidth;
    }
public:
    PwmDriver(pin_name_e pwmPinName, pin_func_e pwmPinFunc
            , unsigned int pwmChannel) {
        drv_pinmux_config(pwmPinName, pwmPinFunc);
        PwmHandle = csi_pwm_initialize(pwmChannel);
        this->PwmChannel = pwmChannel;
        csi_pwm_start(PwmHandle, pwmChannel);
        csi_pwm_config(PwmHandle, pwmChannel ,1e+6 / Frequency, 0);
    }
    PwmDriver(pin_name_e pwmPinName, pin_func_e pwmPinFunc
            , unsigned char pwmChannel, unsigned int Frequency) {
        drv_pinmux_config(pwmPinName, pwmPinFunc);
        PwmHandle = csi_pwm_initialize(pwmChannel);
        this->PwmChannel = pwmChannel;
        this->Frequency = Frequency;
        csi_pwm_start(PwmHandle, pwmChannel);
        csi_pwm_config(PwmHandle, pwmChannel ,1e+6 / Frequency, 0);
    }
    void SetFrequency(unsigned int Frequency) {
        this->Frequency = Frequency;
        unsigned int PulseWidth = CalcPulseWidth();
        csi_pwm_config(PwmHandle, PwmChannel, 1e+6 / Frequency, PulseWidth);
    }
    void PwmWrite(unsigned char Duty) {
        this->Duty = Duty;
        unsigned int PulseWidth = CalcPulseWidth();
        csi_pwm_config(PwmHandle, PwmChannel, 1e+6 / Frequency, PulseWidth);
    }
};

3 实现主逻辑

#define RedActiveEvent 0x01
#define GreenActiveEvent 0x02
#define BlueActiveEvent 0x04

aos_event_t g_event_active;

void TaskRedLedPwm(void* args) {
    bool increase = true;
    unsigned char Duty = 0;
    unsigned int originEventFlag;
    PwmInterface* pwmDriverInstancePtr = new PwmDriver(PB1, PB1_PWM, 0, 10000);
    pwmDriverInstancePtr->PwmWrite(100);
    while(true) {
        aos_event_get(&g_event_active, RedActiveEvent
                , AOS_EVENT_OR_CLEAR, &originEventFlag
            , RHINO_WAIT_FOREVER);
        if (increase && Duty < 100) {
            Duty++;
            aos_event_set(&g_event_active, OR, RedActiveEvent);
        }
        if (!increase && Duty > 0)  {
            Duty--;
            aos_event_set(&g_event_active, OR, RedActiveEvent);
        }
        if (increase && Duty == 100) {
            increase = false;
            Duty--;
            aos_event_set(&g_event_active, OR, RedActiveEvent);
        }
        if (!increase && Duty == 0) {
            increase = true;
            aos_event_set(&g_event_active, OR, GreenActiveEvent);
        }
        pwmDriverInstancePtr->PwmWrite(100 - Duty);
    }
}
void TaskGreenLedPwm(void* args) {
    bool increase = true;
    unsigned char Duty = 0;
    unsigned int originEventFlag;
    PwmInterface *pwmDriverInstancePtr = new PwmDriver(PB2, PB2_PWM, 1, 10000);
    pwmDriverInstancePtr->PwmWrite(100);
    while(true) {
        aos_event_get(&g_event_active, GreenActiveEvent, AOS_EVENT_OR_CLEAR
                , &originEventFlag , RHINO_WAIT_FOREVER);
        if (increase && Duty < 100) {
            Duty++;
            aos_event_set(&g_event_active, OR, GreenActiveEvent);
        }
        if (!increase && Duty > 0)  {
            Duty--;
            aos_event_set(&g_event_active, OR, GreenActiveEvent);
        }
        if (increase && Duty == 100) {
            increase = false;
            Duty--;
            aos_event_set(&g_event_active, OR, GreenActiveEvent);
        }
        if (!increase && Duty == 0) {
            increase = true;
            aos_event_set(&g_event_active, OR, BlueActiveEvent);
        }
        pwmDriverInstancePtr->PwmWrite(100 - Duty);
    }
}
void TaskBlueLedPwm(void* args) {
    bool increase = true;
    unsigned char Duty = 0;
    unsigned int originEventFlag;
    PwmInterface *pwmDriverInstancePtr = new PwmDriver(PB0, PB0_PWM, 0, 10000);
    pwmDriverInstance.PwmWrite(100);
    while(true) {
        aos_event_get(&g_event_active, BlueActiveEvent, AOS_EVENT_OR_CLEAR
                , &originEventFlag, RHINO_WAIT_FOREVER);
        if (increase && Duty < 100) {
            Duty++;
            aos_event_set(&g_event_active, OR, BlueActiveEvent);
        }
        if (!increase && Duty > 0)  {
            Duty--;
            aos_event_set(&g_event_active, OR, BlueActiveEvent);
        }
        if (increase && Duty == 100) {
            increase = false;
            Duty--;
            aos_event_set(&g_event_active, OR, BlueActiveEvent);
        }
        if (!increase && Duty == 0) {
            increase = true;
            aos_event_set(&g_event_active, OR, RedActiveEvent);
        }
        pwmDriverInstancePtr->PwmWrite(100 - Duty);
    }
}

int main(void) {
    board_yoc_init();
    int retVal = 0;
    retVal |= aos_event_new(&g_event_active, RedActiveEvent);
    if (retVal != 0) {
        LOGD("MAIN", "ERROR: can't create event");
    } else {
        retVal |= aos_task_new("Red", TaskRedLedPwm, NULL, 1024);
        retVal |= aos_task_new("Green", TaskGreenLedPwm, NULL, 1024);
        retVal |= aos_task_new("Blue", TaskBlueLedPwm, NULL, 1024);
        if (retVal != 0) {
            LOGD("MAIN", "ERROR: can't create task");
            while(true);
        }
    }
    reutrn retVal;
}

这段代码并不复杂,唯一需要注意的地方在于,在各个Task中对于Event的处理,我们希望各个颜色在先亮后暗的过程完成之后唤起下一个LED的跑马灯流程,故而我们在流程中间需要给自己发送一个Active事件,而在流程结束之后给下一个LED的Task发送一个Avtive事件。

4 浅谈一下代码可移植性问题与适度使用Cpp开发嵌入式

笔者喜欢Cpp的根本原因是Cpp提供的Class为我们提供了很好的可封装性,因为笔者最开始是从Linux开发开始入门的编程,很多时候出于数据安全性的考量,我们不希望某一组函数共用的变量能在外部被访问到,这时Cpp提供的Class就能很好的把这样一组函数和不希望在外部被访问到的变量封装到一起。
至于笔者为何将Interface和Driver封装成两个类,这就涉及到一个代码的可移植性问题了。这里是笔者拙劣的观点,在这里抛砖引玉。由于笔者同时使用的单片机种类较多,很多时候需要在不同的单片机上驱动同一个外设,甚至是在不同单片机之间移植同一套代码。出于不能重复造轮子的考量(其实就是懒),笔者希望将代码分为两个部分:Codebase和Wrapper。CodeBase之中存放各个平台之间通用的代码而Wrapper之中则存放平台之间有差异的代码。
听起来这样似乎很美好,但事实上,在嵌入式开发之中Codebase经常需要调用各种外设,而各种外设的驱动API在各个平台的单片机SDK中又各不相同,这时候,我们就需要约定一种统一的接口供Codebase调用,而这些接口的实现就在Wrapper之中。
看起来此时我们已经解决了这个问题,但这里又带来了新的问题:我们该如何封装这种统一的接口呢?
如果你是一个C语言用户,你可能会不假思索的回答,用宏控制,在不同平台开发时定义不同的宏,决定哪个实现会被编译到二进制之中。诚然,这种实现不失为一种办法,而且性能上其实比笔者的方案更为出色(RAM和ROM开销会略小一些)。
然而,大量的宏会在我们阅读代码的时候带来很大的心智负担,加之某些IDE对于宏控制功能支持的缺失,我们有时候甚至不知道到底使用了哪个实现。而如今的主流MCU早已不是89C52时代那样的性能严重不足了,笔者私以为,用一点额外的二进制大小来换取项目的可维护性时可取的。
当然,我们不能完全的像开发桌面端应用一样在嵌入式之中大量使用诸如STL容器,模板元之类的高级语法,原因很简单:性能开销。
在笔者上文的做法中,可以预见的额外开销只有一个:Cpp中的多态本质上是一个LUT,在编译期编译器会将不同参数的引用转移到不同的符号上,为此我们需要存虚函数表之类的LUT,会增加二进制体积。而当我们使用STL容器的时候,由于STL容器本身的实现方式,会导致大量的碎片化内存,而MCU通常没有MMU(内存管理单元),这也就意味着,我们一旦申请了某个内存,这个内存区域就将永远无法释放,除非程序被reset。加之STL容器内存碎片化的问题,这就会导致整个程序的内存全部被分割的七零八落,我们需要开一个稍大的数组的时候会因为没有一段足够长的连续内存而导致问题。至于模板元,这是个很好的东西,但是模板元的实现加上多态特性,在某些时候会依赖于Cpp提供的RTTI特性,这是一个不小的性能开销。
当然,说了这么多,并不是说我们在开发嵌入式的时候只能将Cpp退化为C With Class这种形式,我们可以在权衡利弊之下选择一些新特性。比如笔者最近在探究能否将Cpp 20规范中的协程用于裸机开发时的IO编程,在大量的IO之下,异步处理会比同步处理快很多(可以参考Java之中的IO于NIO)。
总之,于笔者而言Cpp在嵌入式开发之中是一个非常好用的语言,但在使用其特性的时候需要三思后行,嵌入式开发不比桌面开发,虽然我们可以在可接受范围内承受一些额外的性能开销,但嵌入式终归是比桌面端的资源少了无数倍,我们仍无法承受过多的额外性能开销。

发表回复