使用开发板为正点原子STM32mp157。
参考资料则是正点原子驱动开发教程,以及STM32mp157芯片手册。
跟STM32裸机一样,需要配置相应的硬件寄存器,这里涉及到物理地址映射到虚拟地址。
主要是通过ioremap和iounmap这两个函数进行。
先在内核源码中搜索这两个函数,并且跳转到它们的定义。
可以发现,映射函数返回值是一个指针,也就是我们映射完的虚拟地址。
取消映射函数没有返回值。
了解到这些,我们就可以使用这两个函数了,当然也可以通过其他途径了解这两个函数的用法,这里只是举个例子。
物理地址
学过stm32裸机开发的人都知道,我们进行寄存器直接操作的时候,是需要知道各个寄存器的物理地址的,当然在固件库编程里面已经都给我们定义好了,只要去结构体里面去找就可以了。
那么在linux驱动开发里面,我们也需要去找到物理地址。
那么怎么找呢?
首先打开我们的STM32mp157参考手册。
我们可以看到GPIOs和RCC都是在AHB4总线上的,那么就要找到AHB4的地址。
从这张图,我们可以看到AHB4的地址,不过为了以后移植方便,我决定从APB1开始写,然后AHB4用偏移量表示。
通过查看开发板原理图我可以找到LED0所接的GPIO口为GPIOI0。
知道是哪个口之后,在芯片手册中再继续往下找,找到AHB4外设映射情况。
分别找到相应的物理地址,为了方便,我依然采用了加偏移量的方式定义物理地址。
找到这两个之后,点进RCC的寄存器设置,因为GPIO是在AHB4总线下面的,所以需要使能AHB4下的时钟。
可以看到偏移量是0XA28,同时在bit8上可以使能GPIOI的时钟,记下来!
RCC设置完了之后,就是对我们GPIO的设置了。
回顾一下在STM32中进行裸机开发的时候,是一个写好的结构体,让我们去定义模式、输入/输出方式、速度、上拉/下拉等等。
不记得也没有关系,我们点进GPIO的寄存器设置,里面有各种寄存器。
一个个往下去找,就能找到我们需要的寄存器了,每个下面都有相应的设置,按照这个一个个去设置。
同时这些也都有偏移量,设置好我们的基地址,一个个加起来就行。
设置完就如下:
//寄存器物理地址,通过查芯片参考手册得到
#define PERIPH_BASE (0x40000000)
#define MPU_AHB4_PERIPH_BASE (PERIPH_BASE + 0x10000000)
#define RCC_BASE (MPU_AHB4_PERIPH_BASE + 0x0000)
#define RCC_MP_AHB4ENSETR (RCC_BASE + 0XA28)
#define GPIOI_BASE (MPU_AHB4_PERIPH_BASE + 0xA000)
#define GPIOI_MODER (GPIOI_BASE + 0x0000)
#define GPIOI_OTYPER (GPIOI_BASE + 0x0004)
#define GPIOI_OSPEEDR (GPIOI_BASE + 0x0008)
#define GPIOI_PUPDR (GPIOI_BASE + 0x000C)
#define GPIOI_BSRR (GPIOI_BASE + 0x0018)
尽量按照这种方式,不容易出错,也容易移植。
然后进行地址映射。
static void __iomem *RCC_MP_AHB4ENSETR_PI;
static void __iomem *GPIOI_MODER_PI;
static void __iomem *GPIOI_OTYPER_PI;
static void __iomem *GPIOI_OSPEEDR_PI;
static void __iomem *GPIOI_PUPDR_PI;
static void __iomem *GPIOI_BSRR_PI;
//地址映射
static void led_ioremap(void)
{
RCC_MP_AHB4ENSETR_PI = ioremap(RCC_MP_AHB4ENSETR,4);
GPIOI_MODER_PI = ioremap(GPIOI_MODER,4);
GPIOI_OTYPER_PI = ioremap(GPIOI_OTYPER,4);
GPIOI_OSPEEDR_PI = ioremap(GPIOI_OSPEEDR,4);
GPIOI_PUPDR_PI = ioremap(GPIOI_PUPDR,4);
GPIOI_BSRR_PI = ioremap(GPIOI_BSRR,4);
}
//取消映射
static void led_iounmap(void)
{
iounmap(RCC_MP_AHB4ENSETR_PI);
iounmap(GPIOI_MODER_PI);
iounmap(GPIOI_OTYPER_PI);
iounmap(GPIOI_OSPEEDR_PI);
iounmap(GPIOI_PUPDR_PI);
iounmap(GPIOI_BSRR_PI);
}
这样我们的地址就映射好了,接下来用之前编写过的框架。
入口函数,出口函数,以及注册和注销函数都全部复制过来。
复制过来之后,编译一下,看看有什么地方不对,头文件缺失等,修改一下。
初始化led
按照上面查的,对led进行初始化。
// 入口函数
static int __init led_init(void)
{
int ret = 0;
int val = 0;
//寄存器地址映射
led_ioremap();
//使能GPIO时钟
val = readl(RCC_MP_AHB4ENSETR_PI);
val &= ~(0x1 << 8);//将bit8位清0
val |=(0x1 << 8);//将bit8置1
writel(val,RCC_MP_AHB4ENSETR_PI);
//将GPIOI设置为输出
val = readl(GPIOI_MODER_PI);
val &= ~(0x3 << 0);//将bit0和1位清0s
val |=(0x1 << 0);//将bit0和1置为01
writel(val,GPIOI_MODER_PI);
//将输出设置为推挽输出
val = readl(GPIOI_OTYPER_PI);
val &= ~(0x1 << 0);//将bit0清0,设置为推挽输出
writel(val,GPIOI_OTYPER_PI);
//设置速度
val = readl(GPIOI_OSPEEDR_PI);
val &= ~(0x3 << 0);
val |= (0x3<<0);//设置为超高速
writel(val,GPIOI_OSPEEDR_PI);
//设置上拉下拉
val = readl(GPIOI_PUPDR_PI);
val &= ~(0x3 << 0);
val |= (0x1<<0);//设置为上拉
writel(val,GPIOI_PUPDR_PI);
//将GPIOI默认设置为1,关闭状态
val = readl(GPIOI_BSRR_PI);
val &= ~(0x1 << 0);
val |= (0x1<<0);//设置为1
writel(val,GPIOI_BSRR_PI);
printk("led_init\r\n");
// 注册字符设备
ret = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
if (ret < 0)
{
printk("Led driver register failed!\r\n");
goto fail_register;//要理解查阅的地方
}
return 0;
fail_register:
return -EIO;
}
// 出口函数
static void __exit led_fini(void)
{
led_iounmap();
printk("led_exit\r\n");
// 注销字符设备
unregister_chrdev(LED_MAJOR, LED_NAME);
}
这里有个goto的用法,有个文章说的还行,这个我还在自我理解中...
大致流程就是按照查阅的寄存器一个个设置,这里的与和或相信学过裸机开发的都能够明白,就不介绍了。
这里我们将GPIO0默认设置为高电平,设置为上拉并且初始化置1。
至此,初始化就结束了。
实现灯的亮灭
首先定义两个宏。
因为是从应用层写进来数据,所以我们对write函数进行修改,首先还是定义一个缓冲区,只要一个字节就可以了。
unsigned char databuf[1];//数据缓冲区
同时定义一个灯的状态,也可以不定义,看自己的习惯。
unsigned char ledstat;//灯的状态
将用户传进来的数据保存到我们定义的缓冲区中,然后再传给灯的状态,从而去判断灯的亮灭。
好了,最后我们再来写一个led_switch这个函数,其实没啥,就是一个位的置0和置1。
void led_switch(u8 sta)
{
u32 val = 0;
if (sta == LED_ON)
{
val = readl(GPIOI_BSRR_PI);
val &= ~(0x1 << 16);
val |= (0x1 << 16); // 设置为1
writel(val, GPIOI_BSRR_PI);
}
else if (sta == LED_OFF)
{
val = readl(GPIOI_BSRR_PI);
val &= ~(0x1 << 0);
val |= (0x1 << 0); // 设置为1
writel(val, GPIOI_BSRR_PI);
}
}
至此,驱动文件就写完了,主要就是对芯片手册的查询。
贴一个完整的程序。
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
//寄存器物理地址,通过查芯片参考手册得到
#define PERIPH_BASE (0x40000000)
#define MPU_AHB4_PERIPH_BASE (PERIPH_BASE + 0x10000000)
#define RCC_BASE (MPU_AHB4_PERIPH_BASE + 0x0000)
#define RCC_MP_AHB4ENSETR (RCC_BASE + 0XA28)
#define GPIOI_BASE (MPU_AHB4_PERIPH_BASE + 0xA000)
#define GPIOI_MODER (GPIOI_BASE + 0x0000)
#define GPIOI_OTYPER (GPIOI_BASE + 0x0004)
#define GPIOI_OSPEEDR (GPIOI_BASE + 0x0008)
#define GPIOI_PUPDR (GPIOI_BASE + 0x000C)
#define GPIOI_BSRR (GPIOI_BASE + 0x0018)
#define LED_MAJOR 200
#define LED_NAME "LED"
//定义灯的开关状态
#define LED_ON 1
#define LED_OFF 0
//映射后的寄存器虚拟地址指针
static void __iomem *RCC_MP_AHB4ENSETR_PI;
static void __iomem *GPIOI_MODER_PI;
static void __iomem *GPIOI_OTYPER_PI;
static void __iomem *GPIOI_OSPEEDR_PI;
static void __iomem *GPIOI_PUPDR_PI;
static void __iomem *GPIOI_BSRR_PI;
//地址映射
static void led_ioremap(void)
{
RCC_MP_AHB4ENSETR_PI = ioremap(RCC_MP_AHB4ENSETR,4);
GPIOI_MODER_PI = ioremap(GPIOI_MODER,4);
GPIOI_OTYPER_PI = ioremap(GPIOI_OTYPER,4);
GPIOI_OSPEEDR_PI = ioremap(GPIOI_OSPEEDR,4);
GPIOI_PUPDR_PI = ioremap(GPIOI_PUPDR,4);
GPIOI_BSRR_PI = ioremap(GPIOI_BSRR,4);
}
//取消映射
static void led_iounmap(void)
{
iounmap(RCC_MP_AHB4ENSETR_PI);
iounmap(GPIOI_MODER_PI);
iounmap(GPIOI_OTYPER_PI);
iounmap(GPIOI_OSPEEDR_PI);
iounmap(GPIOI_PUPDR_PI);
iounmap(GPIOI_BSRR_PI);
}
void led_switch(u8 sta)
{
u32 val = 0;
if (sta == LED_ON)
{
val = readl(GPIOI_BSRR_PI);
val &= ~(0x1 << 16);
val |= (0x1 << 16); // 设置为1
writel(val, GPIOI_BSRR_PI);
}
else if (sta == LED_OFF)
{
val = readl(GPIOI_BSRR_PI);
val &= ~(0x1 << 0);
val |= (0x1 << 0); // 设置为1
writel(val, GPIOI_BSRR_PI);
}
}
static int led_open(struct inode *inode, struct file *filp)
{
int ret = 0;
//printk("chrdevbase_open\r\n");
return ret;
}
static int led_release(struct inode *inode, struct file *filp)
{
int ret = 0;
//printk("chrdevbase_release\r\n");
return ret;
}
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt,
loff_t *offt)
{
int ret = 0;
return ret;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t led_write(struct file *filp, const char __user *buf,
size_t cnt, loff_t *offt)
{
int ret = 0;
unsigned char databuf[1];//数据缓冲区
unsigned char ledstat;//灯的状态
ret = copy_from_user(databuf,buf,cnt);
if (ret < 0)
{
printk("kernel receviedata failed!\r\n");
ret = -EFAULT;
}
ledstat = databuf[0];//传进来灯的开关状态
if(ledstat == LED_ON) //开灯
{
led_switch(LED_ON);
}else if(ledstat == LED_OFF)//关灯
{
led_switch(LED_OFF);
}
return ret;
}
const struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.release = led_release,
.write = led_write,
.read = led_read,
};
// 入口函数
static int __init led_init(void)
{
int ret = 0;
int val = 0;
//寄存器地址映射
led_ioremap();
//使能GPIO时钟
val = readl(RCC_MP_AHB4ENSETR_PI);
val &= ~(0x1 << 8);//将bit8位清0
val |=(0x1 << 8);//将bit8置1
writel(val,RCC_MP_AHB4ENSETR_PI);
//将GPIOI设置为输出
val = readl(GPIOI_MODER_PI);
val &= ~(0x3 << 0);//将bit0和1位清0s
val |=(0x1 << 0);//将bit0和1置为01
writel(val,GPIOI_MODER_PI);
//将输出设置为推挽输出
val = readl(GPIOI_OTYPER_PI);
val &= ~(0x1 << 0);//将bit0清0,设置为推挽输出
writel(val,GPIOI_OTYPER_PI);
//设置速度
val = readl(GPIOI_OSPEEDR_PI);
val &= ~(0x3 << 0);
val |= (0x3<<0);//设置为超高速
writel(val,GPIOI_OSPEEDR_PI);
//设置上拉下拉
val = readl(GPIOI_PUPDR_PI);
val &= ~(0x3 << 0);
val |= (0x1<<0);//设置为上拉
writel(val,GPIOI_PUPDR_PI);
//将GPIOI默认设置为1,关闭状态
val = readl(GPIOI_BSRR_PI);
val &= ~(0x1 << 0);
val |= (0x1<<0);//设置为1
writel(val,GPIOI_BSRR_PI);
printk("led_init\r\n");
// 注册字符设备
ret = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
if (ret < 0)
{
printk("Led driver register failed!\r\n");
goto fail_register;//要理解查阅的地方
}
return 0;
fail_register:
return -EIO;
}
// 出口函数
static void __exit led_fini(void)
{
led_iounmap();
printk("led_exit\r\n");
// 注销字符设备
unregister_chrdev(LED_MAJOR, LED_NAME);
}
/*驱动的注册与卸载*/
module_init(led_init); // 入口函数
module_exit(led_fini); // 出口函数
应用测试APP
跟之前的程序差不多,直接复制过来,改一下,这个没啥好说的。
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#define LED_ON 1
#define LED_OFF 0
/*
APP运行命令:./ledAPP filename <0>/<1> 如果是1表示打开,如果是0表示关闭
*/
int main(int argc, char *argv[])
{
int fd, retvalue; // 定义返回值
char *filename; // 要打开的文件
unsigned char databuf[1];
if (argc != 3) // 判断输入的参数是否是三个
{
printf("error usage!\r\n");
return -1;
}
filename = argv[1]; // 第二个参数,既要打开的文件
/*
打开文件
*/
fd = open(filename, O_RDWR); // 读写的方式打开文件
if (fd < 0)
{
printf("Can't open file %s\r\n", filename);
return -1;
}
databuf[0] = atoi(argv[2]); //转为数字
retvalue = write(fd,databuf,1);
if (retvalue < 0)
{
printf("write file failed! %s\r\n", filename);
}
/*
关闭文件
*/
retvalue = close(fd);
if (retvalue < 0)
{
printf("Can't close file%s\r\n", filename);
return -1;
}
return 0;
}
上开发板测试
可以红灯可以实现亮灭。
成功!!
点灯完成。