ASoC: Add Loongson-2K0300 I2S controller and sound card support

Binbin Zhou <zhoubinbin@loongson.cn> says:

This series adds ASoC support for the Loongson-2K0300 SoC and its
associated development boards, including the CTCISZ Forever Pi and the
ATK-DL2K0300B.

Key changes:
- Extend DT bindings to support ls2k0300-i2s and new audio card
  compatibles.
- Refactor the platform I2S driver to handle SoC differences via per-device
  configuration (rev_id, optional APB DMA config, reset sequence).
- Refactor the audio machine driver to support board-specific DAI formats,
  GPIO-based headphone detection/control, speaker enable, and DAPM routing.
- Add jack detection and automatic switching between headphones and
  speakers for the DL2K0300B board.

The patchset also cleans up the existing audio card binding by
referencing the common sound-card properties, and adds new compatibles
for the Loongson-2K0300 variants with proper DAI format handling.

All changes have been tested on Loongson-2K2000 (PCI), Loongson-2K0300
Forever Pi and Loongson-2K0300 DL2K0300B boards.

Link: https://patch.msgid.link/cover.1782439646.git.zhoubinbin@loongson.cn
This commit is contained in:
Mark Brown 2026-07-02 20:04:18 +01:00
commit 8d01573c48
No known key found for this signature in database
GPG Key ID: 24D68B725D5487D0
5 changed files with 267 additions and 26 deletions

View File

@ -8,19 +8,22 @@ title: Loongson 7axxx/2kxxx ASoC audio sound card driver
maintainers:
- Yingkun Meng <mengyingkun@loongson.cn>
- Binbin Zhou <zhoubinbin@loongson.cn>
description:
The binding describes the sound card present in loongson
7axxx/2kxxx platform. The sound card is an ASoC component
which uses Loongson I2S controller to transfer the audio data.
allOf:
- $ref: sound-card-common.yaml#
properties:
compatible:
const: loongson,ls-audio-card
model:
$ref: /schemas/types.yaml#/definitions/string
description: User specified audio sound card name
enum:
- loongson,ls-audio-card # Loongson-2K1000/Loongson-2K2000/LS7A
- loongson,ls2k0300-forever-pi-audio-card # CTCISZ Forever Pi
- loongson,ls2k0300-dl2k0300b-audio-card # ATK-DL2K0300B
mclk-fs:
$ref: simple-card.yaml#/definitions/mclk-fs
@ -45,14 +48,25 @@ properties:
required:
- sound-dai
spkr-en-gpios:
maxItems: 1
description: The GPIO that enables the speakers
hp-ctl-gpios:
maxItems: 1
description: The GPIO that control the headphones
hp-det-gpios:
maxItems: 1
description: The GPIO that detect headphones are plugged in
required:
- compatible
- model
- mclk-fs
- cpu
- codec
additionalProperties: false
unevaluatedProperties: false
examples:
- |
@ -68,3 +82,28 @@ examples:
sound-dai = <&es8323>;
};
};
- |
#include <dt-bindings/gpio/gpio.h>
sound {
compatible = "loongson,ls2k0300-dl2k0300b-audio-card";
model = "loongson-audio";
mclk-fs = <512>;
hp-det-gpios = <&gpio 81 GPIO_ACTIVE_HIGH>;
spkr-en-gpios = <&gpio 86 GPIO_ACTIVE_HIGH>;
hp-ctl-gpios = <&gpio 87 GPIO_ACTIVE_HIGH>;
audio-routing =
"Headphone", "LOUT1",
"Headphone", "ROUT1",
"Speaker", "LOUT2",
"Speaker", "ROUT2";
cpu {
sound-dai = <&i2s>;
};
codec {
sound-dai = <&es8388>;
};
};

View File

@ -14,9 +14,12 @@ allOf:
properties:
compatible:
const: loongson,ls2k1000-i2s
enum:
- loongson,ls2k0300-i2s
- loongson,ls2k1000-i2s
reg:
minItems: 1
items:
- description: Loongson I2S controller Registers.
- description: APB DMA config register for Loongson I2S controller.
@ -49,6 +52,23 @@ required:
unevaluatedProperties: false
if:
properties:
compatible:
contains:
enum:
- loongson,ls2k1000-i2s
then:
properties:
reg:
minItems: 2
else:
properties:
reg:
maxItems: 1
examples:
- |
#include <dt-bindings/clock/loongson,ls2k-clk.h>

View File

@ -405,6 +405,11 @@ static const struct snd_soc_dapm_route es8328_dapm_routes[] = {
{ "Mic Bias", NULL, "Mic Bias Gen" },
{ "LINPUT1", NULL, "Mic Bias" },
{ "RINPUT1", NULL, "Mic Bias" },
{ "LINPUT2", NULL, "Mic Bias" },
{ "RINPUT2", NULL, "Mic Bias" },
{ "Left Mixer", NULL, "Left DAC" },
{ "Left Mixer", "Left Bypass Switch", "Left Line Mux" },
{ "Left Mixer", "Right Playback Switch", "Right DAC" },

View File

@ -2,24 +2,131 @@
//
// Loongson ASoC Audio Machine driver
//
// Copyright (C) 2023 Loongson Technology Corporation Limited
// Copyright (C) 2023-2026 Loongson Technology Corporation Limited
// Author: Yingkun Meng <mengyingkun@loongson.cn>
// Binbin Zhou <zhoubinbin@loongson.cn>
//
#include <linux/acpi.h>
#include <linux/gpio/consumer.h>
#include <linux/module.h>
#include <linux/pci.h>
#include <sound/jack.h>
#include <sound/pcm_params.h>
#include <sound/soc.h>
#include <sound/soc-acpi.h>
#include <linux/acpi.h>
#include <linux/pci.h>
#include <sound/pcm_params.h>
static char codec_name[SND_ACPI_I2C_ID_LEN];
struct loongson_card_data {
struct snd_soc_card snd_card;
unsigned int mclk_fs;
struct gpio_desc *gpiod_hp_det;
struct gpio_desc *gpiod_hp_ctl;
struct gpio_desc *gpiod_spkr_en;
const struct loongson_card_config *cfg;
};
struct loongson_card_config {
unsigned int fmt;
bool add_hp_jack;
bool add_dapm_widgets;
bool add_dapm_routes;
};
static const struct loongson_card_config ls2k1000_card_config = {
.fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_IB_NF | SND_SOC_DAIFMT_CBC_CFC,
.add_hp_jack = false,
.add_dapm_widgets = false,
.add_dapm_routes = false,
};
static const struct loongson_card_config ls2k0300_forever_pi_card_config = {
.fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBC_CFC,
.add_hp_jack = false,
.add_dapm_widgets = false,
.add_dapm_routes = false,
};
static const struct loongson_card_config ls2k0300_dl2k0300b_card_config = {
.fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBC_CFC,
.add_hp_jack = true,
.add_dapm_widgets = true,
.add_dapm_routes = true,
};
static int tegra_machine_event(struct snd_soc_dapm_widget *w,
struct snd_kcontrol *k, int event)
{
struct snd_soc_card *card = snd_soc_dapm_to_card(w->dapm);
struct loongson_card_data *priv = snd_soc_card_get_drvdata(card);
if (!snd_soc_dapm_widget_name_cmp(w, "Speaker"))
gpiod_set_value_cansleep(priv->gpiod_spkr_en,
SND_SOC_DAPM_EVENT_ON(event));
if (!snd_soc_dapm_widget_name_cmp(w, "Headphone"))
gpiod_set_value_cansleep(priv->gpiod_hp_ctl,
SND_SOC_DAPM_EVENT_ON(event));
return 0;
}
static const struct snd_soc_dapm_widget loongson_aosc_dapm_widgets[] = {
SND_SOC_DAPM_HP("Headphone", tegra_machine_event),
SND_SOC_DAPM_SPK("Speaker", tegra_machine_event),
};
/* Headphones Jack */
static struct snd_soc_jack loongson_asoc_hp_jack;
static struct snd_soc_jack_pin loongson_asoc_hp_jack_pins[] = {
{
.pin = "Headphone",
.mask = SND_JACK_HEADPHONE
},
{
.pin = "Speaker",
.mask = SND_JACK_HEADPHONE,
.invert = 1
},
};
static struct snd_soc_jack_gpio loongson_asoc_hp_jack_gpio = {
.name = "Headphones detection",
.report = SND_JACK_HEADPHONE,
.debounce_time = 150,
};
static int loongson_asoc_machine_init(struct snd_soc_pcm_runtime *rtd)
{
struct snd_soc_card *card = rtd->card;
struct loongson_card_data *ls_priv = snd_soc_card_get_drvdata(card);
int ret = 0;
if (!ls_priv->cfg->add_hp_jack || !ls_priv->gpiod_hp_det)
return 0;
ret = snd_soc_card_jack_new_pins(card, "Headphones Jack",
SND_JACK_HEADPHONE,
&loongson_asoc_hp_jack,
loongson_asoc_hp_jack_pins,
ARRAY_SIZE(loongson_asoc_hp_jack_pins));
if (ret) {
dev_err(rtd->dev, "Headphones Jack creation failed: %d\n", ret);
return ret;
}
loongson_asoc_hp_jack_gpio.desc = ls_priv->gpiod_hp_det;
ret = snd_soc_jack_add_gpios(&loongson_asoc_hp_jack, 1, &loongson_asoc_hp_jack_gpio);
if (ret)
dev_err(rtd->dev, "Headphone GPIO not added: %d\n", ret);
return ret;
}
static int loongson_card_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params)
{
@ -45,7 +152,7 @@ static int loongson_card_hw_params(struct snd_pcm_substream *substream,
return ret;
}
return 0;
return snd_soc_runtime_set_dai_fmt(rtd, ls_card->cfg->fmt);
}
static const struct snd_soc_ops loongson_ops = {
@ -61,8 +168,7 @@ static struct snd_soc_dai_link loongson_dai_links[] = {
{
.name = "Loongson Audio Port",
.stream_name = "Loongson Audio",
.dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_IB_NF
| SND_SOC_DAIFMT_CBC_CFC,
.init = loongson_asoc_machine_init,
SND_SOC_DAILINK_REG(analog),
.ops = &loongson_ops,
},
@ -91,7 +197,7 @@ static int loongson_card_parse_acpi(struct loongson_card_data *data)
const char *codec_dai_name;
struct acpi_device *adev;
struct device *phy_dev;
int i;
int i, ret;
/* fixup platform name based on reference node */
adev = loongson_card_acpi_find_device(card, "cpu");
@ -108,7 +214,9 @@ static int loongson_card_parse_acpi(struct loongson_card_data *data)
return -ENOENT;
snprintf(codec_name, sizeof(codec_name), "i2c-%s", acpi_dev_name(adev));
device_property_read_string(card->dev, "codec-dai-name", &codec_dai_name);
ret = device_property_read_string(card->dev, "codec-dai-name", &codec_dai_name);
if (ret)
return ret;
for (i = 0; i < card->num_links; i++) {
loongson_dai_links[i].platforms->name = dev_name(phy_dev);
@ -121,16 +229,35 @@ static int loongson_card_parse_acpi(struct loongson_card_data *data)
static int loongson_card_parse_of(struct loongson_card_data *data)
{
struct device_node *cpu, *codec;
struct snd_soc_card *card = &data->snd_card;
struct device_node *cpu, *codec;
struct device *dev = card->dev;
int ret, i;
data->gpiod_hp_det = devm_gpiod_get_optional(dev, "hp-det", GPIOD_IN);
if (IS_ERR(data->gpiod_hp_det))
return PTR_ERR(data->gpiod_hp_det);
data->gpiod_hp_ctl = devm_gpiod_get_optional(dev, "hp-ctl", GPIOD_OUT_LOW);
if (IS_ERR(data->gpiod_hp_ctl))
return PTR_ERR(data->gpiod_hp_ctl);
data->gpiod_spkr_en = devm_gpiod_get_optional(dev, "spkr-en", GPIOD_OUT_LOW);
if (IS_ERR(data->gpiod_spkr_en))
return PTR_ERR(data->gpiod_spkr_en);
if (data->cfg->add_dapm_routes) {
ret = snd_soc_of_parse_audio_routing(card, "audio-routing");
if (ret)
return ret;
}
cpu = of_get_child_by_name(dev->of_node, "cpu");
if (!cpu) {
dev_err(dev, "platform property missing or invalid\n");
return -EINVAL;
}
codec = of_get_child_by_name(dev->of_node, "codec");
if (!codec) {
dev_err(dev, "audio-codec property missing or invalid\n");
@ -175,12 +302,22 @@ static int loongson_asoc_card_probe(struct platform_device *pdev)
if (!ls_priv)
return -ENOMEM;
ls_priv->cfg = (const struct loongson_card_config *)device_get_match_data(dev);
if (!ls_priv->cfg)
return -EINVAL;
card = &ls_priv->snd_card;
card->dev = dev;
card->owner = THIS_MODULE;
card->dai_link = loongson_dai_links;
card->num_links = ARRAY_SIZE(loongson_dai_links);
if (ls_priv->cfg->add_dapm_widgets) {
card->dapm_widgets = loongson_aosc_dapm_widgets;
card->num_dapm_widgets = ARRAY_SIZE(loongson_aosc_dapm_widgets);
}
snd_soc_card_set_drvdata(card, ls_priv);
ret = device_property_read_string(dev, "model", &card->name);
@ -200,7 +337,19 @@ static int loongson_asoc_card_probe(struct platform_device *pdev)
}
static const struct of_device_id loongson_asoc_dt_ids[] = {
{ .compatible = "loongson,ls-audio-card" },
/* Loongson-2K1000/Loongson-2K2000/LS7A */
{
.compatible = "loongson,ls-audio-card",
.data = &ls2k1000_card_config
},
{
.compatible = "loongson,ls2k0300-forever-pi-audio-card",
.data = &ls2k0300_forever_pi_card_config
},
{
.compatible = "loongson,ls2k0300-dl2k0300b-audio-card",
.data = &ls2k0300_dl2k0300b_card_config
},
{ /* sentinel */ },
};
MODULE_DEVICE_TABLE(of, loongson_asoc_dt_ids);

View File

@ -2,7 +2,7 @@
//
// Loongson I2S controller master mode dirver(platform device)
//
// Copyright (C) 2023-2024 Loongson Technology Corporation Limited
// Copyright (C) 2023-2026 Loongson Technology Corporation Limited
//
// Author: Yingkun Meng <mengyingkun@loongson.cn>
// Binbin Zhou <zhoubinbin@loongson.cn>
@ -21,6 +21,7 @@
#include "loongson_i2s.h"
#include "loongson_dma.h"
/* Loongson-2K1000 APBDMA routing */
#define LOONGSON_I2S_RX_DMA_OFFSET 21
#define LOONGSON_I2S_TX_DMA_OFFSET 18
@ -30,6 +31,11 @@
#define LOONGSON_DMA3_CONF 0x3
#define LOONGSON_DMA4_CONF 0x4
struct loongson_i2s_plat_config {
int rev_id;
int (*i2s_dma_config)(struct platform_device *pdev);
};
static int loongson_i2s_apbdma_config(struct platform_device *pdev)
{
int val;
@ -47,8 +53,18 @@ static int loongson_i2s_apbdma_config(struct platform_device *pdev)
return 0;
}
static const struct loongson_i2s_plat_config ls2k0300_i2s_plat_config = {
.rev_id = 1,
};
static const struct loongson_i2s_plat_config ls2k1000_i2s_plat_config = {
.rev_id = 0,
.i2s_dma_config = loongson_i2s_apbdma_config,
};
static int loongson_i2s_plat_probe(struct platform_device *pdev)
{
const struct loongson_i2s_plat_config *plat_config;
struct device *dev = &pdev->dev;
struct loongson_i2s *i2s;
struct resource *res;
@ -59,12 +75,17 @@ static int loongson_i2s_plat_probe(struct platform_device *pdev)
if (!i2s)
return -ENOMEM;
ret = loongson_i2s_apbdma_config(pdev);
if (ret)
return ret;
plat_config = device_get_match_data(dev);
if (!plat_config)
return -EINVAL;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
i2s->reg_base = devm_ioremap_resource(&pdev->dev, res);
if (plat_config->i2s_dma_config) {
ret = plat_config->i2s_dma_config(pdev);
if (ret)
return ret;
}
i2s->reg_base = devm_platform_get_and_ioremap_resource(pdev, 0, &res);
if (IS_ERR(i2s->reg_base))
return dev_err_probe(dev, PTR_ERR(i2s->reg_base),
"devm_ioremap_resource failed\n");
@ -87,11 +108,17 @@ static int loongson_i2s_plat_probe(struct platform_device *pdev)
if (IS_ERR(i2s_clk))
return dev_err_probe(dev, PTR_ERR(i2s_clk), "clock property invalid\n");
i2s->clk_rate = clk_get_rate(i2s_clk);
i2s->rev_id = plat_config->rev_id;
dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64));
dev_set_name(dev, LS_I2S_DRVNAME);
dev_set_drvdata(dev, i2s);
if (i2s->rev_id == 1) {
regmap_update_bits(i2s->regmap, LS_I2S_CTRL, I2S_CTRL_RESET, I2S_CTRL_RESET);
fsleep(200);
}
ret = devm_snd_soc_register_component(dev, &loongson_i2s_edma_component,
&loongson_i2s_dai, 1);
if (ret)
@ -102,7 +129,8 @@ static int loongson_i2s_plat_probe(struct platform_device *pdev)
}
static const struct of_device_id loongson_i2s_ids[] = {
{ .compatible = "loongson,ls2k1000-i2s" },
{ .compatible = "loongson,ls2k0300-i2s", .data = &ls2k0300_i2s_plat_config },
{ .compatible = "loongson,ls2k1000-i2s", .data = &ls2k1000_i2s_plat_config },
{ /* sentinel */ },
};
MODULE_DEVICE_TABLE(of, loongson_i2s_ids);