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

第1A部分:双精度浮点FIR滤波器

第1A部分是数字FIR滤波器的第一个实现。后续阶段中的大部分代码都与此处的代码相似。

第1A部分中,FIR滤波器的实现非常简单但效率不高。输入样本、滤波器系数和输出样本都是double值。所有的滤波算术都使用双精度进行,这最大限度地提高了算术精度,但性能代价很高,因为在硬件中很难加速这个工作。

此外,此阶段的滤波器逻辑是用普通的C语言实现的。这是一个有用的起点,因为许多用户使用普通的C语言实现算法,以便实现可移植性。

在低成本微控制器上使用double算术来实现滤波器是一个不现实的坏主意。因此,它不是评估后续阶段实现性能的理想参考(第1B部分提供了更好的参考)。然而,它仍然有用,可以展示应用程序因此而遭受的巨大性能损失。

实现

软件组织页面讨论了固件的总体结构。公共代码简要回顾了所有阶段共有的一些元素。本页面将放大并以第1A部分中的filter_task线程的视角来看待问题,该线程实际执行滤波操作。

第1A部分的特定代码位于part1A.c中。

第1A部分使用了filter_coef_double.c中的滤波器系数。

/src/part1A/part1A.c
/**
* 这是实际应用FIR滤波器的硬件线程的线程入口点。
*
* c_audio 是用于与tile[0]交换PCM音频数据的通道。
*/
void filter_task(
chanend_t c_audio)
{
// 以逆时间顺序存储的接收输入样本的历史记录
double sample_history[HISTORY_SIZE] = {0};

// 用于保存输出样本的缓冲区
double frame_output[FRAME_SIZE] = {0};

// 无限循环
while(1) {

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

// 计算输出帧
for(int s = 0; s < FRAME_SIZE; s++){
timer_start(TIMING_SAMPLE);
frame_output[s] = filter_sample(&sample_history[FRAME_SIZE-s-1]);
timer_stop(TIMING_SAMPLE);
}

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

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

这是滤波线程的入口点。初始化后,此函数将循环执行,接收一帧输入音频数据,处理该数据以获得输出音频,并将其传输回tile[0]以写入输出wav文件。

filter_task()以通道端资源作为参数。这是它与wav_io_task进行通信的方式。

sample_history[]是存储先前接收到的输入样本的缓冲区。这些样本以逆时间顺序存储在缓冲区中,最近接收到的样本位于sample_history[0]处。

frame_output[]是用于存放计算得到的输出样本的缓冲区,在发送到wav_io_task之前。

filter_task线程循环直到应用程序通过tile[0]终止。在主循环的每次迭代中,第一步是接收FRAME_SIZE个新的输入样本,并将它们放入样本历史缓冲区(以逆序)(参见下面的rx_frame())。

在接收(和转换)新样本的帧之后,filter_task计算FRAME_SIZE个新的输出样本,每个样本是调用filter_sample()的结果。

在将计算得到的输出样本帧发送回wav_io_task之前,最后一件事是将sample_history[]缓冲区的内容移动,以为下一帧输入音频腾出空间。这样可以保持样本的完全排序。

最后,输出帧被发送到tile[0],并且循环重复执行。

src/part1A/part1A.c
// 将滤波器应用于生成单个输出样本
double filter_sample(
const double sample_history[TAP_COUNT])
{
// 计算sample_history[]和filter_coef[]的内积
double acc = 0.0;
for(int k = 0; k < TAP_COUNT; k++)
acc += sample_history[k] * filter_coef[k];
return acc;
}

filter_sample()是计算单个输出样本值的函数。这是实际进行大部分工作的地方。

每次调用filter_sample()都会生成一个输出样本,因此对filter_sample()的调用对应于一个特定的时间步长,这需要最近的TAP_COUNT个输入样本值,这些样本值是指向当前帧内当前样本的主样本历史缓冲区的。然后,需要根据当前样本在当前帧内的偏移量对输入参数sample_history[]进行调整。

一个双精度累加器被初始化为零,并且在一个简单的for循环中计算样本历史和滤波器系数向量的内积。

滤波器系数向量filter_coef[]由一个TAP_COUNT元素的double数组表示,其中每个元素的值设置为11024=0.0009765625\frac{1}{1024} = 0.0009765625

const double filter_coef[TAP_COUNT] = { ... };
src/part1A/part1A.c
// 接收一帧新音频数据
static inline
void rx_frame(
double buff[],
const chanend_t c_audio)
{
// 输入样本关联的指数
const exponent_t input_exp = -31;

for(int k = 0; k < FRAME_SIZE; k++){
// 从通道中读取PCM样本
const int32_t sample_in = (int32_t) chan_in_word(c_audio);
// 将PCM样本转换为浮点数
const double samp_f = ldexp(sample_in, input_exp);
// 将样本放置在历史缓冲区的开头,以相反的顺序(以匹配滤波器系数的顺序)。
buff[FRAME_SIZE-k-1] = samp_f;
}

timer_start(TIMING_FRAME);
}

rx_frame()接受一个音频数据帧,并通过通道接收。请注意,为了展示完全使用浮点数的实现,理想情况下,我们应该直接将样本数据作为double值接收,但这会增加复杂性,而收益很小,因此我们只是在接收时将接收到的PCM样本转换为double值。

rx_frame()将(double)样本值以逆时间顺序放入给定的缓冲区中,这与filter_sample()所期望的顺序相匹配。

src/part1A/part1A.c
// 发送一帧新的音频数据
static inline
void tx_frame(
const chanend_t c_audio,
const double buff[])
{
// 输出样本关联的指数
const exponent_t output_exp = -31;

timer_stop(TIMING_FRAME);

// 在每帧结束时发送FRAME_SIZE个新的输出样本
for(int k = 0; k < FRAME_SIZE; k++){
// 从帧输出缓冲区获取双精度样本(按正序)
const double samp_f = buff[k];
// 使用输出指数将双精度样本转换回PCM
const q1_31 sample_out = round(ldexp(samp_f, -output_exp));
// 将PCM样本放入输出通道
chan_out_word(c_audio, sample_out);
}
}

tx_frame()rx_frame()相反。它接收一帧的double值的输出音频样本,并将每个样本转换为适当的32位PCM值,然后通过通道发送给wav_io线程。请注意,与rx_frame()不同,此函数不会重新排序发送的样本。

结果

运行s时

用时类型测量时间
每个滤波器系数2469.46 ns
每个输出样本2528.72525 us
每帧647.43289600 ms

输出波形

第1A部分的输出