跳到主要内容位置

模板1-最简单的通用框架模板

GPIO子系统#

在硬件上如何确定GPIO引脚?它属于哪组GPIO?它是这组GPIO的哪个引脚?需要2个参数。

但是在LInux软件上,可以使用引脚编号(所有的组所有的引脚都一起编号)来表示

知道引脚编号后,就可以使用GPIO子系统的函数了。下表列出了gpio子系统相关的函数,左边是新的函数,右边是旧的。

descriptor-basedlegacy
获得GPIO
gpiod_getgpio_request
gpiod_get_index
gpiod_get_arraygpio_request_array
devm_gpiod_get
devm_gpiod_get_index
devm_gpiod_get_array
设置方向
gpiod_direction_inputgpio_direction_input
gpiod_direction_outputgpio_direction_output
读值、写值
gpiod_get_valuegpio_get_value
gpiod_set_valuegpio_set_value
释放GPIO
gpio_freegpio_free
gpiod_putgpio_free_array
gpiod_put_array
devm_gpiod_put
devm_gpiod_put_array

在开发板上使用如下命令可以查看已经使用的GPIO:注意在软件上是从0开始编号的,而在6ull芯片手册里是从1开始编号的。

# cat /sys/kernel/debug/gpio
gpiochip0: GPIOs 0-15, parent: platform/soc:pin-controller@50002000, GPIOA:
gpio-10 ( |heartbeat ) out lo
gpio-14 ( |shutdown ) out hi
gpiochip1: GPIOs 16-31, parent: platform/soc:pin-controller@50002000, GPIOB:
gpio-26 ( |reset ) out hi ACTIVE LOW
gpiochip2: GPIOs 32-47, parent: platform/soc:pin-controller@50002000, GPIOC:
gpiochip3: GPIOs 48-63, parent: platform/soc:pin-controller@50002000, GPIOD:

怎么确定GPIO引脚的编号?

  • 先在开发板的/sys/class/gpio目录下,找到各个gpiochipXXX目录(XXX是这一组的其实编号)
  • 然后进入某个gpiochipXXX目录,查看文件label的内容,就可以知道起始号码XXX对于哪组GPIO(物理地址上的组别)

那么GPIO4_14的号码是96+14=110,可以如下操作读取按键值:

[root@100ask:~]# echo 110 > /sys/class/gpio/export // gpio_request
[root@100ask:~]# echo in > /sys/class/gpio/gpio110/direction // gpio_direction_input
[root@100ask:~]# cat /sys/class/gpio/gpio110/value // gpio_get_value
[root@100ask:~]# echo 110 > /sys/class/gpio/unexport // gpio_free

注意:如果驱动程序已经使用了该引脚,那么将会export失败

对于输出引脚,假设引脚号为N,可以用下面的方法设置它的值为1:

[root@100ask:~]# echo N > /sys/class/gpio/export
[root@100ask:~]# echo out > /sys/class/gpio/gpioN/direction
[root@100ask:~]# echo 1 > /sys/class/gpio/gpioN/value
[root@100ask:~]# echo N > /sys/class/gpio/unexport

中断函数#

使用中断的流程:

  • 确定中断号

  • 注册中断处理函数,使用的函数原型如下:

    int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
    const char *name, void *dev);
  • 在中断处理函数里

    • 分辨中断
    • 处理中断
    • 清除中断

函数细节:

request_irq函数第1个参数是中断号,可以根据GPIO函数获得中断号:

int gpio_to_irq(unsigned int gpio);
int gpiod_to_irq(const struct gpio_desc *desc);

request_irq函数第2个参数是函数指针,指向中断处理函数:

enum irqreturn {
IRQ_NONE = (0 << 0),
IRQ_HANDLED = (1 << 0),
IRQ_WAKE_THREAD = (1 << 1),
};
typedef enum irqreturn irqreturn_t;
typedef irqreturn_t (*irq_handler_t)(int irq, void *dev);

request_irq函数的第3个参数表示中断触发条件,有如下取值:

//触发方式
#define IRQF_TRIGGER_NONE 0x00000000
#define IRQF_TRIGGER_RISING 0x00000001
#define IRQF_TRIGGER_FALLING 0x00000002
#define IRQF_TRIGGER_HIGH 0x00000004
#define IRQF_TRIGGER_LOW 0x00000008
//共享中断,在上面的图中就是共享中断
#define IRQF_SHARED 0x00000080

request_irq函数的第4个参数是中断的名字,可以在执行cat /proc/interrupts的结果里查看。

request_irq函数的第5个参数是给中断处理函数使用的,中断处理函数的第二个参数就是这个参数。

模板解读#

04_template1_gpio_drv

  • file_operations

  • GPIO的操作

  • 中断的处理

  • 定时器

    /* 入口函数gpio_drv_init */
    setup_timer(&gpios[i].key_timer, key_timer_expire, (unsigned long)&gpios[i]);
    //timer_setup(&gpios[i].key_timer, key_timer_expire, 0);
    gpios[i].key_timer.expires = ~0;
    add_timer(&gpios[i].key_timer);
    /* 按键中断处理函数gpio_key_isr */
    //HZ一般定义为100,除以50则为2,表示加上2个滴答;一个滴答的时间是1/100s=10ms. 因此这里表示延迟20ms
    mod_timer(&gpio_desc->key_timer, jiffies + HZ/50);
    /* mod_timer函数原型 */
    * mod_timer - modify a timer's timeout
    * @timer: the timer to be modified
    * @expires: new timeout in jiffies 未来的时间,单位是滴答
    int mod_timer(struct timer_list *timer, unsigned long expires)
    {
    return __mod_timer(timer, expires, false);
    }

    使用定时器进行按键消抖,在按键中断里延长定时器时间,这样多个按键中断,只会触发一次定时器超时;在定时器超时处理函数里进行按键值读取与保存。

交互流程解读#

  • 非阻塞 具体阻塞与否,在于驱动程序有没有判断标记。

    /* APP */
    fd = open(argv[1], O_RDWR | O_NONBLOCK); //非阻塞方式打开
    read(fd, &val, 4);
    /* drv */
    static ssize_t gpio_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
    {
    //printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    int err;
    int key;
    if (is_key_buf_empty() && (file->f_flags & O_NONBLOCK)) //判断是buf为空,且为非阻塞方式,就直接返回
    return -EAGAIN;
    wait_event_interruptible(gpio_wait, !is_key_buf_empty());
    key = get_key();
    err = copy_to_user(buf, &key, 4);
    return 4;
    }
  • 阻塞

    /* APP */
    fd = open(argv[1], O_RDWR); //阻塞方式打开
    read(fd, &val, 4);
    /* drv */
    static DECLARE_WAIT_QUEUE_HEAD(gpio_wait); //声明一个等待队列
    static ssize_t gpio_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
    {
    //printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    int err;
    int key;
    if (is_key_buf_empty() && (file->f_flags & O_NONBLOCK)) //永远不成立
    return -EAGAIN;
    wait_event_interruptible(gpio_wait, !is_key_buf_empty()); //阻塞等待
    key = get_key();
    err = copy_to_user(buf, &key, 4);
    return 4;
    }

    wait_event_interruptible函数原型:等待condition事件为真,上述代码是等待“buf非空”事件 为真;如果condition不成立,就进入休眠并把它放入wq队列中,此时就需要其他函数调用wake_up()把它唤醒。

    /**
    * wait_event_interruptible - sleep until a condition gets true
    * @wq: the waitqueue to wait on
    * @condition: a C expression for the event to wait for
    *
    * The process is put to sleep (TASK_INTERRUPTIBLE) until the
    * @condition evaluates to true or a signal is received.
    * The @condition is checked each time the waitqueue @wq is woken up.
    *
    * wake_up() has to be called after changing any variable that could
    * change the result of the wait condition.
    *
    * The function will return -ERESTARTSYS if it was interrupted by a
    * signal and 0 if @condition evaluated to true.
    */
    #define wait_event_interruptible(wq, condition) \
    ({ \
    int __ret = 0; \
    might_sleep(); \
    if (!(condition)) \
    __ret = __wait_event_interruptible(wq, condition); \
    __ret; \
    })
    #define __wait_event_interruptible_timeout(wq, condition, timeout) \
    ___wait_event(wq, ___wait_cond_timeout(condition), \
    TASK_INTERRUPTIBLE, 0, timeout, \
    __ret = schedule_timeout(__ret))

    当按键按下时,经过定时器延时消抖后,调用定时器的中断函数:使用wake_up_interruptible唤醒gpio_wait队列中的值

    // static void key_timer_expire(struct timer_list *t)
    static void key_timer_expire(unsigned long data)
    {
    /* data ==> gpio */
    // struct gpio_desc *gpio_desc = from_timer(gpio_desc, t, key_timer);
    struct gpio_desc *gpio_desc = (struct gpio_desc *)data;
    int val;
    int key;
    val = gpio_get_value(gpio_desc->gpio);
    //printk("key_timer_expire key %d %d\n", gpio_desc->gpio, val);
    key = (gpio_desc->key) | (val<<8);
    put_key(key);
    wake_up_interruptible(&gpio_wait); //唤醒等待队列中的任务来读buf
    kill_fasync(&button_fasync, SIGIO, POLL_IN);
    }
  • POLL

    • 有data,立即返回
    • 无data,休眠
    • 被唤醒,a.按下/松开 b.超时
    /* 2. 打开文件 */
    fd = open(argv[1], O_RDWR);
    if (fd == -1)
    {
    printf("can not open file %s\n", argv[1]);
    return -1;
    }
    fds[0].fd = fd;
    fds[0].events = POLLIN;
    /* 3. 读文件 */
    ret = poll(fds, 1, timeout_ms);
    if ((ret == 1) && (fds[0].revents & POLLIN))
    {
    read(fd, &val, 4);
    printf("get button : 0x%x\n", val);
    }
    else
    {
    printf("timeout\n");
    }

    应用程序的poll调用sys_poll,然后sys_poll中有一个循环,会先调用驱动程序的drv_poll,如果驱动程序的返回值不为0或者超时,就返回,否则进入休眠。

    • drv_poll:把线程放入wq中,但是未休眠;返回当前event状态(有数据返回POLLIN | POLLRDNORM,无数据返回0)

      static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);
      static unsigned int gpio_key_drv_poll(struct file *fp, poll_table * wait)
      {
      printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
      poll_wait(fp, &gpio_key_wait, wait); //1.把线程放入wq中,但是未休眠
      return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM; //2.返回当前event状态
      }
    • 内核sys_poll函数进入休眠,谁来唤醒?a.超时,内核唤醒;b.中断程序唤醒gpio_key_wait队列

  • 异步通知 发信号:中断/定时器的函数发送信号(主),APP(宾) APP:

    static void sig_func(int sig)
    {
    int val;
    read(fd, &val, 4);
    printf("get button : 0x%x\n", val);
    }
    int main(int argc, char **argv)
    {
    ...
    signal(SIGIO, sig_func); //创建信号及设置处理函数
    /* 2. 打开文件 */
    fd = open(argv[1], O_RDWR);
    if (fd == -1)
    {
    printf("can not open file %s\n", argv[1]);
    return -1;
    }
    fcntl(fd, F_SETOWN, getpid()); //把进程的pid告诉驱动,这样驱动程序就知道信号要发给谁
    //先读出flags,再设置新的flags
    flags = fcntl(fd, F_GETFL);
    fcntl(fd, F_SETFL, flags | FASYNC);
    while (1)
    {
    printf("www.100ask.net \n");
    sleep(2);
    }
    close(fd);
    return 0;
    }

    驱动:

    static irqreturn_t gpio_key_isr(int irq, void *dev_id)
    {
    struct gpio_desc *gpio_desc = dev_id;
    printk("gpio_key_isr key %d irq happened\n", gpio_desc->gpio);
    //HZ一般定义为100,除以50则为2,表示加上2个滴答;一个滴答的时间是1/100s=10ms. 因此这里表示延迟20ms
    //设置定时器的时间为 当前的滴答jiffies + HZ/50个滴答
    mod_timer(&gpio_desc->key_timer, jiffies + HZ/50);
    return IRQ_HANDLED;
    }
    static void key_timer_expire(unsigned long data)
    {
    /* data ==> gpio */
    // struct gpio_desc *gpio_desc = from_timer(gpio_desc, t, key_timer);
    struct gpio_desc *gpio_desc = (struct gpio_desc *)data;
    int val;
    int key;
    val = gpio_get_value(gpio_desc->gpio);
    key = (gpio_desc->key) | (val<<8);
    put_key(key);
    wake_up_interruptible(&gpio_wait);
    kill_fasync(&button_fasync, SIGIO, POLL_IN); //发信号SIGIO,给进程
    }

    进程的信息隐藏在button_fasync结构体里: 在APP中修改了flags,即调用fcntl(fd, F_SETFL, flags | FASYNC);时,会导致驱动程序的.fasync = gpio_drv_fasync函数被调用。所以在驱动定时器中断函数里就可以发信号给进程。

    static int gpio_drv_fasync(int fd, struct file *file, int on)
    {
    if (fasync_helper(fd, file, on, &button_fasync) >= 0) //构造button_fasync结构体
    return 0;
    else
    return -EIO;
    }

    当app接收到信号后就会运行sig_func函数,执行完后就会回到main函数。

补充环形缓冲区#

适合A放入值,B取出值的场景,可以很好地管理值和防止值丢失。

#include "ringbuf.h"
/* 环形缓冲区 */
#define BUF_LEN 128
static int g_buffer[BUF_LEN];
static int r, w;
struct fasync_struct *button_fasync;
#define NEXT_POS(x) ((x+1) % BUF_LEN) //当BUF_LEN是2^n时,可以使用更加快速的:( (x+1) & (BUF_LEN-1) )
int is_buf_empty(void)
{
return (r == w);
}
int is_buf_full(void)
{
return (r == NEXT_POS(w));
}
void put_value(int key)
{
if (!is_buf_full())
{
g_buffer[w] = key;
w = NEXT_POS(w);
}
}
int get_value(void)
{
int key = 0;
if (!is_buf_empty())
{
key = g_buffer[r];
r = NEXT_POS(r);
}
return key;
}

请点击左侧菜单(移动端为右下角)选择要查看的所有笔记吧。

最近更新于