视频:第4.1讲 Linux LED灯驱动实验(直接操作寄存器)-地址映射_哔哩哔哩_bilibili

指南:详见《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.81》41.1节内容

开发板:imx6ull mini

虚拟机:VMware17

ubuntu:ubuntu20.04

一、MMU

详见《指南》41.1.1部分。

MMU的两个功能:

        ①完成虚拟空间到物理空间的映射

        ②内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。

        主要看第①点。

        一个处理器为32位,则对应的虚拟空间大小为2^32=4GB;开发板的DDR3大小为512MB,即物理空间大小为512MB。    现在让物理内存空间映射到虚拟空间,则一个物理地址就会对应多个虚拟地址。这个映射过程就是通过MMU来完成的。

        Linux内核启动的时会初始化MMU以设置内存映射,设置好以后CPU访问的都是虚拟地址。

        比如I.MX6ULL的GPIO1_IO03引脚的复用寄存器的地址为0X020E0068。如果没有开启MMU的话,直接向0X020E0068地址写入数据就可以配置GPIO1_IO03的复用功能。现在开启了MMU并设置了内存映射,不能直接向0X020E0068写入数据。必须使用其对应的虚拟地址。这就涉及到了物理内存和虚拟内存之间的转换,需要用到两个函数:ioremap和iounmap。

二、映射函数

1、ioremap

// 具体实现在linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/mm/nummo.c
void __iomem *__arm_ioremap(phys_addr_t phys_addr, size_t size,	unsigned int mtype)
{
	return (void __iomem *)phys_addr;
}
//cookie   : 要映射的物理起始地址
//size     : 要映射的内存空间大小(字节)
//mtype    : ioremap的类型,ioremap函数一律是MT_DEVICE。 
//return   : __iomem类型的指针,指向映射后的虚拟空间首地址。


// define定义在linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include/asm/io.h

#define ioremap(cookie,size)  __arm_ioremap((cookie), (size), MT_DEVICE)

用法: 

        假如要获取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);   //IMX6ULL的一个寄存器地址是32位4字节

2、iounmap

        卸载驱动时,使用iounmap释放掉ioremap的映射。

// define定义在linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include/asm/io.h
#define iounmap	__arm_iounmap

// 具体实现在linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/mm/nummo.c
void __arm_iounmap(volatile void __iomem *addr) //addr即为虚拟地址
{
}

用法:

iounmap(SW_MUX_GPIO1_IO03);    //SW_MUX_GPIO1_IO03为刚才获得的虚拟地址

三、LED字符设备驱动框架

1、建立文件结构

        在上次的1_chrdevbase根目录下新建新的文件夹2_led。直接把1_chrdevbase的内容复制到2_led下,新的代码直接在其基础上修改即可。

cd .../1_chrdevbase    
cd ..                # cd到1_chrdevbase的根目录
mkdir 2_led
cp 1_chrdevbase/* ./2_led/
cp 1_chrdevbase/.vscode/ ./2_led/ -r
cd 2_led/

rm chrdevbase.code-workspace
rm chrdevbaseAPP
mv chrdevbaseAPP.c ledAPP.c
mv chrdevbase.c led.c
ls -a   # 只留下ledAPP.c  led.c  Makefile  .vscode即可

 VS打开2_led文件夹,另存为工作区

LED(工作区)
└── 2_led
    ├── .vscode
    │   ├── c_cpp_properties.json
    │   └── settings.json
    ├── led.c
    ├── led.code-workspace
    ├── ledAPP.c
    └── Makefile

2、编写代码

2.1 修改Makefile

        只修改obj-m这一行即可:

obj-m := led.o			# 编译文件

2.2 修改led.c

        内存访问函数

        详见(09、文档教程(非常重要)/【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.81.pdf)41.1.2部分。

        当外部寄存器或内存映射到IO空间时,称为I/O端口。 当外部寄存器或内存映射到内存空间时,称为I/O内存。但是ARM没有I/O空间这个概念,只有I/O内存。

        使用ioremap函数将寄存器的物理地址映射到虚拟地址以后,就可以直接通过指针访问这些地址,但是Linux内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。

// 读操作函数
// addr:   虚拟地址
// return: 读取到的数据
u8  readb(const volatile void __iomem *addr)     // 8 bit
u16 readw(const volatile void __iomem *addr)     // 16 bit 
u32 readl(const volatile void __iomem *addr)     // 32 bit


// 写操作函数
// value: 要写入的数据
// addr:  虚拟地址
void writeb(u8 value,  volatile void __iomem *addr)   // 8 bit
void writew(u16 value, volatile void __iomem *addr)   // 16 bit 
void writel(u32 value, volatile void __iomem *addr)   // 32 bit
        寄存器配置

        寄存器配置详见(07、I.MX6U参考资料/02、I.MX6ULL芯片资料/IMX6ULL参考手册.pdf)

        其中CCM_CCGR1在18.6.24

        GPIOx_GDIR在28.5.2

        GPIOx_DR在28.5.1

 要使能GPIO1的时钟,需要将CCM_CCGR1寄存器的bit26、27置为1

 

要使能GPIO1_OI03为output,需要将GPIO_GDIR寄存器bit3置为1

要控制GPIO1_OI03输出高电平则DR的bit3置1,输出低电平则DR的bit3置0。低电平则亮灯

综上,配置代码为:

    /* 地址映射 */
    IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
    SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
    SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
    GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
    GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

    /* 初始化 */
    val = readl(IMX6U_CCM_CCGR1);  // 32bit读取函数
    val &= ~(3 << 26); // 清除26、27位的数据为0,其余位保持不变。  其中3的二进制为11,~为取反操作。
    val |= 3 << 26;    // 将26、27位置为1
    writel(val, IMX6U_CCM_CCGR1);   // 将 val 写入 IMX6U_CCM_CCGR1,使能CCM_CCGR1的gpio1时钟。

    writel(0x5, SW_MUX_GPIO1_IO03); // 复用GPIO1——IO03
    writel(0x10B0, SW_PAD_GPIO1_IO03); //设置GPIO1_IO03电气属性

    val = readl(GPIO1_GDIR);  // 32bit读取函数
    val |= 1 << 3;  // 设置PIO1_IO03为output。 将GPIO1_GDIR第3位 置为1
    writel(val, GPIO1_GDIR);

    /* 开灯 */
    val = readl(GPIO1_DR);  // 32bit读取函数
    val &= ~(1 << 3);       // 将bit3置为低电平,打开LED
    writel(val, GPIO1_DR);
led.c完整代码 
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/init.h>
#include<linux/fs.h>
#include<linux/uaccess.h>
#include <linux/io.h>

#define LED_MAJOR 200
#define LED_NAME  "led"

/* 寄存器物理地址 */
#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 void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;


static int led_open(struct inode * inode, struct file *filp){
    return 0;
}

static int led_release(struct inode * inode, struct file *filp){
    return 0;
}

static ssize_t led_write(struct file *filp, const char __user * buf, size_t count, loff_t * ppos){
    return 0;
}

/* 字符设备操作集合 */
static const struct file_operations led_fope = {
    .owner = THIS_MODULE,
    .open = led_open,
    .release = led_release,
    .write = led_write
};


/* 入口 */
static int __init led_init(void){
    int ret = 0;
    int val = 0;

    /* 地址映射 */
    IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
    SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
    SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
    GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
    GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

    /* 初始化 */
    val = readl(IMX6U_CCM_CCGR1);  // 32bit读取函数
    val &= ~(3 << 26); // 清除26、27位的数据为0,其余位保持不变。  其中3的二进制为11,~为取反操作。
    val |= 3 << 26;    // 将26、27位置为1
    writel(val, IMX6U_CCM_CCGR1);   // 将 val 写入 IMX6U_CCM_CCGR1,使能CCM_CCGR1的gpio1时钟。

    writel(0x5, SW_MUX_GPIO1_IO03); // 复用GPIO1——IO03
    writel(0x10B0, SW_PAD_GPIO1_IO03); //设置GPIO1_IO03电气属性

    val = readl(GPIO1_GDIR);  // 32bit读取函数
    val |= 1 << 3;  // 设置PIO1_IO03为output。 将GPIO1_GDIR第3位 置为1
    writel(val, GPIO1_GDIR);

    /* 开灯 */
    val = readl(GPIO1_DR);  // 32bit读取函数
    val &= ~(1 << 3);       // 将bit3置为低电平,打开LED
    writel(val, GPIO1_DR);


    /* 注册设备 */
    ret = register_chrdev(LED_MAJOR, LED_NAME, &led_fope);
    if(ret<0){
        printk("register chrdev failed!\r\n");
        return -EIO;
    }
    printk("led init\r\n");
    return 0;
}

/* 出口 */
static void __exit led_exit(void){
    /* 关灯 */
    val = readl(GPIO1_DR);  // 32bit读取函数
    val |= (1 << 3);       // 将bit3置为高电平,关闭LED
    writel(val, GPIO1_DR);
    
    /* 取消地址映射 */
    iounmap(IMX6U_CCM_CCGR1);
    iounmap(SW_MUX_GPIO1_IO03);
    iounmap(SW_PAD_GPIO1_IO03);
    iounmap(GPIO1_DR);
    iounmap(GPIO1_GDIR);

    /* 注销设备 */
    unregister_chrdev(LED_MAJOR, LED_NAME);
    printk("led exit\r\n");
}


/* 驱动加载卸载*/
module_init(led_init);
module_exit(led_exit);


MODULE_LICENSE("GPL");

然后编译复制挂载即可:

# vscode终端
make
sudo cp ./led.ko .../rootfs/lib/modules/4.1.15/

# 串口
cd /lib/modules/4.1.15/
modprobe led.ko    # 右下角红灯亮起
rmmod led.ko       # 右下角红灯熄灭

3、改进代码 

3.1 修改led.c

完善了字符设备操作函数write,现在通过write来控制LED亮灭,而不是加载卸载了

#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/init.h>
#include<linux/fs.h>
#include<linux/uaccess.h>
#include <linux/io.h>

#define LED_MAJOR 200
#define LED_NAME  "led"

/* 寄存器物理地址 */
#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 void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

/* LED状态 */
#define LEDOFF 0
#define LEDON  1


/* LED状态翻转 */
static void led_swtich(u8 sta){
    u32 val = 0;

    if(sta == LEDOFF){
        /* 关灯 */
        val = readl(GPIO1_DR);  // 32bit读取函数
        val |= (1 << 3);       // 将bit3置为高电平,关闭LED
        writel(val, GPIO1_DR);
    }
    else if(sta == LEDON){
        /* 开灯 */
        val = readl(GPIO1_DR);  // 32bit读取函数
        val &= ~(1 << 3);       // 将bit3置为低电平,打开LED
        writel(val, GPIO1_DR);
    }
}


static int led_open(struct inode * inode, struct file *filp){
    return 0;
}

static int led_release(struct inode * inode, struct file *filp){
    return 0;
}

static ssize_t led_write(struct file *filp, const char __user * buf, size_t count, loff_t * ppos){
    int retvalue;
    unsigned char databuf[1];
    retvalue = copy_from_user(databuf, buf, count);
    if(retvalue < 0){
        printk("kernel write fauled!\r\n");
        return -EFAULT;
    }

    led_swtich(databuf[0]);
    return 0;
}

/* 字符设备操作集合 */
static const struct file_operations led_fope = {
    .owner = THIS_MODULE,
    .open = led_open,
    .release = led_release,
    .write = led_write
};

/* 入口 */
static int __init led_init(void){
    int ret = 0;
    int val = 0;

    /* 地址映射 */
    IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
    SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
    SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
    GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
    GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

    /* 初始化 */
    val = readl(IMX6U_CCM_CCGR1);  // 32bit读取函数
    val &= ~(3 << 26); // 清除26、27位的数据为0,其余位保持不变。  其中3的二进制为11,~为取反操作。
    val |= 3 << 26;    // 将26、27位置为1
    writel(val, IMX6U_CCM_CCGR1);   // 将 val 写入 IMX6U_CCM_CCGR1,使能CCM_CCGR1的gpio1时钟。

    writel(0x5, SW_MUX_GPIO1_IO03); // 复用GPIO1——IO03
    writel(0x10B0, SW_PAD_GPIO1_IO03); //设置GPIO1_IO03电气属性

    val = readl(GPIO1_GDIR);  // 32bit读取函数
    val |= 1 << 3;  // 设置PIO1_IO03为output。 将GPIO1_GDIR第3位 置为1
    writel(val, GPIO1_GDIR);

    /* 关灯 */
    val = readl(GPIO1_DR);  // 32bit读取函数
    val |= (1 << 3);       // 将bit3置为高电平,关闭LED
    writel(val, GPIO1_DR);

    /* 注册设备 */
    ret = register_chrdev(LED_MAJOR, LED_NAME, &led_fope);
    if(ret<0){
        printk("register chrdev failed!\r\n");
        return -EIO;
    }
    printk("led init\r\n");
    return 0;
}

/* 出口 */
static void __exit led_exit(void){
    
    /* 取消地址映射 */
    iounmap(IMX6U_CCM_CCGR1);
    iounmap(SW_MUX_GPIO1_IO03);
    iounmap(SW_PAD_GPIO1_IO03);
    iounmap(GPIO1_DR);
    iounmap(GPIO1_GDIR);

    /* 注销设备 */
    unregister_chrdev(LED_MAJOR, LED_NAME);
    printk("led exit\r\n");
}

/* 驱动加载卸载*/
module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE("GPL");

3.2 修改ledAPP.c

        led.c中的file_operations结构体注册了.write = led_write,因此在应用程序执行到write(fd, databuf, sizeof(databuf))这一行时会调用内核态中的led_write函数,进行switch

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<string.h>

/* 
 * @description    : main主程序 
 * @param - argc   : argv数组元素个数 
 * @param - argv   : 具体参数 
 * @return         : 0 成功; else失败
 * 调用   ./ledAPP /dev/led <0:1>  0关灯,1开灯
 */ 

#define LEDOFF 0
#define LEDON  1

int main(int argc, char *argv[]){

    if(argc != 3){  // 判断用法是否错误
        printf("Error Usage!\r\n");
        return -1;
    }

    char *filename;
    int fd = 0;
    unsigned char databuf[1];
    int retvalue = 0;

    filename = argv[1];
    fd = open(filename, O_RDWR);  // 读写模式打开驱动文件filename

    if(fd <0){
        printf("file %s open failed!\r\n");
        return -1;
    }
    
    databuf[0] = atoi(argv[2]);  // char 2 int

    retvalue = write(fd, databuf, sizeof(databuf));   // 缓冲区数据写入fd
    if(retvalue <0){
        printf("LED Control Failed!\r\n");
        close(fd);
        return -1;
    }

    close(fd);
    return 0;
}

编译复制挂载执行:

# VSCODE终端
make
arm-linux-gnueabihf-gcc ledAPP.c -o ledAPP
sudo cp ledAPP led.ko /.../rootfs/lib/modules/4.1.15/

# 串口
cd /lib/modules/4.1.15
modprobe led.ko        # 加载
mknod /dev/led c 200 0 # 创建设备节点文件
./ledAPP /dev/led 1    # 开灯
./ledAPP /dev/led 0    # 关灯
rmmod led.ko           # 卸载

 

其他问题

1. uboot突然不能通过tftp下载zImage

第4.2讲 Linux LED灯驱动实验后半部分讲了一下这个问题

  1. 检查ubuntu的ip地址是否有变化
  2. 检查ubuntu能否上网
  3. 检查开发板和ubuntu能否ping通
  4. 查看防火墙是否都关闭了
  5. 检查当前网段中是否有ip重复的问题。先关闭开发板,然后看ubuntu能否ping通开发板ip;然后关闭ubuntu,可以直接用win的终端看能否ping通ubuntu的ip。如果能ping通则有问题,修改重复的ip地址和bootargs、serverip即可。注意1是网关,255是广播,不要用。(开发板直连电脑应该是遇不到这个问题的,开发板接路由器可能出现这个问题)

 

2. LED闪烁,挂载LED驱动没有作用

        如果uboot和内核直接用的是正点原子资料包里(01、例程源码\03、正点原子Uboot和Linux出厂源码)提供的文件,则LED已经被配置为心跳灯,此时自己写的LED驱动会被干扰。

        (10、用户手册\01【正点原子】I.MX6U用户快速体验V2.7)3.1. LED 与蜂鸣器测试部分,使用以下命令来关闭心跳灯:

echo none > /sys/class/leds/sys-led/trigger   // 改变LED的触发模式

 

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐