当前位置:诺佳网 > 电子/半导体 > 接口/总线/驱动 >

关于IO口驱动代码编译

时间:2022-06-09 | 栏目:接口/总线/驱动 | 点击:

IO开发是最基础的,也是入门必备技能,虽然很多已入门的读者来说,IO操作很简单,但对于很多初学者却不那么简单。

微机总线地址

地址总线:

kbit——mbit——gbit 差1024
bit 4,294,967,296
kbit 4,194,304 K
mbit 4,096 M
gbit 4 G

数据总线:

数据总线的宽度对CPU的性能的影响:

物理地址(PA)

虚拟地址(VA)

有关各种地址介绍的博文:

物理地址、虚拟地址、总线地址物理地址和总线地址区别

页表(MMU的单元)

分页管理:799c5f18-e78d-11ec-ba43-dac502259ad0.png

更详细的地址问题看这里

BCM2835芯片手册

下面截取树莓派芯片手册的一张图:79c06f3e-e78d-11ec-ba43-dac502259ad0.png

BCM2835是树莓派3B CPU的型号,是ARM-cotexA53架构cpu Bus是地址总线00000000~FFFFFFFFCPU寻址的范围(4G)DMA是高速拷贝单元,CPU可以发动DMA直接让DMA进行数据拷贝,直接内存访问单元。物理地址(PA)1G虚拟地址(VA)4G若程序大于物理地址1G,是不是就跑不了了,不是的,它有个MMU的单元,把物理地址映射成虚拟地址,我们操作的代码基本上都是在虚拟地址,它有一个映射页表(上面提及到过)

配置树莓派的pin4引脚为输出引脚:


		

功能选择输出/输入(GPIOFunctionSelectRegisters3214-12001=GPIOPin4isanoutput

只需要将GPFSL0这个寄存器的14~12位设置为001就可以了。只需要将0x6(对应的2进制是110)左移12位·然后取反再与上GPFSL0就可以将13、14这两位配置为0,然后再将0x6(对应2进制110)左移12位,然后或上GPFSL0即可将12位置1。

若想找树莓派引脚点这里

树莓派IO操控驱动代码:

ioremap、iounmap:

一. 一般我们的外设都是通过读写设备上的寄存器来进行的,通常包括控制寄存器、状态寄存器、数据寄存器三大类。外设的寄存器通常被连续编址,并且根据CPU的体系架构不同CPU对IO端口的编制方式有两种:

二、 在驱动开发过程中,一般来说外设的IO内存资源的物理地址是已知的,由硬件的设计决定。但是CPU不会为这些已知的外设IO内存资源预先指定虚拟地址的值,所以驱动程序不可以直接就通过外设的物理地址访问到IO内存,而必须要将其映射到虚拟地址空间(通过页表),然后才能根据内核映射过后的虚拟地址来通过内存指令访问这些IO内存,并对其进行操作。

三、 在Linux内核的io.h头文件中声明了ioremap()函数,用来将IO内存资源映射到核心虚拟地址空间(3Gb~4GB)中,当然不用了可以将其取消映射iounmap()。这两个函数在mm/ioremap.c文件中:

开始映射:void*ioremap(unsignedlongphys_addr,unsignedlongsize,unsignedlongflags)
//用map映射一个设备意味着使用户空间的一段地址关联到设备内存上,这使得只要程序在分配的地址范围内进行读取或写入,实际上就是对设备的访问。
第一个参数是映射的起始地址
第二个参数是映射的长度
第二个参数怎么定啊?
====================
这个由你的硬件特性决定。
比如,你只是映射一个32位寄存器,那么长度为4就足够了。
(这里树莓派IO口功能设置寄存器、IO口设置寄存器都是32位寄存器,所以分配四个字节就够了)

比如:GPFSEL0=(volatileunsignedint*)ioremap(0x3f200000,4);
GPSET0=(volatileunsignedint*)ioremap(0x3f20001C,4);
GPCLR0=(volatileunsignedint*)ioremap(0x3f200028,4);
这三行是设置寄存器的地址,volatile的作用是作为指令关键字
确保本条指令不会因编译器的优化而省略,且要求每次直接读值
ioremap函数将物理地址转换为虚拟地址,IO口寄存器映射成普通内存单元进行访问。

解除映射:voidiounmap(void*addr)//取消ioremap所映射的IO地址
比如:
iounmap(GPFSEL0);
iounmap(GPSET0);
iounmap(GPCLR0);//卸载驱动时释放地址映射

树莓派IO口四的驱动代码:

#include//file_operations声明
#include//module_initmodule_exit声明
#include//__init__exit宏定义声明
#include//classdevise声明
#include//copy_from_user的头文件
#include//设备号dev_t类型声明
#include//ioremapiounmap的头文件


staticstructclass*pin4_class;
staticstructdevice*pin4_class_dev;

staticdev_tdevno;//设备号
staticintmajor=231;//主设备号
staticintminor=0;//次设备号
staticchar*module_name="pin4";//模块名

volatileunsignedint*GPFSEL0=NULL;
volatileunsignedint*GPSET0=NULL;
volatileunsignedint*GPCLR0=NULL;
//这三行是设置寄存器的地址
//volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值

//led_open函数
staticintpin4_open(structinode*inode,structfile*file)
{
printk("pin4_open
");//内核的打印函数和printf类似

//配置pin4引脚为输出引脚
*GPFSEL0&=~(0x6<<12);//把bit13、bit14置为0
//0x6是110<<12左移12位 ~取反 &按位与
*GPFSEL0|=~(0x1<<12);//把12置为1|按位或

return0;

}
//read函数
staticintpin4_read(structfile*file,char__user*buf,size_tcount,loff_t*ppos)
{
printk("pin4_read
");//内核的打印函数和printf类似

return0;
}

//led_write函数
staticssize_tpin4_write(structfile*file,constchar__user*buf,size_tcount,loff_t*ppos)
{
intusercmd;
printk("pin4_write
");//内核的打印函数和printf类似

//获取上层write函数的值
copy_from_user(&usercmd,buf,count);//将应用层用户输入的指令读如usercmd里面
//根据值来操作io口,高电平或者低电平
if(usercmd==1){
printk("set1
");
*GPSET0|=0x01<< 4;
}
elseif(usercmd==0){
printk("set0
");
*GPCLR0|=0x01<< 4;
}
else{
printk("undo
");
}
return0;
}

staticstructfile_operationspin4_fops={

.owner=THIS_MODULE,
.open=pin4_open,
.write=pin4_write,
.read=pin4_read,
};

//static限定这个结构体的作用,仅仅只在这个文件。
int__initpin4_drv_init(void)//真实的驱动入口
{
intret;
devno=MKDEV(major,minor);//创建设备号
ret=register_chrdev(major,module_name,&pin4_fops);//注册驱动告诉内核,把这个驱动加入到内核驱动的链表中
pin4_class=class_create(THIS_MODULE,"myfirstdemo");//让代码在dev下自动>生成设备
pin4_class_dev=device_create(pin4_class,NULL,devno,NULL,module_name);//创建设备文件

GPFSEL0=(volatileunsignedint*)ioremap(0x3f200000,4);
GPSET0=(volatileunsignedint*)ioremap(0x3f20001C,4);
GPCLR0=(volatileunsignedint*)ioremap(0x3f200028,4);

printk("insmoddriverpin4success
");
return0;
}

void__exitpin4_drv_exit(void)
{

iounmap(GPFSEL0);
iounmap(GPSET0);
iounmap(GPCLR0);//卸载驱动时释放地址映射

device_destroy(pin4_class,devno);
class_destroy(pin4_class);
unregister_chrdev(major,module_name);//卸载驱动
}
module_init(pin4_drv_init);//入口,内核加载驱动的时候,这个宏会被调用,去调用pin4_drv_init这个函数
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPLv2");

1. 设置寄存器的地址

设置寄存器的地址,但是这样写是有问题的,我们上面讲到了在内核里代码和上层代码访问的是虚拟地址(VA),而现在设置的是物理地址,**==必须把物理地址转换成虚拟地址==**

//这三行是设置寄存器的地址
volatileunsignedint*GPFSEL0=volatile(unsignedint*)0x3f200000;
volatileunsignedint*GPSET0=volatile(unsignedint*)0x3f20001C;
volatileunsignedint*GPCLR0=volatile(unsignedint*)0x3f200028;
//volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值

我们先把地址初始

volatileunsignedint*GPFSEL0=NULL;
volatileunsignedint*GPSET0=NULL;
volatileunsignedint*GPCLR0=NULL;

在初始化int __init pin4_drv_init(void) //真实的驱动入口里赋值。

//整数11//0xb 11 00010001即便是16进制也是整数,左边是volatile unsigned int* GPFSEL0 右边也强制转换成(volatile unsigned int*)

volatile的作用是作为指令关键字,确保本条 ==指令不会因编译器的优化而省略==,==且要求每次直接读值==因为它是地址我希望它是无符号的unsigned

我们在编写驱动程序的时候,IO空间的起始地址是0x3f000000,加上GPIO的偏移量0x2000000,所以GPIO的物理地址应该是从0x3f200000开始的7af6a2a6-e78d-11ec-ba43-dac502259ad0.png然后在这个基础上进行Linux系统的MMU内存虚拟化管理,映射到虚拟地址上。用到了一个函数ioremap

//物理地址转换成虚拟地址,io口寄存器映射成普通内存单元进行访问
GPFSEL0=(volatileunsignedint*)ioremap(0x3f200000,4);
GPSET0=(volatileunsignedint*)ioremap(0x3f20001C,4);
GPCLR0=(volatileunsignedint*)ioremap(0x3f200028,4);//4是4个字节

2. 配置pin4引脚为输出引脚

7b0d48e4-e78d-11ec-ba43-dac502259ad0.png配置pin4引脚为输出引脚 bit 12-14 配置成001

3130······1413121110987654321
00······00100000000000
//配置pin4引脚为输出引脚bit12-14配置成001
*GPFSEL0&=~(0x6<<12);//把bit13、bit14置为0
//0x6是110<<12左移12位 ~取反 &按位与
*GPFSEL0|=~(0x1<<12);//把12置为1|按位或

忘记按位与 按位或 点这里

3. 获取上层write函数的值,根据值来操作io口,高电平或者低电平

copy_form_user(char *buf , user_buf , count)获取上层write函数的值

intusercmd;
copy_from_user(&usercmd,buf,count);//将应用层用户输入的指令读如usercmd里面

//根据值来操作io口,高电平或者低电平
printk("getvalue
");
if(usercmd==1){
printk("set1
");//置1
*GPSET0|=0x01<< 4;//用|或操作目的是不影响其他位
//写1是让寄存器开启置1让bit4为高电平
}
elseif(usercmd==0){
printk("set0
");//清0
*GPCLR0|=0x01<< 4;//用|或操作目的是不影响其他位
//写1是让清0寄存器开启置0让bit4为低电平
}
else{
printk("undo
");//提示不支持该指令
}

4. 解除映射

解除映射:void iounmap(void* addr);//取消ioremap所映射的IO地址

void__exitpin4_drv_exit(void)
{
iounmap(GPFSEL0);//解除映射GPFSEL0
iounmap(GPSET0);//解除映射GPSET0
iounmap(GPCLR0);//解除映射GPCLR0

device_destroy(pin4_class,devno);//先销毁设备
class_destroy(pin4_class);//再销毁类
unregister_chrdev(major,module_name);//卸载驱动

}

上层测试代码:

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

intmain()
{
intfd;
intcmd;
intdata;

fd=open("/dev/pin4",O_RDWR);
if(fd<0){
printf("openfailed
");
}else{
printf("opensuccess
");
}

printf("inputcommnd:1/0
1:setpin4high
0:setpin4low
");
scanf("%d",&cmd);

printf("cmd=%d
",cmd);
fd=write(fd,&cmd,4);//cmd类型是int所以写4
}

驱动卸载

在装完驱动后可以使用指令:sudo rmmod +驱动名(不需要写ko)将驱动卸载。

IO口驱动代码编译

  1. 首先在系统目录/SYSTEM/linux-rpi-4.14.y下使用指令:ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules驱动模块进行编译生成.ko文件.
  2. 然后将编译后的驱动发送到树莓派:scp ./drivers/char/pin4driver.ko pi@192.168.0.104:/home/pi,然后再将上层代码进行编译arm-linux-gnueabihf-gcc pin4test.c -o realtest,然后再将测试代码传到树莓派:scp realtest pi@192.168.43.136:/home/pi/
  3. 然后在树莓派上面使用指令:insmod pin4drive.ko进行加载驱动(然后lsmod即可查看到该驱动),
  4. 然后使用指令:sudo chmod 666 /dev/pin4给予pin4这个设备可访问权限,还可以在虚拟机上面使用mk5sum查看驱动文件的值,并在树莓派上面使用该指令进行查看该驱动文件的值,看是否一致。
  5. dmesg查看内核打印的信息,如下图所示:7b89d2ec-e78d-11ec-ba43-dac502259ad0.png
  6. 然后运行测试代码,在新建一个窗口,使用指令gpio readall可以看到BCM下面的4号引脚模式是输出模式,电平是低电平或高电平(根据输入的上层代码而定,输入0就是低电平,输入1就是高电平),这里我输入的是0,如下图所示:7ba61952-e78d-11ec-ba43-dac502259ad0.png

有关驱动代码里面GPIO口地址的问题:

有关驱动代码里面GPIO口地址的问题:7bc23bc8-e78d-11ec-ba43-dac502259ad0.png7beee3bc-e78d-11ec-ba43-dac502259ad0.png

审核编辑 :李倩

您可能感兴趣的文章:

相关文章