0
点赞
收藏
分享

微信扫一扫

i.MX6ULL驱动开发 | 03-基于字符设备驱动框架点亮LED


一、硬件原理

1. 看原理图确定硬件连接

i.MX6ULL开发板上板载了一个用户LED,如图:

i.MX6ULL驱动开发 | 03-基于字符设备驱动框架点亮LED_#define

LED0连接到GPIO_3,在核心板原理图上查看对应到i.MX6ULL的引脚为​GPIO1_IO03​。

2. 看芯片手册如何控制引脚

因为i.MX系列的外设原理基本一样,所以在本系列文章中,关于外设原理请阅读之前i.MXRT1062中的详细分析。

  • ​​i.MX RT开发笔记-04 | 使用 IOMUXC 和 GPIO 点亮LED​​

2.1. IOMUXC外设选择引脚复用

(1)SW_MUX_CTL_PAD​寄存器:用于设置某个引脚的IOMUX,选择该引脚的功能。
i.MX6ULL驱动开发 | 03-基于字符设备驱动框架点亮LED_#define_02
(2)SW_PAD_CTL_PAD
​寄存器:用于设置某个引脚的属性,比如驱动能力、是否使用上下拉电阻等。

i.MX6ULL驱动开发 | 03-基于字符设备驱动框架点亮LED_引脚_03

2.2. GPIO外设

i.MX6ULL驱动开发 | 03-基于字符设备驱动框架点亮LED_#define_04

(1)配置GPIO引脚方向

i.MX6ULL驱动开发 | 03-基于字符设备驱动框架点亮LED_驱动开发_05

(2)配置GPIO引脚电平

i.MX6ULL驱动开发 | 03-基于字符设备驱动框架点亮LED_驱动开发_06

2.3. 外设时钟使能

CCM模块的CCM Clock Gating Register1寄存器(CCM_CCGR1):

i.MX6ULL驱动开发 | 03-基于字符设备驱动框架点亮LED_linux_07

其中CG13用来控制GPIO1外设时钟:

i.MX6ULL驱动开发 | 03-基于字符设备驱动框架点亮LED_驱动开发_08

二、地址映射——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的虚拟空间​,如同:

i.MX6ULL驱动开发 | 03-基于字符设备驱动框架点亮LED_#define_09

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 UART

0 条评论