| 编辑推荐: | 
                                   
                                   
                                     
                                      本文主要介绍了从led驱动开始学习linux驱动相关内容。 希望对你的学习有帮助。  
                                      本文来自于微信公众号linux大陆,由火龙果软件linda编辑、推荐。 | 
                                   
                                  | 
                             
                           
                           
                           
                            本篇文章继续分享驱动基础: 与硬件无关的led驱动 
                            回顾hello驱动程序,我们的根据实际需求对其进行写字符串与读字符串操作。这里我们当然也要根据实际来思考我们的led驱动程序。 
                          在stm32点灯的时候,一般输出低电平点灯,输出高电平灭灯。在嵌入linux操作系统的情况下,我们自然也要想到有个写1/0的思想。 
                          类比我们上一篇的hello程序: 
                           
                          
                          我们的led程序自然要写入的数据为0/1来点亮、熄灭led。 
                          这里我们做的实验室与硬件无关的led实验:我们的驱动程序在收到应用程序发送过来的0时打印led on、收到1时打印led 
                            off。 
                          模仿上一篇的hello程序,我们修改得到的与硬件无关的led程序(核心部分)如下: 
                        led应用程序: 
                             
                          
                          led驱动程序: 
                           
                          
                            
                          
                          加载led驱动模块及运行应用程序: 
                           
                         与硬件有关的led驱动 
                            上面那一节分享的是与硬件无关的led驱动实验,主要是为了理清led驱动的大体思路。这里我们再加入与硬件有关的相关操作以构造与硬件有关的led驱动程序。 
                          我们在进行stm32的裸机编程的时候,对一些外设进行配置其实就是操作一些地址的过程,这些外设地址在芯片手册中可以看到: 
                            
                          
                          这是地址映射图,这里图中只是列出的外设的边界地址,每个外设又有很多寄存器,这些寄存器的地址都是对外设基地址进行偏移得到的。 
                          同样的,对于nxp的imx6ull芯片来说,也是有类似这样的地址的: 
                            
                          
                          此时我们要编写linux系统下的led驱动,涉及到硬件操作的地方操作的并不是这些地址(物理地址),而是操作系统给我们提供的地址(虚拟地址)。 
                          操作系统根据物理地址来给我们生成一个虚拟地址,我们的led驱动操控这个地址就是间接的操控物理地址。 
                          至于这两个地址是怎么联系起来的,里面个原理我们暂且不展开。我们从函数层面来看,内核给我们提供了ioremap 
                            函数,这个函数可以把物理地址映射为虚拟地址。 
                          这个函数在内核文件arch/arm/include/asm/io.h 
                            中: 
                          
                            
                               void __iomem *ioremap(resource_size_t res_cookie, size_t size);
  
  |   
 
                            res_cookie:要映射给的物理起始地址 。 
                            size:要映射的内存空间大小。 
                            返回值:指向映射后的虚拟空间首地址。 
                            与ioremap函数相对应的函数为: 
                        
                            
                              void iounmap (volatile void __iomem *addr)
   
 |   
  
                            addr:要取消映射的虚拟地址空间首地址。 
                            地址映射完成之后,我们可以直接通过指针来访问虚拟地址,如: 
                         
                            
                               *gpio5_dr &= ~(1 << 3);  /* gpio5_io03输出低电平 */ *gpio5_dr |= (1 << 3);   /* gpio5_io03输出高电平 */
  
  |   
 
                            这里简单介绍一下i.mx 6ull的gpio。对于i.mx 6ull来说,以数字来给io端口(组别)命令,gpio5为第五组,所以gpio5_io03为第五组端口的第3个引脚。 
                          而stm32中是以大写字母来表示端口(组别),如pa3表示a端口的第3个引脚。 
                          i.mx 6ull有 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;
  
  |   
 
                            地址映射完成之后,我们不仅可以通过指针来访问虚拟地址,而且还可以使用内核给我们提供的一些读写函数: 
                         
                            
                               /* 写操作函数 */ void writeb(u8 value, volatile void __iomem *addr); void writew(u16 value, volatile void __iomem *addr); void writel(u32 value, volatile void __iomem *addr); /* 读操作函数 */ u8 readb(const volatile void __iomem *addr); u16 readw(const volatile void __iomem *addr); u32 readl(const volatile void __iomem *addr);
  
  |   
                            
                          
                            writeb、 writew 和 writel 这三个函数分别对应 8bit、 16bit 和 32bit 
                            写操作,参数 value 是要写入的数值, addr 是要写入的地址。 
                          readb、 readw 和 readl 这三个函数分别对应 8bit、 16bit 和 32bit 
                            读操作,参数 addr 就是要读取写内存地址,返回值就是读取到的数据。 
                          此时我们可以把上一节的led_init函数led_drv_write函数进行修改: 
                           
						   
                          与stm32一样,对于i.mx 6ull的gpio外设来说,也有很多寄存器: 
                           
                          
                          上面我们只是点一个灯,如果是要点多个灯呢?那就得操控多个gpio。如果进行地址映射的写法还像上面那样,代码就会显得很臃肿。 
                          回想一下我们stm32,gpio外设通过结构体来管理它的寄存器: 
                        
                          
                          这里的__io是个宏,代表的是c语言的关键字volatile ,为了防止编译器对我们的一些硬件操作进行优化,从而得不到想要的结果。比如: 
                        
                            
                               /* 假设reg为寄存器的地址 */ uint32 *reg; *reg = 0;/* 点灯 */ *reg = 1;/* 灭灯 */
  
  |   
  
                            此时若是reg不加volatile进行修饰,则点灯操作将被优化掉,只执行灭灯操作。 
                          在这里,我们也可以模仿stm32那样子,用一个结构体来对i.mx 6ull的gpio的寄存器进行管理,如: 
                        
                            
                               struct gpio_regdef {   volatile unsigned int dr;   volatile unsigned int gdir;   volatile unsigned int psr;   volatile unsigned int icr1;   volatile unsigned int icr2;   volatile unsigned int imr;   volatile unsigned int isr;   volatile unsigned int edge_sel; };
  
  |   
                            
                          
                            结构体里的成员排序是要按照特定顺序来的: 
                         
                          
                          因为这些寄存器都是相对于gpio外设的基地址作偏移得到的,比如: 
                            
                          
                          不能打乱顺序,否则就不能正确访问到对应的寄存器了。用结构体进行管理之后,我们就可以用类似下面的方式进行映射: 
                        
                            
                               struct gpio_regdef *gpio5 = ioremap(0x20ac000, sizeof(struct gpio_regdef));
  
  |   
  
                            然后就可以向stm32那样来操控gpio寄存器,如: 
                         
                            
                               gpio5->dr &= ~(1 << 3);  /* gpio5_io03输出低电平 */ gpio5->dr |= (1 << 3);   /* gpio5_io03输出高电平 */
  
  |   
  led驱动(升级版) 
                            上一节我们分享的led驱动是一个常规的led驱动,只能适用于我们当前的开发版,所以是一个专用的led驱动程序。 
                          若是换了另一块板,led所连接的gpio引脚可能不一样了,我们就修改我们的驱动程序led_drv.c里与寄存器相关的操作。 
                          有没有更好的办法不用再修改我们的led_drv.c驱动程序了? 
                          若是led_drv.c不用再修改了,那么这个led_drv.c驱动就是一个通用的驱动程序了。具体可查看韦东山老师的《嵌入式linux应用开发完全手册第2版》第五篇第3~7节进行学习。 
                          下面来简单地梳理一下: 
                          
                          
                          由于篇幅问题,具体的部分就不贴出来了。 
                          之前的笔记中:《c语言、嵌入式重点知识:回调函数》中我们也有提到通用与专用的含义,可以了解了解加深对这两个词的认识。 
                          这里我们学到了很重要的思想软件分层的思想及技巧,但也只是点了一下,未来的路还很长,需要持续学习,继续提高。 
                             
                           |