嵌入式Linux驱动开发之点灯

news2025/3/4 3:36:54

 

使用驱动开发的方式点亮一个LED灯。看看两者有啥区别不?

一、先看原理图

首先查看原理图,看看我们的板子上的LED等接在哪一个IO口上面。

好了,看原理图我们知道LED灯接在芯片的GPIO1的第三个引脚上面,也就是GPIO1_IO03。

二、IMX6UL的GPIO操作方法

先掌握三个名词

  • CCM: Clock Controller Module (时钟控制模块)

  • IOMUXC : IOMUX Controller,IO复用控制器

  • GPIO: General-purpose input/output,通用的输入输出口

2.1 GPIO模块结构

参考芯片手册《Chapter 26: General Purpose Input/Output (GPIO)》我们知道了IMX6UL一共有有5组GPIO(GPIO1~GPIO5),每组引脚最多有32个,但是可能实际上并没有那么多。

GPIO1有32个引脚:GPIO1_IO0~GPIO1_IO31;
GPIO2有22个引脚:GPIO2_IO0~GPIO2_IO21;
GPIO3有29个引脚:GPIO3_IO0~GPIO3_IO28;
GPIO4有29个引脚:GPIO4_IO0~GPIO4_IO28;
GPIO5有12个引脚:GPIO5_IO0~GPIO5_IO11;

我们知道IM6ULL有很多的引脚IO,但是并不是每一个引脚都能当做GPIO使用,它可以复用为其他模式的,比如作为I2C的时钟线I2C2_SCL等其他的用处。所以要向把某一IO当做GPIO使用需要将其复用,在linux中负责复用功能的寄存器IOMUXC_SW_MUX。还有要打开这个GPIO的时钟,在linux中叫做CCM,跟STM32一样还要设置它的IO口速度、上下拉电阻啊、驱动能力啊、压摆率(就是 IO 电平跳变所需要的时间,比如从0到1需要多少时间,时间越小波形就越陡,说明压摆率越高)啊等这些,在linux中是用IOMUXC_SW_PAD。

因此如果想要使用某一组GPIO,比如GPIO1_IO03。首先要打开GPIO1的时钟,然后将GPIO1_IO03设置为GPIO模式,而不是IIC模式。然后在设置一下GPIO1_IO03这个引脚的模式,速度、上下拉电阻、压摆率等。然后再设置GPIO1_IO03为输出模式。最后我们就可以向GPIO1_IO03的DR寄存器也就是数据寄存器写入0或者1,就可以输出高低电平来控制LED等的亮灭了。

2.2 打开的时钟

根据芯片手册我们可以看到,要想打开GPIO1_IO03的时钟就需要要去配置CCGR1这个寄存器的CG13这个位

而且我还知道了这个寄存器的地址是20C406CH,因此我们可以写一个宏定义。

#define CCM_CCGR1_BASE    (0X020C406C)//这个寄存器用来打开GPIO1的时钟的
/* 1、使能GPIO1时钟 */
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26); /* 清楚以前的设置 */
val |= (3 << 26); /* 设置新值 */
writel(val, IMX6U_CCM_CCGR1);

2.3 IOMUXC引脚复用和模式配置

参考资料:芯片手册《Chapter 32: IOMUX Controller (IOMUXC)》。对于某个/某组引脚,IOMUXC中有2个寄存器用来设置它。

IOMUXC_SW_MUX_CTL_PAD_pad-name

IOMUXC_SW_MUX_CTL_PAD_<PADNAME> :Mux pad xxx,选择某个pad的功能
IOMUXC_SW_MUX_CTL_GRP_<GROUP NAME>:Mux grp xxx,选择某组引脚的功能

某个引脚,或是某组预设的引脚,都有8个可选的模式(alternate (ALT) MUX_MODE),

比如我们要把这个GPIO1_IO03设置为GPIO模式,就要将这个寄存器的bit[0..3]设置为0101,也就是5.

然后也看到这个寄存器的地址位Address: 20E_0000h base + 68h offset = 20E_0068h

#define SW_MUX_GPIO1_IO03_BASE  (0X020E0068)//这个寄存器是将GPIO1_IO03复用为GPIO的
/* 2、设置GPIO1_IO03的复用功能,将其复用为
 *    GPIO1_IO03,最后设置IO属性。
 */
writel(5, SW_MUX_GPIO1_IO03);

IOMUXC_SW_MUX_CTL_GRP_group-name

IOMUXC_SW_PAD_CTL_PAD_<PAD_NAME>:pad pad xxx,设置某个pad的参数
IOMUXC_SW_PAD_CTL_GRP_<GROUP NAME>:pad grp xxx,设置某组引脚的参数

比如:

2.4 GPIO模块内部

框图如下:

我们暂时只需要关心3个寄存器:

① GPIOx_GDIR:设置引脚方向,每位对应一个引脚,1-output,0-input

② GPIOx_DR:设置输出引脚的电平,每位对应一个引脚,1-高电平,0-低电平

③ GPIOx_PSR:读取引脚的电平,每位对应一个引脚,1-高电平,0-低电平

三、怎么编程

3.1 读GPIO

① 设置CCM_CCGRx寄存器中某位使能对应的GPIO模块,默认是使能的。② 设置IOMUX来选择引脚用于GPIO。③ 设置GPIOx_GDIR中某位为0,把该引脚设置为输入功能。④ 读GPIOx_DR或GPIOx_PSR得到某位的值(读GPIOx_DR返回的是GPIOx_PSR的值)

3.2 写GPIO

① 设置CCM_CCGRx寄存器中某位使能对应的GPIO模块,默认是使能的。② 设置IOMUX来选择引脚用于GPIO。③ 设置GPIOx_GDIR中某位为1,把该引脚设置为输出功能。④ 写GPIOx_DR某位的值。

需要注意的是,你可以设置该引脚的loopback功能,这样就可以从GPIOx_PSR中读到引脚的有实电平;你从GPIOx_DR中读回的只是上次设置的值,它并不能反应引脚的真实电平,比如可能因为硬件故障导致该引脚跟地短路了,你通过设置GPIOx_DR让它输出高电平并不会起效果。

有了上面的知识,我们点亮led灯的流程基本就了解了。

四、GPIO寄存器操作方法

原则:不能影响到其他位。

4.1 直接读写

读出、修改对应位、写入

  • 要设置bit n

val = data_reg;//读出
val = val | (1<<n);//修改
data_reg = val;//写入
  • 要清除bit n

val = data_reg;//读出
val = val & ~(1<<n);//修改
data_reg = val;//写入

4.2 set-and-clear protocol

set_reg,clr_reg,data_reg 三个寄存器对应的是同一个物理寄存器

  • 要设置 bit n:set_reg = (1<<n);

  • 要清除 bit n:clr_reg = (1<<n);

五、编写驱动程序的套路

  • 1、确定主设备号,也可以让内核分配。

  • 2、定义自己的file_operations结构体。

  • 3、实现对应的drv_open/drv_read/drv_write等函数,填入file_operations结构体。

  • 4、把file_operations结构体告诉内核:register_chrdev

  • 5、谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数。

  • 6、有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用unregister_chrdev。

  • 7、其他完善:提供设备信息,自动创建设备节点:class_create,device_create。

驱动怎么操作硬件?

  • 通过ioremap映射寄存器的物理地址得到虚拟地址,读写虚拟地址。

驱动怎么和APP传输数据?

  • 通过copy_to_usercopy_from_user这 2 个函数。

六、地址映射

在编写驱动之前,我们需要先简单了解一下 MMU 这个神器,MMU全称叫做 Memory Manage Unit,也就是内存管理单元。在老版本的Linux中要求处理器必须有MMU,但是现在Linux内核已经支持无MMU的处理器了。MMU主要完成的功能如下:

  • ①、完成虚拟空间到物理空间的映射。

  • ②、内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。

我们重点来看一下第①点,也就是虚拟空间到物理空间的映射,也叫做地址映射。首先了解两个地址概念:虚拟地址(VA,Virtual Address)物理地址(PA,Physcical Address)。对于 32 位的处理器来说,虚拟地址范围是2^32=4GB,我们的开发板上有512MB的DDR3,这512MB的内存就是物理内存,经过MMU可以将其映射到整个4GB的虚拟空间

内存映射

物理内存只有512MB,虚拟内存有4GB,那么肯定存在多个虚拟地址映射到同一个物理地址上去,虚拟地址范围比物理地址范围大的问题处理器自会处理。

Linux内核启动的时候会初始化MMU,设置好内存映射,设置好以后CPU 访问的都是虚拟地址。比如 I.MX6ULL的GPIO1_IO03引脚的复用寄存器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03的地址为0X020E0068。如果没有开启MMU的话直接向0X020E0068这个寄存器地址写入数据就可以配 GPIO1_IO03的复用功能。现在开启了MMU,并且设置了内存映射,因此就不能直接向0X020E0068这个地址写入数据了。我们必须得到 0X020E0068这个物理地址在Linux系统里面对应的虚拟地址,这里就涉及到了物理内存和虚拟内存之间的转换,需要用到两个函数:ioremap 和 iounmap

6.1 ioremap函数

ioremap函 数用于获取指定物理地址空间对应的虚拟地址空间,定义在arch/arm/include/asm/io.h文件中,定义如下:

#include<asm/io.h>
#define ioremap(cookie,size) __arm_ioremap((cookie), (size),MT_DEVICE)

void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size,unsigned int mtype)
{
 return arch_ioremap_caller(phys_addr, size, mtype,__builtin_return_address(0));
}

ioremap 是个宏,有两个参数:cookie 和 size,真正起作用的是函数__arm_ioremap,此函数有三个参数和一个返回值,这些参数和返回值的含义如下:

  • phys_addr:要映射给的物理起始地址。

  • size:要映射的内存空间大小。

  • mtype:ioremap 的类型,可以选择 MT_DEVICE、MT_DEVICE_NONSHARED、MT_DEVICE_CACHED 和 MT_DEVICE_WC,ioremap 函数选择 MT_DEVICE。

  • 返回值:__iomem 类型的指针,指向映射后的虚拟空间首地址

假如我们要获取I.MX6ULL的IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03寄存器对应的虚拟地址,使用如下代码即可:

#define SW_MUX_GPIO1_IO03_BASE  (0X020E0068)
static void __iomem* SW_MUX_GPIO1_IO03;
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);

SW_MUX_GPIO1_IO03_BASE是寄存器物理地址,SW_MUX_GPIO1_IO03是映射后的虚拟地址。对于I.MX6ULL 来说一个寄存器是4 字节(32 位)的,因此映射的内存长度为 4。映射完成以后直接对SW_MUX_GPIO1_IO03进行读写操作即可。实际上,它是按页(4096 字节)进行映射的,是整页整页地映射的。所以说虽然映射的是4字节,实际上映射的是4096字节。

6.2 iounmap函数

卸载驱动的时候需要使用iounmap函数释放掉ioremap函数所做的映射,iounmap函数原型如下:

void iounmap (volatile void __iomem *addr)

iounmap只有一个参数addr,此参数就是要取消映射的虚拟地址空间首地址。假如我们现在要取消掉IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03寄存器的地址映射,使用如下代码即可:

iounmap(SW_MUX_GPIO1_IO03);

6.3 volatile的使用

① 编译器很聪明,会帮我们做些优化,比如:

int a;
a = 0; // 这句话可以优化掉,不影响 a 的结果
a = 1;

② 有时候编译器会自作聪明,比如:

int *p = ioremap(xxxx, 4); // GPIO 寄存器的地址
*p = 0; // 点灯,但是这句话被优化掉了
*p = 1; // 灭灯

③ 对于上面的情况,为了避免编译器自动优化,需要加上 volatile,告诉它这是容易出错的,别乱优化:

volatile int *p = ioremap(xxxx, 4); // GPIO 寄存器的地址
*p = 0; // 点灯,这句话不会被优化掉
*p = 1; // 灭灯

七、I/O内存访问函数

这里说的I/O是输入/输出的意思,并不是我们学习单片机的时候讲的GPIO引脚。这里涉及到两个概念:I/O端口和I/O内存

当外部寄存器或内存映射到IO空间时,称为I/O端口。当外部寄存器或内存映射到内存空间时,称为I/O内存。

但是对于ARM来说没有 I/O 空间这个概念,因此ARM体系下只有I/O内存(可以直接理解为内存)使用ioremap函数将寄存器的物理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是Linux内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作

上面的话是啥意思呢?

我说通俗一点就是:我现在知道了GPIO1_IO03它的时钟寄存器地址是0X020C406C,但是你不能直接操作它

#define CCM_CCGR1_BASE    (0X020C406C)

0X020C406C是它实际存在的也就是物理地址,但是呢在Linux内核启动的时候会初始化MMU,设置好内存映射,设置好以后CPU访问的都是虚拟地址,我们就不能操作实际的物理地址了。怎么办呢?不用怕,Linux提供了ioremap内存映射函数,我知道了实际的物理地址,只要通过这个函数我们就自动的获取到了这个物理地址对应的虚拟地址了

IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);

现在我们就得到了0X020C406C对应的虚拟地址IMX6U_CCM_CCGR1 ,但是呢,现在我们还不能直接操作这个虚拟地址。这又为啥呢?因为使用ioremap函数将寄存器的物理地址映射到虚拟地址以后,按说我们就可以直接通过指针访问这些地址,但是Linux内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。好家伙,Linux内核它不建议这样做,它又提供了读写函数对这个虚拟地址进行操作。那么我们用户只能按照它建议的这样做了。比如我想操作这个地址后4个字节的某几个位,就需要下面这样做,先把这个地址对应的内存空间读出来,然后修改,最后再把修改好的数据写入就可以了。

val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26); /* 清楚以前的设置 */
val |= (3 << 26); /* 设置新值 */
writel(val, IMX6U_CCM_CCGR1);

具体的读操作和写操作函数如下:

1 、读操作函数

读操作函数有如下几个:

u8 readb(const volatile void __iomem *addr)//读8bit
u16 readw(const volatile void __iomem *addr)//读16bit
u32 readl(const volatile void __iomem *addr)//读32bit

readb、readw 和readl这三个函数分别对应 8bit、16bit 和 32bit读操作,参数addr就是要读取写内存地址,返回值就是读取到的数据。

2 、写操作函数

写操作函数有如下几个:

void writeb(u8 value, volatile void __iomem *addr)//写8bit
void writew(u16 value, volatile void __iomem *addr)//写16bit
void writel(u32 value, volatile void __iomem *addr)//写32bit

writeb、writew 和 writel 这三个函数分别对应 8bit、16bit 和 32bit 写操作,参数value是要写入的数值,addr是要写入的地址。

八、程序编写

8.1 编写驱动程序

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

/*

我们要配置某一个GPIO的引脚
1、先打开这个GPIO的时钟
2、在讲这个GPIO复用为GPIO功能
3、设置这个GPIO的参数等
4、设置这个GPIO是输入还是输出
5、向这个GPIO的数据寄存器写数据就可以了
*/

#define LED_MAJOR  200  /* 主设备号 */
#define LED_NAME  "led"  /* 设备名字 */

#define LEDOFF  0    /* 关灯 */
#define LEDON  1    /* 开灯 */
 
/* 寄存器物理地址 */
#define CCM_CCGR1_BASE    (0X020C406C)//这个寄存器用来打开GPIO1的时钟的
#define SW_MUX_GPIO1_IO03_BASE  (0X020E0068)//这个寄存器是将GPIO1_IO03复用为GPIO的
#define SW_PAD_GPIO1_IO03_BASE  (0X020E02F4)//这个寄存器是配置GPIO1_IO03的速度、驱动能力、压摆率等
#define GPIO1_DR_BASE    (0X0209C000)//这个寄存器是GPIO1_IO03的数据寄存器
#define GPIO1_GDIR_BASE    (0X0209C004)//这个寄存器是设置GPIO1_IO03的方向,输入还是输出


/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

/*
 * @description  : LED打开/关闭
 * @param - sta  : LEDON(0) 打开LED,LEDOFF(1) 关闭LED
 * @return    : 无
 */
void led_switch(u8 sta)
{
 u32 val = 0;
 if(sta == LEDON) {
  val = readl(GPIO1_DR);
  val &= ~(1 << 3); 
  writel(val, GPIO1_DR);
 }else if(sta == LEDOFF) {
  val = readl(GPIO1_DR);
  val|= (1 << 3); 
  writel(val, GPIO1_DR);
 } 
}

/*
 * @description  : 打开设备
 * @param - inode  : 传递给驱动的inode
 * @param - filp  : 设备文件,file结构体有个叫做private_data的成员变量
 *        一般在open的时候将private_data指向设备结构体。
 * @return    : 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
 return 0;
}

/*
 * @description  : 从设备读取数据 
 * @param - filp  : 要打开的设备文件(文件描述符)
 * @param - buf  : 返回给用户空间的数据缓冲区
 * @param - cnt  : 要读取的数据长度
 * @param - offt  : 相对于文件首地址的偏移
 * @return    : 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
 return 0;
}

/*
 * @description  : 向设备写数据 
 * @param - filp  : 设备文件,表示打开的文件描述符
 * @param - buf  : 要写给设备写入的数据
 * @param - cnt  : 要写入的数据长度
 * @param - offt  : 相对于文件首地址的偏移
 * @return    : 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
 int retvalue;
 unsigned char databuf[1];
 unsigned char ledstat;

 retvalue = copy_from_user(databuf, buf, cnt);
 if(retvalue < 0) {
  printk("kernel write failed!\r\n");
  return -EFAULT;
 }

 ledstat = databuf[0];  /* 获取状态值 */

 if(ledstat == LEDON) { 
  led_switch(LEDON);  /* 打开LED灯 */
 } else if(ledstat == LEDOFF) {
  led_switch(LEDOFF); /* 关闭LED灯 */
 }
 return 0;
}

/*
 * @description  : 关闭/释放设备
 * @param - filp  : 要关闭的设备文件(文件描述符)
 * @return    : 0 成功;其他 失败
 */
static int led_release(struct inode *inode, struct file *filp)
{
 return 0;
}

/* 设备操作函数 */
static struct file_operations led_fops = {
 .owner   = THIS_MODULE,
 .open   = led_open,
 .read   = led_read,
 .write   = led_write,
 .release = led_release,
};

/*
 * @description : 驱动出口函数
 * @param   : 无
 * @return   : 无
 */
static int __init led_init(void)
{
 int retvalue = 0;
 u32 val = 0;

 /* 初始化LED */
 /* 1、寄存器地址映射 */
 IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
 SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
 SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
 GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
 GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

 /* 2、使能GPIO1时钟 */
 val = readl(IMX6U_CCM_CCGR1);
 val &= ~(3 << 26); /* 清楚以前的设置 */
 val |= (3 << 26); /* 设置新值 */
 writel(val, IMX6U_CCM_CCGR1);

 /* 3、设置GPIO1_IO03的复用功能,将其复用为
  *    GPIO1_IO03,最后设置IO属性。
  */
 writel(5, SW_MUX_GPIO1_IO03);
 
 /*寄存器SW_PAD_GPIO1_IO03设置IO属性
  *bit 16:0 HYS关闭
  *bit [15:14]: 00 默认下拉
     *bit [13]: 0 kepper功能
     *bit [12]: 1 pull/keeper使能
     *bit [11]: 0 关闭开路输出
     *bit [7:6]: 10 速度100Mhz
     *bit [5:3]: 110 R0/6驱动能力
     *bit [0]: 0 低转换率
  */
 writel(0x10B0, SW_PAD_GPIO1_IO03);

 /* 4、设置GPIO1_IO03为输出功能 */
 val = readl(GPIO1_GDIR);
 val &= ~(1 << 3); /* 清除以前的设置 */
 val |= (1 << 3); /* 设置为输出 */
 writel(val, GPIO1_GDIR);

 /* 5、默认关闭LED */
 val = readl(GPIO1_DR);
 val |= (1 << 3); 
 writel(val, GPIO1_DR);

 /* 6、注册字符设备驱动 */
 retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
 if(retvalue < 0){
  printk("register chrdev failed!\r\n");
  return -EIO;
 }
 return 0;
}

/*
 * @description : 驱动出口函数
 * @param   : 无
 * @return   : 无
 */
static void __exit led_exit(void)
{
 /* 取消映射 */
 iounmap(IMX6U_CCM_CCGR1);
 iounmap(SW_MUX_GPIO1_IO03);
 iounmap(SW_PAD_GPIO1_IO03);
 iounmap(GPIO1_DR);
 iounmap(GPIO1_GDIR);

 /* 注销字符设备驱动 */
 unregister_chrdev(LED_MAJOR, LED_NAME);
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zhiguoxin");

有了上面的讲解,代码很简单就不用多说了,就是按照那7步来操作的。

8.2 编写测试程序

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/***************************************************************

使用方法  :
./ledtest /dev/led  0   关闭LED
./ledtest /dev/led  1   打开LED  
***************************************************************/

#define LEDOFF  0
#define LEDON  1

/*
 * @description  : main主程序
 * @param - argc  : argv数组元素个数
 * @param - argv  : 具体参数
 * @return    : 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
 int fd, retvalue;
 char *filename;
 unsigned char databuf[1];
 
 if(argc != 3){
  printf("Error Usage!\r\n");
  return -1;
 }

 filename = argv[1];

 /* 打开led驱动 */
 fd = open(filename, O_RDWR);
 if(fd < 0){
  printf("file %s open failed!\r\n", argv[1]);
  return -1;
 }

 databuf[0] = atoi(argv[2]); /* 要执行的操作:打开或关闭 */

 /* 向/dev/led文件写入数据 */
 retvalue = write(fd, databuf, sizeof(databuf));
 if(retvalue < 0){
  printf("LED Control Failed!\r\n");
  close(fd);
  return -1;
 }

 retvalue = close(fd); /* 关闭文件 */
 if(retvalue < 0){
  printf("file %s close failed!\r\n", argv[1]);
  return -1;
 }
 return 0;
}

测试程序就很简单了,不用多说。

3 8.编写Makefile

KERNELDIR := /home/zhiguoxin/linux/IMX6ULL/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
CURRENT_PATH := $(shell pwd)

obj-m := led.o

build: kernel_modules

kernel_modules:
 $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
 $(CROSS_COMPILE)arm-linux-gnueabihf-gcc -o ledApp ledApp.c 

clean:
 $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
  • 第1行,KERNELDIR表示开发板所使用的Linux内核源码目录,使用绝对路径,大家根据自己的实际情况填写。

  • 第2行,CURRENT_PATH表示当前路径,直接通过运行pwd命令来获取当前所处路径。

  • 第3行,obj-m表示将led.c这个文件编译为led.ko模块。

  • 第8行,具体的编译命令,后面的modules表示编译模块,-C表示将当前的工作目录切换到指定目录中,也就是KERNERLDIR目录。M表示模块源码目录,make modules命令中加入M=dir以后程序会自动到指定的 dir 目录中读取模块的源码并将其编译为.ko 文件。

  • 第9行,使用交叉编译工具链将ledApp.c编译成可以在arm板子上运行的ledApp可执行文件。

Makefile 编写好以后输入make命令编译驱动模块,编译过程如图所示

九、运行测试

9.1 上传程序到开发板执行

开发板启动后通过NFS挂载Ubuntu目录的方式,将相应的文件拷贝到开发板上。简单来说,就是通过NFS在开发板上通过网络直接访问ubuntu虚拟机上的文件,并且就相当于自己本地的文件一样。

因为我的代码都放在/home/zhiguoxin/myproject/alientek_drv_development_source这个目录下,所以我们将这个目录作为NFS共享文件夹。

Ubuntu IP为192.168.10.100,一般都是挂载在开发板的mnt目录下,这个目录是专门用来给我们作为临时挂载的目录。

文件系统目录简介

然后使用MobaXterm软件通过SSH访问开发板。

ubuntu ip:192.168.10.100
windows ip:192.168.10.200
开发板ip:192.168.10.50

在开发板上执行以下命令就可以实现挂载了:

mount -t nfs -o nolock,vers=3 192.168.10.100:/home/zhiguoxin/myproject/alientek_drv_development_source /mnt

就将开饭的mnt目录挂载在ubuntu的/home/zhiguoxin/myproject/alientek_drv_development_source目录下了。这样我们就可以在Ubuntu下修改文件,然后可以直接在开发板上执行可执行文件了。当然我这里的/home/zhiguoxin/myproject/windows之间是一个共享目录,我也可以直接在windows上面修改文件,然后ubuntu和开发板直接进行文件同步了。

9.2 加载驱动模块

驱动模块led.koledApp可执行文件都已经准备好了,接下来就是运行测试。这里我是用挂载的方式将服务端的项目文件夹挂载到arm板的mnt目录,进入到/mnt/02_led目录输入如下命令加载led.ko驱动文件:

insmod led.ko

9.3 创建设备节点文件

驱动加载成功需要在/dev目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。输入如下命令创建/dev/led这个设备节点文件:

mknod /dev/led c 200 0

其中mknod是创建节点命令,/dev/hello_drv 是要创建的节点文件,c表示这是个字符设备,200是设备的主设备号,0是设备的次设备号。创建完成以后就会存在/dev/led这个文件,可以使用ls /dev/led-l命令查看。

9.3 led设备操作测试

一切准备就绪。使用ledtest 软件操作led这个设备,看看是否可以正常打开或关闭led。

./ledApp /dev/led  0   关闭LED
./ledApp /dev/led  1   打开LED 

9.4  卸载驱动模块

如果不再使用某个设备的话可以将其驱动卸载掉,比如输入如下命令卸载掉hello_drv这个设备:

rmmod led.ko

卸载以后使用lsmod命令查看led这个模块还存不存在:

可以看出,此时系统已经没有任何模块了,led这个模块也不存在了,说明模块卸载成功。而且系统中也没有了led这个设备。

至此,led这个设备的整个驱动就验证完成了,驱动工作正常。以后的字符设备驱动实验基本都可以此为模板进行编写。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/104418.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

优化稠密点之尝试通过 Balance 来分摊访问压力

回忆之前的官方架构图&#xff0c;数据在 storaged 中是分片的&#xff0c;且 raft 协议中只有 leader 才会处理请求&#xff0c;所以&#xff0c;重新进行数据平衡操作&#xff0c;是有可能将多个稠密点分摊到不同的服务上以减轻单一服务的压力。同时&#xff0c;我们对整个集…

记录C,C++关键字的位置,直接跳过注释和字符串文本。(修正)

依据第二版本&#xff0c;可以写一个跳过注释的查找函数 C_IndexOfWord Java_IndexOfWord CSharp_IndexOfWord 还有一种方法&#xff0c;可以先把所有注释用空格代替&#xff0c;查出的字符位置也不变。 以前版本&#xff1a; DList<TextColor> Syntax::GetTextColorP…

(三分钟)学会kd-tree 激光SLAM点云搜索常见

Kd-Tree&#xff1a; 今天来介绍一下有关Kdtree的相关概念&#xff0c;它是一维线段树的多维推广。Kd-tree常用在激光点云编程中使用&#xff0c;Kd-tree简称k维树&#xff0c;是一种空间划分的数据结构&#xff0c;常被用于高维空间中的搜索&#xff0c;比如范围搜索和最近邻…

动态规划问题——最长公共子序列问题

题目: 给定两个字符串 str1 和 str2 &#xff0c;返回两个字符串的最长公共子序列。 举例&#xff1a; str1 "1A2C3D4B56" str2 "B1D23CA45B6A" 最长公共子序列为&#xff1a;"123456" 或 "12C4B6" 返回哪个都行 思路&#xf…

「内核知识」Linux下的系统调用write

本文以x86_64平台为例&#xff0c;分析linux下的系统调用是如何被执行的。 假设目标系统调用是&#xff0c;其对应的内核源码为&#xff1a; // fs/read_write.c SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,size_t, count) {return ksys_write(fd, …

通过国产化低代码平台搭建设备管理系统,助力中国航天企业信息化建设

编者按&#xff1a;掌握软件自主权&#xff0c;支持信创国产化。本文分析了国产化低代码平台的意义&#xff0c;并介绍了低低代码平台是如何为航天企业提高信息化建设水平的。 关键词&#xff1a;国产化&#xff0c;第三方对接能力&#xff0c;文件管理 以容器、微服务、DevOp…

Linux内核中的open方法

在linux下&#xff0c;假设我们想打开文件/dev/tty&#xff0c;我们可以使用系统调用open&#xff0c;比如&#xff1a; int fd open("/dev/tty", O_RDWR, 0); 本文将从源码角度看下&#xff0c;在linux内核中&#xff0c;open方法是如何打开文件的。 首先看下入…

【opencv】centos下opencv的编译(带opencv_contrib扩展包)

目录1.安装cmake2.opencv安装文件准备可选安装3.进行编译4.进行安装5.使用测试编译步骤&#xff1a;1.安装cmake 安装wget: 应该已安装gcc工具套组&#xff1a; yum install sudo yum install -y gcc gcc-c make automake yum install -y wget wget https://cmake.org/files…

【树莓派不吃灰】兄弟连篇④ Shell编程

目录1、Shell基础1.1 脚本执行方式1.1.1 echo1.1.2 脚本执行1.2 Bash基本功能1.2.1 history1.2.2 命令补全1.2.3 命令别名1.2.4 常用快捷键1.2.5 输出输入重定向1.2.6 多命令顺序执行与管道符1.2.7 通配符和特殊符号1.3 Bash变量1.3.1 用户自定义变量1.3.2 环境变量1.3.3 位置变…

Hudi入门到实战

简介 Apache Hudi&#xff08;Hadoop Upserts Delete and Incremental&#xff09;是下一代流数据湖平台。Apache Hudi将核心仓库和数据库功能直接引入数据湖。Hudi提供了表、事务、高效的upserts/delete、高级索引、流摄取服务、数据集群/压缩优化和并发&#xff0c;同时保持…

用于医学诊断的菁染料ICG-Sulfo-OSu,活性基团修饰ICG

英文名&#xff1a;ICG-Sulfo-EG4-OSu 激发波长: 780nm; 发射波长: 800nm 分子量: 1777.36 溶剂:DMSO 凯新生物描述&#xff1a; (ICG)是一种用于医学诊断的菁染料。它用于测定心输出量、肝功能和肝脏血流量&#xff0c;以及用于眼科血管造影。它的峰值光谱吸收接近800纳米…

【课程整理】随机系统期末整理

文章目录1 概率论部分 (1-4)概率空间随机变量概率分布随机变量的函数仍然是随机变量条件期望2 随机过程 (5-7)随机过程Martingale停时马尔科夫链3 参数估计 (8-10)参数估计问题充分统计量贝叶斯估计非随机估计部分思维导图如下&#xff0c;私信发送html完整版 1 概率论部分 (1-…

3.path路径模块

目录 1 路径问题 2 直接给绝对路径 3 用 __dirname 表示绝对路径 4 path模块常用方法 4.1 路径拼接 path.join() 4.2 从路径字符串中解析出文件名 path.basename() 4.3 获取路径中的扩展名 path.extname() 1 路径问题 当我们使用 ./ 或是 ../ 表示相对路径的时…

小林Coding阅读笔记:操作系统篇之硬件结构,中断问题

前言 参考/导流&#xff1a; 小林coding - 2.6 什么是软中断&#xff1f;学习意义 学习CPU与外设如何去提升处理效率的设计思想&#xff0c;异步机制的理解与借鉴掌握相关的Linux命令&#xff0c;帮助问题排查 相关说明 该篇博文是个人阅读的重要梳理&#xff0c;仅做简单参…

【修饰性PEG供应商】mPEG-DBCO_DBCO mPEG_甲氧基聚乙二醇环辛炔

【产品描述】 西安凯新生物科技有限公司是国内业PEG供应商&#xff0c;可以提供不同分子量的PEG衍生物&#xff0c;小分子PEG的循环节可以做到1-36个&#xff0c;高分子PEG分子量从1000-40000不等&#xff0c;可以修饰的基团有&#xff1a;氨基类&#xff0c;NHBOC类&#xff0…

多层板PCB设计中电源平面相对地平面为什么要进行内缩

大家是否观察过&#xff0c;有一些人绘制的PCB&#xff0c;在GND层和电源层会进行一定程度的内缩设计&#xff0c;那么大家有没有想过为什么要内缩呢。需要搞清楚这个问题&#xff0c;我们需要来先了解一个知识点&#xff0c;那就是“20H”原则&#xff1a; 20H原则主要是为了减…

MySQL的数据结构

阅读目录MySQL 数据结构用 btree 做的为什么不用红黑树叉树呢&#xff1f;什么是 B-Tree&#xff08;B-树&#xff09;&#xff1f;什么是 BTree&#xff1f;BTree 相对于 B-Tree 的几点不同MySQL 数据结构用 btree 做的 为什么不用红黑树叉树呢&#xff1f; 不用红黑树是因为…

计算机3D数学基础 旋转的三种表示方法 学习笔记

旋转的三种表示方法&#xff1a;1、矩阵 2、欧拉角 3、四元数 矩阵的缺点&#xff0c;记录旋转角度要记录9个数 欧拉角的heading就是绕着y(绿色轴)轴30 &#xff0c;容易理解 三种旋转方式的优缺点&#xff1a; 矩阵&#xff1a;缺点&#xff1a;1、上手难&#xff0c;记…

Android常用布局总结之(LinearLayout、TableLayout、GridLayout、RelativeLayout)

一、LinearLayout 线性布局 LinearLayout 是一个视图组&#xff0c;用于使所有子视图在单个方向&#xff08;垂直或水平&#xff09;保持对齐。您可以使用 android:orientation 属性指定布局方向。 android:orientation&#xff0c;指定布局方向&#xff0c;vertical-竖向布局…

在线点餐网站

开发工具(eclipse/idea/vscode等)&#xff1a; 数据库(sqlite/mysql/sqlserver等)&#xff1a; 功能模块(请用文字描述&#xff0c;至少200字)&#xff1a; 管理员&#xff1a; 1、管理门店介绍、联系我们 2、对公告类型、公告信息增删改查 3、对菜品类型、菜品信息增册改查 4…