PPU(Parallel Processing Unit,并行处理单元) 是英飞凌(Infineon)AURIX™ TC4x系列单片机中的专用协处理器,专为加速高并行计算任务而设计。它与TriCore™ 1.8 CPU协同工作,负责卸载计算密集型任务,如数字信号处理(DSP)和神经网络推理,从而实现最高汽车安全等级(ASIL-D)的人工智能(AI)能力。
备注:本文demo基于tasking编译器分析,如有需求欢迎大家一起交流学习:
support@softor.com.cn
PPU在AURIX™ TC4x系列中的作用:
PPU基于Synopsys DesignWare ARC EV71架构实现,包含两个主要部分:
PPU的向量宽度取决于具体的产品型号:
向量宽度越大,单次可处理的数据越多,并行处理能力越强。
从英飞凌的技术文档中可以看到,PPU的向量DSP单元具有以下特性:
PPU的内存架构设计高效且灵活:
PPU专为汽车和工业应用设计,主要应用场景包括:
在本PPU demo项目中,PPU的作用包括:
相比其他方案,使用PPU的优势:
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
TC4xx系列单片机的PPU是一个强大的并行处理单元,它:
本demo项目正是PPU强大能力的一个缩影,展示了如何通过TASKING SmartCode和RTE库,轻松利用PPU的并行计算能力加速信号处理任务!
这是一个基于TASKING PPU Math库和TASKING RTE(Runtime Environment)库的示例项目,演示如何在TriCore处理器上通过RTE库调用PPU(Parallel Processing Unit)执行复数向量的逆快速傅里叶变换(IFFT)操作。
重要提示:RTE库是TASKING SmartCode开发环境提供的一个标准特色库,专门用于简化多核(TriCore + PPU)之间的通信和任务管理。
ppu_demo_inf/
├── ppu_math_ifft_cf32_app/ # PPU应用程序
│ ├── g_tsk_rte_config_ppu.c # PPU RTE配置
│ ├── g_tsk_rte_init_ppu.c # PPU RTE初始化
│ ├── ppu_main.c # PPU主函数
│ ├── ppu_math_ifft_cf32_app.c # IFFT实现
│ ├── ppu_math_ifft_cf32_app.lsl # 链接脚本
│ └── readme.txt # 说明文档
└── ppu_math_ifft_cf32_tcmain/ # TriCore主程序
├── cstart.c # C启动文件
├── g_tsk_rte_config.h # RTE配置头文件
├── g_tsk_rte_tc0.c # TriCore RTE实现
├── ppu_math_ifft_cf32_tcmain.c # 主程序实现
├── ppu_math_ifft_cf32_tcmain.lsl # 链接脚本
├── readme.txt # 说明文档
└── user_*.h # 用户配置文件
TASKING RTE(Runtime Environment)库是TASKING SmartCode开发环境提供的一个标准特色库,专门为英飞凌AURIX系列芯片(如TC49x)的多核开发而设计。
RTE库解决了多核开发中的以下关键问题:
RTE库包含两个主要部分:
libppu_rte.a):libppu_rte.a):RTE库提供了完整的任务生命周期管理:
任务生命周期:
创建 → 配置 → 提交 → 执行 → 完成 → 通知
↓ ↓ ↓ ↓ ↓ ↓
TriCore TriCore RTE队列 PPU PPU TriCore
**关键API(TriCore侧):
tsk_rte_job_claim():申请任务对象tsk_rte_job_init():初始化任务tsk_rte_op_*():添加任务操作tsk_rte_job_set_notify():设置通知方式tsk_rte_queue_add_job():将任务添加到队列关键API(PPU侧):
tsk_rte_ppu_core_process():处理待执行的任务tsk_rte_ppu_notify():通知任务完成RTE库提供了预分配的内存池机制:
**内存池的优势:
关键API:
tsk_rte_mem_claim():申请内存缓冲区tsk_rte_mem_release():释放内存缓冲区tsk_rte_ppu_buffer_get_address():获取缓冲区地址这是RTE库最特色的功能之一,允许TriCore调用PPU上的函数:
DPF工作原理:
预定义的DPF函数(来自user_dpf_functions.h):
tsk_rte_ppu_write():写内存tsk_rte_ppu_read():读内存tsk_rte_ppu_add():矩阵加法tsk_rte_ppu_sub():矩阵减法tsk_rte_ppu_mul():矩阵乘法tsk_rte_ppu_scalar_mul():标量乘法custom_tsk_ppu_math_ifft_cf32)RTE库支持两种通知方式:
TSK_RTE_NOTIFY_BLOCK):TSK_RTE_NOTIFY_CALLBACK):从makefile可以看到RTE库的使用:
**PPU应用程序链接:
-t "${ECLIPSE_HOME}/../carc/lib/tc49x/libppu_math.a"
-t "${ECLIPSE_HOME}/../carc/lib/tc49x/libppu_rte.a"
-Wl-lppu_rte
**TriCore主程序链接:
-t "${ECLIPSE_HOME}/../ctc/lib/tc18/libppu_rte.a"
最关键的是--new-task选项:
--new-task=ppu,"ppu_math_ifft_cf32_app.out",...
这个选项是TASKING SmartCode链接器的特色功能,它:
RTE库的配置文件(如g_tsk_rte_config.h、user_pools_config.h)通常由SmartCode的配置工具自动生成,确保配置的一致性。
使用TASKING RTE库相比手动实现多核通信的优势:
PPU应用程序负责实际执行IFFT操作,主要包含以下文件:
tsk_rte_ppu_init()初始化PPU RTEtsk_rte_ppu_core_process()处理任务custom_tsk_ppu_math_ifft_cf32tsk_fft_init_cf32() – 初始化FFT所需的旋转因子和位反转表tsk_ifft_cf32() – 执行逆快速傅里叶变换TriCore主程序负责准备数据、调用PPU执行IFFT、验证结果,主要包含以下功能:
synchronous_example函数)asynchronous_example函数)__attribute__((uncached))标记未缓存内存,确保数据一致性,避免缓存一致性问题tsk_rte_mem_claim()申请共享内存缓冲区,用于TriCore和PPU之间的数据交换┌─────────────────────────────────────┐
│ TriCore 侧 │
├─────────────────────────────────────┤
│ 1. 初始化 RTE │
│ 2. 准备输入数据和预期结果 │
│ 3. 申请队列、任务和内存资源 │
│ 4. 配置任务参数和操作序列 │
│ 5. 设置通知方式(同步/异步) │
│ 6. 提交任务到队列 │
│ 7. 等待任务完成(同步/回调) │
│ 8. 读取结果并验证 │
└───────────────┬─────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 任务队列(RTE管理) │
└───────────────┬─────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ PPU 侧 │
├─────────────────────────────────────┤
│ 1. 初始化 RTE │
│ 2. 循环处理任务 │
│ 3. 从队列获取任务 │
│ 4. 解析任务参数 │
│ 5. 从共享内存读取输入数据 │
│ 6. 初始化 FFT(旋转因子、位反转表) │
│ 7. 执行 IFFT 计算 │
│ 8. 将结果写回共享内存 │
│ 9. 通知 TriCore 任务完成 │
└─────────────────────────────────────┘
步骤1:系统初始化
tsk_rte_init()初始化RTE库tsk_rte_ppu_init()初始化PPU RTE,然后进入主循环tsk_rte_ppu_core_process()等待任务步骤2:数据准备
a_c(32个复数元素)expected_result,用于验证计算结果c_c,用于存储PPU的计算结果步骤3:资源申请
tsk_rte_job_queue_claim(TSK_RTE_QUEUE_0)tsk_rte_job_claim(TSK_RTE_JOB_0)tsk_rte_mem_claim(TSK_RTE_MEM_VCCM_4)tsk_rte_mem_claim(TSK_RTE_MEM_VCCM_5)tsk_rte_mem_claim(TSK_RTE_MEM_VCCM_6)tsk_rte_mem_claim(TSK_RTE_MEM_VCCM_7)步骤4:任务配置
custom_tsk_ppu_math_ifft_cf32_payload_ttsk_rte_job_init(job_id)tsk_rte_op_write_memory(job_id, buf_a, a_c, sizeof(a_c))tsk_rte_op_custom_dpf(...)tsk_rte_op_read_memory(job_id, buf_out, c_c, sizeof(c_c))步骤5:设置通知方式
tsk_rte_job_set_notify(job_id, TSK_RTE_NOTIFY_BLOCK, NULL, NULL)tsk_rte_job_set_notify(job_id, TSK_RTE_NOTIFY_CALLBACK, notify_test_callback, (void*)&status)步骤6:提交任务
tsk_rte_queue_add_job(queue_id, job_id, &id)tsk_rte_job_queue_release(queue_id)、tsk_rte_job_release(job_id)、tsk_rte_mem_release(...)步骤7:PPU执行任务
tsk_fft_init_cf32(twiddles, rev, length)tsk_ifft_cf32(a_vccm, c_vccm, twiddles, rev, length)步骤8:结果获取
步骤9:结果验证
c_c与预期结果expected_resultfailed_jobs_sync或failed_jobs_async步骤10:循环执行
synchronous_example()和asynchronous_example()custom_tsk_ppu_math_ifft_cf32_payload_t – 包含IFFT操作所需的参数:代码示例:ppu_math_ifft_cf32_app.c中的custom_tsk_ppu_math_ifft_cf32函数
// 1. 数据获取
__vccm _Complex float *a_vccm = (_Complex float __vccm*)(int)tsk_rte_ppu_buffer_get_address(
unpacked_payload.buffer_id_in0
);
__vccm _Complex float *c_vccm = (_Complex float __vccm*)(int)tsk_rte_ppu_buffer_get_address(
unpacked_payload.buffer_id_out0
);
unsigned int length = unpacked_payload.length;
__vccm float *twiddles = ( float __vccm* )(int)tsk_rte_ppu_buffer_get_address(
unpacked_payload.buffer_id_twiddles
);
__vccm unsigned short *rev = (unsigned short __vccm*)(int)tsk_rte_ppu_buffer_get_address(
unpacked_payload.buffer_id_rev
);
// 2. FFT初始化
tsk_fft_init_cf32(twiddles, rev, length);
// 3. IFFT执行
tsk_ifft_cf32(a_vccm, c_vccm, twiddles, rev, length);
详细解释:
tsk_rte_ppu_buffer_get_address()函数获取各个缓冲区的VCCM地址a_vccm:存储从TriCore传来的复数数组c_vccm:用于存储计算结果twiddles:存储预计算的三角函数值rev:存储FFT计算所需的位反转索引tsk_fft_init_cf32()函数初始化旋转因子和位反转表e^(-j*2πk/N)的值,避免重复计算tsk_ifft_cf32()函数执行逆快速傅里叶变换c_vccm输出缓冲区中代码示例1:TriCore中的数据准备和任务配置(同步模式)
// 1. 资源申请
tsk_rte_queue_id_t queue_id = tsk_rte_job_queue_claim(TSK_RTE_QUEUE_0);
tsk_rte_job_id_t job_id = tsk_rte_job_claim(TSK_RTE_JOB_0);
tsk_rte_mem_id_t buf_a = tsk_rte_mem_claim(TSK_RTE_MEM_VCCM_4);
tsk_rte_mem_id_t buf_out = tsk_rte_mem_claim(TSK_RTE_MEM_VCCM_5);
tsk_rte_mem_id_t buf_twiddles = tsk_rte_mem_claim(TSK_RTE_MEM_VCCM_6);
tsk_rte_mem_id_t buf_rev = tsk_rte_mem_claim(TSK_RTE_MEM_VCCM_7);
// 2. 配置任务参数
custom_tsk_ppu_math_ifft_cf32_payload_t custom_dpf_ifft_pay_load = {
.function_id = CUSTOM_TSK_PPU_MATH_IFFT_CF32_FUNCTION_ID,
.buffer_id_in0 = buf_a,
.buffer_id_out0 = buf_out,
.buffer_id_twiddles = buf_twiddles,
.buffer_id_rev = buf_rev,
.length = ARRAY_SIZE,
};
// 3. 配置任务操作
tsk_rte_job_init(job_id);
tsk_rte_op_write_memory(job_id, buf_a, a_c, sizeof(a_c));
tsk_rte_op_custom_dpf(job_id, (tsk_rte_function_payload_t*)&payload, sizeof(custom_tsk_ppu_math_ifft_cf32_payload_t));
tsk_rte_op_read_memory(job_id, buf_out, c_c, sizeof(c_c));
// 4. 设置通知方式(同步模式)
tsk_rte_job_set_notify(job_id, TSK_RTE_NOTIFY_BLOCK, NULL, NULL);
// 5. 提交任务
tsk_rte_job_id_t id;
tsk_rte_status_t status = tsk_rte_queue_add_job(queue_id, job_id, &id);
代码示例2:TriCore中的异步模式实现
// 设置回调函数
tsk_rte_job_set_notify(job_id, TSK_RTE_NOTIFY_CALLBACK, notify_test_callback, (void*)&status);
// 提交任务
tsk_rte_job_id_t id;
tsk_rte_status_t job_add_status = tsk_rte_queue_add_job(queue_id, job_id, &id);
// 等待回调触发
while (async_trigger == false)
{
__nop(); __nop(); __nop(); __nop(); __nop(); __nop(); __nop();
}
详细解释:
tsk_rte_mem_claim()申请共享内存缓冲区tsk_rte_mem_release()释放资源tsk_rte_queue_claim()获取任务队列tsk_rte_job_claim()获取任务对象tsk_rte_queue_add_job()将任务添加到队列中TSK_RTE_NOTIFY_BLOCK,TriCore会阻塞等待任务完成TSK_RTE_NOTIFY_CALLBACK,通过回调函数通知任务完成代码示例:PPU主循环
int main(void)
{
tsk_rte_ppu_init();
while (true)
{
tsk_rte_ppu_core_process();
}
}
详细解释:
tsk_ifft_cf32()函数内部使用SIMD指令并行计算FFT__vccm修饰符标识VCCM内存访问tsk_fft_init_cf32()预计算旋转因子和位反转表a_c)c_c与expected_result)性能对比示例:
具体加速效果:
ppu_math_ifft_cf32_appppu_math_ifft_cf32_tcmain项目使用TASKING SmartCode v10.4r1编译器,包含两个独立的工具链:
C:\Program Files\TASKING\SmartCode v10.4r1\carc\bin\ccarc-O2 --tradeoff=4--fp-model=clznrC:\Program Files\TASKING\SmartCode v10.4r1\ctc\bin\cctc-O2 --tradeoff=4--fp-model=clznrT--fpu=dp (双精度浮点)编译命令示例(来自ppu_math_ifft_cf32_app的subdir.mk):
# 编译ppu_math_ifft_cf32_app.c
ccarc -o ppu_math_ifft_cf32_app.o ..\ppu_math_ifft_cf32_app.c \
-Ctc49x \
-t \
-Wa-H"sfr/regppu.def" \
-Wa-gAHLs \
--emit-locals=-equs,-symbols \
-H"sfr/regppu.sfr" \
-I"carc/include.ppu_rte" \
-I"carc/include.ppu_math" \
-I"ppu_math_ifft_cf32_tcmain" \
--iso=99 \
-O2 \
--tradeoff=4 \
-g \
-c
关键编译选项说明:
-Ctc49x:指定目标芯片为TC49x-t:生成列表文件-H"sfr/regppu.def":包含PPU特殊功能寄存器定义-I"include.ppu_rte":PPU RTE头文件路径-I"include.ppu_math":PPU Math库头文件路径-O2 --tradeoff=4:开启优化,平衡速度和代码大小编译命令示例(来自ppu_math_ifft_cf32_tcmain的subdir.mk):
# 编译ppu_math_ifft_cf32_tcmain.c
cctc -o ppu_math_ifft_cf32_tcmain.o ..\ppu_math_ifft_cf32_tcmain.c \
-Ctc49x \
--lsl-core=tc0 \
-t \
-I"ctc/include.ppu_rte" \
--iso=11 \
--fp-model=clznrT \
--fpu=dp \
-O2 \
--tradeoff=4 \
--loop=Vlfist \
-g \
-c
关键编译选项说明:
--lsl-core=tc0:指定TriCore核为TC0--fpu=dp:启用双精度浮点单元--loop=Vlfist:启用循环优化链接命令(来自ppu_math_ifft_cf32_app的makefile):
ccarc
# 链接选项内容:
# -o ppu_math_ifft_cf32_app.out
# g_tsk_rte_config_ppu.o
# g_tsk_rte_init_ppu.o
# ppu_main.o
# ppu_math_ifft_cf32_app.o
# -Ctc49x
# -t "lib/tc49x/libppu_math.a"
# -t "lib/tc49x/libppu_rte.a"
# -Wl-OtxycL
# -Wl--map-file=ppu_math_ifft_cf32_app.mapxml:XML
# -Wl-mcrfiklSmNoduQ
# --link-only
# -Wl-lppu_rte
# -g
链接的库文件:
libppu_math.a:PPU数学库,包含FFT/IFFT函数libppu_rte.a:PPU运行时环境库libc_fpu.a:带浮点支持的C标准库librt.a:运行时库链接命令(来自ppu_math_ifft_cf32_tcmain的makefile第27行):
--new-task=ppu,"D: ianpb\ppu_demo_inf\ppu_math_ifft_cf32_app\Debug\ppu_math_ifft_cf32_app.out",-Mppu_math_ifft_cf32_app.mapxml:XML
这是最关键的一步!--new-task选项将PPU程序整合到TriCore的ELF文件中。
完整链接过程:
TriCore主程序链接流程:
├─ TriCore程序 (ppu_math_ifft_cf32_tcmain.o)
├─ RTE库 (libppu_rte.a)
├─ PPU程序 (ppu_math_ifft_cf32_app.out) ← 通过--new-task嵌入
│ └─ PPU应用代码
│ └─ PPU RTE库
│ └─ PPU Math库
└─ 生成最终ELF (ppu_math_ifft_cf32_tcmain.elf)
从ppu_math_ifft_cf32_app.map中可以看到从库中提取的符号:
| tsk_ppu_math_fix_bin_vec.o | libppu_math.a | tsk_fft_init_cf32 |
| tsk_ppu_math_fix_bin_vec.o | libppu_math.a | tsk_ifft_cf32 |
| tsk_rte_job_handling.o | libppu_rte.a | tsk_rte_ppu_core_process |
| tsk_rte_ppu_buffer.o | libppu_rte.a | tsk_rte_ppu_buffer_get_address |
| tsk_rte_ppu_init.o | libppu_rte.a | tsk_rte_ppu_init |
TriCore → PPU的完整调用链:
TriCore侧:
main()
├─ tsk_rte_init() [RTE库]
├─ synchronous_example() / asynchronous_example()
│ ├─ tsk_rte_job_queue_claim() [RTE库]
│ ├─ tsk_rte_job_claim() [RTE库]
│ ├─ tsk_rte_mem_claim() [RTE库]
│ ├─ tsk_rte_job_init() [RTE库]
│ ├─ tsk_rte_op_write_memory() [RTE库]
│ ├─ tsk_rte_op_custom_dpf() [RTE库]
│ ├─ tsk_rte_op_read_memory() [RTE库]
│ ├─ tsk_rte_job_set_notify() [RTE库]
│ └─ tsk_rte_queue_add_job() [RTE库]
→ 任务通过RTE队列发送到PPU
PPU侧:
main()
├─ tsk_rte_ppu_init() [PPU RTE库]
└─ tsk_rte_ppu_core_process() [PPU RTE库] (循环调用)
└─ custom_tsk_ppu_math_ifft_cf32() [用户代码]
├─ tsk_rte_ppu_buffer_get_address() [PPU RTE库]
├─ tsk_fft_init_cf32() [PPU Math库]
└─ tsk_ifft_cf32() [PPU Math库]
1. 硬件上电/复位
↓
2. TriCore启动 (从启动ROM或Flash执行)
↓
3. TriCore执行cstart.c中的启动代码
↓
4. TriCore初始化RTE (tsk_rte_init())
↓
5. TriCore加载并启动PPU程序
(通过RTE和--new-task嵌入的PPU代码)
↓
6. PPU初始化RTE (tsk_rte_ppu_init())
↓
7. PPU进入主循环,等待任务 (tsk_rte_ppu_core_process())
↓
8. TriCore主函数开始执行,提交IFFT任务
↓
9. 任务在TriCore和PPU之间通过RTE队列传递
PPU使用的内存区域:
mpe:ppu:linear:PPU线性地址空间从g_tsk_rte_config_ppu.c和user_dpf_functions.h可以看到:
user_dpf_functions.h中声明自定义函数ppu_math_ifft_cf32_app.c中实现函数g_tsk_rte_config_ppu.c中注册到DPF表g_tsk_rte_function_id.h中分配唯一函数ID调用流程:
TriCore:
tsk_rte_op_custom_dpf(job_id, payload, size)
└─ payload包含function_id = CUSTOM_TSK_PPU_MATH_IFFT_CF32_FUNCTION_ID
→ RTE传递到PPU
PPU:
tsk_rte_ppu_core_process()
└─ 从DPF表中查找function_id
└─ 调用对应的函数custom_tsk_ppu_math_ifft_cf32()
基于对代码、编译产物和配置文件的全面分析,让我从技术底层的角度进行更深入的解析。
让我详细拆解DPF机制的每个环节:
第一步:Payload结构体定义(user_dpf_config.h)
typedef struct
{
tsk_rte_function_id_t function_id; // 函数ID
tsk_rte_mem_id_t buffer_id_in0; // 输入缓冲区ID
tsk_rte_mem_id_t buffer_id_twiddles; // 旋转因子缓冲区
tsk_rte_mem_id_t buffer_id_rev; // 位反转表缓冲区
tsk_rte_mem_id_t buffer_id_out0; // 输出缓冲区ID
unsigned int length; // 数据长度
} custom_tsk_ppu_math_ifft_cf32_payload_t;
这个结构体是TriCore和PPU之间数据传递的契约。
第二步:联合体重载(g_tsk_rte_dpf.h)
typedef union
{
tsk_rte_function_payload_t function_payload;
// ... 其他预定义的payload ...
custom_tsk_ppu_math_ifft_cf32_payload_t custom_tsk_ppu_math_ifft_cf32_payload;
} tsk_rte_payload_t;
使用联合体的好处是:可以通过同一个内存空间传递不同类型的payload,节省内存。
第三步:函数声明(user_dpf_functions.h)
extern tsk_rte_status_t custom_tsk_ppu_math_ifft_cf32(
__uncached tsk_rte_function_payload_t* payload
);
这里使用的是通用的tsk_rte_function_payload_t*指针,而不是具体的payload类型,这是为了统一函数签名。
第四步:DPF表注册(g_tsk_rte_config_ppu.c)
tsk_rte_dpf_t tsk_rte_dpf_table[] = {
tsk_rte_ppu_write, // 索引0
tsk_rte_ppu_write_2d, // 索引1
tsk_rte_ppu_read, // 索引2
tsk_rte_ppu_read_2d, // 索引3
tsk_rte_ppu_add, // 索引4
tsk_rte_ppu_sub, // 索引5
tsk_rte_ppu_mul, // 索引6
tsk_rte_ppu_scalar_mul, // 索引7
custom_tsk_ppu_math_ifft_cf32 // 索引8 ← 我们的函数
};
关键! 函数ID与数组索引的对应关系(g_tsk_rte_function_id.h):
typedef enum
{
TSK_RTE_PPU_WRITE_FUNCTION_ID = 0, // 对应索引0
TSK_RTE_PPU_WRITE_2D_FUNCTION_ID = 1, // 对应索引1
TSK_RTE_PPU_READ_FUNCTION_ID = 2, // 对应索引2
TSK_RTE_PPU_READ_2D_FUNCTION_ID = 3, // 对应索引3
TSK_RTE_PPU_ADD_FUNCTION_ID = 4, // 对应索引4
TSK_RTE_PPU_SUB_FUNCTION_ID = 5, // 对应索引5
TSK_RTE_PPU_MUL_FUNCTION_ID = 6, // 对应索引6
TSK_RTE_PPU_SCALAR_MUL_FUNCTION_ID = 7, // 对应索引7
CUSTOM_TSK_PPU_MATH_IFFT_CF32_FUNCTION_ID = 8, // 对应索引8
TSK_RTE_FUNCTION_ID_INVALID = 0x7fffffff
} tsk_rte_function_id_t;
这是一个优雅的设计:枚举值直接作为数组索引,无需任何映射!
让我们看TriCore侧如何构建和提交任务(ppu_math_ifft_cf32_tcmain.c):
// 第1步:创建具体的payload
custom_tsk_ppu_math_ifft_cf32_payload_t custom_dpf_ifft_pay_load = {
.function_id = CUSTOM_TSK_PPU_MATH_IFFT_CF32_FUNCTION_ID, // 设为8
.buffer_id_in0 = buf_a,
.buffer_id_out0 = buf_out,
.buffer_id_twiddles = buf_twiddles,
.buffer_id_rev = buf_rev,
.length = ARRAY_SIZE,
};
// 第2步:赋值给联合体
tsk_rte_payload_t payload;
memset(&payload, 0, sizeof(payload));
payload.custom_tsk_ppu_math_ifft_cf32_payload = custom_dpf_ifft_pay_load;
// 第3步:添加操作到任务
tsk_rte_op_custom_dpf(
job_id,
(tsk_rte_function_payload_t*)&payload, // 再次转换为通用指针
sizeof(custom_tsk_ppu_math_ifft_cf32_payload_t)
);
然后看PPU侧如何解析和调用(ppu_math_ifft_cf32_app.c):
tsk_rte_status_t custom_tsk_ppu_math_ifft_cf32(
__uncached tsk_rte_function_payload_t* payload // 接收通用指针
)
{
// 第1步:转换回父级指针
__uncached tsk_rte_payload_t* parent_payload =
(__uncached tsk_rte_payload_t*)payload;
// 第2步:提取具体的payload
custom_tsk_ppu_math_ifft_cf32_payload_t unpacked_payload =
parent_payload->custom_tsk_ppu_math_ifft_cf32_payload;
// 第3步:使用payload中的数据
__vccm _Complex float *a_vccm = (_Complex float __vccm*)(int)
tsk_rte_ppu_buffer_get_address(unpacked_payload.buffer_id_in0);
// ... 执行计算 ...
}
RTE内部的调用过程(从map文件推断):
tsk_rte_queue_add_job() → 任务写入队列tsk_rte_ppu_core_process()循环检查队列function_idfunction_id作为索引:tsk_rte_dpf_table[function_id]()让我们看内存是如何管理的(user_pools_config.h和g_tsk_rte_config_ppu.c):
// user_pools_config.h
__aligned__(TSK_RTE_PPU_BUFFER_ALIGNMENT) static __vccm _Complex float fc_a[512];
__aligned__(TSK_RTE_PPU_BUFFER_ALIGNMENT) static __vccm _Complex float fc_c[512];
__aligned__(TSK_RTE_PPU_BUFFER_ALIGNMENT) static __vccm float twiddles[32];
__aligned__(TSK_RTE_PPU_BUFFER_ALIGNMENT) static __vccm unsigned short rev[32];
关键点:
__vccm:指定这些数组放在VCCM内存中__aligned__(TSK_RTE_PPU_BUFFER_ALIGNMENT):对齐到向量宽度,便于SIMD访问// g_tsk_rte_config_ppu.c
__no_sda __uncached tsk_rte_memory_pool_entry_t tsk_rte_memory_partition_pool[] = {
{ .in_use = 0, .mem_id = {.internal = {5, TSK_RTE_MEM_VCCM_1}}, {(void*)a, sizeof(a)} },
{ .in_use = 0, .mem_id = {.internal = {6, TSK_RTE_MEM_VCCM_2}}, {(void*)b, sizeof(b)} },
{ .in_use = 0, .mem_id = {.internal = {7, TSK_RTE_MEM_VCCM_3}}, {(void*)c, sizeof(c)} },
{ .in_use = 0, .mem_id = {.internal = {8, TSK_RTE_MEM_VCCM_4}}, {(void*)fc_a, sizeof(fc_a)} },
{ .in_use = 0, .mem_id = {.internal = {9, TSK_RTE_MEM_VCCM_5}}, {(void*)fc_c, sizeof(fc_c)} },
{ .in_use = 0, .mem_id = {.internal = {10, TSK_RTE_MEM_VCCM_6}}, {(void*)twiddles, sizeof(twiddles)} },
{ .in_use = 0, .mem_id = {.internal = {11, TSK_RTE_MEM_VCCM_7}}, {(void*)rev, sizeof(rev)} }
};
内存池工作原理:
in_use标志tsk_rte_mem_claim()查找in_use == 0的条目,设为1并返回IDtsk_rte_mem_release()将in_use设回0mem_id包含内部索引和类型信息// 在PPU侧
__vccm _Complex float *a_vccm = (_Complex float __vccm*)(int)
tsk_rte_ppu_buffer_get_address(unpacked_payload.buffer_id_in0);
这里发生了什么?
tsk_rte_ppu_buffer_get_address():根据buffer_id在内存池中查找,返回物理地址(int):可能是为了处理指针类型转换(_Complex float __vccm*):转换为具体类型的指针,并标记为VCCM内存让我们看任务是如何管理的:
__no_sda __uncached tsk_rte_job_pool_entry_t tsk_rte_job_pool[] = {
{ .in_use = 0, .job_id = {.internal = {1, TSK_RTE_JOB_0}}, .job = { .operations = { .ops_size = 5 }}},
{ .in_use = 0, .job_id = {.internal = {2, TSK_RTE_JOB_1}}, .job = { .operations = { .ops_size = 5 }}}
};
这里只有2个任务槽位,但每个任务可以包含最多5个操作!
__no_sda __uncached tsk_rte_queue_pool_entry_t tsk_rte_queue_pool[] = {
{ .in_use = 0, .queue_id = {.internal = {3, TSK_RTE_QUEUE_0}}, .queue = { .job_table = { .jobs_size = 4, }}},
{ .in_use = 0, .queue_id = {.internal = {4, TSK_RTE_QUEUE_1}}, .queue = { .job_table = { .jobs_size = 4, }}}
};
每个队列最多容纳4个任务!
PPU使用ARC架构:
TriCore使用TriCore架构:
两者指令集完全不同,必须分别编译!
从TriCore的makefile第27行:
--new-task=ppu,"D: ianpb\ppu_demo_inf\ppu_math_ifft_cf32_app\Debug\ppu_math_ifft_cf32_app.out",...
这个链接器选项的作用:
.out文件为什么要嵌入而不是单独烧录?
让我总结一下上电后的完整执行流程:
上电/复位
↓
TriCore从启动ROM执行
↓
TriCore执行cstart.c(设置堆栈、初始化硬件)
↓
TriCore调用main()
↓
TriCore调用tsk_rte_init()
├─ 初始化内存池
├─ 初始化任务池
├─ 初始化队列池
├─ 查找嵌入的PPU程序(通过--new-task)
├─ 将PPU代码复制到PPU的代码内存
├─ 配置PPU的堆栈和寄存器
└─ 启动PPU核
↓
PPU开始执行
↓
PPU调用tsk_rte_ppu_init()
├─ 初始化PPU侧的RTE
└─ 准备接收任务
↓
PPU进入主循环:tsk_rte_ppu_core_process()
↓
TriCore继续执行main()
↓
TriCore进入循环:
├─ synchronous_example()
│ ├─ 申请资源
│ ├─ 准备payload
│ ├─ 配置任务操作
│ ├─ 提交任务到队列
│ ├─ 阻塞等待
│ └─ 验证结果
└─ asynchronous_example()
├─ 类似,但使用回调
└─ 轮询等待标志
↓
任务在队列中
↓
PPU的tsk_rte_ppu_core_process()发现任务
↓
提取function_id
↓
查找DPF表:tsk_rte_dpf_table[function_id]
↓
调用custom_tsk_ppu_math_ifft_cf32()
├─ 获取缓冲区地址
├─ 初始化FFT
├─ 执行IFFT
└─ 返回
↓
PPU通知TriCore(通过中断或标志)
↓
TriCore唤醒(同步模式)或回调触发(异步模式)
↓
TriCore读取结果
↓
验证并统计
↓
循环继续...
这个系统体现了几个优秀的设计模式:
在深入分析了整个PPU demo项目后,让我从系统架构的角度来总结这个项目的设计思想和技术亮点。
这个demo项目体现了异构多核计算的设计理念,具有以下特点:
系统采用了清晰的分层设计:
应用层(用户代码)
↓
RTE库层(任务管理、内存管理、通信)
↓
硬件抽象层(寄存器访问、中断处理)
↓
硬件层(TriCore、PPU、共享内存)
这种分层设计带来的好处:
所有关键资源(任务、队列、内存)都采用预分配策略:
让我用一个更清晰的方式来描述数据在整个系统中的流动:
TriCore侧:
1. 输入数据 a_c[] (在TriCore的本地内存)
↓
2. tsk_rte_op_write_memory() → 复制到VCCM共享内存
↓
3. 任务提交到队列
PPU侧:
4. 从队列获取任务
↓
5. 从VCCM读取输入数据
↓
6. 执行IFFT计算
↓
7. 结果写回VCCM
↓
8. 通知TriCore
TriCore侧:
9. tsk_rte_op_read_memory() → 从VCCM复制到c_c[]
↓
10. 验证结果
关键观察点:
基于对代码的分析,我识别出以下可能的性能瓶颈:
正如前面提到的,这个项目运用了多个优秀的设计模式,让我深入分析一下:
任务对象封装了:
这种封装使得:
预分配的资源池(任务池、内存池、队列池):
同步和异步两种通知策略:
基于这个demo项目,让我提供一些实际应用的扩展指导,帮助您将这个示例应用到真实的项目中。
如果您想添加自己的PPU计算函数,只需按照以下步骤:
在user_dpf_config.h中添加:
typedef struct
{
tsk_rte_function_id_t function_id;
tsk_rte_mem_id_t buffer_id_input;
tsk_rte_mem_id_t buffer_id_output;
unsigned int param1;
float param2;
} custom_my_function_payload_t;
在g_tsk_rte_dpf.h中添加:
typedef union
{
tsk_rte_function_payload_t function_payload;
// ... 现有的payload ...
custom_my_function_payload_t custom_my_function_payload;
} tsk_rte_payload_t;
在user_dpf_functions.h中添加:
extern tsk_rte_status_t custom_my_function(
__uncached tsk_rte_function_payload_t* payload
);
在g_tsk_rte_function_id.h中添加:
typedef enum
{
// ... 现有的ID ...
CUSTOM_MY_FUNCTION_FUNCTION_ID = 9,
TSK_RTE_FUNCTION_ID_INVALID = 0x7fffffff
} tsk_rte_function_id_t;
在g_tsk_rte_config_ppu.c中添加:
tsk_rte_dpf_t tsk_rte_dpf_table[] = {
// ... 现有的函数 ...
custom_my_function
};
在PPU应用程序中添加:
tsk_rte_status_t custom_my_function(
__uncached tsk_rte_function_payload_t* payload
)
{
// 1. 转换payload
__uncached tsk_rte_payload_t* parent_payload =
(__uncached tsk_rte_payload_t*)payload;
custom_my_function_payload_t unpacked_payload =
parent_payload->custom_my_function_payload;
// 2. 获取缓冲区地址
__vccm float* input = (__vccm float*)(int)
tsk_rte_ppu_buffer_get_address(unpacked_payload.buffer_id_input);
__vccm float* output = (__vccm float*)(int)
tsk_rte_ppu_buffer_get_address(unpacked_payload.buffer_id_output);
// 3. 执行您的计算
// ... 您的代码 ...
return TSK_RTE_STATUS_OK;
}
// 准备payload
custom_my_function_payload_t my_payload = {
.function_id = CUSTOM_MY_FUNCTION_FUNCTION_ID,
.buffer_id_input = buf_in,
.buffer_id_output = buf_out,
.param1 = 100,
.param2 = 3.14f
};
// 赋值给联合体
tsk_rte_payload_t payload;
payload.custom_my_function_payload = my_payload;
// 添加操作
tsk_rte_op_custom_dpf(job_id, (tsk_rte_function_payload_t*)&payload,
sizeof(custom_my_function_payload_t));
当前demo只处理32个复数,如果需要处理更大的数据量,可以考虑:
修改user_pools_config.h中的数组大小:
__aligned__(TSK_RTE_PPU_BUFFER_ALIGNMENT)
static __vccm _Complex float fc_a[1024]; // 从512增加到1024
__aligned__(TSK_RTE_PPU_BUFFER_ALIGNMENT)
static __vccm _Complex float fc_c[1024];
对于超大数据量,可以考虑分块处理:
时间线:
TriCore: 准备块1 → 准备块2 → 准备块3 → ...
PPU: 处理块1 → 处理块2 → 处理块3 → ...
这样可以最大化利用两个核的并行性。
首先需要确定:
定义清晰的接口:
在真实项目中,需要考虑:
基于对这个demo项目的分析,让我总结一些多核开发的最佳实践:
在学习和使用PPU的过程中,您可能会遇到以下问题:
Q: PPU和TriCore是什么关系?
A: PPU是TriCore的协处理器,两者通过共享内存和RTE库进行通信。TriCore负责控制,PPU负责计算。
Q: 必须使用RTE库吗?
A: 理论上可以直接操作硬件,但RTE库大大简化了开发,建议使用。
Q: 这个demo可以在没有硬件的情况下运行吗?
A: 不行,这个demo需要真实的TC49x硬件。不过可以在仿真器中运行。
Q: 如何确定我的函数是否适合放到PPU?
A: 考虑以下因素:
Q: 同步模式和异步模式如何选择?
A:
Q: 如何增加内存池的大小?
A: 修改user_pools_config.h中的数组大小,以及相关的配置文件。
Q: 为什么我的PPU程序没有预期的快?
A: 可能的原因:
Q: 如何测量PPU的执行时间?
A: 可以使用硬件定时器,在TriCore侧测量从提交任务到获取结果的时间。
Q: VCCM大小有限怎么办?
A:
Q: 为什么我的PPU断点不触发?
A: 检查:
tsk_rte_init()之后)Q: 如何查看共享内存的数据?
A: 使用调试器的内存查看功能,输入VCCM区域的地址。
Q: 双核调试时如何协调两个核?
A:
本PPU demo项目展示了如何使用TASKING PPU Math库和RTE库在TriCore处理器上执行复数向量的IFFT操作。通过将计算密集型任务卸载到PPU,可显著提高系统性能,同时释放TriCore资源用于其他任务。
--new-task选项将PPU程序嵌入到TriCore的ELF文件中libppu_math.a、libppu_rte.alibppu_rte.a--new-task实现双核程序的无缝整合该示例提供了同步和异步两种调用模式,适合不同的应用场景:
项目结构清晰,代码实现规范,是学习PPU编程和信号处理的良好示例。
本教程将指导您如何使用iSystem winIDEA调试器来调试这个PPU demo项目。
ppu_math_ifft_cf32_tcmain/Debug/ppu_math_ifft_cf32_tcmain.elf(主要调试文件)ppu_math_ifft_cf32_app/Debug/ppu_math_ifft_cf32_app.out(PPU代码)File → New Workspaced: ianpb\ppu_demo_inf\ppu_demo_inf.xjrfHardware → Select HardwareOKCPU → SelectInfineonAURIX 或 TC49xOKFile → Download Filed: ianpb\ppu_demo_inf\ppu_math_ifft_cf32_tcmain\Debug\ppu_math_ifft_cf32_tcmain.elfOpenwinIDEA会自动从ELF文件中加载调试符号,包括:
--new-task嵌入)Download 按钮(或按 F8)main()函数入口处在TriCore代码中设置断点:
ppu_math_ifft_cf32_tcmain.csynchronous_example()函数tsk_rte_queue_add_job() 调用处在PPU代码中设置断点:
ppu_math_ifft_cf32_app.ccustom_tsk_ppu_math_ifft_cf32()函数tsk_ifft_cf32()调用处常用调试命令:
F5:运行(Go)F10:单步跳过(Step Over)F11:单步进入(Step Into)Shift+F11:单步跳出(Step Out)Ctrl+F2:停止(Stop)F9:运行到光标处(Run to Cursor)winIDEA支持多核调试,您可以:
查看输入数据(TriCore侧):
View → Memory&a_c(输入数组地址)Float Complex 或 Hex查看输出结果:
&c_c(输出数组地址)&expected_result进行对比查看VCCM共享内存(PPU侧):
View → Watcha_c[]:输入数据c_c[]:输出结果expected_result[]:预期结果failed_jobs_sync:同步失败计数failed_jobs_async:异步失败计数a_vccm:PPU输入指针c_vccm:PPU输出指针length:数据长度让我们调试一个完整的同步模式IFFT操作:
步骤1:准备调试
synchronous_example()函数开始处设置断点custom_tsk_ppu_math_ifft_cf32()函数开始处设置断点步骤2:TriCore准备数据
synchronous_example()断点处停止a_c数组的值,确认输入数据正确步骤3:提交任务
tsk_rte_queue_add_job()调用后步骤4:PPU接收任务
tsk_rte_ppu_core_process()调用的unpacked_payload参数是否正确步骤5:执行IFFT计算
tsk_ifft_cf32()调用c_vccm指向的内存步骤6:验证结果
c_c和expected_result的值failed_jobs_sync是否为0--new-task)tsk_rte_init()之后)main()函数设置断点-g选项)winIDEA提供性能分析工具:
Profiler → Start Profiling如果您的调试器支持跟踪:
您可以使用winIDEA的脚本功能自动化调试:
File → Save Workspace通过以上步骤,您应该能够使用winIDEA成功调试这个PPU demo项目,深入理解TriCore和PPU之间的交互过程!
TASKING SmartCode 是英飞凌(Infineon)官方推荐的AURIX系列芯片开发工具链,专为多核实时控制系统设计。它提供了完整的开发环境,包括编译器、调试器、分析工具和丰富的标准库。
--new-task等特色选项实现多核程序的无缝整合--tradeoff参数,可根据需要调整优化侧重点在本PPU demo项目中,SmartCode展现了以下优势:
--new-task选项:将PPU程序嵌入到TriCore ELF文件中g_tsk_rte_config_ppu.c)相比其他开发工具,TASKING SmartCode具有以下优势:
TASKING SmartCode特别适合以下场景:
TASKING SmartCode是一款专业、强大的嵌入式开发工具链,特别适合英飞凌AURIX系列多核芯片的开发。通过本PPU demo项目,我们可以看到它如何通过RTE库、多工具链支持和特色链接技术,大幅简化了多核开发的复杂性,提高了开发效率和系统性能。
对于需要开发高性能实时控制系统的工程师来说,TASKING SmartCode是一个值得投资的工具,可以显著缩短开发周期,提高代码质量和系统可靠性。
欢迎大家一起交流学习:
support@softor.com.cn
tianpengbo@softor.com.cn
作者:tianpengbo / 田朋博。大家如果在项目中遇到相关技术问题,欢迎联系我交流。
support@softor.com.cn
tianpengbo@softor.com.cn
作者:tianpengbo / 田朋博。大家如果在项目中遇到相关技术问题,欢迎联系我交流。
support@softor.com.cn
tianpengbo@softor.com.cn