/* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + Last modified: $Date: 2021-04-26 08:20:34 +0100 (Mon, 26 Apr 2021) $ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + cmx655.c : CMX655 ALSA SoC Audio driver ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */ #include #include #include #include #include #include #include #include #include #include #include #include "cmx655.h" /* #if LINUX_VERSION_CODE < KERNEL_VERSION(5,9,0) #define SOC_COM_READ snd_soc_component_read32 #else #define SOC_COM_READ snd_soc_component_read #endif */ #define SOC_COM_READ snd_soc_component_read /* * Structure to hold info on cmx655 setup * */ struct cmx655DaiData { int iSysClk; unsigned int uiEnabledStreams; bool bBestClkRunning; // Clear if prepare needs to setup the clock int iClkSrc; }; struct cmx655Data { struct regmap *psRegmap; struct cmx655DaiData sDaiData; struct gpio_desc *psResetGpio; // Number of times the class-D overcurrent has been reset unsigned int uiOcCnt; // Max times the class-D overcurrent should be reset unsigned int uiOcCntMax; }; /* * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ * Create Regmap * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */ static const struct reg_default asCmx655RegDefaults[] = { { CMX655_ISR , 0x00 }, { CMX655_ISM , 0x00 }, { CMX655_ISE , 0x00 }, { CMX655_CLKCTRL , 0x00 }, { CMX655_RDIVHI , 0x00 }, { CMX655_RDIVLO , 0x00 }, { CMX655_NDIVHI , 0x00 }, { CMX655_NDIVLO , 0x00 }, { CMX655_PLLCTRL , 0x00 }, { CMX655_SAICTRL , 0x00 }, { CMX655_SAIMUX , 0x00 }, { CMX655_RVF , 0x00 }, { CMX655_LDCTRL , 0x00 }, { CMX655_RDCTRL , 0x00 }, { CMX655_LEVEL , 0x00 }, { CMX655_NGCTRL , 0x00 }, { CMX655_NGTIME , 0x00 }, { CMX655_NGLSTAT , 0x00 }, { CMX655_NGRSTAT , 0x00 }, { CMX655_PVF , 0x00 }, { CMX655_PREAMP , 0x00 }, { CMX655_VOLUME , 0x00 }, { CMX655_ALCCTRL , 0x00 }, { CMX655_ALCTIME , 0x00 }, { CMX655_ALCGAIN , 0x00 }, { CMX655_ALCSTAT , 0x00 }, { CMX655_DST , 0x00 }, { CMX655_CPR , 0x00 }, { CMX655_SYSCTRL , 0x00 }, { CMX655_COMMAND , 0x00 }, { 0x34 , 0x29 }, { 0x35 , 0x40 }, { 0x36 , 0x80 }, { 0x37 , 0x80 }, }; /* * Define all registers as regmap ranges */ static const struct regmap_range asCmx655ValidReg[] = { { 0x00, 0x0a }, { 0x0c, 0x0f }, { 0x1c, 0x1f }, { 0x28, 0x30 }, { 0x32, 0x33 } }; /* * Define read only as regmap ranges */ static const struct regmap_range asCmx655ReadOnlyReg[] = { { 0x00, 0x00}, { 0x1e, 0x1f}, { 0x2e, 0x2e} }; /* * Define write only as regmap ranges */ static const struct regmap_range asCmx655WriteOnlyReg[] = { {0x33, 0x33} }; /* * Define access table for readable registers * Valid register and not write only */ static const struct regmap_access_table sCmx655ReadableReg = { .yes_ranges = asCmx655ValidReg, .n_yes_ranges = ARRAY_SIZE(asCmx655ValidReg), .no_ranges = asCmx655WriteOnlyReg, .n_no_ranges = ARRAY_SIZE(asCmx655WriteOnlyReg) }; /* * Define access table for writeable registers * Valid register and not read only */ static const struct regmap_access_table sCmx655WriteableReg = { .yes_ranges = asCmx655ValidReg, .n_yes_ranges = ARRAY_SIZE(asCmx655ValidReg), .no_ranges = asCmx655ReadOnlyReg, .n_no_ranges = ARRAY_SIZE(asCmx655ReadOnlyReg) }; /* * Define volatile regs with function */ static bool cmx655VolatileReg(struct device *dev, unsigned int reg) { bool bRet; switch (reg) { case CMX655_COMMAND: case CMX655_SYSCTRL: case CMX655_ISR: case CMX655_ISE: bRet = true; break; default: bRet = false; break; } return bRet; }; static const struct regmap_config sCmx655Regmap = { .reg_bits = 8, .val_bits = 8, .max_register = CMX655_COMMAND, .volatile_reg = cmx655VolatileReg, .wr_table = &sCmx655WriteableReg, .rd_table = &sCmx655ReadableReg, .reg_defaults = asCmx655RegDefaults, .num_reg_defaults = ARRAY_SIZE(asCmx655RegDefaults), .cache_type = REGCACHE_RBTREE, }; /* +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ * End of regmap define * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */ /* +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ * Define some functions used by this module to control the CMX655 * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */ /* * Start CMX655 internal clock and wait for clock ready bit */ static int cmx655StartSysClk(struct snd_soc_component *psComponent) { int iRet; int i; unsigned int uiVal; // Dummy read to clear status bits iRet = SOC_COM_READ(psComponent, CMX655_ISR, &uiVal); // Start clock iRet = snd_soc_component_write(psComponent, CMX655_COMMAND, CMX655_CMD_CLOCK_START); if (iRet < 0) { dev_err(psComponent->dev, "Failed to write start clock command %d\n", iRet); return iRet; } // Wait for status bit for (i=0; i< 100; i++) { iRet = SOC_COM_READ(psComponent, CMX655_ISR, &uiVal); if (uiVal & CMX655_ISR_CLKRDY) { break; } } if (i == 100) { // Clock did not start iRet = -EIO; } return iRet; } static int cmx655StopSysClk(struct snd_soc_component *psComponent) { return snd_soc_component_write(psComponent, CMX655_COMMAND, CMX655_CMD_CLOCK_STOP); } /* * Get the clock setup the system clock based on clock Id, DAI master mode * and sample rate * iClkId - Clock source setting as defined in cmx655.h * iMasterMode - Non-zero if the CMX655 is the DAI master * iSRSetting - Setting for sample rate 0 to 3 * piClkSrc - pointer for storing clock source (PLLREF, PLLSEL and * CLKSEL bits) * piRDiv - pointer for storing PLL's RDIV value (13 bits) * piNDiv - pointer for storing PLL's NDIV value (13 bits) * piPllCtrl - pointer for storing PLLCTRL register value (8 bits) */ static int cmx655GetSysClkConfig(int iClkId, int iMasterMode, int iSRSetting, int *piClkSrc, int *piRDiv, int *piNDiv, int *piPllCtrl) { // Do auto selection if (iClkId == CMX655_SYSCLK_AUTO) { if (iMasterMode != 0) { iClkId = CMX655_SYSCLK_RCLK; } else { iClkId = CMX655_SYSCLK_LRCLK; } } // Set default values *piRDiv = 0; *piNDiv = 0; *piPllCtrl = 0; switch (iClkId) { case (CMX655_SYSCLK_RCLK): *piClkSrc = CMX655_CLKCTRL_CLRSRC_RCLK; break; case (CMX655_SYSCLK_LPO): *piClkSrc = CMX655_CLKCTRL_CLRSRC_LPO; break; case (CMX655_SYSCLK_LRCLK): *piClkSrc = CMX655_CLKCTRL_CLRSRC_LRCLK; *piRDiv = 1; switch (iSRSetting) { case (CMX655_CLKCTRL_SR_8K): *piNDiv = 3072; *piPllCtrl = (0 << CMX655_PLLCTRL_LFILT_SHIFT) || (3 << CMX655_PLLCTRL_CPI_SHIFT); break; case (CMX655_CLKCTRL_SR_16K): *piNDiv = 1536; *piPllCtrl = (0 << CMX655_PLLCTRL_LFILT_SHIFT) || (3 << CMX655_PLLCTRL_CPI_SHIFT); break; case (CMX655_CLKCTRL_SR_32K): *piNDiv = 768; *piPllCtrl = (12<< CMX655_PLLCTRL_LFILT_SHIFT) || (3 << CMX655_PLLCTRL_CPI_SHIFT); break; case (CMX655_CLKCTRL_SR_48K): *piNDiv = 512; *piPllCtrl = (12<< CMX655_PLLCTRL_LFILT_SHIFT) || (3 << CMX655_PLLCTRL_CPI_SHIFT); break; default: return -EINVAL; } break; default: return -EINVAL; } return 0; }; /* * Setup the clock and sample rate. The clock needs to be setup at the same * time as the sample rate encase we are using the serial port as the clock * source. * If the clock source the serial port then the PLL settings are dependent on * the sample rate. */ static int cmx655SetupRate(struct snd_soc_component *psComponent, struct snd_pcm_hw_params *psHwParams) { int iRet; struct cmx655Data *psCmx655Data = snd_soc_component_get_drvdata(psComponent); struct cmx655DaiData *psCmx655DaiData = &psCmx655Data->sDaiData; int iSRate = params_rate(psHwParams); int iMasterMode; int iSRateSetting; int iClkSrc; int iRDiv; int iNDiv; int iPllCtrl; int iSysCtrl; int iVol; iRet = SOC_COM_READ(psComponent, CMX655_SAICTRL, &iMasterMode); iMasterMode = iMasterMode & CMX655_SAI_MSTR; // Workout clock settings // Start with sample rate switch (iSRate) { case 8000: iSRateSetting = CMX655_CLKCTRL_SR_8K; break; case 16000: iSRateSetting = CMX655_CLKCTRL_SR_16K; break; case 32000: iSRateSetting = CMX655_CLKCTRL_SR_32K; break; case 48000: iSRateSetting = CMX655_CLKCTRL_SR_48K; break; default: dev_err(psComponent->dev, "Unsupported rate %d\n", iSRate); return -EINVAL; } iRet = cmx655GetSysClkConfig(psCmx655DaiData->iSysClk, iMasterMode, iSRateSetting, &iClkSrc, &iRDiv, &iNDiv, &iPllCtrl); if (iRet < 0) { dev_err(psComponent->dev, "Failed to get system clock settings %i\n", iRet); } // Check if we are using the LRCLK as the source. if (iClkSrc == CMX655_CLKCTRL_CLRSRC_LRCLK) { dev_dbg(psComponent->dev, "Using LRCLK as clk source.\ Using LPO for setup then switch over to LRCLK later"); // Store correct clock source for later use psCmx655DaiData->iClkSrc = iClkSrc; psCmx655DaiData->bBestClkRunning = false; // Need more setup later iClkSrc = CMX655_CLKCTRL_CLRSRC_LPO; } else { psCmx655DaiData->bBestClkRunning = true; } // Test to see if the clock source and sample rate are correct. // If so we can skip the setup if (snd_soc_component_test_bits(psComponent, CMX655_CLKCTRL, CMX655_CLKCTRL_CLRSRC_MASK | CMX655_CLKCTRL_SR_MASK, iClkSrc | iSRateSetting) == 0) { dev_dbg(psComponent->dev, "Rate Setup correct skipping setup\n"); return 0; } // Turn all inputs and outputs off before disabling clock iRet = SOC_COM_READ(psComponent, CMX655_SYSCTRL, &iSysCtrl); snd_soc_component_update_bits(psComponent, CMX655_SYSCTRL, CMX655_SYSCTRL_MICR | CMX655_SYSCTRL_MICL | CMX655_SYSCTRL_PAMP | CMX655_SYSCTRL_LOUT, 0); cmx655StopSysClk(psComponent); // Set new sample rate and clock source snd_soc_component_update_bits(psComponent, CMX655_CLKCTRL, CMX655_CLKCTRL_CLRSRC_MASK | CMX655_CLKCTRL_SR_MASK, iClkSrc | iSRateSetting); // Set new RDIV snd_soc_component_update_bits(psComponent, CMX655_RDIVHI, 0x1F, iRDiv >> 8); snd_soc_component_update_bits(psComponent, CMX655_RDIVLO, 0xFF, iRDiv & 0xFF); // Set new NDIV snd_soc_component_update_bits(psComponent, CMX655_NDIVHI, 0x1F, iNDiv >> 8); snd_soc_component_update_bits(psComponent, CMX655_NDIVLO, 0xFF, iNDiv & 0xFF); // Set new PLLCTRL snd_soc_component_update_bits(psComponent, CMX655_PLLCTRL, 0xFF, iPllCtrl & 0xFF); // Now we can re-start the clock if ((iRet=cmx655StartSysClk(psComponent))<0) { dev_err(psComponent->dev, "System clock failed to start %i\n", iRet); return iRet; } // Turn anything on that we turned off if ((iSysCtrl & (CMX655_SYSCTRL_MICR | CMX655_SYSCTRL_MICL)) > 0) { // Turn on mic(s) snd_soc_component_update_bits(psComponent, CMX655_SYSCTRL, CMX655_SYSCTRL_MICR | CMX655_SYSCTRL_MICL, iSysCtrl); // Wait for filters to settle if (snd_soc_component_test_bits(psComponent, CMX655_RVF, CMX655_VF_DCBLOCK, CMX655_VF_DCBLOCK) == 0) { // DC blocking filter off, Shorter wait usleep_range(3500,4000); } else { // This allows time for Mics and DC blocking filter to settle msleep(320); } } if ((iSysCtrl & (CMX655_SYSCTRL_PAMP | CMX655_SYSCTRL_LOUT)) > 0) { // Turn output(s) on // Store volume iRet = SOC_COM_READ(psComponent, CMX655_VOLUME, &iVol); // Lower volume with smooth on snd_soc_component_write(psComponent, CMX655_VOLUME, 0x80); snd_soc_component_update_bits(psComponent, CMX655_SYSCTRL, CMX655_SYSCTRL_PAMP | CMX655_SYSCTRL_LOUT, iSysCtrl); // Restore volume snd_soc_component_write(psComponent, CMX655_VOLUME, iVol); } return 0; }; /* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ * End of internal functions * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */ /* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ * Define DAI (Digital Audio Interface) component * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */ /* * Callback to set serial port format */ static int cmx655SetDaiFmt(struct snd_soc_dai *psDai, unsigned int uiFmt) { struct snd_soc_component *psComponent = psDai->component; unsigned int uiRegVal = 0; // Set master bit switch (uiFmt & SND_SOC_DAIFMT_MASTER_MASK) { case SND_SOC_DAIFMT_CBM_CFM: uiRegVal = uiRegVal | CMX655_SAI_MSTR; break; case SND_SOC_DAIFMT_CBS_CFS: // Could or in 0 but no need break; default: dev_err(psComponent->dev, "Unsupported digital audio interface master mode\n"); return -EINVAL; } // Set data format switch (uiFmt & SND_SOC_DAIFMT_FORMAT_MASK) { case SND_SOC_DAIFMT_I2S: uiRegVal = uiRegVal | CMX655_SAI_DLY | CMX655_SAI_POL; break; case SND_SOC_DAIFMT_LEFT_J: // Could or in 0 but no need break; default: dev_err(psComponent->dev, "Unsupported digital audio interface data format\n"); return -EINVAL; } // Change invert bits if required switch (uiFmt & SND_SOC_DAIFMT_INV_MASK) { case SND_SOC_DAIFMT_NB_NF: // No inverts do nothing break; case SND_SOC_DAIFMT_NB_IF: uiRegVal = uiRegVal ^ CMX655_SAI_POL; break; case SND_SOC_DAIFMT_IB_NF: uiRegVal = uiRegVal | CMX655_SAI_BINV; break; case SND_SOC_DAIFMT_IB_IF: uiRegVal = (uiRegVal | CMX655_SAI_BINV) ^ CMX655_SAI_POL; break; default: dev_err(psComponent->dev, "Unknown digital audio interface polarity\n"); return -EINVAL; } // Write value to codec snd_soc_component_write(psComponent, CMX655_SAICTRL, uiRegVal); return 0; } /* * Save and check requested ClkId is valid. * Clock is setup as part of hw params */ static int cmx655SetDaiSysclk(struct snd_soc_dai *psDai, int iClkId, unsigned int uiFreq, int iDir) { struct cmx655Data *psCmx655Data = snd_soc_component_get_drvdata(psDai->component); struct cmx655DaiData *psCmx655DaiData = &psCmx655Data->sDaiData; switch (iClkId) { case CMX655_SYSCLK_MIN ... CMX655_SYSCLK_MAX: break; default: return -EINVAL; } psCmx655DaiData->iSysClk = iClkId; return 0; }; /* * Callback to prepare, if running from the LRCLK we will need to * swap to it here. * Cannot do it in hw_params as the CPU's port was not setup */ static int cmx655DaiPrepare(struct snd_pcm_substream *psStream, struct snd_soc_dai *psDai) { int iRet = 0; struct snd_soc_component *psComponent = psDai->component; struct cmx655Data *psCmx655Data = snd_soc_component_get_drvdata(psComponent); struct cmx655DaiData *psCmx655DaiData = &psCmx655Data->sDaiData; if (!psCmx655DaiData->bBestClkRunning) { // Stop the clock change over to the correct one an start it again if ((iRet = cmx655StopSysClk(psComponent)) < 0) { dev_err(psComponent->dev, "Failed to stop clock %d\n", iRet); goto GetOut; } iRet = snd_soc_component_update_bits(psComponent, CMX655_CLKCTRL, CMX655_CLKCTRL_CLRSRC_MASK, psCmx655DaiData->iClkSrc); if (iRet < 0) { dev_err(psComponent->dev, "Failed to set new clock setup %d\n", iRet); goto GetOut; } if ((iRet = cmx655StartSysClk(psComponent)) < 0) { dev_warn(psComponent->dev, "Failed to restart clock\n"); iRet = 0; // This will happen if the CPU driver does not start the LRCLK // until the last point. // For now we will assume the clock will start } psCmx655DaiData->bBestClkRunning = true; } GetOut: return iRet; }; /* * Callback to setup codec params */ static int cmx655HwParams(struct snd_pcm_substream *psStream, struct snd_pcm_hw_params *psHwParams, struct snd_soc_dai *psDai) { int iRet; struct snd_soc_component *psComponent = psDai->component; struct i2c_client *psI2c = to_i2c_client(psComponent->dev); struct cmx655Data *psCmx655Data = snd_soc_component_get_drvdata(psComponent); struct cmx655DaiData *psCmx655DaiData = &psCmx655Data->sDaiData; unsigned int uiEnabledStreams = psCmx655DaiData->uiEnabledStreams; if (psCmx655DaiData->bBestClkRunning) { // Will get here if the clock is in use so don't go stopping it dev_dbg(psComponent->dev, "Clock running. Skipping setup\n"); } else { // Setup clock and sample rate if ((iRet = cmx655SetupRate(psComponent, psHwParams)) < 0) { dev_err(psComponent->dev, "Failed to set rates %d\n", iRet); return iRet; } } // Set mono bit based on channel count if (params_channels(psHwParams) == 1) { dev_dbg(psComponent->dev, "Switching into mono mode\n"); snd_soc_component_update_bits(psComponent, CMX655_SAICTRL, CMX655_SAI_MONO, CMX655_SAI_MONO); }else { snd_soc_component_update_bits(psComponent, CMX655_SAICTRL, CMX655_SAI_MONO, 0); } if (psI2c->irq) { psCmx655Data->uiOcCnt = 0; // Reset overcurrent count } if (uiEnabledStreams == 0){ dev_dbg(psComponent->dev, "First stream to enable, enabling SAI\n"); // If first stream to be enabled // Enable SAI (serial audio interface) port // We need it running before the platform starts. // to avoid I2S sync errors snd_soc_component_update_bits(psComponent, CMX655_SYSCTRL, CMX655_SYSCTRL_SAI, CMX655_SYSCTRL_SAI); } else { dev_dbg(psComponent->dev, "Not first stream to enable, skipping SAI enable\n"); } // Inc enabled streams by 1 psCmx655DaiData->uiEnabledStreams = uiEnabledStreams + 1; return iRet; } /* * Shutdown DAI link */ static void cmx655DaiShutdown(struct snd_pcm_substream *psStream, struct snd_soc_dai *psDai) { struct snd_soc_component *psComponent = psDai->component; struct cmx655Data *psCmx655Data = snd_soc_component_get_drvdata(psComponent); struct cmx655DaiData *psCmx655DaiData = &psCmx655Data->sDaiData; unsigned int uiEnabledStreams = psCmx655DaiData->uiEnabledStreams; if (uiEnabledStreams == 0) { // Protect against shutdown getting called without a start. // This was seen with audacity dev_dbg(psComponent->dev, "Shutdown called when SAI not running\n"); return; } // Reduce enabled streams by 1 uiEnabledStreams = uiEnabledStreams - 1; psCmx655DaiData->uiEnabledStreams = uiEnabledStreams; if (uiEnabledStreams == 0) { dev_dbg(psComponent->dev, "Last stream to disable, disabling SAI\n"); // If no streams left // Disable SAI port snd_soc_component_update_bits(psComponent, CMX655_SYSCTRL, CMX655_SYSCTRL_SAI, 0); // Setup the clock again next time arounf psCmx655DaiData->bBestClkRunning = false; } else { dev_dbg(psComponent->dev, "Not last stream to disable, skipping SAI disable\n"); } } /* * Define CMX655's DAI operations */ static const struct snd_soc_dai_ops sCmx655DaiOps = { .set_sysclk = cmx655SetDaiSysclk, .set_fmt = cmx655SetDaiFmt, .prepare = cmx655DaiPrepare, .hw_params = cmx655HwParams, .shutdown = cmx655DaiShutdown, }; /* * Define CMX655's DAI driver */ static struct snd_soc_dai_driver sCmx655Dai = { .name = "cmx655", .playback = { .stream_name = "CMX655 Playback", .channels_min = 1, .channels_max = 2, .rates = CMX655_RATES, .formats = CMX655_FMTS, }, .capture = { .stream_name = "CMX655 Record", .channels_min = 1, .channels_max = 2, .rates = CMX655_RATES, .formats = CMX655_FMTS, }, .ops = &sCmx655DaiOps, .symmetric_rates = 1 }; /* +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ * End of DAI component * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */ /* +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ * Define component/codec driver * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */ /* * CMX655 IRQ handler thread (runs with interrups enable) * Read status register and take required action */ static irqreturn_t cmx655IrqThread(int iIrq, void *pData) { struct snd_soc_component *psComponent = pData; struct cmx655Data *psCmx655Data = snd_soc_component_get_drvdata(psComponent); unsigned int uiStatus; int iRet; iRet = SOC_COM_READ(psComponent, CMX655_ISR, &uiStatus); if (uiStatus == 0) { // Event was not trigged by CMX655 so let the higher level know return IRQ_NONE; } // Thermal protection event ++++++++++++++++++++++++++++++++++++++++++++++++ if (uiStatus & CMX655_ISR_THERM) { // Do not reset CMX655 codec on over temperature event dev_err(psComponent->dev, "CMX655 class-D over temperature detected\n"); } // Over current event +++++++++++++++++++++++++++++++++++++++++++++++++++++ if (uiStatus & CMX655_ISR_AMPOC) { dev_warn(psComponent->dev, "CMX655 class-D over current detected\n"); if (psCmx655Data->uiOcCnt < psCmx655Data->uiOcCntMax) { // Re enable class-D snd_soc_component_update_bits(psComponent, CMX655_SYSCTRL, CMX655_SYSCTRL_PAMP, CMX655_SYSCTRL_PAMP); if (psCmx655Data->uiOcCntMax <= 10000) { // If overcurrent retries not set to > 10000 keep track of number // of restarts psCmx655Data->uiOcCnt = psCmx655Data->uiOcCnt + 1; } } else { // Re enable count reached, do not try again dev_err(psComponent->dev, "Class-D over current restart attempts exceeded\n"); } } // End of status bit handling ++++++++++++++++++++++++++++++++++++++++++++++ return IRQ_HANDLED; } /* * Method to initailise CMX655 component */ int cmx655ComponentProbe(struct snd_soc_codec *psComponent) { int iRet; struct i2c_client *psI2c = to_i2c_client(psComponent->dev); struct cmx655Data *psCmx655Data = snd_soc_codec_get_drvdata(psComponent); // We need the clock running to write to most of the registers // so lets start that now dev_err(psComponent->dev, "entered CMX655 component probe\n"); if ((iRet = cmx655StartSysClk(&psComponent->component)) < 0) { dev_err(psComponent->dev, "Failed to start system clock %d\n", iRet); goto CodecErr; } psCmx655Data->uiOcCnt = 0; // Set overcurrent count // Enable interrupt if supplied if (psI2c->irq) { iRet = request_threaded_irq(psI2c->irq, NULL, cmx655IrqThread, IRQF_ONESHOT, "cmx655", psComponent); if (iRet < 0) { dev_err(psComponent->dev, "Failed to setup interrupt %d\n", iRet); goto InterruptErr; } // Setup interrupts on cmx655 snd_soc_write(psComponent, CMX655_ISM, (CMX655_ISM_AMPOC | CMX655_ISM_THERM)); } return 0; InterruptErr: CodecErr: return iRet; } /* * Method to tidy up when codec driver removed */ static int cmx655ComponentRemove(struct snd_soc_codec *psComponent) { struct i2c_client *psI2c = to_i2c_client(psComponent->dev); // disable interrupts if used if (psI2c->irq) { // Disable interrupts on cmx655 snd_soc_write(psComponent, CMX655_ISM, 0); // Free interrupt handler free_irq(psI2c->irq, psComponent); } return 0; } /* +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ * Define ALSA SoC controls */ /* * Define required TLV (type-length-value) ranges */ static const DECLARE_TLV_DB_SCALE(auiCmx655Level, -1200, 100, 0); static const DECLARE_TLV_DB_SCALE(auiCmx655NGThresh,-6300, 100, 0); static const DECLARE_TLV_DB_SCALE(auiCmx655Vol, -9100, 100, 1); static const DECLARE_TLV_DB_SCALE(auiCmx655PreAmp, 0, 600, 0); static const DECLARE_TLV_DB_SCALE(auiCmx655DstGain, -6200, 200, 0); static const DECLARE_TLV_DB_SCALE(auiCmx655ALCGain, 0, 100, 0); static const DECLARE_TLV_DB_SCALE(auiCmx655ALCThresh,-3100,100, 0); /* * Define Enums */ static const char *aszCmx655NGRatioText[] = { "1:2", "1:3", "1:4" }; static SOC_ENUM_SINGLE_DECL(sCmx655NGRatioEnum, CMX655_NGCTRL, 5, aszCmx655NGRatioText); static const char *aszCmx655NGAttackText[] = { "1.5ms", "3ms", "4.5ms", "6ms", "12ms", "24ms", "48ms", "96ms" }; static SOC_ENUM_SINGLE_DECL(sCmx655NGAttackEnum, CMX655_NGTIME, 4, aszCmx655NGAttackText); static const char *aszCmx655NGReleaseText[] = { "0.06s", "0.12s", "0.24s", "0.48s", "0.96s", "1.92s", "3.84s", "7.68s" }; static SOC_ENUM_SINGLE_DECL(sCmx655NGReleaseEnum, CMX655_NGTIME, 0, aszCmx655NGReleaseText); static const char *aszCmx655HPFText[] = { "Disabled", "SRate/320", "SRate/53.3", "SRate/26.7" }; static SOC_ENUM_SINGLE_DECL(sCmx655HPFCaptureEnum, CMX655_RVF, 0, aszCmx655HPFText); static SOC_ENUM_SINGLE_DECL(sCmx655HPFPlaybackEnum, CMX655_PVF, 0, aszCmx655HPFText); static const char *aszCmx655CompandingText[] = { "u-Law", "a-Law" }; static SOC_ENUM_SINGLE_DECL(sCmx655CompandingEnum, CMX655_SAIMUX, 5, aszCmx655CompandingText); static const char *aszCmx655ALCRatioText[] = { "1.5:1", "2:1", "4:1", "Inf:1" }; static SOC_ENUM_SINGLE_DECL(sCmx655ALCRatioEnum, CMX655_ALCCTRL, 5, aszCmx655ALCRatioText); // Note: The attack and release times are the same as the Noise gate's. static SOC_ENUM_SINGLE_DECL(sCmx655ALCAttackEnum, CMX655_ALCTIME, 4, aszCmx655NGAttackText); static SOC_ENUM_SINGLE_DECL(sCmx655ALCReleaseEnum, CMX655_ALCTIME, 0, aszCmx655NGReleaseText); /* * Define controls for codec/component */ static const struct snd_kcontrol_new asCmx655SndControls[] = { // Capture ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ SOC_DOUBLE_TLV("Master Capture Volume", CMX655_LEVEL, 4, 0, 15, 0, auiCmx655Level), SOC_SINGLE("DC_Block Capture Switch", CMX655_RVF, CMX655_VF_DCBLOCK_SHIFT, 1, 0), SOC_SINGLE("LPF Capture Switch", CMX655_RVF, 3, 1, 0), SOC_ENUM("Cap_HPF Capture Switch", sCmx655HPFCaptureEnum), // Noise gate SOC_SINGLE("Noise_Gate Capture Switch", CMX655_NGCTRL, 7, 1, 0), SOC_SINGLE_TLV("NG_Threshold Capture Volume",CMX655_NGCTRL, 0, 31, 0, auiCmx655NGThresh), SOC_ENUM("NG_Ratio Capture Switch", sCmx655NGRatioEnum), SOC_ENUM("NG_Attack Capture Switch", sCmx655NGAttackEnum), SOC_ENUM("NG_Release Capture Switch", sCmx655NGReleaseEnum), // Playback +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ SOC_SINGLE_TLV("Master Playback Volume", CMX655_VOLUME, 0, 91, 0, auiCmx655Vol), SOC_SINGLE_TLV("Pre_Amp Playback Volume", CMX655_PREAMP, 0, 3, 0, auiCmx655PreAmp), SOC_SINGLE("Smooth Playback Switch", CMX655_VOLUME, 7, 0x01,0), SOC_SINGLE("DC_Block Playback Switch", CMX655_PVF, CMX655_VF_DCBLOCK_SHIFT, 1, 0), SOC_SINGLE("LPF Playback Switch", CMX655_PVF, 3, 1, 0), SOC_ENUM("Play_HPF Playback Switch", sCmx655HPFPlaybackEnum), SOC_SINGLE("Soft_Mute Playback Switch", CMX655_CPR, 0, 1, 0), SOC_SINGLE("ALC Playback Switch", CMX655_ALCCTRL, 7, 1, 0), SOC_ENUM("ALC_Ratio Playback Switch", sCmx655ALCRatioEnum), SOC_SINGLE_TLV("ALC_Threshold Playback Volume", CMX655_ALCCTRL, 0, 31, 0, auiCmx655ALCThresh), SOC_SINGLE_TLV("ALC_Gain Playback Volume", CMX655_ALCGAIN, 0, 12, 0, auiCmx655ALCGain), SOC_ENUM("ALC_Attack Playback Switch", sCmx655ALCAttackEnum), SOC_ENUM("ALC_Release Playback Switch", sCmx655ALCReleaseEnum), // Digital Sidetone +++++++++++++++++++++++++++++++++++++++++++++++++++++++ SOC_SINGLE_TLV("Sidetone Playback Volume", CMX655_DST, 0, 31, 0, auiCmx655DstGain), // Companding +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ SOC_SINGLE("Companding_En Switch", CMX655_SAIMUX, 4, 1, 0), SOC_ENUM("Companding_Type Switch", sCmx655CompandingEnum), }; /* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ * Define dynamic audio power management (DAPM) widget, controls and routes */ /* * Define Enums */ static const char *aszCmx655MicMuxText[] = { "Normal", "Swapped", "Left only", "Right only" }; static SOC_ENUM_SINGLE_DECL(sCmx655MicMuxEnum, CMX655_SAIMUX, 0, aszCmx655MicMuxText); static const char *aszCmx655AmpMuxText[] = { "Left", "Right", "Mean" }; static SOC_ENUM_SINGLE_DECL(sCmx655AmpMuxEnum, CMX655_SAIMUX, 2, aszCmx655AmpMuxText); static const char *aszCmx655DigitalSidetoneText[] = { "Left", "Right", "Mean" }; static SOC_ENUM_SINGLE_DECL(sCmx655SidetoneEnum, CMX655_DST, 5, aszCmx655DigitalSidetoneText); /* * Define controls for DAPM */ static const struct snd_kcontrol_new sCmx655MicMux = SOC_DAPM_ENUM("Cap_SAI Capture Route", sCmx655MicMuxEnum); static const struct snd_kcontrol_new sCmx655AmpMux = SOC_DAPM_ENUM("Play_SAI Playback Route", sCmx655AmpMuxEnum); static const struct snd_kcontrol_new asCmx655SpkrEn[] = { SOC_DAPM_SINGLE_VIRT("Playback Switch", 1) }; static const struct snd_kcontrol_new asCmx655LOutEn[] = { SOC_DAPM_SINGLE_VIRT("Playback Switch", 1) }; static const struct snd_kcontrol_new sCmx655SidetoneMux = SOC_DAPM_ENUM("DST Route", sCmx655SidetoneEnum); static const struct snd_kcontrol_new asCmx655DSTEn[] = { SOC_DAPM_SINGLE_VIRT("Playback Switch", 1) }; /* * Define DAPM custom events */ /* * Special event triggered after mics are enabled. * Wait for a bit to allow the MIC filters time to settle */ static int cmx655MicDapmEvent(struct snd_soc_dapm_widget *psWidget, struct snd_kcontrol *psControl, int iEvent) { struct snd_soc_component *psComponent = snd_soc_dapm_to_component(psWidget->dapm); int iRegVal, iRet; switch (iEvent) { case (SND_SOC_DAPM_POST_PMU): // After turn on give MIC filters time // Time can be shorter if the DC blocking filter is not enabled iRet = SOC_COM_READ(psComponent, CMX655_RVF, &iRegVal); if ((iRegVal && CMX655_VF_DCBLOCK) > 0) { // This allows time for Mics and DC blocking filter to settle msleep(320); } else { usleep_range(3500,4000); } break; default: break; } return 0; } /* * Define DAPM widgets for codec/component */ static const struct snd_soc_dapm_widget asCmx655DapmWidgets[] = { // Input path widgets ++++++++++++++++++++++++++++++++++++++++++++++++++++++ SND_SOC_DAPM_INPUT("MICL"), SND_SOC_DAPM_INPUT("MICR"), // Custom widgets for Mics to get them to turn on before switches { .id = snd_soc_dapm_mic, .name = "Left Mic", .kcontrol_news = NULL, .num_kcontrols = 0, .reg = CMX655_SYSCTRL, .shift = 1, .mask = 1, .on_val = 1, .off_val = 0, .event = cmx655MicDapmEvent, .event_flags = SND_SOC_DAPM_POST_PMU }, { .id = snd_soc_dapm_mic, .name = "Right Mic", .kcontrol_news = NULL, .num_kcontrols = 0, .reg = CMX655_SYSCTRL, .shift = 0, .mask = 1, .on_val = 1, .off_val = 0, .event = cmx655MicDapmEvent, .event_flags = SND_SOC_DAPM_POST_PMU }, SND_SOC_DAPM_MUX("SAI_L Capture Mux", SND_SOC_NOPM, 0, 0, &sCmx655MicMux), SND_SOC_DAPM_MUX("SAI_R Capture Mux", SND_SOC_NOPM, 0, 0, &sCmx655MicMux), // Note: SAI enable is controlled by DAI's HWparams and shutdown. Using // DAPM control resulted in I2S sync errors on the platform driver SND_SOC_DAPM_AIF_OUT("SAI Left Out", "CMX655 Record", 0, SND_SOC_NOPM, 0, 0), SND_SOC_DAPM_AIF_OUT("SAI Right Out","CMX655 Record", 1, SND_SOC_NOPM, 0, 0), // Output path widgets +++++++++++++++++++++++++++++++++++++++++++++++++++++ SND_SOC_DAPM_AIF_IN("SAI Left In", "CMX655 Playback", 0, SND_SOC_NOPM, 0, 0), SND_SOC_DAPM_AIF_IN("SAI Right In","CMX655 Playback", 1, SND_SOC_NOPM, 0, 0), SND_SOC_DAPM_MUX("Play_SAI Playback Route", SND_SOC_NOPM, 0, 0, &sCmx655AmpMux), SND_SOC_DAPM_DAC("Power Amp", "CMX655 Playback",CMX655_SYSCTRL, 3, 0), SND_SOC_DAPM_DAC("Line Out", "CMX655 Playback", CMX655_SYSCTRL, 4, 0), SND_SOC_DAPM_SWITCH("SPKR_EN", SND_SOC_NOPM, 0, 0, asCmx655SpkrEn), SND_SOC_DAPM_SWITCH("LOUT_EN", SND_SOC_NOPM, 0, 0, asCmx655LOutEn), SND_SOC_DAPM_OUTPUT("SPKR"), SND_SOC_DAPM_OUTPUT("LOUT"), // Digital side tone widgets +++++++++++++++++++++++++++++++++++++++++++++++ SND_SOC_DAPM_SWITCH("DST_EN", SND_SOC_NOPM, 0, 0, asCmx655DSTEn), SND_SOC_DAPM_MUX("DST", CMX655_DST, 7, 0, &sCmx655SidetoneMux), }; /* * Define dynamic audio power management routes for codec/component */ static const struct snd_soc_dapm_route asCmx655DapmRoutes[] = { // Main output path {"SPKR", NULL, "SPKR_EN"}, {"LOUT", NULL, "LOUT_EN"}, {"SPKR_EN", "Playback Switch", "Power Amp"}, {"LOUT_EN", "Playback Switch", "Line Out"}, {"Power Amp", NULL, "Play_SAI Playback Route"}, {"Line Out", NULL, "Play_SAI Playback Route"}, {"Play_SAI Playback Route", "Left", "SAI Left In"}, {"Play_SAI Playback Route", "Right", "SAI Right In"}, {"Play_SAI Playback Route", "Mean", "SAI Left In"}, {"Play_SAI Playback Route", "Mean", "SAI Right In"}, // Main input path {"SAI Right Out", NULL, "SAI_R Capture Mux"}, {"SAI Left Out", NULL, "SAI_L Capture Mux"}, {"SAI_L Capture Mux", "Normal", "Left Mic"}, {"SAI_R Capture Mux", "Normal", "Right Mic"}, {"SAI_L Capture Mux", "Swapped", "Right Mic"}, {"SAI_R Capture Mux", "Swapped", "Left Mic"}, {"SAI_L Capture Mux", "Left only", "Left Mic"}, {"SAI_R Capture Mux", "Left only", "Left Mic"}, {"SAI_L Capture Mux", "Right only", "Right Mic"}, {"SAI_R Capture Mux", "Right only", "Right Mic"}, {"Right Mic", NULL, "MICR"}, {"Left Mic", NULL, "MICL"}, // Digital side tone {"DST", "Left", "Left Mic"}, {"DST", "Right", "Right Mic"}, {"DST", "Mean", "Left Mic"}, {"DST", "Mean", "Right Mic"}, {"DST_EN", "Playback Switch", "DST"}, {"Power Amp", NULL, "DST_EN"}, {"Line Out", NULL, "DST_EN"}, }; /* * Create component driver structure */ static const struct snd_soc_codec_driver sCmx655_codec = { .probe = cmx655ComponentProbe, .remove = cmx655ComponentRemove, .component_driver = { .controls = asCmx655SndControls, .num_controls = ARRAY_SIZE(asCmx655SndControls), .dapm_widgets = asCmx655DapmWidgets, .num_dapm_widgets = ARRAY_SIZE(asCmx655DapmWidgets), .dapm_routes = asCmx655DapmRoutes, .num_dapm_routes = ARRAY_SIZE(asCmx655DapmRoutes), }, }; /* static const struct snd_soc_component_driver sCmx655component = { .controls = asCmx655SndControls, .num_controls = ARRAY_SIZE(asCmx655SndControls), .dapm_widgets = asCmx655DapmWidgets, .num_dapm_widgets = ARRAY_SIZE(asCmx655DapmWidgets), .dapm_routes = asCmx655DapmRoutes, .num_dapm_routes = ARRAY_SIZE(asCmx655DapmRoutes), .probe = cmx655ComponentProbe, .remove = cmx655ComponentRemove }; */ /* +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ * End of component/codec driver * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */ /* +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ * Define I2C driver * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */ /* * Read custom setting (not handled by I2C) from device tree */ static int cmx655ParseDataFromOf(const struct device_node *psDeviceNode, struct cmx655Data *psCmx655Data) { int iRet; unsigned int uiVal; iRet = of_property_read_u32(psDeviceNode, "cmx655,classd-oc-reset-attempts", &uiVal); if (iRet >= 0) { psCmx655Data->uiOcCntMax = uiVal; } else { psCmx655Data->uiOcCntMax = 5; } return 0; } /* * I2C device bind stage used by i2c driver */ static int cmx655I2cProbe(struct i2c_client *psClient, const struct i2c_device_id *device_id) { int iRet; struct cmx655Data *psCmx655Data; struct cmx655DaiData *psCmx655DaiData = dev_get_platdata(&psClient->dev); // Init CMX655 data area dev_err(&psClient->dev, "cmx655I2cProbe entered\n"); psCmx655Data = devm_kzalloc(&psClient->dev, sizeof(*psCmx655Data), GFP_KERNEL); if (!psCmx655Data) { return -ENOMEM; } psCmx655Data->psRegmap = devm_regmap_init_i2c(psClient, &sCmx655Regmap); if (IS_ERR(psCmx655Data->psRegmap)) { dev_err(&psClient->dev, "failed regmap\n"); return PTR_ERR(psCmx655Data->psRegmap); } // Use existing DAI data if found if (psCmx655DaiData) { memcpy(&psCmx655Data->sDaiData, psCmx655DaiData, sizeof(*psCmx655DaiData)); } // Set-up value following the codec reset that will happen in a bit psCmx655Data->sDaiData.uiEnabledStreams = 0; psCmx655Data->sDaiData.bBestClkRunning = false; // Extract data from Device tree if (psClient->dev.of_node) { iRet = cmx655ParseDataFromOf(psClient->dev.of_node, psCmx655Data); if (iRet < 0) { dev_err(&psClient->dev, "Failed to extract data from device tree%d\n", iRet); return iRet; } } // Find gpios // Reset // Find and set low psCmx655Data->psResetGpio = devm_gpiod_get_optional(&psClient->dev, "reset", GPIOD_OUT_LOW); // Reset codec if (psCmx655Data->psResetGpio) { // Hold reset line gpiod_set_value_cansleep(psCmx655Data->psResetGpio, 1); // Time of reset pulse must be greater than 1us // sleep for 10us to 1ms, speed is not critical here usleep_range(10,1000); // release reset line gpiod_set_value_cansleep(psCmx655Data->psResetGpio, 0); }else { dev_err(&psClient->dev, "No reset GPIO, using reset command\n"); regmap_write(psCmx655Data->psRegmap, CMX655_COMMAND, CMX655_CMD_SOFT_RESET); } i2c_set_clientdata(psClient, psCmx655Data); // Register codec component /* iRet = devm_snd_soc_register_component(&psClient->dev, */ /* &sCmx655component, */ /* &sCmx655Dai, 1); */ iRet = snd_soc_register_codec(&psClient->dev, &sCmx655_codec, &sCmx655Dai, 1); dev_err(&psClient->dev, "devm_snd_soc_register_codec returns %d\n", iRet); if (iRet < 0) { dev_err(&psClient->dev, "%s: Register component failed %d\n", __func__, iRet); } dev_err(&psClient->dev, "cmx655I2cProbe returns %d\n", iRet); return iRet; }; /* * I2C device removal stage used by i2c driver */ static int cmx655I2cRemove(struct i2c_client *psClient) { struct cmx655Data *psCmx655Data = i2c_get_clientdata(psClient); // put codec into reset in GPIO given gpiod_set_value_cansleep(psCmx655Data->psResetGpio, 1); // unregister codec snd_soc_unregister_component(&psClient->dev); return 0; }; static const struct i2c_device_id asCmx655DeviceId[] = { { "cmx655", 0 }, { } }; MODULE_DEVICE_TABLE(i2c, asCmx655DeviceId); /* * Define Open Firmware (OF) match table. Supportr for device tree */ static const struct of_device_id asCmx655OfMatch[] = { { .compatible = "cml,cmx655D" }, { } }; MODULE_DEVICE_TABLE(of, asCmx655OfMatch); /* * Define i2c driver stuct */ static struct i2c_driver sCmx655I2cDriver = { .probe = cmx655I2cProbe, .remove = cmx655I2cRemove, .driver = { .name = "cmx655", .of_match_table = asCmx655OfMatch, }, .id_table = asCmx655DeviceId }; module_i2c_driver(sCmx655I2cDriver); /* +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ * End if I2C define * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */ /* +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ * Module info * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */ MODULE_DESCRIPTION("ASoC CMX655 driver"); MODULE_AUTHOR("CML"); MODULE_LICENSE("GPL");