linux的中断学习

1. 前言

  这两天做项目用到中断,发现以前学的那点东西都忘光了,索性写个笔记回忆加强一下记忆!
  首先,中断分为硬中断和软中断。而软中断又细分为软中断、tasklet、工作队列;每种中断都有其优缺点,以及适用场景,具体情况看如下分析。

2. 中断的概念

2.1 中断的上半部和下半部

  要讲中断就要先讲清楚它工作的区域,中断的上半部和下半部。上半部是指中断处理程序;下半部是指一些虽然与中断有相关性但是可以延后执行的任务。(这里的上半部和下半部其实对应的就是硬中断和软中断,且下半部是由上半部调用来执行延时耗时操作的)。
  两者的主要区别在于:中断上半部不能被相同类型的中断打断,而下半部依然可以被中断打断;中断上半部对于时间非常敏感,而下半部基本上都是一些可以延迟的工作。由于二者的这种区别,所以对于一个工作是放在上半部还是放在下半部去执行,可以参考下面4条:
  如果一个任务对时间非常敏感,将其放在中断上半部处理程序中执行。
  如果一个任务和硬件相关,将其放在中断上半部处理程序中执行。
  如果一个任务要保证不被其他中断(特别是相同的中断)打断,将其放在中断上半部处理程序中执行。
  其他所有任务,考虑放在下半部去执行。

2.2 中断的触发流程

2.2.1 硬件层面

1.中断请求:
  外部设备(如键盘、网卡、触摸屏等)发生某些事件(键盘按下、数据到达、触摸屏被触摸)时,会通过中断请求线向cpu发出中断请求信号。
  中断控制器(如APIC、PIC)接收到中断请求信号,并将其排队处理。
2.中断控制器处理:
  中断控制器会根据优先级选择一个中断请求,将中断信号发给CPU。
  中断控制器会发送中断向量(中断类型编号)给CPU,告知是哪种中断。
3.CPU响应:
  CPU完成当前指令后,保存当前的程序计算器和处理器状态寄存器,进入中断处理模式。
  CPU禁用中断(或设置中断屏蔽)以避免嵌套中断,获取中断向量,判断中断类型

2.2.2 软件层面

1.中断向量表:
  根据中断向量,CPU查找中断向量表(IVT),找到对应的中断服务程序(ISR)的入口地址。
2.中断服务程序(ISR)执行:
  CPU跳转到中断服务程序入口,开始执行ISR。
  ISR会处理硬件设备的中断请求,如读取数据、清除中断标志等。
3.下半部处理:
  如果ISR执行时间较长或需要执行复杂操作,会将这些操作延迟到下半部处理。
  下半部处理机制包括软中断、tasklets和工作队列(workqueues),用于处理延迟任务。
4.恢复执行:
  ISR完成后,恢复之前保存的处理器状态和程序计数器。
  CPU重新启用中断(如果之前禁用了中断),继续执行被中断的程序。

2.2.3 流程图

[外部设备事件] --> [中断控制器收到中断请求] --> [中断控制器发送中断信号给CPU] --> [CPU保存状态并禁用中断] --> [CPU根据中断向量查找ISR] --> [ISR处理中断请求] --> [ISR延迟任务到下半部处理] --> [ISR完成后恢复状态] --> [CPU重新启用中断] --> [CPU继续执行被中断的程序]

3 中断的应用

3.1 硬中断

  从上文可知硬中断就是中断的上半部,用于执行不耗时的操作。
  在实际使用中我们申请的中断号(用reques_irq申请),注册中断绑定的中断函数都是硬中断。如下代码所示,触发中断后执行绑定的中断函数,整个过程就是硬中断的调用。

3.1.1 接口

struct device_node *of_find_compatible_node(struct device_node *from,
    const char *type, const char *compat);
 功能:通过compatible属性查找指定节点
 参数:
    @from   - 指向开始路径的节点,如果为NULL,则从根节点开始
    @type   - device_type设备类型,可以为NULL
    @compat - 指向节点的compatible属性的值(字符串)的⾸地址
 返回值:成功:得到节点的⾸地址;失败:NULL 

unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
功能:解析设备树并映射产生软中断号
参数:
     @dev:节点指针
     @index:interrupts后写的中断的描述的下标
返回值:成功返回软中断号,失败返回0

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
        const char *name, void *dev)
功能:注册中断
参数;
    @irq:软中断号 (以后在内核中使用的中断号都是软中断号)
    @handler:中断处理函数的指针
        irqreturn_t irq_handle(int irq, void *dev)
        {
            //中断处理函数,中断处理函数中不能做延时,耗时,甚至休眠的操作

            //return IRQ_NONE; //中断没有处理完成
            return IRQ_HANDLED;//中断执行成功,中断处理完成了
        }
    @flags:中断触发方式
            IRQF_TRIGGER_RISING   //上升沿触发
            IRQF_TRIGGER_FALLING  //下降沿触发
            IRQF_TRIGGER_HIGH     //高电平触发
            IRQF_TRIGGER_LOW      //低电平触发
            IRQF_SHARED           //共享中断
    @name:中断的名字
            cat /proc/interrupts
    @dev:向中断处理函数传递的参数
返回值:成功返回0,失败返回错误码    

const void *free_irq(unsigned int irq, void *dev_id)
功能:释放中断
参数:
     @irq:软中断号
     @dev_id:注册中断时候的第5个参数
返回值:返回devname
interrupt-parent = <&gpio3>;
interrupts = ;

  这是因为设备树的interrupts节点的第一个参数即是它的引脚编号也是它的中断号,既是103号中断(332+(08+7)转换方式看我文章)。
  通过request_irq函数,将设备树的中断号映射到内核获取到软中断号,再与硬中断的函数绑定。

3.1.2示例

#include 
#include 
#include 
#include 
#include 
/*
myirq{
    compatible = "aaaa,myirq";
    interrupt-parent = <&gpiof>;
    interrupts = <9 0>;
};
*/

struct device_node *node;
unsigned int irqno;
#define name "key1";
irqreturn_t key_irq_handle(int irq, void* dev)
{
    printk("执行中断逻辑\n");
    return IRQ_HANDLED;
}
static int __init myirq_init(void)
{
    int i, ret;
    // 1.获取设备树中的节点
    node = of_find_compatible_node(NULL, NULL, "aaaa,myirq");
    if (node == NULL) {
        printk("get node error\n");
        return -EINVAL;
    }
    // 2.映射得到软中断号
        irqno = irq_of_parse_and_map(node, 0);
        if (irqno == 0) {
            printk("get irqno error\n");
            return -EAGAIN;
        }

        // 3.注册中断
        ret = request_irq(irqno, key_irq_handle,
            IRQF_TRIGGER_FALLING, name, 0);
        if (ret) {
            printk("request irq error\n");
            return ret;
        }
    }

    return 0;
}
static void __exit myirq_exit(void)
{
        free_irq(irqno, 0);
}
module_init(myirq_init);
module_exit(myirq_exit);
MODULE_LICENSE("GPL");

3.2 软中断

  软中断是一组静态定义的下半部接口,可以在所有处理器上同时执行,即使两个类型相同也可以。
  但一个软中断不会抢占另一个软中断,唯一可以抢占软中断的是硬中断。
  目前Linux系统最多支持32个软中断,系统已经定义使用了10个,剩下的用户可以自己指定,但是看一下前面的说明!避免自己创建软中断,如果不是需要高频率的线程工作调度,一般来说系统提供的软中断已经够我们使用了,我们在日常开发中最好还是遵循系统给出的指导建议,避免出现异常,日常的学习调试,我们可以创建自己的软中断,加深对这方面知识的理解。上面列出的软中断类型越靠前优先级越高,其中有两个需要关注一下,就是HI_SOFTIRQ和TASKLET_SOFTIRQ,系统已经帮我们初始化好了,tasklet就是基于这两个软中断去实现的。
  这里简介一下软中断的添加流程,实际应用在还是以tasklet和工作队列为主。
1.添加我们自己的软中断

enum
{
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    BLOCK_IOPOLL_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ,
    MY_SOFTIRQ,      /*我自己添加的软中断*/ 
    RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */

    NR_SOFTIRQS
};

2.在kernel/softirq.c中定义自己的软中断处理函数

//我自己定义的软中断处理函数
static void my_softirq_action(struct softirq_action *a)
{
    ...
}

3.初始化

void __init softirq_init(void)
{
    int cpu;

    for_each_possible_cpu(cpu) {
        per_cpu(tasklet_vec, cpu).tail =
            &per_cpu(tasklet_vec, cpu).head;
        per_cpu(tasklet_hi_vec, cpu).tail =
            &per_cpu(tasklet_hi_vec, cpu).head;
    }

    open_softirq(TASKLET_SOFTIRQ, tasklet_action);
    open_softirq(HI_SOFTIRQ, tasklet_hi_action);
    open_softirq(MY_SOFTIRQ, tasklet_hi_action);//我自己定义的软中断
}

4.激活

raise_softirq(MY_SOFTIRQ);

  定义了软中断,那跟系统自带的软中断,它们在什么时候得到执行呢?我们来看一下 do_softirq函数:
  文件路径\:kernel/softirq.c

asmlinkage void do_softirq(void)
{
    __u32 pending;
    unsigned long flags;

    if (in_interrupt())//判断当前是否处于中断状态
        return;

    local_irq_save(flags);//保存中断标记

    pending = local_softirq_pending();

    if (pending)//循环处理已经注册的软中断
        __do_softirq();

    local_irq_restore(flags);
}

  这种方式我没有使用过,虽说效率最高,但调用流程比较复杂,推荐使用其他的软中断。

3.3 tasklet中断

  tasklet是基于软中断的机制实现的

3.3.1 接口

1.分配对象
    struct tasklet_struct
    {
        struct tasklet_struct *next; //tasklet的链表
        unsigned long state;         //是否需要触发底半部的状态
        atomic_t count;              //触发的次数
        bool use_callback;           //true使用callback ,false使用func 
        union {
            void (*func)(unsigned long data); //旧版本
            void (*callback)(struct tasklet_struct *t); //新版本
        };
        unsigned long data; //传递参数
    };

 struct tasklet_struct tasklet;
2.对象初始化
    void tasklet_init(struct tasklet_struct *t,
    void (*func)(unsigned long), unsigned long data) //旧版本的初始化
    void tasklet_setup(struct tasklet_struct *t,
     void (*callback)(struct tasklet_struct *)) //新版本的初始化
3.调用执行
    void tasklet_schedule(struct tasklet_struct *t)

3.3.2 示例

#include 
#include 
#include 
#include 
#include 
#include 
/*
myirq{
    compatible = "aaaa,myirq";
    interrupt-parent = <&gpiof>;
    interrupts = <9 0>;
};
*/
struct device_node *node;
unsigned int irqno;
#define name "key1";
struct tasklet_struct tasklet;
//底半部处理函数
void irq_tasklet_bottom(struct tasklet_struct* tasklet)
{
    //5.执行延时操作逻辑

}
//中断顶半部
irqreturn_t key_irq_handle(int irq, void* dev)
{
    //4.开始底半部
    tasklet_schedule(&tasklet);
    return IRQ_HANDLED;
}

static int __init myirq_init(void)
{
    int i, ret;
    // 0.tasklet_setup初始化
    tasklet_setup(&tasklet, irq_tasklet_bottom);

    // 1.获取设备树中的节点
    node = of_find_compatible_node(NULL, NULL, "aaaa,myirq");
    if (node == NULL) {
        printk("get node error\n");
        return -EINVAL;
    }
    // 2.映射得到软中断号
        irqno = irq_of_parse_and_map(node, 0);
        if (irqno == 0) {
            printk("get irqno error\n");
            return -EAGAIN;
        }

    // 3.注册中断
        ret = request_irq(irqno, key_irq_handle,
            IRQF_TRIGGER_FALLING, name, 0;
        if (ret) {
            printk("request irq error\n");
            return ret;
        }
    }

    return 0;
}
static void __exit myirq_exit(void)
{
        free_irq(irqno, 0);
}
module_init(myirq_init);
module_exit(myirq_exit);
MODULE_LICENSE("GPL");

  定义tasklet变量,实现软中断处理函数,初始化,调度,以上这些就是tasklet的使用步骤了,内核帮我们省略了很多麻烦的实现,所以使用起来比较简单。

3.4 工作队列(workqueue)

  前面已经讲了软中断还有tasklet了,那这里的工作队列和它们有什么区别呢?为什么会存在工作队列机制?
  存在即是合理,既然存在那肯定是用来弥补前两者的缺陷的,所以我们先来分析看看前两者有什么缺点。
  软中断和tasklet是运行于中断上下文的,它们属于内核态没有进程的切换,因此在执行过程中不能休眠,不能阻塞,一旦休眠或者阻塞,则系统直接挂死。比如我调试驱动时候,曾经在中断处理函数中调用spi同步数据的函数,系统直接挂死了,后来看代码的说明才明白,不能在中断中调用休眠,阻塞的函数。因此软中断和tasklet是有一定的使用局限性的,工作队列的出现正是用在软中断和tasklet不能使用的场合,比如需要调用一个具有可延迟函数的特质,但是这个函数又有可能引起休眠、阻塞。

3.4.1 接口

1.分配对象
 struct work_struct {
        atomic_long_t data; //可以向底半部处理函数传递数据
        struct list_head entry; //构成队列项
        work_func_t func; //底半部处理函数
 };
 struct work_struct work;
2.对象初始化
    void mywork_func(struct work_struct *work)
 {
     //工作队列的底半部处理函数
 }
    INIT_WORK(&work, mywork_func) 

3.调用执行
    bool schedule_work(struct work_struct *work)

4.保证底半部执行结束在卸载驱动防止Oops的空指针错误
    cancel_work_sync(&work);

3.4.2 示例

#include 
#include 
#include 
#include 
#include 
#include 
#include 
/*
myirq{
    compatible = "aaaa,myirq";
    interrupt-parent = <&gpiof>;
    interrupts = <9 0>;
};
*/

struct device_node *node;
unsigned int irqno;
#define name "key1";
struct work_struct work;

//底半部处理函数
void irq_work_func(struct work_struct* mwork)
{
     //5.执行延时操作逻辑
}
//中断顶半部
irqreturn_t key_irq_handle(int irq, void* dev)
{

   //4.调用工作队列
    schedule_work(&work);
    return IRQ_HANDLED;
}

static int __init myirq_init(void)
{
    int i, ret;
    // 0.tasklet_setup初始化
    INIT_WORK(&work, irq_work_func);

    // 1.获取设备树中的节点
    node = of_find_compatible_node(NULL, NULL, "aaaa,myirq");
    if (node == NULL) {
        printk("get node error\n");
        return -EINVAL;
    }
    // 2.映射得到软中断号
        irqno = irq_of_parse_and_map(node, 0);
        if (irqno == 0) {
            printk("get irqno error\n");
            return -EAGAIN;
        }

        // 3.注册中断
        ret = request_irq(irqno, key_irq_handle,
            IRQF_TRIGGER_FALLING, name, 0);
        if (ret) {
            printk("request irq error\n");
            return ret;
        }
    }

    return 0;
}
static void __exit myirq_exit(void)
{
    int i;
    cancel_work_sync(&work);
    free_irq(irqno,0);
}
module_init(myirq_init);
module_exit(myirq_exit);
MODULE_LICENSE("GPL");

3.5 应用实例

  工作队列(workqueue)和tasklet的使用思路是差不多的,各有局限性,实际的使用需要根据自身情况来选择。且工作队列还有其他的用法,例如创建一个固定周期调度的工作队列,这个是tasklet无法做到的。
  在这里我引用别人总结的比较直观的一句话:我们在做驱动的时候,关于这三个下半部(也就是以上的三种机制)实现,需要考虑两点:首先,是不是需要一个可调度的实体来执行需要推后完成的工作(即休眠的需要),如果有,工作队列就是唯一的选择,否则最好用tasklet。性能如果是最重要的,那还是软中断吧。
  光看理论没有实践还是不懂,这是我在lcd屏幕中触摸屏的中断代码,展示了在实际项目中对中断得使用。

3.5.1 设备树

  为防止歧义,我只保留了中断相关的设备树信息。

&spi0 {
    #address-cells = <1>;
    #size-cells = <0>;
    tsc2046@0 {
        pinctrl-names = "default";
        pinctrl-0 = <&tp_irq>;

        status = "okay";

        compatible = "ti,tsc2046";
        vcc-supply = <&ads7846reg1v8>;

        spi-max-frequency = <1500000>;
        reg = <0>;      /* CS0 */

        interrupt-parent = <&gpio3>;
        interrupts = ;     /* GPIO  */
        pendown-gpio = <&gpio3 RK_PA7 GPIO_ACTIVE_HIGH>;

        };
};
&pinctrl {
    spi0 {
       tp_irq:tp-irg {
            rockchip,pins = <3 RK_PA7 RK_FUNC_GPIO &pcfg_pull_none>;
           };
    };
};

3.5.2 驱动源码

  如下是我得中断代码。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define my_irq_name "my_irq"

struct device_node *node;
unsigned int irqno;

struct my_touch_data
{
    struct spi_device *spi;
    struct work_struct work;
    void *private_data;         /* 私有数据 */
};

uint16_t touch_buf = 0;
uint16_t value_re;
struct my_touch_data *data;
void read_spi(struct my_touch_data *data,  uint8_t tcmd)
{
    /**************************************************************/
    int ret;
    unsigned char txdata;
    struct spi_message m;
    struct spi_transfer *t;
    struct spi_device *spi = (struct spi_device *)data->private_data;

    t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);   /* 申请内存 */

    /* 第1次,发送要读取的寄存地址 */
    txdata = tcmd;
    t->speed_hz = 5000000;
    t->tx_buf = &txdata;            /* 要发送的数据 */
    t->rx_buf = &touch_buf;         /* 读取到的数据 */
    t->len = 1;                 /* 1个字节 */
    spi_message_init(&m);       /* 初始化spi_message */
    spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
    ret = spi_sync(spi, &m);    /* 同步发送 */

    mdelay(1);

    /* 第2次,读取数据 */
    txdata = 0x00;          /* 随便一个值,此处无意义 */
    t->tx_buf = &txdata;
    t->rx_buf = &touch_buf;         /* 读取到的数据 */
    t->len = 1;             /* 要读取的数据长度 */
    spi_message_init(&m);       /* 初始化spi_message */
    spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
    ret = spi_sync(spi, &m);    /* 同步发送 */

    value_re = touch_buf<<8;

    /* 第3次,读取数据 */
    txdata = 0x00;          /* 随便一个值,此处无意义 */
    t->tx_buf = &txdata;
    t->rx_buf = &touch_buf;         /* 读取到的数据 */
    t->len = 1;             /* 要读取的数据长度 */
    spi_message_init(&m);       /* 初始化spi_message */
    spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
    ret = spi_sync(spi, &m);    /* 同步发送 */

    value_re |= touch_buf;
    value_re >>= 3;

    udelay(200);
    kfree(t);                                   /* 释放内存 */
    return value_re;
}

// 底半部处理函数
void irq_work_func(struct work_struct *mwork)
{
    printk("进入软中断,工作队列\n");
    disable_irq_nosync(irqno);
    read_spi(data, 0xd0);
    enable_irq(irqno);
    disable_irq_nosync(irqno);
    read_spi(data, 0x90);
    enable_irq(irqno);
} 
// 中断顶半部
irqreturn_t key_irq_handle(int irq, void *dev)
{

    printk("进入中断上半部\n");
    if (schedule_work(&data->work))
    {
        printk("调用中断下半部完成\n");
    }else{
        printk("调用中断下半部失败\n");
    }
    return IRQ_HANDLED;
}
int my_touch_probe(struct spi_device *spi)
{
    int ret;
    data = devm_kzalloc(&spi->dev, sizeof(struct my_touch_data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;

    /*******************************************************************/
    // 0.tasklet_setup初始化
    spi->mode = SPI_MODE_0; /*MODE0,CPOL=0,CPHA=0*/
    spi->max_speed_hz = 5000000;
    spi_setup(spi);
    data->private_data = spi; /* 设置私有数据 */

    INIT_WORK(&data->work, irq_work_func);

    /*******************注册中断********************************/
    // 1.获取设备树中的节点
    node = of_find_compatible_node(NULL, NULL, "ti,tsc2046");
    if (node == NULL)
    {
        printk("get node error\n");
        return -EINVAL;
    }
    printk("匹配设备树成功\n");

    // 2.映射得到软中断号
    irqno = irq_of_parse_and_map(node, 0);
    if (irqno == 0)
    {
        printk("get irqno error\n");
        return -EAGAIN;
    }
    printk("获取中断号成功\n");
    // 3.注册中断
    ret = devm_request_irq(&spi->dev, irqno, key_irq_handle,
                           IRQF_TRIGGER_FALLING, my_irq_name, (void *)data);
    if (ret)
    {
        printk("request irq error\n");
        return ret;
    }

    printk("注册中断成功\n");
    /******************中断注册完成*******************************/
    return 0;
}

int my_touch_remove(struct spi_device *spi)
{
    cancel_work_sync(&data->work);
    free_irq(irqno, 0);
    return 0;
}

const struct of_device_id of_match[] = {
    {
        .compatible = "ti,tsc2046",
    },
    {},
};
MODULE_DEVICE_TABLE(of, of_match);

struct spi_driver my_touch = {
    .probe = my_touch_probe,
    .remove = my_touch_remove,
    .driver = {
        .name = "my_touch",
        .of_match_table = of_match,
    },
};
module_spi_driver(my_touch);
MODULE_LICENSE("GPL");

3.5.3 注意事项

1.硬中断中与spi_sync的冲突
  在实际开发中遇到了很多问题。如在硬中断中读取spi数据会导致内核崩溃;后来研究发现原来硬中断中不能阻塞等待,而spi的spi_sync函数会阻塞等待数据返回,这二者一叠加直接导致内核崩溃。
  如明明整个中断函数写的没问题,但就是不能从硬中断调用软中断成功;这是因为软中断中得调用函数有问题导致得(我这里是读取spi数据得函数协议不对导致得)。
  中断调用失败的问题很尴尬,但凡调用的函数有任何错误都会导致中断调用失败。这种情况和以往的经验不同,即使失败起码错误之前的函数执行完成应该打印出信息吧!但中断特有的机制导致,不会有任何打印信息出来,只会显示调用失败。没搞清楚这个问题之前,一点信息不打印一度让我很懵逼。
2.中断与spi通信的冲突
  在上述问题都解决后,让人懵逼的是不返回正确数据,恒定的返回0xff、0x00、0xf8;但各个方面都没问题,对比内核的ads7846.c和网上的xpt2046的驱动发现也没区别,但就是不返回有效数据;后面尝试用线程发送读取数据,就可正常返回。挠破头皮也没发现问题在哪,机缘巧合下给中断触发加了下面的锁才正常返回数据。

    disable_irq_nosync(irqno);
    read_spi(data, 0x90);
    enable_irq(irqno);

  分析觉得问题应该是中断不断的触发,而工作队列的中断是创建一组线程执行中断内的程序。二者叠加导致一直向触摸屏发送读取命令,所以才时序错乱无法返回有效数据的。
3.适配官方的ads7846.c驱动的问题
  在自己的驱动初步跑通后,就准备适配官方的驱动;结果发现怎么测试都卡在中断和spi_sync函数上了,通篇查下来代码匹配没任何问题。后面找了个xpt2046的驱动代码也是卡在这里,没办法逐行读代码分析问题原因;最后分析出原因应该就是代码中对数据处理的问题,导致给触摸屏发送的数据不正常,且spi的协议也有一定的问题才导致适配官方驱动不成功的。考虑到这种数据处理问题需要消耗的精力和时常,还是用自己写的触摸驱动性价比高一些。
  例如:
  使用spi子系统传输时设置返回两个字节就会发生错误,我只能老老实实的返回一个字节单独处理;我看各种例程都是随意设置返回字节数的,所以我怀疑是luckfox的开发板自身的问题。

    req->xfer[1].rx_buf = &req->sample;
    req->xfer[1].len = 2;
    spi_message_add_tail(&req->xfer[1], &req->msg);

4 总结

  至此关于中断的理论与实践就都研究的差不多了,整体下来我觉得在项目中简单得使用中断是没任何问题了,想再深入研究就要分析内核源码了,查看中断得底层实现。写完了才发现定时器中断忘提了,有需要得话,后续再专门写一篇。还有我对项目中遇到得问题都只是基于现有知识得判断,如果有大佬能给出确切得结论还望不吝赐教!!!!

评论

  1. ning
    Windows Edge 128.0.0.0
    1 月前
    2024-9-13 15:08:15

    太赞了!!!

  2. ning
    Windows Edge 128.0.0.0
    1 月前
    2024-9-13 15:12:33

    嘎嘎帅!!

  3. Windows Edge 128.0.0.0
    1 月前
    2024-9-13 15:38:09

    啊啊啊啊啊啊

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇