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

第4B部分:XMath数字滤波器API

第4B部分与之前的所有阶段采用了非常不同的方法。第4B部分使用了lib_xcore_math提供的数字滤波器API。在本示例中,FIR滤波器将由filter_fir_s32_t对象表示。lib_xcore_math中的滤波器API是高度优化的16位和32位FIR滤波器以及32位双二阶滤波器的实现。

lib_xcore_math中的滤波器API本质上是一个定点API,但实际上与输入和输出样本相关联的指数并不固定,只有它们的关系是固定的。具体而言,固定的是输入和输出指数之间的_差异_。

来自lib_xcore_math

本页面引用了来自lib_xcore_math的以下类型和操作:

实现

第4B部分的实现分为3个函数:rx_frame()tx_frame()filter_task()

src/part4B/part4B.c
// 接收一帧新的音频数据
static inline
void rx_frame(
int32_t buff[],
const chanend_t c_audio)
{
for(int k = 0; k < FRAME_SIZE; k++)
buff[k] = (q1_31) chan_in_word(c_audio);

timer_start(TIMING_FRAME);
}

这与第2部分中的tx_frame()几乎完全相同。唯一的区别是新接收到的输入样本按顺序放入buff[],而不是倒序。这是因为表示滤波器的filter_s32_t对象在内部处理自己的状态 - 我们不需要考虑样本顺序。

src/part4B/part4B.c
// 发送一帧新的音频数据
static inline
void tx_frame(
const chanend_t c_audio,
const int32_t buff[])
{
timer_stop(TIMING_FRAME);

for(int k = 0; k < FRAME_SIZE; k++)
chan_out_word(c_audio, buff[k]);
}

这与第2部分中的tx_frame()完全相同。

src/part4B/part4B.c
/**
* 这是应用FIR滤波器的硬件线程的线程入口点。
*
* `c_audio`是用于与tile[0]交换PCM音频数据的通道。
*/
void filter_task(
chanend_t c_audio)
{
// 输入样本相关联的指数
const exponent_t input_exp = -31;
// 输出样本相关联的指数
const exponent_t output_exp = -31;
// 滤波器系数相关联的指数
const exponent_t coef_exp = -30;
// VPU进行32位乘法时应用的右移位数
const right_shift_t vpu_shr = 30;
// 累加器相关联的指数
const exponent_t acc_exp = input_exp + coef_exp + vpu_shr;

// `filter_fir_s32_t`对象所需的算术右移位数。这是将滤波器的累加器右移以获得输出的位数。
const right_shift_t acc_shr = output_exp - acc_exp;

// 用于保存滤波器状态的缓冲区。我们不需要自己管理滤波器状态,但我们必须提供一个缓冲区给它。初始化滤波器不会将滤波器状态清零,因此我们必须在这里进行清零操作。
int32_t filter_state[TAP_COUNT] = {0};

// 滤波器对象本身
filter_fir_s32_t fir_filter;

// 此缓冲区用于存储输入/输出样本。
int32_t sample_buffer[FRAME_SIZE] = {0};

// 在提供样本之前,需要初始化滤波器。
filter_fir_s32_init(&fir_filter,
&filter_state[0],
TAP_COUNT,
&filter_coef[0],
acc_shr);

// 无限循环
while(1) {

// 读取新的一帧
rx_frame(&sample_buffer[0],
c_audio);

// 计算FRAME_SIZE个输出样本。
for(int s = 0; s < FRAME_SIZE; s++){
timer_start(TIMING_SAMPLE);
// 我们可以覆盖sample_buffer[]中的数据,因为滤波器对象会保持自己的历史记录。
// 因此,一旦我们提供了一个样本,我们可以覆盖该内存,从而可以使用相同的数组进行输入和输出。
sample_buffer[s] = filter_fir_s32(&fir_filter,
sample_buffer[s]);
timer_stop(TIMING_SAMPLE);
}

// 发送处理后的帧
tx_frame(c_audio,
&sample_buffer[0]);
}
}

filter_task()的循环之前,有几个需要注意的地方。

首先,我们的sample_buffer[]用于存储输入和输出样本,只有FRAME_SIZE个元素,而不是HISTORY_SIZE。这是因为滤波器本身存储样本历史记录。

接下来,我们现在有一个名为fir_filterfilter_fir_s32_t对象。filter_fir_s32_tlib_xcore_math中定义,表示我们的32位数字FIR滤波器。在使用之前必须对其进行初始化。

滤波器的初始化通过调用filter_fir_s32_init()来完成:

C_API
void filter_fir_s32_init(
filter_fir_s32_t* filter,
int32_t* sample_buffer,
const unsigned tap_count,
const int32_t* coefficients,
const right_shift_t shift);

这需要滤波器对象本身、系数数组的指针以及滤波器可以用于维护滤波器状态的缓冲区。我们提供的滤波器系数是来自filter_coef_q2_30.cQ2.30系数,并且我们声明sample_buffer[]用作状态缓冲区。

参数tap_count是滤波器系数的数量,最后一个参数shift是应用于累加器的算术右移位数,以生成32位输出样本。

滤波器的输出位移值应该使用多少?在这里,我们遵循与第2部分相同的逻辑。

input_exp=output_exp=31acc_exp=input_exp+coef_exp+vpu_shroutput_exp=acc_exp+acc_shracc_shr=output_expacc_exp=31(input_exp+coef_exp+vpu_shr)=31(31+30+30)=0\begin{aligned} \mathtt{input\_exp} &= \mathtt{output\_exp} = -31 \\ \mathtt{acc\_exp} &= \mathtt{input\_exp} + \mathtt{coef\_exp} + \mathtt{vpu\_shr} \\ \mathtt{output\_exp} &= \mathtt{acc\_exp} + \mathtt{acc\_shr} \\ \\ \mathtt{acc\_shr} &= \mathtt{output\_exp} - \mathtt{acc\_exp} \\ &= -31 - (\mathtt{input\_exp} + \mathtt{coef\_exp} + \mathtt{vpu\_shr}) \\ &= -31 - (-31 + -30 + 30) \\ &= 0 \end{aligned}

在这里,由于input_exp = output_expcoef_exp = -vpu_shr,指数和位移都相互抵消了。

filter_task()的循环内部与第2A部分非常相似,但有两个关键的区别。首先,在循环结束时,没有调用memmove()来移动样本历史。滤波器不仅为我们处理滤波器状态,而且在内部使用循环缓冲区来存储样本,因此永远不需要移位。

信息

当然,我们在所有以前的阶段中都可以使用循环缓冲区来存储样本历史。这将避免在每次循环迭代结束时移动TAP_COUNT个样本的开销,但它也明显复杂化了实际的滤波器实现,这就是我们避免使用它的原因。

另一个区别是,在for循环中调用了filter_fir_s32(),而不是filter_sample()

C_API
int32_t filter_fir_s32(
filter_fir_s32_t* filter,
const int32_t new_sample);

filter_fir_s32()是实际执行滤波操作的函数。它接受指向滤波器对象和最新输入样本值的指针,并返回下一个输出样本。我们按顺序使用新的输入样本调用它,并获得输出样本。

结果

计时

计时类型测量时间
每个滤波器系数4.73 ns
每个输出样本4845.99 ns
每帧1294021.62 ns

输出波形

第4B部分输出