驱动:字符驱动操控硬件
驱动操控硬件和裸机操控硬件有什么区别
在嵌入式系统或操作系统开发中,通过驱动程序操控硬件与直接在裸机环境下操控硬件有以下关键区别:
内核的虚拟地址映射方法
在操作系统内核中,将物理内存映射到虚拟地址空间主要有以下几种方式:
- 静态映射(Static Mapping)
原理:在内核启动时预先建立固定的虚拟地址到物理地址的映射关系
特点:映射关系在编译时或启动时确定,效率高但缺乏灵活性
示例场景:内核代码段、全局变量、初始化数据等 - 动态映射(Dynamic Mapping)
原理:在运行时根据需要创建或销毁映射关系
特点:灵活但开销较大,需要显式管理映射生命周期
实现方式:
使用内核 API(如 Linux 的ioremap())映射硬件 I/O 空间
通过内存管理函数(如vmalloc())分配虚拟地址空间 - 永久映射(Permanent Mapping)
原理:为高端内存区域创建长期有效的映射
特点:映射长期存在,简化内存访问
适用场景:内核需要长期访问的高端内存区域 - 临时映射(Temporary Mapping)
原理:临时将物理页映射到特定虚拟地址
特点:短期使用,快速创建和释放,不占用永久映射表项
示例:Linux 内核中的kmap_atomic()函数 - 页表操作
原理:直接操作页表项来建立映射关系
适用场景:实现自定义内存管理或特殊映射需求
风险:需深入了解硬件 MMU 和操作系统页表结构,容易出错
Linux 内核示例:ioremap ()
在 Linux 内核中,常用ioremap()函数将物理 I/O 地址映射到虚拟地址空间:
#include <linux/io.h>// 映射物理地址到虚拟地址
void __iomem *base_addr = ioremap(0x40000000, 0x1000); // 映射0x40000000开始的4KB空间if (base_addr) {// 通过虚拟地址访问硬件寄存器u32 value = readl(base_addr + 0x10); // 读取偏移0x10处的寄存器值writel(0x55AA, base_addr + 0x20); // 向偏移0x20处的寄存器写入值// 使用完毕后释放映射iounmap(base_addr);
}
查看内核版本
make kernelversion
2.6.35.7 静态映射表
(1)主映射表位于:arch/arm/plat-s5p/include/plat/map-s5p.h
map-s5p.h中定义的是模块的寄存器基地址的虚拟地址。
(2)虚拟地址基地址定义在:arch/arm/plat-samsung/include/plat/map-base.h
#define S3C_ADDR_BASE (0xFD000000) // 三星移植时确定的静态映射表的基地址,表中的所有虚拟地址都是以这个地址+偏移量来指定的
(3)GPIO相关的主映射表位于:arch/arm/mach-s5pv210/include/mach/regs-gpio.h
表中是GPIO的各个端口的基地址的定义
(4)GPIO的具体寄存器定义位于:arch/arm/mach-s5pv210/include/mach/gpio-bank.h
这个地方的是虚拟地址,已经和实际的物理地址进行映射。
静态映射操作LED
module_test.c
#include <linux/module.h> // module_init module_exit
#include <linux/init.h> // __init __exit
#include <linux/fs.h>
#include <linux/uaccess.h> // copy_to_user, copy_from_user
#include <linux/kernel.h>
#include <mach/gpio-bank.h>
#include <mach/regs-gpio.h>
#define GPJ0CON S5PV210_GPJ0CON
#define GPJ0DAT S5PV210_GPJ0DAT#define rGPJ0CON *((volatile unsigned int *) GPJ0CON)
#define rGPJ0DAT *((volatile unsigned int *) GPJ0DAT)#define BUFFER_SIZE 100
#define MYMAJOR 200
#define MYNAME "mychartest"int mymajor;
static char buffer[BUFFER_SIZE];int ctrled(int sta)
{if(sta){rGPJ0CON = 0x11111111;//rGPJ0DAT =((1<<3) | (1<<4) | (1<<5)) //灭rGPJ0DAT =((0<<3) | (0<<4) | (0<<5)) ;//亮printk(KERN_INFO "rGPJ0CON = %p\n",rGPJ0CON);printk(KERN_INFO "rGPJ0DAT = %d\n",rGPJ0DAT);printk(KERN_INFO "GPJ0CON = %p\n",GPJ0CON);printk(KERN_INFO "GPJ0DAT = %p\n",GPJ0DAT);}else{rGPJ0DAT =((1<<3) | (1<<4) | (1<<5)); //灭}}
// 打开设备函数
static int my_open(struct inode *inode, struct file *filp) {printk(KERN_INFO "Device opened\n");return 0;
}// 释放设备函数
static int my_release(struct inode *inode, struct file *filp) {printk(KERN_INFO "Device released\n");return 0;
}// 读取设备函数
static ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) {printk(KERN_INFO "my_read\n");if (copy_to_user(buf, buffer, count)) {return -EFAULT;}return 0;
}// 写入设备函数
static ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {printk(KERN_INFO "my_write\n");memset(buffer,0,sizeof(buffer));if (copy_from_user(buffer, buf, count)) {return -EFAULT;}if(!strcmp(buffer,"1")){printk(KERN_INFO "1\n");ctrled(1);}else if(!strcmp(buffer,"0")){printk(KERN_INFO "0\n");ctrled(0);}return 0;
}// 定义file_operations结构体实例
static const struct file_operations my_fops = {.owner = THIS_MODULE,//全部一样,都这样写.open = my_open,.release = my_release,.write = my_write,.read = my_read,};// 模块安装函数
static int __init chrdev_init(void)
{ printk(KERN_INFO "chrdev_init helloworld init\n");//注册字符设备驱动//将第一个参数填写0 成功:返回主设备号 失败:返回负数mymajor = register_chrdev(0, MYNAME, &my_fops);if(mymajor < 0){printk(KERN_ERR "register_chrdev fail\n");return -EINVAL;}printk(KERN_INFO "chrdev_init helloworld successs... mymajor = %d\n",mymajor);return 0;
}// 模块下载函数
static void __exit chrdev_exit(void)
{printk(KERN_INFO "chrdev_exit helloworld exit\n");//注销字符设备驱动unregister_chrdev(MYMAJOR, MYNAME);}module_init(chrdev_init);
module_exit(chrdev_exit);// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证
MODULE_AUTHOR("aston"); // 描述模块的作者
MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息
MODULE_ALIAS("alias xxx"); // 描述模块的别名信息
apptest.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>#define FILE "/dev/mychartest"int main(void)
{int fd = -1;int i = 1000;char buf[100];ssize_t written;fd = open(FILE, O_RDWR);if(fd < 0){printf("open %s error.\n", FILE);return -1;}printf("open %s seccess...]\n", FILE);// 写入数据written = write(fd, "1", 1);if (written < 0) {int err = errno; // 保存errno,避免被后续函数覆盖fprintf(stderr, "write failed: %d (%s)\n", err, strerror(err));close(fd);return -1;}lseek(fd, 0, SEEK_SET);sleep(2);written = write(fd, "0", 1);if (written < 0) {int err = errno; // 保存errno,避免被后续函数覆盖fprintf(stderr, "write failed: %d (%s)\n", err, strerror(err));close(fd);return -1;}lseek(fd, 0, SEEK_SET);sleep(2);written = write(fd, "1", 1);if (written < 0) {int err = errno; // 保存errno,避免被后续函数覆盖fprintf(stderr, "write failed: %d (%s)\n", err, strerror(err));close(fd);return -1;}sleep(2);lseek(fd, 0, SEEK_SET);ssize_t read_bytes = read(fd, buf, sizeof(buf) - 1); // 预留1字节给终止符if (read_bytes < 0) { perror("read failed");close(fd);return -1;}buf[read_bytes] = '\0'; // 手动添加字符串终止符printf("buf: %s\n", buf);`在这里插入代码片`while(i--)close(fd);return 0;
}
卸载:
rmmod module_test.ko
安装:
insmod module_test.ko
创建设备文件:
mknod /dev/mychartest c 238 0
执行应用层程序:
./apptest
添加读取LED信息
// 读取设备函数
static ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) {printk(KERN_INFO "my_read\n");// 确保用户缓冲区足够大if (count < sizeof(unsigned int)) {return -EINVAL;}// 读取寄存器值unsigned int reg_value = rGPJ0DAT;printk(KERN_INFO "reg_value = %d\n",reg_value);printk(KERN_INFO "sizeof(reg_value) = %d\n",sizeof(reg_value));if (copy_to_user(buf, ®_value, sizeof(reg_value))) {return -EFAULT;}// 更新文件位置*f_pos += sizeof(reg_value);// 返回实际传输的字节数return sizeof(reg_value);
}
unsigned int led_status; // 读取LED状态lseek(fd, 0, SEEK_SET);ssize_t read_bytes = read(fd, &led_status, sizeof(led_status));if (read_bytes < 0) { perror("read failed");close(fd);return -1;}// 检查特定引脚的状态printf("led_status: %d\n", led_status);printf("LED3 status: %s\n", (led_status & (1<<3)) ? "OFF" : "ON");printf("LED4 status: %s\n", (led_status & (1<<4)) ? "OFF" : "ON");printf("LED5 status: %s\n", (led_status & (1<<5)) ? "OFF" : "ON");
动态映射操作LED
分开映射 :
#include <linux/module.h> // module_init module_exit
#include <linux/init.h> // __init __exit
#include <linux/fs.h>
#include <linux/uaccess.h> // copy_to_user, copy_from_user
#include <linux/kernel.h>
#include <mach/gpio-bank.h>
#include <mach/regs-gpio.h>
#include <linux/io.h>
#include <linux/ioport.h>//物理地址
#define GPJ0CON_PA 0xE0200240
#define GPJ0DAT_PA 0xE0200244
#define REGION_SIZE 8 // 映射区域大小(覆盖CON和DAT寄存器)#define BUFFER_SIZE 100
#define MYMAJOR 250
#define MYNAME "mychartest"int mymajor;
static char buffer[BUFFER_SIZE];// 映射后的虚拟地址指针
static volatile unsigned int *gpj0con_virt;
static volatile unsigned int *gpj0dat_virt;// 打开设备函数
static int my_open(struct inode *inode, struct file *filp) {printk(KERN_INFO "Device opened\n");*gpj0con_virt |= (0x1 << 12) | (0x1 << 16) | (0x1 << 20);//*gpj0dat_virt &= ~((1 << 3) | (1 << 4) | (1 << 5)); // 点亮 LED3-5(低电平点亮)*gpj0dat_virt = (0 << 3) | (0 << 4) | (0 << 5);return 0;
}// 释放设备函数
static int my_release(struct inode *inode, struct file *filp) {printk(KERN_INFO "Device released\n");// 熄灭LED//*gpj0dat_virt |= (1 << 3) | (1 << 4) | (1 << 5); // 熄灭 LED3-5(高电平熄灭)*gpj0dat_virt = (1 << 3) | (1 << 4) | (1 << 5);return 0;
}// 定义file_operations结构体实例
static const struct file_operations my_fops = {.owner = THIS_MODULE,//全部一样,都这样写.open = my_open,.release = my_release,
};// 模块安装函数
static int __init chrdev_init(void)
{ printk(KERN_INFO "chrdev_init helloworld init\n");//注册字符设备驱动//将第一个参数填写0 成功:返回主设备号 失败:返回负数mymajor = register_chrdev(MYMAJOR, MYNAME, &my_fops);if(mymajor < 0){printk(KERN_ERR "register_chrdev fail\n");return -EINVAL;}printk(KERN_INFO "chrdev_init helloworld successs... MYMAJOR = %d\n",MYMAJOR);// 请求连续内存区域(GPJ0CON + GPJ0DAT共8字节)if (!request_mem_region(GPJ0CON_PA, REGION_SIZE, "GPJ0CON_PA")) {printk(KERN_ERR "%s: Failed to request memory region\n", MYNAME);unregister_chrdev(mymajor, MYNAME);return -EBUSY;}// 映射物理地址gpj0con_virt = ioremap(GPJ0CON_PA, 4);if (!gpj0con_virt) {printk(KERN_ERR "%s: Failed to map GPJ0CON\n", MYNAME);release_mem_region(GPJ0CON_PA, REGION_SIZE); // 释放内存区域unregister_chrdev(MYMAJOR, MYNAME); // 注销字符设备return -ENOMEM;
}gpj0dat_virt = ioremap(GPJ0DAT_PA, 4);if (!gpj0dat_virt) {printk(KERN_ERR "%s: Failed to map GPJ0DAT\n", MYNAME);release_mem_region(GPJ0CON_PA, REGION_SIZE); // 释放内存区域unregister_chrdev(MYMAJOR, MYNAME); // 注销字符设备return -ENOMEM;}printk(KERN_INFO "%s: Memory mapped successfully\n", "GPJ0CON_PA");return 0;
}// 模块下载函数
static void __exit chrdev_exit(void)
{printk(KERN_INFO "chrdev_exit helloworld exit\n");// 解除映射iounmap(gpj0con_virt);iounmap(gpj0dat_virt);// 释放资源release_mem_region(GPJ0CON_PA, REGION_SIZE);//注销字符设备驱动unregister_chrdev(MYMAJOR, MYNAME);}module_init(chrdev_init);
module_exit(chrdev_exit);// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证
MODULE_AUTHOR("aston"); // 描述模块的作者
MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息
MODULE_ALIAS("alias xxx"); // 描述模块的别名信息
仅仅映射四字节:
#include <linux/module.h> // module_init module_exit
#include <linux/init.h> // __init __exit
#include <linux/fs.h>
#include <linux/uaccess.h> // copy_to_user, copy_from_user
#include <linux/kernel.h>
#include <mach/gpio-bank.h>
#include <mach/regs-gpio.h>
#include <linux/io.h>
#include <linux/ioport.h>//物理地址
#define GPJ0CON_PA 0xE0200240
#define GPJ0DAT_PA 0xE0200244#define BUFFER_SIZE 100
#define MYMAJOR 250
#define MYNAME "mychartest"int mymajor;
static char buffer[BUFFER_SIZE];// 映射后的虚拟地址指针
static volatile unsigned int *gpj0con_virt;
static volatile unsigned int *gpj0dat_virt;// 打开设备函数
static int my_open(struct inode *inode, struct file *filp) {printk(KERN_INFO "Device opened\n");*gpj0con_virt |= (0x1 << 12) | (0x1 << 16) | (0x1 << 20);//*gpj0dat_virt &= ~((1 << 3) | (1 << 4) | (1 << 5)); // 点亮 LED3-5(低电平点亮)*gpj0dat_virt = (0 << 3) | (0 << 4) | (0 << 5);return 0;
}// 释放设备函数
static int my_release(struct inode *inode, struct file *filp) {printk(KERN_INFO "Device released\n");// 熄灭LED//*gpj0dat_virt |= (1 << 3) | (1 << 4) | (1 << 5); // 熄灭 LED3-5(高电平熄灭)*gpj0dat_virt = (1 << 3) | (1 << 4) | (1 << 5);return 0;
}// 定义file_operations结构体实例
static const struct file_operations my_fops = {.owner = THIS_MODULE,//全部一样,都这样写.open = my_open,.release = my_release,
};// 模块安装函数
static int __init chrdev_init(void)
{ printk(KERN_INFO "chrdev_init helloworld init\n");//注册字符设备驱动//将第一个参数填写0 成功:返回主设备号 失败:返回负数mymajor = register_chrdev(MYMAJOR, MYNAME, &my_fops);if(mymajor < 0){printk(KERN_ERR "register_chrdev fail\n");return -EINVAL;}printk(KERN_INFO "chrdev_init helloworld successs... MYMAJOR = %d\n",MYMAJOR);// 请求连续内存区域(GPJ0CON + GPJ0DAT共8字节)if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON_PA")) {printk(KERN_ERR "%s: Failed to request memory region\n", MYNAME);unregister_chrdev(mymajor, MYNAME);return -EBUSY;}// 映射物理地址gpj0con_virt = ioremap(GPJ0CON_PA, 4);if (!gpj0con_virt) {printk(KERN_ERR "%s: Failed to map GPJ0CON\n", MYNAME);release_mem_region(GPJ0CON_PA, 4); // 释放内存区域unregister_chrdev(MYMAJOR, MYNAME); // 注销字符设备return -ENOMEM;}gpj0dat_virt = gpj0con_virt + 1;printk(KERN_INFO "%s: Memory mapped successfully\n", "GPJ0CON_PA");return 0;
}// 模块下载函数
static void __exit chrdev_exit(void)
{printk(KERN_INFO "chrdev_exit helloworld exit\n");// 解除映射iounmap(gpj0con_virt);// 释放资源release_mem_region(GPJ0CON_PA, 4);//注销字符设备驱动unregister_chrdev(MYMAJOR, MYNAME);}module_init(chrdev_init);
module_exit(chrdev_exit);// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证
MODULE_AUTHOR("aston"); // 描述模块的作者
MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息
MODULE_ALIAS("alias xxx"); // 描述模块的别名信息
内存映射(Memory Mapping)
request_mem_region:声明物理内存区域占用
向内核声明对一段物理内存区域的占用,防止其他驱动或内核组件重复使用该区域。
本质:标记物理地址范围为 “已使用”,不涉及实际内存映射或访问权限设置。
struct resource *request_mem_region(loff_t start, size_t len, const char *name);
start:物理内存起始地址(如寄存器物理地址)。
len:区域长度(字节数)。
name:设备或驱动名称(用于调试和 proc/iomem 显示)。
返回值
成功:返回指向 struct resource 的指针(可忽略,一般不使用)。
失败:返回 NULL(需处理错误)。
ioremap:映射物理地址到内核虚拟地址
将物理内存地址映射到内核虚拟地址空间,以便通过指针访问硬件寄存器。
本质:建立物理地址与虚拟地址的映射关系,支持缓存和非缓存映射(取决于架构)。
void __iomem *ioremap(phys_addr_t phys_addr, size_t size);
phys_addr:待映射的物理起始地址。
size:映射区域的大小(字节数,需按页对齐,通常为 PAGE_SIZE 的倍数)。
返回值
成功:返回映射后的虚拟地址(类型为 void __iomem *,需强制类型转换为寄存器类型)。
失败:返回 NULL(需处理错误)。
iounmap:解除内存映射
功能
释放 ioremap 建立的虚拟地址映射,回收内核虚拟地址空间。
必须在模块退出或不再使用映射时调用,否则会导致内存泄漏或内核崩溃。
void iounmap(void __iomem *addr);
addr:通过 ioremap 返回的虚拟地址指针。
release_mem_region:释放物理内存区域占用
释放 request_mem_region 声明的物理内存区域占用,允许其他驱动使用该区域。
void release_mem_region(loff_t start, size_t len);
start:物理内存起始地址(需与 request_mem_region 中的 start 一致)。
len:区域长度(需与 request_mem_region 中的 len 一致)。
总结
request_mem_region:声明物理内存占用,避免冲突。
ioremap:建立物理地址到内核虚拟地址的映射,用于寄存器访问。
iounmap:解除映射,回收虚拟地址资源。
release_mem_region:释放物理内存占用,允许重新分配。
学习记录,侵权联系删除。
来源:朱老师物联网大课堂