i.MX6ULL驱动开发 | 03-基于字符设备驱动框架点亮LED
Posted Mculover666
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了i.MX6ULL驱动开发 | 03-基于字符设备驱动框架点亮LED相关的知识,希望对你有一定的参考价值。
一、硬件原理
1. 看原理图确定硬件连接
i.MX6ULL开发板上板载了一个用户LED,如图:
LED0连接到GPIO_3,在核心板原理图上查看对应到i.MX6ULL的引脚为GPIO1_IO03。
2. 看芯片手册如何控制引脚
因为i.MX系列的外设原理基本一样,所以在本系列文章中,关于外设原理请阅读之前i.MXRT1062中的详细分析。
2.1. IOMUXC外设选择引脚复用
(1)SW_MUX_CTL_PAD寄存器:用于设置某个引脚的IOMUX,选择该引脚的功能。
(2)SW_PAD_CTL_PAD寄存器:用于设置某个引脚的属性,比如驱动能力、是否使用上下拉电阻等。
2.2. GPIO外设
(1)配置GPIO引脚方向
(2)配置GPIO引脚电平
2.3. 外设时钟使能
CCM模块的CCM Clock Gating Register1寄存器(CCM_CCGR1):
其中CG13用来控制GPIO1外设时钟:
二、地址映射——MMU
在MCU中可以直接通过绝对地址访问到寄存器,但i.MX6ULL是Cortex-A7内核,带有MMU,事情似乎不妙了起来。
1. 地址映射
MMU全称Memory Manage Unit,内存管理单元。MMU主要完成的功能如下:
- 完成虚拟空间到物理空间的映射
- 内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性
对于32位的处理器来说,虚拟地址(VA,Virual Address)的范围是 2 3 2 = 4 G B 2^32=4GB 232=4GB,而本文所使用的开发板板载512MB的DDR,这512MB就是物理内存,经过MMU可以将其映射到4GB的虚拟空间,如同:
Linux内核启动的时候会初始化MMU,设置内存映射,这之后CPU访问的都是虚拟地址。
2. 地址映射之间的转换
我们只知道寄存器的物理地址,而CPU访问时需要虚拟地址,Linux内核为我们提供了地址之间的转换函数。
(1)ioremap函数
ioremap函数用来获取指定物理地址空间对应的虚拟地址空间,定义在arch/arm/include/asm/io.h
中:
#define ioremap(cookie,size) __arm_ioremap((cookie), (size), MT_DEVICE)
__arm_ioremap
函数定义在arch/arm/mm/ioremap.c
w文件中:
void __iomem *
__arm_ioremap(phys_addr_t phys_addr, size_t size, unsigned int mtype)
return arch_ioremap_caller(phys_addr, size, mtype,
__builtin_return_address(0));
该函数的参数作用如下:
- phys_addr:要映射的物理起始地址
- size:要映射的内存空间大小
- mtype:ioremap的类型,可以选择 MT_DEVICE、 MT_DEVICE_NONSHARED、MT_DEVICE_CACHED 和 MT_DEVICE_WC
该函数的返回值为映射后的虚拟空间首地址。
eg. 获取 I.MX6ULL 的 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 寄存器对应的虚拟地址:
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
static void __iomem* SW_MUX_GPIO1_IO03;
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
(2)iounmap函数
iounmap函数用来释放ioremap函数所做的映射,同样在文件arch/arm/include/asm/io.h
中:
#define iounmap __arm_iounmap
``函数定义如下:
void __arm_iounmap(volatile void __iomem *io_addr)
arch_iounmap(io_addr);
该函数只有一个参数,io_addr表示要取消映射的虚拟地址空间首地址。
eg. 取消 I.MX6ULL 的 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 寄存器对虚拟地址映射:
iounmap(SW_MUX_GPIO1_IO03);
3. 虚拟内存访问函数
使用ioremap函数将寄存器的物理地址映射到虚拟地址之后,其实可以通过指针直接访问这些内存,但是Linux内核不推荐这么做,而是推荐使用一组操作函数来对映射后的虚拟内存进行读写操作。
这些函数在arch/arm/include/asm/io.h
中声明。
(1)读操作函数
#define readb(c) ( u8 __v = readb_relaxed(c); __iormb(); __v; )
#define readw(c) ( u16 __v = readw_relaxed(c); __iormb(); __v; )
#define readl(c) ( u32 __v = readl_relaxed(c); __iormb(); __v; )
这些函数的底层实现函数如下:
static inline u8 __raw_readb(const volatile void __iomem *addr);
static inline u16 __raw_readw(const volatile void __iomem *addr);
static inline u32 __raw_readl(const volatile void __iomem *addr);
(2)写操作函数
#define writeb(v,c) ( __iowmb(); writeb_relaxed(v,c); )
#define writew(v,c) ( __iowmb(); writew_relaxed(v,c); )
#define writel(v,c) ( __iowmb(); writel_relaxed(v,c); )
这些函数的底层实现函数如下:
static inline void __raw_writeb(u8 val, volatile void __iomem *addr);
static inline void __raw_writew(u16 val, volatile void __iomem *addr);
static inline void __raw_writel(u32 val, volatile void __iomem *addr);
四、设备驱动框架如何传递数据
用户应用程序运行在用户态,驱动程序运行在内核态。当应用程序中调用write向驱动程序写入数据时,驱动程序如何get到数据呢?
1. 参数传递
在驱动程序中,我们编写的write函数如下:
static ssize_t led_write(struct file *fp, const char __user *buf, size_t len, loff_t *off)
return 0;
这其中的参数,就用来在调用时,接收应用程序传递下来的数据:
- fp:文件描述符,表示打开的设备文件描述符
- buf:要写给设备的数据
- len:要写入的数据长度
- off:相对于文件首地址的偏移
返回值是一个 ssize_t 类型,用来返回成功写入的字符数,如果写入失败则返回错误码(通常为负值)。
2. 数据拷贝
我们可以直接用指针访问应用程序传下来的buf,但在Linux中推荐进行一次数据拷贝。
定义在文件arch/arm/include/asm/uaccess.h
中。
(1)copy_from_user
:从用户空间拷贝数据。
static inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n)
if (access_ok(VERIFY_READ, from, n))
n = __copy_from_user(to, from, n);
else /* security hole - plug it */
memset(to, 0, n);
return n;
(2)copy_to_user
:拷贝数据到用户空间。
static inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n)
if (access_ok(VERIFY_WRITE, to, n))
n = __copy_to_user(to, from, n);
return n;
五、点亮LED
1、 编写驱动代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define CCM_CCGR1_BASE (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)
#define GPIO1_DR_BASE (0X0209C000)
#define GPIO1_GDIR_BASE (0X0209C004)
static dev_t led_num;
static struct cdev *led_cdev;
static struct class *led_class;
static struct device *led0;
static void __iomem *iMX6ULL_CCM_CCGR1;
static void __iomem *iMX6ULL_SW_MUX_GPIO1_IO03;
static void __iomem *iMX6ULL_SW_PAD_GPIO1_IO03;
static void __iomem *iMX6ULL_GPIO_GDIR;
static void __iomem *iMX6ULL_GPIO1_DR;
/**
* @brief LED板级初始化
*/
static int led_board_init(void)
u32 val;
// 设置寄存器地址映射
iMX6ULL_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
iMX6ULL_SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
iMX6ULL_SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
iMX6ULL_GPIO_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
iMX6ULL_GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
// 使能外设时钟
val = readl(iMX6ULL_CCM_CCGR1);
val &= ~(3 << 26);
val |= (3 << 26);
writel(val, iMX6ULL_CCM_CCGR1);
// 设置IOMUXC引脚复用和引脚属性
writel(5, iMX6ULL_SW_MUX_GPIO1_IO03);
writel(0x10B0, iMX6ULL_SW_PAD_GPIO1_IO03);
// 设置GPIO引脚方向
val = readl(iMX6ULL_GPIO_GDIR);
val &= ~(1 << 3);
val |= (1 << 3);
writel(val, iMX6ULL_GPIO_GDIR);
// 设置GPIO输出高电平,默认关闭LED
val = readl(iMX6ULL_GPIO1_DR);
val |= (1 << 3);
writel(val, iMX6ULL_GPIO1_DR);
return 0;
/**
* @brief LED板级释放
*/
static void led_board_deinit(void)
// 取消寄存器地址映射
iounmap(iMX6ULL_CCM_CCGR1);
iounmap(iMX6ULL_SW_MUX_GPIO1_IO03);
iounmap(iMX6ULL_SW_PAD_GPIO1_IO03);
iounmap(iMX6ULL_GPIO_GDIR);
iounmap(iMX6ULL_GPIO1_DR);
/**
* @brief LED板级释放
*/
static void led_board_ctrl(int status)
u32 val;
if (status == 1)
// 打开LED
val = readl(iMX6ULL_GPIO1_DR);
val &= ~(1 << 3);
writel(val, iMX6ULL_GPIO1_DR);
else
// 关闭LED
val = readl(iMX6ULL_GPIO1_DR);
val |= (1 << 3);
writel(val, iMX6ULL_GPIO1_DR);
static int led_open(struct inode *node, struct file *fp)
return 0;
static ssize_t led_read(struct file *fp, char __user *buf, size_t len, loff_t *off)
return 0;
static ssize_t led_write(struct file *fp, const char __user *buf, size_t len, loff_t *off)
int ret;
unsigned char data_buf[1];
unsigned char led_status;
// 拷贝用户传入数据
ret = copy_from_user(data_buf, buf, 1);
if (ret < 0)
printk("led write failed!\\n");
return -EFAULT;
// 控制LED
led_status = data_buf[0];
if (led_status == 0)
led_board_ctrl(0);
else if (led_status == 1)
led_board_ctrl(1);
return 0;
static int led_close(struct inode *node, struct file *fp)
return 0;
static struct file_operations led_fops =
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_close
;
static int __init led_module_init(void)
int ret;
ret = led_board_init();
if (ret != 0)
printk(KERN_WARNING"led_board_init failed!\\n");
return -1;
// 1. 申请设备号
ret = alloc_chrdev_region(&led_num, 0, 1, "led");
if (ret != 0)
printk(KERN_WARNING"alloc_chrdev_region failed!\\n");
return -1;
// 2. 申请cdev
led_cdev = cdev_alloc();
if (!led_cdev)
printk(KERN_WARNING"cdev_alloc failed!\\n");
return -1;
// 3. 初始化cdev
led_cdev->owner = THIS_MODULE;
led_cdev->ops = &led_fops;
// 4. 注册cdev
ret = cdev_add(led_cdev, led_num, 1);
if (ret != 0)
printk(KERN_WARNING"cdev_add failed!\\n");
return -1;
// 5. 创建设备类
led_class = class_create(THIS_MODULE, "led_class");
if (!led_class)
printk(KERN_WARNING"class_create failed!\\n");
return -1;
// 6. 创建设备节点
led0 = device_create(led_class, NULL, led_num, NULL, "led0");
if (IS_ERR(led0))
printk(KERN_WARNING"device_create failed!\\n");
return -1;
return 0;
static void __exit led_module_exit(void)
led_board_deinit();
// 1. 删除设备节点
device_destroy(led_class, led_num);
// 2. 删除设备类
class_destroy(led_class);
// 3. 删除cdev
cdev_del(led_cdev);
// 4. 释放设备号
unregister_chrdev_region(led_num, 1);
module_init(led_module_init);
module_exit(led_module_exit);
MODULE_AUTHOR("mculover666");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("a led demo");
2. 编译
KERNEL_DIR = /home/mculover666/imx6ull/kernel/linux-imx6ull
obj-m := led.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
3. 编写测试程序
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char *argv[])
int fd;
int ret;
char *filename = NULL;
unsigned char data_buf[1];
// 检查参数
if (argc != 3)
printf("usage: ./test_led [device] [led status]\\n");
// 打开设备文件
filename = argv[1];
fd = open(filename, O_RDWR);
if (fd < 0)
printf("open %s error!\\n", filename);
return 0;
// 写文件
data_buf[0] = atoi(argv[2]);
ret = write(fd, data_buf, sizeof(data_buf));
if (ret < 0)
printf("write %s error!\\n", data_buf);
// 关闭文件
close(fd);
return 0;
编译:
arm-linux-gnueabihf-gcc test_led.c -o test_led
4. 测试
在板子上运行,加载驱动模块:
insmod led.ko
打开led灯:
./test_led /dev/led0 1
关闭led灯:
./test_led /dev/led0 0
以上是关于i.MX6ULL驱动开发 | 03-基于字符设备驱动框架点亮LED的主要内容,如果未能解决你的问题,请参考以下文章
i.MX6ULL驱动开发 | 08 -基于pinctrl子系统和gpio子系统点亮LED
i.MX6ULL驱动开发 | 34 - 基于SPI框架驱动spi lcd(st7789)