基础知识:
有无操作系统区别
对于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 等的统称)。
总线架构
AXI (A dvanced eX tensible I nterface) 先进可扩展接口
带宽最大,速度最快,连接核心(A7, M4)、内存(DDR)、GPU(显卡)。
AHB (A dvanced H igh-performance B us) 先进高性能总线
高速,高吞吐量,在 STM32MP157 中,分为 AHB1, AHB2, AHB3, AHB4, AHB5, AHB6,连接高速外设(USB, DMA, GPIO)。
其中,AHB4是最特殊的一条总线。GPIO、RCC (时钟) 都在这里。而且 AHB4 位于"独立电源域",即使 A7 核心休眠了,M4 核心依然可以通过 AHB4 控制 GPIO。
APB (A 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:销毁设备类。