diff --git a/docs/library/index.rst b/docs/library/index.rst index 2b1d6b9651..ffd373a4df 100644 --- a/docs/library/index.rst +++ b/docs/library/index.rst @@ -97,6 +97,13 @@ the following libraries. network.rst uctypes.rst +The following libraries provide drivers for hardware components. + +.. toctree:: + :maxdepth: 1 + + wm8960.rst + Port-specific libraries ----------------------- diff --git a/docs/library/machine.I2S.rst b/docs/library/machine.I2S.rst index e68a863d7c..b602ac6504 100644 --- a/docs/library/machine.I2S.rst +++ b/docs/library/machine.I2S.rst @@ -72,6 +72,13 @@ uasyncio:: sreader = uasyncio.StreamReader(audio_in) num_read = await sreader.readinto(buf) +Some codec devices like the WM8960 or SGTL5000 require separate initialization +before they can operate with the I2S class. For these, separate drivers are +supplied, which also offer methods for controlling volume, audio processing and +other things. For these drivers see: + +- :ref:`wm8960` + Constructor ----------- diff --git a/docs/library/wm8960.rst b/docs/library/wm8960.rst new file mode 100644 index 0000000000..add003630e --- /dev/null +++ b/docs/library/wm8960.rst @@ -0,0 +1,397 @@ +.. _wm8960: + +:mod:`WM8960` -- Driver for the WM8960 codec +============================================ + +This driver is used to control a WM8960 codec chip. It is a Python +translation of the C-Code provided by NXP/Freescale for their i.MX RT series of +MCUs. Very little has been added, and just a few API related names were changed +or added to cope with the naming style of MicroPython. + +The primary purpose of the driver is initialization and setting operation modes +of the codec. It does not do the audio data processing for the codec. That is +the task of a separate driver. + +The WM8960 supports an I2C interface, in addition to the audio interface. The +connection depends on the interface used and the number of devices in the +system. For the I2C interface, SCL and SDA have to be connected, and of course +GND and Vcc. The I2C default address is ``0x1A``. + +Constructor +----------- + +.. class:: WM8960(i2c, sample_rate, *, bits=16, swap=SWAP_NONE, route=ROUTE_PLAYBACK_RECORD, left_input=INPUT_MIC3, right_input=INPUT_MIC2, sysclk_source=SYSCLK_MCLK, mclk_freq=None, primary=False, adc_sync=SYNC_DAC, protocol=BUS_I2S, i2c_address=WM8960_I2C_ADDR) + + Create a WM8960 driver object, initialize the device with default settings and return the + WM8960 object. + + Only the first two arguments are mandatory. All others are optional. The arguments are: + + - *i2c* is the I2C bus object. + - *sample_rate* is the audio sample rate. Acceptable values are 8000, + 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000, 96000, 192000 + and 384000. Note that not every I2S hardware will support all values. + - *bits* is the number of bits per audio word. Acceptable value are 16, + 20, 24, and 32. + - *swap* swaps the left & right channel, if set; see below for options. + - *route* Setting the audio path in the codec; see below for options. + - *left_input* sets the audio source for the left input channel; + see below for options. + - *right_input* sets the audio source for the right input channel; + see below for options. + - *play_source* sets the audio target for the output audio; + see below for options. + - *sysclk_source* controls whether the internal master clock called + "sysclk" is directly taken from the MCLK input or derived from it + using an internal PLL. It is usually not required to change this. + - *mclk_freq* sets the mclk frequency applied to the MCLK pin of the + codec. If not set, default values are used. + - *primary* lets the WM8960 act as primary or secondary device. The + default setting is ``False``. When set to ``False``, + *sample_rate* and *bits* are controlled by the MCU. + - *adc_sync* sets which input is used for the ADC sync signal. + The default is using the DACLRC pin. + - *protocol* sets the communication protocol. The default is I2S. + See below for all options. + - *i2c_address* sets the I2C address of the WM8960, with default ``0x1A``. + + If *mclk_freq* is not set the following default values are used: + + - sysclk_source == SYSCLK_PLL: 11.2896 MHz for sample rates of 44100, + 22050 and 11015 Hz, and 12.288 Mhz for sample rates < 48000, otherwise + sample_rate * 256. + - sysclk_source == SYSCLK_MCLK: sample_rate * 256. + + If the MCLK signal is applied using, for example,. a separate oscillator, + it must be specified for proper operation. + +Tables of parameter constants +----------------------------- + +.. table:: **Swap Parameter** + :widths: auto + :align: left + + ===== ==== + Value Name + ===== ==== + 0 SWAP_NONE + 1 SWAP_INPUT + 2 SWAP_OUTPUT + ===== ==== + +.. table:: **Protocol Parameter** + :widths: auto + :align: left + + ===== ==== + Value Name + ===== ==== + 2 BUS_I2S + 1 BUS_LEFT_JUSTIFIED + 0 BUS_RIGHT_JUSTIFIED + 3 BUS_PCMA + 19 BUS_PCMB + ===== ==== + +.. table:: **Input Source Parameter** + :widths: auto + :align: left + + ===== ============ ==== + Value Name Type + ===== ============ ==== + 0 INPUT_CLOSED + 1 INPUT_MIC1 Single ended + 2 INPUT_MIC2 Differential + 3 INPUT_MIC3 Differential + 4 INPUT_LINE2 + 5 INPUT_LINE3 + ===== ============ ==== + +.. table:: **Route Parameter** + :widths: auto + :align: left + + ===== ==== + Value Name + ===== ==== + 0 ROUTE_BYPASS + 1 ROUTE_PLAYBACK + 2 ROUTE_PLAYBACK_RECORD + 5 ROUTE_RECORD + ===== ==== + +.. table:: **Master Clock Source Parameter** + :widths: auto + :align: left + + ===== ==== + Value Name + ===== ==== + 0 SYSCLK_MCLK + 1 SYSCLK_PLL + ===== ==== + +.. table:: **Module Names** + :widths: auto + :align: left + + ===== ==== + Value Name + ===== ==== + 0 MODULE_ADC + 1 MODULE_DAC + 2 MODULE_VREF + 3 MODULE_HEADPHONE + 4 MODULE_MIC_BIAS + 5 MODULE_MIC + 6 MODULE_LINE_IN + 7 MODULE_LINE_OUT + 8 MODULE_SPEAKER + 9 MODULE_OMIX + 10 MODULE_MONO_OUT + ===== ==== + +.. table:: **Play Channel Names** + :widths: auto + :align: left + + ===== ==== + Value Name + ===== ==== + 1 PLAY_HEADPHONE_LEFT + 2 PLAY_HEADPHONE_RIGHT + 4 PLAY_SPEAKER_LEFT + 8 PLAY_SPEAKER_RIGHT + ===== ==== + +.. table:: **adc_sync Parameters** + :widths: auto + :align: left + + ===== ==== + Value Name + ===== ==== + 0 SYNC_ADC + 1 SYNC_DAC + ===== ==== + + +Methods +------- + +In addition to initialization, the driver provides some useful methods for +controlling its operation: + +.. method:: WM8960.set_left_input(input_source) + + Specify the source for the left input. The input source names are listed above. + +.. method:: WM8960.set_right_input(input_source) + + Specify the source for the right input. The input source names are listed above. + +.. method:: WM8960.volume(module, volume_l=None, volume_r=None) + + Sets or gets the volume of a certain module. + + If no volume values are supplied, the actual volume tuple is returned. + + If one or two values are supplied, it sets the volume of a certain module. + If two values are provided, the first one is used for the left channel, + the second for the right channel. If only one value is supplied, it is used + for both channels. The value range is normalized to 0.0-100.0 with a + logarithmic scale. + + For a list of suitable modules and db/step, see the table below. + +.. table:: **Module Names and dB steps** + :widths: auto + :align: center + + ======= ==== + dB/Step Name + ======= ==== + 1.28 MODULE_ADC + 1.28 MODULE_DAC + 0.8 MODULE_HEADPHONE + 0.475 MODULE_LINE_IN + 0.8 MODULE_SPEAKER + ======= ==== + +.. method:: WM8960.mute(module, mute, soft=True, ramp=wm8960.MUTE_FAST) + + Mute or unmute the output. If *mute* is True, the output is muted, if ``False`` + it is unmuted. + + If *soft* is set as True, muting will happen as a soft transition. The time for + the transition is defined by *ramp*, which is either ``MUTE_FAST`` or ``MUTE_SLOW``. + +.. method:: WM8960.set_data_route(route) + + Set the audio data route. For the parameter value/names, see the table above. + +.. method:: WM8960.set_module(module, active) + + Enable or disable a module, with *active* being ``False`` or ``True``. For + the list of module names, see the table above. + + Note that enabling ``MODULE_MONO_OUT`` is different from the `WM8960.mono` + method. The first enables output 3, while the `WM8960.mono` method sends a + mono mix to the left and right output. + +.. method:: WM8960.enable_module(module) + + Enable a module. For the list of module names, see the table above. + +.. method:: WM8960.disable_module(module) + + Disable a module. For the list of module names, see the table above. + +.. method:: WM8960.expand_3d(level) + + Enable Stereo 3D exansion. *level* is a number between 0 and 15. + A value of 0 disables the expansion. + +.. method:: WM8960.mono(active) + + If *active* is ``True``, a Mono mix is sent to the left and right output + channel. This is different from enabling the ``MODULE_MONO_MIX``, which + enables output 3. + +.. method:: WM8960.alc_mode(channel, mode=ALC_MODE) + + Enables or disables ALC mode. Parameters are: + + - *channel* enables and sets the channel for ALC. The parameter values are: + + - ALC_OFF: Switch ALC off + - ALS_RIGHT: Use the right input channel + - ALC_LEFT: Use the left input channel + - ALC_STEREO: Use both input channels. + + - *mode* sets the ALC mode. Input values are: + + - ALC_MODE: act as ALC + - ALC_LIMITER: act as limiter. + +.. method:: WM8960.alc_gain(target=-12, max_gain=30, min_gain=-17.25, noise_gate=-78) + + Set the target level, highest and lowest gain levels and the noise gate as dB level. + Permitted ranges are: + + - *target*: -22.5 to -1.5 dB + - *max_gain*: -12 to 30 dB + - *min_gain*: -17 to 25 dB + - *noise_gate*: -78 to -30 dB + + Excess values are limited to the permitted ranges. A value of -78 or less + for *noise_gate* disables the noise gate function. + +.. method:: WM8960.alc_time(attack=24, decay=192, hold=0) + + Set the dynamic characteristic of ALC. The times are given as millisecond + values. Permitted ranges are: + + - *attack*: 6 to 6140 + - *decay*: 24 to 24580 + - *hold*: 0 to 43000 + + Excess values are limited within the permitted ranges. + +.. method:: WM8960.deemphasis(active) + + Enables or disables a deemphasis filter for playback, with *active* being + ``False`` or ``True``. This filter is applied only for sample rates of + 32000, 44100 and 48000. For other sample rates, the filter setting + is silently ignored. + +.. method:: WM8960.deinit() + + Disable all modules. + + +Examples +-------- + +Run WM8960 in secondary mode (default):: + + # Micro_python WM8960 Codec driver + # + # Setting the driver to Slave mode using the default settings + # + from machine import Pin, I2C + import wm8960 + i2c = I2C(0) + wm=wm8960.WM8960(i2c, 32000, left_input=wm8960.INPUT_MIC1) + wm.set_volume(wm8960.MODULE_HEADPHONE, 100) + + +Run WM8960 in primary mode:: + + # Micro_python WM8960 Codec driver + # + # Setting the driver to Master mode using specific audio format settings + # + from machine import Pin, I2C + import wm8960 + + i2c = I2C(0) + wm=wm8960.WM8960(i2c, 44100, primary=True, bits=16) + + +Run WM8960 on a MIMXRT10xx_DEV board in secondary mode (default):: + + # Micro_python WM8960 Codec driver + # + # Setting the driver to Slave mode using the default settings + # swap the input channels such that a MIMXRT Dev board mic, which + # is connected to the right input, is assigned to the left audio channel. + # + from machine import Pin, I2C + import wm8960 + i2c = I2C(0) + wm=wm8960.WM8960(i2c, sample_rate=16_000, + adc_sync=wm8960.SYNC_DAC, + swap=wm8960.SWAP_INPUT, + sysclk_source=wm8960.SYSCLK_MCLK) + + +Record with a Sparkfun WM8960 breakout board with Teensy in secondary mode (default):: + + # Micro_python WM8960 Codec driver + # + # The breakout board uses a fixed 24MHz MCLK. Therefore the internal + # PLL must be used as sysclk, which is the master audio clock. + # The Sparkfun board has the WS pins for RX and TX connected on the + # board. Therefore adc_sync must be set to sync_adc, to configure + # it's ADCLRC pin as input. + # + from machine import Pin, I2C + import wm8960 + i2c = I2C(0) + wm=wm8960.WM8960(i2c, sample_rate=16_000, + adc_sync=wm8960.SYNC_ADC, + sysclk_source=wm8960.SYSCLK_PLL, + mclk_freq=24_000_000, + left_input=wm8960.INPUT_MIC1, + right_input=wm8960.INPUT_CLOSED) + + +Play with a Sparkfun WM8960 breakout board with Teensy in secondary mode (default):: + + # The breakout board uses a fixed 24MHz MCLK. Therefore the internal + # PLL must be used as sysclk, which is the master audio clock. + # The Sparkfun board has the WS pins for RX and TX connected on the + # board. Therefore adc_sync must be set to sync_adc, to configure + # it's ADCLRC pin as input. + + from machine import I2C + i2c=I2C(0) + import wm8960 + wm=wm8960.WM8960(i2c, sample_rate=44_100, + adc_sync=wm8960.SYNC_ADC, + sysclk_source=wm8960.SYSCLK_PLL, + mclk_freq=24_000_000) + wm.set_volume(wm8960.MODULE_HEADPHONE, 100) diff --git a/drivers/codec/wm8960.py b/drivers/codec/wm8960.py new file mode 100644 index 0000000000..ad1cb1cbf8 --- /dev/null +++ b/drivers/codec/wm8960.py @@ -0,0 +1,753 @@ +# +# Driver class for the WM8960 Codec to be used e.g. with MIMXRT_1xxx Boards. +# Derived from the NXP SDK drivers. +# +# Copyright (c) 2015, Freescale Semiconductor, Inc., (C-Code) +# Copyright 2016-2021 NXP, (C-Code) +# All rights reserved. +# +# Translated to MicroPython by Robert Hammelrath, 2022 +# +# SPDX-License-Identifier: BSD-3-Clause +# + +import array +from micropython import const + +# Define the register addresses of WM8960. +_LINVOL = const(0x0) +_RINVOL = const(0x1) +_LOUT1 = const(0x2) +_ROUT1 = const(0x3) +_CLOCK1 = const(0x4) +_DACCTL1 = const(0x5) +_DACCTL2 = const(0x6) +_IFACE1 = const(0x7) +_CLOCK2 = const(0x8) +_IFACE2 = const(0x9) +_LDAC = const(0xA) +_RDAC = const(0xB) +_RESET = const(0xF) +_3D = const(0x10) +_ALC1 = const(0x11) +_ALC2 = const(0x12) +_ALC3 = const(0x13) +_NOISEG = const(0x14) +_LADC = const(0x15) +_RADC = const(0x16) +_ADDCTL1 = const(0x17) +# Register _ADDCTL2 = const(0x18) +_POWER1 = const(0x19) +_POWER2 = const(0x1A) +_ADDCTL3 = const(0x1B) +# Register _APOP1 = const(0x1C) +# Register _APOP2 = const(0x1D) +_LINPATH = const(0x20) +_RINPATH = const(0x21) +_LOUTMIX = const(0x22) +_ROUTMIX = const(0x25) +_MONOMIX1 = const(0x26) +_MONOMIX2 = const(0x27) +_LOUT2 = const(0x28) +_ROUT2 = const(0x29) +_MONO = const(0x2A) +_INBMIX1 = const(0x2B) +_INBMIX2 = const(0x2C) +_BYPASS1 = const(0x2D) +_BYPASS2 = const(0x2E) +_POWER3 = const(0x2F) +_ADDCTL4 = const(0x30) +_CLASSD1 = const(0x31) +# Register _CLASSD3 = const(0x33) +_PLL1 = const(0x34) +_PLL2 = const(0x35) +_PLL3 = const(0x36) +_PLL4 = const(0x37) + +# WM8960 PLLN range */ +_PLL_N_MIN_VALUE = const(6) +_PLL_N_MAX_VALUE = const(12) + +# WM8960 CLOCK2 bits +_CLOCK2_BCLK_DIV_MASK = const(0x0F) +_CLOCK2_DCLK_DIV_MASK = const(0x1C0) +_CLOCK2_DCLK_DIV_SHIFT = const(0x06) + +# Register _IFACE1 +_IFACE1_FORMAT_MASK = const(0x03) +_IFACE1_WL_MASK = const(0x0C) +_IFACE1_WL_SHIFT = const(0x02) +_IFACE1_LRP_MASK = const(0x10) +_IFACE1_MS_MASK = const(0x40) +_IFACE1_DLRSWAP_MASK = const(0x20) +_IFACE1_ALRSWAP_MASK = const(0x100) + +# Register _POWER1 +_POWER1_VREF_MASK = const(0x40) +_POWER1_VREF_SHIFT = const(0x06) +_POWER1_AINL_MASK = const(0x20) +_POWER1_AINR_MASK = const(0x10) +_POWER1_ADCL_MASK = const(0x08) +_POWER1_ADCR_MASK = const(0x0) +_POWER1_MICB_MASK = const(0x02) +_POWER1_MICB_SHIFT = const(0x01) + +# Register _POWER2 +_POWER2_DACL_MASK = const(0x100) +_POWER2_DACR_MASK = const(0x80) +_POWER2_LOUT1_MASK = const(0x40) +_POWER2_ROUT1_MASK = const(0x20) +_POWER2_SPKL_MASK = const(0x10) +_POWER2_SPKR_MASK = const(0x08) +_POWER3_LMIC_MASK = const(0x20) +_POWER3_RMIC_MASK = const(0x10) +_POWER3_LOMIX_MASK = const(0x08) +_POWER3_ROMIX_MASK = const(0x04) + +# Register _DACCTL1 .. 3 +_DACCTL1_MONOMIX_MASK = const(0x10) +_DACCTL1_MONOMIX_SHIFT = const(0x4) +_DACCTL1_DACMU_MASK = const(0x08) +_DACCTL1_DEEM_MASK = const(0x06) +_DACCTL1_DEEM_SHIFT = const(0x01) +_DACCTL2_DACSMM_MASK = const(0x08) +_DACCTL2_DACMR_MASK = const(0x04) +_DACCTL3_ALCSR_MASK = const(0x07) + +# _WM8060_ALC1 .. 3 +_ALC_CHANNEL_MASK = const(0x180) +_ALC_CHANNEL_SHIFT = const(0x7) +_ALC_MODE_MASK = const(0x100) +_ALC_MODE_SHIFT = const(0x8) +_ALC_GAIN_MASK = const(0x70) +_ALC_GAIN_SHIFT = const(0x4) +_ALC_TARGET_MASK = const(0x0F) +_ALC_ATTACK_MASK = const(0x0F) +_ALC_DECAY_MASK = const(0xF0) +_ALC_DECAY_SHIFT = const(4) +_ALC_HOLD_MASK = const(0xF) + +# Register _NOISEG +_NOISEG_LEVEL_SHIFT = const(3) + +_I2C_ADDR = const(0x1A) + +# WM8960 maximum volume values +_MAX_VOLUME_ADC = const(0xFF) +_MAX_VOLUME_DAC = const(0xFF) +_MAX_VOLUME_HEADPHONE = const(0x7F) +_MAX_VOLUME_LINEIN = const(0x3F) +_MAX_VOLUME_SPEAKER = const(0x7F) + +# Config symbol names +# Modules +MODULE_ADC = const(0) # ADC module in WM8960 +MODULE_DAC = const(1) # DAC module in WM8960 +MODULE_VREF = const(2) # VREF module +MODULE_HEADPHONE = const(3) # Headphone +MODULE_MIC_BIAS = const(4) # Mic bias +MODULE_MIC = const(5) # Input Mic +MODULE_LINE_IN = const(6) # Analog in PGA +MODULE_LINE_OUT = const(7) # Line out module +MODULE_SPEAKER = const(8) # Speaker module +MODULE_OMIX = const(9) # Output mixer +MODULE_MONO_OUT = const(10) # Mono mix + +# Route +ROUTE_BYPASS = const(0) # LINEIN->Headphone. +ROUTE_PLAYBACK = const(1) # I2SIN->DAC->Headphone. +ROUTE_PLAYBACK_RECORD = const(2) # I2SIN->DAC->Headphone, LINEIN->ADC->I2SOUT. +ROUTE_RECORD = const(5) # LINEIN->ADC->I2SOUT. + +# Input +INPUT_CLOSED = const(0) # Input device is closed +INPUT_MIC1 = const(1) # Input as single ended mic, only use L/RINPUT1 +INPUT_MIC2 = const(2) # Input as diff. mic, use L/RINPUT1 and L/RINPUT2 +INPUT_MIC3 = const(3) # Input as diff. mic, use L/RINPUT1 and L/RINPUT3 +INPUT_LINE2 = const(4) # Input as line input, only use L/RINPUT2 +INPUT_LINE3 = const(5) # Input as line input, only use L/RINPUT3 + +# ADC sync input +SYNC_ADC = const(0) # Use ADCLRC pin for ADC sync +SYNC_DAC = const(1) # used DACLRC pin for ADC sync + +# Protocol type +BUS_I2S = const(2) # I2S type +BUS_LEFT_JUSTIFIED = const(1) # Left justified mode +BUS_RIGHT_JUSTIFIED = const(0) # Right justified mode +BUS_PCMA = const(3) # PCM A mode +BUS_PCMB = const(3 | (1 << 4)) # PCM B mode + +# Channel swap +SWAP_NONE = const(0) +SWAP_INPUT = const(1) +SWAP_OUTPUT = const(2) + +# Mute settings +MUTE_FAST = const(0) +MUTE_SLOW = const(1) + +# ALC settings +ALC_OFF = const(0) +ALC_RIGHT = const(1) +ALC_LEFT = const(2) +ALC_STEREO = const(3) +ALC_MODE = const(0) # ALC mode +ALC_LIMITER = const(1) # Limiter mode + +# Clock Source +SYSCLK_MCLK = const(0) # sysclk source from external MCLK +SYSCLK_PLL = const(1) # sysclk source from internal PLL + + +class Regs: + # register cache of 56 register. Since registers cannot be read back, they are + # kept in the table for modification + # fmt: off + cache = array.array("H", ( + 0x0097, 0x0097, 0x0000, 0x0000, 0x0000, 0x0008, 0x0000, + 0x000a, 0x01c0, 0x0000, 0x00ff, 0x00ff, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x007b, 0x0100, 0x0032, 0x0000, + 0x00c3, 0x00c3, 0x01c0, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0100, 0x0100, 0x0050, + 0x0050, 0x0050, 0x0050, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0040, 0x0000, 0x0000, 0x0050, 0x0050, 0x0000, 0x0002, + 0x0037, 0x004d, 0x0080, 0x0008, 0x0031, 0x0026, 0x00e9 + )) + # fmt: on + + def __init__(self, i2c, i2c_address=_I2C_ADDR): + self.value_buffer = bytearray(2) + self.i2c = i2c + self.i2c_address = i2c_address + + def __getitem__(self, reg): + return self.cache[reg] + + def __setitem__(self, reg, value): + if type(reg) is tuple: + if type(value) is tuple: + self[reg[0]] = value[0] + self[reg[1]] = value[1] + else: + self[reg[0]] = value + self[reg[1]] = value + else: + if type(value) is tuple: + val = (self.cache[reg] & (~value[0] & 0xFFFF)) | value[1] + else: + val = value + self.cache[reg] = val + self.value_buffer[0] = (reg << 1) | ((val >> 8) & 0x01) + self.value_buffer[1] = val & 0xFF + self.i2c.writeto(self.i2c_address, self.value_buffer) + + +class WM8960: + + _bit_clock_divider_table = { + 2: 0, + 3: 1, + 4: 2, + 6: 3, + 8: 4, + 11: 5, + 12: 6, + 16: 7, + 22: 8, + 24: 9, + 32: 10, + 44: 11, + 48: 12, + } + + _dac_divider_table = { + 1.0 * 256: 0b000, + 1.5 * 256: 0b001, + 2 * 256: 0b010, + 3 * 256: 0b011, + 4 * 256: 0b100, + 5.5 * 256: 0b101, + 6 * 256: 0b110, + } + + _audio_word_length_table = { + 16: 0b00, + 20: 0b01, + 24: 0b10, + 32: 0b11, + } + + _alc_sample_rate_table = { + 48000: 0, + 44100: 0, + 32000: 1, + 24000: 2, + 22050: 2, + 16000: 3, + 12000: 4, + 11025: 4, + 8000: 5, + } + + _volume_config_table = { + MODULE_ADC: (_MAX_VOLUME_ADC, _LADC, 0x100), + MODULE_DAC: (_MAX_VOLUME_DAC, _LDAC, 0x100), + MODULE_HEADPHONE: (_MAX_VOLUME_HEADPHONE, _LOUT1, 0x180), + MODULE_LINE_IN: (_MAX_VOLUME_LINEIN, _LINVOL, 0x140), + MODULE_SPEAKER: (_MAX_VOLUME_SPEAKER, _LOUT2, 0x180), + } + + _input_config_table = { + INPUT_CLOSED: None, + INPUT_MIC1: (0x138, 0x117), + INPUT_MIC2: (0x178, 0x117), + INPUT_MIC3: (0x1B8, 0x117), + INPUT_LINE2: (0, 0xE), + INPUT_LINE3: (0, 0x70), + } + + def __init__( + self, + i2c, + sample_rate=16000, + bits=16, + swap=SWAP_NONE, + route=ROUTE_PLAYBACK_RECORD, + left_input=INPUT_MIC3, + right_input=INPUT_MIC2, + sysclk_source=SYSCLK_MCLK, + mclk_freq=None, + primary=False, + adc_sync=SYNC_DAC, + protocol=BUS_I2S, + i2c_address=_I2C_ADDR, + ): + self.regs = regs = Regs(i2c, i2c_address) + self.sample_rate = sample_rate + + # check parameter consistency and set the sysclk value + if sysclk_source == SYSCLK_PLL: + if sample_rate in (11025, 22050, 44100): + sysclk = 11289600 + else: + sysclk = 12288000 + if sysclk < sample_rate * 256: + sysclk = sample_rate * 256 + if mclk_freq is None: + mclk_freq = sysclk + else: # sysclk_source == SYSCLK_MCLK + if mclk_freq is None: + mclk_freq = sample_rate * 256 + sysclk = mclk_freq + + regs[_RESET] = 0x00 + # VMID=50K, Enable VREF, AINL, AINR, ADCL and ADCR + # I2S_IN (bit 0), I2S_OUT (bit 1), DAP (bit 4), DAC (bit 5), ADC (bit 6) are powered on + regs[_POWER1] = 0xFE + # Enable DACL, DACR, LOUT1, ROUT1, PLL down, SPKL, SPKR + regs[_POWER2] = 0x1F8 + # Enable left and right channel input PGA, left and right output mixer + regs[_POWER3] = 0x3C + + if adc_sync == SYNC_ADC: + # ADC and DAC use different Frame Clock Pins + regs[_IFACE2] = 0x00 # ADCLRC 0x00:Input 0x40:output. + else: + # ADC and DAC use the same Frame Clock Pin + regs[_IFACE2] = 0x40 # ADCLRC 0x00:Input 0x40:output. + self.set_data_route(route) + self.set_protocol(protocol) + + if sysclk_source == SYSCLK_PLL: + self.set_internal_pll_config(mclk_freq, sysclk) + if primary: + self.set_master_clock(sysclk, sample_rate, bits) + # set master bit. + self.regs[_IFACE1] = (0, _IFACE1_MS_MASK) + + self.set_speaker_clock(sysclk) + + # swap channels + if swap & SWAP_INPUT: + regs[_IFACE1] = (0, _IFACE1_ALRSWAP_MASK) + if swap & SWAP_OUTPUT: + regs[_IFACE1] = (0, _IFACE1_DLRSWAP_MASK) + + self.set_left_input(left_input) + self.set_right_input(right_input) + + regs[_ADDCTL1] = 0x0C0 + regs[_ADDCTL4] = 0x60 # Set GPIO1 to 0. + + regs[_BYPASS1] = regs[_BYPASS2] = 0x0 + # ADC volume, 0dB + regs[_LADC, _RADC] = 0x1C3 + # Digital DAC volume, 0dB + regs[_LDAC, _RDAC] = 0x1FF + # Headphone volume, LOUT1 and ROUT1, 0dB + regs[_LOUT1, _ROUT1] = 0x16F + # speaker volume 6dB + regs[_LOUT2, _ROUT2] = 0x1FF + # enable class D output + regs[_CLASSD1] = 0xF7 + # Unmute DAC. + regs[_DACCTL1] = 0x0000 + # Input PGA volume 0 dB + regs[_LINVOL, _RINVOL] = 0x117 + + self.config_data_format(sysclk, sample_rate, bits) + + def deinit(self): + + self.set_module(MODULE_ADC, False) + self.set_module(MODULE_DAC, False) + self.set_module(MODULE_VREF, False) + self.set_module(MODULE_LINE_IN, False) + self.set_module(MODULE_LINE_OUT, False) + self.set_module(MODULE_SPEAKER, False) + + def set_internal_pll_config(self, input_mclk, output_clk): + regs = self.regs + pllF2 = output_clk * 4 + pll_prescale = 0 + sysclk_div = 1 + frac_mode = 0 + + # disable PLL power + regs[_POWER2] = (1, 0) + regs[_CLOCK1] = (7, 0) + + pllN = pllF2 // input_mclk + if pllN < _PLL_N_MIN_VALUE: + input_mclk //= 2 + pll_prescale = 1 + pllN = pllF2 // input_mclk + if pllN < _PLL_N_MIN_VALUE: + sysclk_div = 2 + pllF2 *= 2 + pllN = pllF2 // input_mclk + + if (pllN < _PLL_N_MIN_VALUE) or (pllN > _PLL_N_MAX_VALUE): + raise ValueError("Invalid MCLK vs. sysclk ratio") + + pllK = ((pllF2 % input_mclk) * (1 << 24)) // input_mclk + if pllK != 0: + frac_mode = 1 + + regs[_PLL1] = (frac_mode << 5) | (pll_prescale << 4) | (pllN & 0x0F) + regs[_PLL2] = (pllK >> 16) & 0xFF + regs[_PLL3] = (pllK >> 8) & 0xFF + regs[_PLL4] = pllK & 0xFF + # enable PLL power + regs[_POWER2] = (1, 1) + regs[_CLOCK1] = (7, ((0 if sysclk_div == 1 else sysclk_div) << 1) | 1) + + def set_master_clock(self, sysclk, sample_rate, bit_width): + bit_clock_divider = (sysclk * 2) // (sample_rate * bit_width * 2) + try: + reg_divider = self._bit_clock_divider_table[bit_clock_divider] + except: + raise ValueError("Invalid ratio of sysclk sample rate and bits") + # configure the master bit clock divider + self.regs[_CLOCK2] = (_CLOCK2_BCLK_DIV_MASK, reg_divider) + + def set_speaker_clock(self, sysclk): + speaker_divider_table = (1.5, 2, 3, 4, 6, 8, 12, 16) + for val in range(8): + divider = speaker_divider_table[val] + f = sysclk / divider + if 500_000 < f < 1_000_000: + break + else: + val = 7 + self.regs[_CLOCK2] = ( + _CLOCK2_DCLK_DIV_MASK, + val << _CLOCK2_DCLK_DIV_SHIFT, + ) + + def set_module(self, module, is_enabled): + + is_enabled = 1 if is_enabled else 0 + regs = self.regs + + if module == MODULE_ADC: + + regs[_POWER1] = ( + _POWER1_ADCL_MASK | _POWER1_ADCR_MASK, + (_POWER1_ADCL_MASK | _POWER1_ADCR_MASK) * is_enabled, + ) + + elif module == MODULE_DAC: + + regs[_POWER2] = ( + _POWER2_DACL_MASK | _POWER2_DACR_MASK, + (_POWER2_DACL_MASK | _POWER2_DACR_MASK) * is_enabled, + ) + + elif module == MODULE_VREF: + + regs[_POWER1] = ( + _POWER1_VREF_MASK, + (is_enabled << _POWER1_VREF_SHIFT), + ) + + elif module == MODULE_LINE_IN: + + regs[_POWER1] = ( + _POWER1_AINL_MASK | _POWER1_AINR_MASK, + (_POWER1_AINL_MASK | _POWER1_AINR_MASK) * is_enabled, + ) + regs[_POWER3] = ( + _POWER3_LMIC_MASK | _POWER3_RMIC_MASK, + (_POWER3_LMIC_MASK | _POWER3_RMIC_MASK) * is_enabled, + ) + + elif module == MODULE_LINE_OUT: + + regs[_POWER2] = ( + _POWER2_LOUT1_MASK | _POWER2_ROUT1_MASK, + (_POWER2_LOUT1_MASK | _POWER2_ROUT1_MASK) * is_enabled, + ) + + elif module == MODULE_MIC_BIAS: + + regs[_POWER1] = ( + _POWER1_MICB_MASK, + (is_enabled << _POWER1_MICB_SHIFT), + ) + + elif module == MODULE_SPEAKER: + + regs[_POWER2] = ( + _POWER2_SPKL_MASK | _POWER2_SPKR_MASK, + (_POWER2_SPKL_MASK | _POWER2_SPKR_MASK) * is_enabled, + ) + regs[_CLASSD1] = 0xF7 + + elif module == MODULE_OMIX: + + regs[_POWER3] = ( + _POWER3_LOMIX_MASK | _POWER3_ROMIX_MASK, + (_POWER3_LOMIX_MASK | _POWER3_ROMIX_MASK) * is_enabled, + ) + + elif module == MODULE_MONO_OUT: + + regs[_MONOMIX1] = regs[_MONOMIX2] = is_enabled << 7 + regs[_MONO] = is_enabled << 6 + + else: + raise ValueError("Invalid module") + + def enable_module(self, module): + self.set_module(module, True) + + def disable_module(self, module): + self.set_module(module, False) + + def set_data_route(self, route): + regs = self.regs + if route == ROUTE_BYPASS: + # Bypass means from line-in to HP + # Left LINPUT3 to left output mixer, LINPUT3 left output mixer volume = 0dB + # Right RINPUT3 to right output mixer, RINPUT3 right output mixer volume = 0dB + regs[_LOUTMIX, _ROUTMIX] = 0x80 + + elif route == ROUTE_PLAYBACK: + # Data route I2S_IN-> DAC-> HP + # + # Left DAC to left output mixer, LINPUT3 left output mixer volume = 0dB + # Right DAC to right output mixer, RINPUT3 right output mixer volume = 0dB + regs[_LOUTMIX, _ROUTMIX] = 0x100 + regs[_POWER3] = 0x0C + # Set power for DAC + self.set_module(MODULE_DAC, True) + self.set_module(MODULE_OMIX, True) + self.set_module(MODULE_LINE_OUT, True) + + elif route == ROUTE_PLAYBACK_RECORD: + # + # Left DAC to left output mixer, LINPUT3 left output mixer volume = 0dB + # Right DAC to right output mixer, RINPUT3 right output mixer volume = 0dB + regs[_LOUTMIX, _ROUTMIX] = 0x100 + regs[_POWER3] = 0x3C + self.set_module(MODULE_DAC, True) + self.set_module(MODULE_ADC, True) + self.set_module(MODULE_LINE_IN, True) + self.set_module(MODULE_OMIX, True) + self.set_module(MODULE_LINE_OUT, True) + + elif route == ROUTE_RECORD: + # LINE_IN->ADC->I2S_OUT + # Left and right input boost, LIN3BOOST and RIN3BOOST = 0dB + regs[_POWER3] = 0x30 + # Power up ADC and AIN + self.set_module(MODULE_LINE_IN, True) + self.set_module(MODULE_ADC, True) + + else: + raise ValueError("Invalid route") + + def set_left_input(self, input): + if not input in self._input_config_table.keys(): + raise ValueError("Invalid input") + + input = self._input_config_table[input] + + regs = self.regs + if input is None: + regs[_POWER1] = (_POWER1_AINL_MASK | _POWER1_ADCL_MASK, 0) + elif input[0] == 0: + regs[_POWER1] = (0, _POWER1_AINL_MASK | _POWER1_ADCL_MASK) + regs[_INBMIX1] = input + else: + regs[_POWER1] = (0, _POWER1_AINL_MASK | _POWER1_ADCL_MASK | _POWER1_MICB_MASK) + regs[_LINPATH] = input[0] + regs[_LINVOL] = input[1] + + def set_right_input(self, input): + if not input in self._input_config_table.keys(): + raise ValueError("Invalid input name") + + input = self._input_config_table[input] + + regs = self.regs + if input is None: + regs[_POWER1] = (_POWER1_AINR_MASK | _POWER1_ADCR_MASK, 0) + elif input[0] == 0: + regs[_POWER1] = (0, _POWER1_AINL_MASK | _POWER1_ADCR_MASK) + regs[_INBMIX2] = input + else: + regs[_POWER1] = (0, _POWER1_AINR_MASK | _POWER1_ADCR_MASK | _POWER1_MICB_MASK) + regs[_RINPATH] = input[0] + regs[_RINVOL] = input[1] + + def set_protocol(self, protocol): + self.regs[_IFACE1] = ( + _IFACE1_FORMAT_MASK | _IFACE1_LRP_MASK, + protocol, + ) + + def config_data_format(self, sysclk, sample_rate, bits): + # Compute sample rate divider, dac and adc are the same sample rate + try: + divider = self._dac_divider_table[sysclk // sample_rate] + wl = self._audio_word_length_table[bits] + except: + raise ValueError("Invalid ratio sysclk/sample_rate or invalid bit length") + + self.regs[_CLOCK1] = (0x1F8, divider << 6 | divider << 3) + self.regs[_IFACE1] = (_IFACE1_WL_MASK, wl << _IFACE1_WL_SHIFT) + + def volume(self, module, volume_l=None, volume_r=None): + if not module in self._volume_config_table.keys(): + raise ValueError("Invalid module") + + if volume_l is None: # get volume + vol_max, regnum, _ = self._volume_config_table[module] + return ( + int((self.regs[regnum] & vol_max) * 100 / vol_max + 0.5), + int((self.regs[regnum + 1] & vol_max) * 100 / vol_max + 0.5), + ) + else: # set volume + if volume_r is None: + volume_r = volume_l + + if not ((0 <= volume_l <= 100) and (0 <= volume_r <= 100)): + raise ValueError("Invalid value for volume") + elif not module in self._volume_config_table.keys(): + raise ValueError("Invalid module") + + vol_max, regnum, flags = self._volume_config_table[module] + self.regs[regnum] = int(volume_l * vol_max / 100 + 0.5) | flags + self.regs[regnum + 1] = int(volume_r * vol_max / 100 + 0.5) | flags + + def mute(self, enable, soft=True, ramp=MUTE_FAST): + enable = _DACCTL1_DACMU_MASK if enable else 0 + soft = _DACCTL2_DACSMM_MASK if soft else 0 + ramp = _DACCTL2_DACMR_MASK if ramp == MUTE_SLOW else 0 + self.regs[_DACCTL1] = (_DACCTL1_DACMU_MASK, enable) + self.regs[_DACCTL2] = ( + _DACCTL2_DACSMM_MASK | _DACCTL2_DACMR_MASK, + soft | ramp, + ) + + def expand_3d(self, depth=0): + depth &= 0x0F + cutoff = 0 if self.sample_rate >= 32000 else 0b1100000 + self.regs[_3D] = cutoff | depth << 1 | (1 if depth > 0 else 0) + + def mono(self, enable): + enable = 1 if enable else 0 + self.regs[_DACCTL1] = ( + _DACCTL1_MONOMIX_MASK, + enable << _DACCTL1_MONOMIX_SHIFT, + ) + + def alc_mode(self, channel, mode=ALC_MODE): + if mode != ALC_MODE: + mode = ALC_LIMITER + channel &= 3 + self.regs[_ALC1] = ( + _ALC_CHANNEL_MASK, + channel << _ALC_CHANNEL_SHIFT, + ) + self.regs[_ALC3] = (_ALC_MODE_MASK, mode << _ALC_MODE_SHIFT) + try: + rate = _alc_sample_rate_table[self.sample_rate] + except: + rate = 0 + self.regs[_ADDCTL3] = (_DACCTL3_ALCSR_MASK, rate) + + def alc_gain(self, target=-12, max_gain=30, min_gain=-17.25, noise_gate=-78): + def limit(value, minval, maxval): + value = int(value) + if value < minval: + value = minval + if value > maxval: + value = maxval + return value + + target = limit((16 + (target * 2) // 3), 0, 15) + max_gain = limit((max_gain + 12) // 6, 0, 7) + min_gain = limit((min_gain * 4 + 69) // 24, 0, 7) + noise_gate = limit((noise_gate * 2 + 153) // 3, -1, 31) + self.regs[_ALC1] = ( + _ALC_GAIN_MASK | _ALC_TARGET_MASK, + (max_gain << _ALC_GAIN_SHIFT) | target, + ) + self.regs[_ALC2] = (_ALC_GAIN_MASK, (min_gain << _ALC_GAIN_SHIFT)) + if noise_gate >= 0: + self.regs[_NOISEG] = noise_gate << _NOISEG_LEVEL_SHIFT | 1 + else: + self.regs[_NOISEG] = 0 + + def alc_time(self, attack=24, decay=192, hold=0): + def logb(value, limit): + value = int(value) + lb = 0 + while value > 1: + value >>= 1 + lb += 1 + if lb > limit: + lb = limit + return lb + + attack = logb(attack / 6, 7) + decay = logb(decay / 24, 7) + hold = logb((hold * 3) / 8, 15) + self.regs[_ALC2] = (_ALC_HOLD_MASK, hold) + self.regs[_ALC3] = ( + _ALC_DECAY_MASK | _ALC_ATTACK_MASK, + (decay << _ALC_DECAY_SHIFT) | attack, + ) + + def deemphasis(self, enable): + deem_table = (32000, 44100, 48000) + enable = not not enable + if enable and self.sample_rate in deem_table: + val = deem_table.index(self.sample_rate) + 1 + else: + val = 0 + self.regs[_DACCTL1] = (_DACCTL1_DEEM_MASK, val << _DACCTL1_DEEM_SHIFT)