一、硬件原理
1. 原理图确定硬件连接
i.MX6ULL开发板上有用户LED,如图: LED0连接到GPIO_3.查看核心板原理图中的对应性i.MX6ULL的引脚为。
2. 如何控制芯片手册中的引脚?
因为i.MX该系列的外设原理基本相同,因此在本系列文章中,请阅读外设原理i.MXRT详细分析1062中。
- i.MX RT开发笔记-04 | 使用 IOMUXC 和 GPIO 点亮LED
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,而,如同:
Linux内核启动时会初始化MMU,设置内存映射,。
2. 地址映射之间的转换
我们只知道寄存器的物理地址,CPU访问时需要虚拟地址,Linux内核为我们提供了地址之间的转换函数。
(1)
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函数用来释放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