c++ 协程_AURIX 学习笔记·番外(1)用超简陋协程改造嵌入式程序

bc6f5d1a3aff8929f1e4d9a3bdff58b8.png

概述

代码已推到:

https://github.com/autolaborcenter/test_aurix​github.com

为了应对那些简单重复的任务,节约宝贵的 CPU 计算资源,单片机拥有数量众多的外设,CPU 将任务信息存入控制外设的特殊功能寄存器,然后可以去处理其他任务,等待外设完成任务。例如,AURIX 学习笔记(10)硬件 FFT 中就用到 VADC→DMA→FFT→UART,VADC→DMA 是自动的,然后在 DMA 中断中启动 FFT,再在主函数中等待 FFT 完成,进行发送。由于发送耗时远长于 DMA 中断周期(中断周期 = 1024/600 kHz ≈ 1.7 ms,发送 ASCII 耗时约 70 ms),写成了发送期间 DMA 中断什么都不做,STM 每 250 ms 中断允许下一次 DMA 中断响应。代码大致结构:

#include ...

// 定义全局变量
static volatile boolean flag;
...

int core0_main(void) {
    // 外设初始化,开中断,STM、VADC SCAN 启动
    ...

    while (1) {
        WAIT(!flag);             // 等待 STM 中断启动流程
        WAIT(flag);              // 等待 DMA 中断完成,FFT 启动
        WAIT(is_fft_finished()); // 等待 FFT 完成
        print_fft_ascii();       // 发送
    }
    return (1);
}

// STM 任务
void toggle(void) {
    apply_delay(id, TimeConst_10ms * 25); // 设置下一次任务在 250 ms 后
    IfxPort_togglePin(&MODULE_P10, 5);    // 闪个灯
    IfxPort_togglePin(&MODULE_P10, 6);
    flag = FALSE;                         // 启动流 任务
}

// DMA 中断服务
IFX_INTERRUPT(FUN_DMA_FOR_ADC,
              INT_SERVER_DMA_FOR_VADC,
              INT_PRIORITY_DMA_FOR_VADC) {
    // 等待 STM 启动流程
    if (flag)
        return;
    // DMA 地址循环,需要及时将数据拷贝出 DMA 目标地址
    memcpy(copy_buffer, adc_buffer, sizeof(uint16) * DATA_SIZE);
    // 启动异步 FFT
    run_fft(copy_buffer, DATA_SIZE, fft_buffer);
    // 置 FFT 已启动标志位
    flag = TRUE;
}

可以看到,为了实现异步,整个工作流程被分成了三部分,七零八落地分布在各个地方。这里代码还很简单,所以即使零落也能应付,如果工作流程更复杂一些,有更多状态,或者每个状态里有更多操作呢?

要把调度在各种不同地方的代码集中到一处,我们可以使用协程。关于协程是什么、为什么要用协程、怎么分类等等,并不是这个代码的重点。因为显然,异步使用外设、在中断服务中调度具体任务(而不是仅仅设置标志位)本身就自然发生了协作式多任务,这里说的协程只是把逻辑上连续的代码写在一起而已。

基本概念和示例

我们要在 AURIX 的环境下、使用 C 语言,就不可能实现什么复杂的协程语法。暂时,我们就实现一个最简单的状态机协程即可。

要定义、调用协程,我们必须定义下列原语:

  • 创建协程 CO_BEGIN CO_END
  • 协程挂起 CO_YIELD
  • 创建协程上下文 CO_BUILD
  • 协程恢复 CO_RESUME

其中创建、挂起协程用于定义协程流程,属于声明性语句。换句话说,虽然“挂起协程”听起来像是一个动词,但其实是应该理解为“标记一处协程将会挂起的位置”。

而创建协程上下文、恢复协程用于执行协程,属于实际在代码中调用的语句。创建协程上下文是为了把记录协程状态这件事局部化,把当前运行的是哪个协程、上一次这个协程从哪里挂起保存在一个变量里,方便同时存在多个协程的情况。协程恢复指的是将协程从挂起状态恢复到执行状态。

了解了这些基本概念,来看一下改造后的 Cpu0_Main.c 文件:

#include ...

// 定义全局变量
...

// ============================================= //
// coroutine for signal processing               //
// ============================================= //
static CO_EXIST(signal_coroutine);               // there is a coroutine named `signal_coroutine`
static CO_BUILD(continuation, signal_coroutine); // there is a coroutine continuation of `signal_coroutine` named `continuation`
#define COPY_THEN_FFT 1                          // state 1
#define SEND_BY_UART 2                           // state 2
#define STATE_IDLE 3                             // state 3

CO_BEGIN(signal_coroutine)
// stm task

apply_delay(id, TimeConst_10ms * 25);
IfxPort_togglePin(&MODULE_P10, 5);
IfxPort_togglePin(&MODULE_P10, 6);

CO_YIELD(COPY_THEN_FFT)
// dma for adc

memcpy(copy_buffer, adc_buffer, sizeof(uint16) * DATA_SIZE);
run_fft(copy_buffer, DATA_SIZE, fft_buffer);

CO_YIELD(SEND_BY_UART)
// main function

WAIT(is_fft_finished());
print_fft_ascii();

CO_YIELD(STATE_IDLE)

CO_END
// ============================================= //

int core0_main(void) {
    // 外设初始化,开中断,STM、VADC SCAN 启动
    ...

    while (1)
        if (continuation.state == SEND_BY_UART)
            CO_RESUME(continuation);
    return (1);
}

void toggle(void) {
    CO_LAUNCH(continuation);
}

IFX_INTERRUPT(FUN_DMA_FOR_ADC,
              INT_SERVER_DMA_FOR_VADC,
              INT_PRIORITY_DMA_FOR_VADC) {
    if (continuation.state == COPY_THEN_FFT)
        CO_RESUME(continuation);
} 

连续的流程现在写成了连续的,在各个异步环节的调度过程只是声明了将会恢复协程。有没有觉得代码变得更好理解了呢?

实现

相信熟悉 C 语言或者了解协程的读者一眼就看出这个是怎么实现的了,其实非常简单。如果你没想到,不妨看一个提示:

#include <stdio.h>

void coroutine(int state) {
    switch (state) {
        case 0:
            printf("a");
            break;
        case 1:
            printf("b");
            break;
        case 2:
            printf("c");
            break;
        default:
            break;
    }
}

void resume(void (*coroutine)(int), int state) {
    coroutine(state);
}

// Console: abc
int main() {
    resume(coroutine, 0);
    resume(coroutine, 1);
    resume(coroutine, 2);
}

这个例子够简单了吧!其实前面那个协程就是这个东西,只不过用宏定义把整个 switch 结构都藏了起来。看文件 COROUTINE.h(我习惯把宏和主要放宏的头文件全大写):

#ifndef COROUTINE_H
#define COROUTINE_H

typedef unsigned char state_t;
typedef unsigned char bool_t;

typedef struct {
    volatile state_t state;
    
    bool_t (*function)(state_t);
    
} continuation_t;

#define CO_EXIST(NAME) bool_t NAME(state_t)
#define CO_BEGIN(NAME) bool_t NAME(state_t _STATE_){ switch(_STATE_) { case 0: {
#define CO_YIELD(N)    return 1; } case N: {
#define CO_END         return 0; } default: return 0; }}

#define CO_BUILD(CO, FUN) continuation_t CO = {0, FUN}
#define CO_RESET(CO)      (CO).state = 0
#define CO_RESUME(CO)     ((CO).function((CO).state)) ? (++(CO).state) : 0
#define CO_LAUNCH(CO)     CO_RESET(CO); CO_RESUME(CO)

#endif // COROUTINE_H

一些微小的变化包括:

  • 协程挂起时返回一个 bool_t,指示整个协程是否运行完成;
  • 要求协程的状态号连续,每次协程恢复直接自增,直到停在最后一个状态上(如果循环恢复且不理会返回的 bool_t,最后一个状态会反复运行);
  • 添加 CO_EXIST(存在名为 NAME 的协程)用于声明协程,方便前置创建全局协程上下文;
  • 添加 CO_RESET 重置协程上下文;
  • 添加 CO_LAUNCH 重启协程;

目前这样实现还不能带着自定义上下文,以后如果需要可以考虑支持一下。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值