Set of ARM CCN PMU driver updates:
- fixed a nasty bitfield mangling bug - added new hints to the perf userspace tool - pinned events processing to a single PMU - modified events initialisation so they can be rotated now -----BEGIN PGP SIGNATURE----- Version: GnuPG v1 iQEcBAABAgAGBQJVQ7fuAAoJEL9jRaJfXa5Pr6EH+wd92YXgpFHy4x0O77GdK/gk ErzfFlq/3GX6V+1WkMn0Nfj1vhl/Mq9RF4nSbmEU4c5x0ov48J67b94peo9RChfF +BjaLw0mNijpCvcFYNIOW4qvJr5LKizvaq3uzw7h7AFl24MwRIKrNXJP6qON04sB 9TCekVCWutKORkEmVpl4Wltsb3LpqNwo/ziby2dmgRCsxPeXnOycsk45/4Tc0GSg oR5MXLAuvxvNivNMF83FphiAeqPu1eeSWYkT468qqarr4NWKpbYd6uNTMViVHUcW Yd6pxUwB7dphmLckDOIuJ9QJOfxL263uHaq1taUm1HVp15ldKfWapgVH4hIsUj8= =mXhn -----END PGP SIGNATURE----- Merge tag 'ccn/updates-for-4.2' of git://git.linaro.org/people/pawel.moll/linux into next/drivers Pull "Set of ARM CCN PMU driver updates" from Pawel Moll: - fixed a nasty bitfield mangling bug - added new hints to the perf userspace tool - pinned events processing to a single PMU - modified events initialisation so they can be rotated now * tag 'ccn/updates-for-4.2' of git://git.linaro.org/people/pawel.moll/linux: bus: arm-ccn: Allocate event when it is being added, not initialised bus: arm-ccn: Do not group CCN events with other PMUs bus: arm-ccn: Provide required event arguments bus: arm-ccn: cpumask attribute bus: arm-ccn: Fix node->XP config conversion
This commit is contained in:
commit
11f52002bf
|
@ -33,20 +33,23 @@ directory, with first 8 configurable by user and additional
|
|||
Cycle counter is described by a "type" value 0xff and does
|
||||
not require any other settings.
|
||||
|
||||
The driver also provides a "cpumask" sysfs attribute, which contains
|
||||
a single CPU ID, of the processor which will be used to handle all
|
||||
the CCN PMU events. It is recommended that the user space tools
|
||||
request the events on this processor (if not, the perf_event->cpu value
|
||||
will be overwritten anyway). In case of this processor being offlined,
|
||||
the events are migrated to another one and the attribute is updated.
|
||||
|
||||
Example of perf tool use:
|
||||
|
||||
/ # perf list | grep ccn
|
||||
ccn/cycles/ [Kernel PMU event]
|
||||
<...>
|
||||
ccn/xp_valid_flit/ [Kernel PMU event]
|
||||
ccn/xp_valid_flit,xp=?,port=?,vc=?,dir=?/ [Kernel PMU event]
|
||||
<...>
|
||||
|
||||
/ # perf stat -C 0 -e ccn/cycles/,ccn/xp_valid_flit,xp=1,port=0,vc=1,dir=1/ \
|
||||
/ # perf stat -a -e ccn/cycles/,ccn/xp_valid_flit,xp=1,port=0,vc=1,dir=1/ \
|
||||
sleep 1
|
||||
|
||||
The driver does not support sampling, therefore "perf record" will
|
||||
not work. Also notice that only single cpu is being selected
|
||||
("-C 0") - this is because perf framework does not support
|
||||
"non-CPU related" counters (yet?) so system-wide session ("-a")
|
||||
would try (and in most cases fail) to set up the same event
|
||||
per each CPU.
|
||||
not work. Per-task (without "-a") perf sessions are not supported.
|
||||
|
|
|
@ -166,13 +166,17 @@ struct arm_ccn_dt {
|
|||
|
||||
struct hrtimer hrtimer;
|
||||
|
||||
cpumask_t cpu;
|
||||
struct notifier_block cpu_nb;
|
||||
|
||||
struct pmu pmu;
|
||||
};
|
||||
|
||||
struct arm_ccn {
|
||||
struct device *dev;
|
||||
void __iomem *base;
|
||||
unsigned irq_used:1;
|
||||
unsigned int irq;
|
||||
|
||||
unsigned sbas_present:1;
|
||||
unsigned sbsx_present:1;
|
||||
|
||||
|
@ -212,7 +216,7 @@ static int arm_ccn_node_to_xp_port(int node)
|
|||
|
||||
static void arm_ccn_pmu_config_set(u64 *config, u32 node_xp, u32 type, u32 port)
|
||||
{
|
||||
*config &= ~((0xff << 0) | (0xff << 8) | (0xff << 24));
|
||||
*config &= ~((0xff << 0) | (0xff << 8) | (0x3 << 24));
|
||||
*config |= (node_xp << 0) | (type << 8) | (port << 24);
|
||||
}
|
||||
|
||||
|
@ -336,6 +340,23 @@ static ssize_t arm_ccn_pmu_event_show(struct device *dev,
|
|||
if (event->mask)
|
||||
res += snprintf(buf + res, PAGE_SIZE - res, ",mask=0x%x",
|
||||
event->mask);
|
||||
|
||||
/* Arguments required by an event */
|
||||
switch (event->type) {
|
||||
case CCN_TYPE_CYCLES:
|
||||
break;
|
||||
case CCN_TYPE_XP:
|
||||
res += snprintf(buf + res, PAGE_SIZE - res,
|
||||
",xp=?,port=?,vc=?,dir=?");
|
||||
if (event->event == CCN_EVENT_WATCHPOINT)
|
||||
res += snprintf(buf + res, PAGE_SIZE - res,
|
||||
",cmp_l=?,cmp_h=?,mask=?");
|
||||
break;
|
||||
default:
|
||||
res += snprintf(buf + res, PAGE_SIZE - res, ",node=?");
|
||||
break;
|
||||
}
|
||||
|
||||
res += snprintf(buf + res, PAGE_SIZE - res, "\n");
|
||||
|
||||
return res;
|
||||
|
@ -521,6 +542,25 @@ static struct attribute_group arm_ccn_pmu_cmp_mask_attr_group = {
|
|||
.attrs = arm_ccn_pmu_cmp_mask_attrs,
|
||||
};
|
||||
|
||||
static ssize_t arm_ccn_pmu_cpumask_show(struct device *dev,
|
||||
struct device_attribute *attr, char *buf)
|
||||
{
|
||||
struct arm_ccn *ccn = pmu_to_arm_ccn(dev_get_drvdata(dev));
|
||||
|
||||
return cpumap_print_to_pagebuf(true, buf, &ccn->dt.cpu);
|
||||
}
|
||||
|
||||
static struct device_attribute arm_ccn_pmu_cpumask_attr =
|
||||
__ATTR(cpumask, S_IRUGO, arm_ccn_pmu_cpumask_show, NULL);
|
||||
|
||||
static struct attribute *arm_ccn_pmu_cpumask_attrs[] = {
|
||||
&arm_ccn_pmu_cpumask_attr.attr,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static struct attribute_group arm_ccn_pmu_cpumask_attr_group = {
|
||||
.attrs = arm_ccn_pmu_cpumask_attrs,
|
||||
};
|
||||
|
||||
/*
|
||||
* Default poll period is 10ms, which is way over the top anyway,
|
||||
|
@ -542,6 +582,7 @@ static const struct attribute_group *arm_ccn_pmu_attr_groups[] = {
|
|||
&arm_ccn_pmu_events_attr_group,
|
||||
&arm_ccn_pmu_format_attr_group,
|
||||
&arm_ccn_pmu_cmp_mask_attr_group,
|
||||
&arm_ccn_pmu_cpumask_attr_group,
|
||||
NULL
|
||||
};
|
||||
|
||||
|
@ -587,7 +628,65 @@ static int arm_ccn_pmu_type_eq(u32 a, u32 b)
|
|||
return 0;
|
||||
}
|
||||
|
||||
static void arm_ccn_pmu_event_destroy(struct perf_event *event)
|
||||
static int arm_ccn_pmu_event_alloc(struct perf_event *event)
|
||||
{
|
||||
struct arm_ccn *ccn = pmu_to_arm_ccn(event->pmu);
|
||||
struct hw_perf_event *hw = &event->hw;
|
||||
u32 node_xp, type, event_id;
|
||||
struct arm_ccn_component *source;
|
||||
int bit;
|
||||
|
||||
node_xp = CCN_CONFIG_NODE(event->attr.config);
|
||||
type = CCN_CONFIG_TYPE(event->attr.config);
|
||||
event_id = CCN_CONFIG_EVENT(event->attr.config);
|
||||
|
||||
/* Allocate the cycle counter */
|
||||
if (type == CCN_TYPE_CYCLES) {
|
||||
if (test_and_set_bit(CCN_IDX_PMU_CYCLE_COUNTER,
|
||||
ccn->dt.pmu_counters_mask))
|
||||
return -EAGAIN;
|
||||
|
||||
hw->idx = CCN_IDX_PMU_CYCLE_COUNTER;
|
||||
ccn->dt.pmu_counters[CCN_IDX_PMU_CYCLE_COUNTER].event = event;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Allocate an event counter */
|
||||
hw->idx = arm_ccn_pmu_alloc_bit(ccn->dt.pmu_counters_mask,
|
||||
CCN_NUM_PMU_EVENT_COUNTERS);
|
||||
if (hw->idx < 0) {
|
||||
dev_dbg(ccn->dev, "No more counters available!\n");
|
||||
return -EAGAIN;
|
||||
}
|
||||
|
||||
if (type == CCN_TYPE_XP)
|
||||
source = &ccn->xp[node_xp];
|
||||
else
|
||||
source = &ccn->node[node_xp];
|
||||
ccn->dt.pmu_counters[hw->idx].source = source;
|
||||
|
||||
/* Allocate an event source or a watchpoint */
|
||||
if (type == CCN_TYPE_XP && event_id == CCN_EVENT_WATCHPOINT)
|
||||
bit = arm_ccn_pmu_alloc_bit(source->xp.dt_cmp_mask,
|
||||
CCN_NUM_XP_WATCHPOINTS);
|
||||
else
|
||||
bit = arm_ccn_pmu_alloc_bit(source->pmu_events_mask,
|
||||
CCN_NUM_PMU_EVENTS);
|
||||
if (bit < 0) {
|
||||
dev_dbg(ccn->dev, "No more event sources/watchpoints on node/XP %d!\n",
|
||||
node_xp);
|
||||
clear_bit(hw->idx, ccn->dt.pmu_counters_mask);
|
||||
return -EAGAIN;
|
||||
}
|
||||
hw->config_base = bit;
|
||||
|
||||
ccn->dt.pmu_counters[hw->idx].event = event;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void arm_ccn_pmu_event_release(struct perf_event *event)
|
||||
{
|
||||
struct arm_ccn *ccn = pmu_to_arm_ccn(event->pmu);
|
||||
struct hw_perf_event *hw = &event->hw;
|
||||
|
@ -616,15 +715,14 @@ static int arm_ccn_pmu_event_init(struct perf_event *event)
|
|||
struct arm_ccn *ccn;
|
||||
struct hw_perf_event *hw = &event->hw;
|
||||
u32 node_xp, type, event_id;
|
||||
int valid, bit;
|
||||
struct arm_ccn_component *source;
|
||||
int valid;
|
||||
int i;
|
||||
struct perf_event *sibling;
|
||||
|
||||
if (event->attr.type != event->pmu->type)
|
||||
return -ENOENT;
|
||||
|
||||
ccn = pmu_to_arm_ccn(event->pmu);
|
||||
event->destroy = arm_ccn_pmu_event_destroy;
|
||||
|
||||
if (hw->sample_period) {
|
||||
dev_warn(ccn->dev, "Sampling not supported!\n");
|
||||
|
@ -642,6 +740,16 @@ static int arm_ccn_pmu_event_init(struct perf_event *event)
|
|||
dev_warn(ccn->dev, "Can't provide per-task data!\n");
|
||||
return -EOPNOTSUPP;
|
||||
}
|
||||
/*
|
||||
* Many perf core operations (eg. events rotation) operate on a
|
||||
* single CPU context. This is obvious for CPU PMUs, where one
|
||||
* expects the same sets of events being observed on all CPUs,
|
||||
* but can lead to issues for off-core PMUs, like CCN, where each
|
||||
* event could be theoretically assigned to a different CPU. To
|
||||
* mitigate this, we enforce CPU assignment to one, selected
|
||||
* processor (the one described in the "cpumask" attribute).
|
||||
*/
|
||||
event->cpu = cpumask_first(&ccn->dt.cpu);
|
||||
|
||||
node_xp = CCN_CONFIG_NODE(event->attr.config);
|
||||
type = CCN_CONFIG_TYPE(event->attr.config);
|
||||
|
@ -711,48 +819,20 @@ static int arm_ccn_pmu_event_init(struct perf_event *event)
|
|||
node_xp, type, port);
|
||||
}
|
||||
|
||||
/* Allocate the cycle counter */
|
||||
if (type == CCN_TYPE_CYCLES) {
|
||||
if (test_and_set_bit(CCN_IDX_PMU_CYCLE_COUNTER,
|
||||
ccn->dt.pmu_counters_mask))
|
||||
return -EAGAIN;
|
||||
/*
|
||||
* We must NOT create groups containing mixed PMUs, although software
|
||||
* events are acceptable (for example to create a CCN group
|
||||
* periodically read when a hrtimer aka cpu-clock leader triggers).
|
||||
*/
|
||||
if (event->group_leader->pmu != event->pmu &&
|
||||
!is_software_event(event->group_leader))
|
||||
return -EINVAL;
|
||||
|
||||
hw->idx = CCN_IDX_PMU_CYCLE_COUNTER;
|
||||
ccn->dt.pmu_counters[CCN_IDX_PMU_CYCLE_COUNTER].event = event;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Allocate an event counter */
|
||||
hw->idx = arm_ccn_pmu_alloc_bit(ccn->dt.pmu_counters_mask,
|
||||
CCN_NUM_PMU_EVENT_COUNTERS);
|
||||
if (hw->idx < 0) {
|
||||
dev_warn(ccn->dev, "No more counters available!\n");
|
||||
return -EAGAIN;
|
||||
}
|
||||
|
||||
if (type == CCN_TYPE_XP)
|
||||
source = &ccn->xp[node_xp];
|
||||
else
|
||||
source = &ccn->node[node_xp];
|
||||
ccn->dt.pmu_counters[hw->idx].source = source;
|
||||
|
||||
/* Allocate an event source or a watchpoint */
|
||||
if (type == CCN_TYPE_XP && event_id == CCN_EVENT_WATCHPOINT)
|
||||
bit = arm_ccn_pmu_alloc_bit(source->xp.dt_cmp_mask,
|
||||
CCN_NUM_XP_WATCHPOINTS);
|
||||
else
|
||||
bit = arm_ccn_pmu_alloc_bit(source->pmu_events_mask,
|
||||
CCN_NUM_PMU_EVENTS);
|
||||
if (bit < 0) {
|
||||
dev_warn(ccn->dev, "No more event sources/watchpoints on node/XP %d!\n",
|
||||
node_xp);
|
||||
clear_bit(hw->idx, ccn->dt.pmu_counters_mask);
|
||||
return -EAGAIN;
|
||||
}
|
||||
hw->config_base = bit;
|
||||
|
||||
ccn->dt.pmu_counters[hw->idx].event = event;
|
||||
list_for_each_entry(sibling, &event->group_leader->sibling_list,
|
||||
group_entry)
|
||||
if (sibling->pmu != event->pmu &&
|
||||
!is_software_event(sibling))
|
||||
return -EINVAL;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
@ -835,9 +915,15 @@ static void arm_ccn_pmu_event_start(struct perf_event *event, int flags)
|
|||
arm_ccn_pmu_read_counter(ccn, hw->idx));
|
||||
hw->state = 0;
|
||||
|
||||
if (!ccn->irq_used)
|
||||
hrtimer_start(&ccn->dt.hrtimer, arm_ccn_pmu_timer_period(),
|
||||
HRTIMER_MODE_REL);
|
||||
/*
|
||||
* Pin the timer, so that the overflows are handled by the chosen
|
||||
* event->cpu (this is the same one as presented in "cpumask"
|
||||
* attribute).
|
||||
*/
|
||||
if (!ccn->irq)
|
||||
__hrtimer_start_range_ns(&ccn->dt.hrtimer,
|
||||
arm_ccn_pmu_timer_period(), 0,
|
||||
HRTIMER_MODE_REL_PINNED, 0);
|
||||
|
||||
/* Set the DT bus input, engaging the counter */
|
||||
arm_ccn_pmu_xp_dt_config(event, 1);
|
||||
|
@ -852,7 +938,7 @@ static void arm_ccn_pmu_event_stop(struct perf_event *event, int flags)
|
|||
/* Disable counting, setting the DT bus to pass-through mode */
|
||||
arm_ccn_pmu_xp_dt_config(event, 0);
|
||||
|
||||
if (!ccn->irq_used)
|
||||
if (!ccn->irq)
|
||||
hrtimer_cancel(&ccn->dt.hrtimer);
|
||||
|
||||
/* Let the DT bus drain */
|
||||
|
@ -1014,8 +1100,13 @@ static void arm_ccn_pmu_event_config(struct perf_event *event)
|
|||
|
||||
static int arm_ccn_pmu_event_add(struct perf_event *event, int flags)
|
||||
{
|
||||
int err;
|
||||
struct hw_perf_event *hw = &event->hw;
|
||||
|
||||
err = arm_ccn_pmu_event_alloc(event);
|
||||
if (err)
|
||||
return err;
|
||||
|
||||
arm_ccn_pmu_event_config(event);
|
||||
|
||||
hw->state = PERF_HES_STOPPED;
|
||||
|
@ -1029,6 +1120,8 @@ static int arm_ccn_pmu_event_add(struct perf_event *event, int flags)
|
|||
static void arm_ccn_pmu_event_del(struct perf_event *event, int flags)
|
||||
{
|
||||
arm_ccn_pmu_event_stop(event, PERF_EF_UPDATE);
|
||||
|
||||
arm_ccn_pmu_event_release(event);
|
||||
}
|
||||
|
||||
static void arm_ccn_pmu_event_read(struct perf_event *event)
|
||||
|
@ -1079,12 +1172,39 @@ static enum hrtimer_restart arm_ccn_pmu_timer_handler(struct hrtimer *hrtimer)
|
|||
}
|
||||
|
||||
|
||||
static int arm_ccn_pmu_cpu_notifier(struct notifier_block *nb,
|
||||
unsigned long action, void *hcpu)
|
||||
{
|
||||
struct arm_ccn_dt *dt = container_of(nb, struct arm_ccn_dt, cpu_nb);
|
||||
struct arm_ccn *ccn = container_of(dt, struct arm_ccn, dt);
|
||||
unsigned int cpu = (long)hcpu; /* for (long) see kernel/cpu.c */
|
||||
unsigned int target;
|
||||
|
||||
switch (action & ~CPU_TASKS_FROZEN) {
|
||||
case CPU_DOWN_PREPARE:
|
||||
if (!cpumask_test_and_clear_cpu(cpu, &dt->cpu))
|
||||
break;
|
||||
target = cpumask_any_but(cpu_online_mask, cpu);
|
||||
if (target < 0)
|
||||
break;
|
||||
perf_pmu_migrate_context(&dt->pmu, cpu, target);
|
||||
cpumask_set_cpu(target, &dt->cpu);
|
||||
WARN_ON(irq_set_affinity(ccn->irq, &dt->cpu) != 0);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return NOTIFY_OK;
|
||||
}
|
||||
|
||||
|
||||
static DEFINE_IDA(arm_ccn_pmu_ida);
|
||||
|
||||
static int arm_ccn_pmu_init(struct arm_ccn *ccn)
|
||||
{
|
||||
int i;
|
||||
char *name;
|
||||
int err;
|
||||
|
||||
/* Initialize DT subsystem */
|
||||
ccn->dt.base = ccn->base + CCN_REGION_SIZE;
|
||||
|
@ -1136,20 +1256,58 @@ static int arm_ccn_pmu_init(struct arm_ccn *ccn)
|
|||
};
|
||||
|
||||
/* No overflow interrupt? Have to use a timer instead. */
|
||||
if (!ccn->irq_used) {
|
||||
if (!ccn->irq) {
|
||||
dev_info(ccn->dev, "No access to interrupts, using timer.\n");
|
||||
hrtimer_init(&ccn->dt.hrtimer, CLOCK_MONOTONIC,
|
||||
HRTIMER_MODE_REL);
|
||||
ccn->dt.hrtimer.function = arm_ccn_pmu_timer_handler;
|
||||
}
|
||||
|
||||
return perf_pmu_register(&ccn->dt.pmu, name, -1);
|
||||
/* Pick one CPU which we will use to collect data from CCN... */
|
||||
cpumask_set_cpu(smp_processor_id(), &ccn->dt.cpu);
|
||||
|
||||
/*
|
||||
* ... and change the selection when it goes offline. Priority is
|
||||
* picked to have a chance to migrate events before perf is notified.
|
||||
*/
|
||||
ccn->dt.cpu_nb.notifier_call = arm_ccn_pmu_cpu_notifier;
|
||||
ccn->dt.cpu_nb.priority = CPU_PRI_PERF + 1,
|
||||
err = register_cpu_notifier(&ccn->dt.cpu_nb);
|
||||
if (err)
|
||||
goto error_cpu_notifier;
|
||||
|
||||
/* Also make sure that the overflow interrupt is handled by this CPU */
|
||||
if (ccn->irq) {
|
||||
err = irq_set_affinity(ccn->irq, &ccn->dt.cpu);
|
||||
if (err) {
|
||||
dev_err(ccn->dev, "Failed to set interrupt affinity!\n");
|
||||
goto error_set_affinity;
|
||||
}
|
||||
}
|
||||
|
||||
err = perf_pmu_register(&ccn->dt.pmu, name, -1);
|
||||
if (err)
|
||||
goto error_pmu_register;
|
||||
|
||||
return 0;
|
||||
|
||||
error_pmu_register:
|
||||
error_set_affinity:
|
||||
unregister_cpu_notifier(&ccn->dt.cpu_nb);
|
||||
error_cpu_notifier:
|
||||
ida_simple_remove(&arm_ccn_pmu_ida, ccn->dt.id);
|
||||
for (i = 0; i < ccn->num_xps; i++)
|
||||
writel(0, ccn->xp[i].base + CCN_XP_DT_CONTROL);
|
||||
writel(0, ccn->dt.base + CCN_DT_PMCR);
|
||||
return err;
|
||||
}
|
||||
|
||||
static void arm_ccn_pmu_cleanup(struct arm_ccn *ccn)
|
||||
{
|
||||
int i;
|
||||
|
||||
irq_set_affinity(ccn->irq, cpu_possible_mask);
|
||||
unregister_cpu_notifier(&ccn->dt.cpu_nb);
|
||||
for (i = 0; i < ccn->num_xps; i++)
|
||||
writel(0, ccn->xp[i].base + CCN_XP_DT_CONTROL);
|
||||
writel(0, ccn->dt.base + CCN_DT_PMCR);
|
||||
|
@ -1285,6 +1443,7 @@ static int arm_ccn_probe(struct platform_device *pdev)
|
|||
{
|
||||
struct arm_ccn *ccn;
|
||||
struct resource *res;
|
||||
unsigned int irq;
|
||||
int err;
|
||||
|
||||
ccn = devm_kzalloc(&pdev->dev, sizeof(*ccn), GFP_KERNEL);
|
||||
|
@ -1309,6 +1468,7 @@ static int arm_ccn_probe(struct platform_device *pdev)
|
|||
res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
|
||||
if (!res)
|
||||
return -EINVAL;
|
||||
irq = res->start;
|
||||
|
||||
/* Check if we can use the interrupt */
|
||||
writel(CCN_MN_ERRINT_STATUS__PMU_EVENTS__DISABLE,
|
||||
|
@ -1318,13 +1478,12 @@ static int arm_ccn_probe(struct platform_device *pdev)
|
|||
/* Can set 'disable' bits, so can acknowledge interrupts */
|
||||
writel(CCN_MN_ERRINT_STATUS__PMU_EVENTS__ENABLE,
|
||||
ccn->base + CCN_MN_ERRINT_STATUS);
|
||||
err = devm_request_irq(ccn->dev, res->start,
|
||||
arm_ccn_irq_handler, 0, dev_name(ccn->dev),
|
||||
ccn);
|
||||
err = devm_request_irq(ccn->dev, irq, arm_ccn_irq_handler, 0,
|
||||
dev_name(ccn->dev), ccn);
|
||||
if (err)
|
||||
return err;
|
||||
|
||||
ccn->irq_used = 1;
|
||||
ccn->irq = irq;
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue