

The aim of this project is to test if it is possible to output 16 bit audio using a single digital output from a pure digital Tiny Tapeout design. For this purpose, it implements a noise-shaping delta-sigma converter that feeds into a pulse width modulator to create the digital output signal.
The design can be driven with sample data (eg using the PIO functionality in the RP2040/RP2350 microcontroller on the demo board). There is also a built-in triangle wave generator for testing without needing to feed sample data.
Assume that we want to output an audio signal at 48 kHz from a design that is clocked at 50 MHz, using single digital output pin fed by a flip-flop. That gives us about 1040 cycles to output each sample. (This design can go up to 66.66 MHz, but the main target is to get a working output at 50 MHz). If we output each sample by controlling the average over a 1040 cycle period, we get a little more than 10 bits of resolution. This can for instance be done with pulse width modulation (PWM).
One way to look at it is that we are taking the original unquantized output signal and adding noise to it so that it becomes a quantized signal with a resolution that can actually be output. The target can be a one bit signal sampled at 50 MHz, or a signal with higher bit depth and lower frequency output using PWM for instance (we will aim for the latter). There is a least amount of noise we need to add to make the output signal quantized, but the noise doesn't have to be added equally across the frequency spectrum. This design uses a noise shaping sigma-delta modulator to shape the quantization noise to put most of it above the audible frequency range.
Let's use a 4th order filter (the highest order that seems to be useful with a clock frequency of 50 MHz in this setup). We will consider two kinds of error:
(see the figure at the top.)
At each sample, we will make a quantization error.
Assume (for the purpose of illustration) that we make a quantization error of 1 in the first sample.
The 4th order filter will add corrections to the following samples 4 samples, with values -4, 6, -4, 1.
Taken together, the sequence of disturbances caused by the first quantization error of 1 becomes
1, -4, 6, -4, 1
where the the first 1 comes from the quantization noise in the first sample, and the rest is corrections added by the filter.
This sequence is the impulse response of a filter (1 - z^-1)^4 with 4 zeros at z = 1 (zero frequency), and has very little low frequency content.
Adding the corrections will add more energy to the quantization noise (in this case, the variance will be 70x higher, 1^2+4^2+6^2+4^2+1^2 = 70), but the low frequency components of the quantization noise will be greatly reduced by the corrections.
The corrections can not be applied exactly because there will be quantization errors in the following samples as well, but these errors can be corrected in the same way.
The quantization error is bounded, and the bound doesn't grow due to the corrections, so the corrections will be bounded as well.
If the quantizer rounds the output to the nearest integer, the magnitude of the quantization error is <= 0.5, and the 4th order filter adds noise of up to +- 7.5 output steps to each sample.
The output of the delta-sigma modulator is fed to a pulse width modulator with programmable period. The PWM period sets the range of the output signal. We have to make sure that the period is long enough to fit the variation of both the quantization noise and the input signal. Setting the PWM period is a tradeoff between output resolution and range on one hand, and output frequency on the other:
In this case, a period of 47 cycles may be appropriate, to allow an output swing of 0-31 from the input signal combined with 0-14 (if we add 7.5) for the noise. 45 cycles would be enough for this purpose, but since rising and falling edges may propagate at different speeds throught the TT multiplexer, we also want to avoid hitting 0% or 100% duty cycle to keep one rising and one falling edge per pulse in the output signal.
The default behavior is to let the quantizer calculate the output by rounding the target output to the nearest integer. The hope is that the quantization error should behave as white noise. Then, even if some of it feeds through as audible noise at (higher) audible frequencies, it is just a little added noise. Round to nearest quantization produces the smallest quantization error, but the statistical properties may be unpredictable. If the quantization error ends up including prominent frequency components that change with the input signal, it could create objectionable artifacts in some cases.
TODO: rectangular and triangular noise.
The design contains a number of 16 bit registers. To write to one,
data_part_in = 1data_in[7:0] to the low bytedata_part_in = 0data_in[7:0] to the high byteaddr[2:0] to the desired register addressdata_part_in = 1Registers:
| Register | Field | Name | Description |
|---|---|---|---|
| 0 | reg0[15:0] |
u |
input signal |
| 1 | reg1[6:0] |
period_minus_1 |
|
reg1[10:8] |
u_rshift |
input shift | |
reg1[12] |
reset_lfsr |
for testing | |
reg1[13] |
force_err_0 |
for testing | |
reg1[15:14] |
pwm_mode |
||
| 2 | reg2[3:2] |
filter_choice |
|
reg2[11:8] |
n_decorrelate |
noise decorrelation | |
reg2[15:14] |
noise_mode |
||
| 3 | reg3[13:0] |
delta_u |
for triangle generator |
sample_toggle_out toggles each time pulse_divider+1 PWM pulses have been output. This can be used to time transfers of new input signal values u.echo_out echoes the value of echo_in. This could be used to test how a signal that is routed next to dac_out in the TT chip influences the noise performance.This project needs some way to use the dac_out output signal, and preferably to low pass filter it to filter out the high frequency noise.
Mike's audio Pmod (https://github.com/MichaelBell/tt-audio-pmod) can be used to convert the output to an audio signal for listening and includes a low pass filter to reduce frequencies above the audible range.
| # | Input | Output | Bidirectional |
|---|---|---|---|
| 0 | data_in[0] | pulse_width[0] | sample_toggle_out |
| 1 | data_in[1] | pulse_width[1] | addr_in[0] |
| 2 | data_in[2] | pulse_width[2] | addr_in[1] |
| 3 | data_in[3] | pulse_width[3] | addr_in[2] |
| 4 | data_in[4] | pulse_width[4] | data_part_in |
| 5 | data_in[5] | pulse_width[5] | echo_in |
| 6 | data_in[6] | pulse_width[6] | echo_out |
| 7 | data_in[7] | pulse_width[7] | dac_out |