第四章 麒麟芯片中断与定时器驱动:从GIC到hrtimer
大家好,我是你们的老朋友。今天我们来聊聊麒麟芯片里一个绕不开的话题——中断与定时器。说实话,我刚入行那会儿,觉得中断就是“CPU被叫停,去干别的事”,但真正上手写驱动才发现,这里面的门道深着呢。尤其是麒麟芯片用的GIC(通用中断控制器),跟ARM标准GIC还有不少差异,踩坑是常有的事。
这一章,我会从GIC基础讲起,带你走一遍中断注册与处理的完整流程,再聊聊tasklet和workqueue这对“好兄弟”,最后落到内核定时器和hrtimer的驱动实现上。嗯,内容不少,但都是干货。
4.1 中断控制器(GIC)基础
GIC,全称Generic Interrupt Controller,是ARM架构下中断管理的核心。麒麟芯片用的GIC版本通常是GIC-400或GIC-500系列,支持SPI(共享外设中断)、PPI(私有外设中断)和SGI(软件生成中断)。
我个人习惯把GIC理解成一个“总机接线员”。外设要通知CPU,先打给GIC,GIC再根据优先级和亲和性,把电话转给合适的CPU核心。你想想看,如果没有这个接线员,所有外设直接喊CPU,那CPU得疯掉。
GIC的寄存器空间分为两部分:Distributor(分发器)和CPU Interface(CPU接口)。Distributor负责全局中断管理,比如使能、优先级、路由;CPU Interface则负责把中断信号送给具体哪个CPU核。
举个例子,你要使能一个SPI中断,需要操作GICD_ISENABLER寄存器。代码大概长这样:
// 使能SPI中断号32
#define GICD_BASE 0x2C001000
#define GICD_ISENABLER (GICD_BASE + 0x100)
void gic_enable_spi_irq(unsigned int irq_num) {
unsigned int reg_offset = (irq_num / 32) * 4;
unsigned int bit_mask = 1 << (irq_num % 32);
writel(bit_mask, GICD_ISENABLER + reg_offset);
}
这里要注意,GIC的中断号是从0开始的,但Linux内核里IRQ号通常从16开始(前16个留给异常)。所以你在写驱动时,记得做偏移映射。我曾经因为没做这个映射,调试了整整一个下午,最后发现是中断号对不上。
4.2 中断注册与处理流程
在Linux内核里,注册中断的标准接口是request_irq()。但麒麟芯片的驱动开发中,我更推荐用devm_request_irq()——它能自动管理资源,省得你忘了释放。
中断处理流程,说白了就是三步:
- 中断发生:外设拉高中断线,GIC检测到后,查找路由表,决定发给哪个CPU。
- CPU响应:CPU保存现场,跳转到异常向量表,进入内核的中断处理入口。
- 执行handler:内核根据IRQ号,调用你注册的中断处理函数。
这里有个坑:中断处理函数是在中断上下文中执行的,不能睡眠,不能调用可能引起阻塞的函数。我刚开始写驱动时,在中断处理里调了printk,结果系统直接死锁——因为printk内部用了自旋锁。
来看一个典型的中断注册示例:
static irqreturn_t my_irq_handler(int irq, void *dev_id) {
// 快速处理:读取状态寄存器,清除中断标志
unsigned int status = readl(MY_DEV_STATUS);
if (status & MY_DEV_IRQ_FLAG) {
writel(MY_DEV_IRQ_FLAG, MY_DEV_CLEAR);
// 调度下半部
tasklet_schedule(&my_tasklet);
return IRQ_HANDLED;
}
return IRQ_NONE;
}
static int my_probe(struct platform_device *pdev) {
int irq = platform_get_irq(pdev, 0);
if (irq < 0) return irq;
return devm_request_irq(&pdev->dev, irq, my_irq_handler,
IRQF_TRIGGER_RISING, "my_device", NULL);
}
注意IRQF_TRIGGER_RISING这个标志。麒麟芯片的GPIO中断,触发方式一定要跟硬件设计匹配。我曾经遇到一个案子,硬件工程师说用上升沿触发,结果实际是下降沿——板子一上电就疯狂进中断,差点烧了电机。
4.3 tasklet与workqueue:下半部的两种选择
中断处理分上半部和下半部。上半部处理紧急的、必须关中断做的事;下半部处理那些可以慢慢来的事。tasklet和workqueue就是下半部的两种实现。
tasklet:基于软中断实现,执行在中断上下文中,不能睡眠。它的特点是快,但限制多。我一般用它来处理简单的、确定性的工作,比如清除标志位、更新统计计数。
workqueue:基于内核线程实现,执行在进程上下文中,可以睡眠。适合做复杂操作,比如I2C读写、内存分配、文件操作。
怎么选?我有个简单的判断标准:
- 如果下半部要做的事,能在几十微秒内完成,用tasklet。
- 如果需要等待硬件响应、或者要调用可能阻塞的函数,用workqueue。
tasklet的使用示例:
// 定义tasklet
static void my_tasklet_handler(unsigned long data) {
// 这里做下半部处理,不能睡眠
struct my_device *dev = (struct my_device *)data;
dev->stats.irq_count++;
}
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, (unsigned long)&my_dev);
// 在上半部中调度
tasklet_schedule(&my_tasklet);
workqueue的使用示例:
// 定义work
static void my_work_handler(struct work_struct *work) {
struct my_device *dev = container_of(work, struct my_device, my_work);
// 这里可以睡眠,比如读取传感器数据
i2c_smbus_read_byte_data(dev->client, dev->reg);
}
// 初始化work
INIT_WORK(&my_dev.my_work, my_work_handler);
// 在上半部中调度
schedule_work(&my_dev.my_work);
嗯,这里要提醒一下:workqueue默认使用系统全局的system_wq,但如果你的驱动对实时性有要求,建议创建自己的workqueue。麒麟芯片的多核架构下,你可以把workqueue绑定到特定CPU上,避免缓存抖动。
4.4 内核定时器与高精度定时器(hrtimer)
定时器驱动,说白了就是让内核在未来的某个时间点执行一段代码。Linux内核提供了两种定时器:传统的timer_list和现代的hrtimer。
传统内核定时器基于jiffies,精度受限于HZ值。比如HZ=100时,精度只有10ms。对于大多数外设(比如按键消抖、LED闪烁),这个精度够用了。
来看一个简单的timer_list示例:
static void my_timer_callback(struct timer_list *t) {
struct my_device *dev = from_timer(dev, t, my_timer);
// 定时到后执行的操作
gpio_set_value(dev->led_pin, !gpio_get_value(dev->led_pin));
// 重新启动定时器,实现周期性闪烁
mod_timer(&dev->my_timer, jiffies + msecs_to_jiffies(500));
}
// 初始化定时器
timer_setup(&dev->my_timer, my_timer_callback, 0);
mod_timer(&dev->my_timer, jiffies + msecs_to_jiffies(500));
但如果你需要微秒甚至纳秒级的精度,传统定时器就不够用了。这时候要用hrtimer(高精度定时器)。hrtimer基于ktime,精度可以达到纳秒级,底层依赖硬件定时器(比如ARM的Generic Timer)。
麒麟芯片的hrtimer驱动实现,我建议直接使用hrtimer_start()接口。注意,hrtimer的回调函数运行在软中断上下文中,不能睡眠。
static enum hrtimer_restart my_hrtimer_callback(struct hrtimer *hrt) {
struct my_device *dev = container_of(hrt, struct my_device, my_hrtimer);
// 高精度定时任务
dev->sample_counter++;
// 如果需要周期性触发,返回HRTIMER_RESTART并设置下次时间
hrtimer_forward_now(hrt, ktime_set(0, 1000000)); // 1ms周期
return HRTIMER_RESTART;
}
// 初始化hrtimer
hrtimer_init(&dev->my_hrtimer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
dev->my_hrtimer.function = my_hrtimer_callback;
hrtimer_start(&dev->my_hrtimer, ktime_set(0, 1000000), HRTIMER_MODE_REL);
printk,结果导致系统响应延迟飙升——因为printk内部会尝试获取控制台锁,而那个锁可能被其他进程持有。
另外,hrtimer的时钟源选择也很重要。麒麟芯片通常支持CLOCK_MONOTONIC和CLOCK_REALTIME。我个人习惯用CLOCK_MONOTONIC,因为它不受系统时间调整的影响,适合做周期性任务。
小结
这一章我们聊了GIC的基础配置、中断注册与处理流程、下半部的两种实现方式,以及定时器驱动的两种选择。说实话,中断和定时器是嵌入式驱动开发中最容易出问题的部分——不是代码写不对,而是对硬件行为理解不到位。
下一章,我们会深入麒麟芯片的DMA驱动,看看怎么用DMA实现零拷贝数据传输。嗯,那又是一个充满坑的世界,但我会把踩过的坑都告诉你。
公众号:蓝海资料掘金营,微信deep3321