3945 lines
98 KiB
C
3945 lines
98 KiB
C
/* Copyright (c) 2014-2015, The Linux Foundation. All rights reserved.
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 2 and
|
|
* only version 2 as published by the Free Software Foundation.
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
|
|
#define DRIVER_NAME "radio-silabs"
|
|
#define DRIVER_CARD "Silabs FM Radio Receiver"
|
|
#define DRIVER_DESC "Driver for Silabs FM Radio receiver"
|
|
|
|
#include <linux/version.h>
|
|
#include <linux/init.h> /* Initdata */
|
|
#include <linux/delay.h> /* udelay */
|
|
#include <linux/uaccess.h> /* copy to/from user */
|
|
#include <linux/kfifo.h> /* lock free circular buffer */
|
|
#include <linux/param.h>
|
|
#include <linux/i2c.h>
|
|
#include <linux/irq.h>
|
|
#include <linux/gpio.h>
|
|
#include <linux/interrupt.h>
|
|
|
|
/* kernel includes */
|
|
#include <linux/kernel.h>
|
|
#include <linux/module.h>
|
|
#include <linux/videodev2.h>
|
|
#include <linux/mutex.h>
|
|
#include <linux/unistd.h>
|
|
#include <linux/atomic.h>
|
|
#include <linux/workqueue.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/err.h>
|
|
#include <linux/pwm.h>
|
|
#include <linux/regulator/consumer.h>
|
|
#include <linux/pinctrl/consumer.h>
|
|
#include <linux/clk.h>
|
|
#include <linux/of_gpio.h>
|
|
#include <media/v4l2-common.h>
|
|
#include <media/v4l2-ioctl.h>
|
|
#include <media/v4l2-device.h>
|
|
#include "radio-silabs.h"
|
|
|
|
struct silabs_fm_device {
|
|
struct i2c_client *client;
|
|
struct pwm_device *pwm;
|
|
bool is_len_gpio_valid;
|
|
struct fm_power_vreg_data *dreg;
|
|
struct fm_power_vreg_data *areg;
|
|
int reset_gpio;
|
|
int int_gpio;
|
|
int status_gpio;
|
|
struct pinctrl *fm_pinctrl;
|
|
struct pinctrl_state *gpio_state_active;
|
|
struct pinctrl_state *gpio_state_suspend;
|
|
struct video_device *videodev;
|
|
struct v4l2_device v4l2_dev;
|
|
/* driver management */
|
|
atomic_t users;
|
|
/* To send commands*/
|
|
u8 write_buf[WRITE_REG_NUM];
|
|
/* TO read events, data*/
|
|
u8 read_buf[READ_REG_NUM];
|
|
/*RDS buffers + Radio event buffer*/
|
|
struct kfifo data_buf[SILABS_FM_BUF_MAX];
|
|
struct silabs_fm_recv_conf_req recv_conf;
|
|
struct completion sync_req_done;
|
|
/* for the first tune, we need to set properties for digital audio. */
|
|
u8 first_tune;
|
|
int tune_req;
|
|
/* 1 if tune is pending, 2 if seek is pending, 0 otherwise.*/
|
|
u8 seek_tune_status;
|
|
/* command that is being sent to chip. */
|
|
u8 cmd;
|
|
u8 antenna;
|
|
u8 g_search_mode;
|
|
bool is_search_cancelled;
|
|
unsigned int mode;
|
|
/* regional settings */
|
|
enum silabs_region_t region;
|
|
/* power mode */
|
|
bool lp_mode;
|
|
int handle_irq;
|
|
/* global lock */
|
|
struct mutex lock;
|
|
/* buffer locks*/
|
|
spinlock_t buf_lock[SILABS_FM_BUF_MAX];
|
|
/* work queue */
|
|
struct workqueue_struct *wqueue;
|
|
struct workqueue_struct *wqueue_scan;
|
|
struct workqueue_struct *wqueue_af;
|
|
struct workqueue_struct *wqueue_rds;
|
|
struct work_struct rds_worker;
|
|
struct delayed_work work;
|
|
struct delayed_work work_scan;
|
|
struct delayed_work work_af;
|
|
/* wait queue for blocking event read */
|
|
wait_queue_head_t event_queue;
|
|
/* wait queue for raw rds read */
|
|
wait_queue_head_t read_queue;
|
|
int irq;
|
|
int status_irq;
|
|
int tuned_freq_khz;
|
|
int dwell_time_sec;
|
|
u16 pi; /* PI of tuned channel */
|
|
u8 pty; /* programe type of the tuned channel */
|
|
u16 block[NO_OF_RDS_BLKS];
|
|
u8 rt_display[MAX_RT_LEN]; /* RT that will be displayed */
|
|
u8 rt_tmp0[MAX_RT_LEN]; /* high probability RT */
|
|
u8 rt_tmp1[MAX_RT_LEN]; /* low probability RT */
|
|
u8 rt_cnt[MAX_RT_LEN]; /* high probability RT's hit count */
|
|
u8 rt_flag; /* A/B flag of RT */
|
|
bool valid_rt_flg; /* validity of A/B flag */
|
|
u8 ps_display[MAX_PS_LEN]; /* PS that will be displayed */
|
|
u8 ps_tmp0[MAX_PS_LEN]; /* high probability PS */
|
|
u8 ps_tmp1[MAX_PS_LEN]; /* low probability PS */
|
|
u8 ps_cnt[MAX_PS_LEN]; /* high probability PS's hit count */
|
|
u8 rt_plus_carrier;
|
|
u8 ert_carrier;
|
|
u8 ert_buf[MAX_ERT_LEN];
|
|
u8 ert_len;
|
|
u8 c_byt_pair_index;
|
|
u8 utf_8_flag;
|
|
u8 rt_ert_flag;
|
|
u8 formatting_dir;
|
|
bool is_af_jump_enabled;
|
|
bool is_af_tune_in_progress;
|
|
u16 af_avg_th;
|
|
u8 af_wait_timer;
|
|
u8 af_rssi_th; /* allowed rssi is 0-127 */
|
|
u8 rssi_th; /* 0 - 127 */
|
|
u8 sinr_th; /* 0 - 127 */
|
|
u8 rds_fifo_cnt; /* 0 - 25 */
|
|
struct silabs_af_info af_info1;
|
|
struct silabs_af_info af_info2;
|
|
struct silabs_srch_list_compl srch_list;
|
|
};
|
|
|
|
static int silabs_fm_request_irq(struct silabs_fm_device *radio);
|
|
static int tune(struct silabs_fm_device *radio, u32 freq);
|
|
static int silabs_seek(struct silabs_fm_device *radio, int dir, int wrap);
|
|
static int cancel_seek(struct silabs_fm_device *radio);
|
|
static int configure_interrupts(struct silabs_fm_device *radio, u8 val);
|
|
static void silabs_fm_q_event(struct silabs_fm_device *radio,
|
|
enum silabs_evt_t event);
|
|
|
|
static bool is_valid_rssi(int rssi)
|
|
{
|
|
if ((rssi >= MIN_RSSI) &&
|
|
(rssi <= MAX_RSSI))
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
static bool is_valid_sinr(int sinr)
|
|
{
|
|
if ((sinr >= MIN_SNR) &&
|
|
(sinr <= MAX_SNR))
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
static bool is_valid_rds_fifo_cnt(int cnt)
|
|
{
|
|
if ((cnt >= MIN_RDS_FIFO_CNT) &&
|
|
(cnt <= MAX_RDS_FIFO_CNT))
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
static int silabs_fm_i2c_read(struct silabs_fm_device *radio, u8 len)
|
|
{
|
|
int i = 0, retval = 0;
|
|
struct i2c_msg msgs[1];
|
|
|
|
msgs[0].addr = radio->client->addr;
|
|
msgs[0].len = len;
|
|
msgs[0].flags = I2C_M_RD;
|
|
msgs[0].buf = (u8 *)radio->read_buf;
|
|
|
|
for (i = 0; i < 2; i++) {
|
|
retval = i2c_transfer(radio->client->adapter, msgs, 1);
|
|
if (retval == 1)
|
|
break;
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
static int silabs_fm_i2c_write(struct silabs_fm_device *radio, u8 len)
|
|
{
|
|
struct i2c_msg msgs[1];
|
|
int i = 0, retval = 0;
|
|
|
|
msgs[0].addr = radio->client->addr;
|
|
msgs[0].len = len;
|
|
msgs[0].flags = 0;
|
|
msgs[0].buf = (u8 *)radio->write_buf;
|
|
|
|
for (i = 0; i < 2; i++) {
|
|
retval = i2c_transfer(radio->client->adapter, msgs, 1);
|
|
if (retval == 1)
|
|
break;
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
static int silabs_fm_pinctrl_select(struct silabs_fm_device *radio, bool on)
|
|
{
|
|
struct pinctrl_state *pins_state;
|
|
int ret;
|
|
|
|
pins_state = on ? radio->gpio_state_active
|
|
: radio->gpio_state_suspend;
|
|
|
|
if (!IS_ERR_OR_NULL(pins_state)) {
|
|
ret = pinctrl_select_state(radio->fm_pinctrl, pins_state);
|
|
if (ret) {
|
|
FMDERR("%s: cannot set pin state\n", __func__);
|
|
return ret;
|
|
}
|
|
} else {
|
|
FMDERR("%s: not a valid %s pin state\n", __func__,
|
|
on ? "pmx_fm_active" : "pmx_fm_suspend");
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int fm_configure_gpios(struct silabs_fm_device *radio, bool on)
|
|
{
|
|
int rc = 0;
|
|
int fm_reset_gpio = radio->reset_gpio;
|
|
int fm_int_gpio = radio->int_gpio;
|
|
int fm_status_gpio = radio->status_gpio;
|
|
|
|
if (on) {
|
|
/*
|
|
* Turn ON sequence
|
|
* GPO1/status gpio configuration.
|
|
* Keep the GPO1 to high till device comes out of reset.
|
|
*/
|
|
if (fm_status_gpio > 0) {
|
|
FMDERR("status gpio is provided, setting it to high\n");
|
|
rc = gpio_direction_output(fm_status_gpio, 1);
|
|
if (rc) {
|
|
FMDERR("unable to set gpio %d direction(%d)\n",
|
|
fm_status_gpio, rc);
|
|
return rc;
|
|
}
|
|
/* Wait for the value to take effect on gpio. */
|
|
msleep(100);
|
|
}
|
|
|
|
/*
|
|
* GPO2/Interrupt gpio configuration.
|
|
* Keep the GPO2 to low till device comes out of reset.
|
|
*/
|
|
rc = gpio_direction_output(fm_int_gpio, 0);
|
|
if (rc) {
|
|
FMDERR("unable to set the gpio %d direction(%d)\n",
|
|
fm_int_gpio, rc);
|
|
return rc;
|
|
}
|
|
/* Wait for the value to take effect on gpio. */
|
|
msleep(100);
|
|
|
|
/*
|
|
* Reset pin configuration.
|
|
* write "0'' to make sure the chip is in reset.
|
|
*/
|
|
rc = gpio_direction_output(fm_reset_gpio, 0);
|
|
if (rc) {
|
|
FMDERR("Unable to set direction\n");
|
|
return rc;
|
|
}
|
|
/* Wait for the value to take effect on gpio. */
|
|
msleep(100);
|
|
/* write "1" to bring the chip out of reset.*/
|
|
rc = gpio_direction_output(fm_reset_gpio, 1);
|
|
if (rc) {
|
|
FMDERR("Unable to set direction\n");
|
|
return rc;
|
|
}
|
|
/* Wait for the value to take effect on gpio. */
|
|
msleep(100);
|
|
|
|
rc = gpio_direction_input(fm_int_gpio);
|
|
if (rc) {
|
|
FMDERR("unable to set the gpio %d direction(%d)\n",
|
|
fm_int_gpio, rc);
|
|
return rc;
|
|
}
|
|
/* Wait for the value to take effect on gpio. */
|
|
msleep(100);
|
|
|
|
if (fm_status_gpio > 0) {
|
|
FMDERR("setting status gpio as input\n");
|
|
rc = gpio_direction_input(fm_status_gpio);
|
|
if (rc) {
|
|
FMDERR("unable to set gpio %d direction(%d)\n",
|
|
fm_status_gpio, rc);
|
|
return rc;
|
|
}
|
|
/* Wait for the value to take effect on gpio. */
|
|
msleep(100);
|
|
}
|
|
|
|
|
|
} else {
|
|
/*Turn OFF sequence */
|
|
gpio_set_value(fm_reset_gpio, 0);
|
|
|
|
rc = gpio_direction_input(fm_reset_gpio);
|
|
if (rc)
|
|
FMDERR("Unable to set direction\n");
|
|
/* Wait for some time for the value to take effect. */
|
|
msleep(100);
|
|
if (fm_status_gpio > 0) {
|
|
rc = gpio_direction_input(fm_status_gpio);
|
|
if (rc)
|
|
FMDERR("Unable to set dir for status gpio\n");
|
|
msleep(100);
|
|
}
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
static int silabs_fm_areg_cfg(struct silabs_fm_device *radio, bool on)
|
|
{
|
|
int rc = 0;
|
|
struct fm_power_vreg_data *vreg;
|
|
|
|
vreg = radio->areg;
|
|
if (!vreg) {
|
|
FMDERR("In %s, areg is NULL\n", __func__);
|
|
return rc;
|
|
}
|
|
if (on) {
|
|
FMDBG("vreg is : %s", vreg->name);
|
|
if (vreg->set_voltage_sup) {
|
|
rc = regulator_set_voltage(vreg->reg,
|
|
vreg->low_vol_level,
|
|
vreg->high_vol_level);
|
|
if (rc < 0) {
|
|
FMDERR("set_vol(%s) fail %d\n", vreg->name, rc);
|
|
return rc;
|
|
}
|
|
}
|
|
rc = regulator_enable(vreg->reg);
|
|
if (rc < 0) {
|
|
FMDERR("reg enable(%s) failed.rc=%d\n", vreg->name, rc);
|
|
if (vreg->set_voltage_sup) {
|
|
regulator_set_voltage(vreg->reg,
|
|
0,
|
|
vreg->high_vol_level);
|
|
}
|
|
return rc;
|
|
}
|
|
vreg->is_enabled = true;
|
|
|
|
} else {
|
|
rc = regulator_disable(vreg->reg);
|
|
if (rc < 0) {
|
|
FMDERR("reg disable(%s) fail rc=%d\n", vreg->name, rc);
|
|
return rc;
|
|
}
|
|
vreg->is_enabled = false;
|
|
|
|
if (vreg->set_voltage_sup) {
|
|
/* Set the min voltage to 0 */
|
|
rc = regulator_set_voltage(vreg->reg,
|
|
0,
|
|
vreg->high_vol_level);
|
|
if (rc < 0) {
|
|
FMDERR("set_vol(%s) fail %d\n", vreg->name, rc);
|
|
return rc;
|
|
}
|
|
}
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
static int silabs_fm_dreg_cfg(struct silabs_fm_device *radio, bool on)
|
|
{
|
|
int rc = 0;
|
|
struct fm_power_vreg_data *vreg;
|
|
|
|
vreg = radio->dreg;
|
|
if (!vreg) {
|
|
FMDERR("In %s, dreg is NULL\n", __func__);
|
|
return rc;
|
|
}
|
|
|
|
if (on) {
|
|
FMDBG("vreg is : %s", vreg->name);
|
|
if (vreg->set_voltage_sup) {
|
|
rc = regulator_set_voltage(vreg->reg,
|
|
vreg->low_vol_level,
|
|
vreg->high_vol_level);
|
|
if (rc < 0) {
|
|
FMDERR("set_vol(%s) fail %d\n", vreg->name, rc);
|
|
return rc;
|
|
}
|
|
}
|
|
|
|
rc = regulator_enable(vreg->reg);
|
|
if (rc < 0) {
|
|
FMDERR("reg enable(%s) failed.rc=%d\n", vreg->name, rc);
|
|
if (vreg->set_voltage_sup) {
|
|
regulator_set_voltage(vreg->reg,
|
|
0,
|
|
vreg->high_vol_level);
|
|
}
|
|
return rc;
|
|
}
|
|
vreg->is_enabled = true;
|
|
} else {
|
|
rc = regulator_disable(vreg->reg);
|
|
if (rc < 0) {
|
|
FMDERR("reg disable(%s) fail. rc=%d\n", vreg->name, rc);
|
|
return rc;
|
|
}
|
|
vreg->is_enabled = false;
|
|
|
|
if (vreg->set_voltage_sup) {
|
|
/* Set the min voltage to 0 */
|
|
rc = regulator_set_voltage(vreg->reg,
|
|
0,
|
|
vreg->high_vol_level);
|
|
if (rc < 0) {
|
|
FMDERR("set_vol(%s) fail %d\n", vreg->name, rc);
|
|
return rc;
|
|
}
|
|
}
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
static int silabs_fm_power_cfg(struct silabs_fm_device *radio, bool on)
|
|
{
|
|
int rc = 0;
|
|
|
|
if (on) {
|
|
/* Turn ON sequence */
|
|
rc = silabs_fm_dreg_cfg(radio, on);
|
|
if (rc < 0) {
|
|
FMDERR("In %s, dreg cfg failed %x\n", __func__, rc);
|
|
return rc;
|
|
}
|
|
rc = silabs_fm_areg_cfg(radio, on);
|
|
if (rc < 0) {
|
|
FMDERR("In %s, areg cfg failed %x\n", __func__, rc);
|
|
silabs_fm_dreg_cfg(radio, false);
|
|
return rc;
|
|
}
|
|
/* If pinctrl is supported, select active state */
|
|
if (radio->fm_pinctrl) {
|
|
rc = silabs_fm_pinctrl_select(radio, true);
|
|
if (rc)
|
|
FMDERR("%s: error setting active pin state\n",
|
|
__func__);
|
|
}
|
|
|
|
rc = fm_configure_gpios(radio, on);
|
|
if (rc < 0) {
|
|
FMDERR("fm_power gpio config failed\n");
|
|
silabs_fm_dreg_cfg(radio, false);
|
|
silabs_fm_areg_cfg(radio, false);
|
|
return rc;
|
|
}
|
|
} else {
|
|
/* Turn OFF sequence */
|
|
rc = fm_configure_gpios(radio, on);
|
|
if (rc < 0)
|
|
FMDERR("fm_power gpio config failed");
|
|
|
|
/* If pinctrl is supported, select suspend state */
|
|
if (radio->fm_pinctrl) {
|
|
rc = silabs_fm_pinctrl_select(radio, false);
|
|
if (rc)
|
|
FMDERR("%s: error setting suspend pin state\n",
|
|
__func__);
|
|
}
|
|
rc = silabs_fm_dreg_cfg(radio, on);
|
|
if (rc < 0)
|
|
FMDERR("In %s, dreg cfg failed %x\n", __func__, rc);
|
|
rc = silabs_fm_areg_cfg(radio, on);
|
|
if (rc < 0)
|
|
FMDERR("In %s, areg cfg failed %x\n", __func__, rc);
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
static bool is_enable_rx_possible(struct silabs_fm_device *radio)
|
|
{
|
|
bool retval = true;
|
|
|
|
if (radio->mode == FM_OFF || radio->mode == FM_RECV)
|
|
retval = false;
|
|
|
|
return retval;
|
|
}
|
|
|
|
static int read_cts_bit(struct silabs_fm_device *radio)
|
|
{
|
|
int retval = 1, i = 0;
|
|
|
|
for (i = 0; i < CTS_RETRY_COUNT; i++) {
|
|
memset(radio->read_buf, 0, READ_REG_NUM);
|
|
|
|
retval = silabs_fm_i2c_read(radio, READ_REG_NUM);
|
|
|
|
if (retval < 0) {
|
|
FMDERR("%s: failure reading the response, error %d\n",
|
|
__func__, retval);
|
|
continue;
|
|
} else
|
|
FMDBG("%s: successfully read the response from soc\n",
|
|
__func__);
|
|
|
|
if (radio->read_buf[0] & ERR_BIT_MASK) {
|
|
FMDERR("%s: error bit set\n", __func__);
|
|
switch (radio->read_buf[1]) {
|
|
case BAD_CMD:
|
|
FMDERR("%s: cmd %d, error BAD_CMD\n",
|
|
__func__, radio->cmd);
|
|
break;
|
|
case BAD_ARG1:
|
|
FMDERR("%s: cmd %d, error BAD_ARG1\n",
|
|
__func__, radio->cmd);
|
|
break;
|
|
case BAD_ARG2:
|
|
FMDERR("%s: cmd %d, error BAD_ARG2\n",
|
|
__func__, radio->cmd);
|
|
break;
|
|
case BAD_ARG3:
|
|
FMDERR("%s: cmd %d, error BAD_ARG3\n",
|
|
__func__, radio->cmd);
|
|
break;
|
|
case BAD_ARG4:
|
|
FMDERR("%s: cmd %d, error BAD_ARG4\n",
|
|
__func__, radio->cmd);
|
|
break;
|
|
case BAD_ARG5:
|
|
FMDERR("%s: cmd %d, error BAD_ARG5\n",
|
|
__func__, radio->cmd);
|
|
case BAD_ARG6:
|
|
FMDERR("%s: cmd %d, error BAD_ARG6\n",
|
|
__func__, radio->cmd);
|
|
break;
|
|
case BAD_ARG7:
|
|
FMDERR("%s: cmd %d, error BAD_ARG7\n",
|
|
__func__, radio->cmd);
|
|
break;
|
|
case BAD_PROP:
|
|
FMDERR("%s: cmd %d, error BAD_PROP\n",
|
|
__func__, radio->cmd);
|
|
break;
|
|
case BAD_BOOT_MODE:
|
|
FMDERR("%s:cmd %d,err BAD_BOOT_MODE\n",
|
|
__func__, radio->cmd);
|
|
break;
|
|
default:
|
|
FMDERR("%s: cmd %d, unknown error\n",
|
|
__func__, radio->cmd);
|
|
break;
|
|
}
|
|
retval = -EINVAL;
|
|
goto bad_cmd_arg;
|
|
|
|
}
|
|
|
|
if (radio->read_buf[0] & CTS_INT_BIT_MASK) {
|
|
FMDBG("In %s, CTS bit is set\n", __func__);
|
|
break;
|
|
}
|
|
/*
|
|
* Give some time if the chip is not done with processing
|
|
* previous command.
|
|
*/
|
|
msleep(100);
|
|
}
|
|
|
|
FMDBG("In %s, status byte is %x\n", __func__, radio->read_buf[0]);
|
|
|
|
bad_cmd_arg:
|
|
return retval;
|
|
}
|
|
|
|
static int send_cmd(struct silabs_fm_device *radio, u8 total_len)
|
|
{
|
|
int retval = 0;
|
|
|
|
retval = silabs_fm_i2c_write(radio, total_len);
|
|
|
|
if (retval > 0) {
|
|
FMDBG("In %s, successfully written command %x to soc\n",
|
|
__func__, radio->write_buf[0]);
|
|
} else {
|
|
FMDERR("In %s, error %d writing command %d to soc\n",
|
|
__func__, retval, radio->write_buf[1]);
|
|
}
|
|
|
|
retval = read_cts_bit(radio);
|
|
|
|
return retval;
|
|
}
|
|
|
|
static int get_property(struct silabs_fm_device *radio, u16 prop, u16 *pvalue)
|
|
{
|
|
int retval = 0;
|
|
|
|
mutex_lock(&radio->lock);
|
|
memset(radio->write_buf, 0, WRITE_REG_NUM);
|
|
|
|
/* track command that is being sent to chip. */
|
|
radio->cmd = GET_PROPERTY_CMD;
|
|
radio->write_buf[0] = GET_PROPERTY_CMD;
|
|
/* reserved, always write 0 */
|
|
radio->write_buf[1] = 0;
|
|
/* property high byte */
|
|
radio->write_buf[2] = HIGH_BYTE_16BIT(prop);
|
|
/* property low byte */
|
|
radio->write_buf[3] = LOW_BYTE_16BIT(prop);
|
|
|
|
FMDBG("in %s, radio->write_buf[2] is %x\n",
|
|
__func__, radio->write_buf[2]);
|
|
FMDBG("in %s, radio->write_buf[3] is %x\n",
|
|
__func__, radio->write_buf[3]);
|
|
|
|
retval = send_cmd(radio, GET_PROP_CMD_LEN);
|
|
if (retval < 0)
|
|
FMDERR("In %s, error getting property %d\n", __func__, prop);
|
|
else
|
|
*pvalue = (radio->read_buf[2] << 8) + radio->read_buf[3];
|
|
|
|
mutex_unlock(&radio->lock);
|
|
|
|
return retval;
|
|
}
|
|
|
|
|
|
static int set_property(struct silabs_fm_device *radio, u16 prop, u16 value)
|
|
{
|
|
int retval = 0;
|
|
|
|
mutex_lock(&radio->lock);
|
|
|
|
memset(radio->write_buf, 0, WRITE_REG_NUM);
|
|
|
|
/* track command that is being sent to chip. */
|
|
radio->cmd = SET_PROPERTY_CMD;
|
|
radio->write_buf[0] = SET_PROPERTY_CMD;
|
|
/* reserved, always write 0 */
|
|
radio->write_buf[1] = 0;
|
|
/* property high byte */
|
|
radio->write_buf[2] = HIGH_BYTE_16BIT(prop);
|
|
/* property low byte */
|
|
radio->write_buf[3] = LOW_BYTE_16BIT(prop);
|
|
|
|
/* value high byte */
|
|
radio->write_buf[4] = HIGH_BYTE_16BIT(value);
|
|
/* value low byte */
|
|
radio->write_buf[5] = LOW_BYTE_16BIT(value);
|
|
|
|
retval = send_cmd(radio, SET_PROP_CMD_LEN);
|
|
if (retval < 0)
|
|
FMDERR("In %s, error setting property %d\n", __func__, prop);
|
|
|
|
mutex_unlock(&radio->lock);
|
|
|
|
return retval;
|
|
}
|
|
|
|
static void update_search_list(struct silabs_fm_device *radio, int freq)
|
|
{
|
|
int temp_freq = freq;
|
|
|
|
temp_freq = temp_freq -
|
|
(radio->recv_conf.band_low_limit * TUNE_STEP_SIZE);
|
|
temp_freq = temp_freq / 50;
|
|
radio->srch_list.rel_freq[radio->srch_list.num_stations_found].
|
|
rel_freq_lsb = GET_LSB(temp_freq);
|
|
radio->srch_list.rel_freq[radio->srch_list.num_stations_found].
|
|
rel_freq_msb = GET_MSB(temp_freq);
|
|
radio->srch_list.num_stations_found++;
|
|
}
|
|
|
|
static void silabs_scan(struct work_struct *work)
|
|
{
|
|
struct silabs_fm_device *radio;
|
|
int current_freq_khz;
|
|
u8 valid;
|
|
u8 bltf;
|
|
u32 temp_freq_khz;
|
|
int retval = 0;
|
|
struct kfifo *data_b;
|
|
int len = 0;
|
|
|
|
FMDBG("+%s, getting radio handle from work struct\n", __func__);
|
|
radio = container_of(work, struct silabs_fm_device, work_scan.work);
|
|
|
|
if (unlikely(radio == NULL)) {
|
|
FMDERR(":radio is null");
|
|
return;
|
|
}
|
|
|
|
current_freq_khz = radio->tuned_freq_khz;
|
|
FMDBG("current freq is %d\n", current_freq_khz);
|
|
|
|
radio->seek_tune_status = SCAN_PENDING;
|
|
/* tune to lowest freq of the band */
|
|
retval = tune(radio, radio->recv_conf.band_low_limit * TUNE_STEP_SIZE);
|
|
if (retval < 0) {
|
|
FMDERR("%s: Tune to lower band limit failed with error %d\n",
|
|
__func__, retval);
|
|
goto seek_tune_fail;
|
|
}
|
|
|
|
/* wait for tune to complete. */
|
|
if (!wait_for_completion_timeout(&radio->sync_req_done,
|
|
msecs_to_jiffies(WAIT_TIMEOUT_MSEC)))
|
|
FMDERR("In %s, didn't receive STC for tune\n", __func__);
|
|
else
|
|
FMDBG("In %s, received STC for tune\n", __func__);
|
|
while (1) {
|
|
/* If scan is cancelled or FM is not ON, break */
|
|
if (radio->is_search_cancelled == true) {
|
|
FMDBG("%s: scan cancelled\n", __func__);
|
|
if (radio->g_search_mode == SCAN_FOR_STRONG)
|
|
goto seek_tune_fail;
|
|
else
|
|
goto seek_cancelled;
|
|
} else if (radio->mode != FM_RECV) {
|
|
FMDERR("%s: FM is not in proper state\n", __func__);
|
|
return;
|
|
}
|
|
|
|
retval = silabs_seek(radio, SRCH_DIR_UP, WRAP_DISABLE);
|
|
if (retval < 0) {
|
|
FMDERR("Scan operation failed with error %d\n", retval);
|
|
goto seek_tune_fail;
|
|
}
|
|
/* wait for seek to complete */
|
|
if (!wait_for_completion_timeout(&radio->sync_req_done,
|
|
msecs_to_jiffies(WAIT_TIMEOUT_MSEC))) {
|
|
FMDERR("%s: didn't receive STC for seek\n", __func__);
|
|
/* FM is not correct state or scan is cancelled */
|
|
continue;
|
|
} else
|
|
FMDBG("%s: received STC for seek\n", __func__);
|
|
|
|
mutex_lock(&radio->lock);
|
|
memset(radio->write_buf, 0, WRITE_REG_NUM);
|
|
|
|
radio->cmd = FM_TUNE_STATUS_CMD;
|
|
|
|
radio->write_buf[0] = FM_TUNE_STATUS_CMD;
|
|
radio->write_buf[1] = 0;
|
|
|
|
retval = send_cmd(radio, TUNE_STATUS_CMD_LEN);
|
|
if (retval < 0) {
|
|
FMDERR("%s: FM_TUNE_STATUS_CMD failed with error %d\n",
|
|
__func__, retval);
|
|
}
|
|
|
|
valid = radio->read_buf[1] & VALID_MASK;
|
|
bltf = radio->read_buf[1] & BLTF_MASK;
|
|
|
|
temp_freq_khz = ((u32)(radio->read_buf[2] << 8) +
|
|
radio->read_buf[3])*
|
|
TUNE_STEP_SIZE;
|
|
mutex_unlock(&radio->lock);
|
|
FMDBG("In %s, freq is %d\n", __func__, temp_freq_khz);
|
|
|
|
if ((valid) && (radio->g_search_mode == SCAN)) {
|
|
FMDBG("val bit set, posting SILABS_EVT_TUNE_SUCC\n");
|
|
silabs_fm_q_event(radio, SILABS_EVT_TUNE_SUCC);
|
|
}
|
|
|
|
if (bltf) {
|
|
FMDBG("bltf bit is set\n");
|
|
break;
|
|
}
|
|
/*
|
|
* If scan is cancelled or FM is not ON, break ASAP so that we
|
|
* don't need to sleep for dwell time.
|
|
*/
|
|
if (radio->is_search_cancelled == true) {
|
|
FMDBG("%s: scan cancelled\n", __func__);
|
|
if (radio->g_search_mode == SCAN_FOR_STRONG)
|
|
goto seek_tune_fail;
|
|
else
|
|
goto seek_cancelled;
|
|
} else if (radio->mode != FM_RECV) {
|
|
FMDERR("%s: FM is not in proper state\n", __func__);
|
|
return;
|
|
}
|
|
|
|
if (radio->g_search_mode == SCAN) {
|
|
/* sleep for dwell period */
|
|
msleep(radio->dwell_time_sec * 1000);
|
|
/* need to queue the event when the seek completes */
|
|
silabs_fm_q_event(radio, SILABS_EVT_SCAN_NEXT);
|
|
} else if ((valid) &&
|
|
(radio->g_search_mode == SCAN_FOR_STRONG)) {
|
|
update_search_list(radio, temp_freq_khz);
|
|
}
|
|
}
|
|
|
|
seek_tune_fail:
|
|
if (radio->g_search_mode == SCAN_FOR_STRONG) {
|
|
len = radio->srch_list.num_stations_found * 2 +
|
|
sizeof(radio->srch_list.num_stations_found);
|
|
data_b = &radio->data_buf[SILABS_FM_BUF_SRCH_LIST];
|
|
kfifo_in_locked(data_b, &radio->srch_list, len,
|
|
&radio->buf_lock[SILABS_FM_BUF_SRCH_LIST]);
|
|
silabs_fm_q_event(radio, SILABS_EVT_NEW_SRCH_LIST);
|
|
}
|
|
/* tune to original frequency */
|
|
retval = tune(radio, current_freq_khz);
|
|
if (retval < 0)
|
|
FMDERR("%s: Tune to orig freq failed with error %d\n",
|
|
__func__, retval);
|
|
else {
|
|
if (!wait_for_completion_timeout(&radio->sync_req_done,
|
|
msecs_to_jiffies(WAIT_TIMEOUT_MSEC)))
|
|
FMDERR("%s: didn't receive STC for tune\n", __func__);
|
|
else
|
|
FMDBG("%s: received STC for tune\n", __func__);
|
|
}
|
|
seek_cancelled:
|
|
silabs_fm_q_event(radio, SILABS_EVT_SEEK_COMPLETE);
|
|
radio->seek_tune_status = NO_SEEK_TUNE_PENDING;
|
|
}
|
|
|
|
static void silabs_search(struct silabs_fm_device *radio, bool on)
|
|
{
|
|
int current_freq_khz;
|
|
|
|
current_freq_khz = radio->tuned_freq_khz;
|
|
|
|
if (on) {
|
|
FMDBG("%s: Queuing the work onto scan work q\n", __func__);
|
|
queue_delayed_work(radio->wqueue_scan, &radio->work_scan,
|
|
msecs_to_jiffies(SILABS_DELAY_MSEC));
|
|
} else {
|
|
cancel_seek(radio);
|
|
silabs_fm_q_event(radio, SILABS_EVT_SEEK_COMPLETE);
|
|
}
|
|
}
|
|
|
|
static void get_rds_status(struct silabs_fm_device *radio)
|
|
{
|
|
int retval = 0;
|
|
|
|
mutex_lock(&radio->lock);
|
|
memset(radio->write_buf, 0, WRITE_REG_NUM);
|
|
radio->cmd = FM_RDS_STATUS_CMD;
|
|
radio->write_buf[0] = FM_RDS_STATUS_CMD;
|
|
radio->write_buf[1] |= FM_RDS_STATUS_IN_INTACK;
|
|
|
|
retval = send_cmd(radio, RDS_CMD_LEN);
|
|
if (retval < 0) {
|
|
FMDERR("In %s, Get RDS failed %d\n", __func__, retval);
|
|
mutex_unlock(&radio->lock);
|
|
return;
|
|
}
|
|
|
|
memset(radio->read_buf, 0, sizeof(radio->read_buf));
|
|
|
|
retval = silabs_fm_i2c_read(radio, RDS_RSP_LEN);
|
|
|
|
if (retval < 0) {
|
|
FMDERR("In %s, failed to read the resp from soc %d\n",
|
|
__func__, retval);
|
|
mutex_unlock(&radio->lock);
|
|
return;
|
|
} else {
|
|
FMDBG("In %s, successfully read the response from soc\n",
|
|
__func__);
|
|
}
|
|
|
|
radio->block[0] = ((u16)radio->read_buf[MSB_OF_BLK_0] << 8) |
|
|
(u16)radio->read_buf[LSB_OF_BLK_0];
|
|
radio->block[1] = ((u16)radio->read_buf[MSB_OF_BLK_1] << 8) |
|
|
(u16)radio->read_buf[LSB_OF_BLK_1];
|
|
radio->block[2] = ((u16)radio->read_buf[MSB_OF_BLK_2] << 8) |
|
|
(u16)radio->read_buf[LSB_OF_BLK_2];
|
|
radio->block[3] = ((u16)radio->read_buf[MSB_OF_BLK_3] << 8) |
|
|
(u16)radio->read_buf[LSB_OF_BLK_3];
|
|
mutex_unlock(&radio->lock);
|
|
}
|
|
|
|
static void pi_handler(struct silabs_fm_device *radio, u16 current_pi)
|
|
{
|
|
if (radio->pi != current_pi) {
|
|
FMDBG("PI code of radio->block[0] = %x\n", current_pi);
|
|
radio->pi = current_pi;
|
|
} else {
|
|
FMDBG(" Received same PI code\n");
|
|
}
|
|
}
|
|
|
|
static void pty_handler(struct silabs_fm_device *radio, u8 current_pty)
|
|
{
|
|
if (radio->pty != current_pty) {
|
|
FMDBG("PTY code of radio->block[1] = %x\n", current_pty);
|
|
radio->pty = current_pty;
|
|
} else {
|
|
FMDBG("PTY repeated\n");
|
|
}
|
|
}
|
|
|
|
static void update_ps(struct silabs_fm_device *radio, u8 addr, u8 ps)
|
|
{
|
|
u8 i;
|
|
bool ps_txt_chg = false;
|
|
bool ps_cmplt = true;
|
|
u8 *data;
|
|
struct kfifo *data_b;
|
|
|
|
if (radio->ps_tmp0[addr] == ps) {
|
|
if (radio->ps_cnt[addr] < PS_VALIDATE_LIMIT) {
|
|
radio->ps_cnt[addr]++;
|
|
} else {
|
|
radio->ps_cnt[addr] = PS_VALIDATE_LIMIT;
|
|
radio->ps_tmp1[addr] = ps;
|
|
}
|
|
} else if (radio->ps_tmp1[addr] == ps) {
|
|
if (radio->ps_cnt[addr] >= PS_VALIDATE_LIMIT) {
|
|
ps_txt_chg = true;
|
|
radio->ps_cnt[addr] = PS_VALIDATE_LIMIT + 1;
|
|
} else {
|
|
radio->ps_cnt[addr] = PS_VALIDATE_LIMIT;
|
|
}
|
|
radio->ps_tmp1[addr] = radio->ps_tmp0[addr];
|
|
radio->ps_tmp0[addr] = ps;
|
|
} else if (!radio->ps_cnt[addr]) {
|
|
radio->ps_tmp0[addr] = ps;
|
|
radio->ps_cnt[addr] = 1;
|
|
} else {
|
|
radio->ps_tmp1[addr] = ps;
|
|
}
|
|
|
|
if (ps_txt_chg) {
|
|
for (i = 0; i < MAX_PS_LEN; i++) {
|
|
if (radio->ps_cnt[i] > 1)
|
|
radio->ps_cnt[i]--;
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < MAX_PS_LEN; i++) {
|
|
if (radio->ps_cnt[i] < PS_VALIDATE_LIMIT) {
|
|
ps_cmplt = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (ps_cmplt) {
|
|
for (i = 0; (i < MAX_PS_LEN) &&
|
|
(radio->ps_display[i] == radio->ps_tmp0[i]); i++)
|
|
;
|
|
if (i == MAX_PS_LEN) {
|
|
FMDBG("Same PS string repeated\n");
|
|
return;
|
|
}
|
|
|
|
for (i = 0; i < MAX_PS_LEN; i++)
|
|
radio->ps_display[i] = radio->ps_tmp0[i];
|
|
|
|
data = kmalloc(PS_EVT_DATA_LEN, GFP_ATOMIC);
|
|
if (data != NULL) {
|
|
data[0] = NO_OF_PS;
|
|
data[1] = radio->pty;
|
|
data[2] = (radio->pi >> 8) & 0xFF;
|
|
data[3] = (radio->pi & 0xFF);
|
|
data[4] = 0;
|
|
memcpy(data + OFFSET_OF_PS,
|
|
radio->ps_tmp0, MAX_PS_LEN);
|
|
data_b = &radio->data_buf[SILABS_FM_BUF_PS_RDS];
|
|
kfifo_in_locked(data_b, data, PS_EVT_DATA_LEN,
|
|
&radio->buf_lock[SILABS_FM_BUF_PS_RDS]);
|
|
FMDBG("Q the PS event\n");
|
|
silabs_fm_q_event(radio, SILABS_EVT_NEW_PS_RDS);
|
|
kfree(data);
|
|
} else {
|
|
FMDERR("Memory allocation failed for PTY\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
static void display_rt(struct silabs_fm_device *radio)
|
|
{
|
|
u8 len = 0, i = 0;
|
|
u8 *data;
|
|
struct kfifo *data_b;
|
|
bool rt_cmplt = true;
|
|
|
|
for (i = 0; i < MAX_RT_LEN; i++) {
|
|
if (radio->rt_cnt[i] < RT_VALIDATE_LIMIT) {
|
|
rt_cmplt = false;
|
|
return;
|
|
}
|
|
if (radio->rt_tmp0[i] == END_OF_RT)
|
|
break;
|
|
}
|
|
|
|
if (rt_cmplt) {
|
|
while ((len < MAX_RT_LEN) && (radio->rt_tmp0[len] != END_OF_RT))
|
|
len++;
|
|
|
|
for (i = 0; (i < len) &&
|
|
(radio->rt_display[i] == radio->rt_tmp0[i]); i++)
|
|
;
|
|
if (i == len) {
|
|
FMDBG("Same RT string repeated\n");
|
|
return;
|
|
}
|
|
for (i = 0; i < len; i++)
|
|
radio->rt_display[i] = radio->rt_tmp0[i];
|
|
data = kmalloc(len + OFFSET_OF_RT, GFP_ATOMIC);
|
|
if (data != NULL) {
|
|
data[0] = len; /* len of RT */
|
|
data[1] = radio->pty;
|
|
data[2] = (radio->pi >> 8) & 0xFF;
|
|
data[3] = (radio->pi & 0xFF);
|
|
data[4] = radio->rt_flag;
|
|
memcpy(data + OFFSET_OF_RT, radio->rt_display, len);
|
|
data_b = &radio->data_buf[SILABS_FM_BUF_RT_RDS];
|
|
kfifo_in_locked(data_b, data, OFFSET_OF_RT + len,
|
|
&radio->buf_lock[SILABS_FM_BUF_RT_RDS]);
|
|
FMDBG("Q the RT event\n");
|
|
silabs_fm_q_event(radio, SILABS_EVT_NEW_RT_RDS);
|
|
kfree(data);
|
|
} else {
|
|
FMDERR("Memory allocation failed for PTY\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
static void rt_handler(struct silabs_fm_device *radio, u8 ab_flg,
|
|
u8 cnt, u8 addr, u8 *rt)
|
|
{
|
|
u8 i;
|
|
bool rt_txt_chg = 0;
|
|
|
|
if (ab_flg != radio->rt_flag && radio->valid_rt_flg) {
|
|
for (i = 0; i < sizeof(radio->rt_cnt); i++) {
|
|
if (!radio->rt_tmp0[i]) {
|
|
radio->rt_tmp0[i] = ' ';
|
|
radio->rt_cnt[i]++;
|
|
}
|
|
}
|
|
memset(radio->rt_cnt, 0, sizeof(radio->rt_cnt));
|
|
memset(radio->rt_tmp0, 0, sizeof(radio->rt_tmp0));
|
|
memset(radio->rt_tmp1, 0, sizeof(radio->rt_tmp1));
|
|
}
|
|
|
|
radio->rt_flag = ab_flg;
|
|
radio->valid_rt_flg = true;
|
|
|
|
for (i = 0; i < cnt; i++) {
|
|
if (radio->rt_tmp0[addr+i] == rt[i]) {
|
|
if (radio->rt_cnt[addr+i] < RT_VALIDATE_LIMIT) {
|
|
radio->rt_cnt[addr+i]++;
|
|
} else {
|
|
radio->rt_cnt[addr+i] = RT_VALIDATE_LIMIT;
|
|
radio->rt_tmp1[addr+i] = rt[i];
|
|
}
|
|
} else if (radio->rt_tmp1[addr+i] == rt[i]) {
|
|
if (radio->rt_cnt[addr+i] >= RT_VALIDATE_LIMIT) {
|
|
rt_txt_chg = true;
|
|
radio->rt_cnt[addr+i] = RT_VALIDATE_LIMIT + 1;
|
|
} else {
|
|
radio->rt_cnt[addr+i] = RT_VALIDATE_LIMIT;
|
|
}
|
|
radio->rt_tmp1[addr+i] = radio->rt_tmp0[addr+i];
|
|
radio->rt_tmp0[addr+i] = rt[i];
|
|
} else if (!radio->rt_cnt[addr+i]) {
|
|
radio->rt_tmp0[addr+i] = rt[i];
|
|
radio->rt_cnt[addr+i] = 1;
|
|
} else {
|
|
radio->rt_tmp1[addr+i] = rt[i];
|
|
}
|
|
}
|
|
|
|
if (rt_txt_chg) {
|
|
for (i = 0; i < MAX_RT_LEN; i++) {
|
|
if (radio->rt_cnt[i] > 1)
|
|
radio->rt_cnt[i]--;
|
|
}
|
|
}
|
|
display_rt(radio);
|
|
}
|
|
|
|
static void silabs_ev_ert(struct silabs_fm_device *radio)
|
|
{
|
|
u8 *data = NULL;
|
|
struct kfifo *data_b;
|
|
|
|
if (radio->ert_len <= 0)
|
|
return;
|
|
|
|
data = kmalloc((radio->ert_len + ERT_OFFSET), GFP_ATOMIC);
|
|
if (data != NULL) {
|
|
data[0] = radio->ert_len;
|
|
data[1] = radio->utf_8_flag;
|
|
data[2] = radio->formatting_dir;
|
|
memcpy((data + ERT_OFFSET), radio->ert_buf, radio->ert_len);
|
|
data_b = &radio->data_buf[SILABS_FM_BUF_ERT];
|
|
kfifo_in_locked(data_b, data, (radio->ert_len + ERT_OFFSET),
|
|
&radio->buf_lock[SILABS_FM_BUF_ERT]);
|
|
silabs_fm_q_event(radio, SILABS_EVT_NEW_ERT);
|
|
kfree(data);
|
|
}
|
|
}
|
|
|
|
static void silabs_buff_ert(struct silabs_fm_device *radio)
|
|
{
|
|
int i;
|
|
u16 info_byte = 0;
|
|
u8 byte_pair_index;
|
|
|
|
byte_pair_index = radio->block[1] & APP_GRP_typ_MASK;
|
|
if (byte_pair_index == 0) {
|
|
radio->c_byt_pair_index = 0;
|
|
radio->ert_len = 0;
|
|
}
|
|
FMDBG("c_byt_pair_index = %x\n", radio->c_byt_pair_index);
|
|
if (radio->c_byt_pair_index == byte_pair_index) {
|
|
for (i = 2; i <= 3; i++) {
|
|
info_byte = radio->block[i];
|
|
FMDBG("info_byte = %x\n", info_byte);
|
|
FMDBG("ert_len = %x\n", radio->ert_len);
|
|
if (radio->ert_len > (MAX_ERT_LEN - 2))
|
|
return;
|
|
radio->ert_buf[radio->ert_len] = radio->block[i] >> 8;
|
|
radio->ert_buf[radio->ert_len + 1] =
|
|
radio->block[i] & 0xFF;
|
|
radio->ert_len += ERT_CNT_PER_BLK;
|
|
FMDBG("utf_8_flag = %d\n", radio->utf_8_flag);
|
|
if ((radio->utf_8_flag == 0) &&
|
|
(info_byte == END_OF_RT)) {
|
|
radio->ert_len -= ERT_CNT_PER_BLK;
|
|
break;
|
|
} else if ((radio->utf_8_flag == 1) &&
|
|
(radio->block[i] >> 8 == END_OF_RT)) {
|
|
info_byte = END_OF_RT;
|
|
radio->ert_len -= ERT_CNT_PER_BLK;
|
|
break;
|
|
} else if ((radio->utf_8_flag == 1) &&
|
|
((radio->block[i] & 0xFF)
|
|
== END_OF_RT)) {
|
|
info_byte = END_OF_RT;
|
|
radio->ert_len--;
|
|
break;
|
|
}
|
|
}
|
|
if ((byte_pair_index == MAX_ERT_SEGMENT) ||
|
|
(info_byte == END_OF_RT)) {
|
|
silabs_ev_ert(radio);
|
|
radio->c_byt_pair_index = 0;
|
|
radio->ert_len = 0;
|
|
}
|
|
radio->c_byt_pair_index++;
|
|
} else {
|
|
radio->ert_len = 0;
|
|
radio->c_byt_pair_index = 0;
|
|
}
|
|
}
|
|
|
|
|
|
static void silabs_rt_plus(struct silabs_fm_device *radio)
|
|
{
|
|
u8 tag_type1, tag_type2;
|
|
u8 *data = NULL;
|
|
int len = 0;
|
|
u16 grp_typ;
|
|
struct kfifo *data_b;
|
|
|
|
grp_typ = radio->block[1] & APP_GRP_typ_MASK;
|
|
/*
|
|
*right most 3 bits of Lsb of block 2
|
|
* and left most 3 bits of Msb of block 3
|
|
*/
|
|
tag_type1 = (((grp_typ & TAG1_MSB_MASK) << TAG1_MSB_OFFSET) |
|
|
(radio->block[2] >> TAG1_LSB_OFFSET));
|
|
/*
|
|
*right most 1 bit of lsb of 3rd block
|
|
* and left most 5 bits of Msb of 4th block
|
|
*/
|
|
tag_type2 = (((radio->block[2] & TAG2_MSB_MASK)
|
|
<< TAG2_MSB_OFFSET) |
|
|
(radio->block[2] >> TAG2_LSB_OFFSET));
|
|
|
|
if (tag_type1 != DUMMY_CLASS)
|
|
len += RT_PLUS_LEN_1_TAG;
|
|
if (tag_type2 != DUMMY_CLASS)
|
|
len += RT_PLUS_LEN_1_TAG;
|
|
|
|
if (len != 0) {
|
|
len += RT_PLUS_OFFSET;
|
|
data = kmalloc(len, GFP_ATOMIC);
|
|
} else {
|
|
FMDERR("%s:Len is zero\n", __func__);
|
|
return;
|
|
}
|
|
if (data != NULL) {
|
|
data[0] = len;
|
|
len = RT_ERT_FLAG_OFFSET;
|
|
data[len++] = radio->rt_ert_flag;
|
|
if (tag_type1 != DUMMY_CLASS) {
|
|
data[len++] = tag_type1;
|
|
/*
|
|
*start position of tag1
|
|
*right most 5 bits of msb of 3rd block
|
|
*and left most bit of lsb of 3rd block
|
|
*/
|
|
data[len++] = (radio->block[2] >> TAG1_POS_LSB_OFFSET)
|
|
& TAG1_POS_MSB_MASK;
|
|
/*
|
|
*length of tag1
|
|
*left most 6 bits of lsb of 3rd block
|
|
*/
|
|
data[len++] = (radio->block[2] >> TAG1_LEN_OFFSET) &
|
|
TAG1_LEN_MASK;
|
|
}
|
|
if (tag_type2 != DUMMY_CLASS) {
|
|
data[len++] = tag_type2;
|
|
/*
|
|
*start position of tag2
|
|
*right most 3 bit of msb of 4th block
|
|
*and left most 3 bits of lsb of 4th block
|
|
*/
|
|
data[len++] = (radio->block[3] >> TAG2_POS_LSB_OFFSET) &
|
|
TAG2_POS_MSB_MASK;
|
|
/*
|
|
*length of tag2
|
|
*right most 5 bits of lsb of 4th block
|
|
*/
|
|
data[len++] = radio->block[3] & TAG2_LEN_MASK;
|
|
}
|
|
data_b = &radio->data_buf[SILABS_FM_BUF_RT_PLUS];
|
|
kfifo_in_locked(data_b, data, len,
|
|
&radio->buf_lock[SILABS_FM_BUF_RT_PLUS]);
|
|
silabs_fm_q_event(radio, SILABS_EVT_NEW_RT_PLUS);
|
|
kfree(data);
|
|
} else {
|
|
FMDERR("%s:memory allocation failed\n", __func__);
|
|
}
|
|
}
|
|
|
|
static void silabs_raw_rds_handler(struct silabs_fm_device *radio)
|
|
{
|
|
u16 aid, app_grp_typ;
|
|
|
|
aid = radio->block[3];
|
|
app_grp_typ = radio->block[1] & APP_GRP_typ_MASK;
|
|
FMDBG("app_grp_typ = %x\n", app_grp_typ);
|
|
FMDBG("AID = %x", aid);
|
|
|
|
switch (aid) {
|
|
case ERT_AID:
|
|
radio->utf_8_flag = (radio->block[2] & 1);
|
|
radio->formatting_dir = EXTRACT_BIT(radio->block[2],
|
|
ERT_FORMAT_DIR_BIT);
|
|
if (radio->ert_carrier != app_grp_typ) {
|
|
silabs_fm_q_event(radio, SILABS_EVT_NEW_ODA);
|
|
radio->ert_carrier = app_grp_typ;
|
|
}
|
|
break;
|
|
case RT_PLUS_AID:
|
|
/*Extract 5th bit of MSB (b7b6b5b4b3b2b1b0)*/
|
|
radio->rt_ert_flag = EXTRACT_BIT(radio->block[2],
|
|
RT_ERT_FLAG_BIT);
|
|
if (radio->rt_plus_carrier != app_grp_typ) {
|
|
silabs_fm_q_event(radio, SILABS_EVT_NEW_ODA);
|
|
radio->rt_plus_carrier = app_grp_typ;
|
|
}
|
|
break;
|
|
default:
|
|
FMDBG("Not handling the AID of %x\n", aid);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static int set_hard_mute(struct silabs_fm_device *radio, bool val)
|
|
{
|
|
int retval = 0;
|
|
|
|
if (val == true) {
|
|
retval = set_property(radio, RX_HARD_MUTE_PROP, HARD_MUTE_MASK);
|
|
|
|
if (retval < 0)
|
|
FMDERR("%s: set_hard_mute failed with error %d\n",
|
|
__func__, retval);
|
|
} else {
|
|
retval = set_property(radio, RX_HARD_MUTE_PROP, 0);
|
|
|
|
if (retval < 0)
|
|
FMDERR("%s: set_hard_mute failed with error %d\n",
|
|
__func__, retval);
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
static int get_rssi(struct silabs_fm_device *radio, u8 *prssi)
|
|
{
|
|
int retval = 0;
|
|
|
|
mutex_lock(&radio->lock);
|
|
|
|
memset(radio->write_buf, 0, WRITE_REG_NUM);
|
|
|
|
/* track command that is being sent to chip.*/
|
|
radio->cmd = FM_RSQ_STATUS_CMD;
|
|
radio->write_buf[0] = FM_RSQ_STATUS_CMD;
|
|
radio->write_buf[1] = 1;
|
|
|
|
retval = send_cmd(radio, RSQ_STATUS_CMD_LEN);
|
|
|
|
if (retval < 0)
|
|
FMDERR("%s: get_rsq_status failed with error %d\n",
|
|
__func__, retval);
|
|
|
|
FMDBG("%s: rssi is %d\n", __func__, radio->read_buf[4]);
|
|
*prssi = radio->read_buf[4];
|
|
mutex_unlock(&radio->lock);
|
|
|
|
return retval;
|
|
}
|
|
|
|
static bool is_valid_freq(struct silabs_fm_device *radio, u32 freq)
|
|
{
|
|
u32 band_low_limit = radio->recv_conf.band_low_limit * TUNE_STEP_SIZE;
|
|
u32 band_high_limit = radio->recv_conf.band_high_limit * TUNE_STEP_SIZE;
|
|
u8 spacing;
|
|
|
|
if (radio->recv_conf.ch_spacing == 0)
|
|
spacing = CH_SPACING_200;
|
|
else if (radio->recv_conf.ch_spacing == 1)
|
|
spacing = CH_SPACING_100;
|
|
else if (radio->recv_conf.ch_spacing == 2)
|
|
spacing = CH_SPACING_50;
|
|
|
|
if ((freq >= band_low_limit) &&
|
|
(freq <= band_high_limit) &&
|
|
((freq - band_low_limit) % spacing == 0))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool is_new_freq(struct silabs_fm_device *radio, u32 freq)
|
|
{
|
|
u8 i = 0;
|
|
|
|
for (i = 0; i < radio->af_info2.size; i++) {
|
|
if (freq == radio->af_info2.af_list[i])
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool is_different_af_list(struct silabs_fm_device *radio)
|
|
{
|
|
u8 i = 0, j = 0;
|
|
u32 freq;
|
|
|
|
if (radio->af_info1.orig_freq_khz != radio->af_info2.orig_freq_khz)
|
|
return true;
|
|
|
|
/* freq is same, check if the AFs are same. */
|
|
for (i = 0; i < radio->af_info1.size; i++) {
|
|
freq = radio->af_info1.af_list[i];
|
|
for (j = 0; j < radio->af_info2.size; j++) {
|
|
if (freq == radio->af_info2.af_list[j])
|
|
break;
|
|
}
|
|
|
|
/* freq is not there in list2 i.e list1, list2 are different.*/
|
|
if (j == radio->af_info2.size)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static void reset_af_info(struct silabs_fm_device *radio)
|
|
{
|
|
radio->af_info1.inval_freq_cnt = 0;
|
|
radio->af_info1.cnt = 0;
|
|
radio->af_info1.index = 0;
|
|
radio->af_info1.size = 0;
|
|
radio->af_info1.orig_freq_khz = 0;
|
|
memset(radio->af_info1.af_list, 0, sizeof(radio->af_info1.af_list));
|
|
|
|
radio->af_info2.inval_freq_cnt = 0;
|
|
radio->af_info2.cnt = 0;
|
|
radio->af_info2.index = 0;
|
|
radio->af_info2.size = 0;
|
|
radio->af_info2.orig_freq_khz = 0;
|
|
memset(radio->af_info2.af_list, 0, sizeof(radio->af_info2.af_list));
|
|
}
|
|
|
|
static void update_af_list(struct silabs_fm_device *radio)
|
|
{
|
|
bool retval;
|
|
u8 i = 0;
|
|
u8 af_data = radio->block[2] >> 8;
|
|
u32 af_freq_khz;
|
|
|
|
struct kfifo *buff;
|
|
struct af_list_ev ev;
|
|
spinlock_t lock = radio->buf_lock[SILABS_FM_BUF_AF_LIST];
|
|
|
|
for (; i < NO_OF_AF_IN_GRP; i++, af_data = radio->block[2] & 0xFF) {
|
|
|
|
if (af_data >= MIN_AF_CNT_CODE && af_data <= MAX_AF_CNT_CODE) {
|
|
|
|
FMDBG("%s: resetting af info, freq %u, pi %u\n",
|
|
__func__, radio->tuned_freq_khz, radio->pi);
|
|
radio->af_info2.inval_freq_cnt = 0;
|
|
radio->af_info2.cnt = 0;
|
|
radio->af_info2.orig_freq_khz = 0;
|
|
|
|
/* AF count. */
|
|
radio->af_info2.cnt = af_data - NO_AF_CNT_CODE;
|
|
radio->af_info2.orig_freq_khz = radio->tuned_freq_khz;
|
|
radio->af_info2.pi = radio->pi;
|
|
|
|
FMDBG("%s: current freq is %u, AF cnt is %u\n",
|
|
__func__, radio->tuned_freq_khz, radio->af_info2.cnt);
|
|
|
|
} else if (af_data >= MIN_AF_FREQ_CODE &&
|
|
af_data <= MAX_AF_FREQ_CODE &&
|
|
radio->af_info2.orig_freq_khz != 0 &&
|
|
radio->af_info2.size < MAX_NO_OF_AF) {
|
|
|
|
af_freq_khz = SCALE_AF_CODE_TO_FREQ_KHZ(af_data);
|
|
retval = is_valid_freq(radio, af_freq_khz);
|
|
if (retval == false) {
|
|
FMDBG("%s: Invalid AF\n", __func__);
|
|
radio->af_info2.inval_freq_cnt++;
|
|
continue;
|
|
}
|
|
|
|
retval = is_new_freq(radio, af_freq_khz);
|
|
if (retval == false) {
|
|
FMDBG("%s: Duplicate AF\n", __func__);
|
|
radio->af_info2.inval_freq_cnt++;
|
|
continue;
|
|
}
|
|
|
|
/* update the AF list */
|
|
radio->af_info2.af_list[radio->af_info2.size++] =
|
|
af_freq_khz;
|
|
FMDBG("%s: AF is %u\n", __func__, af_freq_khz);
|
|
if ((radio->af_info2.size +
|
|
radio->af_info2.inval_freq_cnt ==
|
|
radio->af_info2.cnt) &&
|
|
is_different_af_list(radio)) {
|
|
|
|
/* Copy the list to af_info1. */
|
|
radio->af_info1.cnt = radio->af_info2.cnt;
|
|
radio->af_info1.size = radio->af_info2.size;
|
|
radio->af_info1.pi = radio->af_info2.pi;
|
|
radio->af_info1.orig_freq_khz =
|
|
radio->af_info2.orig_freq_khz;
|
|
memset(radio->af_info1.af_list,
|
|
0,
|
|
sizeof(radio->af_info1.af_list));
|
|
|
|
memcpy(radio->af_info1.af_list,
|
|
radio->af_info2.af_list,
|
|
sizeof(radio->af_info2.af_list));
|
|
|
|
/* AF list changed, post it to user space */
|
|
memset(&ev, 0, sizeof(struct af_list_ev));
|
|
|
|
ev.tune_freq_khz =
|
|
radio->af_info1.orig_freq_khz;
|
|
ev.pi_code = radio->pi;
|
|
ev.af_size = radio->af_info1.size;
|
|
|
|
memcpy(&ev.af_list[0],
|
|
radio->af_info1.af_list,
|
|
GET_AF_LIST_LEN(ev.af_size));
|
|
|
|
buff = &radio->data_buf[SILABS_FM_BUF_AF_LIST];
|
|
kfifo_in_locked(buff,
|
|
(u8 *)&ev,
|
|
GET_AF_EVT_LEN(ev.af_size),
|
|
&lock);
|
|
|
|
FMDBG("%s: posting AF list evt, curr freq %u\n",
|
|
__func__, ev.tune_freq_khz);
|
|
|
|
silabs_fm_q_event(radio,
|
|
SILABS_EVT_NEW_AF_LIST);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void silabs_af_tune(struct work_struct *work)
|
|
{
|
|
struct silabs_fm_device *radio;
|
|
int retval = 0, i = 0;
|
|
u8 rssi = 0;
|
|
u32 freq = 0;
|
|
|
|
radio = container_of(work, struct silabs_fm_device, work_af.work);
|
|
|
|
if (radio->af_info2.size == 0) {
|
|
FMDBG("%s: Empty AF list\n", __func__);
|
|
radio->is_af_tune_in_progress = false;
|
|
return;
|
|
}
|
|
radio->af_avg_th = 0;
|
|
for (i = 0; i < radio->af_wait_timer; i++) {
|
|
retval = get_rssi(radio, &rssi);
|
|
if (retval < 0)
|
|
FMDERR("%s: getting rssi failed\n", __func__);
|
|
radio->af_avg_th += rssi;
|
|
msleep(1000);
|
|
}
|
|
radio->af_avg_th = radio->af_avg_th/radio->af_wait_timer;
|
|
if (radio->af_avg_th >= radio->af_rssi_th) {
|
|
FMDBG("Not required to do Af jump\n");
|
|
return;
|
|
}
|
|
|
|
|
|
/* Disable all other interrupts except STC, RDS */
|
|
retval = configure_interrupts(radio, ENABLE_STC_RDS_INTERRUPTS);
|
|
|
|
/* Mute until AF tuning finishes */
|
|
retval = set_hard_mute(radio, true);
|
|
|
|
while (1) {
|
|
if (radio->mode != FM_RECV) {
|
|
FMDERR("%s: Drv is not in proper state\n", __func__);
|
|
goto end;
|
|
}
|
|
|
|
if (radio->seek_tune_status != NO_SEEK_TUNE_PENDING) {
|
|
FMDBG("%s: manual tune, search issued\n", __func__);
|
|
break;
|
|
}
|
|
|
|
if (radio->is_af_jump_enabled != true) {
|
|
FMDBG("%s: AF jump is disabled\n", __func__);
|
|
break;
|
|
}
|
|
|
|
/* If no more AFs left, tune to original frequency and break */
|
|
if (radio->af_info2.index >= radio->af_info2.size) {
|
|
FMDBG("%s: No more AFs, tuning to original freq %u\n",
|
|
__func__, radio->af_info2.orig_freq_khz);
|
|
|
|
freq = radio->af_info2.orig_freq_khz;
|
|
|
|
retval = tune(radio, freq);
|
|
if (retval < 0) {
|
|
FMDERR("%s: tune failed, error %d\n",
|
|
__func__, retval);
|
|
goto err_tune_fail;
|
|
}
|
|
|
|
/* wait for tune to finish */
|
|
if (!wait_for_completion_timeout(&radio->sync_req_done,
|
|
msecs_to_jiffies(WAIT_TIMEOUT_MSEC))) {
|
|
FMDERR("%s: didn't receive STC for tune\n",
|
|
__func__);
|
|
/* FM is not correct state */
|
|
continue;
|
|
} else
|
|
FMDBG("%s: received STC for tune\n", __func__);
|
|
|
|
goto err_tune_fail;
|
|
}
|
|
|
|
freq = radio->af_info2.af_list[radio->af_info2.index++];
|
|
|
|
FMDBG("%s: tuning to freq %u\n", __func__, freq);
|
|
|
|
retval = tune(radio, freq);
|
|
if (retval < 0) {
|
|
FMDERR("%s: tune failed, error %d\n",
|
|
__func__, retval);
|
|
goto err_tune_fail;
|
|
}
|
|
|
|
/* wait for tune to finish */
|
|
if (!wait_for_completion_timeout(&radio->sync_req_done,
|
|
msecs_to_jiffies(WAIT_TIMEOUT_MSEC))) {
|
|
FMDERR("%s: didn't receive STC for tune\n",
|
|
__func__);
|
|
/* FM is not correct state */
|
|
continue;
|
|
} else
|
|
FMDBG("%s: received STC for tune\n", __func__);
|
|
|
|
retval = get_rssi(radio, &rssi);
|
|
if (retval < 0) {
|
|
FMDERR("%s: getting rssi failed\n", __func__);
|
|
goto err_tune_fail;
|
|
}
|
|
|
|
if (rssi >= radio->af_rssi_th) {
|
|
u8 j = 0;
|
|
/* clear stale RDS interrupt */
|
|
get_rds_status(radio);
|
|
for (; j < AF_PI_WAIT_TIME && radio->pi == 0; j++) {
|
|
/* Wait for PI to be received. */
|
|
msleep(100);
|
|
FMDBG("%s: sleeping for 100ms for PI\n",
|
|
__func__);
|
|
}
|
|
|
|
if (radio->pi == 0 || radio->pi != radio->af_info2.pi) {
|
|
FMDBG("%s: pi %d, af freq pi %d not equal\n",
|
|
__func__, radio->pi, radio->af_info2.pi);
|
|
continue;
|
|
}
|
|
|
|
FMDBG("%s: found AF freq(%u) >= AF th with pi %d\n",
|
|
__func__, freq, radio->pi);
|
|
/* Notify FM UI about the new freq */
|
|
FMDBG("%s: posting TUNE_SUCC event\n", __func__);
|
|
silabs_fm_q_event(radio, SILABS_EVT_TUNE_SUCC);
|
|
|
|
break;
|
|
} else {
|
|
FMDBG("%s: rssi: %u, af_rssi_th: %u not eq contnuing\n",
|
|
__func__, rssi, radio->af_rssi_th);
|
|
}
|
|
}
|
|
|
|
err_tune_fail:
|
|
/*
|
|
* At this point, we are tuned to either original freq or AF with >=
|
|
* AF rssi threshold
|
|
*/
|
|
if (freq != radio->af_info2.orig_freq_khz) {
|
|
FMDBG("tuned freq different than original,reset af info\n");
|
|
reset_af_info(radio);
|
|
}
|
|
|
|
radio->is_af_tune_in_progress = false;
|
|
|
|
/* Clear the stale RDS int bit. */
|
|
get_rds_status(radio);
|
|
retval = configure_interrupts(radio, ENABLE_STC_RDS_INTERRUPTS);
|
|
|
|
/* Clear the stale RSQ int bit. */
|
|
get_rssi(radio, &rssi);
|
|
retval = configure_interrupts(radio, ENABLE_RSQ_INTERRUPTS);
|
|
|
|
end:
|
|
/* Unmute */
|
|
retval = set_hard_mute(radio, false);
|
|
return;
|
|
}
|
|
|
|
/* When RDS interrupt is received, read and process RDS data. */
|
|
static void rds_handler(struct work_struct *worker)
|
|
{
|
|
struct silabs_fm_device *radio;
|
|
u8 rt_blks[NO_OF_RDS_BLKS];
|
|
u8 grp_type, addr, ab_flg;
|
|
|
|
radio = container_of(worker, struct silabs_fm_device, rds_worker);
|
|
|
|
if (!radio) {
|
|
FMDERR("%s:radio is null\n", __func__);
|
|
return;
|
|
}
|
|
|
|
FMDBG("Entered rds_handler\n");
|
|
|
|
get_rds_status(radio);
|
|
|
|
pi_handler(radio, radio->block[0]);
|
|
|
|
grp_type = radio->block[1] >> OFFSET_OF_GRP_TYP;
|
|
|
|
FMDBG("grp_type = %d\n", grp_type);
|
|
|
|
if (grp_type & 0x01)
|
|
pi_handler(radio, radio->block[2]);
|
|
|
|
pty_handler(radio, (radio->block[1] >> OFFSET_OF_PTY) & PTY_MASK);
|
|
|
|
switch (grp_type) {
|
|
case RDS_TYPE_0A:
|
|
update_af_list(radio);
|
|
/* fall through */
|
|
case RDS_TYPE_0B:
|
|
addr = (radio->block[1] & PS_MASK) * NO_OF_CHARS_IN_EACH_ADD;
|
|
FMDBG("RDS is PS\n");
|
|
update_ps(radio, addr+0, radio->block[3] >> 8);
|
|
update_ps(radio, addr+1, radio->block[3] & 0xff);
|
|
break;
|
|
case RDS_TYPE_2A:
|
|
FMDBG("RDS is RT 2A group\n");
|
|
rt_blks[0] = (u8)(radio->block[2] >> 8);
|
|
rt_blks[1] = (u8)(radio->block[2] & 0xFF);
|
|
rt_blks[2] = (u8)(radio->block[3] >> 8);
|
|
rt_blks[3] = (u8)(radio->block[3] & 0xFF);
|
|
addr = (radio->block[1] & 0xf) * 4;
|
|
ab_flg = (radio->block[1] & 0x0010) >> 4;
|
|
rt_handler(radio, ab_flg, CNT_FOR_2A_GRP_RT, addr, rt_blks);
|
|
break;
|
|
case RDS_TYPE_2B:
|
|
FMDBG("RDS is RT 2B group\n");
|
|
rt_blks[0] = (u8)(radio->block[3] >> 8);
|
|
rt_blks[1] = (u8)(radio->block[3] & 0xFF);
|
|
rt_blks[2] = 0;
|
|
rt_blks[3] = 0;
|
|
addr = (radio->block[1] & 0xf) * 2;
|
|
ab_flg = (radio->block[1] & 0x0010) >> 4;
|
|
radio->rt_tmp0[MAX_LEN_2B_GRP_RT] = END_OF_RT;
|
|
radio->rt_tmp1[MAX_LEN_2B_GRP_RT] = END_OF_RT;
|
|
radio->rt_cnt[MAX_LEN_2B_GRP_RT] = RT_VALIDATE_LIMIT;
|
|
rt_handler(radio, ab_flg, CNT_FOR_2B_GRP_RT, addr, rt_blks);
|
|
break;
|
|
case RDS_TYPE_3A:
|
|
FMDBG("RDS is 3A group\n");
|
|
silabs_raw_rds_handler(radio);
|
|
break;
|
|
default:
|
|
FMDERR("Not handling the group type %d\n", grp_type);
|
|
break;
|
|
}
|
|
FMDBG("rt_plus_carrier = %x\n", radio->rt_plus_carrier);
|
|
FMDBG("ert_carrier = %x\n", radio->ert_carrier);
|
|
if (radio->rt_plus_carrier && (grp_type == radio->rt_plus_carrier))
|
|
silabs_rt_plus(radio);
|
|
else if (radio->ert_carrier && (grp_type == radio->ert_carrier))
|
|
silabs_buff_ert(radio);
|
|
return;
|
|
}
|
|
|
|
/* to enable, disable interrupts. */
|
|
static int configure_interrupts(struct silabs_fm_device *radio, u8 val)
|
|
{
|
|
int retval = 0;
|
|
u16 prop_val = 0;
|
|
|
|
switch (val) {
|
|
case DISABLE_ALL_INTERRUPTS:
|
|
prop_val = 0;
|
|
retval = set_property(radio, GPO_IEN_PROP, prop_val);
|
|
if (retval < 0)
|
|
FMDERR("%s: error disabling interrupts\n", __func__);
|
|
break;
|
|
case ENABLE_STC_RDS_INTERRUPTS:
|
|
/* enable STC and RDS interrupts. */
|
|
prop_val = RDS_INT_BIT_MASK | STC_INT_BIT_MASK;
|
|
|
|
retval = set_property(radio, GPO_IEN_PROP, prop_val);
|
|
if (retval < 0)
|
|
FMDERR("%s: error enabling STC, RDS interrupts\n",
|
|
__func__);
|
|
break;
|
|
case ENABLE_STC_INTERRUPTS:
|
|
/* enable STC interrupts only. */
|
|
prop_val = STC_INT_BIT_MASK;
|
|
|
|
retval = set_property(radio, GPO_IEN_PROP, prop_val);
|
|
if (retval < 0)
|
|
FMDERR("%s: error enabling STC interrupts\n", __func__);
|
|
break;
|
|
case ENABLE_RDS_INTERRUPTS:
|
|
/* enable RDS interrupts. */
|
|
prop_val = RDS_INT_BIT_MASK | STC_INT_BIT_MASK;
|
|
if (radio->is_af_jump_enabled)
|
|
prop_val |= RSQ_INT_BIT_MASK;
|
|
|
|
retval = set_property(radio, GPO_IEN_PROP, prop_val);
|
|
if (retval < 0)
|
|
FMDERR("%s: error enabling RDS interrupts\n",
|
|
__func__);
|
|
break;
|
|
case DISABLE_RDS_INTERRUPTS:
|
|
/* disable RDS interrupts. */
|
|
prop_val = STC_INT_BIT_MASK;
|
|
if (radio->is_af_jump_enabled)
|
|
prop_val |= RSQ_INT_BIT_MASK;
|
|
|
|
retval = set_property(radio, GPO_IEN_PROP, prop_val);
|
|
if (retval < 0)
|
|
FMDERR("%s: error disabling RDS interrupts\n",
|
|
__func__);
|
|
break;
|
|
case ENABLE_RSQ_INTERRUPTS:
|
|
/* enable RSQ interrupts. */
|
|
prop_val = RSQ_INT_BIT_MASK | STC_INT_BIT_MASK;
|
|
if (radio->lp_mode != true)
|
|
prop_val |= RDS_INT_BIT_MASK;
|
|
|
|
retval = set_property(radio, GPO_IEN_PROP, prop_val);
|
|
if (retval < 0)
|
|
FMDERR("%s: error enabling RSQ interrupts\n",
|
|
__func__);
|
|
break;
|
|
case DISABLE_RSQ_INTERRUPTS:
|
|
/* disable RSQ interrupts. */
|
|
prop_val = STC_INT_BIT_MASK;
|
|
if (radio->lp_mode != true)
|
|
prop_val |= RDS_INT_BIT_MASK;
|
|
|
|
retval = set_property(radio, GPO_IEN_PROP, prop_val);
|
|
if (retval < 0)
|
|
FMDERR("%s: error disabling RSQ interrupts\n",
|
|
__func__);
|
|
break;
|
|
default:
|
|
FMDERR("%s: invalid value %u\n", __func__, val);
|
|
retval = -EINVAL;
|
|
break;
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
static int get_int_status(struct silabs_fm_device *radio)
|
|
{
|
|
int retval = 0;
|
|
|
|
mutex_lock(&radio->lock);
|
|
|
|
memset(radio->write_buf, 0, WRITE_REG_NUM);
|
|
|
|
/* track command that is being sent to chip.*/
|
|
radio->cmd = GET_INT_STATUS_CMD;
|
|
radio->write_buf[0] = GET_INT_STATUS_CMD;
|
|
|
|
retval = send_cmd(radio, GET_INT_STATUS_CMD_LEN);
|
|
|
|
if (retval < 0)
|
|
FMDERR("%s: get_int_status failed with error %d\n",
|
|
__func__, retval);
|
|
|
|
mutex_unlock(&radio->lock);
|
|
|
|
return retval;
|
|
}
|
|
|
|
static void reset_rds(struct silabs_fm_device *radio)
|
|
{
|
|
radio->pi = 0;
|
|
/* reset PS bufferes */
|
|
memset(radio->ps_display, 0, sizeof(radio->ps_display));
|
|
memset(radio->ps_tmp0, 0, sizeof(radio->ps_tmp0));
|
|
memset(radio->ps_cnt, 0, sizeof(radio->ps_cnt));
|
|
|
|
/* reset RT buffers */
|
|
memset(radio->rt_display, 0, sizeof(radio->rt_display));
|
|
memset(radio->rt_tmp0, 0, sizeof(radio->rt_tmp0));
|
|
memset(radio->rt_tmp1, 0, sizeof(radio->rt_tmp1));
|
|
memset(radio->rt_cnt, 0, sizeof(radio->rt_cnt));
|
|
}
|
|
|
|
static int initialize_recv(struct silabs_fm_device *radio)
|
|
{
|
|
int retval = 0;
|
|
|
|
retval = set_property(radio,
|
|
FM_SEEK_TUNE_SNR_THRESHOLD_PROP,
|
|
DEFAULT_SNR_TH);
|
|
if (retval < 0) {
|
|
FMDERR("%s: FM_SEEK_TUNE_SNR_THRESHOLD_PROP fail error %d\n",
|
|
__func__, retval);
|
|
goto set_prop_fail;
|
|
}
|
|
|
|
radio->sinr_th = DEFAULT_SNR_TH;
|
|
|
|
retval = set_property(radio,
|
|
FM_SEEK_TUNE_RSSI_THRESHOLD_PROP,
|
|
DEFAULT_RSSI_TH);
|
|
if (retval < 0) {
|
|
FMDERR("%s: FM_SEEK_TUNE_RSSI_THRESHOLD_PROP fail error %d\n",
|
|
__func__, retval);
|
|
goto set_prop_fail;
|
|
}
|
|
|
|
radio->rssi_th = DEFAULT_RSSI_TH;
|
|
|
|
retval = set_property(radio,
|
|
FM_RSQ_RSSI_LO_THRESHOLD_PROP,
|
|
DEFAULT_AF_RSSI_LOW_TH);
|
|
if (retval < 0) {
|
|
FMDERR("%s: FM_RSQ_RSSI_LO_THRESHOLD_PROP fail error %d\n",
|
|
__func__, retval);
|
|
goto set_prop_fail;
|
|
}
|
|
|
|
radio->af_rssi_th = DEFAULT_AF_RSSI_LOW_TH;
|
|
|
|
retval = set_property(radio,
|
|
FM_RSQ_INT_SOURCE_PROP,
|
|
RSSI_LOW_TH_INT_BIT_MASK);
|
|
if (retval < 0) {
|
|
FMDERR("%s: FM_RSQ_INT_SOURCE_PROP fail error %d\n",
|
|
__func__, retval);
|
|
goto set_prop_fail;
|
|
}
|
|
|
|
retval = set_property(radio,
|
|
FM_RDS_INT_FIFO_COUNT_PROP,
|
|
FIFO_CNT_16);
|
|
if (retval < 0) {
|
|
FMDERR("%s: FM_RDS_INT_FIFO_COUNT_PROP fail error %d\n",
|
|
__func__, retval);
|
|
goto set_prop_fail;
|
|
}
|
|
|
|
radio->rds_fifo_cnt = FIFO_CNT_16;
|
|
|
|
set_prop_fail:
|
|
return retval;
|
|
|
|
}
|
|
|
|
static void init_ssr(struct silabs_fm_device *radio)
|
|
{
|
|
int retval = 0;
|
|
|
|
mutex_lock(&radio->lock);
|
|
|
|
memset(radio->write_buf, 0, WRITE_REG_NUM);
|
|
/*
|
|
* Configure status gpio to low in active state. When chip is reset for
|
|
* some reason, status gpio becomes high since pull-up resistor is
|
|
* installed. No need to return error even if it fails, since normal
|
|
* FM functionality can still work fine.
|
|
*/
|
|
|
|
radio->cmd = GPIO_CTL_CMD;
|
|
radio->write_buf[0] = GPIO_CTL_CMD;
|
|
radio->write_buf[1] = GPIO1_OUTPUT_ENABLE_MASK;
|
|
|
|
retval = send_cmd(radio, GPIO_CTL_CMD_LEN);
|
|
|
|
if (retval < 0) {
|
|
FMDERR("%s: setting Silabs gpio1 as op to chip fail, err %d\n",
|
|
__func__, retval);
|
|
goto end;
|
|
}
|
|
|
|
memset(radio->write_buf, 0, WRITE_REG_NUM);
|
|
|
|
/* track command that is being sent to chip.*/
|
|
radio->cmd = GPIO_SET_CMD;
|
|
radio->write_buf[0] = GPIO_SET_CMD;
|
|
radio->write_buf[1] = GPIO_OUTPUT_LOW_MASK;
|
|
|
|
retval = send_cmd(radio, GPIO_SET_CMD_LEN);
|
|
|
|
if (retval < 0)
|
|
FMDERR("%s: setting gpios to low failed, error %d\n",
|
|
__func__, retval);
|
|
|
|
end:
|
|
mutex_unlock(&radio->lock);
|
|
}
|
|
|
|
static int enable(struct silabs_fm_device *radio)
|
|
{
|
|
int retval = 0;
|
|
|
|
retval = read_cts_bit(radio);
|
|
|
|
if (retval < 0)
|
|
return retval;
|
|
|
|
mutex_lock(&radio->lock);
|
|
|
|
memset(radio->write_buf, 0, WRITE_REG_NUM);
|
|
|
|
/* track command that is being sent to chip.*/
|
|
radio->cmd = POWER_UP_CMD;
|
|
radio->write_buf[0] = POWER_UP_CMD;
|
|
radio->write_buf[1] = ENABLE_GPO2_INT_MASK;
|
|
|
|
radio->write_buf[2] = AUDIO_OPMODE_DIGITAL;
|
|
|
|
retval = send_cmd(radio, POWER_UP_CMD_LEN);
|
|
|
|
if (retval < 0) {
|
|
FMDERR("%s: enable failed with error %d\n", __func__, retval);
|
|
mutex_unlock(&radio->lock);
|
|
goto send_cmd_fail;
|
|
}
|
|
|
|
mutex_unlock(&radio->lock);
|
|
|
|
/* enable interrupts */
|
|
retval = configure_interrupts(radio, ENABLE_STC_RDS_INTERRUPTS);
|
|
if (retval < 0)
|
|
FMDERR("In %s, configure_interrupts failed with error %d\n",
|
|
__func__, retval);
|
|
|
|
/* initialize with default configuration */
|
|
retval = initialize_recv(radio);
|
|
reset_rds(radio); /* Clear the existing RDS data */
|
|
init_ssr(radio);
|
|
if (retval >= 0) {
|
|
if (radio->mode == FM_RECV_TURNING_ON) {
|
|
FMDBG("In %s, posting SILABS_EVT_RADIO_READY event\n",
|
|
__func__);
|
|
silabs_fm_q_event(radio, SILABS_EVT_RADIO_READY);
|
|
radio->mode = FM_RECV;
|
|
}
|
|
}
|
|
send_cmd_fail:
|
|
return retval;
|
|
|
|
}
|
|
|
|
static int disable(struct silabs_fm_device *radio)
|
|
{
|
|
int retval = 0;
|
|
|
|
mutex_lock(&radio->lock);
|
|
|
|
memset(radio->write_buf, 0, WRITE_REG_NUM);
|
|
|
|
/* track command that is being sent to chip. */
|
|
radio->cmd = POWER_DOWN_CMD;
|
|
radio->write_buf[0] = POWER_DOWN_CMD;
|
|
|
|
retval = send_cmd(radio, POWER_DOWN_CMD_LEN);
|
|
if (retval < 0)
|
|
FMDERR("%s: disable failed with error %d\n",
|
|
__func__, retval);
|
|
|
|
mutex_unlock(&radio->lock);
|
|
|
|
if (radio->mode == FM_TURNING_OFF || radio->mode == FM_RECV) {
|
|
FMDBG("%s: posting SILABS_EVT_RADIO_DISABLED event\n",
|
|
__func__);
|
|
silabs_fm_q_event(radio, SILABS_EVT_RADIO_DISABLED);
|
|
radio->mode = FM_OFF;
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
static int set_chan_spacing(struct silabs_fm_device *radio, u16 spacing)
|
|
{
|
|
int retval = 0;
|
|
u16 prop_val = 0;
|
|
|
|
if (spacing == 0)
|
|
prop_val = FM_RX_SPACE_200KHZ;
|
|
else if (spacing == 1)
|
|
prop_val = FM_RX_SPACE_100KHZ;
|
|
else if (spacing == 2)
|
|
prop_val = FM_RX_SPACE_50KHZ;
|
|
|
|
retval = set_property(radio, FM_SEEK_FREQ_SPACING_PROP, prop_val);
|
|
if (retval < 0)
|
|
FMDERR("In %s, error setting channel spacing\n", __func__);
|
|
else
|
|
radio->recv_conf.ch_spacing = spacing;
|
|
|
|
return retval;
|
|
|
|
}
|
|
|
|
static int set_emphasis(struct silabs_fm_device *radio, u16 emp)
|
|
{
|
|
int retval = 0;
|
|
u16 prop_val = 0;
|
|
|
|
if (emp == 0)
|
|
prop_val = FM_RX_EMP75;
|
|
else if (emp == 1)
|
|
prop_val = FM_RX_EMP50;
|
|
|
|
retval = set_property(radio, FM_DEEMPHASIS_PROP, prop_val);
|
|
if (retval < 0)
|
|
FMDERR("In %s, error setting emphasis\n", __func__);
|
|
else
|
|
radio->recv_conf.emphasis = emp;
|
|
|
|
return retval;
|
|
|
|
}
|
|
|
|
static int tune(struct silabs_fm_device *radio, u32 freq_khz)
|
|
{
|
|
int retval = 0;
|
|
u16 freq_16bit = (u16)(freq_khz/TUNE_STEP_SIZE);
|
|
|
|
FMDBG("In %s, freq is %d\n", __func__, freq_khz);
|
|
|
|
/*
|
|
* when we are tuning for the first time, we must set digital audio
|
|
* properties.
|
|
*/
|
|
if (radio->first_tune) {
|
|
/* I2S mode, rising edge */
|
|
retval = set_property(radio, DIGITAL_OUTPUT_FORMAT_PROP, 0);
|
|
if (retval < 0) {
|
|
FMDERR("%s: set output format prop failed, error %d\n",
|
|
__func__, retval);
|
|
goto set_prop_fail;
|
|
}
|
|
|
|
/* 48khz sample rate */
|
|
retval = set_property(radio,
|
|
DIGITAL_OUTPUT_SAMPLE_RATE_PROP,
|
|
SAMPLE_RATE_48_KHZ);
|
|
if (retval < 0) {
|
|
FMDERR("%s: set sample rate prop failed, error %d\n",
|
|
__func__, retval);
|
|
goto set_prop_fail;
|
|
}
|
|
radio->first_tune = false;
|
|
}
|
|
|
|
mutex_lock(&radio->lock);
|
|
|
|
memset(radio->write_buf, 0, WRITE_REG_NUM);
|
|
|
|
/* track command that is being sent to chip.*/
|
|
radio->cmd = FM_TUNE_FREQ_CMD;
|
|
|
|
radio->write_buf[0] = FM_TUNE_FREQ_CMD;
|
|
/* reserved */
|
|
radio->write_buf[1] = 0;
|
|
/* freq high byte */
|
|
radio->write_buf[2] = HIGH_BYTE_16BIT(freq_16bit);
|
|
/* freq low byte */
|
|
radio->write_buf[3] = LOW_BYTE_16BIT(freq_16bit);
|
|
radio->write_buf[4] = 0;
|
|
|
|
FMDBG("In %s, radio->write_buf[2] %x, radio->write_buf[3]%x\n",
|
|
__func__, radio->write_buf[2], radio->write_buf[3]);
|
|
|
|
retval = send_cmd(radio, TUNE_FREQ_CMD_LEN);
|
|
if (retval < 0)
|
|
FMDERR("In %s, tune failed with error %d\n", __func__, retval);
|
|
|
|
mutex_unlock(&radio->lock);
|
|
|
|
set_prop_fail:
|
|
return retval;
|
|
}
|
|
|
|
static int silabs_seek(struct silabs_fm_device *radio, int dir, int wrap)
|
|
{
|
|
int retval = 0;
|
|
|
|
mutex_lock(&radio->lock);
|
|
|
|
memset(radio->write_buf, 0, WRITE_REG_NUM);
|
|
|
|
/* track command that is being sent to chip. */
|
|
radio->cmd = FM_SEEK_START_CMD;
|
|
|
|
radio->write_buf[0] = FM_SEEK_START_CMD;
|
|
if (wrap)
|
|
radio->write_buf[1] = SEEK_WRAP_MASK;
|
|
|
|
if (dir == SRCH_DIR_UP)
|
|
radio->write_buf[1] |= SEEK_UP_MASK;
|
|
|
|
retval = send_cmd(radio, SEEK_CMD_LEN);
|
|
if (retval < 0)
|
|
FMDERR("In %s, seek failed with error %d\n", __func__, retval);
|
|
|
|
mutex_unlock(&radio->lock);
|
|
return retval;
|
|
}
|
|
|
|
|
|
static int cancel_seek(struct silabs_fm_device *radio)
|
|
{
|
|
int retval = 0;
|
|
|
|
mutex_lock(&radio->lock);
|
|
|
|
memset(radio->write_buf, 0, WRITE_REG_NUM);
|
|
|
|
/* track command that is being sent to chip. */
|
|
radio->cmd = FM_TUNE_STATUS_CMD;
|
|
|
|
radio->write_buf[0] = FM_TUNE_STATUS_CMD;
|
|
radio->write_buf[1] = CANCEL_SEEK_MASK;
|
|
|
|
retval = send_cmd(radio, TUNE_STATUS_CMD_LEN);
|
|
if (retval < 0)
|
|
FMDERR("%s: cancel_seek failed, error %d\n", __func__, retval);
|
|
|
|
mutex_unlock(&radio->lock);
|
|
radio->is_search_cancelled = true;
|
|
|
|
return retval;
|
|
|
|
}
|
|
|
|
static void silabs_fm_q_event(struct silabs_fm_device *radio,
|
|
enum silabs_evt_t event)
|
|
{
|
|
|
|
struct kfifo *data_b;
|
|
unsigned char evt = event;
|
|
|
|
data_b = &radio->data_buf[SILABS_FM_BUF_EVENTS];
|
|
|
|
FMDBG("updating event_q with event %x\n", event);
|
|
if (kfifo_in_locked(data_b,
|
|
&evt,
|
|
1,
|
|
&radio->buf_lock[SILABS_FM_BUF_EVENTS]))
|
|
wake_up_interruptible(&radio->event_queue);
|
|
}
|
|
|
|
static int clear_stc_int(struct silabs_fm_device *radio)
|
|
{
|
|
int retval = 0;
|
|
|
|
mutex_lock(&radio->lock);
|
|
|
|
memset(radio->write_buf, 0, WRITE_REG_NUM);
|
|
|
|
/* track command that is being sent to chip. */
|
|
radio->cmd = FM_TUNE_STATUS_CMD;
|
|
|
|
radio->write_buf[0] = FM_TUNE_STATUS_CMD;
|
|
radio->write_buf[1] = INTACK_MASK;
|
|
|
|
retval = send_cmd(radio, TUNE_STATUS_CMD_LEN);
|
|
if (retval < 0)
|
|
FMDERR("%s: clear_stc_int fail, error %d\n", __func__, retval);
|
|
|
|
mutex_unlock(&radio->lock);
|
|
|
|
return retval;
|
|
}
|
|
|
|
static void silabs_interrupts_handler(struct silabs_fm_device *radio)
|
|
{
|
|
int retval = 0;
|
|
u8 rssi = 0;
|
|
|
|
if (unlikely(radio == NULL)) {
|
|
FMDERR("%s:radio is null", __func__);
|
|
return;
|
|
}
|
|
|
|
FMDBG("%s: ISR fired for cmd %x, reading status bytes\n",
|
|
__func__, radio->cmd);
|
|
|
|
/* Get int status to know which interrupt is this(STC/RDS/etc) */
|
|
retval = get_int_status(radio);
|
|
|
|
if (retval < 0) {
|
|
FMDERR("%s: failure reading the resp from soc with error %d\n",
|
|
__func__, retval);
|
|
return;
|
|
}
|
|
FMDBG("%s: successfully read the resp from soc, status byte is %x\n",
|
|
__func__, radio->read_buf[0]);
|
|
|
|
|
|
if (radio->read_buf[0] & STC_INT_BIT_MASK) {
|
|
FMDBG("%s: STC bit set for cmd %x\n", __func__, radio->cmd);
|
|
if (radio->seek_tune_status == TUNE_PENDING) {
|
|
FMDBG("In %s, posting SILABS_EVT_TUNE_SUCC event\n",
|
|
__func__);
|
|
silabs_fm_q_event(radio, SILABS_EVT_TUNE_SUCC);
|
|
radio->seek_tune_status = NO_SEEK_TUNE_PENDING;
|
|
radio->is_af_tune_in_progress = false;
|
|
} else if (radio->seek_tune_status == SEEK_PENDING) {
|
|
FMDBG("%s: posting SILABS_EVT_SEEK_COMPLETE event\n",
|
|
__func__);
|
|
silabs_fm_q_event(radio, SILABS_EVT_SEEK_COMPLETE);
|
|
/* post tune comp evt since seek results in a tune.*/
|
|
FMDBG("%s: posting SILABS_EVT_TUNE_SUCC\n",
|
|
__func__);
|
|
silabs_fm_q_event(radio, SILABS_EVT_TUNE_SUCC);
|
|
radio->seek_tune_status = NO_SEEK_TUNE_PENDING;
|
|
|
|
} else if (radio->seek_tune_status == SCAN_PENDING) {
|
|
/*
|
|
* when scan is pending and STC int is set, signal
|
|
* so that scan can proceed
|
|
*/
|
|
FMDBG("In %s, signalling scan thread\n", __func__);
|
|
complete(&radio->sync_req_done);
|
|
} else if (radio->is_af_tune_in_progress == true) {
|
|
/*
|
|
* when AF tune is going on and STC int is set, signal
|
|
* so that AF tune can proceed.
|
|
*/
|
|
FMDBG("In %s, signalling AF tune thread\n", __func__);
|
|
complete(&radio->sync_req_done);
|
|
}
|
|
/* clear the STC interrupt. */
|
|
clear_stc_int(radio);
|
|
reset_rds(radio); /* Clear the existing RDS data */
|
|
return;
|
|
}
|
|
|
|
if (radio->read_buf[0] & RSQ_INT_BIT_MASK) {
|
|
FMDBG("RSQ interrupt received, clearing the RSQ int bit\n");
|
|
|
|
/* clear RSQ interrupt bits until AF tune is complete. */
|
|
(void)get_rssi(radio, &rssi);
|
|
/* Don't process RSQ until AF tune is complete. */
|
|
if (radio->is_af_tune_in_progress == true)
|
|
return;
|
|
|
|
if (radio->is_af_jump_enabled &&
|
|
radio->af_info2.size != 0 &&
|
|
rssi <= radio->af_rssi_th) {
|
|
|
|
radio->is_af_tune_in_progress = true;
|
|
FMDBG("%s: Queuing to AF work Q, freq %u, rssi %u\n",
|
|
__func__, radio->tuned_freq_khz, rssi);
|
|
queue_delayed_work(radio->wqueue_af, &radio->work_af,
|
|
msecs_to_jiffies(SILABS_DELAY_MSEC));
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (radio->read_buf[0] & RDS_INT_BIT_MASK) {
|
|
FMDBG("RDS interrupt received\n");
|
|
/* Don't process RDS until AF tune is complete. */
|
|
if (radio->is_af_tune_in_progress == true) {
|
|
/* get PI only */
|
|
get_rds_status(radio);
|
|
pi_handler(radio, radio->block[0]);
|
|
return;
|
|
}
|
|
schedule_work(&radio->rds_worker);
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
|
|
static void read_int_stat(struct work_struct *work)
|
|
{
|
|
struct silabs_fm_device *radio;
|
|
|
|
radio = container_of(work, struct silabs_fm_device, work.work);
|
|
|
|
silabs_interrupts_handler(radio);
|
|
}
|
|
|
|
static void silabs_fm_disable_irq(struct silabs_fm_device *radio)
|
|
{
|
|
int irq;
|
|
|
|
irq = radio->irq;
|
|
disable_irq_wake(irq);
|
|
free_irq(irq, radio);
|
|
|
|
irq = radio->status_irq;
|
|
disable_irq_wake(irq);
|
|
free_irq(irq, radio);
|
|
|
|
cancel_work_sync(&radio->rds_worker);
|
|
flush_workqueue(radio->wqueue_rds);
|
|
cancel_delayed_work_sync(&radio->work);
|
|
flush_workqueue(radio->wqueue);
|
|
cancel_delayed_work_sync(&radio->work_scan);
|
|
flush_workqueue(radio->wqueue_scan);
|
|
cancel_delayed_work_sync(&radio->work_af);
|
|
flush_workqueue(radio->wqueue_af);
|
|
}
|
|
|
|
static irqreturn_t silabs_fm_isr(int irq, void *dev_id)
|
|
{
|
|
struct silabs_fm_device *radio = dev_id;
|
|
/*
|
|
* The call to queue_delayed_work ensures that a minimum delay
|
|
* (in jiffies) passes before the work is actually executed. The return
|
|
* value from the function is nonzero if the work_struct was actually
|
|
* added to queue (otherwise, it may have already been there and will
|
|
* not be added a second time).
|
|
*/
|
|
|
|
queue_delayed_work(radio->wqueue, &radio->work,
|
|
msecs_to_jiffies(SILABS_DELAY_MSEC));
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
static irqreturn_t silabs_fm_status_isr(int irq, void *dev_id)
|
|
{
|
|
struct silabs_fm_device *radio = dev_id;
|
|
|
|
if (radio->mode == FM_TURNING_OFF || radio->mode == FM_RECV) {
|
|
FMDERR("%s: chip in bad state, posting DISABLED event\n",
|
|
__func__);
|
|
silabs_fm_q_event(radio, SILABS_EVT_RADIO_DISABLED);
|
|
radio->mode = FM_OFF;
|
|
}
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
static int silabs_fm_request_irq(struct silabs_fm_device *radio)
|
|
{
|
|
int retval;
|
|
int irq;
|
|
|
|
irq = radio->irq;
|
|
|
|
/*
|
|
* Use request_any_context_irq, So that it might work for nested or
|
|
* nested interrupts.
|
|
*/
|
|
retval = request_any_context_irq(irq, silabs_fm_isr,
|
|
IRQ_TYPE_EDGE_FALLING, "fm interrupt", radio);
|
|
if (retval < 0) {
|
|
FMDERR("Couldn't acquire FM gpio %d\n", irq);
|
|
return retval;
|
|
} else {
|
|
FMDBG("FM GPIO %d registered\n", irq);
|
|
}
|
|
retval = enable_irq_wake(irq);
|
|
if (retval < 0) {
|
|
FMDERR("Could not enable FM interrupt\n ");
|
|
free_irq(irq , radio);
|
|
return retval;
|
|
}
|
|
|
|
irq = radio->status_irq;
|
|
|
|
retval = request_any_context_irq(irq, silabs_fm_status_isr,
|
|
IRQ_TYPE_EDGE_RISING, "fm status interrupt",
|
|
radio);
|
|
if (retval < 0) {
|
|
FMDERR("Couldn't acquire FM status gpio %d\n", irq);
|
|
/* Do not error out for status int. FM can work without it. */
|
|
return 0;
|
|
} else {
|
|
FMDBG("FM status GPIO %d registered\n", irq);
|
|
}
|
|
retval = enable_irq_wake(irq);
|
|
if (retval < 0) {
|
|
FMDERR("Could not enable FM status interrupt\n ");
|
|
free_irq(irq , radio);
|
|
/* Do not error out for status int. FM can work without it. */
|
|
return 0;
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
static int silabs_fm_fops_open(struct file *file)
|
|
{
|
|
struct silabs_fm_device *radio = video_get_drvdata(video_devdata(file));
|
|
int retval = -ENODEV;
|
|
|
|
if (unlikely(radio == NULL)) {
|
|
FMDERR("%s:radio is null", __func__);
|
|
return -EINVAL;
|
|
}
|
|
|
|
INIT_DELAYED_WORK(&radio->work, read_int_stat);
|
|
INIT_DELAYED_WORK(&radio->work_scan, silabs_scan);
|
|
INIT_DELAYED_WORK(&radio->work_af, silabs_af_tune);
|
|
INIT_WORK(&radio->rds_worker, rds_handler);
|
|
|
|
init_completion(&radio->sync_req_done);
|
|
if (!atomic_dec_and_test(&radio->users)) {
|
|
FMDBG("%s: Device already in use. Try again later", __func__);
|
|
atomic_inc(&radio->users);
|
|
return -EBUSY;
|
|
}
|
|
|
|
/* initial gpio pin config & Power up */
|
|
retval = silabs_fm_power_cfg(radio, TURNING_ON);
|
|
if (retval) {
|
|
FMDERR("%s: failed config gpio & pmic\n", __func__);
|
|
goto open_err_setup;
|
|
}
|
|
radio->irq = gpio_to_irq(radio->int_gpio);
|
|
|
|
if (radio->irq < 0) {
|
|
FMDERR("%s: gpio_to_irq returned %d\n", __func__, radio->irq);
|
|
goto open_err_req_irq;
|
|
}
|
|
|
|
FMDBG("irq number is = %d\n", radio->irq);
|
|
|
|
if (radio->status_gpio > 0) {
|
|
radio->status_irq = gpio_to_irq(radio->status_gpio);
|
|
|
|
if (radio->status_irq < 0) {
|
|
FMDERR("%s: gpio_to_irq returned %d for status gpio\n",
|
|
__func__, radio->irq);
|
|
goto open_err_req_irq;
|
|
}
|
|
|
|
FMDBG("status irq number is = %d\n", radio->status_irq);
|
|
}
|
|
|
|
/* enable irq */
|
|
retval = silabs_fm_request_irq(radio);
|
|
if (retval < 0) {
|
|
FMDERR("%s: failed to request irq\n", __func__);
|
|
goto open_err_req_irq;
|
|
}
|
|
|
|
radio->handle_irq = 0;
|
|
radio->first_tune = true;
|
|
return 0;
|
|
|
|
open_err_req_irq:
|
|
silabs_fm_power_cfg(radio, TURNING_OFF);
|
|
open_err_setup:
|
|
radio->handle_irq = 1;
|
|
atomic_inc(&radio->users);
|
|
return retval;
|
|
}
|
|
|
|
static int silabs_fm_fops_release(struct file *file)
|
|
{
|
|
struct silabs_fm_device *radio = video_get_drvdata(video_devdata(file));
|
|
int retval = 0;
|
|
|
|
if (unlikely(radio == NULL))
|
|
return -EINVAL;
|
|
|
|
if (radio->mode == FM_RECV) {
|
|
radio->mode = FM_OFF;
|
|
retval = disable(radio);
|
|
if (retval < 0)
|
|
FMDERR("Err on disable FM %d\n", retval);
|
|
}
|
|
|
|
FMDBG("%s, Disabling the IRQs\n", __func__);
|
|
/* disable irq */
|
|
silabs_fm_disable_irq(radio);
|
|
|
|
retval = silabs_fm_power_cfg(radio, TURNING_OFF);
|
|
if (retval < 0)
|
|
FMDERR("%s: failed to configure gpios\n", __func__);
|
|
|
|
atomic_inc(&radio->users);
|
|
|
|
return retval;
|
|
}
|
|
|
|
static struct v4l2_queryctrl silabs_fm_v4l2_queryctrl[] = {
|
|
{
|
|
.id = V4L2_CID_AUDIO_VOLUME,
|
|
.type = V4L2_CTRL_TYPE_INTEGER,
|
|
.name = "Volume",
|
|
.minimum = 0,
|
|
.maximum = 15,
|
|
.step = 1,
|
|
.default_value = 15,
|
|
},
|
|
{
|
|
.id = V4L2_CID_AUDIO_BALANCE,
|
|
.flags = V4L2_CTRL_FLAG_DISABLED,
|
|
},
|
|
{
|
|
.id = V4L2_CID_AUDIO_BASS,
|
|
.flags = V4L2_CTRL_FLAG_DISABLED,
|
|
},
|
|
{
|
|
.id = V4L2_CID_AUDIO_TREBLE,
|
|
.flags = V4L2_CTRL_FLAG_DISABLED,
|
|
},
|
|
{
|
|
.id = V4L2_CID_AUDIO_MUTE,
|
|
.type = V4L2_CTRL_TYPE_BOOLEAN,
|
|
.name = "Mute",
|
|
.minimum = 0,
|
|
.maximum = 1,
|
|
.step = 1,
|
|
.default_value = 1,
|
|
},
|
|
{
|
|
.id = V4L2_CID_AUDIO_LOUDNESS,
|
|
.flags = V4L2_CTRL_FLAG_DISABLED,
|
|
},
|
|
{
|
|
.id = V4L2_CID_PRIVATE_SILABS_SRCHON,
|
|
.type = V4L2_CTRL_TYPE_BOOLEAN,
|
|
.name = "Search on/off",
|
|
.minimum = 0,
|
|
.maximum = 1,
|
|
.step = 1,
|
|
.default_value = 1,
|
|
|
|
},
|
|
{
|
|
.id = V4L2_CID_PRIVATE_SILABS_STATE,
|
|
.type = V4L2_CTRL_TYPE_INTEGER,
|
|
.name = "radio 0ff/rx/tx/reset",
|
|
.minimum = 0,
|
|
.maximum = 3,
|
|
.step = 1,
|
|
.default_value = 1,
|
|
|
|
},
|
|
{
|
|
.id = V4L2_CID_PRIVATE_SILABS_REGION,
|
|
.type = V4L2_CTRL_TYPE_INTEGER,
|
|
.name = "radio standard",
|
|
.minimum = 0,
|
|
.maximum = 2,
|
|
.step = 1,
|
|
.default_value = 0,
|
|
},
|
|
{
|
|
.id = V4L2_CID_PRIVATE_SILABS_SIGNAL_TH,
|
|
.type = V4L2_CTRL_TYPE_INTEGER,
|
|
.name = "Signal Threshold",
|
|
.minimum = 0x80,
|
|
.maximum = 0x7F,
|
|
.step = 1,
|
|
.default_value = 0,
|
|
},
|
|
{
|
|
.id = V4L2_CID_PRIVATE_SILABS_EMPHASIS,
|
|
.type = V4L2_CTRL_TYPE_BOOLEAN,
|
|
.name = "Emphasis",
|
|
.minimum = 0,
|
|
.maximum = 1,
|
|
.default_value = 0,
|
|
},
|
|
{
|
|
.id = V4L2_CID_PRIVATE_SILABS_RDS_STD,
|
|
.type = V4L2_CTRL_TYPE_BOOLEAN,
|
|
.name = "RDS standard",
|
|
.minimum = 0,
|
|
.maximum = 1,
|
|
.default_value = 0,
|
|
},
|
|
{
|
|
.id = V4L2_CID_PRIVATE_SILABS_SPACING,
|
|
.type = V4L2_CTRL_TYPE_INTEGER,
|
|
.name = "Channel spacing",
|
|
.minimum = 0,
|
|
.maximum = 2,
|
|
.default_value = 0,
|
|
},
|
|
{
|
|
.id = V4L2_CID_PRIVATE_SILABS_RDSON,
|
|
.type = V4L2_CTRL_TYPE_BOOLEAN,
|
|
.name = "RDS on/off",
|
|
.minimum = 0,
|
|
.maximum = 1,
|
|
.default_value = 0,
|
|
},
|
|
{
|
|
.id = V4L2_CID_PRIVATE_SILABS_RDSGROUP_MASK,
|
|
.type = V4L2_CTRL_TYPE_INTEGER,
|
|
.name = "RDS group mask",
|
|
.minimum = 0,
|
|
.maximum = 0xFFFFFFFF,
|
|
.default_value = 0,
|
|
},
|
|
{
|
|
.id = V4L2_CID_PRIVATE_SILABS_RDSGROUP_PROC,
|
|
.type = V4L2_CTRL_TYPE_INTEGER,
|
|
.name = "RDS processing",
|
|
.minimum = 0,
|
|
.maximum = 0xFF,
|
|
.default_value = 0,
|
|
},
|
|
{
|
|
.id = V4L2_CID_PRIVATE_SILABS_RDSD_BUF,
|
|
.type = V4L2_CTRL_TYPE_INTEGER,
|
|
.name = "RDS data groups to buffer",
|
|
.minimum = 1,
|
|
.maximum = 21,
|
|
.default_value = 0,
|
|
},
|
|
{
|
|
.id = V4L2_CID_PRIVATE_SILABS_PSALL,
|
|
.type = V4L2_CTRL_TYPE_BOOLEAN,
|
|
.name = "pass all ps strings",
|
|
.minimum = 0,
|
|
.maximum = 1,
|
|
.default_value = 0,
|
|
},
|
|
{
|
|
.id = V4L2_CID_PRIVATE_SILABS_LP_MODE,
|
|
.type = V4L2_CTRL_TYPE_BOOLEAN,
|
|
.name = "Low power mode",
|
|
.minimum = 0,
|
|
.maximum = 1,
|
|
.default_value = 0,
|
|
},
|
|
{
|
|
.id = V4L2_CID_PRIVATE_SILABS_ANTENNA,
|
|
.type = V4L2_CTRL_TYPE_BOOLEAN,
|
|
.name = "headset/internal",
|
|
.minimum = 0,
|
|
.maximum = 1,
|
|
.default_value = 0,
|
|
},
|
|
|
|
};
|
|
|
|
static int silabs_fm_vidioc_querycap(struct file *file, void *priv,
|
|
struct v4l2_capability *capability)
|
|
{
|
|
struct silabs_fm_device *radio = video_get_drvdata(video_devdata(file));
|
|
|
|
if (unlikely(radio == NULL)) {
|
|
FMDERR("%s:radio is null", __func__);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (unlikely(capability == NULL)) {
|
|
FMDERR("%s:capability is null", __func__);
|
|
return -EINVAL;
|
|
}
|
|
|
|
strlcpy(capability->driver, DRIVER_NAME, sizeof(capability->driver));
|
|
strlcpy(capability->card, DRIVER_CARD, sizeof(capability->card));
|
|
snprintf(capability->bus_info, 4, "I2C");
|
|
capability->capabilities = V4L2_CAP_TUNER | V4L2_CAP_RADIO;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int silabs_fm_vidioc_queryctrl(struct file *file, void *priv,
|
|
struct v4l2_queryctrl *qc)
|
|
{
|
|
unsigned char i;
|
|
int retval = -EINVAL;
|
|
|
|
if (unlikely(qc == NULL)) {
|
|
FMDERR("%s:qc is null", __func__);
|
|
return -EINVAL;
|
|
}
|
|
|
|
|
|
for (i = 0; i < ARRAY_SIZE(silabs_fm_v4l2_queryctrl); i++) {
|
|
if (qc->id && qc->id == silabs_fm_v4l2_queryctrl[i].id) {
|
|
memcpy(qc, &(silabs_fm_v4l2_queryctrl[i]),
|
|
sizeof(*qc));
|
|
retval = 0;
|
|
break;
|
|
}
|
|
}
|
|
if (retval < 0)
|
|
FMDERR("query conv4ltrol failed with %d\n", retval);
|
|
|
|
return retval;
|
|
}
|
|
|
|
static int silabs_fm_vidioc_g_ctrl(struct file *file, void *priv,
|
|
struct v4l2_control *ctrl)
|
|
{
|
|
struct silabs_fm_device *radio = video_get_drvdata(video_devdata(file));
|
|
int retval = 0;
|
|
|
|
if (unlikely(radio == NULL)) {
|
|
FMDERR(":radio is null");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (ctrl == NULL) {
|
|
FMDERR("%s, v4l2 ctrl is null\n", __func__);
|
|
return -EINVAL;
|
|
}
|
|
|
|
switch (ctrl->id) {
|
|
case V4L2_CID_AUDIO_VOLUME:
|
|
break;
|
|
case V4L2_CID_AUDIO_MUTE:
|
|
break;
|
|
|
|
case V4L2_CID_PRIVATE_SILABS_RDSGROUP_PROC:
|
|
ctrl->value = 0;
|
|
retval = 0;
|
|
break;
|
|
case V4L2_CID_PRIVATE_SILABS_GET_SINR:
|
|
|
|
mutex_lock(&radio->lock);
|
|
radio->cmd = FM_TUNE_STATUS_CMD;
|
|
|
|
radio->write_buf[0] = FM_TUNE_STATUS_CMD;
|
|
radio->write_buf[1] = 0;
|
|
|
|
retval = send_cmd(radio, TUNE_STATUS_CMD_LEN);
|
|
if (retval < 0) {
|
|
FMDERR("%s: FM_TUNE_STATUS_CMD failed with error %d\n",
|
|
__func__, retval);
|
|
mutex_unlock(&radio->lock);
|
|
break;
|
|
}
|
|
|
|
/* sinr */
|
|
ctrl->value = radio->read_buf[5];
|
|
mutex_unlock(&radio->lock);
|
|
FMDBG("%s: V4L2_CID_PRIVATE_SILABS_GET_SINR, val %d\n",
|
|
__func__, ctrl->value);
|
|
break;
|
|
case V4L2_CID_PRIVATE_SILABS_SINR_THRESHOLD:
|
|
FMDBG("%s: V4L2_CID_PRIVATE_SILABS_SINR_THRESHOLD, val %d\n",
|
|
__func__, radio->sinr_th);
|
|
|
|
ctrl->value = radio->sinr_th;
|
|
break;
|
|
case V4L2_CID_PRIVATE_SILABS_RSSI_TH:
|
|
FMDBG("%s: V4L2_CID_PRIVATE_SILABS_RSSI_TH, val %d\n",
|
|
__func__, radio->rssi_th);
|
|
|
|
ctrl->value = radio->rssi_th;
|
|
break;
|
|
case V4L2_CID_PRIVATE_SILABS_AF_JUMP_RSSI_TH:
|
|
FMDBG("%s: V4L2_CID_PRIVATE_SILABS_AF_JUMP_RSSI_TH, val %d\n",
|
|
__func__, radio->af_rssi_th);
|
|
|
|
ctrl->value = radio->af_rssi_th;
|
|
break;
|
|
|
|
case V4L2_CID_PRIVATE_SILABS_RDSD_BUF:
|
|
FMDBG("%s: V4L2_CID_PRIVATE_SILABS_RDSD_BUF, val %d\n",
|
|
__func__, radio->rds_fifo_cnt);
|
|
|
|
ctrl->value = radio->rds_fifo_cnt;
|
|
break;
|
|
case V4L2_CID_PRIVATE_SILABS_AF_RMSSI_SAMPLES:
|
|
FMDBG("%s: V4L2_CID_PRIVATE_SILABS_AF_RMSSI_SAMPLES, val %d\n",
|
|
__func__, ctrl->value);
|
|
ctrl->value = radio->af_wait_timer;
|
|
break;
|
|
default:
|
|
retval = -EINVAL;
|
|
break;
|
|
}
|
|
|
|
if (retval < 0)
|
|
FMDERR("get control failed with %d, id: %x\n",
|
|
retval, ctrl->id);
|
|
|
|
return retval;
|
|
}
|
|
|
|
static int silabs_fm_vidioc_s_ctrl(struct file *file, void *priv,
|
|
struct v4l2_control *ctrl)
|
|
{
|
|
struct silabs_fm_device *radio = video_get_drvdata(video_devdata(file));
|
|
int retval = 0;
|
|
|
|
if (unlikely(radio == NULL)) {
|
|
FMDERR("%s:radio is null", __func__);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (unlikely(ctrl == NULL)) {
|
|
FMDERR("%s:ctrl is null", __func__);
|
|
return -EINVAL;
|
|
}
|
|
|
|
switch (ctrl->id) {
|
|
case V4L2_CID_PRIVATE_SILABS_STATE:
|
|
/* check if already on */
|
|
if (ctrl->value == FM_RECV) {
|
|
if (is_enable_rx_possible(radio) != 0) {
|
|
FMDERR("%s: fm is not in proper state\n",
|
|
__func__);
|
|
retval = -EINVAL;
|
|
goto end;
|
|
}
|
|
radio->mode = FM_RECV_TURNING_ON;
|
|
|
|
retval = enable(radio);
|
|
if (retval < 0) {
|
|
FMDERR("Error while enabling RECV FM %d\n",
|
|
retval);
|
|
radio->mode = FM_OFF;
|
|
goto end;
|
|
}
|
|
} else if (ctrl->value == FM_OFF) {
|
|
retval = configure_interrupts(radio,
|
|
DISABLE_ALL_INTERRUPTS);
|
|
if (retval < 0)
|
|
FMDERR("configure_interrupts failed %d\n",
|
|
retval);
|
|
flush_workqueue(radio->wqueue);
|
|
cancel_work_sync(&radio->rds_worker);
|
|
flush_workqueue(radio->wqueue_rds);
|
|
radio->mode = FM_TURNING_OFF;
|
|
retval = disable(radio);
|
|
if (retval < 0) {
|
|
FMDERR("Err on disable recv FM %d\n", retval);
|
|
radio->mode = FM_RECV;
|
|
goto end;
|
|
}
|
|
}
|
|
break;
|
|
case V4L2_CID_PRIVATE_SILABS_SPACING:
|
|
if (!is_valid_chan_spacing(ctrl->value)) {
|
|
retval = -EINVAL;
|
|
FMDERR("%s: channel spacing is not valid\n", __func__);
|
|
goto end;
|
|
}
|
|
retval = set_chan_spacing(radio, (u16)ctrl->value);
|
|
if (retval < 0) {
|
|
FMDERR("Error in setting channel spacing\n");
|
|
goto end;
|
|
}
|
|
break;
|
|
case V4L2_CID_PRIVATE_SILABS_EMPHASIS:
|
|
retval = set_emphasis(radio, (u16)ctrl->value);
|
|
if (retval < 0) {
|
|
FMDERR("Error in setting emphasis\n");
|
|
goto end;
|
|
}
|
|
break;
|
|
case V4L2_CID_PRIVATE_SILABS_ANTENNA:
|
|
if (ctrl->value == 0 || ctrl->value == 1) {
|
|
retval = set_property(radio,
|
|
FM_ANTENNA_INPUT_PROP,
|
|
ctrl->value);
|
|
if (retval < 0)
|
|
FMDERR("Setting antenna type failed\n");
|
|
else
|
|
radio->antenna = ctrl->value;
|
|
} else {
|
|
retval = -EINVAL;
|
|
FMDERR("%s: antenna type is not valid\n", __func__);
|
|
goto end;
|
|
}
|
|
break;
|
|
case V4L2_CID_PRIVATE_SILABS_SOFT_MUTE:
|
|
retval = 0;
|
|
break;
|
|
case V4L2_CID_PRIVATE_SILABS_REGION:
|
|
case V4L2_CID_PRIVATE_SILABS_SRCH_ALGORITHM:
|
|
case V4L2_CID_PRIVATE_SILABS_SET_AUDIO_PATH:
|
|
/*
|
|
* These private controls are place holders to keep the
|
|
* driver compatible with changes done in the frameworks
|
|
* which are specific to TAVARUA.
|
|
*/
|
|
retval = 0;
|
|
break;
|
|
case V4L2_CID_PRIVATE_SILABS_SRCHMODE:
|
|
if (is_valid_srch_mode(ctrl->value)) {
|
|
radio->g_search_mode = ctrl->value;
|
|
} else {
|
|
FMDERR("%s: srch mode is not valid\n", __func__);
|
|
retval = -EINVAL;
|
|
goto end;
|
|
}
|
|
break;
|
|
case V4L2_CID_PRIVATE_SILABS_SCANDWELL:
|
|
if ((ctrl->value >= MIN_DWELL_TIME) &&
|
|
(ctrl->value <= MAX_DWELL_TIME)) {
|
|
radio->dwell_time_sec = ctrl->value;
|
|
} else {
|
|
FMDERR("%s: scandwell period is not valid\n", __func__);
|
|
retval = -EINVAL;
|
|
}
|
|
break;
|
|
case V4L2_CID_PRIVATE_SILABS_SRCHON:
|
|
silabs_search(radio, (bool)ctrl->value);
|
|
break;
|
|
case V4L2_CID_PRIVATE_SILABS_RDS_STD:
|
|
return retval;
|
|
break;
|
|
case V4L2_CID_PRIVATE_SILABS_RDSON:
|
|
return retval;
|
|
break;
|
|
case V4L2_CID_PRIVATE_SILABS_RDSGROUP_MASK:
|
|
retval = set_property(radio,
|
|
FM_RDS_INT_SOURCE_PROP,
|
|
RDS_INT_BIT);
|
|
if (retval < 0) {
|
|
FMDERR("In %s, FM_RDS_INT_SOURCE_PROP failed %d\n",
|
|
__func__, retval);
|
|
goto end;
|
|
}
|
|
break;
|
|
case V4L2_CID_PRIVATE_SILABS_RDSD_BUF:
|
|
if (is_valid_rds_fifo_cnt(ctrl->value)) {
|
|
retval = set_property(radio,
|
|
FM_RDS_INT_FIFO_COUNT_PROP,
|
|
ctrl->value);
|
|
if (retval < 0) {
|
|
FMDERR("%s: setting rds fifo cnt failed %d\n",
|
|
__func__, retval);
|
|
goto end;
|
|
}
|
|
|
|
radio->rds_fifo_cnt = ctrl->value;
|
|
} else
|
|
retval = -EINVAL;
|
|
|
|
break;
|
|
case V4L2_CID_PRIVATE_SILABS_RDSGROUP_PROC:
|
|
/* Enabled all with uncorrectable */
|
|
retval = set_property(radio,
|
|
FM_RDS_CONFIG_PROP,
|
|
UNCORRECTABLE_RDS_EN);
|
|
if (retval < 0) {
|
|
FMDERR("In %s, FM_RDS_CONFIG_PROP failed %d\n",
|
|
__func__, retval);
|
|
goto end;
|
|
}
|
|
break;
|
|
case V4L2_CID_PRIVATE_SILABS_LP_MODE:
|
|
FMDBG("In %s, V4L2_CID_PRIVATE_SILABS_LP_MODE, val is %d\n",
|
|
__func__, ctrl->value);
|
|
if (ctrl->value) {
|
|
/* disable RDS interrupts */
|
|
retval = configure_interrupts(radio,
|
|
ENABLE_RDS_INTERRUPTS);
|
|
radio->lp_mode = true;
|
|
} else {
|
|
/* enable RDS interrupts */
|
|
retval = configure_interrupts(radio,
|
|
DISABLE_RDS_INTERRUPTS);
|
|
radio->lp_mode = false;
|
|
}
|
|
|
|
if (retval < 0) {
|
|
FMDERR("In %s, setting low power mode failed %d\n",
|
|
__func__, retval);
|
|
goto end;
|
|
}
|
|
break;
|
|
case V4L2_CID_PRIVATE_SILABS_AF_JUMP:
|
|
FMDBG("%s: V4L2_CID_PRIVATE_SILABS_AF_JUMP, val is %d\n",
|
|
__func__, ctrl->value);
|
|
if (ctrl->value)
|
|
/* enable RSQ interrupts */
|
|
retval = configure_interrupts(radio,
|
|
ENABLE_RSQ_INTERRUPTS);
|
|
else
|
|
/* disable RSQ interrupts */
|
|
retval = configure_interrupts(radio,
|
|
DISABLE_RSQ_INTERRUPTS);
|
|
if (retval < 0) {
|
|
FMDERR("%s: setting AF jump mode failed %d\n",
|
|
__func__, retval);
|
|
goto end;
|
|
}
|
|
/* Save the AF jump state */
|
|
radio->is_af_jump_enabled = ctrl->value;
|
|
break;
|
|
case V4L2_CID_PRIVATE_SILABS_SINR_THRESHOLD:
|
|
FMDBG("%s: V4L2_CID_PRIVATE_SILABS_SINR_THRESHOLD, val is %d\n",
|
|
__func__, ctrl->value);
|
|
if (is_valid_sinr(ctrl->value)) {
|
|
retval = set_property(radio,
|
|
FM_SEEK_TUNE_SNR_THRESHOLD_PROP,
|
|
ctrl->value);
|
|
if (retval < 0) {
|
|
FMDERR("%s: setting sinr th failed, error %d\n",
|
|
__func__, retval);
|
|
goto end;
|
|
}
|
|
|
|
radio->sinr_th = ctrl->value;
|
|
|
|
} else {
|
|
retval = -EINVAL;
|
|
FMDERR("%s: Invalid sinr\n", __func__);
|
|
}
|
|
break;
|
|
case V4L2_CID_PRIVATE_SILABS_RSSI_TH:
|
|
FMDBG("%s: V4L2_CID_PRIVATE_SILABS_RSSI_TH, val is %d\n",
|
|
__func__, ctrl->value);
|
|
if (is_valid_rssi(ctrl->value)) {
|
|
retval = set_property(radio,
|
|
FM_SEEK_TUNE_RSSI_THRESHOLD_PROP,
|
|
ctrl->value);
|
|
if (retval < 0) {
|
|
FMDERR("%s: setting rssi th failed, error %d\n",
|
|
__func__, retval);
|
|
goto end;
|
|
}
|
|
|
|
radio->rssi_th = ctrl->value;
|
|
|
|
} else {
|
|
retval = -EINVAL;
|
|
FMDERR("%s: Invalid sinr\n", __func__);
|
|
}
|
|
break;
|
|
case V4L2_CID_PRIVATE_SILABS_AF_JUMP_RSSI_TH:
|
|
FMDBG("%s: V4L2_CID_PRIVATE_SILABS_AF_JUMP_RSSI_TH, val %d\n",
|
|
__func__, ctrl->value);
|
|
if (is_valid_rssi(ctrl->value)) {
|
|
retval = set_property(radio,
|
|
FM_RSQ_RSSI_LO_THRESHOLD_PROP,
|
|
ctrl->value);
|
|
if (retval < 0) {
|
|
FMDERR("%s: setting af rssi th failed err %d\n",
|
|
__func__, retval);
|
|
goto end;
|
|
}
|
|
|
|
radio->af_rssi_th = ctrl->value;
|
|
|
|
} else {
|
|
retval = -EINVAL;
|
|
FMDERR("%s: Invalid sinr\n", __func__);
|
|
}
|
|
|
|
break;
|
|
case V4L2_CID_PRIVATE_SILABS_AF_RMSSI_SAMPLES:
|
|
FMDBG("%s: V4L2_CID_PRIVATE_SILABS_AF_RMSSI_SAMPLES, val %d\n",
|
|
__func__, ctrl->value);
|
|
if ((ctrl->value >= 0) || (ctrl->value <= MAX_AF_WAIT_SEC))
|
|
radio->af_wait_timer = ctrl->value;
|
|
break;
|
|
case V4L2_CID_PRIVATE_SILABS_SRCH_CNT:
|
|
retval = 0;
|
|
break;
|
|
default:
|
|
retval = -EINVAL;
|
|
break;
|
|
}
|
|
|
|
if (retval < 0)
|
|
FMDERR("set control failed with %d, id:%x\n", retval, ctrl->id);
|
|
|
|
end:
|
|
return retval;
|
|
}
|
|
|
|
static int silabs_fm_vidioc_s_tuner(struct file *file, void *priv,
|
|
const struct v4l2_tuner *tuner)
|
|
{
|
|
struct silabs_fm_device *radio = video_get_drvdata(video_devdata(file));
|
|
int retval = 0;
|
|
u16 prop_val = 0;
|
|
|
|
if (unlikely(radio == NULL)) {
|
|
FMDERR("%s:radio is null", __func__);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (unlikely(tuner == NULL)) {
|
|
FMDERR("%s:tuner is null", __func__);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (tuner->index > 0)
|
|
return -EINVAL;
|
|
|
|
FMDBG("In %s, setting top and bottom band limits\n", __func__);
|
|
|
|
prop_val = (u16)((tuner->rangelow / TUNE_PARAM) / TUNE_STEP_SIZE);
|
|
FMDBG("In %s, tuner->rangelow is %d, setting bottom band to %d\n",
|
|
__func__, tuner->rangelow, prop_val);
|
|
|
|
retval = set_property(radio, FM_SEEK_BAND_BOTTOM_PROP, prop_val);
|
|
if (retval < 0)
|
|
FMDERR("In %s, error %d setting lower limit freq\n",
|
|
__func__, retval);
|
|
else
|
|
radio->recv_conf.band_low_limit = prop_val;
|
|
|
|
prop_val = (u16)((tuner->rangehigh / TUNE_PARAM) / TUNE_STEP_SIZE);
|
|
FMDBG("In %s, tuner->rangehigh is %d, setting top band to %d\n",
|
|
__func__, tuner->rangehigh, prop_val);
|
|
|
|
retval = set_property(radio, FM_SEEK_BAND_TOP_PROP, prop_val);
|
|
if (retval < 0)
|
|
FMDERR("In %s, error %d setting upper limit freq\n",
|
|
__func__, retval);
|
|
else
|
|
radio->recv_conf.band_high_limit = prop_val;
|
|
|
|
if (retval < 0)
|
|
FMDERR(": set tuner failed with %d\n", retval);
|
|
|
|
return retval;
|
|
}
|
|
|
|
static int silabs_fm_vidioc_g_tuner(struct file *file, void *priv,
|
|
struct v4l2_tuner *tuner)
|
|
{
|
|
int retval = 0;
|
|
struct silabs_fm_device *radio = video_get_drvdata(video_devdata(file));
|
|
|
|
if (unlikely(radio == NULL)) {
|
|
FMDERR(":radio is null");
|
|
return -EINVAL;
|
|
}
|
|
if (tuner == NULL) {
|
|
FMDERR("%s, tuner is null\n", __func__);
|
|
return -EINVAL;
|
|
}
|
|
if (tuner->index > 0) {
|
|
FMDERR("Invalid Tuner Index");
|
|
return -EINVAL;
|
|
}
|
|
|
|
mutex_lock(&radio->lock);
|
|
|
|
memset(radio->write_buf, 0, WRITE_REG_NUM);
|
|
|
|
/* track command that is being sent to chip. */
|
|
radio->cmd = FM_TUNE_STATUS_CMD;
|
|
|
|
radio->write_buf[0] = FM_TUNE_STATUS_CMD;
|
|
radio->write_buf[1] = 0;
|
|
|
|
retval = send_cmd(radio, TUNE_STATUS_CMD_LEN);
|
|
if (retval < 0) {
|
|
FMDERR("In %s, FM_TUNE_STATUS_CMD failed with error %d\n",
|
|
__func__, retval);
|
|
mutex_unlock(&radio->lock);
|
|
goto get_prop_fail;
|
|
}
|
|
|
|
/* rssi */
|
|
tuner->signal = radio->read_buf[4];
|
|
mutex_unlock(&radio->lock);
|
|
|
|
retval = get_property(radio,
|
|
FM_SEEK_BAND_BOTTOM_PROP,
|
|
&radio->recv_conf.band_low_limit);
|
|
if (retval < 0) {
|
|
FMDERR("%s: get FM_SEEK_BAND_BOTTOM_PROP failed, error %d\n",
|
|
__func__, retval);
|
|
goto get_prop_fail;
|
|
}
|
|
|
|
FMDBG("In %s, radio->recv_conf.band_low_limit is %d\n",
|
|
__func__, radio->recv_conf.band_low_limit);
|
|
retval = get_property(radio,
|
|
FM_SEEK_BAND_TOP_PROP,
|
|
&radio->recv_conf.band_high_limit);
|
|
if (retval < 0) {
|
|
FMDERR("In %s, get FM_SEEK_BAND_TOP_PROP failed, error %d\n",
|
|
__func__, retval);
|
|
goto get_prop_fail;
|
|
}
|
|
FMDBG("In %s, radio->recv_conf.band_high_limit is %d\n",
|
|
__func__, radio->recv_conf.band_high_limit);
|
|
|
|
tuner->type = V4L2_TUNER_RADIO;
|
|
tuner->rangelow =
|
|
radio->recv_conf.band_low_limit * TUNE_STEP_SIZE * TUNE_PARAM;
|
|
tuner->rangehigh =
|
|
radio->recv_conf.band_high_limit * TUNE_STEP_SIZE * TUNE_PARAM;
|
|
tuner->rxsubchans = V4L2_TUNER_SUB_MONO | V4L2_TUNER_SUB_STEREO;
|
|
tuner->capability = V4L2_TUNER_CAP_LOW;
|
|
|
|
tuner->audmode = 0;
|
|
tuner->afc = 0;
|
|
|
|
get_prop_fail:
|
|
return retval;
|
|
}
|
|
|
|
static int silabs_fm_vidioc_g_frequency(struct file *file, void *priv,
|
|
struct v4l2_frequency *freq)
|
|
{
|
|
struct silabs_fm_device *radio = video_get_drvdata(video_devdata(file));
|
|
u32 f;
|
|
u8 snr, rssi;
|
|
int retval = 0;
|
|
|
|
if (unlikely(radio == NULL)) {
|
|
FMDERR(":radio is null");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (freq == NULL) {
|
|
FMDERR("%s, v4l2 freq is null\n", __func__);
|
|
return -EINVAL;
|
|
}
|
|
|
|
mutex_lock(&radio->lock);
|
|
memset(radio->write_buf, 0, WRITE_REG_NUM);
|
|
|
|
/* track command that is being sent to chip. */
|
|
radio->cmd = FM_TUNE_STATUS_CMD;
|
|
|
|
radio->write_buf[0] = FM_TUNE_STATUS_CMD;
|
|
radio->write_buf[1] = 0;
|
|
|
|
retval = send_cmd(radio, TUNE_STATUS_CMD_LEN);
|
|
if (retval < 0) {
|
|
FMDERR("In %s, get station freq cmd failed with error %d\n",
|
|
__func__, retval);
|
|
mutex_unlock(&radio->lock);
|
|
goto send_cmd_fail;
|
|
}
|
|
|
|
f = (radio->read_buf[2] << 8) + radio->read_buf[3];
|
|
freq->frequency = f * TUNE_PARAM * TUNE_STEP_SIZE;
|
|
radio->tuned_freq_khz = f * TUNE_STEP_SIZE;
|
|
|
|
rssi = radio->read_buf[4];
|
|
snr = radio->read_buf[5];
|
|
mutex_unlock(&radio->lock);
|
|
|
|
FMDBG("In %s, freq is %d, rssi %u, snr %u\n",
|
|
__func__, f * TUNE_STEP_SIZE, rssi, snr);
|
|
|
|
send_cmd_fail:
|
|
return retval;
|
|
}
|
|
|
|
static int silabs_fm_vidioc_s_frequency(struct file *file, void *priv,
|
|
const struct v4l2_frequency *freq)
|
|
{
|
|
struct silabs_fm_device *radio = video_get_drvdata(video_devdata(file));
|
|
int retval = -1;
|
|
u32 f = 0;
|
|
|
|
if (unlikely(radio == NULL)) {
|
|
FMDERR("%s:radio is null", __func__);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (unlikely(freq == NULL)) {
|
|
FMDERR("%s:freq is null", __func__);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (freq->type != V4L2_TUNER_RADIO)
|
|
return -EINVAL;
|
|
|
|
f = (freq->frequency)/TUNE_PARAM;
|
|
|
|
FMDBG("Calling tune with freq %u\n", f);
|
|
|
|
radio->seek_tune_status = TUNE_PENDING;
|
|
|
|
retval = tune(radio, f);
|
|
|
|
/* save the current frequency if tune is successful. */
|
|
if (retval > 0) {
|
|
radio->tuned_freq_khz = f;
|
|
/* Clear AF list */
|
|
reset_af_info(radio);
|
|
cancel_delayed_work_sync(&radio->work_af);
|
|
flush_workqueue(radio->wqueue_af);
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
static int silabs_fm_vidioc_s_hw_freq_seek(struct file *file, void *priv,
|
|
const struct v4l2_hw_freq_seek *seek)
|
|
{
|
|
struct silabs_fm_device *radio = video_get_drvdata(video_devdata(file));
|
|
int dir;
|
|
int retval = 0;
|
|
|
|
if (unlikely(radio == NULL)) {
|
|
FMDERR("%s:radio is null", __func__);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (unlikely(seek == NULL)) {
|
|
FMDERR("%s:seek is null", __func__);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (seek->seek_upward)
|
|
dir = SRCH_DIR_UP;
|
|
else
|
|
dir = SRCH_DIR_DOWN;
|
|
|
|
radio->is_search_cancelled = false;
|
|
|
|
if (radio->g_search_mode == SEEK) {
|
|
/* seek */
|
|
FMDBG("starting seek\n");
|
|
|
|
radio->seek_tune_status = SEEK_PENDING;
|
|
|
|
retval = silabs_seek(radio, dir, WRAP_ENABLE);
|
|
|
|
} else if ((radio->g_search_mode == SCAN) ||
|
|
(radio->g_search_mode == SCAN_FOR_STRONG)) {
|
|
/* scan */
|
|
if (radio->g_search_mode == SCAN_FOR_STRONG) {
|
|
FMDBG("starting search list\n");
|
|
memset(&radio->srch_list, 0,
|
|
sizeof(struct silabs_srch_list_compl));
|
|
} else {
|
|
FMDBG("starting scan\n");
|
|
}
|
|
silabs_search(radio, START_SCAN);
|
|
|
|
} else {
|
|
retval = -EINVAL;
|
|
FMDERR("In %s, invalid search mode %d\n",
|
|
__func__, radio->g_search_mode);
|
|
}
|
|
|
|
if (retval > 0) {
|
|
/* Clear AF list */
|
|
reset_af_info(radio);
|
|
cancel_delayed_work_sync(&radio->work_af);
|
|
flush_workqueue(radio->wqueue_af);
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
static int silabs_fm_vidioc_dqbuf(struct file *file, void *priv,
|
|
struct v4l2_buffer *buffer)
|
|
{
|
|
|
|
struct silabs_fm_device *radio = video_get_drvdata(video_devdata(file));
|
|
enum silabs_buf_t buf_type = -1;
|
|
u8 buf_fifo[STD_BUF_SIZE] = {0};
|
|
struct kfifo *data_fifo = NULL;
|
|
u8 *buf = NULL;
|
|
int len = 0, retval = -1;
|
|
|
|
if ((radio == NULL) || (buffer == NULL)) {
|
|
FMDERR("radio/buffer is NULL\n");
|
|
return -ENXIO;
|
|
}
|
|
buf_type = buffer->index;
|
|
buf = (u8 *)buffer->m.userptr;
|
|
len = buffer->length;
|
|
FMDBG("%s: requesting buffer %d\n", __func__, buf_type);
|
|
|
|
if ((buf_type < SILABS_FM_BUF_MAX) && (buf_type >= 0)) {
|
|
data_fifo = &radio->data_buf[buf_type];
|
|
if (buf_type == SILABS_FM_BUF_EVENTS) {
|
|
if (wait_event_interruptible(radio->event_queue,
|
|
kfifo_len(data_fifo)) < 0) {
|
|
return -EINTR;
|
|
}
|
|
}
|
|
} else {
|
|
FMDERR("invalid buffer type\n");
|
|
return -EINVAL;
|
|
}
|
|
if (len <= STD_BUF_SIZE) {
|
|
buffer->bytesused = kfifo_out_locked(data_fifo, &buf_fifo[0],
|
|
len, &radio->buf_lock[buf_type]);
|
|
} else {
|
|
FMDERR("kfifo_out_locked can not use len more than 128\n");
|
|
return -EINVAL;
|
|
}
|
|
retval = copy_to_user(buf, &buf_fifo[0], buffer->bytesused);
|
|
if (retval > 0) {
|
|
FMDERR("Failed to copy %d bytes of data\n", retval);
|
|
return -EAGAIN;
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
static int silabs_fm_pinctrl_init(struct silabs_fm_device *radio)
|
|
{
|
|
int retval = 0;
|
|
|
|
radio->fm_pinctrl = devm_pinctrl_get(&radio->client->dev);
|
|
if (IS_ERR_OR_NULL(radio->fm_pinctrl)) {
|
|
FMDERR("%s: target does not use pinctrl\n", __func__);
|
|
retval = PTR_ERR(radio->fm_pinctrl);
|
|
return retval;
|
|
}
|
|
|
|
radio->gpio_state_active =
|
|
pinctrl_lookup_state(radio->fm_pinctrl,
|
|
"pmx_fm_active");
|
|
if (IS_ERR_OR_NULL(radio->gpio_state_active)) {
|
|
FMDERR("%s: cannot get FM active state\n", __func__);
|
|
retval = PTR_ERR(radio->gpio_state_active);
|
|
goto err_active_state;
|
|
}
|
|
|
|
radio->gpio_state_suspend =
|
|
pinctrl_lookup_state(radio->fm_pinctrl,
|
|
"pmx_fm_suspend");
|
|
if (IS_ERR_OR_NULL(radio->gpio_state_suspend)) {
|
|
FMDERR("%s: cannot get FM suspend state\n", __func__);
|
|
retval = PTR_ERR(radio->gpio_state_suspend);
|
|
goto err_suspend_state;
|
|
}
|
|
|
|
return retval;
|
|
|
|
err_suspend_state:
|
|
radio->gpio_state_suspend = 0;
|
|
|
|
err_active_state:
|
|
radio->gpio_state_active = 0;
|
|
|
|
return retval;
|
|
}
|
|
|
|
static int silabs_parse_dt(struct device *dev,
|
|
struct silabs_fm_device *radio)
|
|
{
|
|
int rc = 0;
|
|
struct device_node *np = dev->of_node;
|
|
|
|
radio->reset_gpio = of_get_named_gpio(np, "silabs,reset-gpio", 0);
|
|
if (radio->reset_gpio < 0) {
|
|
FMDERR("silabs-reset-gpio not provided in device tree");
|
|
return radio->reset_gpio;
|
|
}
|
|
|
|
rc = gpio_request(radio->reset_gpio, "fm_rst_gpio_n");
|
|
if (rc) {
|
|
FMDERR("unable to request gpio %d (%d)\n",
|
|
radio->reset_gpio, rc);
|
|
return rc;
|
|
}
|
|
|
|
radio->int_gpio = of_get_named_gpio(np, "silabs,int-gpio", 0);
|
|
if (radio->int_gpio < 0) {
|
|
FMDERR("silabs-int-gpio not provided in device tree");
|
|
rc = radio->int_gpio;
|
|
goto err_int_gpio;
|
|
}
|
|
|
|
rc = gpio_request(radio->int_gpio, "silabs_fm_int_n");
|
|
if (rc) {
|
|
FMDERR("unable to request gpio %d (%d)\n",
|
|
radio->int_gpio, rc);
|
|
goto err_int_gpio;
|
|
}
|
|
|
|
radio->status_gpio = of_get_named_gpio(np, "silabs,status-gpio", 0);
|
|
if (radio->status_gpio < 0) {
|
|
FMDERR("silabs-status-gpio not provided in device tree");
|
|
} else {
|
|
rc = gpio_request(radio->status_gpio, "silabs_fm_stat_n");
|
|
if (rc) {
|
|
FMDERR("unable to request status gpio %d (%d)\n",
|
|
radio->status_gpio, rc);
|
|
goto err_status_gpio;
|
|
}
|
|
}
|
|
return rc;
|
|
|
|
err_status_gpio:
|
|
gpio_free(radio->int_gpio);
|
|
err_int_gpio:
|
|
gpio_free(radio->reset_gpio);
|
|
|
|
return rc;
|
|
}
|
|
|
|
static int silabs_dt_parse_vreg_info(struct device *dev,
|
|
struct fm_power_vreg_data *vreg, const char *vreg_name)
|
|
{
|
|
int ret = 0;
|
|
u32 vol_suply[2];
|
|
struct device_node *np = dev->of_node;
|
|
|
|
ret = of_property_read_u32_array(np, vreg_name, vol_suply, 2);
|
|
if (ret < 0) {
|
|
FMDERR("Invalid property name\n");
|
|
ret = -EINVAL;
|
|
} else {
|
|
vreg->low_vol_level = vol_suply[0];
|
|
vreg->high_vol_level = vol_suply[1];
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static int silabs_fm_vidioc_g_fmt_type_private(struct file *file, void *priv,
|
|
struct v4l2_format *f)
|
|
{
|
|
return 0;
|
|
|
|
}
|
|
|
|
static const struct v4l2_ioctl_ops silabs_fm_ioctl_ops = {
|
|
.vidioc_querycap = silabs_fm_vidioc_querycap,
|
|
.vidioc_queryctrl = silabs_fm_vidioc_queryctrl,
|
|
.vidioc_g_ctrl = silabs_fm_vidioc_g_ctrl,
|
|
.vidioc_s_ctrl = silabs_fm_vidioc_s_ctrl,
|
|
.vidioc_g_tuner = silabs_fm_vidioc_g_tuner,
|
|
.vidioc_s_tuner = silabs_fm_vidioc_s_tuner,
|
|
.vidioc_g_frequency = silabs_fm_vidioc_g_frequency,
|
|
.vidioc_s_frequency = silabs_fm_vidioc_s_frequency,
|
|
.vidioc_s_hw_freq_seek = silabs_fm_vidioc_s_hw_freq_seek,
|
|
.vidioc_dqbuf = silabs_fm_vidioc_dqbuf,
|
|
.vidioc_g_fmt_type_private = silabs_fm_vidioc_g_fmt_type_private,
|
|
};
|
|
|
|
static const struct v4l2_file_operations silabs_fm_fops = {
|
|
.owner = THIS_MODULE,
|
|
.ioctl = video_ioctl2,
|
|
#ifdef CONFIG_COMPAT
|
|
.compat_ioctl32 = v4l2_compat_ioctl32,
|
|
#endif
|
|
.open = silabs_fm_fops_open,
|
|
.release = silabs_fm_fops_release,
|
|
};
|
|
|
|
|
|
static const struct video_device silabs_fm_viddev_template = {
|
|
.fops = &silabs_fm_fops,
|
|
.ioctl_ops = &silabs_fm_ioctl_ops,
|
|
.name = DRIVER_NAME,
|
|
.release = video_device_release,
|
|
};
|
|
|
|
static int silabs_fm_probe(struct i2c_client *client,
|
|
const struct i2c_device_id *id)
|
|
{
|
|
|
|
struct silabs_fm_device *radio;
|
|
struct regulator *vreg = NULL;
|
|
int retval = 0;
|
|
int i = 0;
|
|
int kfifo_alloc_rc = 0;
|
|
|
|
if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
|
|
FMDERR("%s: no support for i2c read/write byte data\n",
|
|
__func__);
|
|
return -EIO;
|
|
}
|
|
|
|
vreg = regulator_get(&client->dev, "va");
|
|
|
|
if (IS_ERR(vreg)) {
|
|
/*
|
|
* if analog voltage regulator, VA is not ready yet, return
|
|
* -EPROBE_DEFER to kernel so that probe will be called at
|
|
* later point of time.
|
|
*/
|
|
if (PTR_ERR(vreg) == -EPROBE_DEFER) {
|
|
FMDERR("In %s, areg probe defer\n", __func__);
|
|
return PTR_ERR(vreg);
|
|
}
|
|
}
|
|
/* private data allocation */
|
|
radio = kzalloc(sizeof(struct silabs_fm_device), GFP_KERNEL);
|
|
if (!radio) {
|
|
FMDERR("Memory not allocated for radio\n");
|
|
retval = -ENOMEM;
|
|
goto err_initial;
|
|
}
|
|
|
|
retval = silabs_parse_dt(&client->dev, radio);
|
|
if (retval) {
|
|
FMDERR("%s: Parsing DT failed(%d)", __func__, retval);
|
|
regulator_put(vreg);
|
|
kfree(radio);
|
|
return retval;
|
|
}
|
|
|
|
radio->client = client;
|
|
|
|
i2c_set_clientdata(client, radio);
|
|
if (!IS_ERR(vreg)) {
|
|
radio->areg = devm_kzalloc(&client->dev,
|
|
sizeof(struct fm_power_vreg_data),
|
|
GFP_KERNEL);
|
|
if (!radio->areg) {
|
|
FMDERR("%s: allocating memory for areg failed\n",
|
|
__func__);
|
|
regulator_put(vreg);
|
|
kfree(radio);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
radio->areg->reg = vreg;
|
|
radio->areg->name = "va";
|
|
radio->areg->is_enabled = 0;
|
|
retval = silabs_dt_parse_vreg_info(&client->dev,
|
|
radio->areg, "silabs,va-supply-voltage");
|
|
if (retval < 0) {
|
|
FMDERR("%s: parsing va-supply failed\n", __func__);
|
|
goto mem_alloc_fail;
|
|
}
|
|
}
|
|
|
|
vreg = regulator_get(&client->dev, "vdd");
|
|
|
|
if (IS_ERR(vreg)) {
|
|
FMDERR("In %s, vdd supply is not provided\n", __func__);
|
|
} else {
|
|
radio->dreg = devm_kzalloc(&client->dev,
|
|
sizeof(struct fm_power_vreg_data),
|
|
GFP_KERNEL);
|
|
if (!radio->dreg) {
|
|
FMDERR("%s: allocating memory for dreg failed\n",
|
|
__func__);
|
|
retval = -ENOMEM;
|
|
regulator_put(vreg);
|
|
goto mem_alloc_fail;
|
|
}
|
|
|
|
radio->dreg->reg = vreg;
|
|
radio->dreg->name = "vdd";
|
|
radio->dreg->is_enabled = 0;
|
|
retval = silabs_dt_parse_vreg_info(&client->dev,
|
|
radio->dreg, "silabs,vdd-supply-voltage");
|
|
if (retval < 0) {
|
|
FMDERR("%s: parsing vdd-supply failed\n", __func__);
|
|
goto err_dreg;
|
|
}
|
|
}
|
|
|
|
/* Initialize pin control*/
|
|
retval = silabs_fm_pinctrl_init(radio);
|
|
if (retval) {
|
|
FMDERR("%s: silabs_fm_pinctrl_init returned %d\n",
|
|
__func__, retval);
|
|
/* if pinctrl is not supported, -EINVAL is returned*/
|
|
if (retval == -EINVAL)
|
|
retval = 0;
|
|
} else {
|
|
FMDBG("silabs_fm_pinctrl_init success\n");
|
|
}
|
|
|
|
radio->wqueue = NULL;
|
|
radio->wqueue_scan = NULL;
|
|
radio->wqueue_af = NULL;
|
|
radio->wqueue_rds = NULL;
|
|
|
|
/* video device allocation */
|
|
radio->videodev = video_device_alloc();
|
|
if (!radio->videodev) {
|
|
FMDERR("radio->videodev is NULL\n");
|
|
goto err_dreg;
|
|
}
|
|
/* initial configuration */
|
|
memcpy(radio->videodev, &silabs_fm_viddev_template,
|
|
sizeof(silabs_fm_viddev_template));
|
|
strlcpy(radio->v4l2_dev.name, DRIVER_NAME,
|
|
sizeof(radio->v4l2_dev.name));
|
|
retval = v4l2_device_register(NULL, &radio->v4l2_dev);
|
|
if (retval)
|
|
goto err_dreg;
|
|
radio->videodev->v4l2_dev = &radio->v4l2_dev;
|
|
|
|
/*allocate internal buffers for decoded rds and event buffer*/
|
|
for (i = 0; i < SILABS_FM_BUF_MAX; i++) {
|
|
spin_lock_init(&radio->buf_lock[i]);
|
|
|
|
if (i == SILABS_FM_BUF_RAW_RDS)
|
|
kfifo_alloc_rc = kfifo_alloc(&radio->data_buf[i],
|
|
FM_RDS_BUF * 3, GFP_KERNEL);
|
|
else if (i == SILABS_FM_BUF_RT_RDS)
|
|
kfifo_alloc_rc = kfifo_alloc(&radio->data_buf[i],
|
|
STD_BUF_SIZE * 2, GFP_KERNEL);
|
|
else
|
|
kfifo_alloc_rc = kfifo_alloc(&radio->data_buf[i],
|
|
STD_BUF_SIZE, GFP_KERNEL);
|
|
|
|
if (kfifo_alloc_rc != 0) {
|
|
FMDERR("%s: failed allocating buffers %d\n",
|
|
__func__, kfifo_alloc_rc);
|
|
retval = -ENOMEM;
|
|
goto err_fifo_alloc;
|
|
}
|
|
}
|
|
/* initializing the device count */
|
|
atomic_set(&radio->users, 1);
|
|
|
|
/* radio initializes to normal mode */
|
|
radio->lp_mode = 0;
|
|
radio->handle_irq = 1;
|
|
radio->af_wait_timer = AF_WAIT_SEC;
|
|
/* init lock */
|
|
mutex_init(&radio->lock);
|
|
radio->tune_req = 0;
|
|
radio->seek_tune_status = 0;
|
|
init_completion(&radio->sync_req_done);
|
|
/* initialize wait queue for event read */
|
|
init_waitqueue_head(&radio->event_queue);
|
|
/* initialize wait queue for raw rds read */
|
|
init_waitqueue_head(&radio->read_queue);
|
|
|
|
video_set_drvdata(radio->videodev, radio);
|
|
|
|
/*
|
|
* Start the worker thread for event handling and register read_int_stat
|
|
* as worker function
|
|
*/
|
|
radio->wqueue = create_singlethread_workqueue("sifmradio");
|
|
|
|
if (!radio->wqueue) {
|
|
retval = -ENOMEM;
|
|
goto err_fifo_alloc;
|
|
}
|
|
|
|
FMDBG("%s: creating work q for scan\n", __func__);
|
|
radio->wqueue_scan = create_singlethread_workqueue("sifmradioscan");
|
|
|
|
if (!radio->wqueue_scan) {
|
|
retval = -ENOMEM;
|
|
goto err_wqueue_scan;
|
|
}
|
|
|
|
FMDBG("%s: creating work q for af\n", __func__);
|
|
radio->wqueue_af = create_singlethread_workqueue("sifmradioaf");
|
|
|
|
if (!radio->wqueue_af) {
|
|
retval = -ENOMEM;
|
|
goto err_wqueue_af;
|
|
}
|
|
|
|
radio->wqueue_rds = create_singlethread_workqueue("sifmradiords");
|
|
|
|
if (!radio->wqueue_rds) {
|
|
retval = -ENOMEM;
|
|
goto err_wqueue_rds;
|
|
}
|
|
|
|
/* register video device */
|
|
retval = video_register_device(radio->videodev,
|
|
VFL_TYPE_RADIO,
|
|
RADIO_NR);
|
|
if (retval != 0) {
|
|
FMDERR("Could not register video device\n");
|
|
goto err_all;
|
|
}
|
|
return 0;
|
|
|
|
err_all:
|
|
destroy_workqueue(radio->wqueue_rds);
|
|
err_wqueue_rds:
|
|
destroy_workqueue(radio->wqueue_af);
|
|
err_wqueue_af:
|
|
destroy_workqueue(radio->wqueue_scan);
|
|
err_wqueue_scan:
|
|
destroy_workqueue(radio->wqueue);
|
|
err_fifo_alloc:
|
|
for (i--; i >= 0; i--)
|
|
kfifo_free(&radio->data_buf[i]);
|
|
video_device_release(radio->videodev);
|
|
err_dreg:
|
|
if (radio->dreg && radio->dreg->reg) {
|
|
regulator_put(radio->dreg->reg);
|
|
devm_kfree(&client->dev, radio->dreg);
|
|
}
|
|
mem_alloc_fail:
|
|
if (radio->areg && radio->areg->reg) {
|
|
regulator_put(radio->areg->reg);
|
|
devm_kfree(&client->dev, radio->areg);
|
|
}
|
|
kfree(radio);
|
|
err_initial:
|
|
return retval;
|
|
}
|
|
|
|
static int silabs_fm_remove(struct i2c_client *client)
|
|
{
|
|
int i;
|
|
struct silabs_fm_device *radio = i2c_get_clientdata(client);
|
|
|
|
if (unlikely(radio == NULL)) {
|
|
FMDERR("%s:radio is null", __func__);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (radio->dreg && radio->dreg->reg) {
|
|
regulator_put(radio->dreg->reg);
|
|
devm_kfree(&client->dev, radio->dreg);
|
|
}
|
|
|
|
if (radio->areg && radio->areg->reg) {
|
|
regulator_put(radio->areg->reg);
|
|
devm_kfree(&client->dev, radio->areg);
|
|
}
|
|
/* disable irq */
|
|
destroy_workqueue(radio->wqueue);
|
|
destroy_workqueue(radio->wqueue_scan);
|
|
destroy_workqueue(radio->wqueue_af);
|
|
destroy_workqueue(radio->wqueue_rds);
|
|
|
|
video_unregister_device(radio->videodev);
|
|
|
|
/* free internal buffers */
|
|
for (i = 0; i < SILABS_FM_BUF_MAX; i++)
|
|
kfifo_free(&radio->data_buf[i]);
|
|
|
|
/* free state struct */
|
|
kfree(radio);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct i2c_device_id silabs_i2c_id[] = {
|
|
{ DRIVER_NAME, 0 },
|
|
{ },
|
|
};
|
|
MODULE_DEVICE_TABLE(i2c, silabs_i2c_id);
|
|
|
|
static const struct of_device_id silabs_fm_match[] = {
|
|
{.compatible = "silabs,si4705"},
|
|
{}
|
|
};
|
|
|
|
static struct i2c_driver silabs_fm_driver = {
|
|
.probe = silabs_fm_probe,
|
|
.driver = {
|
|
.owner = THIS_MODULE,
|
|
.name = "silabs-fm",
|
|
.of_match_table = silabs_fm_match,
|
|
},
|
|
.remove = silabs_fm_remove,
|
|
.id_table = silabs_i2c_id,
|
|
};
|
|
|
|
|
|
static int __init radio_module_init(void)
|
|
{
|
|
return i2c_add_driver(&silabs_fm_driver);
|
|
}
|
|
module_init(radio_module_init);
|
|
|
|
static void __exit radio_module_exit(void)
|
|
{
|
|
i2c_del_driver(&silabs_fm_driver);
|
|
}
|
|
module_exit(radio_module_exit);
|
|
|
|
MODULE_LICENSE("GPL v2");
|
|
MODULE_DESCRIPTION(DRIVER_DESC);
|
|
|