在嵌入式Linux系统中,由于从硬件到软件都是自己定制的,所以很多时候需要对自己定义的设备编写驱动程序。本例就以野火STM32MP157开发板为例,讨论如何控制开发板上三个LED的亮灭。
先来看一下LED部分的电路原理图,如下所示。
从上图中可以看到,三个RGB颜色的二极管采用共阳接法, 因此在给STM32MP157的PA13、PG2、PB5端口输出高电平时,三个发光管匀熄灭,输出低电平时,三个发光管匀点亮。
下面给出了完整的驱动程序代码,文件名为led.c。
#include <linux/init.h> #include <linux/module.h> #include <linux/cdev.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/io.h> //以下定义总线及寄存器的物理地址 #define AHB4_PERIPH_BASE (0x50000000) #define RCC_BASE (AHB4_PERIPH_BASE + 0x0000) #define RCC_MP_GPIOENA (RCC_BASE + 0xA28) #define GPIOA_BASE (AHB4_PERIPH_BASE + 0x2000) #define GPIOA_MODER (GPIOA_BASE + 0x0000) #define GPIOA_OTYPER (GPIOA_BASE + 0x0004) #define GPIOA_OSPEEDR (GPIOA_BASE + 0x0008) #define GPIOA_PUPDR (GPIOA_BASE + 0x000C) #define GPIOA_ODR (GPIOA_BASE + 0x0014) #define GPIOA_BSRR (GPIOA_BASE + 0x0018) #define GPIOG_BASE (AHB4_PERIPH_BASE + 0x8000) #define GPIOG_MODER (GPIOG_BASE + 0x0000) #define GPIOG_OTYPER (GPIOG_BASE + 0x0004) #define GPIOG_OSPEEDR (GPIOG_BASE + 0x0008) #define GPIOG_PUPDR (GPIOG_BASE + 0x000C) #define GPIOG_ODR (GPIOG_BASE + 0x0014) #define GPIOG_BSRR (GPIOG_BASE + 0x0018) #define GPIOB_BASE (AHB4_PERIPH_BASE + 0x3000) #define GPIOB_MODER (GPIOB_BASE + 0x0000) #define GPIOB_OTYPER (GPIOB_BASE + 0x0004) #define GPIOB_OSPEEDR (GPIOB_BASE + 0x0008) #define GPIOB_PUPDR (GPIOB_BASE + 0x000C) #define GPIOB_ODR (GPIOB_BASE + 0x0014) #define GPIOB_BSRR (GPIOB_BASE + 0x0018) //以下定义时钟控制寄存器名称 static void __iomem *RCC_MP_AHB4ENSETR; static void __iomem *GPIO_MODER_PA; static void __iomem *GPIO_OTYPER_PA; static void __iomem *GPIO_OSPEEDR_PA; static void __iomem *GPIO_PUPDR_PA; static void __iomem *GPIO_ODR_PA; static void __iomem *GPIO_BSRR_PA; static void __iomem *GPIO_MODER_PB; static void __iomem *GPIO_OTYPER_PB; static void __iomem *GPIO_OSPEEDR_PB; static void __iomem *GPIO_PUPDR_PB; static void __iomem *GPIO_ODR_PB; static void __iomem *GPIO_BSRR_PB; static void __iomem *GPIO_MODER_PG; static void __iomem *GPIO_OTYPER_PG; static void __iomem *GPIO_OSPEEDR_PG; static void __iomem *GPIO_PUPDR_PG; static void __iomem *GPIO_ODR_PG; static void __iomem *GPIO_BSRR_PG; //申明一个字符型设备结构体led_dev struct led_dev {dev_t devid; //设备号struct cdev chrdev; //字符设备结构体struct class *class; //类结构体struct device *device; //设备结构体 }; //定义一个led_dev类型的结构体,名称为led struct led_dev led; //实现open函数,为file_oprations结构体成员函数 static int led_open(struct inode *inode, struct file *filp) { unsigned int tmp;//以下使能GPIOA、GPIOB、GPIOG端口时钟tmp = ioread32(RCC_MP_AHB4ENSETR);tmp |= (0x1 << 6) | (0x1 << 1) | (0x1 << 0);iowrite32(tmp, RCC_MP_AHB4ENSETR);//把led结构体保存在file结构体的私有变量中filp->private_data = &led;return 0; } //实现write函数,为file_oprations结构体成员函数 static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) {unsigned char value;unsigned long n;n = copy_from_user(&value, buf, cnt); //从应用空间获取值switch(value) //根据应用空间的值判断具体操作 {case 0: //全部点亮三个LEDiowrite32(0x20000000, GPIO_BSRR_PA);iowrite32(0x200000, GPIO_BSRR_PB);iowrite32(0x40000, GPIO_BSRR_PG);break;case 1: //点亮红色LEDiowrite32(0x20000000, GPIO_BSRR_PA);break;case 2: //点亮绿色LEDiowrite32(0x40000, GPIO_BSRR_PG);break;case 3: //点亮蓝色LEDiowrite32(0x200000, GPIO_BSRR_PB);break;case 4: //熄灭红色LEDiowrite32(0x2000, GPIO_BSRR_PA);break;case 5: //熄灭绿色LEDiowrite32(0x20, GPIO_BSRR_PB);break;case 6: //熄灭蓝色LEDiowrite32(0x04, GPIO_BSRR_PG);break;case 7: //全部熄灭三个LEDiowrite32(0x2000, GPIO_BSRR_PA);iowrite32(0x20, GPIO_BSRR_PB);iowrite32(0x04, GPIO_BSRR_PG);break;default: //全部熄灭iowrite32(0x2000, GPIO_BSRR_PA);iowrite32(0x20, GPIO_BSRR_PB);iowrite32(0x04, GPIO_BSRR_PG);break;} return 0; } //实现release函数,为file_oprations结构体函数 static int led_release(struct inode *inode, struct file *filp) {unsigned int tmp;//以下禁能GPIOA、GPIOB、GPIOG端口时钟tmp = ioread32(RCC_MP_AHB4ENSETR);tmp &= ~0x43;iowrite32(tmp, RCC_MP_AHB4ENSETR);return 0; } //定义一个file_oprations类型的结构体,名为led_dev_fops,包含上述申明的成员函数 static struct file_operations led_dev_fops = {.owner = THIS_MODULE,.open = led_open, //指定open函数成员.write = led_write, //指定write函数成员.release = led_release, //指定release函数成员 }; //初始化函数,此处为驱动模块的入口函数 static int __init led_init(void) {unsigned int tmp;//以下实现各个寄存器的地址映射RCC_MP_AHB4ENSETR = ioremap(RCC_MP_GPIOENA, 4);GPIO_MODER_PA = ioremap(GPIOA_MODER, 4);GPIO_OTYPER_PA = ioremap(GPIOA_OTYPER, 4);GPIO_OSPEEDR_PA = ioremap(GPIOA_OSPEEDR, 4);GPIO_PUPDR_PA = ioremap(GPIOA_PUPDR, 4);GPIO_ODR_PA = ioremap(GPIOA_ODR, 4);GPIO_BSRR_PA = ioremap(GPIOA_BSRR, 4);GPIO_MODER_PB = ioremap(GPIOB_MODER, 4);GPIO_OTYPER_PB = ioremap(GPIOB_OTYPER, 4);GPIO_OSPEEDR_PB = ioremap(GPIOB_OSPEEDR, 4);GPIO_PUPDR_PB = ioremap(GPIOB_PUPDR, 4);GPIO_ODR_PB = ioremap(GPIOB_ODR, 4);GPIO_BSRR_PB = ioremap(GPIOB_BSRR, 4);GPIO_MODER_PG = ioremap(GPIOG_MODER, 4);GPIO_OTYPER_PG = ioremap(GPIOG_OTYPER, 4);GPIO_OSPEEDR_PG = ioremap(GPIOG_OSPEEDR, 4);GPIO_PUPDR_PG = ioremap(GPIOG_PUPDR, 4);GPIO_ODR_PG = ioremap(GPIOG_ODR, 4);GPIO_BSRR_PG = ioremap(GPIOG_BSRR, 4);//以下使能GPIOA、GPIOB、GPIOG端口时钟tmp = ioread32(RCC_MP_AHB4ENSETR);tmp |= (0x1 << 6) | (0x1 << 1) | (0x1 << 0);iowrite32(tmp, RCC_MP_AHB4ENSETR);//以下把GPIOA、GPIOB、GPIOG端口配置为输出、上位模式tmp = ioread32(GPIO_MODER_PA);tmp &= ~(0x3 << 26);tmp |= (0x1 << 26);iowrite32(tmp, GPIO_MODER_PA);tmp = ioread32(GPIO_MODER_PB);tmp &= ~(0x3 << 10);tmp |= (0x1 << 10);iowrite32(tmp, GPIO_MODER_PB);tmp = ioread32(GPIO_MODER_PG);tmp &= ~(0x3 << 4);tmp |= (0x1 << 4);iowrite32(tmp, GPIO_MODER_PG);tmp = ioread32(GPIO_PUPDR_PA);tmp &= ~(0x3 << 26);tmp |= (0x1 << 26);iowrite32(tmp, GPIO_PUPDR_PA);tmp = ioread32(GPIO_PUPDR_PB);tmp &= ~(0x3 << 10);tmp |= (0x1 << 10);iowrite32(tmp, GPIO_PUPDR_PB);tmp = ioread32(GPIO_PUPDR_PG);tmp &= ~(0x3 << 4);tmp |= (0x1 << 4);iowrite32(tmp, GPIO_PUPDR_PG);//以下设定GPIOA、GPIOB、GPIOG端口初始值iowrite32(0x2000, GPIO_BSRR_PA);iowrite32(0x20, GPIO_BSRR_PB);iowrite32(0x04, GPIO_BSRR_PG);//以下禁能GPIOA、GPIOB、GPIOG端口时钟tmp = ioread32(RCC_MP_AHB4ENSETR);tmp &= ~0x43;iowrite32(tmp, RCC_MP_AHB4ENSETR);//申请主设备号if(alloc_chrdev_region(&led.devid, 0, 1, "led") < 0){printk("Couldn't alloc_chrdev_region!\r\n");return -EFAULT;}led.chrdev.owner = THIS_MODULE;//绑定前面申明的file_oprations类型的结构体到字符设备cdev_init(&led.chrdev, &led_dev_fops);//填充上面申请到的主设备号到字符设备if(cdev_add(&led.chrdev,led.devid, 1) < 0){printk("Couldn't add chrdev!\r\n");return -EFAULT;}//创建类 led.class = class_create(THIS_MODULE, "led_dev");//根据创建的类生成一个设备节点led.device = device_create(led.class, NULL, led.devid, NULL, "led");return 0; } //退出函数,此处为驱动模块的出口函数 static void __exit led_exit(void) {//以下实现各个寄存器的解除映射 iounmap(RCC_MP_AHB4ENSETR);iounmap(GPIO_MODER_PA);iounmap(GPIO_OTYPER_PA);iounmap(GPIO_OSPEEDR_PA);iounmap(GPIO_PUPDR_PA);iounmap(GPIO_BSRR_PA);iounmap(GPIO_MODER_PB);iounmap(GPIO_OTYPER_PB);iounmap(GPIO_OSPEEDR_PB);iounmap(GPIO_PUPDR_PB);iounmap(GPIO_BSRR_PB);iounmap(GPIO_MODER_PG);iounmap(GPIO_OTYPER_PG);iounmap(GPIO_OSPEEDR_PG);iounmap(GPIO_PUPDR_PG);iounmap(GPIO_BSRR_PG);//删除字符设备cdev_del(&led.chrdev);//释放主设备号unregister_chrdev_region(led.devid, 1);//销毁设备节点device_destroy(led.class, led.devid);//销毁类class_destroy(led.class); } module_init(led_init); //模块入口申明 module_exit(led_exit); //模块出口申明 MODULE_LICENSE("GPL"); //GPL协议申明
以上程序在操作端口电平时使用了BSRR寄存器,如果要操作ODR寄存器,则上述代码中的led_write函数可换成如下的形式即可。
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) {unsigned char value;unsigned int tmp;unsigned long n;n = copy_from_user(&value, buf, cnt); //从应用空间获取值switch(value) //根据应用空间的值判断具体操作 {case 0: //全部点亮三个LEDtmp = ioread32(GPIO_ODR_PA);tmp &= ~0x2000;iowrite32(tmp, GPIO_ODR_PA);tmp = ioread32(GPIO_ODR_PB);tmp &= ~0x20;iowrite32(tmp, GPIO_ODR_PB);tmp = ioread32(GPIO_ODR_PG);tmp &= ~0x04;iowrite32(tmp, GPIO_ODR_PG);break;case 1: //点亮红色LEDtmp = ioread32(GPIO_ODR_PA);tmp &= ~0x2000;iowrite32(tmp, GPIO_ODR_PA);break;case 2: //点亮绿色LEDtmp = ioread32(GPIO_ODR_PG);tmp &= ~0x04;iowrite32(tmp, GPIO_ODR_PG);break;case 3: //点亮蓝色LEDtmp = ioread32(GPIO_ODR_PB);tmp &= ~0x20;iowrite32(tmp, GPIO_ODR_PB);break;case 4: //熄灭红色LEDtmp = ioread32(GPIO_ODR_PA);tmp |= 0x2000;iowrite32(tmp, GPIO_ODR_PA);break;case 5: //熄灭绿色LEDtmp = ioread32(GPIO_ODR_PB);tmp |= 0x20;iowrite32(tmp, GPIO_ODR_PB);break;case 6: //熄灭蓝色LEDtmp = ioread32(GPIO_ODR_PG);tmp |= 0x04;iowrite32(tmp, GPIO_ODR_PG);break;case 7: //全部熄灭三个LEDtmp = ioread32(GPIO_ODR_PA);tmp |= 0x2000;iowrite32(tmp, GPIO_ODR_PA);tmp = ioread32(GPIO_ODR_PB);tmp |= 0x20;iowrite32(tmp, GPIO_ODR_PB);tmp = ioread32(GPIO_ODR_PG);tmp |= 0x04;iowrite32(tmp, GPIO_ODR_PG);break;default: //全部熄灭tmp = ioread32(GPIO_ODR_PA);tmp |= 0x2000;iowrite32(tmp, GPIO_ODR_PA);tmp = ioread32(GPIO_ODR_PB);tmp |= 0x20;iowrite32(tmp, GPIO_ODR_PB);tmp = ioread32(GPIO_ODR_PG);tmp |= 0x04;iowrite32(tmp, GPIO_ODR_PG);break;}return 0; }
驱动程序属于内核态程序,所以还需要配套一个Makefile文件,其内容如下。
KERNEL_DIR=/opt/ebf_linux_kernel_mp157_depth1/build_image/build ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- export ARCH CROSS_COMPILE obj-m := led.o all:$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules clean:$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
其中的第一行指定了开发板的内核源代码位置,若放到了其他路径可自行修改。
以下是测试用的应用程序代码,文件名为app.c。
#include <stdio.h> #include <fcntl.h> #include <string.h> #include <unistd.h> int main(int argc, char *argv[]) {int fd;unsigned char val = 0;fd = open("/dev/led", O_RDWR);if( fd < 0 )printf("can`t open\n");if( argc != 3 ){printf("Usage :\n");printf("%s <all|red|green|blue> <on|off>\n", argv[0]);return 0;}if(strcmp(argv[1], "all") == 0){if(strcmp(argv[2], "on") == 0)val = 0;elseval = 7;}else if(strcmp(argv[1], "red") == 0){if(strcmp(argv[2], "on") == 0)val = 1;elseval = 4;}else if(strcmp(argv[1], "green") == 0){if(strcmp(argv[2], "on") == 0)val = 2;elseval = 6;}else if(strcmp(argv[1], "blue") == 0){if(strcmp(argv[2], "on") == 0)val = 3;elseval = 5;}write(fd, &val, 1);close(fd);return 0; }
完成后,先执行make命令编译驱动程序,若成功会生成名为led.ko的驱动模块文件。然后对应用程序进行交叉编译,执行“arm-linux-gnueabihf-gcc app.c -o app”即可。
完成后,把编译生成的驱动模块文件led.ko和应用程序文件app一起拷贝到NFS共享目录下 。然后在开发板上执行“insmod led.ko”,把模块插入到内核中,可执行lsmod命令查看一下是否加载成功,并查看一下/dev目录是否已经生成了设备节点文件led。此外,还可以执行“cat /proc/devices”查看一下led设备的主设备号。
以上都正常后,就可以执行应用程序来进行测试了。输入“./app all on”并回车,可看到开发板上三个LED全部点亮,如下图所示。
再输入“./app all off”并回车,可看到它们又全部熄灭,如下图所示。
还可以执行其他命令,来点亮或熄灭单独的LED,比如,单独点亮红色的LED,可执行命“./app red on”,熄灭执行命令“./app red off”等等,以此类推进行检查验证,此处就不再赘述了。
关于驱动程序的具体原理,会在后面再单独作讨论。