5-Linux驱动开发-关于LED的字符设备

基础知识:

有无操作系统区别

对于led操纵引出有无操作系统:在无操作系统(裸机)时,驱动程序是直接操作寄存器,由主程序直接调用其函数;在有操作系统时,驱动程序是连接硬件和内核的"桥梁",不仅要包含操作寄存器的代码,还必须实现一套由操作系统规定的标准接口(如read/write),嵌入内核后让上层应用能通过统一的系统调用来访问硬件。

虚拟内存地址映射

内存管理单元 MMU:防止了直接访问物理内存,通过抽象内存空间。

地址转换函数: ioremap() 地址映射和取消地址映射 iounmap()

c 复制代码
void __iomem *ioremap(phys_addr_t paddr, unsigned long size);
/*
paddr:被映射的 IO 起始地址(物理地址);
size:需要映射的空间大小,以字节为单位;
返回值:返回一段虚拟地址空间的起始地址,
*/

void iounmap(void *addr)
//addr:需要取消 ioremap 映射之后的起始地址(虚拟地址)。
硬件基础
异构双核核心

大核 (MPU): 双核 Cortex-A7,用于运行 Linux 操作系统,可以处理复杂的应用、网络、界面、文件系统。(运行Linux驱动的核心)

小核 (MCU): 单核 Cortex-M4,用于运行裸机代码或 FreeRTOS,可以处理实时控制、低功耗任务。

**说明:**它们是两个独立的大脑,但它们在一个"房子"里,共用同一套"家具"(外设)和"地址系统"(内存映射)。

重要概念

Bus Matrix (总线矩阵)--连接 AXI, AHB, APB,让数据可以在不同总线之间"换乘"。

Bridge (桥)-连接不同速度总线--AHB to APB bridge (从 AHB 主干道到 APB 小路)。

Master (主设备)发出者--CPU (Cortex-A7) 是 Master,它主动发出读写指令。

Slave (从设备)响应者--GPIO 控制器是 Slave,它被动等待 CPU 来读写它的寄存器。

Peripheral (外设)--芯片内部的功能模块(如 GPIO, UART, I2C 等的统称)。

总线架构

AXIA dvanced eX tensible I nterface) 先进可扩展接口

带宽最大,速度最快,连接核心(A7, M4)、内存(DDR)、GPU(显卡)。

AHBA dvanced H igh-performance B us) 先进高性能总线

高速,高吞吐量,在 STM32MP157 中,分为 AHB1, AHB2, AHB3, AHB4, AHB5, AHB6,连接高速外设(USB, DMA, GPIO)。

其中,AHB4是最特殊的一条总线。GPIO、RCC (时钟) 都在这里。而且 AHB4 位于"独立电源域",即使 A7 核心休眠了,M4 核心依然可以通过 AHB4 控制 GPIO。

APBA dvanced P eripheral B us)先进外设总线

低速,低功耗,针对简单的外设优化,有APB1, APB2, APB3, APB4, APB5,连接通信接口(UART串口, I2C, SPI)

html 复制代码
参考手册
2.5.2 Memory map and register boundary addresses查看对应的基准地址
13.4 GPIO registers查看对应寄存器的偏移
10.7.155 RCC AHB4 Periph. Enable For MPU Set Register(RCC_MP_AHB4ENSETR)EN = Enable (使能/开启)GPIO的控制开关

下图来源于 2.5.2 Memory map and register boundary addresses

上图:左边是地址 (Address)信息,中间是相应的存储(Memory/Storage)信息,右边的是外设部门 (Peripherals/Buses)信息

gpio详情如下:

偏移量 (Offset) 寄存器名称 (Acronym) 详细名称
0x00 GPIOx_MODER 模式寄存器 (Mode register)
0x04 GPIOx_OTYPER 输出类型寄存器 (Output type register)
0x08 GPIOx_OSPEEDR 速度寄存器 (Output speed register)
0x0C GPIOx_PUPDR 上下拉配置寄存器 (Pull-up/pull-down register)
0x10 GPIOx_IDR 输入寄存器 (Input data register)
0x14 GPIOx_ODR 输出寄存器 (Output data register)
0x18 GPIOx_BSRR 置位/复位寄存器 (Bit set/reset register)
0x1C GPIOx_LCKR 锁定配置寄存器 (Lock configuration register)
0x20 GPIOx_AFRL 复用功能低位寄存器 (Alternate function low register)
0x24 GPIOx_AFRH 复用功能高位寄存器 (Alternate function high register)
0x28 GPIOx_BRR 位复位寄存器 (Bit reset register)

部分进行详细说明: gpio类似为机械手

  • MODER模式寄存器--回答的是要这个gpio口干嘛?--分配具体任务

    • 输入模式 (Input): 让手不要动,只用来摸(感知)外面的信号(是高电平还是低电平?)。
    • 通用输出模式 (Output): 让手主动动起来,去按外面的开关(输出高电平或低电平)。
    • 复用模式 (Alternate): 把这只手"借给"芯片里的其他部门(如串口部门、I2C部门)去用。
    • 模拟模式 (Analog): 这只手不再做开关动作,而是去感知电压的微弱变化(像 ADC 测量电压)。
  • OTYPER输出类型--回答的是gpio口的活动力度多大?--供电大小

    • 推挽输出 (Push-Pull):这只手有劲,用力推(输出高电平/1),用力拉(输出低电平/0),需要可以供电。
    • 开漏输出 (Open-Drain):这只手只会用力拉(输出 0),但没力气推(输出 1 时就是"松手")。
  • OSPEEDR速度寄存器--回答的是速度多快?--低速 / 中速 / 高速,动作越快,信号反应越快,但越费电,也越容易产生噪音(干扰)。

  • PUPDR上下拉--回答的是只手处于"闲置"或者"输入"状态,外面没东西连着它,它该怎么办?--默认状态

    • 上拉 (Pull-up)默认高电平
    • 下拉 (Pull-down)默认低电平
    • 无 (No pull)不确定的浮空状态
  • ODR vs BSRR--回答的是怎么指挥手去按开关?--都是用来控制输出,方法不同

    • ODR (输出寄存器)记录所有引脚状态的"备忘录",(笨办法)
    • BSRR (置位/复位寄存器)是有许多独立按钮的遥控器,(此办法推荐)
      • 原子操作,按"3号键"的时候,绝对不会碰到"4号键"或"2号键"。原子操作(不可打断,互不干扰)。

需要说明的代码详解

通过局部,找到整体部分说明

说明:我手里有一个指针 inode->i_cdev (参数1),我知道它是 struct led_chrdev (参数2) 类型结构体cdev里面的 dev (参数3) 的成员。帮我算出这个 struct led_chrdev 的起始地址。

c 复制代码
/*
container_of(ptr, type, member)
第一个参数 (ptr):你手里有什么,内核已知线索。
第二个参数 (type):你想找什么,告诉宏,想恢复出来的"大结构体"到底是什么类型。
第三个参数 (member):它叫什么名字,告诉宏,在这个大结构体里,成员被命名为什么。
*/
//inode->i_cdev指向字符设备结构体 ,
//给局部变量赋值
struct led_chrdev *led_cdev =(struct led_chrdev *)container_of(inode->i_cdev, struct led_chrdev, dev);
//存入 file 结构体,这里是第二次计算
filp->private_data = container_of(inode->i_cdev, struct led_chrdev, dev);

//上述多余
//计算一次,并赋值给局部变量
led_cdev = container_of(inode->i_cdev, struct led_chrdev, dev);
//直接把局部变量的值,赋给 private_data
filp->private_data = led_cdev;

上述部分参照以下具体结构体:

c 复制代码
struct inode {
    umode_t             i_mode;     // 文件的权限和类型 (是文件还是设备?)
    unsigned long       i_ino;      // inode 编号
    dev_t               i_rdev;     // 【关键】设备号 (主设备号 + 次设备号)
    loff_t              i_size;     // 文件大小
    
    union {
        struct block_device *i_bdev;
        struct cdev         *i_cdev; // 指向字符设备结构体 (如果是字符设备的话)
    };
    // ... 
};
c 复制代码
struct cdev {
	struct kobject kobj; //Kernel Object
	struct module *owner; //Module Owner
	const struct file_operations *ops;//File Operations
	struct list_head list;//List Head
	dev_t dev;//Device Number
	unsigned int count;
};
led灯配置方法说明

只说明两个代表性的,模式寄存器和输出类型寄存器。
模式寄存器

c 复制代码
//要先读取再写入
//设置模式寄存器:输出模式
val = ioread32(led_cdev->va_moder);
//<<左移运算符,将对应功能移动到相应pin引脚上面
//将要写入的部分置为零
val &= ~((unsigned int)0X3 << (2 * led_cdev->led_pin));
//将对应功能写入
val |= ((unsigned int)0X1 << (2 * led_cdev->led_pin));
iowrite32(val,led_cdev->va_moder);

像模式寄存器会发现每个功能占两位(范围0-3)所以(2 * led_cdev->led_pin),需要的功能为01因此写入(unsigned int)0X1。

设置输出类型寄存器

c 复制代码
// 设置输出类型寄存器:推挽模式
val = ioread32(led_cdev->va_otyper);
val &= ~((unsigned int)0X1 << led_cdev->led_pin);
iowrite32(val, led_cdev->va_otyper);

发现这个功能的值只有两个数,因此只占一位,所以移动到对应pin只需要左移这些<< led_cdev->led_pin。由于只有一位就没有先制空,直接通过写入一取反的方法使其他位置为1,把对应要写入的0写入。

led_cdev.c

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 DEV_NAME            "led_chrdev"
//定义了允许分配设备数量
#define DEV_CNT                 (3)

//查参考手册可知以下信息
#define AHB4_PERIPH_BASE (0x50000000)

#define RCC_BASE (AHB4_PERIPH_BASE + 0x0000)
#define RCC_MP_GPIOENA (RCC_BASE + 0XA28)
//GPIO(A_G,Z)偏移地址从2开始 A-2 B-3 ... G-8
#define GPIOA_BASE (AHB4_PERIPH_BASE + 0x2000)
//模式寄存器 置 GPIO 引脚的模式。输入模式、通用输出模式、复用模式、模拟模式。
#define GPIOA_MODER (GPIOA_BASE + 0x0000)
//输出类型寄存器 设置 GPIO 引脚的输出类型
#define GPIOA_OTYPER (GPIOA_BASE + 0x0004)
//速度寄存器 设置 GPIO 引脚的输出速度等级
#define GPIOA_OSPEEDR (GPIOA_BASE + 0x0008)
//上下拉配置寄存器 设置 GPIO 引脚的上下拉状态
#define GPIOA_PUPDR (GPIOA_BASE + 0x000C)
//置位/复位寄存器  设置 IO 输出的电平高低
#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_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_BSRR (GPIOB_BASE + 0x0018)

//设备号变量
static dev_t devno;
//设备类定义
struct class *led_chrdev_class;
//定义并初始化led_chrdev结构体 应用于开始定义的参数
struct led_chrdev {
	struct cdev dev;
	unsigned int __iomem *va_moder; 	// 模式寄存器虚拟地址保存变量
	unsigned int __iomem *va_otyper; 	// 输出类型寄存器虚拟地址保存变量
	unsigned int __iomem *va_ospeedr; 	// 速度配置寄存器虚拟地址保存变量
	unsigned int __iomem *va_pupdr; 	// 上下拉寄存器虚拟地址保存变量
	unsigned int __iomem *va_bsrr; 		// 置位寄存器虚拟地址保存变量
	
	unsigned int led_pin; // 引脚偏移
};

//存放映射后的虚拟地址 
//__iomem宏 告诉编译器这个指针指向的不是内存条里的普通 RAM,而是硬件寄存器(I/O Memory)。
unsigned int __iomem *va_clkaddr;

//C99 指定初始化
//基于以上结构体创建 led_cdev 结构体数组
//.成员名 = 值
static struct led_chrdev led_cdev[DEV_CNT] = {
	{.led_pin = 13}, // 对于数组第0个元素:只把 led_pin 设为 13,其他全自动清零
	{.led_pin = 2}, // 对于数组第1个元素:只把 led_pin 设为 2,其他全自动清零
	{.led_pin = 5}, // 对于数组第2个元素:只把 led_pin 设为 5,其他全自动清零
};


//打开相应配置,并进行配置
static int led_chrdev_open(struct inode *inode, struct file *filp)
{
	//一个临时存储变量的东西,只要写入了这里面数值就没用了
	unsigned int val = 0;
	struct led_chrdev *led_cdev =
	//container_of反推方法,用于计算出包含这个cdev的大结构体led_chrdev的起始地址。
	    (struct led_chrdev *)container_of(inode->i_cdev, struct led_chrdev, dev);
	filp->private_data =
	    container_of(inode->i_cdev, struct led_chrdev, dev);

	printk("open\n");

	val |= (0x43); // 开启a、b、g的时钟 
	iowrite32(val, va_clkaddr);

	// 设置模式寄存器:输出模式
	val = ioread32(led_cdev->va_moder);
	val &= ~((unsigned int)0X3 << (2 * led_cdev->led_pin));
	val |= ((unsigned int)0X1 << (2 * led_cdev->led_pin));
	iowrite32(val,led_cdev->va_moder);
	// 设置输出类型寄存器:推挽模式
	val = ioread32(led_cdev->va_otyper);
	val &= ~((unsigned int)0X1 << led_cdev->led_pin);
	iowrite32(val, led_cdev->va_otyper);
	// 设置输出速度寄存器:高速
	val = ioread32(led_cdev->va_ospeedr);
	val &= ~((unsigned int)0X3 << (2 * led_cdev->led_pin));
	val |= ((unsigned int)0x2 << (2 * led_cdev->led_pin));
	iowrite32(val, led_cdev->va_ospeedr);
	// 设置上下拉寄存器:上拉
	val = ioread32(led_cdev->va_pupdr);
	val &= ~((unsigned int)0X3 << (2*led_cdev->led_pin));
	val |= ((unsigned int)0x1 << (2*led_cdev->led_pin));
	iowrite32(val,led_cdev->va_pupdr);
	// 设置置位寄存器:默认输出低电平
	val = ioread32(led_cdev->va_bsrr);
	val |= ((unsigned int)0x1 << (led_cdev->led_pin + 16));
	iowrite32(val, led_cdev->va_bsrr);

	return 0;
}

static int led_chrdev_release(struct inode *inode, struct file *filp)
{

	return 0;
}

//写入内容
static ssize_t led_chrdev_write(struct file *filp, const char __user * buf,size_t count, loff_t * ppos)
{
	unsigned long val = 0;
	unsigned long ret = 0;

	int tmp = count;

	//找到对应基址
	struct led_chrdev *led_cdev = (struct led_chrdev *)filp->private_data;
	
	//copy_from_user的高级版本,从用户空间获取并转换数据,先把用户空间的字符串安全地拷贝进内核,再把字符串 '1' 转换成无符号长整型数字 1。
	/*
	buf: 用户发来的字符串。
	tmp: 长度。
	10: 进制。告诉内核这是十进制数。
	&ret: 结果存这里。转换后的数字(0 或 1)会被存入 ret 变量。
	*/ 
	kstrtoul_from_user(buf, tmp, 10, &ret);

	iowrite32(0x43, va_clkaddr); // 开启GPIO时钟

	//特殊:BSRR 寄存器不是用来"存东西"的,它是用来"发指令"
	//不管上次您写了什么,硬件电路在执行完操作后,寄存器内部的状态位就会复位
	//所以,ioread32(led_cdev->va_bsrr) 这行代码,从硬件上读取回来的数值永远是 0
	val = ioread32(led_cdev->va_bsrr);
	if (ret == 0){
		//STM32 的 BSRR 寄存器,高 16 位 (16-31) 用于"清除/复位" (Reset),写 1 到 (pin + 16) 的位置,引脚就会变低电平。
		val |= (0x01 << (led_cdev->led_pin+16)); // 设置GPIO引脚输出低电平
	}
	else{
		//STM32 的 BSRR 寄存器,低 16 位 (0-15) 用于"置位" (Set),写 1 到 (pin) 的位置,引脚就会变高电平。
		val |= (0x01 << led_cdev->led_pin); // 设置GPIO引脚输出高电平
	}

	iowrite32(val, led_cdev->va_bsrr);
	*ppos += tmp;// 更新文件读写位置 (对 LED 没用,但是标准动作)
	return tmp;// 告诉 echo 命令:"成功写入了这么多个字节"
}

//定义了驱动程序的功能映射表。
//连接 用户空间 (User Space) 和 内核空间 (Kernel Space)
//定义并注册了这个结构体,Linux 内核就会自动帮您处理所有复杂的系统调用路由,最终精准地执行您的代码。
static struct file_operations led_chrdev_fops = {
	.owner = THIS_MODULE,
	.open = led_chrdev_open,
	.release = led_chrdev_release,
	.write = led_chrdev_write,
};



//之前的代码定义了怎么干活。 现在的 init 函数负责搭建干活的环境。
/*
没有虚拟地址,操作硬件会崩溃。
没有设备号,内核不认识你。
没有注册,用户的 open/write 请求无法送达。
*/

static __init int led_chrdev_init(void)
{
	int i = 0;
	dev_t cur_dev;

	printk("led chrdev init \n");
	
	va_clkaddr = ioremap(RCC_MP_GPIOENA, 4);// 映射物理地址到虚拟地址:gpio时钟rcc寄存器

	led_cdev[0].va_moder   = ioremap(GPIOA_MODER, 4);	// 映射模式寄存器 物理地址 到 虚拟地址
	led_cdev[0].va_otyper  = ioremap(GPIOA_OTYPER, 4);	// 映射输出类型寄存器 物理地址 到 虚拟地址
	led_cdev[0].va_ospeedr = ioremap(GPIOA_OSPEEDR, 4);	// 映射速度配置寄存器 物理地址 到 虚拟地址
	led_cdev[0].va_pupdr   = ioremap(GPIOA_PUPDR, 4);	// 映射上下拉寄存器 物理地址 到 虚拟地址
	led_cdev[0].va_bsrr    = ioremap(GPIOA_BSRR, 4);	// 映射置位寄存器 物理地址 到 虚拟地址

	led_cdev[1].va_moder   = ioremap(GPIOG_MODER, 4);
	led_cdev[1].va_otyper  = ioremap(GPIOG_OTYPER, 4);
	led_cdev[1].va_ospeedr = ioremap(GPIOG_OSPEEDR, 4);
	led_cdev[1].va_pupdr   = ioremap(GPIOG_PUPDR, 4);
	led_cdev[1].va_bsrr    = ioremap(GPIOG_BSRR, 4);

	led_cdev[2].va_moder   = ioremap(GPIOB_MODER, 4);
	led_cdev[2].va_otyper  = ioremap(GPIOB_OTYPER, 4);
	led_cdev[2].va_ospeedr = ioremap(GPIOB_OSPEEDR, 4);
	led_cdev[2].va_pupdr   = ioremap(GPIOB_PUPDR, 4);
	led_cdev[2].va_bsrr    = ioremap(GPIOB_BSRR, 4);

    //先申请设备号
	alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME);

	led_chrdev_class = class_create(THIS_MODULE, "led_chrdev");

	//注册驱动,告诉内核用pin号来区分不同的设备
	for (; i < DEV_CNT; i++) {
		cdev_init(&led_cdev[i].dev, &led_chrdev_fops);
		led_cdev[i].dev.owner = THIS_MODULE;

		cur_dev = MKDEV(MAJOR(devno), MINOR(devno) + i);

		cdev_add(&led_cdev[i].dev, cur_dev, 1);
		// /自动创建设备文件 DEV_NAME "%d"
		device_create(led_chrdev_class, NULL, cur_dev, NULL,DEV_NAME "%d", i);
	}

	return 0;
}

module_init(led_chrdev_init);

static __exit void led_chrdev_exit(void)
{
	int i;
	dev_t cur_dev;
	printk("led chrdev exit\n");
	
	iounmap(va_clkaddr); // 释放GPIO时钟寄存器虚拟地址

	for (i = 0; i < DEV_CNT; i++) {
		iounmap(led_cdev[i].va_moder); 		// 释放模式寄存器虚拟地址
		iounmap(led_cdev[i].va_otyper); 	// 释放输出类型寄存器虚拟地址
		iounmap(led_cdev[i].va_ospeedr); 	// 释放速度配置寄存器虚拟地址
		iounmap(led_cdev[i].va_pupdr); 		// 释放上下拉寄存器虚拟地址
		iounmap(led_cdev[i].va_bsrr); 		// 释放置位寄存器虚拟地址
	}

	for (i = 0; i < DEV_CNT; i++) {
		cur_dev = MKDEV(MAJOR(devno), MINOR(devno) + i);

		device_destroy(led_chrdev_class, cur_dev);

		cdev_del(&led_cdev[i].dev);

	}
	unregister_chrdev_region(devno, DEV_CNT);
	class_destroy(led_chrdev_class);

}

module_exit(led_chrdev_exit);

MODULE_AUTHOR("embedfire");
MODULE_LICENSE("GPL");
html 复制代码
1.先对功能进行定义 
	led_chrdev_open
		负责初始化硬件
		通过 container_of 找到对应 LED
		开启时钟,配置 MODER (模式)、OTYPER (输出类型)、OSPEEDR (速度) 等寄存器
	led_chrdev_write
		负责控制硬件
		调用 kstrtoul_from_user 转换数据,操作 BSRR 寄存器控制 LED 亮灭
	led_chrdev_release
		负责关闭资源(当前为空,做占位符)。
2.然后进行关联,将定义好的功能函数挂钩到内核接口上。
	led_chrdev_fops (结构体)将内核标准的 .open 指向 led_chrdev_open,.write 指向 led_chrdev_write。
3.进行初始化
	led_chrdev_init 搭建运行环境,让驱动生效
		ioremap:将物理寄存器地址映射为虚拟地址
		alloc_chrdev_region:向内核申请主次设备号
		class_create:创建设备类 (led_chrdev_class),为自动创建节点做准备
		for 循环 (针对每个 LED 执行):
			cdev_init:将 led_cdev 结构体与 led_chrdev_fops 关联。
			cdev_add:向内核注册驱动。
			device_create:自动在 /dev 下生成设备文件 (led_chrdev0, 1, 2)。
4.退出
	led_chrdev_exit 清理资源,恢复原状
		ounmap:释放寄存器的虚拟地址映射。
		for 循环 (针对每个 LED 执行):
			device_destroy:删除 /dev 下的设备文件。
			cdev_del:注销驱动。
		unregister_chrdev_region:归还设备号。
		class_destroy:销毁设备类。
相关推荐
小白勇闯网安圈5 分钟前
Vmware的Ubuntu构建极简版Linux发行版
linux
刘某的Cloud9 分钟前
shell脚本-read-输入
linux·运维·bash·shell·read
莫问前程_满城风雨10 分钟前
verilog 可变范围的bit选择
运维·服务器·verilog
AEMC马广川14 分钟前
能源托管项目中“企业认证+人才证书”双轨评分策略分析
大数据·运维·人工智能·能源
无锡耐特森15 分钟前
CANopen转Profinet网关:小设备撬动自动化产线大效率
运维·自动化
就是蠢啊15 分钟前
51单片机——红外遥控(二)
单片机·嵌入式硬件·51单片机
broad-sky30 分钟前
Ubuntu上查看USB相机连接的是哪个口,如何查看
linux·数码相机·ubuntu
秋深枫叶红30 分钟前
嵌入式第三十七篇——linux系统编程——线程控制
linux·学习·线程·系统编程
Big_潘大师39 分钟前
STM32串口中断
stm32·单片机·嵌入式硬件
可爱又迷人的反派角色“yang”42 分钟前
ansible的概念及基本操作(一)
运维·ansible