Linux内核编程(四)gpio寄存器驱动编写


  
   寄存器操作通常使用较少,因为已经有系统的库函数接口供给我们使用。本文主要为了了解如何使用寄存器来编写驱动,这里要根据实际的开发板手册进行编写。

一、c语言位操作

  在操作寄存器前,我们需要复习一下c语言中的位操作。方便我们后续编写gpio寄存器。

1. 按位逻辑运算符

(1) &(按位与):有0为0,全1为1。

int a=153;  // 1001 1001
int b=60;   //0011 1100

a &= b;     //a=a&b;
//(1001 1001) & (0011 1100)
结果为:
a=24        //0001 1000;

(2)|(按位或):有1为1,全0为0。

int a=153;   //1001 1001
int b=60;    //0011 1100

a |= b;     // a= a | b;
//(1001 1001) | (0011 1100)
结果为:
a=189      //1011 1101;

(3)~(非):所有位取反,1变0,0变1。

~(0001 1100)	 //表达式
 (1110 0011)	 // 结果值

(4)^(异或):位值不同为1,相同为0。

(1100 0001) ^ (0101 1011)
结果为:
(1001 1010)

2. 位移运算符

(1)左移:<<

int val = 1;	//0000 0001
val <<= 2;     //(0000 0001) << 2
结果为:
(0000 0100)

(2)右移:>>

int val = 16;   //0001 0000
val >>=3;      //(0001 0000) >> 3
结果为:
(0000 0010)

3. 寄存器位操作用法

(1)置1位( |= ): 将寄存器MODER第8位置1,第21位置1。

  GPIOC->MODER  |= (1<<8);   
  GPIOA ->MODER |= (1<<21);

(2)清0位( &=~ ): 将寄存器OTYPER第4位置0。

GPIOC->OTYPER &=~ (1<<4);

(3)切换位( ^= ): 是指打开已关闭的位,或关闭已打开的位。可以使用按位异或运算符(^)切换位。

val为0000 1111 ,MASK是1011 0110

val ^= MASK;
(0000 1111) ^ (1011 0110)
结果为
(1011 1001)

(4)掩码: 隐藏了val中的其他位。

int val=0xff;  // 1111 1111
int MASK=2;    // 0000 0010
val &= MASK
//val中除了1号位以外的所有位都设置为0。
//1号位的值不变。这个过程叫做使用掩码,掩码中的0隐藏了val中的其他位。

  

二、gpio寄存器编写流程(如操作PG_13口。实现功能:按下key按键时,蜂鸣器响,灯亮。)

  
具体需要根据开发板手册来使用。这里为了演示如下:
  

1. 需要操作的寄存器

(1)使得gpio正常使用,一般配置以下寄存器即可:

a.功能寄存器(Port Controller Register)。 //模式
b.数据寄存器 (data Register)。 //读写数据

(2)以下寄存器不配置,使用默认即可。

a.驱动等级寄存器。(Multi-Driving Register) //默认等级1级即可
b.上下拉寄存器。(PULL Register) //默认无上下拉即可。

  

2. 获取相应模块内存映射的基地址。(端口控制寄存器的基地址如下图所示)

  由于端口寄存器都在PIO模块下,所以需要获取PIO模块的地址作为基地址。

#define PIO (0X01C20800)          //PIO的基地址。

                在这里插入图片描述
  

3. 获取PG寄存器的起始地址

  如:GPIOG_13端口在PG第1组寄存器 PG Configure Register 1中。要操作GPIOG_13就要获取PG Configure Register 1寄存器的地址。
  寄存器地址:基地址+偏移量。
  PG Configure Register 1的地址为: 0X01C20800 + 0XDC
在这里插入图片描述

  

4. 地址映射:将物理地址映射为虚拟地址。

unsigned int *vir_pg_base;    //接收PG隐射后的初始地址

vir_pg_base=ioremap(addr, size);  
  //addr:要操作寄存器的起始地址。
  //size:映射内容的大小。
  //返回值:返回虚拟地址首地址。

(1)由于我们可能操作多个PG口,所有我们不妨将整个的PG组进行地址映射。将PG第0组的地址映射即可。
此时PG第0组寄存器起始地址为:0X01C20800 + 0XD8。将该地址作为上述函数的addr参数。
        在这里插入图片描述
  
(2)映射内容的大小:PG组共有9个寄存器。则需要映射大小为36个字节。将该字节数36大小传入size参数中。
      在这里插入图片描述
  

5. 计算映射后的功能寄存器和数据寄存器的地址

(1)计算方式: 映射后的初始地址 + 偏移量。 每个寄存器之间相差四个字节。

unsigned int *vir_pg_cfg1;
unsigned int *vir_pg_data;

vir_pg_cfg1 = vir_pg_base +1;  //计算出PG第1组寄存器的地址。
vir_pg_data = vir_pg_base +4; //计算出数据寄存器的地址。

(2) 为什么是+1或者+4呢?
  原因: 这是与定义的变量类型有关的。定义的变量类型为int,所以+1相当于加4个字节。如果定义的变量为char类型,那么+1相当于加1个字节。
  映射后的初始地址为PG第0组的地址,操作的PG13口在PG第1组寄存器中,每组寄存器相差四个字节。第0组寄存器和第1组寄存器相差四个字节,所以为+1。数据寄存器与PG第0组寄存器初始地址相差16个字节,所以为+4。

                
  

6. 功能寄存器的位操作

  因为PG13口为蜂鸣器,所以我们要配置输出模式。(即 20位为1,21位为0,22位为0即可)
在这里插入图片描述
在这里插入图片描述

*vir_pg_cfg1 &= ~(0x7 <<20);    //将第20-22位全置0。
*vir_pg_cfg1 |=  (0x1 <<20);    //将第20位置1。

  

7. 数据寄存器的位操作

因为操作PG13口,则需要操作第13位。蜂鸣器置0不响,蜂鸣器置1响。
在这里插入图片描述

*vir_pg_data &= ~(0x1 <<13); 	//配置无效电平   
*vir_pg_data |=  (0x1 <<13); 	//配置有效电平   

  
  

附录一:计算地址偏移量方法

  我们可以直接通过查看手册获取偏移量,以下过程只为了明白偏移量如何计算。查看要操作的GPIO口在哪个寄存器组中。再根据n=x-‘A’(如GPIOG,则x为‘G’)算出偏移量。
在这里插入图片描述
计算PG各寄存器组的地址偏移量。

(1)PG第0组寄存器 PG_CFG0的偏移量。Offset=( ‘G’ - ‘A’ )*(0x24)+(0x00)。得0xD8。
查看手册验证结果:
        在这里插入图片描述

(2)PG第1组寄存器 PG_CFG1的偏移量。Offset=( ‘G’ - ‘A’ )(0x24)+(0x04)。得0xDC。
查看手册验证结果:
        在这里插入图片描述
(3)PG第2组寄存器 PG_CFG2的偏移量。Offset=( ‘G’ - ‘A’ )
(0x24)+(0x08)。得0xE0。
查看手册验证结果:
        在这里插入图片描述
(4)PG第3组寄存器 PG_CFG3的偏移量。Offset=( ‘G’ - ‘A’ )*(0x24)+(0x0C)。得0xE4。
查看手册验证结果:
        在这里插入图片描述
  

附录二:完整代码

(1)gpio.c:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/sys_config.h>
#include <linux/gpio.h>
#include <asm/uaccess.h>

#define BEEP	 (13)    //GPIOG13
#define KEY      (4)     //GPIOB4
#define LED      (6)     //GPIOB6


#define KBUF_SIZE (1024)
#define MIN(x, y)   ((x)<(y)?(x):(y))
char wbuf[KBUF_SIZE];   //存储用户传输来的数据。
char rbuf[KBUF_SIZE];   //存储用户要读的数据。


#define PIO (0X01C20800)          //PIO的基地址。
#define PG_BASE ((PIO)+ 0XD8)     //PG的基地址
#define PB_BASE ((PIO)+ 0x24)     //PB的基地址

#define BASE_SIZE 36	              //PG组所有寄存器一共的字节大小


unsigned int *vir_pg_base;   //虚拟的PG初始地址
unsigned int *vir_pg_cfg1;   //虚拟的PG第1组寄存器的地址
unsigned int *vir_pg_data;   //虚拟的PG数据寄存器地址

unsigned int *vir_pb_base;   //虚拟的PG初始地址
unsigned int *vir_pb_cfg0;   //虚拟的PG第1组寄存器的地址
unsigned int *vir_pb_data;   //虚拟的PG数据寄存器地址




int gpio_open (struct inode *node, struct file *fp)
{
    //pg13  BEEP
	*vir_pg_cfg1 &= ~(0x7 <<20);
	*vir_pg_cfg1 |=  (0x1 <<20);    //配置pg13为输出模式
	*vir_pg_data &= ~(0x1 <<13);     //配置无效电平

	 //pb4  KEY
	*vir_pb_cfg0 &= ~(0x7 <<16);   //配置pb4为输入模式

	 //pb6  LED
	*vir_pb_cfg0 &= ~(0x7 <<24); 
	*vir_pb_cfg0 |=  (0x1 <<24);    //配置pb6为输出模式
	*vir_pb_data |=  (0x1 <<6);     //配置无效电平,led高电平灭
	return 0;
}
int gpio_release (struct inode *node, struct file *fp)
{
	*vir_pg_data &= ~(0x1 <<13); 	//配置无效电平
    *vir_pg_cfg1 |=  (0x7 <<20);    //配置pg13为失能 
    
    *vir_pb_cfg0 |=  (0x7 <<16);    //配置pb4为失能

	*vir_pb_data |=  (0x1 <<6);     //配置无效电平,led高电平灭
	*vir_pb_cfg0 |=  (0x7 <<24);    //配置pb6为失能 

	return 0;
}

ssize_t gpio_read (struct file *fp, char __user *ubuff, size_t size, loff_t *offset)
{  
    int ret;
	unsigned char key=(*vir_pb_data >>4) & 0x01;
	
	rbuf[0]=key+48; 
	
	ret=copy_to_user(ubuff, rbuf, MIN(1, size));
	if(ret != 0) {
		pr_err("copy_to_user error\r\n");
		return -EINVAL;
	}
	
	return MIN(1, size);
}


ssize_t gpio_write(struct file *fp, const char __user *ubuf, size_t size, loff_t *offset)
{
	int ret;
	memset(wbuf, 0, sizeof(wbuf));
	ret = copy_from_user(wbuf, ubuf, MIN(KBUF_SIZE, size));
	if(ret != 0) {
		pr_err("copy_from_user error\r\n");
		return -EINVAL;
	}
	
	if( strstr(wbuf, "BEEP_ON") )       *vir_pg_data |=  (0x1 <<13);      //置1
    else if(strstr(wbuf, "BEEP_OFF")) 	*vir_pg_data &= ~(0x1 <<13);     //置0
	else if(strstr(wbuf, "LED_OFF")) 	*vir_pb_data |=  (0x1 <<6);      
	else if(strstr(wbuf, "LED_ON")) 	*vir_pb_data &= ~(0x1 <<6);   
	
	return MIN(KBUF_SIZE, size);  //返回写入的字节数
}


const struct file_operations gpio_fops={
    .owner=THIS_MODULE,     //文件操作集的拥有者是 本模块。
    .read= gpio_read,
  	.write= gpio_write,
  	.open= gpio_open,
  	.release= gpio_release,
};


struct miscdevice mygpio={
	.minor=MISC_DYNAMIC_MINOR,    //系统自动分配次设备号
	.name ="my_gpio",
	.fops=&gpio_fops,
};




static int __init gpio_init(void)
{
  int ret;
  ret=misc_register(&mygpio);  //注册一个杂项设备
  if(ret!=0){
     pr_err("misc_register error");
	 return -EINVAL;   //返回错误编码
  }

  //将物理地址映射为虚拟地址   
   vir_pg_base=ioremap(PG_BASE, BASE_SIZE);   
   if(vir_pg_base ==NULL){
       pr_err("ioremap error");
       goto error1;
   }

   vir_pb_base=ioremap(PB_BASE, BASE_SIZE);   
   if(vir_pg_base ==NULL){
       pr_err("ioremap error");
       goto error2;
   }
   
   vir_pg_cfg1 = vir_pg_base +1; //计算出vir_pg_cfg1的地址。
   vir_pg_data = vir_pg_base +4;//计算出vir_pg_data的地址。

   vir_pb_cfg0 = vir_pb_base +0; //计算出vir_pb_cfg0的地址。
   vir_pb_data = vir_pb_base +4;//计算出vir_pb_data的地址。
   
    return 0;   

error2:       //error2执行后会继续顺着往下执行,执行error1.
	iounmap(vir_pb_base);	   //取消映射
	
error1:
     misc_deregister(&mygpio);  //从内核注销一个杂项设备
	
	 return -1;
}

static void __exit gpio_exit(void)
{
   misc_deregister(&mygpio);  //从内核注销一个杂项设备
}

module_init(gpio_init);
module_exit(gpio_exit);

MODULE_LICENSE("GPL");


(2)app.c:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char **arg)
{
	int fd;
	char data[1];

	fd = open("/dev/my_gpio", O_RDWR);
	if(fd < 0) {
		perror("open error");
		return -1;
	}
	
   while(1){
	    read(fd,data,sizeof(data));
	   if(data[0]=='0'){
		   write(fd, "BEEP_ON", sizeof("BEEP_ON"));
	       write(fd, "LED_ON", sizeof("LED_ON"));
		}else{
		       write(fd, "BEEP_OFF", sizeof("BEEP_OFF"));
			   write(fd, "LED_OFF",  sizeof("LED_OFF"));
	    }
  	}
   
	close(fd);
	return 0;
}

(3)Makefile:

#多个c文件生成一个ko文件。
obj-m +=gpio.o           #把.c文件的名字加.o写到这里

# KDIR 内核源码路径,根据自己需要设置
KDIR:=/home/qjl/work/lichee/linux-3.10

all:
#ARCH: 指当前编译的驱动模块的架构
#CROSS_COMPILE:指明交叉编译器的前缀
#-C: 指定去$(KDIR)目录下执行Makefile
#M:告知Makefile,需要的编译文件在哪
#modules: 这个规则是用于编译驱动模块的
	@make ARCH=arm64 CROSS_COMPILE=aarch64-linux- -C $(KDIR) M=$(PWD) modules 
	@rm -fr .tmp_versions *.o *.mod.o *.mod.c *.bak *.symvers *.markers *.unsigned *.order *~ .*.*.cmd .*.*.*.cmd
	
clean:
	@make ARCH=arm64 CROSS_COMPILE=aarch64-linux- -C $(KDIR) M=$(PWD) modules clean
	@rm -rf *.ko
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
PWM-GPIO驱动程序是Linux内核中的一个驱动模块,用于控制嵌入式系统中的GPIO引脚产生PWM信号。该驱动程序允许开发人员通过编程的方式来控制GPIO引脚的电平变化,从而产生不同占空比的PWM信号。 在Linux内核中,PWM-GPIO驱动程序通过向用户空间提供了相应的接口来实现PWM信号的控制。开发人员可以通过打开相应的设备节点,并使用相应的系统调用函数来设置PWM的频率、占空比等参数,从而实现对GPIO引脚的PWM信号的控制。 驱动程序的核心部分是一个PWM子系统,它与GPIO子系统紧密集成。PWM子系统负责管理PWM信号的生成和控制,而GPIO子系统负责管理GPIO引脚的配置和操作。PWM-GPIO驱动程序在这两个子系统之间起着桥梁的作用。 PWM-GPIO驱动程序的实现方式与硬件平台相关,每个平台可能有不同的具体实现。在驱动程序的初始化过程中,必须先配置GPIO引脚的功能为PWM模式,并将相应的寄存器映射到内核中,以便能够通过对寄存器的操作来控制GPIO引脚。驱动程序还需要初始化PWM子系统,为每个GPIO引脚分配相应的PWM通道,并根据需求设置PWM的频率、占空比等参数。 通过PWM-GPIO驱动程序,开发人员可以方便地利用Linux内核的功能来实现对嵌入式系统中GPIO引脚产生PWM信号的控制。这为开发PWM驱动、控制舵机、LED等应用提供了便捷的方式。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值