2.4在Linux内核中操作寄存器

前面章节写的都是不涉及具体硬件的驱动程序,本章节基于STM32MP157硬件,编写一个通过寄存器控制LED灯的驱动程序

硬件原理图

如下是LED灯的原理图,它的正极通过电阻连接到3.3v,负极通过LED0网络标号连接到核心板的A98引脚,在核心板中又将A98引脚连接到STM32MP157的GPIO I0(LED正极通过电阻接3.3v,负极连接到GPIO I0),此时可以通过控制GPIO I0的电平就可控制LED的亮灭,输出高电平(即为1)熄灭LED,输出低电平(即0)点亮LED。
2.4在Linux内核中操作寄存器_第1张图片
2.4在Linux内核中操作寄存器_第2张图片

芯片寄存器

GPIO的操作主要涉及到两种类型的寄存器,分别是时钟管理模块RCC的寄存器和GPIO外设模块的寄存器

时钟控制寄存器

这里只需要关注一个打开/关闭GPIO I外设模块时钟的寄存器
2.4在Linux内核中操作寄存器_第3张图片
寄存器bit0~bit10分别控制GPIO A到GPIO K的外设模块时钟,为1时使能相应的时钟

GOIO控制寄存器

  1. GPIOx_MODER
    端口模式寄存器,配置GPIO的工作描述,可以间GPIO配置为输入、输出、复用、模拟
    2.4在Linux内核中操作寄存器_第4张图片
  2. GPIOx_OTYPER
    GPIO输出类型配置,可以将GPIO配置为开漏输出或者推挽输出,
    2.4在Linux内核中操作寄存器_第5张图片
  3. GPIOx_OSPEEDR
    配置GPIO输出速率等级,对于点亮LED没有要求,如果将GPIO复用为高速接口时建议配置为超高速模式
    2.4在Linux内核中操作寄存器_第6张图片
  4. GPIOx_PUPDR
    上下拉配置寄存器,可以配置GPIO外设模块内部的上下拉电阻
    2.4在Linux内核中操作寄存器_第7张图片
  5. GPIOx_IDR
    输入寄存器,它可以反应GPIO当时的电平状态
    2.4在Linux内核中操作寄存器_第8张图片
  6. GPIOx_ODR
    输出寄存器,通过向指定位写0或1可以控制GPIO输出低电平或高电平
    2.4在Linux内核中操作寄存器_第9张图片
  7. GPIOx_BSRR
    输出寄存器置位/复位控制寄存器,通过向高16位的某一位写1可以复位输出寄存器中的对应位,通过向低16位中的某一位写1可以置为输出寄存器中的对应位
    2.4在Linux内核中操作寄存器_第10张图片

在Linux中访问寄存器

Linux中使用虚拟地址访问内存(包括外设寄存器),因此无法直接通过芯片手册给的物理地址分为对应的外设寄存器,必需要先用系统提供的接口对内存(外设寄存器)地址进行映射,将物理地址转换为虚拟地址,然后在通过虚拟地址去读写内存(外设寄存器),下面是一些常用的外设寄存器映射和读写的接口:

//申请内存资源,在系统中内存资源不可以被重复申请,如果此段内存在别的地方被申请过了此操作会执行失败
#define request_region(start,n,name)
//释放内存资源
#define release_mem_region(start,n)	
//将内存映射到虚拟地址
void __iomem *ioremap(unsigned long port, unsigned long size)
//取消ioremap的映射
void iounmap(volatile void __iomem *virtual)
//读取8为
u8 readb(const volatile void __iomem *addr)
int ioread8(void __iomem *addr)
//读16位
u16 readw(const volatile void __iomem *addr)
unsigned int ioread16(void __iomem *addr)
//读32位
u32 readl(const volatile void __iomem *addr)
unsigned int ioread32(void __iomem *addr)
//写8位
void writeb(u8 b, volatile void __iomem *addr)
void iowrite8(u8 b, void __iomem *addr)
//写16位
void writew(u16 b, volatile void __iomem *addr)
void iowrite16(u16 b, void __iomem *addr)
//写32位
void writel(u32 b, volatile void __iomem *addr)
void iowrite32(u32 b, void __iomem *addr)
//按8位为单位连续读
void ioread8_rep(void __iomem *addr, void *dst, unsigned long count)
//按16位为单位连续读
void ioread16_rep(void __iomem *addr, void *dst, unsigned long count)
//按32位为单位连续读
void ioread32_rep(void __iomem *addr, void *dst, unsigned long count)
//按8位为单位连续写
void iowrite8_rep(void __iomem *addr, const void *src, unsigned long count)
//按16位为单位连续写
void iowrite16_rep(void __iomem *addr, const void *src, unsigned long count)
//按32位为单位连续写
void iowrite32_rep(void __iomem *addr, const void *src, unsigned long count)

在Linux内核中一般按如下步骤操作外设寄存器

  1. 用request_mem_region申请内存资源(可选)
  2. 用ioremap映射寄存器的物理地址到虚拟地址空间
  3. 使用内核提供的读写函数读写寄存器(对于ARM平台可以直接通过取指针运算符的方式读写寄存器)
  4. 用iounmap取消内存映射
  5. 用release_mem_region释放前面申请的内存资源(可选)
    上述第1、2步一般在驱动初始化阶段执行,4、5步在驱动卸载阶段执行,寄存器被映射后可反复读写,直到被取消映射

Linux一些其他的内存操作

这里顺便提一下Linux内核中一些常用的内存操作函数

//申请指定大小的内存,内存在物理地址上连续
void *kmalloc(size_t size, gfp_t gfp_mask)
//释放内存,与kmalloc配合使用
void kfree(const void *block)
//申请指定大小的内存,内存在物理地址上不连续,但此函数相对于上面3个函数可以申请更大的内存空间
void *vmalloc(unsigned long size)
//释放内存,与vmalloc配合使用
void vfree(const void *addr)
//申请2的n次方页内存,内存在物理地址上连续
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
//释放内存,与__get_free_pages配合使用
void free_pages(unsigned long addr, unsigned int order)
//申请一页内存,内存在物理地址上连续
#define __get_free_page(gfp_mask)
//释放内存,与__get_free_page配合使用
#define free_page(addr)
/* 内存分配掩码gfp_mask
 *  GFP_ATOMIC 以原子方式分配内存,内存分配过程不会引起进程切换   
 *     GFP_KERNEL 常规方式内存
 *     GFP_USER 为用户空间分配,也需要内核空间能访问,常被映射到用户空间
 *     GFP_DMA 在ZONE_DMA区域分配内存,要求分配的内存能进行DMA映射
 */
 //根据结构体成员变量的地址计算出结构体变量的地址
 #define container_of(ptr, type, member)
 //将应用层的数据拷贝到内核层,应用层与内核层之间不能通过赋值运算符拷贝,不能通过memcpy拷贝
 unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n)
 //将内核层的数据拷贝到应用层
 unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n)

代码实现

下面是一个控制通过GPIO控制LED状态的代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

//使用自动分配设备号
#define USING_ALLOC_DEV_ID

//主设备号
#define LED_MAJOR				500
//次设备号
#define LED_MINOR				0
//设备名称
#define LED_NAME				"test_led"

//总线基地址定义
#define PERIPH_BASE				0x40000000
#define MPU_AHB4_PERIPH_BASE	(PERIPH_BASE + 0x10000000)
#define RCC_BASE				(MPU_AHB4_PERIPH_BASE + 0x0000)
#define GPIOI_BASE				(MPU_AHB4_PERIPH_BASE + 0xA000)

//RCC寄存器地址,用于使能GPIOI的时钟(RCC_BASE + 0XA28)
//RCC时钟寄存器,控制GPIOI的时钟
#define RCC_MP_AHB4ENSETR_OFFSET	0XA28

//GPIO寄存器地址,用于控制GPIO
//端口模式寄存器,配置输入、输出、复用、模拟
#define GPIOI_MODER_OFFSET			0x0000
//端口输出类型寄存器
#define GPIOI_OTYPER_OFFSET			0x0004
//端口输出速度寄存器
#define GPIOI_OSPEEDR_OFFSET		0x0008
//端口上拉/下拉寄存器
#define GPIOI_PUPDR_OFFSET			0x000C
//端口置位/复位寄存器,其中高16位用于控制置位,低16位用于控制复位
#define GPIOI_BSRR_OFFSET			0x0018

struct led_device{
	//IO内存虚拟地址
	void __iomem *RCC_ADDR;
	void __iomem *GPIOI_ADDR;

	//寄存器虚拟地址
	void __iomem *RCC_MP_AHB4ENSETR;
	void __iomem *GPIOI_MODER;
	void __iomem *GPIOI_OTYPER;
	void __iomem *GPIOI_OSPEEDR;
	void __iomem *GPIOI_PUPDR;
	void __iomem *GPIOI_BSRR;

	//I/O内存资源,同一段IO内存只能申请一次,但是可以映射多次
	struct resource *RCC_resource;
	struct resource *GPIOI_resource;

	//设备号
	dev_t dev;
	//设备对象
	struct cdev cdev;
	//class对象
	struct class *cls;
	//led状态,0灭,1亮
	int led_state;
};

//led句柄
static struct led_device led;

void led_switch(struct led_device *led, uint8_t state)
{
	uint32_t val = 0;

	if(state)
	{
		//设置GPIO0为低电平,点亮led
		val = readl(led->GPIOI_BSRR);
		val |= (1 << 16);
		writel(val, led->GPIOI_BSRR);
	}
	else
	{
		//设置GPIOI0为高电平,熄灭led
		val = readl(led->GPIOI_BSRR);
		val |= (1 << 0);
		writel(val, led->GPIOI_BSRR);
	}
}

static int led_open(struct inode *inode, struct file *file)
{
	struct led_device *led;

	//根据结构体成员变量的地址找到结构体变量的地址
	led = container_of(inode->i_cdev, struct led_device, cdev);
	file->private_data = led;

	return 0;
}

static int led_release(struct inode *inode, struct file *file)
{
	return 0;
}

static ssize_t led_read(struct file *file, char __user *buf, size_t len, loff_t *pos)
{
	char kernal_data;
	struct led_device *led = file->private_data;

	if(led->led_state == 0)
		kernal_data = '0';
	else if(led->led_state == 1)
		kernal_data = '1';
	else
		return -EINVAL;
	
	//将数据从内核空间拷贝到用户空间
	if(copy_to_user(buf, &kernal_data, 1))
		return -EFAULT;

	return 1;
}

static ssize_t led_write(struct file *file, const char __user *buf, size_t len, loff_t *pos)
{
	char user_data;
	struct led_device *led = file->private_data;
	
	//将数据从用户框架拷贝到内核框架
	if(copy_from_user(&user_data, buf, 1))
		return -EFAULT;

	if(user_data == '0')
	{
		//熄灭led
		led->led_state = 0;
		led_switch(led, 0);
	}
	else if(user_data == '1')
	{
		//点亮led
		led->led_state = 1;
		led_switch(led, 1);
	}
	else
		return -EINVAL;

	return len;
}

//取消I/O内存映射
static void io_unmap(struct led_device *led)
{
	if(led->RCC_ADDR)
	{
		/* 取消IO内存映射
		 * addr 映射后的虚拟地址
		 */
		iounmap(led->RCC_ADDR);
		led->RCC_ADDR = NULL;
		led->RCC_MP_AHB4ENSETR = NULL;
	}
	if(led->GPIOI_ADDR)
	{
		iounmap(led->GPIOI_ADDR);
		led->GPIOI_ADDR = NULL;
		led->GPIOI_MODER = NULL;
		led->GPIOI_OTYPER = NULL;
		led->GPIOI_OSPEEDR = NULL;
		led->GPIOI_PUPDR = NULL;
		led->GPIOI_BSRR = NULL;
	}

//	if(led->RCC_resource)
//	{
//		/* 释放I/O内存资源
//		 * start 物理地址起始
//		 * n 大小
//		 */
//		release_mem_region(RCC_BASE, 4096);
//		led->RCC_resource = NULL;
//	}
//	if(led->GPIOI_resource)
//	{
//		release_mem_region(GPIOI_BASE, 128);
//		led->GPIOI_resource = NULL;
//	}
}

//进行I/O内存映射
static int io_map(struct led_device *led)
{
//	//在系统中I/O内存资源不可以被重复申请,因为内核中的官方驱动模块依据申请了此段IO内存资源,所以这里无法再次申请
//	/* 申请IO内存资源
//	 * start 物理地址起始
//	 * n 大小
//	 * name 内存资源名称
//	 * 成功返回内存资源句柄
//	 */
//	led->RCC_resource = request_mem_region(RCC_BASE, 4096, "RCC_BASE");
//	if(!led->RCC_resource)
//	{
//		io_unmap(led);
//		return -EIO;
//	}
//	led->GPIOI_resource = request_mem_region(GPIOI_BASE, 128, "GPIOI_BASE");
//	if(!led->GPIOI_resource)
//	{
//		io_unmap(led);
//		return -EIO;
//	}

	//在系统中I/O内存可以被重复映射
	/* 映射IO寄存器
	 * port 寄存器物理地址
	 * size 映射大小
	 * 成功返回映射后的虚拟地址
	 **/
	led->RCC_ADDR = ioremap(RCC_BASE, 4096);
	if(!led->RCC_ADDR)
	{
		io_unmap(led);
		return -EIO;
	}
	led->RCC_MP_AHB4ENSETR = led->RCC_ADDR + RCC_MP_AHB4ENSETR_OFFSET;
	led->GPIOI_ADDR = ioremap(GPIOI_BASE, 128);
	if(!led->GPIOI_ADDR)
	{
		io_unmap(led);
		return -EIO;
	}
	led->GPIOI_MODER = led->GPIOI_ADDR + GPIOI_MODER_OFFSET;
	led->GPIOI_OTYPER = led->GPIOI_ADDR + GPIOI_OTYPER_OFFSET;
	led->GPIOI_OSPEEDR = led->GPIOI_ADDR + GPIOI_OSPEEDR_OFFSET;
	led->GPIOI_PUPDR = led->GPIOI_ADDR + GPIOI_PUPDR_OFFSET;
	led->GPIOI_BSRR = led->GPIOI_ADDR + GPIOI_BSRR_OFFSET;

	return 0;
}

static void gpio_init(struct led_device *led)
{
	uint32_t val;

	/* 使能PI时钟 */
	val = readl(led->RCC_MP_AHB4ENSETR);
	val |= (0X1 << 8);
	writel(val, led->RCC_MP_AHB4ENSETR);
	/* 设置PI0通用的输出模式。*/
	val = readl(led->GPIOI_MODER);
	val &= ~(0X3 << 0);
	val |= (0X1 << 0);
	writel(val, led->GPIOI_MODER);
	/* 设置PI0为推挽模式。*/
	val = readl(led->GPIOI_OTYPER);
	val &= ~(0X1 << 0);
	writel(val, led->GPIOI_OTYPER);
	/* 设置PI0为高速。*/
	val = readl(led->GPIOI_OSPEEDR);
	val &= ~(0X3 << 0);
	val |= (0x2 << 0);
	writel(val, led->GPIOI_OSPEEDR);
	/* 设置PI0为上拉。*/
	val = readl(led->GPIOI_PUPDR);
	val &= ~(0X3 << 0);
	val |= (0x1 << 0);
	writel(val, led->GPIOI_PUPDR);
	/* 默认开启LED */
	led_switch(led, 1);
	led->led_state = 1;
}

static void gpio_deinit(struct led_device *led)
{
	uint32_t val;

	/* 关闭LED */
	led_switch(led, 0);
	led->led_state = 0;
	/* 设置PI0为悬空。*/
	val = readl(led->GPIOI_PUPDR);
	val &= ~(0X3 << 0);
	writel(val, led->GPIOI_PUPDR);
	/* 设置PI0通用的输入模式。*/
	val = readl(led->GPIOI_MODER);
	val &= ~(0X3 << 0);
	writel(val, led->GPIOI_MODER);
}

static struct file_operations led_ops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.release = led_release,
	.read = led_read,
	.write = led_write,
};
static int register_led(struct led_device *led)
{
	int result;
	struct device *device;

#ifndef USING_ALLOC_DEV_ID
	//注册设备ID
	led->dev = MKDEV(LED_MAJOR, LED_MINOR);
	result = register_chrdev_region(led->dev, 1, LED_NAME);
	if(result != 0)
		return result;
#else
	//动态分配设备ID
	result = alloc_chrdev_region(&led->dev, LED_MINOR, 1, LED_NAME);
	if(result != 0)
		return result;
#endif

	//初始化CDEV对象
	cdev_init(&led->cdev, &led_ops);
	led->cdev.owner = THIS_MODULE;

	//添加cdev对象到系统
	result = cdev_add(&led->cdev, led->dev, 1);
	if(result != 0)
	{
		unregister_chrdev_region(led->dev, 1);
		return result;
	}

	//自动创建设备文件
	led->cls = class_create(THIS_MODULE, LED_NAME);
	if(IS_ERR(led->cls))
	{
		cdev_del(&led->cdev);
		unregister_chrdev_region(led->dev, 1);
		return PTR_ERR(led->cls);
	}
	device = device_create(led->cls, NULL, led->dev, NULL, "led_test");
	if(IS_ERR(device))
	{
		class_destroy(led->cls);
		cdev_del(&led->cdev);
		unregister_chrdev_region(led->dev, 1);
		return PTR_ERR(device);
	}

	printk("MAJOR %d,MINOR %d\r\n", MAJOR(led->dev), MINOR(led->dev));

	return 0;
}

static void unregister_led(struct led_device *led)
{
	//删除设备文件
	device_destroy(led->cls, led->dev);
	class_destroy(led->cls);
	//从内核删除cdev对象
	cdev_del(&led->cdev);
	//注销设备ID
	unregister_chrdev_region(led->dev, 1);
}

static int __init led_init(void)
{
	int result;

	//IO内存映射
	result = io_map(&led);
	if(result != 0)
	{
		printk("map io mem failed\r\n");
		return result;
	}
	//初始化LED设备
	gpio_init(&led);

	//注册led字符设备
	result = register_led(&led);
	if(result != 0)
	{
		gpio_deinit(&led);
		io_unmap(&led);
		printk("register led failed\r\n");
		return result;
	}

	return 0;
}

static void __exit led_exit(void)
{
	//注销led设备
	unregister_led(&led);
	//反初始化GPIO
	gpio_deinit(&led);
	//取消IO内存映射
	io_unmap(&led);
}

module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("csdn");
MODULE_DESCRIPTION("led test");

实验

  1. 从这里下载代码,并进行编译,然后拷贝到目标板跟目录
  2. 通过命令insmod led.ko加载内核模块,此时可见LED被点亮,/dev/目录下多了一个名叫led_test的设备文件
    在这里插入图片描述
  3. 通过命令echo 0 >> /dev/led_test向设备文件写字符串”0“可以关闭LED,通过echo 1 >> /dev/led_test向设备文件写字符串“1”可以打开LED
    在这里插入图片描述
  4. 通过cat /dev/led_test可以读取LED状态,因为read函数每次都返回1(表示读取到一个字符),所以cat命令无法自动结束
    在这里插入图片描述

你可能感兴趣的:(linux,stm32,arm开发,驱动开发)