| // SPDX-License-Identifier: GPL-2.0 |
| // |
| // Common functions for loongson I2S controller driver |
| // |
| // Copyright (C) 2023 Loongson Technology Corporation Limited. |
| // Author: Yingkun Meng <mengyingkun@loongson.cn> |
| // |
| |
| #include <linux/module.h> |
| #include <linux/platform_device.h> |
| #include <linux/delay.h> |
| #include <linux/pm_runtime.h> |
| #include <linux/dma-mapping.h> |
| #include <sound/soc.h> |
| #include <linux/regmap.h> |
| #include <sound/pcm_params.h> |
| #include "loongson_i2s.h" |
| |
| #define LOONGSON_I2S_FORMATS (SNDRV_PCM_FMTBIT_S8 | \ |
| SNDRV_PCM_FMTBIT_S16_LE | \ |
| SNDRV_PCM_FMTBIT_S20_3LE | \ |
| SNDRV_PCM_FMTBIT_S24_LE) |
| |
| #define LOONGSON_I2S_TX_ENABLE (I2S_CTRL_TX_EN | I2S_CTRL_TX_DMA_EN) |
| #define LOONGSON_I2S_RX_ENABLE (I2S_CTRL_RX_EN | I2S_CTRL_RX_DMA_EN) |
| |
| #define LOONGSON_I2S_DEF_DELAY 10 |
| #define LOONGSON_I2S_DEF_TIMEOUT 500000 |
| |
| static int loongson_i2s_trigger(struct snd_pcm_substream *substream, int cmd, |
| struct snd_soc_dai *dai) |
| { |
| struct loongson_i2s *i2s = snd_soc_dai_get_drvdata(dai); |
| unsigned int mask; |
| int ret = 0; |
| |
| switch (cmd) { |
| case SNDRV_PCM_TRIGGER_START: |
| case SNDRV_PCM_TRIGGER_RESUME: |
| case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: |
| mask = (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) ? |
| LOONGSON_I2S_TX_ENABLE : LOONGSON_I2S_RX_ENABLE; |
| regmap_update_bits(i2s->regmap, LS_I2S_CTRL, mask, mask); |
| break; |
| case SNDRV_PCM_TRIGGER_STOP: |
| case SNDRV_PCM_TRIGGER_SUSPEND: |
| case SNDRV_PCM_TRIGGER_PAUSE_PUSH: |
| mask = (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) ? |
| LOONGSON_I2S_TX_ENABLE : LOONGSON_I2S_RX_ENABLE; |
| regmap_update_bits(i2s->regmap, LS_I2S_CTRL, mask, 0); |
| break; |
| default: |
| ret = -EINVAL; |
| } |
| |
| return ret; |
| } |
| |
| static int loongson_i2s_hw_params(struct snd_pcm_substream *substream, |
| struct snd_pcm_hw_params *params, |
| struct snd_soc_dai *dai) |
| { |
| struct loongson_i2s *i2s = snd_soc_dai_get_drvdata(dai); |
| u32 clk_rate = i2s->clk_rate; |
| u32 sysclk = i2s->sysclk; |
| u32 bits = params_width(params); |
| u32 chans = params_channels(params); |
| u32 fs = params_rate(params); |
| u32 bclk_ratio, mclk_ratio; |
| u32 mclk_ratio_frac; |
| u32 val = 0; |
| |
| switch (i2s->rev_id) { |
| case 0: |
| bclk_ratio = DIV_ROUND_CLOSEST(clk_rate, |
| (bits * chans * fs * 2)) - 1; |
| mclk_ratio = DIV_ROUND_CLOSEST(clk_rate, (sysclk * 2)) - 1; |
| |
| /* According to 2k1000LA user manual, set bits == depth */ |
| val |= (bits << 24); |
| val |= (bits << 16); |
| val |= (bclk_ratio << 8); |
| val |= mclk_ratio; |
| regmap_write(i2s->regmap, LS_I2S_CFG, val); |
| |
| break; |
| case 1: |
| bclk_ratio = DIV_ROUND_CLOSEST(sysclk, |
| (bits * chans * fs * 2)) - 1; |
| mclk_ratio = clk_rate / sysclk; |
| mclk_ratio_frac = DIV_ROUND_CLOSEST_ULL(((u64)clk_rate << 16), |
| sysclk) - (mclk_ratio << 16); |
| |
| regmap_read(i2s->regmap, LS_I2S_CFG, &val); |
| val |= (bits << 24); |
| val |= (bclk_ratio << 8); |
| if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) |
| val |= (bits << 16); |
| else |
| val |= bits; |
| regmap_write(i2s->regmap, LS_I2S_CFG, val); |
| |
| val = (mclk_ratio_frac << 16) | mclk_ratio; |
| regmap_write(i2s->regmap, LS_I2S_CFG1, val); |
| |
| break; |
| default: |
| dev_err(i2s->dev, "I2S revision invalid\n"); |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| static int loongson_i2s_set_dai_sysclk(struct snd_soc_dai *dai, int clk_id, |
| unsigned int freq, int dir) |
| { |
| struct loongson_i2s *i2s = snd_soc_dai_get_drvdata(dai); |
| |
| i2s->sysclk = freq; |
| |
| return 0; |
| } |
| |
| static int loongson_i2s_enable_mclk(struct loongson_i2s *i2s) |
| { |
| u32 val; |
| |
| if (i2s->rev_id == 0) |
| return 0; |
| |
| regmap_update_bits(i2s->regmap, LS_I2S_CTRL, |
| I2S_CTRL_MCLK_EN, I2S_CTRL_MCLK_EN); |
| |
| return regmap_read_poll_timeout_atomic(i2s->regmap, |
| LS_I2S_CTRL, val, |
| val & I2S_CTRL_MCLK_READY, |
| LOONGSON_I2S_DEF_DELAY, |
| LOONGSON_I2S_DEF_TIMEOUT); |
| } |
| |
| static int loongson_i2s_enable_bclk(struct loongson_i2s *i2s) |
| { |
| u32 val; |
| |
| if (i2s->rev_id == 0) |
| return 0; |
| |
| return regmap_read_poll_timeout_atomic(i2s->regmap, |
| LS_I2S_CTRL, val, |
| val & I2S_CTRL_CLK_READY, |
| LOONGSON_I2S_DEF_DELAY, |
| LOONGSON_I2S_DEF_TIMEOUT); |
| } |
| |
| static int loongson_i2s_set_fmt(struct snd_soc_dai *dai, unsigned int fmt) |
| { |
| struct loongson_i2s *i2s = snd_soc_dai_get_drvdata(dai); |
| int ret; |
| |
| switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) { |
| case SND_SOC_DAIFMT_I2S: |
| break; |
| case SND_SOC_DAIFMT_RIGHT_J: |
| regmap_update_bits(i2s->regmap, LS_I2S_CTRL, I2S_CTRL_MSB, |
| I2S_CTRL_MSB); |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| |
| switch (fmt & SND_SOC_DAIFMT_CLOCK_PROVIDER_MASK) { |
| case SND_SOC_DAIFMT_BC_FC: |
| break; |
| case SND_SOC_DAIFMT_BP_FC: |
| /* Enable master mode */ |
| regmap_update_bits(i2s->regmap, LS_I2S_CTRL, I2S_CTRL_MASTER, |
| I2S_CTRL_MASTER); |
| ret = loongson_i2s_enable_bclk(i2s); |
| if (ret < 0) |
| dev_warn(dai->dev, "wait BCLK ready timeout\n"); |
| break; |
| case SND_SOC_DAIFMT_BC_FP: |
| /* Enable MCLK */ |
| ret = loongson_i2s_enable_mclk(i2s); |
| if (ret < 0) |
| dev_warn(dai->dev, "wait MCLK ready timeout\n"); |
| break; |
| case SND_SOC_DAIFMT_BP_FP: |
| /* Enable MCLK */ |
| ret = loongson_i2s_enable_mclk(i2s); |
| if (ret < 0) |
| dev_warn(dai->dev, "wait MCLK ready timeout\n"); |
| |
| /* Enable master mode */ |
| regmap_update_bits(i2s->regmap, LS_I2S_CTRL, I2S_CTRL_MASTER, |
| I2S_CTRL_MASTER); |
| |
| ret = loongson_i2s_enable_bclk(i2s); |
| if (ret < 0) |
| dev_warn(dai->dev, "wait BCLK ready timeout\n"); |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| static int loongson_i2s_dai_probe(struct snd_soc_dai *cpu_dai) |
| { |
| struct loongson_i2s *i2s = dev_get_drvdata(cpu_dai->dev); |
| |
| snd_soc_dai_init_dma_data(cpu_dai, &i2s->playback_dma_data, |
| &i2s->capture_dma_data); |
| snd_soc_dai_set_drvdata(cpu_dai, i2s); |
| |
| return 0; |
| } |
| |
| static const struct snd_soc_dai_ops loongson_i2s_dai_ops = { |
| .probe = loongson_i2s_dai_probe, |
| .trigger = loongson_i2s_trigger, |
| .hw_params = loongson_i2s_hw_params, |
| .set_sysclk = loongson_i2s_set_dai_sysclk, |
| .set_fmt = loongson_i2s_set_fmt, |
| }; |
| |
| struct snd_soc_dai_driver loongson_i2s_dai = { |
| .name = "loongson-i2s", |
| .playback = { |
| .stream_name = "CPU-Playback", |
| .channels_min = 1, |
| .channels_max = 2, |
| .rates = SNDRV_PCM_RATE_8000_96000, |
| .formats = LOONGSON_I2S_FORMATS, |
| }, |
| .capture = { |
| .stream_name = "CPU-Capture", |
| .channels_min = 1, |
| .channels_max = 2, |
| .rates = SNDRV_PCM_RATE_8000_96000, |
| .formats = LOONGSON_I2S_FORMATS, |
| }, |
| .ops = &loongson_i2s_dai_ops, |
| .symmetric_rate = 1, |
| }; |
| |
| static int i2s_suspend(struct device *dev) |
| { |
| struct loongson_i2s *i2s = dev_get_drvdata(dev); |
| |
| regcache_cache_only(i2s->regmap, true); |
| |
| return 0; |
| } |
| |
| static int i2s_resume(struct device *dev) |
| { |
| struct loongson_i2s *i2s = dev_get_drvdata(dev); |
| |
| regcache_cache_only(i2s->regmap, false); |
| regcache_mark_dirty(i2s->regmap); |
| return regcache_sync(i2s->regmap); |
| } |
| |
| const struct dev_pm_ops loongson_i2s_pm = { |
| SYSTEM_SLEEP_PM_OPS(i2s_suspend, i2s_resume) |
| }; |