API Reference

adafruit_nau88l21

CircuitPython driver for the Nuvoton NAU88L21 audio codec

  • Author(s): Tim Cocks

Implementation Notes

Hardware:

  • The NAU88L21 is a stereo codec: a DAC driving a headphone amplifier, and an ADC fed by a differential microphone front-end PGA.

  • Registers are 16-bit addresses with 16-bit big-endian data. Every descriptor in this module therefore has to be built with register_width=2 and lsb_first=False;

  • CAUTION: the 3.5mm amplifier can drive sensitive earbuds to levels that could damage your hearing. Teenage Engineering documentation states: TING is designed to connect to RIDDIM or any sound system, not directly to headphones. the maximum output is 2 VRMS. This can be very loud when connected directly to headphones.

Clocking, and why it needs I2S running first

CircuitPythons rp2 I2S generates BCLK and WS, the codec runs from that external clock (what the datasheet calls slave mode). Its SYSCLK comes from the internal FLL locked to the incoming bit clock, which means BCLK must already be running before NAU88L21.configure_clocks is called, otherwise the FLL has nothing to lock to. The correct order is:

i2s = audiobusio.I2SOut(bit_clock, word_select, data)
i2s.play(some_looping_sample, loop=True)   # BCLK starts here
codec.configure_clocks()                   # now the FLL can lock

The driver does not own the I2SOut/I2SIn object; you construct it and keep it.

Full duplex

The internal clock mode object has to exist before the external clock mode one has anything to sync to, and the codec’s FLL has to have BCLK before it can lock:

i2s = audiobusio.I2SOut(bit_clock, word_select, data_out)
codec.configure_clocks()
codec.headphone_output = True
codec.configure_microphone_input()
mic = audioi2sin.I2SIn(bit_clock, word_select, data_in, sample_rate=48000,
                       bit_depth=16, external_clock=True,
                       left_justified=True)

Input left_justified

I2SIn wants left_justified=True even though the codec is in Philips mode. The codec drives ADCDAT one bit clock earlier than CircuitPython’s own internal clock mode transmitter emits it, and the I2SIn left_justified flag is what takes that bit back.

Effect chains order

Wire effect chains from the output backwards. I2SOut.play() restarts the internal clock mode state machine, which knocks an external_clock=True I2SIn off the frame it had locked to; that I2SIn is re-synced only by a play() call made on the mic, and effects do not propagate that down to their own source. So the call that hands over the mic must come last:

i2s.play(effect)      # first
effect.play(mic)      # last

Software and Dependencies:

class adafruit_nau88l21.NAU88L21(i2c: I2C, address: int = _DEFAULT_ADDRESS, reset: bool = True)

Driver for the Nuvoton NAU88L21 stereo audio codec.

Parameters:
  • i2c – The I2C bus the codec is connected to.

  • address – The I2C device address. Defaults to 0x1B.

  • reset – Reset the codec during construction. Defaults to True.

The codec must have a running bit clock before configure_clocks is called; see the module documentation for the ordering.

property adc_biquad: bool

Whether the capture-path biquad filter (BIQ0) is running.

Enabling a filter whose coefficients have never been programmed is silence, not a bypass. reset leaves every coefficient at zero. Set adc_biquad_coefficients first, or to BIQUAD_PASSTHROUGH.

Getter:

True if the ADC biquad is enabled.

Setter:

Enable or disable the ADC biquad.

property adc_biquad_coefficients: Tuple[float, ...]

The capture-path biquad coefficients, as (b0, b1, b2, a1, a2).

The filter is the usual second-order section:

        b0 + b1*z**-1 + b2*z**-2
H(z) = --------------------------
         1 + a1*z**-1 + a2*z**-2

which is exactly the sign convention scipy.signal.butter and friends return, so b, a = butter(...) maps straight onto (b[0], b[1], b[2], a[1], a[2]).

Coefficients are stored in a 19-bit S2.16 fixed-point format, so the range is -4.0 to +3.99998 in steps of 1/65536 and values outside it are clipped. Round-tripping through this property therefore returns the quantized coefficients, not the ones that were written.

Getter:

Return the five coefficients as floats.

Setter:

Write all five in a single ten-register transaction, leaving the enable bit as it was.

property adc_drc: bool

Whether the capture-path dynamic range compressor is running.

Setting this to True applies the chip’s default curve by calling configure_adc_drc with no arguments; use that method to shape it.

Getter:

True if the ADC DRC is enabled.

Setter:

Enable (with default settings) or disable the ADC DRC.

property adc_soft_mute: bool

The ADC soft mute, which ramps both capture channels down together.

Getter:

True if the ADC is muted.

Setter:

Mute or unmute the ADC.

property adc_volume: float

The ADC digital volume in dB, averaged across the two channels.

Same scale as dac_volume: -66 dB to +24 dB in 0.5 dB steps, 0 dB unity. Setting it writes both channels of R35 at once.

Note that on a board with a single microphone on the left channel only the left half of this register does anything audible; the right channel’s code reads back but has no signal to scale.

Getter:

Return the volume in dB.

Setter:

Set both channels to the same volume in dB, clipped to range.

property bit_clock_inverted: bool

Which BCLK edge the codec latches on.

Only needed if captured words come back rotated by a bit – the datasheet’s external clock timing already matches CircuitPython’s edge convention, so this should not be necessary.

Getter:

Return True if the bit clock is inverted.

Setter:

Invert or un-invert the bit clock.

property bit_depth: int

The I2S word length in bits, as last set by configure_clocks.

Getter:

Return the configured bit depth.

chip_id

The device ID register, which always reads 0x1B23.

config_mic_bias(power_down: bool = False, voltage: int = 6, internal_resistor: bool = False) None

Configure the MICBIAS supply.

Parameters:
  • power_down – Power MICBIAS down. configure_microphone_input powers it up, so this is how it goes back off on its own.

  • voltage – Output level select, 0-7: 0 is VDDA, then 1x, 1.1x, 1.2x, 1.3x, 1.4x and 1.53x for 6 and 7. 6 is the reset default and what the microphone path uses.

  • internal_resistor – Connect the internal 2 kOhm resistor between MICBIAS and MICGND. Only needed if the board does not fit an external one.

configure_adc_drc(knee1_db: float = -6, knee2_db: float = -20, knee3_db: float = -36, knee4_db: float = -50, limiter_slope: int = 1, compressor1_slope: int = 1, compressor2_slope: int = 1, expander_slope: int = 4, noise_gate_slope: int = 4, attack: int = 5, decay: int = 7, peak_attack: int = 3, peak_decay: int = 4, smooth_filter: bool = True) None

Configure and enable the capture-path dynamic range compressor.

The DRC is a five-section static curve. Reading down from the loudest input: the limiter above knee1_db, compressor 1 between knee 1 and knee 2, compressor 2 between knee 2 and knee 3, the expander between knee 3 and knee 4, and the noise gate below knee 4. Every argument defaults to the chip’s own reset value, so calling this with no arguments enables the datasheet’s default curve.

Parameters:
  • knee1_db – Limiter knee, 0 to -31 dB.

  • knee2_db – Compressor 1 knee, 0 to -63 dB.

  • knee3_db – Compressor 2 knee, -18 to -81 dB.

  • knee4_db – Expander knee, -35 to -98 dB.

  • limiter_slope – Denominator of the limiter’s 1:N ratio: 1 (no limiting), 2, 4, 8, 16, 32, 64, or 0 for a hard limit.

  • compressor1_slope – Same, but only 1, 2, 4, 8, 16 or 0.

  • compressor2_slope – Same as compressor1_slope.

  • expander_slope – Numerator of the expander’s N:1 ratio: 1, 2 or 4. (8 is reserved on the capture path.)

  • noise_gate_slope – Numerator of the noise gate’s N:1 ratio: 1, 2, 4 or 8.

  • attack – Attack time code, 0-12. The times are multiples of Ts = 1/sample_rate – 0 is Ts, then 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095 and 8191 Ts. They are codes rather than milliseconds because this driver deliberately does not know the sample rate; see configure_clocks.

  • decay – Decay time code, 0-10: 63 Ts, then 127, 255, 511, 1023, 2047, 4095, 8191, 16383, 32767 and 65535 Ts.

  • peak_attack – Peak detector attack code, 0-7, same series as attack.

  • peak_decay – Peak detector release code, 0-7, same series as decay.

  • smooth_filter – Round the corners of the static curve at the knee points.

configure_adc_highpass(frequency: float = 120.0, sample_rate: int = 48000, q: float = 0.7071, enable: bool = True) None

Program the capture biquad as a DC-blocking high-pass filter.

The microphone word carries a large DC offset plus a slow sub-audio bias drift – thousands of counts, and it grows with mic_gain. Left in, it eats output headroom and, on speech transients, shows up in a passthrough as a loud subsonic “wind” that can drive the output into clipping. This loads BIQ0 with a second-order RBJ high-pass whose exact double zero at DC removes the offset at the source, before the I2S interface, so nothing downstream has to deal with it.

Doing it here rather than with a host-side biquad matters: a high-pass at a corner this far below Nyquist has coefficients very close to (1, -2, 1)/(1, -2, 1), and host filters built in fixed point round away the tiny differences that give the stop-band its depth, so they leak tens of dB of sub-bass. This chip runs the section at full internal precision, and forcing the numerator symmetric below keeps the DC null exact even after the coefficients are quantized to the register’s S2.16 format.

Parameters:
  • frequency – -3 dB corner in Hz. 120 keeps speech fundamentals while cutting the drift; go higher to cut more low end.

  • sample_rate – The ADC’s sample rate, i.e. the I2S frame rate the codec is clocked at. Must match it or the corner lands elsewhere.

  • q – Filter Q. 0.7071 is a maximally flat (Butterworth) response.

  • enable – Leave the filter switched on afterwards.

configure_clocks(bit_depth: int = 16, mclk_freq: int | None = None, left_justified: bool = False) None

Configure the codec’s I2S interface and clocking.

The bit clock must already be running when this is called. With the default mclk_freq=None the codec’s SYSCLK comes from its FLL locked to BCLK, and an FLL with no reference will not lock. Construct the I2SOut object and play() a looping sample first, at any sample rate, then call this.

Parameters:
  • bit_depth – I2S word length: 16, 20, 24 or 32. CircuitPython always sends 16-bit stereo, so leave this at 16.

  • mclk_freqNone (the default) locks the FLL to BCLK and leaves the MCLK pin unused. 12_000_000 instead takes SYSCLK straight from a 12 MHz clock on the MCLK pin with the FLL bypassed, which requires microcontroller.cpu.frequency = 144_000_000 and a pwmio.PWMOut driving that pin, and which pins the sample rate at 46875 Hz. The FLL is the better default.

  • left_justified – Use left-justified framing rather than Philips I2S. This is the framing the codec both expects on DACDAT and drives on ADCDAT, so it must match how the I2SOut object was built.

configure_dac_drc(knee1_db: float = -6, knee2_db: float = -20, knee3_db: float = -36, knee4_db: float = -50, limiter_slope: int = 2, compressor1_slope: int = 1, compressor2_slope: int = 1, expander_slope: int = 4, noise_gate_slope: int = 4, attack: int = 5, decay: int = 7, peak_attack: int = 3, peak_decay: int = 4, smooth_filter: bool = True) None

Configure and enable the playback-path dynamic range compressor.

The DRC is a five-section static curve. Reading down from the loudest input: the limiter above knee1_db, compressor 1 between knee 1 and knee 2, compressor 2 between knee 2 and knee 3, the expander between knee 3 and knee 4, and the noise gate below knee 4. Every argument defaults to the chip’s own reset value, so calling this with no arguments enables the datasheet’s default curve.

Parameters:
  • knee1_db – Limiter knee, 0 to -31 dB.

  • knee2_db – Compressor 1 knee, 0 to -63 dB.

  • knee3_db – Compressor 2 knee, -18 to -81 dB.

  • knee4_db – Expander knee, -35 to -98 dB.

  • limiter_slope – Denominator of the limiter’s 1:N ratio: 1 (no limiting), 2, 4, 8, 16, 32, 64, or 0 for a hard limit.

  • compressor1_slope – Same, but only 1, 2, 4, 8, 16 or 0.

  • compressor2_slope – Same as compressor1_slope.

  • expander_slope – Numerator of the expander’s N:1 ratio: 1, 2 or 4. (8 is reserved on the capture path.)

  • noise_gate_slope – Numerator of the noise gate’s N:1 ratio: 1, 2, 4 or 8.

  • attack – Attack time code, 0-12. The times are multiples of Ts = 1/sample_rate – 0 is Ts, then 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095 and 8191 Ts. They are codes rather than milliseconds because this driver deliberately does not know the sample rate; see configure_clocks.

  • decay – Decay time code, 0-10: 63 Ts, then 127, 255, 511, 1023, 2047, 4095, 8191, 16383, 32767 and 65535 Ts.

  • peak_attack – Peak detector attack code, 0-7, same series as attack.

  • peak_decay – Peak detector release code, 0-7, same series as decay.

  • smooth_filter – Round the corners of the static curve at the knee points.

The DAC block’s reset limiter slope is 1:2.

Note that the DRC sits after the crosstalk/sidetone mixer, at the end of the playback path, so a quiet mixed-in signal can be squashed by it.

configure_microphone_input(gain_db: float = _QUICKSTART_MIC_GAIN_DB, settle: bool = True) None

Power up the microphone-to-ADC path.

This is what microphone_input = True runs; call it directly to choose the front-end gain, or to return immediately instead of waiting out the bias settling time.

Parameters:
  • gain_db – Front-end PGA gain in dB, -1 to 36. See mic_gain.

  • settle – Wait ~0.4 s for the microphone bias and the ADC front end to settle before returning. With settle=False the first few hundred milliseconds of a capture are DC drift with almost no audio in them, so only turn it off if you are going to discard or high-pass that part anyway.

property dac_biquad: bool

Whether the playback-path biquad filter (BIQ1) is running.

Same caveat as adc_biquad: zeroed coefficients are silence.

Getter:

True if the DAC biquad is enabled.

Setter:

Enable or disable the DAC biquad.

property dac_biquad_coefficients: Tuple[float, ...]

The playback-path biquad coefficients, as (b0, b1, b2, a1, a2).

See adc_biquad_coefficients for the format and its limits.

Getter:

Return the five coefficients as floats.

Setter:

Write all five in a single ten-register transaction, leaving the enable bit as it was.

property dac_drc: bool

Whether the playback-path dynamic range compressor is running.

Setting this to True applies the chip’s default curve by calling configure_dac_drc with no arguments; use that method to shape it.

Getter:

True if the DAC DRC is enabled.

Setter:

Enable (with default settings) or disable the DAC DRC.

property dac_soft_mute: bool

The DAC soft mute, which ramps both channels down together.

This is the only mute the DAC path has – the chip has no per-channel DAC mute bit, so there is no left_dac_mute / right_dac_mute here. A single channel can still be silenced through its left_dac_channel_volume / right_dac_channel_volume.

Getter:

True if the DAC is muted.

Setter:

Mute or unmute the DAC.

property dac_volume: float

The DAC digital volume in dB, averaged across the two channels.

Range is -66 dB (soft) to +24 dB (loud) in 0.5 dB steps; 0 dB is unity. Setting it writes both channels of R34 at once.

This is the level feeding the headphone amplifier, so it interacts with headphone_volume, headphone_left_mute, headphone_right_mute and dac_soft_mute.

Getter:

Return the volume in dB.

Setter:

Set both channels to the same volume in dB, clipped to range.

property headphone_left_mute: bool

The left headphone amplifier mute.

Getter:

True if the left headphone channel is muted.

Setter:

Mute or unmute the left headphone channel.

property headphone_output: bool

Headphone output helper with quickstart default settings.

Setting this to True powers up the whole DAC-to-headphone chain and picks levels intended for quiet listening on sensitive low-impedance earbuds:

  • dac_volume = -20 dB

  • headphone_volume = -9 dB (the quietest the analog stage goes)

Setting it to False powers the headphone drivers and the charge pump back down, leaving the microphone path alone.

Either value leaves the microphone path as it found it, so this and microphone_input can be set in either order to run full duplex.

configure_clocks must have been called first – with no SYSCLK the DAC has no clock to run from.

Getter:

True if any of the headphone output drivers are powered.

Setter:

This sets several properties at once, including both DAC enables, both DAC channel volumes, the headphone analog volume, and the DAC and headphone mutes.

property headphone_right_mute: bool

The right headphone amplifier mute.

Getter:

True if the right headphone channel is muted.

Setter:

Mute or unmute the right headphone channel.

property headphone_volume: float

The headphone analog volume in dB, averaged across the two channels.

T his stage has only four steps: 0, -3, -6 and -9 dB. Values in between are rounded to the nearest step and values outside the range are clipped, so the level actually applied is worth reading back. Most of the usable attenuation lives in dac_volume.

Getter:

Return the volume in dB.

Setter:

Set both channels to the nearest available step.

property left_dac: bool

The left DAC enabled status.

Getter:

True if the left DAC is enabled.

Setter:

Enable or disable the left DAC.

property left_dac_channel_volume: float

The left DAC channel digital volume in dB, -66 to +24.

Getter:

Return the volume in dB.

Setter:

Set the volume in dB, clipped to range.

property mclk_freq: int | None

The MCLK pin frequency, as last set by configure_clocks.

None means SYSCLK comes from the FLL locked to BCLK and the MCLK pin is unused.

Getter:

Return the configured MCLK frequency in Hz, or None.

property mic_gain: float

The analog front-end PGA gain in dB, -1 to 36 in 1 dB steps.

This is the gain applied to the microphone before the ADC, and it is the one that changes the signal-to-noise ratio of a capture – unlike adc_volume, which scales what the ADC already produced. Setting it writes both channels.

Getter:

Return the left channel’s gain in dB.

Setter:

Set both channels to the same gain in dB, clipped to range.

property microphone_input: bool

Microphone input helper with quickstart default settings.

Setting this to True powers up the whole microphone-to-ADC chain – bias, MICBIAS, both front-end PGAs at mic_gain = 36 dB, the ADC analog front end and both ADC channels – and then waits for the bias to settle before returning. It is equivalent to configure_microphone_input(); use that method directly to pick a different gain or to skip the settling delay.

Setting it to False powers the microphone path back down, leaving the headphone path alone.

configure_clocks must have been called first.

Getter:

True if either microphone PGA is powered.

Setter:

This sets several properties at once, including mic_gain, adc_volume and both ADC enables, and it blocks for about 0.4 s while the bias settles.

reset() None

Reset the codec to its power-on defaults.

It is written twice to clear the internal state machines; one write clears the registers alone.

property right_dac: bool

The right DAC enabled status.

Getter:

True if the right DAC is enabled.

Setter:

Enable or disable the right DAC.

property right_dac_channel_volume: float

The right DAC channel digital volume in dB, -66 to +24.

Getter:

Return the volume in dB.

Setter:

Set the volume in dB, clipped to range.