概述
代码已推到:
https://github.com/autolaborcenter/test_aurixgithub.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
重启协程;
目前这样实现还不能带着自定义上下文,以后如果需要可以考虑支持一下。