2073 lines
53 KiB
C
2073 lines
53 KiB
C
|
/*
|
||
|
* drivers/spi/spi-im501.c
|
||
|
* (C) Copyright 2014-2018
|
||
|
* Fortemedia, Inc. <www.fortemedia.com>
|
||
|
*
|
||
|
* Copyright (c) 2019, NVIDIA CORPORATION. All rights reserved.
|
||
|
*
|
||
|
* Author: HenryZhang <henryhzhang@fortemedia.com>;
|
||
|
* LiFu <fuli@fortemedia.com>
|
||
|
*
|
||
|
* This program is free software; you can redistribute it and/or
|
||
|
* modify it under the terms of the GNU General Public License as
|
||
|
* published by the Free Software Foundation; either version 2 of
|
||
|
* the License, or (at your option) any later version.
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
#include <linux/init.h>
|
||
|
#include <linux/module.h>
|
||
|
#include <linux/input.h>
|
||
|
#include <linux/spi/spi.h>
|
||
|
#include <linux/device.h>
|
||
|
#include <linux/firmware.h>
|
||
|
#include <linux/platform_device.h>
|
||
|
#include <linux/delay.h>
|
||
|
#include <linux/atomic.h>
|
||
|
#include <linux/irq.h>
|
||
|
#include <linux/interrupt.h>
|
||
|
#include <linux/slab.h>
|
||
|
#include <linux/gpio.h>
|
||
|
#include <linux/sched.h>
|
||
|
#include <linux/kthread.h>
|
||
|
#include <linux/uaccess.h>
|
||
|
#include <linux/miscdevice.h>
|
||
|
#include <linux/regulator/consumer.h>
|
||
|
#include <linux/pm_qos.h>
|
||
|
#include <linux/sysfs.h>
|
||
|
#include <linux/clk.h>
|
||
|
#include <linux/kfifo.h>
|
||
|
#include <linux/time.h>
|
||
|
#include <linux/rtc.h>
|
||
|
#include <sound/core.h>
|
||
|
#include <sound/pcm.h>
|
||
|
#include <sound/pcm_params.h>
|
||
|
#include <sound/soc.h>
|
||
|
#include <sound/soc-dapm.h>
|
||
|
#include <sound/initval.h>
|
||
|
#include <sound/tlv.h>
|
||
|
#include <asm/unistd.h>
|
||
|
|
||
|
#include "im501.h"
|
||
|
#include "spi-im501.h"
|
||
|
|
||
|
#ifdef CUBIETRUCK
|
||
|
#include <mach/sys_config.h>
|
||
|
#else
|
||
|
#include <linux/irq.h>
|
||
|
#include <linux/of_gpio.h>
|
||
|
#include <linux/hrtimer.h>
|
||
|
#endif
|
||
|
|
||
|
//#define SHOW_DL_TIME
|
||
|
#ifdef SHOW_DL_TIME
|
||
|
struct timex txc;
|
||
|
struct timex txc2;
|
||
|
struct timeval tv;
|
||
|
struct rtc_time rt;
|
||
|
#endif
|
||
|
|
||
|
//Fuli 20161215 define spi speed as constant
|
||
|
#define SPI_LOW_SPEED 1000000
|
||
|
#ifdef CUBIETRUCK
|
||
|
#define SPI_HIGH_SPEED 6000000
|
||
|
#else
|
||
|
#define SPI_HIGH_SPEED 20000000
|
||
|
#endif
|
||
|
//
|
||
|
#define FW_RD_CHECK
|
||
|
#define FW_BURST_RD_CHECK
|
||
|
//#define FW_MEM_RD_CHECK
|
||
|
|
||
|
//#define ENABLE_KERNEL_DUMP
|
||
|
#define ENABLE_KFIFO
|
||
|
#define MAX_KFIFO_BUFFER_SIZE (131072*2) /* >4 seconds */
|
||
|
#define CODEC2IM501_PDM_CLKI
|
||
|
|
||
|
//fuli 20170629 for new protocol of oneshot
|
||
|
#define SM501
|
||
|
//
|
||
|
|
||
|
void enable_pdm_clock(void);
|
||
|
void disable_pdm_clock(void);
|
||
|
static atomic_t dmic_clk_cnt = ATOMIC_INIT(0);
|
||
|
|
||
|
int im501_dsp_mode_old = -1;
|
||
|
int im501_vbuf_trans_status;
|
||
|
int ap_sleep_flag;
|
||
|
static struct spi_device *im501_spi;
|
||
|
int current_firmware_type = WAKEUP_FIRMWARE;
|
||
|
u8 firmware_type = WAKEUP_FIRMWARE; //0: wakeup firmware; 1: ultrasound firmware
|
||
|
|
||
|
//#define VERIFY_PCM_KFIFO
|
||
|
#ifdef VERIFY_PCM_KFIFO
|
||
|
#define PCM_FILE "im501_oneshot_rec.pcm"
|
||
|
u8 *g_pcm_buf;
|
||
|
u32 g_pcm_size;
|
||
|
u32 g_offset;
|
||
|
#endif //VERIFY_PCM_KFIFO
|
||
|
|
||
|
#define IM501_CDEV_NAME "im501_pcm"
|
||
|
#define REC_FILE_PATH "/data/im501_oneshot_kernel.pcm"
|
||
|
#define SPI_REC_FILE_PATH "/data/im501_spi_rec_kernel.pcm"
|
||
|
|
||
|
struct im501_data_t {
|
||
|
struct mutex lock;
|
||
|
struct mutex spi_op_lock;
|
||
|
dev_t record_chrdev;
|
||
|
struct cdev record_cdev;
|
||
|
struct device *record_dev;
|
||
|
struct kfifo *pcm_kfifo;
|
||
|
spinlock_t pcm_fifo_lock;
|
||
|
struct class *cdev_class;
|
||
|
atomic_t audio_owner;
|
||
|
int irq_gpio;
|
||
|
int reset_gpio;
|
||
|
};
|
||
|
struct im501_data_t *im501_data;
|
||
|
|
||
|
int im501_irq;
|
||
|
static struct work_struct im501_irq_work;
|
||
|
static struct work_struct im501_fw_load_work;
|
||
|
static struct workqueue_struct *im501_irq_wq;
|
||
|
static int im501_host_irqstatus; // To notify the HAL about the incoming irq.
|
||
|
//Fuli 20161216 resolve the issue about can only invoke interrupt once
|
||
|
static void im501_irq_handling_work(struct work_struct *work);
|
||
|
//
|
||
|
|
||
|
//Fuli 20170922 to support SPI recording
|
||
|
static char im501_spi_record_started;
|
||
|
struct file *fpdata = (struct file *)-1;
|
||
|
mm_segment_t oldfs;
|
||
|
static loff_t offset;
|
||
|
unsigned int output_counter;
|
||
|
|
||
|
int check_dsp_status(void);
|
||
|
|
||
|
int im501_spi_read_reg(u8 reg, u8 *val)
|
||
|
{
|
||
|
struct spi_message message;
|
||
|
struct spi_transfer x[2];
|
||
|
int status;
|
||
|
u8 write_buf[2];
|
||
|
u8 read_buf[1];
|
||
|
|
||
|
write_buf[0] = IM501_SPI_CMD_REG_RD;
|
||
|
write_buf[1] = reg & 0xff;
|
||
|
|
||
|
spi_message_init(&message);
|
||
|
memset(x, 0, sizeof(x));
|
||
|
|
||
|
x[0].len = 2;
|
||
|
x[0].tx_buf = write_buf;
|
||
|
spi_message_add_tail(&x[0], &message);
|
||
|
|
||
|
x[1].len = 1;
|
||
|
x[1].rx_buf = read_buf;
|
||
|
spi_message_add_tail(&x[1], &message);
|
||
|
|
||
|
status = spi_sync(im501_spi, &message);
|
||
|
|
||
|
*val = read_buf[0];
|
||
|
|
||
|
return status;
|
||
|
}
|
||
|
|
||
|
EXPORT_SYMBOL_GPL(im501_spi_read_reg);
|
||
|
|
||
|
int im501_spi_write_reg(u8 reg, u8 val)
|
||
|
{
|
||
|
int status;
|
||
|
u8 write_buf[3];
|
||
|
|
||
|
write_buf[0] = IM501_SPI_CMD_REG_WR;
|
||
|
write_buf[1] = reg;
|
||
|
write_buf[2] = val;
|
||
|
|
||
|
status = spi_write(im501_spi, write_buf, sizeof(write_buf));
|
||
|
|
||
|
if (status)
|
||
|
dev_err(&im501_spi->dev, "%s error %d\n", __func__, status);
|
||
|
|
||
|
return status;
|
||
|
}
|
||
|
|
||
|
EXPORT_SYMBOL_GPL(im501_spi_write_reg);
|
||
|
|
||
|
int im501_spi_read_dram(u32 addr, u8 *pdata)
|
||
|
{
|
||
|
struct spi_message message;
|
||
|
struct spi_transfer x[2];
|
||
|
int status, i;
|
||
|
u8 write_buf[6];
|
||
|
u8 read_buf[4];
|
||
|
u8 spi_cmd = IM501_SPI_CMD_DM_RD, addr_msb;
|
||
|
|
||
|
addr_msb = (addr >> 24) & 0xff;
|
||
|
if (addr_msb == 0x10) {
|
||
|
spi_cmd = IM501_SPI_CMD_IM_RD;
|
||
|
} else if (addr_msb == 0xF) {
|
||
|
spi_cmd = IM501_SPI_CMD_DM_RD;
|
||
|
}
|
||
|
|
||
|
write_buf[0] = spi_cmd;
|
||
|
write_buf[1] = addr & 0xFF;
|
||
|
write_buf[2] = (addr >> 8) & 0xFF;
|
||
|
write_buf[3] = (addr >> 16) & 0xFF;
|
||
|
write_buf[4] = 2;
|
||
|
write_buf[5] = 0;
|
||
|
|
||
|
spi_message_init(&message);
|
||
|
memset(x, 0, sizeof(x));
|
||
|
|
||
|
x[0].len = 6;
|
||
|
x[0].tx_buf = write_buf;
|
||
|
spi_message_add_tail(&x[0], &message);
|
||
|
|
||
|
x[1].len = 4;
|
||
|
x[1].rx_buf = read_buf;
|
||
|
spi_message_add_tail(&x[1], &message);
|
||
|
|
||
|
status = spi_sync(im501_spi, &message);
|
||
|
|
||
|
for (i = 0; i < 4; i++) {
|
||
|
*(pdata + i) = *(read_buf + i);
|
||
|
}
|
||
|
|
||
|
return status;
|
||
|
}
|
||
|
|
||
|
EXPORT_SYMBOL_GPL(im501_spi_read_dram);
|
||
|
|
||
|
int im501_spi_write_dram(u32 addr, u8 *pdata)
|
||
|
{
|
||
|
int status;
|
||
|
u8 buf[10];
|
||
|
|
||
|
buf[0] = IM501_SPI_CMD_DM_WR;
|
||
|
buf[1] = addr & 0xFF;
|
||
|
buf[2] = (addr >> 8) & 0xFF;
|
||
|
buf[3] = (addr >> 16) & 0xFF;
|
||
|
buf[4] = 2; //2 words
|
||
|
buf[5] = 0;
|
||
|
|
||
|
buf[6] = *pdata;
|
||
|
buf[7] = *(pdata + 1);
|
||
|
buf[8] = *(pdata + 2);
|
||
|
buf[9] = *(pdata + 3);
|
||
|
|
||
|
status = spi_write(im501_spi, buf, sizeof(buf));
|
||
|
|
||
|
return status;
|
||
|
|
||
|
}
|
||
|
|
||
|
EXPORT_SYMBOL_GPL(im501_spi_write_dram);
|
||
|
|
||
|
/**
|
||
|
* im501_spi_burst_read_dram - Read data from SPI by im501 dsp memory address.
|
||
|
* @addr: Start address.
|
||
|
* @rxbuf: Data Buffer for reading.
|
||
|
* @len: Data length, it must be a multiple of 8.
|
||
|
*
|
||
|
* Returns true for success.
|
||
|
*/
|
||
|
int im501_spi_burst_read_dram(u32 addr, u8 *rxbuf, size_t len)
|
||
|
{
|
||
|
int status;
|
||
|
u8 spi_cmd = IM501_SPI_CMD_DM_RD, addr_msb;
|
||
|
u8 write_buf[6];
|
||
|
unsigned int end, offset = 0;
|
||
|
|
||
|
struct spi_message message;
|
||
|
struct spi_transfer x[2];
|
||
|
|
||
|
addr_msb = (addr >> 24) & 0xff;
|
||
|
if (addr_msb == 0x10) {
|
||
|
spi_cmd = IM501_SPI_CMD_IM_RD;
|
||
|
} else if (addr_msb == 0xF) {
|
||
|
spi_cmd = IM501_SPI_CMD_DM_RD;
|
||
|
}
|
||
|
|
||
|
while (offset < len) {
|
||
|
if (offset + IM501_SPI_BUF_LEN <= len)
|
||
|
end = IM501_SPI_BUF_LEN;
|
||
|
else
|
||
|
end = len % IM501_SPI_BUF_LEN;
|
||
|
|
||
|
write_buf[0] = spi_cmd;
|
||
|
write_buf[1] = ((addr + offset) & 0x000000ff) >> 0;
|
||
|
write_buf[2] = ((addr + offset) & 0x0000ff00) >> 8;
|
||
|
write_buf[3] = ((addr + offset) & 0x00ff0000) >> 16;
|
||
|
write_buf[4] = (end >> 1) & 0xff;
|
||
|
write_buf[5] = (end >> (1 + 8)) & 0xff;
|
||
|
|
||
|
spi_message_init(&message);
|
||
|
memset(x, 0, sizeof(x));
|
||
|
|
||
|
x[0].len = 6;
|
||
|
x[0].tx_buf = write_buf;
|
||
|
spi_message_add_tail(&x[0], &message);
|
||
|
|
||
|
x[1].len = end;
|
||
|
x[1].rx_buf = rxbuf + offset;
|
||
|
spi_message_add_tail(&x[1], &message);
|
||
|
|
||
|
status = spi_sync(im501_spi, &message);
|
||
|
|
||
|
if (status) {
|
||
|
dev_err(&im501_spi->dev, "%s: error in spi_sync\n", __func__);
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
offset += IM501_SPI_BUF_LEN;
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
EXPORT_SYMBOL_GPL(im501_spi_burst_read_dram);
|
||
|
|
||
|
//Fuli 20161215 extract a invert copy function for burst write
|
||
|
int im501_8byte_invert_copy(u8 *source, u8 *target)
|
||
|
{
|
||
|
int i = 0;
|
||
|
|
||
|
if (source == NULL || target == NULL)
|
||
|
return -1;
|
||
|
|
||
|
for (i = 0; i < 8; i++) {
|
||
|
target[7 - i] = source[i];
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
//
|
||
|
|
||
|
//Fuli 20161215 extract a copy function for burst write
|
||
|
int im501_8byte_copy(u8 *source, u8 *target)
|
||
|
{
|
||
|
int i = 0;
|
||
|
|
||
|
if (source == NULL || target == NULL)
|
||
|
return -1;
|
||
|
|
||
|
for (i = 0; i < 8; i++) {
|
||
|
target[i] = source[i];
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
//
|
||
|
|
||
|
/**
|
||
|
* im501_spi_burst_write - Write data to SPI by im501 dsp memory address.
|
||
|
* @addr: Start address.
|
||
|
* @txbuf: Data Buffer for writng.
|
||
|
* @len: Data length, it must be a multiple of 8.
|
||
|
* @type: Firmware type, MSB and LSB, the iM501 firmware is in MSB, but the EFT firmware is in LSB.
|
||
|
*
|
||
|
* Returns true for success.
|
||
|
*/
|
||
|
int im501_spi_burst_write(u32 addr, const u8 *txbuf, size_t len, int fw_type)
|
||
|
{
|
||
|
u8 spi_cmd, *write_buf, *local_buf;
|
||
|
u32 offset = 0;
|
||
|
int i, local_len, end, status;
|
||
|
|
||
|
spi_cmd =
|
||
|
(addr == 0x10000000) ? IM501_SPI_CMD_IM_WR : IM501_SPI_CMD_DM_WR;
|
||
|
|
||
|
local_len = (len % 8) ? (((len / 8) + 1) * 8) : len;
|
||
|
local_buf = kzalloc(local_len, GFP_KERNEL);
|
||
|
if (local_buf == NULL)
|
||
|
return -ENOMEM;
|
||
|
|
||
|
memset(local_buf, 0, local_len);
|
||
|
memcpy(local_buf, txbuf, len);
|
||
|
|
||
|
write_buf = kzalloc(IM501_SPI_BUF_LEN + 6, GFP_KERNEL);
|
||
|
if (write_buf == NULL) {
|
||
|
kfree(local_buf);
|
||
|
return -ENOMEM;
|
||
|
}
|
||
|
|
||
|
write_buf[0] = spi_cmd; //Assign the command byte first, since it is fixed.
|
||
|
while (offset < local_len) {
|
||
|
if (offset + IM501_SPI_BUF_LEN <= local_len) {
|
||
|
end = IM501_SPI_BUF_LEN;
|
||
|
} else {
|
||
|
end = local_len % IM501_SPI_BUF_LEN;
|
||
|
}
|
||
|
|
||
|
write_buf[1] = ((addr + offset) & 0x000000ff) >> 0; //The memory address
|
||
|
write_buf[2] = ((addr + offset) & 0x0000ff00) >> 8;
|
||
|
write_buf[3] = ((addr + offset) & 0x00ff0000) >> 16;
|
||
|
write_buf[4] = ((end / 2) & 0x00ff) >> 0; //The word counter low byte.
|
||
|
write_buf[5] = ((end / 2) & 0xff00) >> 8; //Assign the high order word counter, since it is fixed.
|
||
|
|
||
|
if (fw_type == IM501_DSP_FW) {
|
||
|
for (i = 0; i < end; i += 8) {
|
||
|
im501_8byte_invert_copy(local_buf + offset + i,
|
||
|
write_buf + i + 6);
|
||
|
}
|
||
|
} else if (fw_type == IM501_EFT_FW) {
|
||
|
for (i = 0; i < end; i += 8) {
|
||
|
im501_8byte_copy(local_buf + offset + i,
|
||
|
write_buf + i + 6);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
status = spi_write(im501_spi, write_buf, end + 6);
|
||
|
|
||
|
if (status) {
|
||
|
dev_err(&im501_spi->dev, "%s error %d\n", __func__,
|
||
|
status);
|
||
|
kfree(write_buf);
|
||
|
kfree(local_buf);
|
||
|
return status;
|
||
|
}
|
||
|
|
||
|
offset += IM501_SPI_BUF_LEN;
|
||
|
}
|
||
|
|
||
|
kfree(write_buf);
|
||
|
kfree(local_buf);
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
EXPORT_SYMBOL_GPL(im501_spi_burst_write);
|
||
|
|
||
|
//read frame counter to check DSP is working or not
|
||
|
int check_dsp_status(void)
|
||
|
{
|
||
|
u32 framecount1, framecount2;
|
||
|
int err = ESUCCESS;
|
||
|
|
||
|
err =
|
||
|
im501_spi_read_dram(TO_DSP_FRAMECOUNTER_ADDR, (u8 *) &framecount1);
|
||
|
if (err != ESUCCESS) {
|
||
|
dev_err(&im501_spi->dev, "%s: call im501_spi_read_dram() return error(%d)\n",
|
||
|
__func__, err);
|
||
|
return err;
|
||
|
}
|
||
|
|
||
|
msleep(50);
|
||
|
|
||
|
err =
|
||
|
im501_spi_read_dram(TO_DSP_FRAMECOUNTER_ADDR, (u8 *) &framecount2);
|
||
|
if (err != ESUCCESS) {
|
||
|
dev_err(&im501_spi->dev, "%s: call im501_spi_read_dram() return error(%d)\n",
|
||
|
__func__, err);
|
||
|
return err;
|
||
|
}
|
||
|
|
||
|
framecount1 = framecount1 >> 16;
|
||
|
framecount2 = framecount2 >> 16;
|
||
|
dev_dbg(&im501_spi->dev, "%s: framecount1=%x, framecount2=%x\n", __func__, framecount1,
|
||
|
framecount2);
|
||
|
if (framecount1 != framecount2) {
|
||
|
dev_info(&im501_spi->dev, "%s: im501 is working normally.\n", __func__);
|
||
|
} else {
|
||
|
dev_err(&im501_spi->dev, "%s: im501 is NOT working.\n", __func__);
|
||
|
return EDSPNOTWORK;
|
||
|
}
|
||
|
|
||
|
return ESUCCESS;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This function should be implemented by customers as a callback function.
|
||
|
* The following is just a sample to control the PDM_CLKI on/off through UIF.
|
||
|
* The parameter @pdm_clki_rate is reserved for future use.
|
||
|
*/
|
||
|
int codec2im501_pdm_clki_set(bool set_flag, u32 rate)
|
||
|
{
|
||
|
if (set_flag == NORMAL_MODE) {
|
||
|
dev_dbg(&im501_spi->dev, "%s: enable pdm_clki rate %d\n", __func__, rate);
|
||
|
enable_pdm_clock();
|
||
|
msleep(20);
|
||
|
} else {
|
||
|
dev_dbg(&im501_spi->dev, "%s: disable pdm_clki rate %d\n", __func__, rate);
|
||
|
disable_pdm_clock();
|
||
|
msleep(20);
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
int im501_reset(void)
|
||
|
{
|
||
|
if (im501_data->reset_gpio == -1)
|
||
|
return 0;
|
||
|
|
||
|
dev_info(&im501_spi->dev, "%s: reset im501 with gpio_number = %d\n", __func__, im501_data->reset_gpio);
|
||
|
|
||
|
dev_dbg(&im501_spi->dev, "%s: set reset pin to low.\n", __func__);
|
||
|
gpio_set_value(im501_data->reset_gpio, 0);
|
||
|
msleep(20);
|
||
|
dev_dbg(&im501_spi->dev, "%s: set reset pin to high.\n", __func__);
|
||
|
gpio_set_value(im501_data->reset_gpio, 1);
|
||
|
msleep(20);
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
void writeDataFile(char *buf, int count)
|
||
|
{
|
||
|
loff_t ret;
|
||
|
if (!IS_ERR(fpdata)) {
|
||
|
oldfs = get_fs();
|
||
|
set_fs(get_ds());
|
||
|
dev_dbg(&im501_spi->dev, "writing %d\n", (int)count);
|
||
|
ret = vfs_write(fpdata, buf, count, &offset);
|
||
|
set_fs(oldfs);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void openFileForWrite(unsigned char *filePath)
|
||
|
{
|
||
|
#ifdef ENABLE_KERNEL_DUMP
|
||
|
offset = 0;
|
||
|
oldfs = get_fs();
|
||
|
set_fs(get_ds());
|
||
|
if (!IS_ERR(fpdata)) {
|
||
|
filp_close(fpdata, NULL);
|
||
|
fpdata = NULL;
|
||
|
}
|
||
|
fpdata = filp_open(filePath, O_CREAT | O_RDWR, 0666);
|
||
|
if (IS_ERR(fpdata))
|
||
|
dev_err(&im501_spi->dev, "file open failed %s\n", filePath);
|
||
|
else
|
||
|
dev_info(&im501_spi->dev, "file open success %s\n", filePath);
|
||
|
set_fs(oldfs);
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
void closeDataFile(void)
|
||
|
{
|
||
|
if (!IS_ERR(fpdata)) {
|
||
|
oldfs = get_fs();
|
||
|
set_fs(get_ds());
|
||
|
filp_close(fpdata, NULL);
|
||
|
fpdata = (struct file *)-1;
|
||
|
set_fs(oldfs);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
unsigned char fetch_voice_data(unsigned int start_addr, unsigned int data_length)
|
||
|
{
|
||
|
unsigned char err = ESUCCESS;
|
||
|
unsigned char *pbuf;
|
||
|
unsigned int i, a, b;
|
||
|
unsigned int read_size = 2048;
|
||
|
|
||
|
#ifdef SHOW_DL_TIME
|
||
|
do_gettimeofday(&(txc.time));
|
||
|
#endif
|
||
|
a = data_length / read_size;
|
||
|
b = data_length % read_size;
|
||
|
|
||
|
//dev_err(&im501_spi->dev, "%s: data_length=%d, a=%d, b=%d\n", __func__, data_length, a, b);
|
||
|
pbuf = (unsigned char *)kzalloc(read_size, GFP_KERNEL);
|
||
|
if (!pbuf) {
|
||
|
dev_err(&im501_spi->dev, "%s: pbuf allocation failure.\n", __func__);
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
for (i = 0; i < a; i++) {
|
||
|
err = im501_spi_burst_read_dram(start_addr, pbuf, read_size);
|
||
|
if (err != NO_ERR) {
|
||
|
if (pbuf)
|
||
|
kfree(pbuf);
|
||
|
dev_err(&im501_spi->dev, "%s: im501_spi_burst_read_dram() failure.\n",
|
||
|
__func__);
|
||
|
return err;
|
||
|
}
|
||
|
|
||
|
start_addr += read_size;
|
||
|
|
||
|
#ifdef ENABLE_KERNEL_DUMP
|
||
|
if (!IS_ERR(fpdata))
|
||
|
writeDataFile(pbuf, read_size);
|
||
|
#endif
|
||
|
//fuli 20170515 to reduce output dev_err(&im501_spi->dev, "%s: put the voice data to pcm FIFO.\n", __func__);
|
||
|
//if(i % 10 == 0) dev_err(&im501_spi->dev, "%s: put the voice data to pcm FIFO.\n", __func__);
|
||
|
//
|
||
|
#ifdef ENABLE_KFIFO
|
||
|
|
||
|
if (!kfifo_is_full(im501_data->pcm_kfifo)) {
|
||
|
spin_lock(&im501_data->pcm_fifo_lock);
|
||
|
kfifo_in(im501_data->pcm_kfifo, pbuf, read_size);
|
||
|
spin_unlock(&im501_data->pcm_fifo_lock);
|
||
|
}
|
||
|
else {
|
||
|
dev_err(&im501_spi->dev, "%s: PCM FIFO is full.\n", __func__);
|
||
|
}
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
if (b > 0) {
|
||
|
err = im501_spi_burst_read_dram(start_addr, pbuf, b);
|
||
|
if (err != ESUCCESS) {
|
||
|
dev_err(&im501_spi->dev,
|
||
|
"%s: im501_spi_burst_read_dram failure with %d.\n",
|
||
|
__func__, err);
|
||
|
if (pbuf)
|
||
|
kfree(pbuf);
|
||
|
return err;
|
||
|
}
|
||
|
#ifdef ENABLE_KERNEL_DUMP
|
||
|
if (!IS_ERR(fpdata))
|
||
|
writeDataFile(pbuf, b);
|
||
|
#endif
|
||
|
|
||
|
#ifdef ENABLE_KFIFO
|
||
|
if (!kfifo_is_full(im501_data->pcm_kfifo)) {
|
||
|
spin_lock(&im501_data->pcm_fifo_lock);
|
||
|
kfifo_in(im501_data->pcm_kfifo, pbuf, b);
|
||
|
spin_unlock(&im501_data->pcm_fifo_lock);
|
||
|
}
|
||
|
else {
|
||
|
dev_err(&im501_spi->dev, "%s: PCM FIFO is full.\n", __func__);
|
||
|
}
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
if (pbuf)
|
||
|
kfree(pbuf);
|
||
|
#ifdef SHOW_DL_TIME
|
||
|
do_gettimeofday(&(txc2.time));
|
||
|
dev_err(&im501_spi->dev, "%s: get one bank used %ld us\n", __func__,
|
||
|
(unsigned
|
||
|
long)((unsigned long)(txc2.time.tv_sec -
|
||
|
txc.time.tv_sec) * 1000000L +
|
||
|
(unsigned long)(txc2.time.tv_usec - txc.time.tv_usec)));
|
||
|
#endif
|
||
|
return err;
|
||
|
|
||
|
}
|
||
|
|
||
|
EXPORT_SYMBOL_GPL(fetch_voice_data);
|
||
|
|
||
|
unsigned char parse_to_host_command(to_host_cmd cmd)
|
||
|
{
|
||
|
unsigned char err = ESUCCESS;
|
||
|
unsigned int address = HW_VOICE_BUF_START;
|
||
|
u8 *pdata;
|
||
|
u32 spi_speed = SPI_HIGH_SPEED;
|
||
|
int ret;
|
||
|
char *hotword_str[2] = { "IM501_HOTWORD", NULL };
|
||
|
//fuli 20170629 for new protocol
|
||
|
int bank, sync_word_pos, data_size = HW_VOICE_BUF_BANK_SIZE;
|
||
|
//
|
||
|
|
||
|
//dev_err(&im501_spi->dev, "%s: cmd_byte = %#x\n", __func__, cmd.cmd_byte);
|
||
|
if ((im501_vbuf_trans_status == 1) && (cmd.status == 1) && (cmd.cmd_byte == TO_HOST_CMD_DATA_BUF_RDY)) { //Reuest host to read To-Host Buffer-Fast
|
||
|
//dev_err(&im501_spi->dev, "%s: data ready\n", __func__);
|
||
|
#ifdef SM501 //fuli 20170629 the protocol is different from old version
|
||
|
data_size = ((cmd.attri >> 13) & 0x7FF) << 1; //16 bits sample, one sample occupies 2 bytes.
|
||
|
bank = (cmd.attri >> 11) & 0x03;
|
||
|
sync_word_pos = cmd.attri & 0x7FF;
|
||
|
if ((bank == BANK0) || (bank == BANK0_SYNC)) {
|
||
|
address = HW_VOICE_BUF_BANK0;
|
||
|
} else if ((bank == BANK1) || (bank == BANK1_SYNC)) {
|
||
|
address = HW_VOICE_BUF_BANK1;
|
||
|
}
|
||
|
#else //im502BE
|
||
|
//fuli 20170629 no use voice_buf_data.index = (cmd.attri >>8) & 0xFFFF; //package index
|
||
|
if ((cmd.attri & 0xFF) == 0xF0) {
|
||
|
address = HW_VOICE_BUF_START; //BANK0 address
|
||
|
} else if ((cmd.attri & 0xFF) == 0xF9) {
|
||
|
address = HW_VOICE_BUF_START + HW_VOICE_BUF_BANK_SIZE; //BANK1 address
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
if (im501_host_irqstatus == 1)
|
||
|
dev_dbg(&im501_spi->dev, "%s: data ready at address=%x, data_size=%x\n",
|
||
|
__func__, address, data_size);
|
||
|
err = fetch_voice_data(address, data_size);
|
||
|
if (err != ESUCCESS) {
|
||
|
dev_err(&im501_spi->dev, "%s: fetch_voice_data() return error.\n",
|
||
|
__func__);
|
||
|
return err;
|
||
|
}
|
||
|
//cmd.status = 0;
|
||
|
//Should be modified in accordance with customers scenario definitions.
|
||
|
pdata = (u8 *) &cmd;
|
||
|
pdata[3] &= 0x7F; //changes the bit-31 to 0 to indicate that the hot has finished with the task.
|
||
|
//dev_err(&im501_spi->dev, "%s: pdata[%#x, %#x, %#x, %#x]\n", __func__, pdata[0], pdata[1], pdata[2], pdata[3]);
|
||
|
err = im501_spi_write_dram(TO_HOST_CMD_ADDR, pdata);
|
||
|
if (err != ESUCCESS) {
|
||
|
dev_err(&im501_spi->dev, "%s: im501_spi_write_dram() return error.\n",
|
||
|
__func__);
|
||
|
return err;
|
||
|
}
|
||
|
} else { //treat all other interrupt as keyword detect
|
||
|
//if(cmd.cmd_byte == TO_HOST_CMD_KEYWORD_DET) {//Info host Keywords detected
|
||
|
//dev_err(&im501_spi->dev, "%s: im501_vbuf_trans_status = %d\n", __func__, im501_vbuf_trans_status);
|
||
|
if ((im501_vbuf_trans_status == 0) && (im501_dsp_mode_old == POWER_SAVING_MODE)) { //only available when not transfer voice and in PSM
|
||
|
im501_vbuf_trans_status = 1;
|
||
|
im501_host_irqstatus = 1;
|
||
|
|
||
|
#ifdef CODEC2IM501_PDM_CLKI
|
||
|
//to change the iM501 from PSM to Normal mode, and to keep the same setting
|
||
|
//as the mode before changing to PSM, the Host just needs to turn on the PDMCLKI,
|
||
|
//and no additional command is needed.
|
||
|
codec2im501_pdm_clki_set(NORMAL_MODE, 0);
|
||
|
im501_dsp_mode_old = NORMAL_MODE;
|
||
|
mdelay(5);
|
||
|
#endif
|
||
|
dev_info(&im501_spi->dev, "%s: keyword detected.\n", __func__);
|
||
|
//dev_err(&im501_spi->dev, "%s: ap_sleep_flag = %d, im501_dsp_mode_old = %d\n", __func__, ap_sleep_flag, im501_dsp_mode_old);
|
||
|
if (ap_sleep_flag) {
|
||
|
im501_spi->mode = SPI_MODE_0; /* clk active low */
|
||
|
im501_spi->bits_per_word = 8;
|
||
|
im501_spi->max_speed_hz = spi_speed; //SPI_HIGH_SPEED
|
||
|
|
||
|
ret = spi_setup(im501_spi);
|
||
|
if (ret < 0) {
|
||
|
dev_err(&im501_spi->dev,
|
||
|
"spi_setup() failed\n");
|
||
|
return -EIO;
|
||
|
}
|
||
|
msleep(20);
|
||
|
|
||
|
ap_sleep_flag = 0;
|
||
|
}
|
||
|
//cmd.status = 0;
|
||
|
//Should be modified in accordance with customers scenario definitions.
|
||
|
pdata = (u8 *) &cmd;
|
||
|
pdata[3] &= 0x7F; //changes the bit-31 to 0 to indicate that the hot has finished with the task.
|
||
|
//dev_err(&im501_spi->dev, "%s: pdata[%#x, %#x, %#x, %#x]\n", __func__, pdata[0], pdata[1], pdata[2], pdata[3]);
|
||
|
err = im501_spi_write_dram(TO_HOST_CMD_ADDR, pdata);
|
||
|
if (err != ESUCCESS) {
|
||
|
dev_err(&im501_spi->dev,
|
||
|
"%s: 1111 im501_spi_write_dram() return error.\n",
|
||
|
__func__);
|
||
|
return err;
|
||
|
}
|
||
|
//dev_err(&im501_spi->dev, "%s: start to get oneshot and save to file!!!!!!!!!!!!!!!!!!!!!!\n", __func__);
|
||
|
openFileForWrite(REC_FILE_PATH);
|
||
|
request_start_voice_buf_trans();
|
||
|
kobject_uevent_env(&im501_spi->dev.kobj, KOBJ_CHANGE, hotword_str);
|
||
|
|
||
|
} else {
|
||
|
dev_info(&im501_spi->dev,
|
||
|
"%s: Do not process other interrupt during process "
|
||
|
"keyword detection and oneshot, or DSP is in normal mode.\n",
|
||
|
__func__);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return err;
|
||
|
}
|
||
|
|
||
|
unsigned char im501_send_to_dsp_command(to_501_cmd cmd)
|
||
|
{
|
||
|
unsigned char err;
|
||
|
unsigned int i;
|
||
|
u8 pTempData[8];
|
||
|
u8 *pdata;
|
||
|
|
||
|
pdata = (u8 *) &cmd;
|
||
|
//dev_err(&im501_spi->dev, "%s: attr=%#x, ext=%#x, status=%#x, cmd=%#x\n", __func__, cmd.attri, cmd.cmd_byte_ext, cmd.status, cmd.cmd_byte);
|
||
|
|
||
|
err = im501_spi_write_dram(TO_DSP_CMD_ADDR, pdata);
|
||
|
if (err != ESUCCESS) {
|
||
|
dev_err(&im501_spi->dev, "%s: call im501_spi_write_dram() return error(%d)\n",
|
||
|
__func__, err);
|
||
|
return err;
|
||
|
}
|
||
|
|
||
|
err = im501_spi_write_reg(0x01, cmd.cmd_byte); //generate interrupt to DSP
|
||
|
if (err != ESUCCESS) {
|
||
|
dev_err(&im501_spi->dev, "%s: call im501_spi_write_reg() return error(%d)\n",
|
||
|
__func__, err);
|
||
|
return err;
|
||
|
}
|
||
|
|
||
|
memset(pTempData, 0, 8);
|
||
|
|
||
|
for (i = 0; i < 200; i++) { //wait for (50*100us = 5ms) to check if DSP finished
|
||
|
err = im501_spi_read_dram(TO_DSP_CMD_ADDR, pTempData);
|
||
|
if (err != ESUCCESS) {
|
||
|
dev_err(&im501_spi->dev,
|
||
|
"%s: call im501_spi_read_dram() return error(%d)\n",
|
||
|
__func__, err);
|
||
|
return err;
|
||
|
}
|
||
|
//dev_err(&im501_spi->dev, "%s: pTempData[%#x, %#x, %#x, %#x]\n", __func__, pTempData[0], pTempData[1], pTempData[2], pTempData[3]);
|
||
|
cmd.status = pTempData[3] >> 7;
|
||
|
cmd.cmd_byte_ext = pTempData[3] & 0x7F;
|
||
|
cmd.attri =
|
||
|
pTempData[0] | (pTempData[1] * 256) | (pTempData[2] * 256 *
|
||
|
256);
|
||
|
dev_info(&im501_spi->dev, "%s: cmd[%#x, %#x, %#x]\n", __func__, cmd.status,
|
||
|
cmd.cmd_byte_ext, cmd.attri);
|
||
|
|
||
|
if (cmd.status != 0) {
|
||
|
err = TO_501_CMD_ERR;
|
||
|
} else {
|
||
|
err = ESUCCESS;
|
||
|
break;
|
||
|
}
|
||
|
msleep(20);
|
||
|
}
|
||
|
|
||
|
return err;
|
||
|
}
|
||
|
|
||
|
unsigned char im501_send_message_to_dsp(unsigned char cmd_index, unsigned int para)
|
||
|
{
|
||
|
unsigned char err = ESUCCESS;
|
||
|
to_501_cmd cmd;
|
||
|
|
||
|
cmd.attri = para & 0xFFFFFF;
|
||
|
cmd.cmd_byte_ext = (para >> 24) & 0x7F;
|
||
|
cmd.status = 1;
|
||
|
|
||
|
cmd.cmd_byte = cmd_index; //((cmd_index & 0x3F) << 2) | 0x01; //D[1] : "1", interrupt DSP. This bit generates NMI (non-mask-able interrupt), D[0]: "1" generate mask-able interrupt
|
||
|
|
||
|
dev_dbg(&im501_spi->dev, "%s: attri=%#x, cmd_byte_ext=%#x, status=%#x, cmd_byte=%#x\n",
|
||
|
__func__, cmd.attri, cmd.cmd_byte_ext, cmd.status, cmd.cmd_byte);
|
||
|
err = im501_send_to_dsp_command(cmd);
|
||
|
|
||
|
if (err != ESUCCESS) {
|
||
|
dev_err(&im501_spi->dev, "%s: fail to send message(%02x) to dsp. err=%d\n",
|
||
|
__func__, cmd_index >> 2, err);
|
||
|
}
|
||
|
return err;
|
||
|
}
|
||
|
|
||
|
unsigned char request_start_voice_buf_trans(void)
|
||
|
{
|
||
|
unsigned char err = ESUCCESS;
|
||
|
err = im501_send_message_to_dsp(TO_DSP_CMD_REQ_START_BUF_TRANS, 0);
|
||
|
|
||
|
//Fuli 20161216 clear fifo when start to detect ketwords.
|
||
|
dev_info(&im501_spi->dev, "%s: clear pcm fifo", __func__);
|
||
|
spin_lock(&im501_data->pcm_fifo_lock);
|
||
|
kfifo_reset(im501_data->pcm_kfifo);
|
||
|
spin_unlock(&im501_data->pcm_fifo_lock);
|
||
|
|
||
|
return err;
|
||
|
}
|
||
|
|
||
|
unsigned char request_stop_voice_buf_trans(void)
|
||
|
{
|
||
|
unsigned char err = ESUCCESS;
|
||
|
|
||
|
dev_info(&im501_spi->dev, "%s: stop voice transfer\n", __func__);
|
||
|
err = im501_send_message_to_dsp(TO_DSP_CMD_REQ_STOP_BUF_TRANS, 0);
|
||
|
im501_vbuf_trans_status = 0;
|
||
|
|
||
|
return err;
|
||
|
}
|
||
|
|
||
|
unsigned char request_enter_psm(void)
|
||
|
{
|
||
|
unsigned char err = ESUCCESS;
|
||
|
|
||
|
if (im501_vbuf_trans_status == 1) {
|
||
|
// stop voice transfer.
|
||
|
request_stop_voice_buf_trans();
|
||
|
}
|
||
|
|
||
|
im501_vbuf_trans_status = 0;
|
||
|
|
||
|
if (im501_dsp_mode_old != POWER_SAVING_MODE) { //The current dsp mode is not PSM.
|
||
|
dev_info(&im501_spi->dev,
|
||
|
"ap_sleep_flag = %d, im501_dsp_mode_old = %d. iM501 is going to sleep.\n",
|
||
|
ap_sleep_flag, im501_dsp_mode_old);
|
||
|
dev_dbg(&im501_spi->dev,
|
||
|
"%s: the command is %#x\n", __func__, TO_DSP_CMD_REQ_ENTER_PSM);
|
||
|
|
||
|
err = im501_send_message_to_dsp(TO_DSP_CMD_REQ_ENTER_PSM, 0);
|
||
|
if (ESUCCESS != err) {
|
||
|
dev_err(&im501_spi->dev,
|
||
|
"%s: error occurs when switch to power saving mode, error = %d\n",
|
||
|
__func__, err);
|
||
|
}
|
||
|
#ifdef CODEC2IM501_PDM_CLKI
|
||
|
mdelay(100);
|
||
|
codec2im501_pdm_clki_set(POWER_SAVING_MODE, 0);
|
||
|
mdelay(5);
|
||
|
#endif
|
||
|
ap_sleep_flag = 1;
|
||
|
im501_dsp_mode_old = POWER_SAVING_MODE;
|
||
|
dev_dbg(&im501_spi->dev, "%s: ap_sleep_flag = %d, im501_dsp_mode_old = %d\n",
|
||
|
__func__, ap_sleep_flag, im501_dsp_mode_old);
|
||
|
}
|
||
|
|
||
|
return err;
|
||
|
}
|
||
|
|
||
|
unsigned char request_enter_normal(void)
|
||
|
{
|
||
|
unsigned char err = ESUCCESS;
|
||
|
|
||
|
dev_info(&im501_spi->dev, "%s: entering...\n", __func__);
|
||
|
if (im501_vbuf_trans_status == 1) {
|
||
|
// stop voice transfer.
|
||
|
request_stop_voice_buf_trans();
|
||
|
}
|
||
|
|
||
|
im501_vbuf_trans_status = 0;
|
||
|
|
||
|
if (im501_dsp_mode_old != NORMAL_MODE) { //The current dsp mode is not NORMAL
|
||
|
#ifdef CODEC2IM501_PDM_CLKI
|
||
|
mdelay(5);
|
||
|
codec2im501_pdm_clki_set(NORMAL_MODE, 0);
|
||
|
mdelay(100);
|
||
|
#endif
|
||
|
|
||
|
//fuli 20170629
|
||
|
err = im501_send_message_to_dsp(TO_DSP_CMD_REQ_ENTER_NORMAL, 0);
|
||
|
if (ESUCCESS != err) {
|
||
|
dev_err(&im501_spi->dev,
|
||
|
"%s: error occurs when switch to normal mode, error = %d\n",
|
||
|
__func__, err);
|
||
|
}
|
||
|
|
||
|
ap_sleep_flag = 0;
|
||
|
im501_dsp_mode_old = NORMAL_MODE;
|
||
|
dev_info(&im501_spi->dev,
|
||
|
"%s: ap_sleep_flag = %d, im501_dsp_mode_old = %d\n",
|
||
|
__func__, ap_sleep_flag, im501_dsp_mode_old);
|
||
|
}
|
||
|
|
||
|
return err;
|
||
|
}
|
||
|
|
||
|
int im501_spi_burst_read_check(u32 start_addr, u8 *buf, u32 data_length)
|
||
|
{
|
||
|
int err;
|
||
|
unsigned char *pbuf;
|
||
|
unsigned int i, a, b;
|
||
|
|
||
|
dev_info(&im501_spi->dev, "%s: entering...\n", __func__);
|
||
|
a = data_length / IM501_SPI_BUF_LEN;
|
||
|
b = data_length % IM501_SPI_BUF_LEN;
|
||
|
|
||
|
pbuf = (unsigned char *)kzalloc(IM501_SPI_BUF_LEN, GFP_KERNEL);
|
||
|
if (!pbuf) {
|
||
|
dev_err(&im501_spi->dev, "%s: pbuf allocation failure.\n", __func__);
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
for (i = 0; i < a; i++) {
|
||
|
dev_dbg(&im501_spi->dev, "%s: start_addr = %#x\n", __func__, start_addr);
|
||
|
err =
|
||
|
im501_spi_burst_read_dram(start_addr, pbuf,
|
||
|
IM501_SPI_BUF_LEN);
|
||
|
if (err != ESUCCESS) {
|
||
|
return err;
|
||
|
}
|
||
|
memcpy(buf + i * IM501_SPI_BUF_LEN, pbuf, IM501_SPI_BUF_LEN);
|
||
|
|
||
|
start_addr += IM501_SPI_BUF_LEN;
|
||
|
}
|
||
|
|
||
|
if (b > 0) {
|
||
|
err = im501_spi_burst_read_dram(start_addr, pbuf, b);
|
||
|
if (err != ESUCCESS) {
|
||
|
return err;
|
||
|
}
|
||
|
memcpy(buf + i * IM501_SPI_BUF_LEN, pbuf, b);
|
||
|
}
|
||
|
|
||
|
if (pbuf)
|
||
|
kfree(pbuf);
|
||
|
return err;
|
||
|
}
|
||
|
|
||
|
EXPORT_SYMBOL_GPL(im501_spi_burst_read_check);
|
||
|
|
||
|
#ifdef FW_RD_CHECK
|
||
|
static void im501_8byte_swap(u8 *rxbuf, u32 len)
|
||
|
{
|
||
|
u8 local_buf[8];
|
||
|
int i;
|
||
|
|
||
|
for (i = 0; i < len / 8 * 8; i += 8) {
|
||
|
local_buf[0] = rxbuf[i + 0];
|
||
|
local_buf[1] = rxbuf[i + 1];
|
||
|
local_buf[2] = rxbuf[i + 2];
|
||
|
local_buf[3] = rxbuf[i + 3];
|
||
|
local_buf[4] = rxbuf[i + 4];
|
||
|
local_buf[5] = rxbuf[i + 5];
|
||
|
local_buf[6] = rxbuf[i + 6];
|
||
|
local_buf[7] = rxbuf[i + 7];
|
||
|
|
||
|
rxbuf[i + 0] = local_buf[7];
|
||
|
rxbuf[i + 1] = local_buf[6];
|
||
|
rxbuf[i + 2] = local_buf[5];
|
||
|
rxbuf[i + 3] = local_buf[4];
|
||
|
rxbuf[i + 4] = local_buf[3];
|
||
|
rxbuf[i + 5] = local_buf[2];
|
||
|
rxbuf[i + 6] = local_buf[1];
|
||
|
rxbuf[i + 7] = local_buf[0];
|
||
|
}
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
static size_t im501_read_file(char *file_path, const u8 **buf)
|
||
|
{
|
||
|
loff_t pos = 0;
|
||
|
unsigned int file_size = 0;
|
||
|
int read_len;
|
||
|
struct file *fp;
|
||
|
|
||
|
dev_info(&im501_spi->dev, "file_path = %s\n", file_path);
|
||
|
fp = filp_open(file_path, O_RDONLY, 0);
|
||
|
if (!IS_ERR(fp)) {
|
||
|
dev_info(&im501_spi->dev, "The file is present.\n");
|
||
|
|
||
|
file_size = vfs_llseek(fp, pos, SEEK_END);
|
||
|
|
||
|
*buf = kzalloc(file_size, GFP_KERNEL);
|
||
|
if (*buf == NULL) {
|
||
|
filp_close(fp, 0);
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
read_len = kernel_read(fp, pos, (char *)*buf, file_size);
|
||
|
if (read_len <= 0) {
|
||
|
dev_err(&im501_spi->dev, "kernel_read error\n");
|
||
|
read_len = 0;
|
||
|
}
|
||
|
|
||
|
filp_close(fp, 0);
|
||
|
|
||
|
return read_len;
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static int im501_dsp_load_single_fw_file(u8 *fw_name, u32 addr, int fw_type)
|
||
|
{
|
||
|
u8 *data;
|
||
|
size_t size = 0;
|
||
|
#ifdef FW_RD_CHECK
|
||
|
u8 *local_buf;
|
||
|
int i;
|
||
|
#endif
|
||
|
|
||
|
#ifdef SHOW_DL_TIME
|
||
|
do_gettimeofday(&(txc.time));
|
||
|
rtc_time_to_tm(txc.time.tv_sec, &rt);
|
||
|
dev_info(&im501_spi->dev, "%s: start time: %d-%d-%d %d:%d:%d \n", __func__,
|
||
|
rt.tm_year + 1900, rt.tm_mon, rt.tm_mday, rt.tm_hour, rt.tm_min,
|
||
|
rt.tm_sec);
|
||
|
#endif
|
||
|
|
||
|
size = im501_read_file(fw_name, (const u8 **)&data);
|
||
|
if (!size) {
|
||
|
dev_err(&im501_spi->dev, "%s: firmware %s open failed.\n",
|
||
|
__func__, fw_name);
|
||
|
return -1;
|
||
|
}
|
||
|
if (size) {
|
||
|
dev_info(&im501_spi->dev, "%s: firmware %s size = %zu, addr = %#x\n", __func__,
|
||
|
fw_name, size, addr);
|
||
|
im501_spi_burst_write(addr, data, size, fw_type);
|
||
|
|
||
|
#ifdef FW_RD_CHECK
|
||
|
local_buf = (u8 *) kzalloc(size, GFP_KERNEL);
|
||
|
if (!local_buf)
|
||
|
return -ENOMEM;
|
||
|
#ifdef FW_BURST_RD_CHECK
|
||
|
im501_spi_burst_read_dram(addr, local_buf, size);
|
||
|
if (fw_type == IM501_DSP_FW)
|
||
|
im501_8byte_swap(local_buf, size);
|
||
|
|
||
|
for (i = 0; i < size; i++) {
|
||
|
if (local_buf[i] != data[i]) {
|
||
|
dev_err(&im501_spi->dev, "%s: fw read %#x vs write %#x @ %#x\n",
|
||
|
__func__, local_buf[i], data[i],
|
||
|
addr + i);
|
||
|
}
|
||
|
}
|
||
|
#endif
|
||
|
kfree(local_buf);
|
||
|
#endif
|
||
|
kfree(data);
|
||
|
}
|
||
|
#ifdef SHOW_DL_TIME
|
||
|
do_gettimeofday(&(txc.time));
|
||
|
rtc_time_to_tm(txc.time.tv_sec, &rt);
|
||
|
dev_info(&im501_spi->dev, "%s: end time: %d-%d-%d %d:%d:%d \n", __func__,
|
||
|
rt.tm_year + 1900, rt.tm_mon, rt.tm_mday, rt.tm_hour, rt.tm_min,
|
||
|
rt.tm_sec);
|
||
|
#endif
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
EXPORT_SYMBOL_GPL(im501_dsp_load_single_fw_file);
|
||
|
|
||
|
static int im501_dsp_load_fw(u8 firmware_type)
|
||
|
{ //0: wakeup firmware; 1: ultrasound firmware
|
||
|
int err = 0;
|
||
|
const struct firmware *fw = NULL;
|
||
|
int fw_type = IM501_DSP_FW;
|
||
|
|
||
|
#ifdef FW_RD_CHECK
|
||
|
u8 *local_buf;
|
||
|
int i;
|
||
|
#endif
|
||
|
|
||
|
dev_info(&im501_spi->dev, "%s: firmware_type=%d\n", __func__, firmware_type);
|
||
|
#ifdef SHOW_DL_TIME
|
||
|
do_gettimeofday(&(txc.time));
|
||
|
rtc_time_to_tm(txc.time.tv_sec, &rt);
|
||
|
dev_info(&im501_spi->dev, "%s: start time: %d-%d-%d %d:%d:%d \n", __func__,
|
||
|
rt.tm_year + 1900, rt.tm_mon, rt.tm_mday, rt.tm_hour, rt.tm_min,
|
||
|
rt.tm_sec);
|
||
|
#endif
|
||
|
|
||
|
if (firmware_type == WAKEUP_FIRMWARE)
|
||
|
err = request_firmware(&fw, "im501_iram0.bin", &im501_spi->dev);
|
||
|
else
|
||
|
err = request_firmware(&fw, "../firmware1/im501_iram0.bin",
|
||
|
&im501_spi->dev);
|
||
|
|
||
|
if (!fw) {
|
||
|
dev_err(&im501_spi->dev,
|
||
|
"%s: im501_iram0.bin firmware request failed.\n", __func__);
|
||
|
return err;
|
||
|
}
|
||
|
|
||
|
if (fw) {
|
||
|
dev_info(&im501_spi->dev, "%s: firmware im501_iram0.bin size = %zu \n", __func__,
|
||
|
fw->size);
|
||
|
im501_spi_burst_write(0x10000000, fw->data, fw->size, fw_type);
|
||
|
|
||
|
#ifdef FW_RD_CHECK
|
||
|
local_buf = (u8 *) kzalloc(fw->size, GFP_KERNEL);
|
||
|
if (!local_buf)
|
||
|
return -ENOMEM;
|
||
|
#ifdef FW_BURST_RD_CHECK
|
||
|
im501_spi_burst_read_dram(0x10000000, local_buf, fw->size);
|
||
|
if (fw_type == IM501_DSP_FW)
|
||
|
im501_8byte_swap(local_buf, fw->size);
|
||
|
|
||
|
for (i = 0; i < fw->size; i++) {
|
||
|
if (local_buf[i] != fw->data[i]) {
|
||
|
dev_err(&im501_spi->dev, "%s: fw read %#x vs write %#x @ %#x\n",
|
||
|
__func__, local_buf[i], fw->data[i],
|
||
|
0x10000000 + i);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
#endif
|
||
|
kfree(local_buf);
|
||
|
#endif
|
||
|
release_firmware(fw);
|
||
|
fw = NULL;
|
||
|
}
|
||
|
|
||
|
if (firmware_type == WAKEUP_FIRMWARE)
|
||
|
err = request_firmware(&fw, "im501_dram0.bin", &im501_spi->dev);
|
||
|
else
|
||
|
err = request_firmware(&fw, "../firmware1/im501_dram0.bin",
|
||
|
&im501_spi->dev);
|
||
|
|
||
|
if (!fw) {
|
||
|
dev_err(&im501_spi->dev,
|
||
|
"%s: im501_dram0.bin firmware request failed.\n", __func__);
|
||
|
return err;
|
||
|
}
|
||
|
|
||
|
if (fw) {
|
||
|
dev_info(&im501_spi->dev, "%s: firmware im501_dram0.bin size = %zu\n", __func__,
|
||
|
fw->size);
|
||
|
im501_spi_burst_write(0x0ffc0000, fw->data, fw->size, fw_type);
|
||
|
|
||
|
#ifdef FW_RD_CHECK
|
||
|
local_buf = (u8 *) kzalloc(fw->size, GFP_KERNEL);
|
||
|
if (!local_buf)
|
||
|
return -ENOMEM;
|
||
|
|
||
|
#ifdef FW_BURST_RD_CHECK
|
||
|
im501_spi_burst_read_dram(0x0ffc0000, local_buf, fw->size);
|
||
|
if (fw_type == IM501_DSP_FW)
|
||
|
im501_8byte_swap(local_buf, fw->size);
|
||
|
|
||
|
for (i = 0; i < fw->size; i++) {
|
||
|
if (local_buf[i] != fw->data[i]) {
|
||
|
dev_err(&im501_spi->dev, "%s: fw read %#x vs write %#x @ %#x\n",
|
||
|
__func__, local_buf[i], fw->data[i],
|
||
|
0x0ffc0000 + i);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
#endif
|
||
|
kfree(local_buf);
|
||
|
#endif
|
||
|
|
||
|
release_firmware(fw);
|
||
|
fw = NULL;
|
||
|
}
|
||
|
|
||
|
if (firmware_type == WAKEUP_FIRMWARE)
|
||
|
err = request_firmware(&fw, "im501_dram1.bin", &im501_spi->dev);
|
||
|
else
|
||
|
err = request_firmware(&fw, "../firmware1/im501_dram1.bin",
|
||
|
&im501_spi->dev);
|
||
|
|
||
|
if (!fw) {
|
||
|
dev_err(&im501_spi->dev,
|
||
|
"%s: im501_dram1.bin firmware request failed.\n", __func__);
|
||
|
return err;
|
||
|
}
|
||
|
|
||
|
if (fw) {
|
||
|
dev_info(&im501_spi->dev, "%s: firmware im501_dram1.bin size = %zu\n", __func__, fw->size);
|
||
|
im501_spi_burst_write(0x0ffe0000, fw->data, fw->size, fw_type);
|
||
|
|
||
|
#ifdef FW_RD_CHECK
|
||
|
local_buf = (u8 *) kzalloc(fw->size, GFP_KERNEL);
|
||
|
if (!local_buf)
|
||
|
return -ENOMEM;
|
||
|
|
||
|
#ifdef FW_BURST_RD_CHECK
|
||
|
im501_spi_burst_read_dram(0x0ffe0000, local_buf, fw->size);
|
||
|
if (fw_type == IM501_DSP_FW)
|
||
|
im501_8byte_swap(local_buf, fw->size);
|
||
|
|
||
|
for (i = 0; i < fw->size; i++) {
|
||
|
if (local_buf[i] != fw->data[i]) {
|
||
|
dev_err(&im501_spi->dev, "%s: fw read %#x vs write %#x @ %#x\n",
|
||
|
__func__, local_buf[i], fw->data[i],
|
||
|
0x0ffe0000 + i);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
#endif
|
||
|
kfree(local_buf);
|
||
|
#endif
|
||
|
release_firmware(fw);
|
||
|
fw = NULL;
|
||
|
}
|
||
|
|
||
|
#ifdef SHOW_DL_TIME
|
||
|
do_gettimeofday(&(txc.time));
|
||
|
rtc_time_to_tm(txc.time.tv_sec, &rt);
|
||
|
dev_info(&im501_spi->dev, "%s: end time: %d-%d-%d %d:%d:%d \n", __func__,
|
||
|
rt.tm_year + 1900, rt.tm_mon, rt.tm_mday, rt.tm_hour, rt.tm_min,
|
||
|
rt.tm_sec);
|
||
|
#endif
|
||
|
dev_info(&im501_spi->dev, "%s: firmware is loaded\n", __func__);
|
||
|
return err;
|
||
|
}
|
||
|
|
||
|
void im501_write_check_spi_reg(u8 reg, u8 val)
|
||
|
{
|
||
|
u8 ret_val;
|
||
|
|
||
|
im501_spi_write_reg(reg, val);
|
||
|
im501_spi_read_reg(0x00, (u8 *) &ret_val);
|
||
|
if (ret_val != val)
|
||
|
dev_err(&im501_spi->dev,
|
||
|
"%s: read back value(%x) of reg%02x is different from the written value(%x). \n",
|
||
|
__func__, ret_val, reg, val);
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
void enable_pdm_clock(void)
|
||
|
{
|
||
|
if (atomic_read(&dmic_clk_cnt) > 0) {
|
||
|
dev_info(&im501_spi->dev, "PDM clock already enabled\n");
|
||
|
return;
|
||
|
}
|
||
|
atomic_set(&dmic_clk_cnt, 1);
|
||
|
}
|
||
|
|
||
|
void disable_pdm_clock(void)
|
||
|
{
|
||
|
if (atomic_read(&dmic_clk_cnt) > 0)
|
||
|
atomic_set(&dmic_clk_cnt, 0);
|
||
|
else
|
||
|
dev_info(&im501_spi->dev, "PDM clock already disabled\n");
|
||
|
}
|
||
|
|
||
|
void im501_start_capture_cb(void)
|
||
|
{
|
||
|
int err;
|
||
|
|
||
|
err = im501_send_message_to_dsp(TO_DSP_CMD_REQ_ENTER_BYPASS, 0x1);
|
||
|
if (ESUCCESS != err) {
|
||
|
dev_err(&im501_spi->dev,
|
||
|
"%s: error when enabling hw bypass mode, error = %d\n",
|
||
|
__func__, err);
|
||
|
}
|
||
|
msleep(20);
|
||
|
}
|
||
|
|
||
|
static void im501_fw_load(struct work_struct *work)
|
||
|
{
|
||
|
u32 addr, regval;
|
||
|
|
||
|
dev_info(&im501_spi->dev, "%s: entering...\n", __func__);
|
||
|
|
||
|
#ifdef CODEC2IM501_PDM_CLKI
|
||
|
codec2im501_pdm_clki_set(NORMAL_MODE, 0);
|
||
|
mdelay(5); //Apply PDM_CLKI, and then wait for 1024 clock cycles
|
||
|
#endif
|
||
|
|
||
|
//reset DSP
|
||
|
im501_reset();
|
||
|
|
||
|
//Enable DSP Clock, put DSP on hold and remove DSP reset
|
||
|
im501_write_check_spi_reg(0x00, 0x07);
|
||
|
im501_write_check_spi_reg(0x00, 0x05);
|
||
|
|
||
|
mdelay(1);
|
||
|
|
||
|
//Enable PDM_DATAO1
|
||
|
addr = 0xFFFFF10;
|
||
|
im501_spi_read_dram(addr, (u8 *)®val);
|
||
|
regval = regval & 0xFBFFFFFF;
|
||
|
im501_spi_write_dram(addr, (u8 *)®val);
|
||
|
|
||
|
if (im501_dsp_load_fw(firmware_type) != 0) {
|
||
|
dev_err(&im501_spi->dev, "im501 firmware load failed\n");
|
||
|
codec2im501_pdm_clki_set(POWER_SAVING_MODE, 0);
|
||
|
return;
|
||
|
}
|
||
|
mdelay(1);
|
||
|
|
||
|
//Remove DSP “on hold” to “run"
|
||
|
im501_write_check_spi_reg(0x00, 0x04);
|
||
|
|
||
|
//check DSP working or not
|
||
|
check_dsp_status();
|
||
|
|
||
|
im501_dsp_mode_old = NORMAL_MODE;
|
||
|
|
||
|
//after firmware download, keep DSP in NORMAL mode
|
||
|
codec2im501_pdm_clki_set(POWER_SAVING_MODE, 0);
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
static void im501_spi_lock(struct mutex *lock)
|
||
|
{
|
||
|
mutex_lock(lock);
|
||
|
}
|
||
|
|
||
|
static void im501_spi_unlock(struct mutex *lock)
|
||
|
{
|
||
|
mutex_unlock(lock);
|
||
|
}
|
||
|
|
||
|
//Fuli 20171012 to support SPI recording
|
||
|
unsigned char im501_switch_to_spi_recording_mode(void)
|
||
|
{
|
||
|
return im501_send_message_to_dsp(TO_DSP_CMD_REQ_SWITCH_SPI_REC, 1);
|
||
|
}
|
||
|
|
||
|
unsigned char im501_switch_to_normal_mode(void)
|
||
|
{
|
||
|
return im501_send_message_to_dsp(TO_DSP_CMD_REQ_SWITCH_SPI_REC, 0);
|
||
|
}
|
||
|
|
||
|
static void im501_irq_handling_work(struct work_struct *work)
|
||
|
{
|
||
|
unsigned char err;
|
||
|
to_host_cmd cmd;
|
||
|
u8 *pdata;
|
||
|
u32 spi_speed = SPI_LOW_SPEED;
|
||
|
int ret;
|
||
|
|
||
|
//dev_err(&im501_spi->dev, "%s: entering...\n", __func__);
|
||
|
if (im501_dsp_mode_old == POWER_SAVING_MODE) {
|
||
|
dev_info(&im501_spi->dev, "%s: iM501 is in PSM, set the spi speed to %d\n",
|
||
|
__func__, spi_speed);
|
||
|
im501_spi->mode = SPI_MODE_0; /* clk active low */
|
||
|
im501_spi->bits_per_word = 8;
|
||
|
im501_spi->max_speed_hz = spi_speed;
|
||
|
|
||
|
ret = spi_setup(im501_spi);
|
||
|
if (ret < 0) {
|
||
|
dev_err(&im501_spi->dev, "spi_setup() failed\n");
|
||
|
return; // -EIO;
|
||
|
}
|
||
|
msleep(20);
|
||
|
}
|
||
|
|
||
|
pdata = (u8 *) &cmd;
|
||
|
err = im501_spi_read_dram(TO_HOST_CMD_ADDR, pdata);
|
||
|
if (err != ESUCCESS) {
|
||
|
dev_err(&im501_spi->dev, "%s: im501_spi_read_dram error, error=%d\n",
|
||
|
__func__, err);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if ((im501_host_irqstatus == 1) || ((output_counter % 100) == 0)) {
|
||
|
dev_dbg(&im501_spi->dev, "%s: pdata[%#x, %#x, %#x, %#x]\n", __func__, pdata[0],
|
||
|
pdata[1], pdata[2], pdata[3]);
|
||
|
}
|
||
|
output_counter++;
|
||
|
|
||
|
cmd.status = pdata[3] >> 7;
|
||
|
cmd.cmd_byte = pdata[3] & 0x7F;
|
||
|
cmd.attri = pdata[0] | (pdata[1] * 256) | (pdata[2] * 256 * 256);
|
||
|
//dev_err(&im501_spi->dev, "%s: cmd[%#x, %#x, %#x]\n", __func__, cmd.status, cmd.cmd_byte, cmd.attri);
|
||
|
|
||
|
if (cmd.status != 1) {
|
||
|
dev_err(&im501_spi->dev, "%s: got wrong command from DSP.\n", __func__);
|
||
|
}
|
||
|
|
||
|
im501_spi_lock(&im501_data->spi_op_lock);
|
||
|
err = parse_to_host_command(cmd);
|
||
|
im501_spi_unlock(&im501_data->spi_op_lock);
|
||
|
|
||
|
if (err != ESUCCESS) {
|
||
|
dev_err(&im501_spi->dev,
|
||
|
"%s: error calling parse_to_host_command(), error=%d\n",
|
||
|
__func__, err);
|
||
|
}
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
static irqreturn_t im501_irq_handler(int irq, void *para)
|
||
|
{
|
||
|
bool ret;
|
||
|
|
||
|
ret = queue_work(im501_irq_wq, &im501_irq_work);
|
||
|
if (!ret)
|
||
|
dev_err(&im501_spi->dev, "%s: queue_work failed", __func__);
|
||
|
|
||
|
return IRQ_HANDLED;
|
||
|
}
|
||
|
|
||
|
/* Access to the audio buffer is controlled through "audio_owner". Either the
|
||
|
* character device can be opened. */
|
||
|
static int im501_record_open(struct inode *inode, struct file *file)
|
||
|
{
|
||
|
dev_dbg(&im501_spi->dev, "%s: entering...\n", __func__);
|
||
|
if (!atomic_add_unless(&im501_data->audio_owner, 1, 1))
|
||
|
return -EBUSY;
|
||
|
dev_dbg(&im501_spi->dev, "%s: im501_data->audio_owner.counter = %d\n", __func__,
|
||
|
im501_data->audio_owner.counter);
|
||
|
|
||
|
file->private_data = im501_data;
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static int im501_record_release(struct inode *inode, struct file *file)
|
||
|
{
|
||
|
dev_dbg(&im501_spi->dev, "%s: decrease audio_owner.\n", __func__);
|
||
|
atomic_dec(&im501_data->audio_owner);
|
||
|
dev_dbg(&im501_spi->dev, "%s: im501_data->audio_owner.counter = %d\n", __func__,
|
||
|
im501_data->audio_owner.counter);
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
/* The write function is a hack to load the A-model on systems where the
|
||
|
* firmware files are not accesible to the user. */
|
||
|
static ssize_t im501_record_write(struct file *file,
|
||
|
const char __user *buf,
|
||
|
size_t count_want, loff_t *f_pos)
|
||
|
{
|
||
|
dev_dbg(&im501_spi->dev, "%s: entering...\n", __func__);
|
||
|
return count_want;
|
||
|
}
|
||
|
|
||
|
/* Read out of the kfifo (as long as data is available). */
|
||
|
static ssize_t im501_record_read(struct file *file,
|
||
|
char __user *buf,
|
||
|
size_t count_want, loff_t *f_pos)
|
||
|
{
|
||
|
struct im501_data_t *im501_data = (struct im501_data_t *)file->private_data;
|
||
|
size_t not_copied;
|
||
|
ssize_t to_copy = count_want;
|
||
|
int avail;
|
||
|
unsigned int copied, total_copied = 0;
|
||
|
unsigned long timeout = jiffies + msecs_to_jiffies(500);
|
||
|
|
||
|
dev_dbg(&im501_spi->dev, "%s: entering... count_want = %d, *f_pos = %d\n",
|
||
|
__func__, (int)count_want, (int)*f_pos);
|
||
|
avail = kfifo_len(im501_data->pcm_kfifo);
|
||
|
//dev_info(&im501_spi->dev, "%s: avail = %d\n", __func__, avail);
|
||
|
|
||
|
while ((total_copied < count_want) && time_before(jiffies, timeout)) {
|
||
|
int ret;
|
||
|
to_copy = avail;
|
||
|
if (count_want - total_copied < avail)
|
||
|
to_copy = count_want - total_copied;
|
||
|
|
||
|
ret = kfifo_to_user(im501_data->pcm_kfifo, buf + total_copied,
|
||
|
to_copy, &copied);
|
||
|
if (ret)
|
||
|
return -EIO;
|
||
|
dev_dbg(&im501_spi->dev, "%s: %d bytes are copied:\n", __func__, copied);
|
||
|
|
||
|
total_copied += copied;
|
||
|
avail = kfifo_len(im501_data->pcm_kfifo);
|
||
|
if ((total_copied < count_want) && !avail)
|
||
|
msleep(20);
|
||
|
}
|
||
|
|
||
|
if (total_copied < count_want)
|
||
|
dev_err(&im501_spi->dev, "im501: timeout during reading\n");
|
||
|
|
||
|
not_copied = count_want - total_copied;
|
||
|
*f_pos = *f_pos + (count_want - not_copied);
|
||
|
|
||
|
return count_want - not_copied;
|
||
|
}
|
||
|
|
||
|
static const struct file_operations record_fops = {
|
||
|
.owner = THIS_MODULE,
|
||
|
.open = im501_record_open,
|
||
|
.release = im501_record_release,
|
||
|
.read = im501_record_read,
|
||
|
.write = im501_record_write,
|
||
|
};
|
||
|
|
||
|
static int im501_create_cdev(struct spi_device *spi)
|
||
|
{
|
||
|
int ret = 0, err = -1;
|
||
|
struct device *dev = &spi->dev;
|
||
|
int cdev_major, dev_no;
|
||
|
|
||
|
atomic_set(&im501_data->audio_owner, 0);
|
||
|
|
||
|
ret = alloc_chrdev_region(&im501_data->record_chrdev, 0, 1,
|
||
|
IM501_CDEV_NAME);
|
||
|
if (ret) {
|
||
|
dev_err(dev, "%s: failed to allocate character device\n",
|
||
|
__func__);
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
cdev_major = MAJOR(im501_data->record_chrdev);
|
||
|
|
||
|
im501_data->cdev_class = class_create(THIS_MODULE, IM501_CDEV_NAME);
|
||
|
if (IS_ERR(im501_data->cdev_class)) {
|
||
|
dev_err(dev, "%s: failed to create class\n", __func__);
|
||
|
return err;
|
||
|
}
|
||
|
|
||
|
dev_no = MKDEV(cdev_major, 1);
|
||
|
|
||
|
cdev_init(&im501_data->record_cdev, &record_fops);
|
||
|
|
||
|
im501_data->record_cdev.owner = THIS_MODULE;
|
||
|
|
||
|
ret = cdev_add(&im501_data->record_cdev, dev_no, 1);
|
||
|
if (ret) {
|
||
|
dev_err(dev, "%s: failed to add character device\n", __func__);
|
||
|
return ret;
|
||
|
}
|
||
|
dev_info(&im501_spi->dev, "%s: cdev_add ok...\n", __func__);
|
||
|
|
||
|
im501_data->record_dev = device_create(im501_data->cdev_class, NULL,
|
||
|
dev_no, NULL, "%s",
|
||
|
IM501_CDEV_NAME);
|
||
|
if (IS_ERR(im501_data->record_dev)) {
|
||
|
dev_err(&im501_spi->dev, "%s: could not create device\n",
|
||
|
__func__);
|
||
|
return err;
|
||
|
}
|
||
|
dev_info(&im501_spi->dev, "%s: device_create ok...\n", __func__);
|
||
|
|
||
|
spin_lock_init(&im501_data->pcm_fifo_lock);
|
||
|
im501_data->pcm_kfifo = (struct kfifo *)kmalloc(sizeof(struct kfifo), GFP_KERNEL);
|
||
|
if (im501_data->pcm_kfifo == NULL) {
|
||
|
dev_err(&im501_spi->dev, "no fifo for pcm_kfifo\n");
|
||
|
return -ENOMEM;
|
||
|
}
|
||
|
|
||
|
ret = kfifo_alloc(im501_data->pcm_kfifo, MAX_KFIFO_BUFFER_SIZE, GFP_KERNEL);
|
||
|
if (ret) {
|
||
|
dev_err(&im501_spi->dev, "no kfifo memory\n");
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
static ssize_t im501_spi_device_read(struct file *file, char __user *buffer,
|
||
|
size_t length, loff_t *offset)
|
||
|
{
|
||
|
char str[5];
|
||
|
size_t ret = 0;
|
||
|
char *local_buffer;
|
||
|
dev_cmd_mode_gs *get_mode_ret_data;
|
||
|
dev_cmd_reg_rw *get_reg_ret_data;
|
||
|
dev_cmd_long *get_addr_ret_data;
|
||
|
dev_cmd_short *get_irq_status;
|
||
|
dev_cmd_fw_type *get_firmware_type_ret_data;
|
||
|
int temp = 0;
|
||
|
|
||
|
local_buffer = (char *)kzalloc(length * sizeof(char), GFP_KERNEL);
|
||
|
if (!local_buffer) {
|
||
|
dev_err(&im501_spi->dev, "%s: local_buffer allocation failure.\n", __func__);
|
||
|
goto out;
|
||
|
}
|
||
|
temp = copy_from_user(local_buffer, buffer, length);
|
||
|
|
||
|
//dev_err(&im501_spi->dev, "local_buffer = %d:, length = %d\n", local_buffer[0], length);
|
||
|
switch (local_buffer[0]) {
|
||
|
case FM_SMVD_REG_READ:
|
||
|
get_reg_ret_data = (dev_cmd_reg_rw *) local_buffer;
|
||
|
im501_spi_read_reg(get_reg_ret_data->reg_addr,
|
||
|
(u8 *) &get_reg_ret_data->reg_val);
|
||
|
ret = sizeof(dev_cmd_reg_rw);
|
||
|
break;
|
||
|
|
||
|
case FM_SMVD_DSP_ADDR_READ:
|
||
|
get_addr_ret_data = (dev_cmd_long *) local_buffer;
|
||
|
im501_spi_read_dram(get_addr_ret_data->addr,
|
||
|
(u8 *) &get_addr_ret_data->val);
|
||
|
ret = sizeof(dev_cmd_long);
|
||
|
break;
|
||
|
|
||
|
case FM_SMVD_MODE_GET:
|
||
|
get_mode_ret_data = (dev_cmd_mode_gs *) local_buffer;
|
||
|
get_mode_ret_data->dsp_mode = (char)im501_dsp_mode_old;
|
||
|
ret = sizeof(dev_cmd_mode_gs);
|
||
|
break;
|
||
|
|
||
|
case FM_SMVD_HOST_IRQQUERY:
|
||
|
get_irq_status = (dev_cmd_short *) local_buffer;
|
||
|
get_irq_status->val = im501_host_irqstatus;
|
||
|
if (im501_host_irqstatus == 1) {
|
||
|
im501_host_irqstatus = 0;
|
||
|
dev_info(&im501_spi->dev, "%s: iM501 irq is coming..\n", __func__);
|
||
|
}
|
||
|
ret = sizeof(dev_cmd_short);
|
||
|
break;
|
||
|
|
||
|
case FM_SMVD_GET_FW_TYPE:
|
||
|
get_firmware_type_ret_data = (dev_cmd_fw_type *) local_buffer;
|
||
|
get_firmware_type_ret_data->firmware_type =
|
||
|
(char)current_firmware_type;
|
||
|
ret = sizeof(dev_cmd_fw_type);
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
ret = sprintf(str, "0");
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
if (copy_to_user(buffer, local_buffer, ret))
|
||
|
ret = -EFAULT;
|
||
|
|
||
|
out:
|
||
|
if (local_buffer)
|
||
|
kfree(local_buffer);
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
static ssize_t im501_spi_device_write(struct file *file,
|
||
|
const char __user *buffer, size_t length,
|
||
|
loff_t *offset)
|
||
|
{
|
||
|
dev_cmd_long *local_dev_cmd = NULL;
|
||
|
dev_cmd_fwdl *local_dev_cmd_fwdl;
|
||
|
/*if spi speed is fast enough, we can get total data from SPI together
|
||
|
dev_cmd_start_rec *local_rec_cmd;
|
||
|
*/
|
||
|
dev_cmd_message *local_msg_cmd;
|
||
|
|
||
|
unsigned int cmd_name, cmd_addr, cmd_val;
|
||
|
int dsp_mode;
|
||
|
int new_firmware_type;
|
||
|
unsigned char err = ESUCCESS;
|
||
|
|
||
|
dev_info(&im501_spi->dev, "%s: entering...\n", __func__);
|
||
|
if (im501_dsp_mode_old == -1) {
|
||
|
dev_err(&im501_spi->dev, "%s: iM501 firmware not loaded.\n", __func__);
|
||
|
err = -EIO;
|
||
|
goto out;
|
||
|
}
|
||
|
|
||
|
local_dev_cmd =
|
||
|
(dev_cmd_long *) kzalloc(sizeof(dev_cmd_long), GFP_KERNEL);
|
||
|
if (!local_dev_cmd) {
|
||
|
dev_err(&im501_spi->dev, "%s: local_dev_cmd allocation failure.\n", __func__);
|
||
|
err = -ENOMEM;
|
||
|
goto out;
|
||
|
}
|
||
|
|
||
|
err = copy_from_user(local_dev_cmd, buffer, min(sizeof(dev_cmd_long), length));
|
||
|
if (err) {
|
||
|
dev_err(&im501_spi->dev, "%s: copy_from_user error\n", __func__);
|
||
|
goto out;
|
||
|
}
|
||
|
|
||
|
dev_info(&im501_spi->dev, "local_dev_cmd->cmd_name = %d, length = %zu\n",
|
||
|
local_dev_cmd->cmd_name, length);
|
||
|
cmd_name = local_dev_cmd->cmd_name;
|
||
|
|
||
|
switch (cmd_name) {
|
||
|
//The short commands
|
||
|
case FM_SMVD_REG_WRITE: //Command #1
|
||
|
cmd_addr = local_dev_cmd->addr;
|
||
|
cmd_val = local_dev_cmd->val;
|
||
|
im501_spi_write_reg(cmd_addr, cmd_val);
|
||
|
break;
|
||
|
|
||
|
case FM_SMVD_DSP_ADDR_WRITE: //Command #3
|
||
|
cmd_addr = local_dev_cmd->addr;
|
||
|
cmd_val = local_dev_cmd->val;
|
||
|
im501_spi_write_dram(cmd_addr, (u8 *) &cmd_val);
|
||
|
break;
|
||
|
|
||
|
case FM_SMVD_MODE_SET: //Command #4
|
||
|
dev_info(&im501_spi->dev, "%s: FM_SMVD_MODE_SET dsp_mode = %d\n", __func__,
|
||
|
local_dev_cmd->addr);
|
||
|
dsp_mode = local_dev_cmd->addr;
|
||
|
// Temporarily set to PSM mode.
|
||
|
if (dsp_mode == 0)
|
||
|
request_enter_psm();
|
||
|
else if (dsp_mode == 1)
|
||
|
request_enter_normal();
|
||
|
else if (dsp_mode == 4) {
|
||
|
request_stop_voice_buf_trans();
|
||
|
msleep(2000);
|
||
|
closeDataFile();
|
||
|
output_counter = 0;
|
||
|
}
|
||
|
//im501_dsp_mode_old = dsp_mode;
|
||
|
break;
|
||
|
|
||
|
case FM_SMVD_DL_EFT_FW:
|
||
|
local_dev_cmd_fwdl = (dev_cmd_fwdl *) local_dev_cmd;
|
||
|
// Ensure filename is terminated
|
||
|
local_dev_cmd_fwdl->buf[sizeof(local_dev_cmd_fwdl->buf)-1] = '\0';
|
||
|
im501_dsp_load_single_fw_file(local_dev_cmd_fwdl->buf,
|
||
|
local_dev_cmd_fwdl->dsp_addr,
|
||
|
IM501_EFT_FW);
|
||
|
break;
|
||
|
|
||
|
case FM_SMVD_SWITCH_FW:
|
||
|
dev_info(&im501_spi->dev, "%s: FM_SMVD_SWITCH_FW firmware_type = %d\n", __func__,
|
||
|
local_dev_cmd->addr);
|
||
|
new_firmware_type = local_dev_cmd->addr;
|
||
|
|
||
|
if ((new_firmware_type == 0) || (new_firmware_type == 1)) {
|
||
|
if (new_firmware_type == current_firmware_type) {
|
||
|
dev_err(&im501_spi->dev,
|
||
|
"%s: requested firmware type is the same as current firmware type, not need to change it.\n",
|
||
|
__func__);
|
||
|
} else {
|
||
|
firmware_type = new_firmware_type;
|
||
|
schedule_work(&im501_fw_load_work);
|
||
|
}
|
||
|
} else {
|
||
|
dev_err(&im501_spi->dev,
|
||
|
"%s: requested firmware type is not supported.\n",
|
||
|
__func__);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
//Fuli 20170922 to support SPI recording
|
||
|
//do the same as one shot, app and lib will get sample data then generate pcm and wav by channel
|
||
|
case FM_SMVD_START_SPI_REC:
|
||
|
openFileForWrite(SPI_REC_FILE_PATH);
|
||
|
|
||
|
im501_spi_record_started = 1;
|
||
|
im501_host_irqstatus = 0; //not wakeup by keywords
|
||
|
|
||
|
//it will be set when im501 wakeup from PSM, set it here for SPI recording
|
||
|
im501_vbuf_trans_status = 1;
|
||
|
request_enter_normal();
|
||
|
im501_switch_to_spi_recording_mode();
|
||
|
request_start_voice_buf_trans();
|
||
|
|
||
|
break;
|
||
|
|
||
|
case FM_SMVD_STOP_SPI_REC:
|
||
|
request_stop_voice_buf_trans();
|
||
|
im501_spi_record_started = 0;
|
||
|
|
||
|
//not sure it is needed or not
|
||
|
im501_switch_to_normal_mode();
|
||
|
|
||
|
msleep(1000);
|
||
|
request_enter_psm();
|
||
|
closeDataFile();
|
||
|
output_counter = 0;
|
||
|
break;
|
||
|
|
||
|
//Fuli 20171022 to support general message interface
|
||
|
case FM_SMVD_SEND_MESSAGE:
|
||
|
local_msg_cmd = (dev_cmd_message *) local_dev_cmd;
|
||
|
dev_info(&im501_spi->dev,
|
||
|
"%s: will send message 0x%02X to DSP with data 0x%08X.\n",
|
||
|
__func__, local_msg_cmd->message_index,
|
||
|
local_msg_cmd->message_data);
|
||
|
err = im501_send_message_to_dsp(
|
||
|
((local_msg_cmd->message_index & 0x3F) << 2) | 0x01,
|
||
|
local_msg_cmd->message_data);
|
||
|
break;
|
||
|
|
||
|
case FM_SMVD_ENABLE_HOTWORD_DETECT:
|
||
|
dev_info(&im501_spi->dev, "%s: enable hotword\n", __func__);
|
||
|
codec2im501_pdm_clki_set(NORMAL_MODE, 0);
|
||
|
mdelay(100);
|
||
|
request_enter_psm();
|
||
|
break;
|
||
|
|
||
|
case FM_SMVD_DISABLE_HOTWORD_DETECT:
|
||
|
dev_info(&im501_spi->dev, "%s: disable hotword\n", __func__);
|
||
|
request_enter_normal();
|
||
|
mdelay(100);
|
||
|
codec2im501_pdm_clki_set(POWER_SAVING_MODE, 0);
|
||
|
break;
|
||
|
|
||
|
|
||
|
default:
|
||
|
break;
|
||
|
}
|
||
|
out:
|
||
|
if (local_dev_cmd)
|
||
|
kfree(local_dev_cmd);
|
||
|
return length;
|
||
|
}
|
||
|
|
||
|
struct file_operations im501_spi_fops = {
|
||
|
.owner = THIS_MODULE,
|
||
|
.read = im501_spi_device_read,
|
||
|
.write = im501_spi_device_write,
|
||
|
};
|
||
|
|
||
|
static struct miscdevice im501_spi_dev = {
|
||
|
.minor = MISC_DYNAMIC_MINOR,
|
||
|
.name = "im501_spi",
|
||
|
.fops = &im501_spi_fops
|
||
|
};
|
||
|
|
||
|
//Henry add for try
|
||
|
static const struct spi_device_id im501_spi_id[] = {
|
||
|
{"im501_spi", 0},
|
||
|
{}
|
||
|
};
|
||
|
|
||
|
MODULE_DEVICE_TABLE(spi, im501_spi_id);
|
||
|
//End
|
||
|
|
||
|
#ifdef CONFIG_OF
|
||
|
static struct of_device_id im501_spi_dt_ids[] = {
|
||
|
{.compatible = "fortemedia,im501_spi"},
|
||
|
{},
|
||
|
};
|
||
|
|
||
|
MODULE_DEVICE_TABLE(of, im501_spi_dt_ids);
|
||
|
#endif
|
||
|
|
||
|
static int im501_spi_probe(struct spi_device *spi)
|
||
|
{
|
||
|
int ret;
|
||
|
#ifdef CUBIETRUCK
|
||
|
enum gpio_eint_trigtype trigtype;
|
||
|
u32 trigenabled = 0;
|
||
|
script_item_value_type_e type;
|
||
|
script_item_u val;
|
||
|
int irq_gpio; /* irq gpio define */
|
||
|
#else
|
||
|
struct device *dev = &spi->dev;
|
||
|
struct device_node *np = dev->of_node;
|
||
|
#endif
|
||
|
u32 spi_speed;
|
||
|
|
||
|
im501_spi = spi;
|
||
|
|
||
|
im501_data =
|
||
|
(struct im501_data_t *)kzalloc(sizeof(struct im501_data_t),
|
||
|
GFP_KERNEL);
|
||
|
if (im501_data == NULL) {
|
||
|
dev_err(&im501_spi->dev, "%s: im501_data allocate fail\n", __func__);
|
||
|
return -ENOMEM;
|
||
|
}
|
||
|
|
||
|
mutex_init(&im501_data->spi_op_lock);
|
||
|
|
||
|
#ifdef CODEC2IM501_PDM_CLKI
|
||
|
codec2im501_pdm_clki_set(POWER_SAVING_MODE, 0);
|
||
|
mdelay(5);
|
||
|
#endif
|
||
|
|
||
|
#ifdef CUBIETRUCK
|
||
|
type = script_get_item("spi_board0", "max_speed_hz", &val);
|
||
|
if (SCIRPT_ITEM_VALUE_TYPE_INT != type) {
|
||
|
dev_err(&im501_spi->dev, "%s: request max_speed_hz error!\n", __func__);
|
||
|
spi_speed = SPI_HIGH_SPEED;
|
||
|
} else {
|
||
|
spi_speed = val.val;
|
||
|
}
|
||
|
#else
|
||
|
ret = of_property_read_u32(np, "spi-max-frequency", &spi_speed);
|
||
|
if (ret && ret != -EINVAL)
|
||
|
spi_speed = SPI_HIGH_SPEED;
|
||
|
#endif
|
||
|
|
||
|
spi->mode = SPI_MODE_0; // clk active low
|
||
|
spi->bits_per_word = 8;
|
||
|
spi->max_speed_hz = spi_speed;
|
||
|
|
||
|
ret = spi_setup(spi);
|
||
|
if (ret < 0) {
|
||
|
dev_err(&spi->dev, "spi_setup() failed\n");
|
||
|
return -EIO;
|
||
|
}
|
||
|
dev_info(&im501_spi->dev, "%s: setup spi, spi_speed=%d \n", __func__, spi_speed);
|
||
|
|
||
|
firmware_type = WAKEUP_FIRMWARE;
|
||
|
/*request_firmware_nowait(THIS_MODULE, FW_ACTION_HOTPLUG,
|
||
|
"im501_fw", &spi->dev, GFP_KERNEL,
|
||
|
(void *)&firmware_type, im501_fw_loaded);
|
||
|
*/
|
||
|
im501_create_cdev(spi);
|
||
|
|
||
|
ret = misc_register(&im501_spi_dev);
|
||
|
if (ret)
|
||
|
dev_err(&spi->dev, "Couldn't register control device\n");
|
||
|
|
||
|
im501_vbuf_trans_status = 0;
|
||
|
|
||
|
INIT_WORK(&im501_irq_work, im501_irq_handling_work);
|
||
|
INIT_WORK(&im501_fw_load_work, im501_fw_load);
|
||
|
im501_irq_wq = create_singlethread_workqueue("im501_irq_wq");
|
||
|
|
||
|
#ifdef CUBIETRUCK
|
||
|
irq_gpio = GPIOH(16);
|
||
|
im501_irq =
|
||
|
sw_gpio_irq_request(irq_gpio, TRIG_EDGE_POSITIVE,
|
||
|
(peint_handle) im501_irq_handler,
|
||
|
NULL);
|
||
|
dev_err(&im501_spi->dev, "%s: im501_irq = %d\n", __func__, im501_irq);
|
||
|
if (im501_irq == 0) {
|
||
|
dev_err(&im501_spi->dev, "%s: IM501 sw_gpio_irq_request failed\n", __func__);
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
ret = sw_gpio_eint_get_trigtype(irq_gpio, &trigtype);
|
||
|
if (ret != 0) {
|
||
|
dev_err(&im501_spi->dev, "%s: sw_gpio_eint_get_trigtype() failed.\n", __func__);
|
||
|
} else {
|
||
|
if (trigtype == TRIG_EDGE_POSITIVE) {
|
||
|
dev_err(&im501_spi->dev, "%s: trigger type is TRIG_EDGE_POSITIVE.\n",
|
||
|
__func__);
|
||
|
} else {
|
||
|
dev_err(&im501_spi->dev, "%s: trigger type is %d\n", __func__, trigtype);
|
||
|
}
|
||
|
}
|
||
|
ret = sw_gpio_eint_get_enable(irq_gpio, &trigenabled);
|
||
|
if (ret != 0) {
|
||
|
dev_err(&im501_spi->dev, "%s: sw_gpio_eint_get_enable() failed.\n", __func__);
|
||
|
} else {
|
||
|
dev_err(&im501_spi->dev, "%s: trigger enabled is %d\n", __func__, trigenabled);
|
||
|
}
|
||
|
#else
|
||
|
im501_data->irq_gpio = of_get_gpio(np, 0);
|
||
|
if (gpio_request(im501_data->irq_gpio, "im501_irq")) {
|
||
|
dev_err(&im501_spi->dev, "irq_gpio %d request failed!\n", im501_data->irq_gpio);
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
if (gpio_direction_input(im501_data->irq_gpio)) {
|
||
|
dev_err(&im501_spi->dev, "irq_gpio gpio_direction_input failed!\n");
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
im501_irq = gpio_to_irq(im501_data->irq_gpio);
|
||
|
irq_set_irq_type(im501_irq, IRQ_TYPE_EDGE_RISING);
|
||
|
ret = request_irq(im501_irq, im501_irq_handler, IRQF_TRIGGER_RISING,
|
||
|
"im501_spi", NULL);
|
||
|
if (ret) {
|
||
|
dev_err(&im501_spi->dev, "%s: irq %d request fail!\n", __func__, im501_irq);
|
||
|
return ret;
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
im501_data->reset_gpio = of_get_named_gpio(np, "reset-gpios", 0);
|
||
|
if (gpio_request(im501_data->reset_gpio, "im501_reset")) {
|
||
|
dev_err(&im501_spi->dev, "reset_gpio %d request failed!\n", im501_data->reset_gpio);
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
if (gpio_direction_output(im501_data->reset_gpio, 1)) {
|
||
|
dev_err(&im501_spi->dev, "reset_gpio gpio_direction_output failed!\n");
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
schedule_work(&im501_fw_load_work);
|
||
|
|
||
|
dev_info(&im501_spi->dev, "%s: success\n", __func__);
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static int im501_spi_remove(struct spi_device *spi)
|
||
|
{
|
||
|
dev_info(&im501_spi->dev, "%s: entering...\n", __func__);
|
||
|
|
||
|
#ifdef CUBIETRUCK
|
||
|
sw_gpio_irq_free(im501_irq);
|
||
|
#endif
|
||
|
|
||
|
flush_workqueue(im501_irq_wq);
|
||
|
destroy_workqueue(im501_irq_wq);
|
||
|
gpio_free(im501_data->irq_gpio);
|
||
|
gpio_free(im501_data->reset_gpio);
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static struct spi_driver im501_spi_driver = {
|
||
|
.driver = {
|
||
|
.name = "im501_spi",
|
||
|
.owner = THIS_MODULE,
|
||
|
#ifdef CONFIG_OF
|
||
|
.of_match_table = of_match_ptr(im501_spi_dt_ids),
|
||
|
#endif
|
||
|
},
|
||
|
.probe = im501_spi_probe,
|
||
|
.remove = im501_spi_remove,
|
||
|
//Henry add for try
|
||
|
.id_table = im501_spi_id,
|
||
|
//End of try
|
||
|
};
|
||
|
|
||
|
//module_spi_driver(im501_spi_driver);
|
||
|
|
||
|
static int __init im501_spi_init(void)
|
||
|
{
|
||
|
int status;
|
||
|
|
||
|
status = spi_register_driver(&im501_spi_driver);
|
||
|
if (status < 0) {
|
||
|
dev_err(&im501_spi->dev, "%s: im501_spi_driver failure. status = %d\n", __func__,
|
||
|
status);
|
||
|
}
|
||
|
dev_info(&im501_spi->dev, "%s: im501_spi_driver success. status = %d\n", __func__, status);
|
||
|
return status;
|
||
|
}
|
||
|
|
||
|
module_init(im501_spi_init);
|
||
|
|
||
|
static void __exit im501_spi_exit(void)
|
||
|
{
|
||
|
spi_unregister_driver(&im501_spi_driver);
|
||
|
}
|
||
|
|
||
|
module_exit(im501_spi_exit);
|
||
|
|
||
|
MODULE_DESCRIPTION("IM501 SPI driver");
|
||
|
MODULE_AUTHOR("<henryhzhang@fortemedia.com>, <fuli@fortemedia.com>");
|
||
|
MODULE_LICENSE("GPL v2");
|