树莓派字符设备驱动点灯

BCM2835 关于MMU的描述


BCM2835除了arm的MMU之外,还使用了第二个MMU将物理内存地址(ARM physical address) 映射成系统总线地址(VC CPU bus address) 。数据手册中罗列的寄存器地址并不是物理内存地址,而是系统总线地址。

因此,在调用 ioremap() 函数前,需要将总线地址转换成物理地址。

从数据手册中得知:总线地址上的外设寄存器起始地址是0x7000 0000 ,物理地址上的外设寄存器起始地址是0x2000 0000。

将总线地址换成物理地址。

CPU bus addressField NameARM physical addressDescription
0x7E20 0000GPFSEL00X2000 0000Select function
0X7E20 001CGPSET00X2000 001CSet bit
0X7E20 0028GPCLR00X2000 0028Clear bit

驱动模块程序示例

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/string.h>

#define cdevname "test_cdev"

enum gpio{
   GPIO0 = 0U,
   GPIO1,
   GPIO2 = 4U,//由于GPIO2是i2c,不希望修改该端口功能,我这里临时改为GPIO4
   GPIO3,
   GPIO4,
   GPIO5,
};

#define GPFSEL0 0x20200000U //GPIO0~9的复用寄存器,三个bit位表示一个模式 in:000 out:001
#define GPSET0 0x2020001CU //0~31bit位分别对应GPIO0~31,将bit设置1输出高,设置0无效
#define GPCLR0 0x20200028U //0~31bit位分别对应GPIO0~31,将bit设置1输出低,设置0无效

volatile unsigned int * GPIOAF_BASE = NULL; //定义对应寄存器的虚拟内存起始地址
volatile unsigned int * GPIOSET_BASE = NULL;
volatile unsigned int * GPIOCLR_BASE = NULL;

int major = 240; //手动指定设备号
char copybuff[128] = {0,}; //交换用户数据的缓存空间

ssize_t cdev_read(struct file *file, char __user *ubuffp, size_t size, loff_t *offs) {
   int error;
   if(size > sizeof(copybuff))
       size = sizeof(copybuff);
   
   error = copy_to_user(ubuffp, copybuff, size); //成功返回0, 失败返回未成功拷贝字节个数
   if(error) {
       printk(KERN_ERR "copy to user error, failed cpy size:%d byte\n", error);
       return -EINVAL; //返回错误码,注意是负数
  }

   printk(KERN_INFO "%s, %s, %d\n", __FILE__, __func__, __LINE__);
   return size;    //read()成功返回读取的字节个数
}
ssize_t cdev_write(struct file *file, const char __user *ubuffp, size_t size, loff_t *offs) {
   int error;
   if(size > sizeof(copybuff))
       size = sizeof(copybuff);

   error = copy_from_user(copybuff, ubuffp, size); //成功返回0, 失败返回未成功拷贝字节个数
   if(error) {
       printk(KERN_ERR "copy from user error, failed copy size:%d byte\n", error);
       return -EINVAL; //返回错误码,注意是负数
  }
       
   if(!strcmp(copybuff, "LED_ON")) { //strcmp()两者相同返回 0
       *GPIOSET_BASE |= (0x01U << GPIO2);
       printk("LED_ON\n");
  }

   if(!strcmp(copybuff, "LED_OFF")) {
       *GPIOCLR_BASE |= (0x01U << GPIO2);
       printk("LED_OFF\n");
  }
   memset(copybuff, 0, size);
   
   printk(KERN_INFO "%s, %s, %d\n", __FILE__, __func__, __LINE__);
   return size;
}
int cdv_open(struct inode *inode, struct file *file) {
   printk(KERN_INFO "%s, %s, %d\n", __FILE__, __func__, __LINE__);
   return 0;
}
int cdev_close(struct inode *inode, struct file *file) {
   printk(KERN_INFO "%s, %s, %d\n", __FILE__, __func__, __LINE__);
   return 0;
}

const struct file_operations fops = {
  .open   = cdv_open,
  .read   = cdev_read,
  .write  = cdev_write,
  .release = cdev_close,
};

static int __init chrdev_init(void) {
   int ret;
   //注册字符设备驱动
   ret = register_chrdev(major, cdevname, &fops);
   if(ret < 0) {
       printk(KERN_ERR "register char device error");
       return -EAGAIN;
  }

   GPIOAF_BASE = ioremap(GPFSEL0, 4); //将物理地址映射成虚拟地址,成功返回地址失败返回NULL
   if(GPIOAF_BASE == NULL) {
       printk(KERN_EMERG "GPIOAF_BASE ioremap error\n");
       return -ENOMEM;
  }

   GPIOSET_BASE = ioremap(GPSET0, 4);
   if(GPIOSET_BASE == NULL) {
       printk(KERN_EMERG "GPIOSET_BASE ioremap error\n");
       return -ENOMEM;
  }

   GPIOCLR_BASE = ioremap(GPCLR0, 4);
   if(GPIOCLR_BASE == NULL) {
       printk(KERN_EMERG "GPIOCLR_BASE ioremap error\n");
       return -ENOMEM;
  }

   //设置 GPIO2 为输出模式
   *GPIOAF_BASE &= ~(0x07U << (3*GPIO2));
   *GPIOAF_BASE |= 0x01U << (3*GPIO2);

   printk(KERN_INFO "%s, %s, %d\n", __FILE__, __func__, __LINE__);
   return 0;
}

static void __exit chrdev_exit(void) {
   unregister_chrdev(major, cdevname); //注销设备号

   iounmap(GPIOAF_BASE); //取消虚拟地址映射
   iounmap(GPIOSET_BASE);
   iounmap(GPIOCLR_BASE);
   
   GPIOAF_BASE = NULL; //防止出现野指针
GPIOSET_BASE = NULL;
GPIOCLR_BASE = NULL;

   printk(KERN_EMERG "%s, %s, %d\n",__FILE__, __func__, __LINE__);
}

module_init(chrdev_init);
module_exit(chrdev_exit);
MODULE_LICENSE("GPL");

测试程序示例

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

char LED_ON[] = "LED_ON";
char LED_OFF[] = "LED_OFF";

int main(int argc, char const *argv[])
{
   int fd;
   fd = open("/dev/test_cdev", O_RDWR);
   if (fd == -1)
  {
       perror("open /dev/test_cdev error");
       return -1;
  }
   while (1)
  {
       write(fd, LED_OFF, sizeof(LED_OFF));
       printf("%s\n", LED_OFF);
       sleep(1);
       write(fd, LED_ON, sizeof(LED_ON));
       printf("%s\n", LED_ON);
       sleep(1);
  }

   close(fd);
   return 0;
}

测试步骤

#编译模块和编译app
​
#1.安装模块
sudo insmod cdev.ko
#2.创建设备节点
sudo mknod /dev/test_cdev c 240 0
#3.修改设备文件权限
sudo chmod 777 /dev/test_cdev
#4.运行程序
./app
​
#树莓派与LED硬件连接:GPIO4 -> 1K电阻 -> LED -> GND

源代码和数据手册下载链接