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