Skip to main content
欢迎来到PAWPAW技术文档网站了解更多信息

第3C部分:使用XMath的BFP FIR滤波器API

第3C部分中,我们最终使用lib_xcore_math的块浮点(BFP)API。

在这里,我们不希望相对于第3B部分的实现有明显的性能提升。归根结底,两个阶段最终都使用相同的函数(vect_s32_dot())来完成计算滤波器输出的大部分工作。相反,在这个阶段,我们将看到如何使用lib_xcore_math的BFP API来简化我们的代码,因为它可以为我们处理大部分的繁琐工作。

尽管如此,这并不是使用BFP算术的理想应用,特别是由于我们的应用要求固定点输出样本。

来自lib_xcore_math

本页面引用了lib_xcore_math中以下操作:

实现

src/part3C/part3C.c
// 计算 int32 向量的头空间。
static inline
headroom_t calc_headroom(
bfp_s32_t* vec)
{
return bfp_s32_headroom(vec);
}

在这个阶段,calc_headroom()只是调用了提供的BFP向量上的bfp_s32_headroom()操作。bfp_s32_headroom()会同时更新bfp_s32_t对象的hr字段,并返回该头空间。在这种情况下,calc_headroom()也返回该头空间,尽管在第3C部分中,返回的值不需要也不会被使用。

src/part3C/part3C.c
/**
* 这是实际应用FIR滤波器的硬件线程的线程入口点。
*
* `c_audio` 是用于与tile[0]交换PCM音频数据的通道。
*/
void filter_task(
chanend_t c_audio)
{

// 初始化表示滤波器系数的BFP向量
const exponent_t coef_exp = -30;
bfp_s32_init(&bfp_filter_coef, (int32_t*) &filter_coef[0],
coef_exp, TAP_COUNT, 1);

// 表示样本历史记录的BFP向量
int32_t sample_history_buff[HISTORY_SIZE] = {0};
bfp_s32_t sample_history;
bfp_s32_init(&sample_history, &sample_history_buff[0], -200,
HISTORY_SIZE, 0);

// 表示输出帧的BFP向量
int32_t frame_output_buff[FRAME_SIZE] = {0};
bfp_s32_t frame_output;
bfp_s32_init(&frame_output, &frame_output_buff[0], 0,
FRAME_SIZE, 0);

// 无限循环
while(1) {

// 读取新的帧
rx_and_merge_frame(&sample_history,
c_audio);

// 计算输出帧
filter_frame(&frame_output,
&sample_history);

// 为向量前面的新样本腾出空间。
memmove(&sample_history.data[FRAME_SIZE],
&sample_history.data[0],
TAP_COUNT * sizeof(int32_t));

// 发送处理后的帧
tx_frame(c_audio,
&frame_output);
}
}

第3C部分中,filter_task()与之前的两个阶段非常相似。这次,在进入主线程循环之前,我们需要在开始时进行一些调用来初始化一些BFP向量。值得注意的是,在这个阶段,我们必须初始化bfp_filter_coef,即表示滤波器系数的BFP向量,在之前的阶段中我们不需要这样做。

bfp_s32_init()用于初始化每个BFP向量,将bfp_s32_t对象与指数、长度以及最重要的元素缓冲区关联起来。bfp_s32_init()的最后一个参数是一个布尔值,指示在初始化过程中是否应计算向量的头空间。

计算头空间涉及迭代数组的数据,当不需要时应避免这样做。特别是,如果元素缓冲区尚未填充初始值,通常没有必要计算头空间。

src/part3C/part3C.c
// 计算整个输出帧
void filter_frame(
bfp_s32_t* frame_out,
const bfp_s32_t* sample_history)
{
// 初始化一个新的BFP向量,它是对sample_history[]向量的TAP_COUNT元素窗口的“视图”。
// history_view[]窗口将在每个输出样本中“滑动”sample_history[]。这不是您通常需要做的事情。
bfp_s32_t history_view;
bfp_s32_init(&history_view, &sample_history->data[FRAME_SIZE],
sample_history->exp, TAP_COUNT, 0);
history_view.hr = sample_history->hr; // 可能不是完全正确的,但是是安全的

// 计算FRAME_SIZE个输出样本。
for(int s = 0; s < FRAME_SIZE; s++){
timer_start(TIMING_SAMPLE);
// 将窗口向下滑动一个索引,以获取更新的样本
history_view.data = history_view.data - 1;
// 获取下一个输出样本
float_s64_t samp = filter_sample(&history_view);

// 由于每次调用filter_sample()时的指数和头空间都相同,输出指数也将相同(在计算点积之前计算)。因此,我们将使用第一个结果指示使用的指数...再加上8,就像在前一个阶段一样,出于同样的原因(int40 -> int32)
if(!s)
frame_out->exp = samp.exp + 8;

frame_out->data[s] = float_s64_to_fixed(samp,
frame_out->exp);
timer_stop(TIMING_SAMPLE);
}
}

第3C部分中的filter_frame()有些棘手。实际上,lib_xcore_math的BFP API没有提供适用于这种情况的卷积操作。确实有一对32位BFP卷积函数bfp_s32_convolve_valid()bfp_s32_convolve_same(),但这些函数针对并且仅支持1357个元素的小卷积核进行了优化。

实际上,对于使用lib_xcore_math实现FIR滤波器来说,BFP可能不是正确的方法(我们将在第4B部分第4C部分中看到更好的方法)。

但是为了保持本教程的精神,在这个阶段中,我们尽量使用BFP API来实现滤波器。

filter_frame()首先初始化一个名为history_view的新的BFP向量。然而,history_view没有自己的数据缓冲区,而是指向sample_history的数据缓冲区中的某个地址。此外,sample_historylengthHISTORY_SIZE(1280),而history_view的长度为TAP_COUNT(1024)。history_view使用与sample_history相同的指数和头空间。综上所述,history_view类似于对sample_history向量的某部分的一种窗口(因此称为历史_"视图"_)。

对于filter_sample()计算的每个输出样本,history_viewdata指针向后滑动1个元素,靠近sample_history向量的起始位置。回想一下,在之前的所有阶段中,我们实际上也做了类似的事情 - 在之前的阶段中,每次调用filter_sample()都会将指向历史向量的不同位置的指针传递给它。这基本上是在做同样的事情,以一种纠正第3C部分filter_sample()需要与bfp_filter_coef_具有相同长度的BFP向量_的方式。

第3C部分filter_frame()的另一个棘手之处在于确定输出指数。在这里,我们_本来可以_调用vect_s32_dot_prepare()来确定输出指数。为了坚持使用BFP函数,它采取了不同的方法,利用了我们对情况的了解。

特别是,我们知道在内部bfp_s32_dot()实际上是调用vect_s32_dot_prepare(),并且因为每次调用时history_viewbfp_filter_coef的指数和头空间都将相同(对于给定的帧),我们知道输出指数每次都将相同。因此,我们没有调用vect_s32_dot_prepare(),而是基于filter_sample()返回的第一个指数来确定输出指数。

然而,filter_sample()返回一个float_s64_t,由于vect_s32_dot()的实现方式,它的64位尾数不会超过40位。我们需要确保结果适应32位,因此我们在输出指数上加上8,并使用float_s64_to_fixed()将样本移位后放入frame_out中。

信息

明确一点,如果您在实际应用中发现自己需要这样做,最好还是回到更低级别的Vector API。这并不意味着要在应用程序或应用程序组件中摒弃所有的bfp_s32_t,而是在实现您的操作的函数中,您可能希望拆解bfp_s32_t并直接使用Vector API对其字段进行操作。

另外,如果您正试图使用BFP或Vector API来实现时域FIR或IIR滤波器,您可能需要查看数字滤波器API

src/part3C/part3C.c
/**
* 将滤波器应用于生成单个输出样本。
*
* `sample_history[]`是表示计算当前输出所需的`TAP_COUNT`历史样本的BFP向量。
*/
float_s64_t filter_sample(
const bfp_s32_t* sample_history)
{
// 计算点积
return bfp_s32_dot(sample_history, &bfp_filter_coef);
}

第3C部分中的filter_sample()非常简单。它只是调用bfp_s32_dot()来计算提供的sample_history BFP向量和滤波器系数BFP向量的内积。

在这个阶段,滤波器系数由类型为bfp_s32_tbfp_filter_coef表示。该向量使用与之前两个阶段相同的基础系数数组,但它们被包装在一个BFP向量中,该向量跟踪用户的长度、指数和头空间。在使用之前,需要初始化bfp_filter_coef,在filter_task()中进入主线程循环之前进行初始化。

src/part3C/part3C.c
// 发送一个新的音频数据帧
static inline
void tx_frame(
const chanend_t c_audio,
bfp_s32_t* frame_out)
{
const exponent_t output_exp = -31;

// 输出通道期望的是具有固定指数output_exp的PCM样本,因此在发送之前,我们需要将样本转换为正确的指数。
bfp_s32_use_exponent(frame_out, output_exp);

timer_stop(TIMING_FRAME);

// 然后发送样本
for(int k = 0; k < FRAME_SIZE; k++)
chan_out_word(c_audio, frame_out->data[k]);

}

第3C部分中的tx_frame()第3B部分中的函数类似。它的工作是确保输出样本使用指数-31,然后将它们发送到wav_io线程。

第3B部分中,这是通过显式计算应用于每个样本值的移位并在将输出样本放入通道之前应用这些移位来实现的。然而,在第3C部分中,这是通过调用bfp_s32_use_exponent()来实现的。

bfp_s32_use_exponent()将BFP向量强制为使用特定的指数,根据需要移动元素以实现这一目标。这是从块浮点域(本阶段的滤波器)转换为固定点域(wav_io线程)的简单方法。

src/part3C/part3C.c
static inline 
void rx_frame(
bfp_s32_t* frame_in,
const chanend_t c_audio)
{
// 我们事先知道输入样本将具有固定指数input_exp,并且没有理由改变它,因此我们将使用它。
frame_in->exp = -31;

for(int k = 0; k < FRAME_SIZE; k++)
frame_in->data[k] = chan_in_word(c_audio);

timer_start(TIMING_FRAME);

// 确保头空间正确
calc_headroom(frame_in);
}

第3C部分中的rx_frame()与之前两个阶段非常相似。唯一的区别是现在的尾数、指数和头空间被封装在bfp_s32_t对象中。

src/part3C/part3C.c
// 接收一个新的音频数据帧并将其合并到样本历史记录中
static inline
void rx_and_merge_frame(
bfp_s32_t* sample_history,
const chanend_t c_audio)
{
// 用于存放新帧的BFP向量。
int32_t frame_in_buff[FRAME_SIZE];
bfp_s32_t frame_in;
bfp_s32_init(&frame_in, frame_in_buff, 0, FRAME_SIZE, 0);

// 接收一个新的输入帧
rx_frame(&frame_in, c_audio);

// 如果需要,重新缩放BFP向量以便可以合并
const exponent_t min_frame_in_exp = frame_in.exp - frame_in.hr;
const exponent_t min_history_exp = sample_history->exp - sample_history->hr;
const exponent_t new_exp = MAX(min_frame_in_exp, min_history_exp);

bfp_s32_use_exponent(sample_history, new_exp);
bfp_s32_use_exponent(&frame_in, new_exp);

// 现在我们可以合并新帧(反向顺序)
for(int k = 0; k < FRAME_SIZE; k++)
sample_history->data[FRAME_SIZE-1-k] = frame_in.data[k];

// 确保头空间正确
calc_headroom(sample_history);
}

第3C部分中的rx_and_merge_frame()比前两个阶段简单一些。首先,初始化一个临时的BFP向量frame_in,并调用rx_frame()将新帧数据填充到其中。之后,仍然需要协调frame_insample_history的指数,但这次一旦选择了指数,我们就使用bfp_s32_use_exponent()在两个向量上确保它们的指数相等。之后,将新数据复制到样本历史记录中。

结果

计时

计时类型测量时间
每个滤波器系数16.13 ns
每个输出样本16515.55 ns
每个帧4312690.50 ns

输出波形

第3C部分输出