2020-04-17 18:13:41 -04:00
// SPDX-License-Identifier: GPL-2.0-only
//
// Codec driver for Microsemi ZL38060 Connected Home Audio Processor.
//
// Copyright(c) 2020 Sven Van Asbroeck
// The ZL38060 is very flexible and configurable. This driver implements only a
// tiny subset of the chip's possible configurations:
//
// - DSP block bypassed: DAI routed straight to DACs
// microphone routed straight to DAI
// - chip's internal clock is driven by a 12 MHz external crystal
// - chip's DAI connected to CPU is I2S, and bit + frame clock master
// - chip must be strapped for "host boot": in this mode, firmware will be
// provided by this driver.
# include <linux/gpio/consumer.h>
# include <linux/gpio/driver.h>
# include <linux/property.h>
# include <linux/spi/spi.h>
# include <linux/regmap.h>
# include <linux/module.h>
# include <linux/ihex.h>
# include <sound/pcm_params.h>
# include <sound/core.h>
# include <sound/pcm.h>
# include <sound/soc.h>
# define DRV_NAME "zl38060"
# define ZL38_RATES (SNDRV_PCM_RATE_8000 |\
SNDRV_PCM_RATE_16000 | \
SNDRV_PCM_RATE_48000 )
# define ZL38_FORMATS SNDRV_PCM_FMTBIT_S16_LE
# define HBI_FIRMWARE_PAGE 0xFF
# define ZL38_MAX_RAW_XFER 0x100
# define REG_TDMA_CFG_CLK 0x0262
# define CFG_CLK_PCLK_SHIFT 4
# define CFG_CLK_PCLK_MASK (0x7ff << CFG_CLK_PCLK_SHIFT)
# define CFG_CLK_PCLK(bits) ((bits - 1) << CFG_CLK_PCLK_SHIFT)
# define CFG_CLK_MASTER BIT(15)
# define CFG_CLK_FSRATE_MASK 0x7
# define CFG_CLK_FSRATE_8KHZ 0x1
# define CFG_CLK_FSRATE_16KHZ 0x2
# define CFG_CLK_FSRATE_48KHZ 0x6
# define REG_CLK_CFG 0x0016
# define CLK_CFG_SOURCE_XTAL BIT(15)
# define REG_CLK_STATUS 0x0014
# define CLK_STATUS_HWRST BIT(0)
# define REG_PARAM_RESULT 0x0034
# define PARAM_RESULT_READY 0xD3D3
# define REG_PG255_BASE_HI 0x000C
# define REG_PG255_OFFS(addr) ((HBI_FIRMWARE_PAGE << 8) | (addr & 0xFF))
# define REG_FWR_EXEC 0x012C
# define REG_CMD 0x0032
# define REG_HW_REV 0x0020
# define REG_FW_PROD 0x0022
# define REG_FW_REV 0x0024
# define REG_SEMA_FLAGS 0x0006
# define SEMA_FLAGS_BOOT_CMD BIT(0)
# define SEMA_FLAGS_APP_REBOOT BIT(1)
# define REG_HW_REV 0x0020
# define REG_FW_PROD 0x0022
# define REG_FW_REV 0x0024
# define REG_GPIO_DIR 0x02DC
# define REG_GPIO_DAT 0x02DA
# define BOOTCMD_LOAD_COMPLETE 0x000D
# define BOOTCMD_FW_GO 0x0008
# define FIRMWARE_MAJOR 2
# define FIRMWARE_MINOR 2
struct zl38_codec_priv {
struct device * dev ;
struct regmap * regmap ;
bool is_stream_in_use [ 2 ] ;
struct gpio_chip * gpio_chip ;
} ;
static int zl38_fw_issue_command ( struct regmap * regmap , u16 cmd )
{
unsigned int val ;
int err ;
err = regmap_read_poll_timeout ( regmap , REG_SEMA_FLAGS , val ,
! ( val & SEMA_FLAGS_BOOT_CMD ) , 10000 ,
10000 * 100 ) ;
if ( err )
return err ;
err = regmap_write ( regmap , REG_CMD , cmd ) ;
if ( err )
return err ;
err = regmap_update_bits ( regmap , REG_SEMA_FLAGS , SEMA_FLAGS_BOOT_CMD ,
SEMA_FLAGS_BOOT_CMD ) ;
if ( err )
return err ;
return regmap_read_poll_timeout ( regmap , REG_CMD , val , ! val , 10000 ,
10000 * 100 ) ;
}
static int zl38_fw_go ( struct regmap * regmap )
{
int err ;
err = zl38_fw_issue_command ( regmap , BOOTCMD_LOAD_COMPLETE ) ;
if ( err )
return err ;
return zl38_fw_issue_command ( regmap , BOOTCMD_FW_GO ) ;
}
static int zl38_fw_enter_boot_mode ( struct regmap * regmap )
{
unsigned int val ;
int err ;
err = regmap_update_bits ( regmap , REG_CLK_STATUS , CLK_STATUS_HWRST ,
CLK_STATUS_HWRST ) ;
if ( err )
return err ;
return regmap_read_poll_timeout ( regmap , REG_PARAM_RESULT , val ,
val = = PARAM_RESULT_READY , 1000 , 50000 ) ;
}
static int
zl38_fw_send_data ( struct regmap * regmap , u32 addr , const void * data , u16 len )
{
__be32 addr_base = cpu_to_be32 ( addr & ~ 0xFF ) ;
int err ;
err = regmap_raw_write ( regmap , REG_PG255_BASE_HI , & addr_base ,
sizeof ( addr_base ) ) ;
if ( err )
return err ;
return regmap_raw_write ( regmap , REG_PG255_OFFS ( addr ) , data , len ) ;
}
static int zl38_fw_send_xaddr ( struct regmap * regmap , const void * data )
{
/* execution address from ihex: 32-bit little endian.
* device register expects 32 - bit big endian .
*/
u32 addr = le32_to_cpup ( data ) ;
__be32 baddr = cpu_to_be32 ( addr ) ;
return regmap_raw_write ( regmap , REG_FWR_EXEC , & baddr , sizeof ( baddr ) ) ;
}
static int zl38_load_firmware ( struct device * dev , struct regmap * regmap )
{
const struct ihex_binrec * rec ;
const struct firmware * fw ;
u32 addr ;
u16 len ;
int err ;
/* how to get this firmware:
* 1. request and download chip firmware from Microsemi
* ( provided by Microsemi in srec format )
* 2. convert downloaded firmware from srec to ihex . Simple tool :
* https : //gitlab.com/TheSven73/s3-to-irec
* 3. convert ihex to binary ( . fw ) using ihex2fw tool which is included
* with the Linux kernel sources
*/
err = request_ihex_firmware ( & fw , " zl38060.fw " , dev ) ;
if ( err )
return err ;
err = zl38_fw_enter_boot_mode ( regmap ) ;
if ( err )
goto out ;
rec = ( const struct ihex_binrec * ) fw - > data ;
while ( rec ) {
addr = be32_to_cpu ( rec - > addr ) ;
len = be16_to_cpu ( rec - > len ) ;
if ( addr ) {
/* regular data ihex record */
err = zl38_fw_send_data ( regmap , addr , rec - > data , len ) ;
} else if ( len = = 4 ) {
/* execution address ihex record */
err = zl38_fw_send_xaddr ( regmap , rec - > data ) ;
} else {
err = - EINVAL ;
}
if ( err )
goto out ;
/* next ! */
rec = ihex_next_binrec ( rec ) ;
}
err = zl38_fw_go ( regmap ) ;
out :
release_firmware ( fw ) ;
return err ;
}
static int zl38_software_reset ( struct regmap * regmap )
{
unsigned int val ;
int err ;
err = regmap_update_bits ( regmap , REG_SEMA_FLAGS , SEMA_FLAGS_APP_REBOOT ,
SEMA_FLAGS_APP_REBOOT ) ;
if ( err )
return err ;
/* wait for host bus interface to settle.
* Not sure if this is required : Microsemi ' s vendor driver does this ,
* but the firmware manual does not mention it . Leave it in , there ' s
* little downside , apart from a slower reset .
*/
msleep ( 50 ) ;
return regmap_read_poll_timeout ( regmap , REG_SEMA_FLAGS , val ,
! ( val & SEMA_FLAGS_APP_REBOOT ) , 10000 ,
10000 * 100 ) ;
}
static int zl38_set_fmt ( struct snd_soc_dai * dai , unsigned int fmt )
{
struct zl38_codec_priv * priv = snd_soc_dai_get_drvdata ( dai ) ;
int err ;
switch ( fmt & SND_SOC_DAIFMT_FORMAT_MASK ) {
case SND_SOC_DAIFMT_I2S :
/* firmware default is normal i2s */
break ;
default :
return - EINVAL ;
}
switch ( fmt & SND_SOC_DAIFMT_INV_MASK ) {
case SND_SOC_DAIFMT_NB_NF :
/* firmware default is normal bitclock and frame */
break ;
default :
return - EINVAL ;
}
2021-09-21 21:51:17 +01:00
switch ( fmt & SND_SOC_DAIFMT_CLOCK_PROVIDER_MASK ) {
case SND_SOC_DAIFMT_CBP_CFP :
2020-04-17 18:13:41 -04:00
/* always 32 bits per frame (= 16 bits/channel, 2 channels) */
err = regmap_update_bits ( priv - > regmap , REG_TDMA_CFG_CLK ,
CFG_CLK_MASTER | CFG_CLK_PCLK_MASK ,
CFG_CLK_MASTER | CFG_CLK_PCLK ( 32 ) ) ;
if ( err )
return err ;
break ;
default :
return - EINVAL ;
}
return 0 ;
}
static int zl38_hw_params ( struct snd_pcm_substream * substream ,
struct snd_pcm_hw_params * params ,
struct snd_soc_dai * dai )
{
struct zl38_codec_priv * priv = snd_soc_dai_get_drvdata ( dai ) ;
bool tx = substream - > stream = = SNDRV_PCM_STREAM_PLAYBACK ;
unsigned int fsrate ;
int err ;
/* We cannot change hw_params while the dai is already in use - the
* software reset will corrupt the audio . However , this is not required ,
* as the chip ' s TDM buses are fully symmetric , which mandates identical
* rates , channels , and samplebits for record and playback .
*/
if ( priv - > is_stream_in_use [ ! tx ] )
goto skip_setup ;
switch ( params_rate ( params ) ) {
case 8000 :
fsrate = CFG_CLK_FSRATE_8KHZ ;
break ;
case 16000 :
fsrate = CFG_CLK_FSRATE_16KHZ ;
break ;
case 48000 :
fsrate = CFG_CLK_FSRATE_48KHZ ;
break ;
default :
return - EINVAL ;
2020-05-26 02:43:51 +08:00
}
2020-04-17 18:13:41 -04:00
err = regmap_update_bits ( priv - > regmap , REG_TDMA_CFG_CLK ,
CFG_CLK_FSRATE_MASK , fsrate ) ;
if ( err )
return err ;
/* chip requires a software reset to apply audio register changes */
err = zl38_software_reset ( priv - > regmap ) ;
if ( err )
return err ;
skip_setup :
priv - > is_stream_in_use [ tx ] = true ;
return 0 ;
}
static int zl38_hw_free ( struct snd_pcm_substream * substream ,
struct snd_soc_dai * dai )
{
struct zl38_codec_priv * priv = snd_soc_dai_get_drvdata ( dai ) ;
bool tx = substream - > stream = = SNDRV_PCM_STREAM_PLAYBACK ;
priv - > is_stream_in_use [ tx ] = false ;
return 0 ;
}
/* stereo bypass with no AEC */
static const struct reg_sequence cp_config_stereo_bypass [ ] = {
/* interconnects must be programmed first */
{ 0x0210 , 0x0005 } , /* DAC1 in <= I2S1-L */
{ 0x0212 , 0x0006 } , /* DAC2 in <= I2S1-R */
{ 0x0214 , 0x0001 } , /* I2S1-L in <= MIC1 */
{ 0x0216 , 0x0001 } , /* I2S1-R in <= MIC1 */
{ 0x0224 , 0x0000 } , /* AEC-S in <= n/a */
{ 0x0226 , 0x0000 } , /* AEC-R in <= n/a */
/* output enables must be programmed next */
{ 0x0202 , 0x000F } , /* enable I2S1 + DAC */
} ;
static const struct snd_soc_dai_ops zl38_dai_ops = {
. set_fmt = zl38_set_fmt ,
. hw_params = zl38_hw_params ,
. hw_free = zl38_hw_free ,
} ;
static struct snd_soc_dai_driver zl38_dai = {
. name = " zl38060-tdma " ,
. playback = {
. stream_name = " Playback " ,
. channels_min = 2 ,
. channels_max = 2 ,
. rates = ZL38_RATES ,
. formats = ZL38_FORMATS ,
} ,
. capture = {
. stream_name = " Capture " ,
. channels_min = 2 ,
. channels_max = 2 ,
. rates = ZL38_RATES ,
. formats = ZL38_FORMATS ,
} ,
. ops = & zl38_dai_ops ,
2021-01-15 13:55:29 +09:00
. symmetric_rate = 1 ,
. symmetric_sample_bits = 1 ,
2020-04-17 18:13:41 -04:00
. symmetric_channels = 1 ,
} ;
static const struct snd_soc_dapm_widget zl38_dapm_widgets [ ] = {
SND_SOC_DAPM_OUTPUT ( " DAC1 " ) ,
SND_SOC_DAPM_OUTPUT ( " DAC2 " ) ,
SND_SOC_DAPM_INPUT ( " DMICL " ) ,
} ;
static const struct snd_soc_dapm_route zl38_dapm_routes [ ] = {
{ " DAC1 " , NULL , " Playback " } ,
{ " DAC2 " , NULL , " Playback " } ,
{ " Capture " , NULL , " DMICL " } ,
} ;
static const struct snd_soc_component_driver zl38_component_dev = {
. dapm_widgets = zl38_dapm_widgets ,
. num_dapm_widgets = ARRAY_SIZE ( zl38_dapm_widgets ) ,
. dapm_routes = zl38_dapm_routes ,
. num_dapm_routes = ARRAY_SIZE ( zl38_dapm_routes ) ,
. endianness = 1 ,
. non_legacy_dai_naming = 1 ,
} ;
static void chip_gpio_set ( struct gpio_chip * c , unsigned int offset , int val )
{
struct regmap * regmap = gpiochip_get_data ( c ) ;
unsigned int mask = BIT ( offset ) ;
regmap_update_bits ( regmap , REG_GPIO_DAT , mask , val ? mask : 0 ) ;
}
static int chip_gpio_get ( struct gpio_chip * c , unsigned int offset )
{
struct regmap * regmap = gpiochip_get_data ( c ) ;
unsigned int mask = BIT ( offset ) ;
unsigned int val ;
int err ;
err = regmap_read ( regmap , REG_GPIO_DAT , & val ) ;
if ( err )
return err ;
return ! ! ( val & mask ) ;
}
static int chip_direction_input ( struct gpio_chip * c , unsigned int offset )
{
struct regmap * regmap = gpiochip_get_data ( c ) ;
unsigned int mask = BIT ( offset ) ;
return regmap_update_bits ( regmap , REG_GPIO_DIR , mask , 0 ) ;
}
static int
chip_direction_output ( struct gpio_chip * c , unsigned int offset , int val )
{
struct regmap * regmap = gpiochip_get_data ( c ) ;
unsigned int mask = BIT ( offset ) ;
chip_gpio_set ( c , offset , val ) ;
return regmap_update_bits ( regmap , REG_GPIO_DIR , mask , mask ) ;
}
static const struct gpio_chip template_chip = {
. owner = THIS_MODULE ,
. label = DRV_NAME ,
. base = - 1 ,
. ngpio = 14 ,
. direction_input = chip_direction_input ,
. direction_output = chip_direction_output ,
. get = chip_gpio_get ,
. set = chip_gpio_set ,
. can_sleep = true ,
} ;
static int zl38_check_revision ( struct device * dev , struct regmap * regmap )
{
unsigned int hwrev , fwprod , fwrev ;
int fw_major , fw_minor , fw_micro ;
int err ;
err = regmap_read ( regmap , REG_HW_REV , & hwrev ) ;
if ( err )
return err ;
err = regmap_read ( regmap , REG_FW_PROD , & fwprod ) ;
if ( err )
return err ;
err = regmap_read ( regmap , REG_FW_REV , & fwrev ) ;
if ( err )
return err ;
fw_major = ( fwrev > > 12 ) & 0xF ;
fw_minor = ( fwrev > > 8 ) & 0xF ;
fw_micro = fwrev & 0xFF ;
dev_info ( dev , " hw rev 0x%x, fw product code %d, firmware rev %d.%d.%d " ,
hwrev & 0x1F , fwprod , fw_major , fw_minor , fw_micro ) ;
if ( fw_major ! = FIRMWARE_MAJOR | | fw_minor < FIRMWARE_MINOR ) {
dev_err ( dev , " unsupported firmware. driver supports %d.%d " ,
FIRMWARE_MAJOR , FIRMWARE_MINOR ) ;
return - EINVAL ;
}
return 0 ;
}
static int zl38_bus_read ( void * context ,
const void * reg_buf , size_t reg_size ,
void * val_buf , size_t val_size )
{
struct spi_device * spi = context ;
const u8 * reg_buf8 = reg_buf ;
size_t len = 0 ;
u8 offs , page ;
u8 txbuf [ 4 ] ;
if ( reg_size ! = 2 | | val_size > ZL38_MAX_RAW_XFER )
return - EINVAL ;
offs = reg_buf8 [ 1 ] > > 1 ;
page = reg_buf8 [ 0 ] ;
if ( page ) {
txbuf [ len + + ] = 0xFE ;
txbuf [ len + + ] = page = = HBI_FIRMWARE_PAGE ? 0xFF : page - 1 ;
txbuf [ len + + ] = offs ;
txbuf [ len + + ] = val_size / 2 - 1 ;
} else {
txbuf [ len + + ] = offs | 0x80 ;
txbuf [ len + + ] = val_size / 2 - 1 ;
}
return spi_write_then_read ( spi , txbuf , len , val_buf , val_size ) ;
}
static int zl38_bus_write ( void * context , const void * data , size_t count )
{
struct spi_device * spi = context ;
u8 buf [ 4 + ZL38_MAX_RAW_XFER ] ;
size_t val_len , len = 0 ;
const u8 * data8 = data ;
u8 offs , page ;
if ( count > ( 2 + ZL38_MAX_RAW_XFER ) | | count < 4 )
return - EINVAL ;
val_len = count - 2 ;
offs = data8 [ 1 ] > > 1 ;
page = data8 [ 0 ] ;
if ( page ) {
buf [ len + + ] = 0xFE ;
buf [ len + + ] = page = = HBI_FIRMWARE_PAGE ? 0xFF : page - 1 ;
buf [ len + + ] = offs ;
buf [ len + + ] = ( val_len / 2 - 1 ) | 0x80 ;
} else {
buf [ len + + ] = offs | 0x80 ;
buf [ len + + ] = ( val_len / 2 - 1 ) | 0x80 ;
}
memcpy ( buf + len , data8 + 2 , val_len ) ;
len + = val_len ;
return spi_write ( spi , buf , len ) ;
}
static const struct regmap_bus zl38_regmap_bus = {
. read = zl38_bus_read ,
. write = zl38_bus_write ,
. max_raw_write = ZL38_MAX_RAW_XFER ,
. max_raw_read = ZL38_MAX_RAW_XFER ,
} ;
static const struct regmap_config zl38_regmap_conf = {
. reg_bits = 16 ,
. val_bits = 16 ,
. reg_stride = 2 ,
. use_single_read = true ,
. use_single_write = true ,
} ;
static int zl38_spi_probe ( struct spi_device * spi )
{
struct device * dev = & spi - > dev ;
struct zl38_codec_priv * priv ;
struct gpio_desc * reset_gpio ;
int err ;
/* get the chip to a known state by putting it in reset */
reset_gpio = devm_gpiod_get_optional ( dev , " reset " , GPIOD_OUT_HIGH ) ;
if ( IS_ERR ( reset_gpio ) )
return PTR_ERR ( reset_gpio ) ;
if ( reset_gpio ) {
/* datasheet: need > 10us for a digital + analog reset */
usleep_range ( 15 , 50 ) ;
/* take the chip out of reset */
gpiod_set_value_cansleep ( reset_gpio , 0 ) ;
/* datasheet: need > 3ms for digital section to become stable */
usleep_range ( 3000 , 10000 ) ;
}
priv = devm_kzalloc ( dev , sizeof ( * priv ) , GFP_KERNEL ) ;
if ( ! priv )
return - ENOMEM ;
priv - > dev = dev ;
dev_set_drvdata ( dev , priv ) ;
priv - > regmap = devm_regmap_init ( dev , & zl38_regmap_bus , spi ,
& zl38_regmap_conf ) ;
if ( IS_ERR ( priv - > regmap ) )
return PTR_ERR ( priv - > regmap ) ;
err = zl38_load_firmware ( dev , priv - > regmap ) ;
if ( err )
return err ;
err = zl38_check_revision ( dev , priv - > regmap ) ;
if ( err )
return err ;
priv - > gpio_chip = devm_kmemdup ( dev , & template_chip ,
sizeof ( template_chip ) , GFP_KERNEL ) ;
if ( ! priv - > gpio_chip )
return - ENOMEM ;
# ifdef CONFIG_OF_GPIO
priv - > gpio_chip - > of_node = dev - > of_node ;
# endif
err = devm_gpiochip_add_data ( dev , priv - > gpio_chip , priv - > regmap ) ;
if ( err )
return err ;
/* setup the cross-point switch for stereo bypass */
err = regmap_multi_reg_write ( priv - > regmap , cp_config_stereo_bypass ,
ARRAY_SIZE ( cp_config_stereo_bypass ) ) ;
if ( err )
return err ;
/* setup for 12MHz crystal connected to the chip */
err = regmap_update_bits ( priv - > regmap , REG_CLK_CFG , CLK_CFG_SOURCE_XTAL ,
CLK_CFG_SOURCE_XTAL ) ;
if ( err )
return err ;
return devm_snd_soc_register_component ( dev , & zl38_component_dev ,
& zl38_dai , 1 ) ;
}
static const struct of_device_id zl38_dt_ids [ ] = {
{ . compatible = " mscc,zl38060 " , } ,
{ /* sentinel */ }
} ;
MODULE_DEVICE_TABLE ( of , zl38_dt_ids ) ;
static const struct spi_device_id zl38_spi_ids [ ] = {
{ " zl38060 " , 0 } ,
{ /* sentinel */ }
} ;
MODULE_DEVICE_TABLE ( spi , zl38_spi_ids ) ;
static struct spi_driver zl38060_spi_driver = {
. driver = {
. name = DRV_NAME ,
. of_match_table = of_match_ptr ( zl38_dt_ids ) ,
} ,
. probe = zl38_spi_probe ,
. id_table = zl38_spi_ids ,
} ;
module_spi_driver ( zl38060_spi_driver ) ;
MODULE_DESCRIPTION ( " ASoC ZL38060 driver " ) ;
MODULE_AUTHOR ( " Sven Van Asbroeck <TheSven73@gmail.com> " ) ;
MODULE_LICENSE ( " GPL v2 " ) ;