嵌入式之路,贵在日常点滴
---阿杰在线送代码
目录
①树莓派上卸载之前的pin4驱动,删除上层应用文件和.ko文件
②框架和上层应用程序在Ubuntu中进行交叉编译并发送至树莓派
简单了解:DMA(direct memory access)(直接存储器访问)
有关各种地址介绍的博文:
写驱动是为了操作IO口,实现自己的wiringpi库,跟硬件打交道。
首先要理解以下3个地址的概念:
通俗来说:cpu能够访问的内存范围
现象:电脑装了32位的系统,明明内存条有8G,却只能识别3.8G左右,这是因为32位仅能表示/访问
2^32^=4,294,967,296bit=4,194,304Kb=4096Mb=4G左右。只有装了64位的,才能够识别到8G。
树莓派装载32位操作系统,寻址自然是4G。
树莓派的内存
cat /proc/meminfo
大概是927M
硬件实际地址或绝对地址
逻辑地址(基于算法的地址,软件层面的地址,是假地址)便称为虚拟地址
以树莓派为例,总线可以访问4G,物理地址只有1G,但需要运行的程序>1G,如果把程序全部都加载到内存是不可取的。
物理地址数据的运行真正是拿虚拟地址来操作的,虚拟地址可以比1G大,总线地址能看到4个G,就可以把1个G的物理地址映射成4个的虚拟地址。当物理地址不能满足程序运行空间需求时,如果没有虚拟地址,程序就不能正常运行。
cat /proc/cpuinfo
BCM2835,它是ARM-cotexA53架构

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

物理地址的1M通过扩充映射成为4M的虚拟地址,这中间有个设计的算法叫页表。
这个表决定了这个4M被映射到虚拟内存的哪一个段,通过MMU进行管理。
将虚拟地址映射为物理地址有一个算法(页表)决定了将虚拟地址映射到物理地址的哪个位置,页表是通过MMU(分页内存管理单元)来管理的,就是设计完页表后通过MMU来执行将虚拟地址映射为物理地址。
MMU:Memory Management Unit
之前驱动框架的代码仅仅用来检测、走一遍整个驱动的架构。那么在这一个小节,将着手实现pin4引脚输出高 / 低电平。
驱动开发两大利器:芯片手册和电路图(电路图主要用来寻找寄存器,树莓派的芯片手册清楚地给出了各个寄存器,所以电路图很难找到)。
General Purpose I/O (GPIO)板块
查看芯片手册的目的性很强:做哪一块的开发,就只看那一块,现在要开发的是GPIO,熟悉控制IO口的寄存器最为重要。
如果看完这部分的文档,你对于以下几个问题(后面有解析)有清晰的答案,说明你真正读懂了这一部分的开发。
①操作逻辑:简言之就是怎么进行配置相关寄存器,这些配置步骤和思想其实都很类似。
②需要重点掌握的寄存器有哪些?例如输入 / 输出控制寄存器
输出 0 / 1控制寄存器
清除状态寄存器
捕捉信息
在新的平台也要学会捕捉类似的关键信息:选择输入还是输出,0/1,怎么清除,上升沿下降沿等。(配置过32 / 51寄存器的应该对这些很熟悉)
从下图中可以大概了解到所有的IO口被分成了0~5组。

描述部分也很重要,大多涉及使用方法
总共有6个组(0组~5组)
pin0-pin9 0组
![]()
pin10-pin19 1组
pin20-pin29 2组
pin30-pin39 3组
pin40-pin49 4组
pin50-pin53 5组

IO编号要看好



通过文档阅读,可以整理出关键的信息:
有3个最基本的要清楚:
①选择IO是 输入 / 输出控制寄存器:GPFSEL
②输出0 / 1寄存器:GPSET
③清除寄存器:GPCLR
操作逻辑:
以GPFSEL0寄存器举例,要操作的引脚为pin4,其对应的分组为0。只要在这个分组下,把14-12位设置为001,就能配置pin4引脚为输出。
总之还是要自己多看多翻阅,这里仅为简单的导读。
- volatile unsigned int* GPFSEL0 = NULL;
- volatile unsigned int* GPSET0 = NULL;
- volatile unsigned int* GPCLR0 = NULL;
要想写出上面的代码,要掌握以下几点:
其中寄存器的0表示的是分组,目标操作的IO是pin4,由文档可知,属于寄存器分组0。
加volatile在此处是 : 防止编译器优化(可能是省略,也可能是更改)这些寄存器变量,常见于在内核中对IO口进行操作。
volatile的作用是作为指令关键字,确保本条 指令不会因编译器的优化而省略,且要求每次直接读值
在①的基础上,在驱动的初始化pin4_drv_init中添加寄存器地址配置
- GPFSEL0 = (volatile unsigned int *)ioremap(0x3f200000,4);
- GPSET0 = (volatile unsigned int *)ioremap(0x3f20001C,4);
- GPCLR0 = (volatile unsigned int *)ioremap(0x3f200028,4);
要想写出上面的代码,要掌握以下几点:
并不是用下图这个地址来对应GPIO功能选择寄存器0的地址,否则编译后运行会有段错误。

IO口的起始地址是0x3f000000,加上GPIO的偏移量0x2000000,所以GPIO的实际物理地址应该是从0x3f200000开始的,然后在这个基础上进行Linux系统的MMU内存虚拟化管理,映射到虚拟地址上,编程都是操作虚拟地址。
根据偏移值,弄清楚寄存器的物理地址(真实地址)
可以看到寄存器GPSET0相对于GPIO物理地址的偏移值为1C。即0x3f20001C
同样的方法,寄存器GPCLR0的偏移值为28,即0x3f200028
寄存器GPFSEL0的偏移值为0,即0x3f200000
ioremap函数因为代码操作的是虚拟地址,代码中直接用物理地址肯定不行,需要进行转换,将IO口寄存器映射成普通内存单元进行访问。
使用函数ioremap:
函数原型:
void *ioremap(unsigned long phys_addr, unsigned long size)
phys_addr:要映射的起始的IO物理地址;
size:要映射的空间的大小;
可以看到只要32位寄存器GPFSEL0的14-12位配置为001,其它位不管,即可配置pin4为输出引脚

当然直接暴力赋值(0000…001…0000)是不可取的,会把其他的IO口给影响。最好的结果是只改变了14-12位。
运用与(&) / 或(|)运算进行位操作
- *GPFSEL0 &= ~(0x6 << 12);
- *GPFSEL0 |= (0x1 << 12);
获取上层write函数的值:copy_from_user函数
函数介绍
unsigned long copy_from_user(void * to, const void __user * from, unsigned long n)
此函数将from指针指向的用户空间地址开始的连续n个字节的数据产送到to指针指向的内核空间地址,简言之是用于将用户空间的数据传送到内核空间
第一个参数to是内核空间的数据目标地址指针,
第二个参数from是用户空间的数据源地址指针,
第三个参数n是数据长度。
如果数据拷贝成功,则返回零;否则,返回没有拷贝成功的数据字节数。
- int userCmd;上层写的是整型数1,底层就要对应起来用int.如果是字符则用char
-
- copy_from_user(&userCmd,buf,count);
-
- if(userCmd == 1){
- printk("set 1\n");
- *GPSET0 |= 0x1 << 4;
- }else if(userCmd == 0){
- printk("set 0\n");
- *GPCLR0 |= 0x1 << 4;
- }else{
- printk("cmd error\n");
- }
说明(这也是操作逻辑的一部分啦):
①这个GPSET0,0指的是分组,不是设置成低电平。
②左移4位,是因为GPSET0寄存器的第4位对应pin4,只要把第4位设置为1,表示这个寄存器就对pin4发挥作用,设置成高电平,如果是0则 no effct(手册内容)。
iounmap函数void iounmap(void* addr)//取消ioremap所映射的IO地址
- iounmap(GPFSEL0);
- iounmap(GPSET0);
- iounmap(GPCLR0);
- #include
//file_operations声明 - #include
//module_init module_exit声明 - #include
//__init __exit 宏定义声明 - #include
//class devise声明 - #include
//copy_from_user 的头文件 - #include
//设备号 dev_t 类型声明 - #include
//ioremap iounmap的头文件 -
- static struct class *pin4_class;
- static struct device *pin4_class_dev;
-
- static dev_t devno; //设备号
- static int major =231; //主设备号
- static int minor =0; //次设备号
- static char *module_name="pin4"; //模块名
-
- volatile unsigned int* GPFSEL0 = NULL;
- volatile unsigned int* GPSET0 = NULL;
- volatile unsigned int* GPCLR0 = NULL;
-
- //pin4_open函数
- static int pin4_open(struct inode *inode,struct file *file)
- {
- printk("pin4_open\n"); //内核的打印函数,和printf类似
- //open的时候配置pin4为输出引脚
- *GPFSEL0 &= ~(0x6 << 12);
- *GPFSEL0 |= (0x1 << 12);
-
- return 0;
- }
-
- //pin4_write函数
- static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
- {
- int userCmd;//上层写的是整型数1,底层就要对应起来用int.如果是字符则用char
-
- printk("pin4_write\n");
- //获取上层write的值
- copy_from_user(&userCmd,buf,count);//用户空间向内核空间传输数据
-
- //根据值来执行操作
- if(userCmd == 1){
- printk("set 1\n");
- *GPSET0 |= 0x1 << 4;
- }else if(userCmd == 0){
- printk("set 0\n");
- *GPCLR0 |= 0x1 << 4;
- }else{
- printk("cmd error\n");
- }
-
- return 0;
- }
-
- static struct file_operations pin4_fops = {
-
- .owner = THIS_MODULE,
- .open = pin4_open,
- .write = pin4_write,
- };
-
- int __init pin4_drv_init(void) //驱动的真正入口
- {
-
- int ret;
- printk("insmod driver pin4 success\n");
- 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 = (volatile unsigned int *)ioremap(0x3f200000,4);
- GPSET0 = (volatile unsigned int *)ioremap(0x3f20001C,4);
- GPCLR0 = (volatile unsigned int *)ioremap(0x3f200028,4);
-
- return 0;
- }
-
- void __exit pin4_drv_exit(void)//可以发现和init刚好是相反的执行顺序。
- {
- 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); //入口:内核加载驱动的时候,这个宏会被调用,而真正的驱动入口是它调用的函数
- module_exit(pin4_drv_exit);
- MODULE_LICENSE("GPL v2");
- #include
- #include
- #include
- #include
-
- int main()
- {
- int fd;
- int cmd;
-
- fd = open("/dev/pin4",O_RDWR);
- if(fd < 0){
- printf("open failed\n");
- perror("reson");
- }else{
- printf("open success\n");
- }
-
- printf("请输入0 / 1\n 0:设置pin4为低电平\n 1:设置pin4为高电平\n");
- scanf("%d",&cmd);
-
- if(cmd == 0){
- printf("pin4设置成低电平\n");
- }else if(cmd == 1){
- printf("pin4设置成高电平\n");
- }
-
- fd = write(fd,&cmd,1);//写一个字符'1',写一个字节
- return 0;
- }
-
sudo rmmod pin4drive
用lsmod查看是否卸载成功。
此文(二.3:在Ubuntu上交叉编译) 有涉及,一样的操作不再赘述。
注意:
因为在上文( 二、3,4)中,树莓派上已经有来自Ubuntu发送过来的上层文件pin4test和驱动文件.ko。
而在Ubuntu的/char目录下也因为模块编译生成了.ko ,.mod等文件
但这都没关系,直接复制上文新的驱动框架,新的上层代码,打开原来的2个.c文件(在上文 二.3 中创建的),覆盖。之后进行交叉编译,新生成的文件会覆盖掉原来的文件。
框架交叉编译后:
红色框表示编译生成了需要的模块
而蓝色框的警告可以不用理会(copy_form_user前两个参数传入的是空类型的指针,在框架代码中没有进行强制转换)
pin4_drv_init函数是在demsg后被调用
sudo insmod pin4driver.ko
用dmesg可以看到内核打印出“驱动装载成功”(打印信息来自框架代码)
给权限
sudo chmod 666 /dev/pin4
./pin4test
驱动成功运行
输入1时,用命令gpio readall查看pin4引脚变化,应为OUT 1

输入0时,再用命令gpio readall查看pin4引脚变化,应为OUT 0

用dmesg打开内核打印界面,可以看到内核的printk已经被调用,配置执行。

当然,很多情况下不是直接就能够成功运行的,学会调试很重要。
出问题先看上层(因为上层简单,好修改),后看底层。
Mode我们配置的是输出模式,如果是IN,或者ALT2等,说明底层的模式配置出了问题,大多是寄存器的移位没搞明白。
多看看内核的打印信息,write打印信息,变量的值等等。



大数据的快速拷贝单元。
使用cp指令拷贝大文件会很大程度占用CPU资源,DMA是专门用来做协助数据拷贝的微控制器,CPU可以发动DMA使其进行数据的拷贝。
多用于检测原来的“同名”文件是否已经被新的替代,也可以用作检查拷贝过程中是否损坏。
md5sum file.c
唯一标识,相同则两个文件为同一文件
在命令模式下,首先执行 gg 这里是跳至文件首行
再执行:dG 这样就清空了整个文件!