| /* |
| * arch/arm/plat-ambarella/generic/ambarella_adc_drv.c |
| * |
| * Author: Bing-Liang Hu <blhu@ambarella.com> |
| * |
| * History: |
| * 2014/07/21 - [Cao Rongrong] Re-design the mechanism with client |
| * |
| * Copyright (C) 2014-2019, Ambarella, Inc. |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 2 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| * |
| */ |
| |
| #include <linux/delay.h> |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/io.h> |
| #include <linux/slab.h> |
| #include <linux/platform_device.h> |
| #include <linux/interrupt.h> |
| #include <linux/clk.h> |
| #include <linux/of.h> |
| #include <mach/hardware.h> |
| #include <plat/adc.h> |
| #include <plat/rct.h> |
| |
| static DEFINE_MUTEX(client_mutex); |
| static LIST_HEAD(client_list); |
| static struct ambadc_host *ambarella_adc; |
| |
| static ssize_t ambarella_adc_show(struct device *dev, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| ssize_t ret = 0; |
| u32 i, data; |
| |
| for (i = 0; i < ADC_NUM_CHANNELS; i++) { |
| data = ambarella_adc_read_level(i); |
| ret += sprintf(&buf[ret], "adc%d=0x%x\n", i, data); |
| } |
| |
| return ret; |
| } |
| static DEVICE_ATTR(adcsys, 0444, ambarella_adc_show, NULL); |
| |
| static int ambarella_adc_fifo_ctrl(u32 fid, u16 fcid) |
| { |
| u32 reg=0; |
| u32 reg_val=0; |
| |
| switch(fid) { |
| case 0: |
| reg = ADC_FIFO_CTRL_0_REG; |
| reg_val = ADC_FIFO_OVER_INT_EN |
| | ADC_FIFO_UNDR_INT_EN |
| | ADC_FIFO_TH |
| | (fcid << ADC_FIFO_ID_SHIFT) |
| | ADC_FIFO_DEPTH; |
| break; |
| case 1: |
| reg = ADC_FIFO_CTRL_1_REG; |
| reg_val = ADC_FIFO_OVER_INT_EN |
| | ADC_FIFO_UNDR_INT_EN |
| | ADC_FIFO_TH |
| | (fcid << ADC_FIFO_ID_SHIFT) |
| | ADC_FIFO_DEPTH; |
| break; |
| case 2: |
| reg = ADC_FIFO_CTRL_2_REG; |
| reg_val = ADC_FIFO_OVER_INT_EN |
| | ADC_FIFO_UNDR_INT_EN |
| | ADC_FIFO_TH |
| | (fcid << ADC_FIFO_ID_SHIFT) |
| | ADC_FIFO_DEPTH; |
| break; |
| break; |
| case 3: |
| reg = ADC_FIFO_CTRL_3_REG; |
| reg_val = ADC_FIFO_OVER_INT_EN |
| | ADC_FIFO_UNDR_INT_EN |
| | ADC_FIFO_TH |
| | (fcid << ADC_FIFO_ID_SHIFT) |
| | ADC_FIFO_DEPTH; |
| break; |
| break; |
| default: |
| pr_err("%s: invalid fifo NO = %d.\n", |
| __func__, fid); |
| return -1; |
| } |
| |
| amba_writel(reg, reg_val); |
| |
| amba_writel(ADC_FIFO_CTRL_REG, ADC_FIFO_CONTROL_CLEAR); |
| return 0; |
| } |
| |
| static void ambarella_adc_enable(void) |
| { |
| /* select adc scaler as clk_adc and power up adc */ |
| amba_writel(ADC16_CTRL_REG, 0); |
| |
| #if (ADC_SUPPORT_SLOT == 1) |
| /* soft reset adc controller, set slot, and then enable it */ |
| amba_writel(ADC_CONTROL_REG, ADC_CONTROL_RESET); |
| amba_setbitsl(ADC_CONTROL_REG, ADC_CONTROL_MODE); |
| #endif |
| |
| amba_setbitsl(ADC_ENABLE_REG, ADC_CONTROL_ENABLE); |
| udelay(3); // wait for ADC to stabilize |
| |
| #if (ADC_SUPPORT_SLOT == 1) |
| amba_writel(ADC_SLOT_NUM_REG, 0x0); |
| amba_writel(ADC_SLOT_PERIOD_REG, 0xffff); |
| amba_writel(ADC_SLOT_CTRL_0_REG, 0x0fff); |
| #endif |
| //test fifo read in fifo_0 |
| if(ambarella_adc->fifo_mode){ |
| ambarella_adc_fifo_ctrl(0,1); |
| } |
| } |
| |
| static void ambarella_adc_disable(void) |
| { |
| amba_clrbitsl(ADC_ENABLE_REG, ADC_CONTROL_ENABLE); |
| amba_writel(ADC16_CTRL_REG, ADC_CTRL_SCALER_POWERDOWN | ADC_CTRL_POWERDOWN); |
| } |
| |
| static void ambarella_adc_start(void) |
| { |
| amba_setbitsl(ADC_CONTROL_REG, ADC_CONTROL_START); |
| } |
| |
| static void ambarella_adc_stop(void) |
| { |
| amba_clrbitsl(ADC_CONTROL_REG, ADC_CONTROL_MODE | ADC_CONTROL_START); |
| } |
| |
| static void ambarella_adc_reset(void) |
| { |
| ambarella_adc_disable(); |
| ambarella_adc_enable(); |
| ambarella_adc_start(); |
| } |
| |
| struct ambadc_client *ambarella_adc_register_client(struct device *dev, |
| u32 mode, ambadc_client_callback callback) |
| { |
| struct ambadc_host *amb_adc = ambarella_adc; |
| struct ambadc_client *client; |
| |
| if (!dev || (mode == AMBADC_CONTINUOUS && !callback)) { |
| dev_err(dev, "dev or callback missing\n"); |
| return NULL; |
| } |
| |
| client = kzalloc(sizeof(struct ambadc_client), GFP_KERNEL); |
| if (!client) { |
| dev_err(dev, "no memory for adc client\n"); |
| return NULL; |
| } |
| |
| client->dev = dev; |
| client->dev->parent = amb_adc->dev; |
| client->mode = mode; |
| client->callback = callback; |
| client->host = amb_adc; |
| |
| mutex_lock(&client_mutex); |
| list_add_tail(&client->node, &client_list); |
| mutex_unlock(&client_mutex); |
| |
| if (mode == AMBADC_CONTINUOUS) { |
| amb_adc->keep_start = true; |
| ambarella_adc_enable(); |
| ambarella_adc_start(); |
| |
| if (amb_adc->polling_mode) { |
| queue_delayed_work(system_wq, |
| &amb_adc->work, msecs_to_jiffies(20)); |
| } |
| } |
| |
| return client; |
| } |
| EXPORT_SYMBOL(ambarella_adc_register_client); |
| |
| void ambarella_adc_unregister_client(struct ambadc_client *client) |
| { |
| struct ambadc_host *amb_adc = client->host; |
| struct ambadc_client *_client; |
| u32 keep_start = 0; |
| |
| mutex_lock(&client_mutex); |
| |
| list_del(&client->node); |
| kfree(client); |
| |
| list_for_each_entry(_client, &client_list, node) { |
| if (_client->mode == AMBADC_CONTINUOUS) { |
| keep_start = 1; |
| break; |
| } |
| } |
| |
| mutex_unlock(&client_mutex); |
| |
| if (keep_start == 0) { |
| amb_adc->keep_start = false; |
| ambarella_adc_stop(); |
| } |
| } |
| EXPORT_SYMBOL(ambarella_adc_unregister_client); |
| |
| int ambarella_adc_read_level(u32 ch) |
| { |
| struct ambadc_host *amb_adc = ambarella_adc; |
| int data = -EIO; |
| int ready = 0; |
| u32 i,count; |
| |
| if (ch >= ADC_NUM_CHANNELS) |
| return -EINVAL; |
| |
| if (amb_adc == NULL) |
| return -ENODEV; |
| |
| if (!amb_adc->keep_start) |
| { |
| ambarella_adc_enable(); |
| ambarella_adc_start(); |
| } |
| |
| // wait for status |
| for (i = 0; i < 10000; i++) { |
| if (amba_tstbitsl(ADC_STATUS_REG, ADC_STATUS)) { |
| ready = 1; |
| break; |
| } |
| udelay(1); |
| } |
| |
| udelay(10); |
| if (ready) { |
| if(!ambarella_adc->fifo_mode){ |
| data = amba_readl(ADC_DATA_REG(ch)); |
| } else { |
| count = 0x7ff & amba_readl(ADC_FIFO_STATUS_0_REG); |
| if(count > 4) |
| count -= 4; |
| |
| for(i = 0; i < count; i++){ |
| data = amba_readl(ADC_FIFO_DATA0_REG + i*4); |
| // printk("call back [0x%x]=0x%x,count=0x%x,0x%x\n", |
| // (ADC_FIFO_DATA0_REG + i*4),data,count,ch); |
| } |
| //data = data/count; |
| } |
| } |
| |
| if (!amb_adc->keep_start) |
| ambarella_adc_stop(); |
| |
| return data; |
| } |
| EXPORT_SYMBOL(ambarella_adc_read_level); |
| |
| int ambarella_adc_set_threshold(struct ambadc_client *client, |
| u32 ch, u32 low, u32 high) |
| { |
| struct ambadc_client *_client; |
| int value, rval = 0; |
| |
| if (ch >= ADC_NUM_CHANNELS) |
| return -EINVAL; |
| |
| if (client->mode != AMBADC_CONTINUOUS) { |
| dev_err(client->dev, "Invalid adc mode: %d\n", client->mode); |
| return -EINVAL; |
| } |
| |
| /* interrupt will be happened when level < low OR level > high */ |
| value = ADC_EN_HI(!!high) | ADC_EN_LO(!!low) | |
| ADC_VAL_HI(high) | ADC_VAL_LO(low); |
| |
| mutex_lock(&client_mutex); |
| |
| /* it's not allowed that more than one clients want to set different |
| * threshold for the same adc channel. */ |
| list_for_each_entry(_client, &client_list, node) { |
| if (_client == client) |
| continue; |
| /* check if other clients set the threshold for this channel */ |
| if (_client->threshold[ch] && (_client->threshold[ch] != value)) { |
| if (value == 0) { |
| rval = 0; |
| goto exit; |
| } else { |
| rval = -EBUSY; |
| goto exit; |
| } |
| } |
| } |
| |
| if(!ambarella_adc->fifo_mode) |
| amba_writel(ADC_CHAN_INTR_REG(ch), value); |
| |
| exit: |
| if (rval == 0) |
| client->threshold[ch] = value; |
| |
| mutex_unlock(&client_mutex); |
| |
| return rval; |
| } |
| EXPORT_SYMBOL(ambarella_adc_set_threshold); |
| |
| static void ambarella_adc_client_callback(u32 ch) |
| { |
| struct ambadc_client *client; |
| u32 data; |
| |
| data = ambarella_adc_read_level(ch); |
| |
| list_for_each_entry(client, &client_list, node) { |
| if (client->callback && client->threshold[ch]) |
| client->callback(client, ch, data); |
| } |
| } |
| |
| static void ambarella_adc_polling_work(struct work_struct *work) |
| { |
| struct ambadc_host *amb_adc; |
| u32 i; |
| |
| amb_adc = container_of(work, struct ambadc_host, work.work); |
| |
| for (i = 0; i < ADC_NUM_CHANNELS; i++) |
| ambarella_adc_client_callback(i); |
| |
| if (amb_adc->keep_start) { |
| queue_delayed_work(system_wq, |
| &amb_adc->work, msecs_to_jiffies(20)); |
| } |
| } |
| |
| static irqreturn_t ambarella_adc_irq(int irq, void *dev_id) |
| { |
| u32 i, int_src,rval,chanNo; |
| struct ambadc_host *amb_adc = dev_id; |
| |
| chanNo = 0; |
| if(!amb_adc->fifo_mode){ |
| int_src = amba_readl(ADC_DATA_INTR_TABLE_REG); |
| amba_writel(ADC_DATA_INTR_TABLE_REG, int_src); |
| chanNo = int_src; |
| }else{ |
| rval = amba_readl(ADC_CTRL_INTR_TABLE_REG); |
| if(rval){ |
| ambarella_adc_reset(); |
| return IRQ_HANDLED; |
| } |
| rval = amba_readl(ADC_FIFO_INTR_TABLE_REG); |
| amba_writel(ADC_FIFO_INTR_TABLE_REG, rval); |
| |
| for(i=0;i<ADC_FIFO_NUMBER;i++){ |
| if((rval & (1 << i)) == 0) |
| continue; |
| rval = amba_readl(ADC_FIFO_CTRL_X_REG(i)); |
| rval = (rval & (0x0000f000)) >> 12; |
| chanNo = 1 << rval; |
| } |
| } |
| |
| for (i = 0; i < ADC_NUM_CHANNELS; i++) { |
| if ((chanNo & (1 << i)) == 0) |
| continue; |
| |
| ambarella_adc_client_callback(i); |
| } |
| |
| return IRQ_HANDLED; |
| } |
| |
| static int ambarella_adc_probe(struct platform_device *pdev) |
| { |
| struct ambadc_host *amb_adc; |
| struct device_node *np = pdev->dev.of_node; |
| int ret = 0; |
| |
| amb_adc = devm_kzalloc(&pdev->dev, |
| sizeof(struct ambadc_host), GFP_KERNEL); |
| if (!amb_adc) { |
| dev_err(&pdev->dev, "Failed to allocate memory!\n"); |
| return -ENOMEM; |
| } |
| |
| ret = of_property_read_u32(np, "clock-frequency", &amb_adc->clk); |
| if (ret < 0) { |
| dev_err(&pdev->dev, "Get clock-frequency failed!\n"); |
| return -ENODEV; |
| } |
| |
| //amb_adc->fifo_mode=1;//test fifo mode |
| |
| amb_adc->polling_mode = !!of_find_property(np, "amb,polling-mode", NULL); |
| if (amb_adc->polling_mode) { |
| INIT_DELAYED_WORK(&amb_adc->work, ambarella_adc_polling_work); |
| } else { |
| amb_adc->irq = platform_get_irq(pdev, 0); |
| if (amb_adc->irq < 0) { |
| dev_err(&pdev->dev, "Can not get irq !\n"); |
| return -ENXIO; |
| } |
| |
| ret = devm_request_irq(&pdev->dev, amb_adc->irq, |
| ambarella_adc_irq, IRQF_TRIGGER_HIGH, |
| dev_name(&pdev->dev), amb_adc); |
| if (ret < 0) { |
| dev_err(&pdev->dev, "Can not request irq %d!\n", amb_adc->irq); |
| return -ENXIO; |
| } |
| } |
| |
| |
| ret = clk_set_rate(clk_get(NULL, "gclk_adc"), amb_adc->clk); |
| if (ret < 0) |
| return ret; |
| |
| amb_adc->dev = &pdev->dev; |
| |
| ambarella_adc = amb_adc; |
| |
| ambarella_adc_enable(); |
| if(amb_adc->fifo_mode == 1){ |
| amb_adc->keep_start = true; |
| ambarella_adc_start(); |
| } |
| |
| ret = device_create_file(&pdev->dev, &dev_attr_adcsys); |
| if (ret != 0) { |
| dev_err(&pdev->dev, "can not create file : %d\n", ret); |
| return ret; |
| } |
| |
| platform_set_drvdata(pdev, amb_adc); |
| |
| dev_info(&pdev->dev, "Ambarella ADC driver init\n"); |
| |
| return 0; |
| } |
| |
| static int ambarella_adc_remove(struct platform_device *pdev) |
| { |
| device_remove_file(&pdev->dev, &dev_attr_adcsys); |
| ambarella_adc_disable(); |
| return 0; |
| } |
| |
| #ifdef CONFIG_PM |
| static int ambarella_adc_suspend(struct platform_device *pdev, |
| pm_message_t state) |
| { |
| ambarella_adc_disable(); |
| return 0; |
| } |
| |
| static int ambarella_adc_resume(struct platform_device *pdev) |
| { |
| struct ambadc_host *amb_adc = platform_get_drvdata(pdev); |
| struct ambadc_client *client; |
| u32 ch; |
| |
| clk_set_rate(clk_get(NULL, "gclk_adc"), amb_adc->clk); |
| ambarella_adc_enable(); |
| |
| if (amb_adc->keep_start) |
| ambarella_adc_start(); |
| |
| if(ambarella_adc->fifo_mode) |
| goto exit; |
| |
| for (ch = 0; ch < ADC_NUM_CHANNELS; ch++) { |
| list_for_each_entry(client, &client_list, node) { |
| if (client->threshold[ch] == 0) |
| continue; |
| amba_writel(ADC_CHAN_INTR_REG(ch), client->threshold[ch]); |
| } |
| } |
| |
| exit: |
| return 0; |
| } |
| #endif |
| |
| static const struct of_device_id ambarella_adc_dt_ids[] = { |
| {.compatible = "ambarella,adc", }, |
| {}, |
| }; |
| MODULE_DEVICE_TABLE(of, ambarella_adc_dt_ids); |
| |
| static struct platform_driver ambarella_adc_driver = { |
| .driver = { |
| .name = "ambarella-adc", |
| .owner = THIS_MODULE, |
| .of_match_table = ambarella_adc_dt_ids, |
| }, |
| .probe = ambarella_adc_probe, |
| .remove = ambarella_adc_remove, |
| #ifdef CONFIG_PM |
| .suspend = ambarella_adc_suspend, |
| .resume = ambarella_adc_resume, |
| #endif |
| }; |
| |
| module_platform_driver(ambarella_adc_driver); |
| MODULE_AUTHOR("BingLiang Hu <blhu@ambarella.com>"); |
| MODULE_DESCRIPTION("Ambarella ADC Driver"); |
| MODULE_LICENSE("GPL"); |
| |