EECS151 / riscv-cpu / scripts / synth / NCO.py
NCO.py
Raw
#!/usr/bin/env python3
# Requires: pip install spfpm

from FixedPoint import FXfamily, FXnum
from enum import Enum, auto
import numpy as np
import sys
from typing import Tuple
import argparse

NCOType = Tuple[FXnum, FXnum, FXnum, FXnum]
output_type = FXfamily(n_bits=16, n_intbits=4)

class NCO:
    def __init__(self, fsamp: float, interpolate: bool) -> None:
        self.fsamp = fsamp
        self.interpolate = interpolate
        self.output_type = output_type
        self.phase_acc = 0 # type: int
        self.N = 24
        self.M = 8
        self.lut_entries = 2**self.M

        self.sine_lut_float = [np.sin(i * 2*np.pi / self.lut_entries) for i in range(self.lut_entries)]
        self.sine_lut_fixed = [FXnum(x, family=self.output_type) for x in self.sine_lut_float]

        self.square_lut_fixed = [FXnum(1, family=self.output_type) for x in range(int(self.lut_entries/2))] + \
            [FXnum(-1, family=self.output_type) for x in range(int(self.lut_entries/2))]

        self.triangle_lut_float = [np.max(1 - np.abs(x)) for x in np.linspace(-1, 1, self.lut_entries)]
        self.triangle_lut_float = [x*2 - 1 for x in self.triangle_lut_float] # scale to range from -1 to 1
        self.triangle_lut_fixed = [FXnum(x, family=self.output_type) for x in self.triangle_lut_float]

        self.sawtooth_lut_float = [x - np.floor(x) for x in np.linspace(0, 1-1e-16, self.lut_entries)]
        self.sawtooth_lut_float = [x*2 - 1 for x in self.sawtooth_lut_float] # scaling again
        self.sawtooth_lut_fixed = [FXnum(x, family=self.output_type) for x in self.sawtooth_lut_float]

    def next_sample(self, fcw: int) -> NCOType:
        lut_index = (self.phase_acc >> (self.N - self.M)) & int('1'*self.M, 2) # take MSB M bits of phase_acc
        samples = []
        for lut in [self.sine_lut_fixed, self.square_lut_fixed, self.triangle_lut_fixed, self.sawtooth_lut_fixed]:
            if self.interpolate is False:
                samples.append(lut[lut_index])
            else:
                samp1 = lut[lut_index]
                samp2 = lut[(lut_index + 1) % self.lut_entries]
                residual = self.phase_acc & int('1'*(self.N - self.M), 2) # take LSB (N-M) bits of phase_acc
                residual = FXnum(residual/(2**(self.N - self.M)), family=self.output_type) # Cast residual as fixed point
                diff = samp2 - samp1
                samples.append(samp1 + residual*diff)

        self.phase_acc = self.phase_acc + fcw
        self.phase_acc = self.phase_acc % 2**self.N # overflow on N bits
        return samples

    def next_sample_f(self, freq: float) -> Tuple[FXnum, FXnum, FXnum, FXnum]:
        return self.next_sample(self.phase_increment(freq))

    def phase_increment(self, freq: float) -> int:
        return int(round((freq / self.fsamp) * 2**self.N))

    def effective_frequency(self, phase_increment: int) -> float:
        return (phase_increment * self.fsamp) / (2**self.N)

    def reset(self) -> None:
        self.phase_acc = 0

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='NCO Model')
    parser.add_argument('--analysis', dest='analysis', action='store_true', help='compare ideal sampling with NCO samples')
    parser.add_argument('--golden', dest='golden', action='store_true', help='dump golden NCO samples for comparison with RTL')
    parser.add_argument('--sine-lut', dest='sine_lut', action='store_true', help='dump the sine LUT values')
    parser.add_argument('--square-lut', dest='square_lut', action='store_true', help='dump the square LUT values')
    parser.add_argument('--triangle-lut', dest='triangle_lut', action='store_true', help='dump the triangle LUT values')
    parser.add_argument('--sawtooth-lut', dest='sawtooth_lut', action='store_true', help='dump the sawtooth LUT values')
    parser.add_argument('--sine-plot', dest='sine_plot', action='store_true', help='plot the sine LUT values')
    parser.add_argument('--square-plot', dest='square_plot', action='store_true', help='plot the square LUT values')
    parser.add_argument('--triangle-plot', dest='triangle_plot', action='store_true', help='plot the triangle LUT values')
    parser.add_argument('--sawtooth-plot', dest='sawtooth_plot', action='store_true', help='plot the sawtooth LUT values')
    args = parser.parse_args()

    fsamp = 30e3
    nco = NCO(fsamp, interpolate = True)
    fsig = 880
    num_periods = 5
    num_samples = int(np.ceil(fsamp / fsig)) * num_periods
    nco_samples = [nco.next_sample_f(fsig) for n in range(num_samples)] # only take the sine sample

    if args.analysis:
        nco_sine_samples = [x[0] for x in nco_samples]
        nco_samples_float = [float(x) for x in nco_sine_samples]
        phase_increment = nco.phase_increment(fsig)
        effective_freq = nco.effective_frequency(phase_increment)
        ideal_samples = [np.sin(2*np.pi*effective_freq*n/fsamp) for n in range(num_samples)]

        ## Plot NCO vs ideal samples
        import matplotlib.pyplot as plt
        fig, ax = plt.subplots(2, 1)
        ax[0].plot(nco_samples_float, '*')
        ax[0].plot(ideal_samples, '*')
        ax[0].legend(['NCO Samples', 'Ideal Samples'])
        ax[0].set_xlabel('Sample Number (n)')
        ax[0].set_ylabel('Amplitude')
        ax[1].plot(np.abs(np.array(nco_samples_float) - np.array(ideal_samples)))
        ax[1].legend(['NCO Error'])
        ax[1].set_xlabel('Sample Number (n)')
        ax[1].set_ylabel('Absolute Error')
        plt.show()

    if args.golden:
        for val in nco_samples:
            print('{}'.format(val[0].toBinaryString().replace('.', ''))) # only consider the sine samples

    if args.sine_lut:
        sine_lut = [x.toBinaryString() for x in nco.sine_lut_fixed]
        for val in sine_lut:
            print('{}'.format(val.replace('.', '')))
    if args.sine_plot:
        import matplotlib.pyplot as plt
        sine_lut = [float(x) for x in nco.sine_lut_fixed]
        plt.plot(sine_lut, '.')
        plt.show()

    if args.square_lut:
        square_lut = [x.toBinaryString() for x in nco.square_lut_fixed]
        for val in square_lut:
            print('{}'.format(val.replace('.', '')))
    if args.square_plot:
        import matplotlib.pyplot as plt
        square_lut = [float(x) for x in nco.square_lut_fixed]
        plt.plot(square_lut, '.')
        plt.show()

    if args.triangle_lut:
        triangle_lut = [x.toBinaryString() for x in nco.triangle_lut_fixed]
        for val in triangle_lut:
            print('{}'.format(val.replace('.', '')))
    if args.triangle_plot:
        import matplotlib.pyplot as plt
        triangle_lut = [float(x) for x in nco.triangle_lut_fixed]
        plt.plot(triangle_lut, '.')
        plt.show()

    if args.sawtooth_lut:
        saw_lut = [x.toBinaryString() for x in nco.sawtooth_lut_fixed]
        for val in saw_lut:
            print('{}'.format(val.replace('.', '')))
    if args.sawtooth_plot:
        import matplotlib.pyplot as plt
        sawtooth_lut = [float(x) for x in nco.sawtooth_lut_fixed]
        plt.plot(sawtooth_lut, '.')
        plt.show()