I have recently been trying to understand the Phaser implementation, and I have noticed a few elements of the interpolation filter chain implementation which do not seem consistent with DSP theory.

This primarily concerns the MAC_HBF_Upsampler module, which should perform two things (1) upsampling (2) Filtering of the up-sampled output. To start with we review the standard FIR filter with 4N-1 taps:

We know for a half band filter every odd indexed tap except the centre is zero. Hence we can remove them as follows:

Now, as is typical, the MAC_HBF_Upsampler implementation used a folded FIR filter to exploit the symmetry of the FIR filter coefficients, leaving our filter as follows:

This should be how the HBF of this module is implemented, notably the middle filter coefficient still contributes to the output y(n). What we see however from reading the Migen is that the filter coefficients are built up from the input as follows:

coef = []
for i, c in enumerate(coeff[: (len(coeff) + 1) // 2 : 2]):
    coef.append(Signal((width_coef + 1, True), reset_less=True, reset=c))

With this implementation, if our filter coefficients were [1, 0, 2, 3, 2, 0, 1] (as is the format of a HBF), the above code would yield coef = [1, 2], which does not contain the centre coefficient. This is evidenced by looking at the traces for a simulation of the hbf0 component in the InterpolateChannel module of Phaser, where we see the following:

The taps used in the design are the first 10 non-zero taps, missing out the middle tap (which should have value 131072). So, my first questions is, is this an intentional design decision, or is this just an error in implementation? I currently could not implement the HBF behaviour using the MAC_SYM_FIR module, which indicates to me that there is an error here, as they are both fundamentally FIR filters.

My second issue is with the way the upsampling is implemented. Typically the upsampling procedure is performed before the HBF, and is usually implemented with zero stuffing. I understand there are switched architectures which avoid the need to us zero stuffing, however I don't think that is used here. What is used here is that to implement a 2x upsampling the we see the following code fragment:

If(
   # emit trivial sample at halfway computation + pipelen
   pos == (len(coef) // 2) + dsp_pipelen -  1, 
   self.output.data.eq(x[len(coef)]),
   self.output.stb.eq(1),
 )

By my reading the 'upsampled' output is just the HBF output with random samples from the input inserted. I do not understand how this implementation follows from any of the theory. So, not only is the HBF not being applied to an upsampled signal, the upsampled signal contains arbitrary samples which (by my understanding) should be zeros.

If someone could explain the rationale between these design decision i'd appreciate it as there is not much supporting evidence other than the filtering jupyter notebook found at https://github.com/quartiq/phaser/blob/master/filter.ipynb. Inside this we see the test setup is

steps = [
    # 1/10  samples per cycles
    Gain(5**4/2.**9),  # compensate ciccomp+cic+shift gain
    # 1/10
    ciccomp,
    # 1/10
    US(2), HBF(10, .4).quantize(1 << 18),
    # 1/5
    US(2), HBF(5, 1/4.).quantize(1 << 18),
    # 2/5
    US(5), CIC(5, 1, 5),
    # 2/1
]

We can see this implements the zero stuffing upsampling prior to the HBF as expected, but then as discussed this behaviour is not reflected in the Migen implementation.

Thank you!

It's just the polyphase implementation.
The resource where your pasted graphics come from might have some info on that as well. Or any DSP book.

@rjo That has been a good lead for some reading, thank you. After reviewing the theory I still have an issue with the implementation, which I'm happy for you to correct me on. For 2x upsampling we need to build a filter which does the equivalent to our input signal being 0-stuffed every other cycle. That is, in the 'first' cycle we have

with our input signal [x(n), x(n-1), x(n-2) ... ] in black and the half band filter coefficients [h(0, h(1), .. h(6)] in red. we see that the output for this cycle is given by:

y(n) = x(n)h(0) + x(n-1)h(2) + x(n-2)h(4) + x(n-3)h(6)

Which, as is seen in the migen misses out the centre filter coefficient h(3)! However, in the next cycle we would shift in a new 0 sample (if we were doing a traditional zero-stuffing), and so we'd see the following:

We now see that the output for this cycle is given by:

y(n+1) = h(3)x(n-1)

Which now utilises the central tap. Hence, if we were to do a polyphase implementation we'd need to retain that central tap coefficient and then every other cycle switch between the two equations above. That does not seem to be implemented here, only the first equation.

As I said, i'm open to being corrected, but this is my understanding from reading a textbook.

Thanks.

EDIT: I think I understand now. The centre tap (in the above examples h(3)) has weight 131072, which is 1 << 17. Given the output is right shifted by 17, (the coefficient width), there is no point performing the coefficient multiplication, we just output the previous sample, which means to say y(n+1) = x(n-1), which is what is seen in the Migen. I believe I understand things now.