| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Copyright (C) 2012 Texas Instruments |
| * Author: Tomi Valkeinen <tomi.valkeinen@ti.com> |
| */ |
| |
| #define DSS_SUBSYS_NAME "APPLY" |
| |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/slab.h> |
| #include <linux/spinlock.h> |
| #include <linux/jiffies.h> |
| #include <linux/delay.h> |
| #include <linux/interrupt.h> |
| #include <linux/seq_file.h> |
| |
| #include <video/omapfb_dss.h> |
| |
| #include "dss.h" |
| #include "dss_features.h" |
| #include "dispc-compat.h" |
| |
| #define DISPC_IRQ_MASK_ERROR (DISPC_IRQ_GFX_FIFO_UNDERFLOW | \ |
| DISPC_IRQ_OCP_ERR | \ |
| DISPC_IRQ_VID1_FIFO_UNDERFLOW | \ |
| DISPC_IRQ_VID2_FIFO_UNDERFLOW | \ |
| DISPC_IRQ_SYNC_LOST | \ |
| DISPC_IRQ_SYNC_LOST_DIGIT) |
| |
| #define DISPC_MAX_NR_ISRS 8 |
| |
| struct omap_dispc_isr_data { |
| omap_dispc_isr_t isr; |
| void *arg; |
| u32 mask; |
| }; |
| |
| struct dispc_irq_stats { |
| unsigned long last_reset; |
| unsigned irq_count; |
| unsigned irqs[32]; |
| }; |
| |
| static struct { |
| spinlock_t irq_lock; |
| u32 irq_error_mask; |
| struct omap_dispc_isr_data registered_isr[DISPC_MAX_NR_ISRS]; |
| u32 error_irqs; |
| struct work_struct error_work; |
| |
| #ifdef CONFIG_FB_OMAP2_DSS_COLLECT_IRQ_STATS |
| spinlock_t irq_stats_lock; |
| struct dispc_irq_stats irq_stats; |
| #endif |
| } dispc_compat; |
| |
| |
| #ifdef CONFIG_FB_OMAP2_DSS_COLLECT_IRQ_STATS |
| static void dispc_dump_irqs(struct seq_file *s) |
| { |
| unsigned long flags; |
| struct dispc_irq_stats stats; |
| |
| spin_lock_irqsave(&dispc_compat.irq_stats_lock, flags); |
| |
| stats = dispc_compat.irq_stats; |
| memset(&dispc_compat.irq_stats, 0, sizeof(dispc_compat.irq_stats)); |
| dispc_compat.irq_stats.last_reset = jiffies; |
| |
| spin_unlock_irqrestore(&dispc_compat.irq_stats_lock, flags); |
| |
| seq_printf(s, "period %u ms\n", |
| jiffies_to_msecs(jiffies - stats.last_reset)); |
| |
| seq_printf(s, "irqs %d\n", stats.irq_count); |
| #define PIS(x) \ |
| seq_printf(s, "%-20s %10d\n", #x, stats.irqs[ffs(DISPC_IRQ_##x)-1]) |
| |
| PIS(FRAMEDONE); |
| PIS(VSYNC); |
| PIS(EVSYNC_EVEN); |
| PIS(EVSYNC_ODD); |
| PIS(ACBIAS_COUNT_STAT); |
| PIS(PROG_LINE_NUM); |
| PIS(GFX_FIFO_UNDERFLOW); |
| PIS(GFX_END_WIN); |
| PIS(PAL_GAMMA_MASK); |
| PIS(OCP_ERR); |
| PIS(VID1_FIFO_UNDERFLOW); |
| PIS(VID1_END_WIN); |
| PIS(VID2_FIFO_UNDERFLOW); |
| PIS(VID2_END_WIN); |
| if (dss_feat_get_num_ovls() > 3) { |
| PIS(VID3_FIFO_UNDERFLOW); |
| PIS(VID3_END_WIN); |
| } |
| PIS(SYNC_LOST); |
| PIS(SYNC_LOST_DIGIT); |
| PIS(WAKEUP); |
| if (dss_has_feature(FEAT_MGR_LCD2)) { |
| PIS(FRAMEDONE2); |
| PIS(VSYNC2); |
| PIS(ACBIAS_COUNT_STAT2); |
| PIS(SYNC_LOST2); |
| } |
| if (dss_has_feature(FEAT_MGR_LCD3)) { |
| PIS(FRAMEDONE3); |
| PIS(VSYNC3); |
| PIS(ACBIAS_COUNT_STAT3); |
| PIS(SYNC_LOST3); |
| } |
| #undef PIS |
| } |
| #endif |
| |
| /* dispc.irq_lock has to be locked by the caller */ |
| static void _omap_dispc_set_irqs(void) |
| { |
| u32 mask; |
| int i; |
| struct omap_dispc_isr_data *isr_data; |
| |
| mask = dispc_compat.irq_error_mask; |
| |
| for (i = 0; i < DISPC_MAX_NR_ISRS; i++) { |
| isr_data = &dispc_compat.registered_isr[i]; |
| |
| if (isr_data->isr == NULL) |
| continue; |
| |
| mask |= isr_data->mask; |
| } |
| |
| dispc_write_irqenable(mask); |
| } |
| |
| int omap_dispc_register_isr(omap_dispc_isr_t isr, void *arg, u32 mask) |
| { |
| int i; |
| int ret; |
| unsigned long flags; |
| struct omap_dispc_isr_data *isr_data; |
| |
| if (isr == NULL) |
| return -EINVAL; |
| |
| spin_lock_irqsave(&dispc_compat.irq_lock, flags); |
| |
| /* check for duplicate entry */ |
| for (i = 0; i < DISPC_MAX_NR_ISRS; i++) { |
| isr_data = &dispc_compat.registered_isr[i]; |
| if (isr_data->isr == isr && isr_data->arg == arg && |
| isr_data->mask == mask) { |
| ret = -EINVAL; |
| goto err; |
| } |
| } |
| |
| isr_data = NULL; |
| ret = -EBUSY; |
| |
| for (i = 0; i < DISPC_MAX_NR_ISRS; i++) { |
| isr_data = &dispc_compat.registered_isr[i]; |
| |
| if (isr_data->isr != NULL) |
| continue; |
| |
| isr_data->isr = isr; |
| isr_data->arg = arg; |
| isr_data->mask = mask; |
| ret = 0; |
| |
| break; |
| } |
| |
| if (ret) |
| goto err; |
| |
| _omap_dispc_set_irqs(); |
| |
| spin_unlock_irqrestore(&dispc_compat.irq_lock, flags); |
| |
| return 0; |
| err: |
| spin_unlock_irqrestore(&dispc_compat.irq_lock, flags); |
| |
| return ret; |
| } |
| EXPORT_SYMBOL(omap_dispc_register_isr); |
| |
| int omap_dispc_unregister_isr(omap_dispc_isr_t isr, void *arg, u32 mask) |
| { |
| int i; |
| unsigned long flags; |
| int ret = -EINVAL; |
| struct omap_dispc_isr_data *isr_data; |
| |
| spin_lock_irqsave(&dispc_compat.irq_lock, flags); |
| |
| for (i = 0; i < DISPC_MAX_NR_ISRS; i++) { |
| isr_data = &dispc_compat.registered_isr[i]; |
| if (isr_data->isr != isr || isr_data->arg != arg || |
| isr_data->mask != mask) |
| continue; |
| |
| /* found the correct isr */ |
| |
| isr_data->isr = NULL; |
| isr_data->arg = NULL; |
| isr_data->mask = 0; |
| |
| ret = 0; |
| break; |
| } |
| |
| if (ret == 0) |
| _omap_dispc_set_irqs(); |
| |
| spin_unlock_irqrestore(&dispc_compat.irq_lock, flags); |
| |
| return ret; |
| } |
| EXPORT_SYMBOL(omap_dispc_unregister_isr); |
| |
| static void print_irq_status(u32 status) |
| { |
| if ((status & dispc_compat.irq_error_mask) == 0) |
| return; |
| |
| #define PIS(x) (status & DISPC_IRQ_##x) ? (#x " ") : "" |
| |
| pr_debug("DISPC IRQ: 0x%x: %s%s%s%s%s%s%s%s%s\n", |
| status, |
| PIS(OCP_ERR), |
| PIS(GFX_FIFO_UNDERFLOW), |
| PIS(VID1_FIFO_UNDERFLOW), |
| PIS(VID2_FIFO_UNDERFLOW), |
| dss_feat_get_num_ovls() > 3 ? PIS(VID3_FIFO_UNDERFLOW) : "", |
| PIS(SYNC_LOST), |
| PIS(SYNC_LOST_DIGIT), |
| dss_has_feature(FEAT_MGR_LCD2) ? PIS(SYNC_LOST2) : "", |
| dss_has_feature(FEAT_MGR_LCD3) ? PIS(SYNC_LOST3) : ""); |
| #undef PIS |
| } |
| |
| /* Called from dss.c. Note that we don't touch clocks here, |
| * but we presume they are on because we got an IRQ. However, |
| * an irq handler may turn the clocks off, so we may not have |
| * clock later in the function. */ |
| static irqreturn_t omap_dispc_irq_handler(int irq, void *arg) |
| { |
| int i; |
| u32 irqstatus, irqenable; |
| u32 handledirqs = 0; |
| u32 unhandled_errors; |
| struct omap_dispc_isr_data *isr_data; |
| struct omap_dispc_isr_data registered_isr[DISPC_MAX_NR_ISRS]; |
| |
| spin_lock(&dispc_compat.irq_lock); |
| |
| irqstatus = dispc_read_irqstatus(); |
| irqenable = dispc_read_irqenable(); |
| |
| /* IRQ is not for us */ |
| if (!(irqstatus & irqenable)) { |
| spin_unlock(&dispc_compat.irq_lock); |
| return IRQ_NONE; |
| } |
| |
| #ifdef CONFIG_FB_OMAP2_DSS_COLLECT_IRQ_STATS |
| spin_lock(&dispc_compat.irq_stats_lock); |
| dispc_compat.irq_stats.irq_count++; |
| dss_collect_irq_stats(irqstatus, dispc_compat.irq_stats.irqs); |
| spin_unlock(&dispc_compat.irq_stats_lock); |
| #endif |
| |
| print_irq_status(irqstatus); |
| |
| /* Ack the interrupt. Do it here before clocks are possibly turned |
| * off */ |
| dispc_clear_irqstatus(irqstatus); |
| /* flush posted write */ |
| dispc_read_irqstatus(); |
| |
| /* make a copy and unlock, so that isrs can unregister |
| * themselves */ |
| memcpy(registered_isr, dispc_compat.registered_isr, |
| sizeof(registered_isr)); |
| |
| spin_unlock(&dispc_compat.irq_lock); |
| |
| for (i = 0; i < DISPC_MAX_NR_ISRS; i++) { |
| isr_data = ®istered_isr[i]; |
| |
| if (!isr_data->isr) |
| continue; |
| |
| if (isr_data->mask & irqstatus) { |
| isr_data->isr(isr_data->arg, irqstatus); |
| handledirqs |= isr_data->mask; |
| } |
| } |
| |
| spin_lock(&dispc_compat.irq_lock); |
| |
| unhandled_errors = irqstatus & ~handledirqs & dispc_compat.irq_error_mask; |
| |
| if (unhandled_errors) { |
| dispc_compat.error_irqs |= unhandled_errors; |
| |
| dispc_compat.irq_error_mask &= ~unhandled_errors; |
| _omap_dispc_set_irqs(); |
| |
| schedule_work(&dispc_compat.error_work); |
| } |
| |
| spin_unlock(&dispc_compat.irq_lock); |
| |
| return IRQ_HANDLED; |
| } |
| |
| static void dispc_error_worker(struct work_struct *work) |
| { |
| int i; |
| u32 errors; |
| unsigned long flags; |
| static const unsigned fifo_underflow_bits[] = { |
| DISPC_IRQ_GFX_FIFO_UNDERFLOW, |
| DISPC_IRQ_VID1_FIFO_UNDERFLOW, |
| DISPC_IRQ_VID2_FIFO_UNDERFLOW, |
| DISPC_IRQ_VID3_FIFO_UNDERFLOW, |
| }; |
| |
| spin_lock_irqsave(&dispc_compat.irq_lock, flags); |
| errors = dispc_compat.error_irqs; |
| dispc_compat.error_irqs = 0; |
| spin_unlock_irqrestore(&dispc_compat.irq_lock, flags); |
| |
| dispc_runtime_get(); |
| |
| for (i = 0; i < omap_dss_get_num_overlays(); ++i) { |
| struct omap_overlay *ovl; |
| unsigned bit; |
| |
| ovl = omap_dss_get_overlay(i); |
| bit = fifo_underflow_bits[i]; |
| |
| if (bit & errors) { |
| DSSERR("FIFO UNDERFLOW on %s, disabling the overlay\n", |
| ovl->name); |
| ovl->disable(ovl); |
| msleep(50); |
| } |
| } |
| |
| for (i = 0; i < omap_dss_get_num_overlay_managers(); ++i) { |
| struct omap_overlay_manager *mgr; |
| unsigned bit; |
| |
| mgr = omap_dss_get_overlay_manager(i); |
| bit = dispc_mgr_get_sync_lost_irq(i); |
| |
| if (bit & errors) { |
| int j; |
| |
| DSSERR("SYNC_LOST on channel %s, restarting the output " |
| "with video overlays disabled\n", |
| mgr->name); |
| |
| dss_mgr_disable(mgr); |
| |
| for (j = 0; j < omap_dss_get_num_overlays(); ++j) { |
| struct omap_overlay *ovl; |
| ovl = omap_dss_get_overlay(j); |
| |
| if (ovl->id != OMAP_DSS_GFX && |
| ovl->manager == mgr) |
| ovl->disable(ovl); |
| } |
| |
| dss_mgr_enable(mgr); |
| } |
| } |
| |
| if (errors & DISPC_IRQ_OCP_ERR) { |
| DSSERR("OCP_ERR\n"); |
| for (i = 0; i < omap_dss_get_num_overlay_managers(); ++i) { |
| struct omap_overlay_manager *mgr; |
| |
| mgr = omap_dss_get_overlay_manager(i); |
| dss_mgr_disable(mgr); |
| } |
| } |
| |
| spin_lock_irqsave(&dispc_compat.irq_lock, flags); |
| dispc_compat.irq_error_mask |= errors; |
| _omap_dispc_set_irqs(); |
| spin_unlock_irqrestore(&dispc_compat.irq_lock, flags); |
| |
| dispc_runtime_put(); |
| } |
| |
| int dss_dispc_initialize_irq(void) |
| { |
| int r; |
| |
| #ifdef CONFIG_FB_OMAP2_DSS_COLLECT_IRQ_STATS |
| spin_lock_init(&dispc_compat.irq_stats_lock); |
| dispc_compat.irq_stats.last_reset = jiffies; |
| dss_debugfs_create_file("dispc_irq", dispc_dump_irqs); |
| #endif |
| |
| spin_lock_init(&dispc_compat.irq_lock); |
| |
| memset(dispc_compat.registered_isr, 0, |
| sizeof(dispc_compat.registered_isr)); |
| |
| dispc_compat.irq_error_mask = DISPC_IRQ_MASK_ERROR; |
| if (dss_has_feature(FEAT_MGR_LCD2)) |
| dispc_compat.irq_error_mask |= DISPC_IRQ_SYNC_LOST2; |
| if (dss_has_feature(FEAT_MGR_LCD3)) |
| dispc_compat.irq_error_mask |= DISPC_IRQ_SYNC_LOST3; |
| if (dss_feat_get_num_ovls() > 3) |
| dispc_compat.irq_error_mask |= DISPC_IRQ_VID3_FIFO_UNDERFLOW; |
| |
| /* |
| * there's SYNC_LOST_DIGIT waiting after enabling the DSS, |
| * so clear it |
| */ |
| dispc_clear_irqstatus(dispc_read_irqstatus()); |
| |
| INIT_WORK(&dispc_compat.error_work, dispc_error_worker); |
| |
| _omap_dispc_set_irqs(); |
| |
| r = dispc_request_irq(omap_dispc_irq_handler, &dispc_compat); |
| if (r) { |
| DSSERR("dispc_request_irq failed\n"); |
| return r; |
| } |
| |
| return 0; |
| } |
| |
| void dss_dispc_uninitialize_irq(void) |
| { |
| dispc_free_irq(&dispc_compat); |
| } |
| |
| static void dispc_mgr_disable_isr(void *data, u32 mask) |
| { |
| struct completion *compl = data; |
| complete(compl); |
| } |
| |
| static void dispc_mgr_enable_lcd_out(enum omap_channel channel) |
| { |
| dispc_mgr_enable(channel, true); |
| } |
| |
| static void dispc_mgr_disable_lcd_out(enum omap_channel channel) |
| { |
| DECLARE_COMPLETION_ONSTACK(framedone_compl); |
| int r; |
| u32 irq; |
| |
| if (!dispc_mgr_is_enabled(channel)) |
| return; |
| |
| /* |
| * When we disable LCD output, we need to wait for FRAMEDONE to know |
| * that DISPC has finished with the LCD output. |
| */ |
| |
| irq = dispc_mgr_get_framedone_irq(channel); |
| |
| r = omap_dispc_register_isr(dispc_mgr_disable_isr, &framedone_compl, |
| irq); |
| if (r) |
| DSSERR("failed to register FRAMEDONE isr\n"); |
| |
| dispc_mgr_enable(channel, false); |
| |
| /* if we couldn't register for framedone, just sleep and exit */ |
| if (r) { |
| msleep(100); |
| return; |
| } |
| |
| if (!wait_for_completion_timeout(&framedone_compl, |
| msecs_to_jiffies(100))) |
| DSSERR("timeout waiting for FRAME DONE\n"); |
| |
| r = omap_dispc_unregister_isr(dispc_mgr_disable_isr, &framedone_compl, |
| irq); |
| if (r) |
| DSSERR("failed to unregister FRAMEDONE isr\n"); |
| } |
| |
| static void dispc_digit_out_enable_isr(void *data, u32 mask) |
| { |
| struct completion *compl = data; |
| |
| /* ignore any sync lost interrupts */ |
| if (mask & (DISPC_IRQ_EVSYNC_EVEN | DISPC_IRQ_EVSYNC_ODD)) |
| complete(compl); |
| } |
| |
| static void dispc_mgr_enable_digit_out(void) |
| { |
| DECLARE_COMPLETION_ONSTACK(vsync_compl); |
| int r; |
| u32 irq_mask; |
| |
| if (dispc_mgr_is_enabled(OMAP_DSS_CHANNEL_DIGIT)) |
| return; |
| |
| /* |
| * Digit output produces some sync lost interrupts during the first |
| * frame when enabling. Those need to be ignored, so we register for the |
| * sync lost irq to prevent the error handler from triggering. |
| */ |
| |
| irq_mask = dispc_mgr_get_vsync_irq(OMAP_DSS_CHANNEL_DIGIT) | |
| dispc_mgr_get_sync_lost_irq(OMAP_DSS_CHANNEL_DIGIT); |
| |
| r = omap_dispc_register_isr(dispc_digit_out_enable_isr, &vsync_compl, |
| irq_mask); |
| if (r) { |
| DSSERR("failed to register %x isr\n", irq_mask); |
| return; |
| } |
| |
| dispc_mgr_enable(OMAP_DSS_CHANNEL_DIGIT, true); |
| |
| /* wait for the first evsync */ |
| if (!wait_for_completion_timeout(&vsync_compl, msecs_to_jiffies(100))) |
| DSSERR("timeout waiting for digit out to start\n"); |
| |
| r = omap_dispc_unregister_isr(dispc_digit_out_enable_isr, &vsync_compl, |
| irq_mask); |
| if (r) |
| DSSERR("failed to unregister %x isr\n", irq_mask); |
| } |
| |
| static void dispc_mgr_disable_digit_out(void) |
| { |
| DECLARE_COMPLETION_ONSTACK(framedone_compl); |
| int r, i; |
| u32 irq_mask; |
| int num_irqs; |
| |
| if (!dispc_mgr_is_enabled(OMAP_DSS_CHANNEL_DIGIT)) |
| return; |
| |
| /* |
| * When we disable the digit output, we need to wait for FRAMEDONE to |
| * know that DISPC has finished with the output. |
| */ |
| |
| irq_mask = dispc_mgr_get_framedone_irq(OMAP_DSS_CHANNEL_DIGIT); |
| num_irqs = 1; |
| |
| if (!irq_mask) { |
| /* |
| * omap 2/3 don't have framedone irq for TV, so we need to use |
| * vsyncs for this. |
| */ |
| |
| irq_mask = dispc_mgr_get_vsync_irq(OMAP_DSS_CHANNEL_DIGIT); |
| /* |
| * We need to wait for both even and odd vsyncs. Note that this |
| * is not totally reliable, as we could get a vsync interrupt |
| * before we disable the output, which leads to timeout in the |
| * wait_for_completion. |
| */ |
| num_irqs = 2; |
| } |
| |
| r = omap_dispc_register_isr(dispc_mgr_disable_isr, &framedone_compl, |
| irq_mask); |
| if (r) |
| DSSERR("failed to register %x isr\n", irq_mask); |
| |
| dispc_mgr_enable(OMAP_DSS_CHANNEL_DIGIT, false); |
| |
| /* if we couldn't register the irq, just sleep and exit */ |
| if (r) { |
| msleep(100); |
| return; |
| } |
| |
| for (i = 0; i < num_irqs; ++i) { |
| if (!wait_for_completion_timeout(&framedone_compl, |
| msecs_to_jiffies(100))) |
| DSSERR("timeout waiting for digit out to stop\n"); |
| } |
| |
| r = omap_dispc_unregister_isr(dispc_mgr_disable_isr, &framedone_compl, |
| irq_mask); |
| if (r) |
| DSSERR("failed to unregister %x isr\n", irq_mask); |
| } |
| |
| void dispc_mgr_enable_sync(enum omap_channel channel) |
| { |
| if (dss_mgr_is_lcd(channel)) |
| dispc_mgr_enable_lcd_out(channel); |
| else if (channel == OMAP_DSS_CHANNEL_DIGIT) |
| dispc_mgr_enable_digit_out(); |
| else |
| WARN_ON(1); |
| } |
| |
| void dispc_mgr_disable_sync(enum omap_channel channel) |
| { |
| if (dss_mgr_is_lcd(channel)) |
| dispc_mgr_disable_lcd_out(channel); |
| else if (channel == OMAP_DSS_CHANNEL_DIGIT) |
| dispc_mgr_disable_digit_out(); |
| else |
| WARN_ON(1); |
| } |
| |
| static inline void dispc_irq_wait_handler(void *data, u32 mask) |
| { |
| complete((struct completion *)data); |
| } |
| |
| int omap_dispc_wait_for_irq_interruptible_timeout(u32 irqmask, |
| unsigned long timeout) |
| { |
| |
| int r; |
| long time_left; |
| DECLARE_COMPLETION_ONSTACK(completion); |
| |
| r = omap_dispc_register_isr(dispc_irq_wait_handler, &completion, |
| irqmask); |
| |
| if (r) |
| return r; |
| |
| time_left = wait_for_completion_interruptible_timeout(&completion, |
| timeout); |
| |
| omap_dispc_unregister_isr(dispc_irq_wait_handler, &completion, irqmask); |
| |
| if (time_left == 0) |
| return -ETIMEDOUT; |
| |
| if (time_left == -ERESTARTSYS) |
| return -ERESTARTSYS; |
| |
| return 0; |
| } |