上周碰到的问题是,在将ctp驱动由先前的下降沿触发改为低电平触发中断时发现中断回调函数不能被调用。分析原因后得到的结论是中断注册注册的时机不对。先前的中断注册发生在ctp上电之前,当中断注册完成后立刻执行上电动作,此时的中断引脚会由低电平变为高电平。这也就意味着如果我使用低电平触发中断的话,那么ctp上电前的中断引脚一定是低电平的,而中断注册又发生在上电之前,这必然会导致在上电前就会发生中断。
另外,在中断处理函数中首先会disable 中断,而在底半部(工作队列)执行结束后才会enable中断。这样看来似乎不会有什么错误。的确,这里的disable和enable的匹配是完整的。但是由于某个原因我画蛇添足的在中断注册完毕后立刻调用了disable中断而且还调用了cancel_work_sync函数,紧接着又调用了enable中断。本来这样做的目的只是为了在中断注册成功后避免中断处理函数被调用。但是这适得其反,根本没有起到作用,而且还导致一个很难发现的隐蔽错误!!
分析一下:中断注册完成后,由于还没有给ctp上电,所以此时的中断引脚是低电平(我注册的中断是采用低电平触发的),所以此时中断处理函数立刻被调用,在中断处理函数中首先disable中断,然后调用queue_work将工作转移到底半部,中断处理函数退出。本来只要这个queue_work被执行了,那么中断的enable也就会被执行,那就没有什么问题会发生了,可是鬼使神差的我却在前面说的中断注册完成后立刻调用了disable中断并调用了cancel_work_sync。这个cancel_work_sync函数调用后立刻会取消工作队列中的未完成的内容,这就会导致上面的那个queue_work无法执行完毕,并导致其后的enable中断动作没有机会执行了。继而导致接下来的中断没有办法被系统捕获(因为中断没有被enable)确切的说是因为enable和disable的调用次数比匹配造成的。再明白点儿就是因为disable的调用次数大于enable的调用次数,从而导致中断处于disable状态。
解决方案:request_irq的调用应该放到所有的准备工作都完成后再进行!我们应该确保只有在你有能力处理中断的时候在去注册中断!因为只要request_irq正确返回就意味着可以响应中断了,你想要在该注册函数返回后立刻调用disable_irq来暂时屏蔽中断,然后在后面的某个时刻在enable中断是不对的!因为在你disable_irq之前可能中断已经发生并且调用了中断处理函数!所以不要试图在request_irq之后采用立刻调用disable的方式来达到你的目的。这样做会有潜质的危险。!!!
一般对中断的注册都放在probe的最后一步来做!这样可以避免一些中断过早被调用的问题!
另外一个值得注意的问题就是linux在irq的架构当中实现了enable和disable的匹配使用方法,这意味着你的每一次disable都需要有一个enable被调用。也就是要成对使用!你可以连续5次调用disable函数来disable一个中断,这不会有任何问题,但是,接下来你想要enable这个中断的时候,你必然要调用5次enable才能打开这个中断,并且如果因为你的疏忽致使enable的次数大于disable的次数,kernel还会给你打印一个unbalanced的信息!
最后做一个小总结:如果你打算disable一个中断,并打算在某种情况下重新enable它,那你一定要谨慎的做,以确保它在你需要的时候一定被enable了。一般我们会在中断处理函数进去的时候disable一个irq,并在中断处理函数退出的时候enable这个irq。这样做当然能让disable和enable匹配的很好了。而且中断处理函数一般都很短小,也能让这二者的匹配调用得到保证。但有的时候你可能想把disable放到中断当中,而把enable放到底半部去完成。这是就要慎重了,你要仔细检查你的底半部的代码是否一定会在你需要的时候被执行,稍有不慎你可能就会造成底半部的enable未被执行,就像我前面提到的调用cancel_work_sync会导致底半部执行被取消,从而导致缺少一次匹配的enable,继而导致中断不可用。
顺便说一下,flush_workqueue函数可以确保让堆积在底半部的工作完全被执行,你可以酌情使用这个函数完成你期望的动作。