summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/library/index.rst7
-rw-r--r--docs/library/machine.I2S.rst7
-rw-r--r--docs/library/wm8960.rst397
-rw-r--r--drivers/codec/wm8960.py753
4 files changed, 1164 insertions, 0 deletions
diff --git a/docs/library/index.rst b/docs/library/index.rst
index 2b1d6b965..ffd373a4d 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 e68a863d7..b602ac650 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 000000000..add003630
--- /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 000000000..ad1cb1cbf
--- /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)