/*
 * CS42L51 A/D, D/A Converter handling routines by SPI bus.
 *
 * Copyright 2006 Sony Corporation.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *
 */

#include <linux/delay.h>		/* for udelay() */
#include <linux/errno.h>		/* for error number */
#include <linux/string.h>		/* for memcpy() */
#include <linux/spinlock.h>
#include <linux/prdbg.h>
#include "cs4251.h"

/* board specific external functions */
extern int cs4251_spi_write(void *buf, size_t bytes);

/* CS42L51 control structure */
typedef struct {
	spinlock_t		spinlock;
	unsigned char	regs[CS4251_MAX_REG_NO+1];	/* shadow registers */
} cs4251_ctrl_t;

static cs4251_ctrl_t	s_Ctrl = {
	.spinlock = SPIN_LOCK_UNLOCKED,
	.regs = {
		0x00,	/* 00: unoffcial register (initial value is unknown) */
		0xd8,	/* 01: chip ID and revision (read only) */
		0x00,	/* 02: power control 1 */
		0xae,	/* 03: */
		0x00,	/* 04: */
		0x00,	/* 05: */
		0xa0,	/* 06: */
		0x00,	/* 07: */
		0x60,	/* 08: */
		0x06,	/* 09: */
		0x00,	/* 0a: */
		0x00,	/* 0b: */
		0x00,	/* 0c: */
		0x00,	/* 0d: */
		0x80,	/* 0e: */
		0x80,	/* 0f: */
		0x80,	/* 10: */
		0x80,	/* 11: */
		0x00,	/* 12: */
		0x00,	/* 13: */
		0x00,	/* 14: */
		0x88,	/* 15: */
		0x00,	/* 16: */
		0x00,	/* 17: */
		0x00,	/* 18: */
		0x00,	/* 19: */
		0x40,	/* 1a: */
		0x00,	/* 1b: */
		0x00,	/* 1c: */
		0x00,	/* 1d: */
		0x00,	/* 1e: */
		0x00,	/* 1f: */
		0x00,	/* 20: status (read only) */
		0x50,	/* 21: */
		0x00,	/* 22: unoffcial register (initial value is unknown) */
		0x00,	/* 23: unoffcial register (initial value is unknown) */
		0x00,	/* 24: unoffcial register (initial value is unknown) */
		0x00,	/* 25: unoffcial register (initial value is unknown) */
		0x00,	/* 26: unoffcial register (initial value is unknown) */
		0x00,	/* 27: unoffcial register (initial value is unknown) */
		0x00,	/* 28: unoffcial register (initial value is unknown) */
		0x00,	/* 29: unoffcial register (initial value is unknown) */
		0x00,	/* 2a: unoffcial register (initial value is unknown) */
		0x00,	/* 2b: unoffcial register (initial value is unknown) */
		0x00,	/* 2c: unoffcial register (initial value is unknown) */
		0x00,	/* 2d: unoffcial register (initial value is unknown) */
		0x00,	/* 2e: unoffcial register (initial value is unknown) */
		0x00,	/* 2f: unoffcial register (initial value is unknown) */
		0x00,	/* 30: unoffcial register (initial value is unknown) */
		0x00,	/* 31: unoffcial register (initial value is unknown) */
		0x00,	/* 32: unoffcial register (initial value is unknown) */
		0x00,	/* 33: unoffcial register (initial value is unknown) */
		0x00,	/* 34: unoffcial register (initial value is unknown) */
		0x00,	/* 35: unoffcial register (initial value is unknown) */
		0x00,	/* 36: unoffcial register (initial value is unknown) */
		0x00,	/* 37: unoffcial register (initial value is unknown) */
		0x00,	/* 38: unoffcial register (initial value is unknown) */
	}
};

/* read a data from register */
inline int cs4251_read_reg(int reg, unsigned char *data)
{
	if (reg < CS4251_MIN_REG_NO || reg > CS4251_MAX_REG_NO) {
		PrErr("cs4251_read_reg: bad reg -- %d\n", reg);
		return -EINVAL;
	}

	*data = s_Ctrl.regs[reg];
	PrDebug("reg_no=%xh, data=%xh\n", reg, *data);
	return 0;
}

/* send data to CS4251 by SPI bus */
static inline int SendToCS4251(int reg, int read_flag, int incr_flag,
						unsigned char *data, int len)
{
	char	buf[16];
	cs4251_spi_frame_t	*packet;
	int		result;

	memset(buf, 0x00, sizeof(buf));
	packet = (cs4251_spi_frame_t *)buf;

	packet->addr = CS4251_CHIP_ADDR | ((read_flag) ? CS4251_READ_BIT : 0);
	packet->map = (reg & 0x7f) | ((incr_flag) ? CS4251_MAP_INCR_BIT : 0);
	memcpy(packet->data, data, len);

	len += offsetof(cs4251_spi_frame_t, data);
	result = cs4251_spi_write(packet, len);
	if (result < len) {
		PrErr("cs4251_spi_write: result=%d\n", result);
		return -EIO;
	}
	return 0;
}

/* write a data to register */
inline int cs4251_write_reg(int reg, unsigned char data)
{
	int	result;

	/* range error or read only register ? */
	if (reg < CS4251_MIN_REG_NO || reg > CS4251_MAX_REG_NO ||
		reg == 0x01 || reg == 0x20) {
		PrErr("cs4251_write_reg: bad reg -- %d\n", reg);
		return -EINVAL;
	}

	PrDebug("reg_no=%xh, data=%xh\n", reg, data);

	result = SendToCS4251(reg, 0 /* false */, 0 /* false */, &data, 1);
	if (result == 0) {
		s_Ctrl.regs[reg] = data;
	}
	return result;
}

/* restore registers */
int cs4251_restore_regs(void)
{
	int	no, result = 0;

	for (no = 0x02; no <= 0x21; no++) {
		if (no == 0x20) continue;	/* read only */

		result = SendToCS4251(no, 0 /* false */, 0 /* false */,
						&s_Ctrl.regs[no], 1);
		if (result != 0) {
			break;
		}
	}
	return result;
}

/* read modify write to register without spinlock */
static inline int ModifyReg(int reg, unsigned char mask, unsigned char value)
{
	unsigned char	data;

	cs4251_read_reg(reg, &data);
	data &= ~mask;
	data |= (value & mask);
	return cs4251_write_reg(reg, data);
}

/* read modify write to register with spinlock */
int cs4251_modify_reg(int reg, unsigned char mask, unsigned char value)
{
	unsigned long	flags;
	int		result;

	spin_lock_irqsave(&s_Ctrl.spinlock, flags);
	result = ModifyReg(reg, mask, value);
	spin_unlock_irqrestore(&s_Ctrl.spinlock, flags);
	return result;
}

int cs4251_set_pga_vol(int a_ch, int b_ch)
{
	unsigned long	flags;
	int	result;

	spin_lock_irqsave(&s_Ctrl.spinlock, flags);

	result = ModifyReg(0x0a, CS4251_REG0A_PGAA_VOL_MASK, a_ch);
	if (result == 0) {
		result = ModifyReg(0x0b, CS4251_REG0B_PGAB_VOL_MASK, b_ch);
	}

	spin_unlock_irqrestore(&s_Ctrl.spinlock, flags);
	return result;
}

int cs4251_set_mute_adc_mix(bool_t flag)
{
	unsigned long	flags;
	int	result;

	spin_lock_irqsave(&s_Ctrl.spinlock, flags);

	result = ModifyReg(0x0e, CS4251_REG0E_MUTE_ADCMIXA,
				(flag) ? CS4251_REG0E_MUTE_ADCMIXA : 0);
	if (result == 0) {
		result = ModifyReg(0x0f, CS4251_REG0F_MUTE_ADCMIXB,
					(flag) ? CS4251_REG0F_MUTE_ADCMIXB : 0);
	}

	spin_unlock_irqrestore(&s_Ctrl.spinlock, flags);
	return result;
}

int cs4251_set_adc_mix_vol(int a_ch, int b_ch)
{
	unsigned long	flags;
	int	result;

	spin_lock_irqsave(&s_Ctrl.spinlock, flags);

	result = ModifyReg(0x0e, CS4251_REG0E_ADCMIXA_VOL_MASK, a_ch);
	if (result == 0) {
		result = ModifyReg(0x0f, CS4251_REG0F_ADCMIXB_VOL_MASK, b_ch);
	}

	spin_unlock_irqrestore(&s_Ctrl.spinlock, flags);
	return result;
}

int cs4251_set_mute_pcm_mix(bool_t flag)
{
	unsigned long	flags;
	int	result;

	spin_lock_irqsave(&s_Ctrl.spinlock, flags);

	result = ModifyReg(0x10, CS4251_REG10_MUTE_PCMMIXA,
				(flag) ? CS4251_REG10_MUTE_PCMMIXA : 0);
	if (result == 0) {
		result = ModifyReg(0x11, CS4251_REG11_MUTE_PCMMIXB,
					(flag) ? CS4251_REG11_MUTE_PCMMIXB : 0);
	}

	spin_unlock_irqrestore(&s_Ctrl.spinlock, flags);
	return result;
}

int cs4251_set_pcm_mix_vol(int a_ch, int b_ch)
{
	unsigned long	flags;
	int	result;

	spin_lock_irqsave(&s_Ctrl.spinlock, flags);

	result = ModifyReg(0x10, CS4251_REG10_PCMMIXA_VOL_MASK, a_ch);
	if (result == 0) {
		result = ModifyReg(0x11, CS4251_REG11_PCMMIXB_VOL_MASK, b_ch);
	}

	spin_unlock_irqrestore(&s_Ctrl.spinlock, flags);
	return result;
}

/* set analog output volume */
int cs4251_set_aout_vol(int a_ch, int b_ch)
{
	unsigned long	flags;
	int	result;

	spin_lock_irqsave(&s_Ctrl.spinlock, flags);

	result = cs4251_write_reg(0x16, a_ch & CS4251_REG16_AOUTA_VOL_MASK);
	if (result == 0) {
		result = cs4251_write_reg(0x17, b_ch & CS4251_REG17_AOUTB_VOL_MASK);
	}

	spin_unlock_irqrestore(&s_Ctrl.spinlock, flags);
	return result;
}

/* end of cs4251.c */
