当前位置: 首页 > news >正文

Linux驱动学习之第二个驱动程序(LED点亮关闭驱动程序-分层设计思想,使其能适应不同的板子-驱动程序模块为多个源文件怎么写Makefile)

目录

  • 看这篇博文前请先掌握下面这些博文中的知识
  • 需要的PDF资料
  • 完整源代码
    • board_fire_imx6ull-pro.c中的代码
    • leddrv.c中的代码
    • ledtest.c中的代码
  • 程序设计思想和文件结构
  • 实现分层思想的具体方法
    • 概述
    • 具体实现分析
      • 定义结构体`led_operations`用来集合各个单板硬件层面操作LED的函数
      • 定义具体的某个单板的操作LED的各个具体函数,并作为结构体`led_operations`的一个实例的成员函数
      • 定义函数`get_board_led_opr`返回结构体`led_operations`的一个实例的指针
      • 上层文件(leddrv.c)的模块初始化函数中调用函数`get_board_led_opr`获得单板的底层操作实例
      • 在驱动程序中去调用底层函数对硬件的操作
  • 本篇博文略去对驱动程序代码的分析,重点分析底层对硬件操作的代码
  • 驱动程序代码虽然略去分析,但是模块初始化函数中注册驱动程序的方法和之前的有所不同【函数register_chrdev()的介绍】
    • register_chrdev函数原型
    • 参数详解
    • 返回值
    • 示例代码
    • 卸载驱动时对应的函数
    • 小结
  • 在硬件层面操作的代码的分析(`board_fire_imx6ull-pro.c`中的代码)
    • 定义用于存储寄存器映射地址值的指针
      • 关键字`volatile`的含义
    • LED控制初始化操作函数`board_demo_led_init`分析
      • 源代码
      • `ioremap`函数分析
      • GPIO操作的第01步:使能对应GPIO口的时钟(CCGR寄存器)
      • GPIO操作的第02步:设置对应的GPIO口引脚为GPIO功能【】,而不是别的复用功能
      • GPIO操作的第03步:设置对应的GPIO口为输出引脚(IO口中的Output)【GDIR寄存器】
      • GPIO操作的第04步:找到对应的GPIO口的数据寄存器并映射到虚拟地址空间【(GPIOx_DR寄存器】
    • LED控制函数`board_demo_led_ctl`分析
    • LED控制的关闭函数(释放内存映射)
  • Makfile文件的编写
  • 交叉编译
  • 上板测试
  • 附工程压缩文件和PDF文档

看这篇博文前请先掌握下面这些博文中的知识

https://blog.csdn.net/wenhao_ir/article/details/144888989
https://blog.csdn.net/wenhao_ir/article/details/144901797
https://blog.csdn.net/wenhao_ir/article/details/144881830

需要的PDF资料

开发板的原理图文件:
https://pan.baidu.com/s/1CEwPbcNUIicA1HVGwG6e5A?pwd=m9wb
IMX6ULL-CPU芯片的操作手册:
https://pan.baidu.com/s/1hZWmgmvYAA9mvzAHsPiw4g?pwd=eiuh

完整源代码

board_fire_imx6ull-pro.c中的代码

#include <linux/module.h>#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <asm/io.h>#include "led_opr.h"static volatile unsigned int *CCM_CCGR1                              ;
static volatile unsigned int *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
static volatile unsigned int *GPIO5_GDIR                             ;
static volatile unsigned int *GPIO5_DR                               ;static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */       
{unsigned int val;//printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);if (which == 0){if (!CCM_CCGR1){CCM_CCGR1                               = ioremap(0x20C406C, 4);IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x2290014, 4);GPIO5_GDIR                              = ioremap(0x020AC000 + 0x4, 4);GPIO5_DR                                = ioremap(0x020AC000 + 0, 4);}/* GPIO5_IO03 *//* a. 使能GPIO5的时钟信号* set CCM to enable GPIO5* CCM_CCGR1[CG15] 0x20C406C* bit[31:30] = 0b11*/*CCM_CCGR1 |= (3<<30);/* b. 设置GPIO5_IO03用于GPIO* set IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3*      to configure GPIO5_IO03 as GPIO* IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3  0x2290014* bit[3:0] = 0b0101 alt5*/val = *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;val &= ~(0xf);val |= (5);  //5的二进制表示为0101*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = val;/* b. 设置GPIO5_IO03作为output引脚* set GPIO5_GDIR to configure GPIO5_IO03 as output* GPIO5_GDIR  0x020AC000 + 0x4* bit[3] = 0b1*/*GPIO5_GDIR |= (1<<3);}return 0;
}static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
{//printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");if (which == 0){if (status) /* on: output 0*/{/* d. 设置GPIO5_DR输出低电平* set GPIO5_DR to configure GPIO5_IO03 output 0* GPIO5_DR 0x020AC000 + 0* bit[3] = 0b0*/*GPIO5_DR &= ~(1<<3);}else  /* off: output 1*/{/* e. 设置GPIO5_IO3输出高电平* set GPIO5_DR to configure GPIO5_IO03 output 1* GPIO5_DR 0x020AC000 + 0* bit[3] = 0b1*/ *GPIO5_DR |= (1<<3);}}return 0;
}static int board_demo_led_close(int which) /* whice表示具体是要操作哪个LED*/
{if (which == 0){if (CCM_CCGR1) {iounmap(CCM_CCGR1);CCM_CCGR1 = NULL;}if (IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3) {iounmap(IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3);IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = NULL;}if (GPIO5_GDIR) {iounmap(GPIO5_GDIR);GPIO5_GDIR = NULL;}if (GPIO5_DR) {iounmap(GPIO5_DR);GPIO5_DR = NULL;}}return 0;
}static struct led_operations board_demo_led_opr = {.num  = 1,.init = board_demo_led_init,.ctl  = board_demo_led_ctl,.close = board_demo_led_close,
};struct led_operations *get_board_led_opr(void)
{return &board_demo_led_opr;
}

leddrv.c中的代码

#include <linux/module.h>#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>#include "led_opr.h"/* 1. 确定主设备号                                                                 */
static int major = 0;
static struct class *led_class;
struct led_operations *p_led_opr;#define MIN(a, b) (a < b ? a : b)/* 3. 实现对应的open/read/write等函数,填入file_operations结构体                   */
static ssize_t led_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);return 0;
}/* write(fd, &val, 1); */
static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{int err;char status;struct inode *inode = file_inode(file);int minor = iminor(inode);printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);err = copy_from_user(&status, buf, 1);/* 根据次设备号和status控制LED */p_led_opr->ctl(minor, status);return 1;
}static int led_drv_open (struct inode *node, struct file *file)
{int minor = iminor(node);printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);/* 根据次设备号初始化LED */p_led_opr->init(minor);return 0;
}static int led_drv_close (struct inode *node, struct file *file)
{int minor = iminor(node);int err;/* 根据次设备号解除相关LED的寄存器映射关系 */err = p_led_opr->close(minor);printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);printk(KERN_INFO "LED driver: resources released.\n");return 0;
}/* 2. 定义自己的file_operations结构体                                              */
static struct file_operations led_drv = {.owner	 = THIS_MODULE,.open    = led_drv_open,.read    = led_drv_read,.write   = led_drv_write,.release = led_drv_close,
};/* 4. 把file_operations结构体告诉内核:注册驱动程序                                */
/* 5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数 */
static int __init led_init(void)
{int err;int i;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);major = register_chrdev(0, "imx6ull_led", &led_drv);  /* /dev/led */led_class = class_create(THIS_MODULE, "imx6ull_led_class");err = PTR_ERR(led_class);if (IS_ERR(led_class)) {printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);unregister_chrdev(major, "led");return -1;}p_led_opr = get_board_led_opr();for (i = 0; i < p_led_opr->num; i++)device_create(led_class, NULL, MKDEV(major, i), NULL, "imx6ull_led%d", i); /* /dev/imx6ull_led0,1,... */return 0;
}/* 6. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数           */
static void __exit led_exit(void)
{int i;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);for (i = 0; i < p_led_opr->num; i++)device_destroy(led_class, MKDEV(major, i)); /* /dev/100ask_led0,1,... */class_destroy(led_class);unregister_chrdev(major, "imx6ull_led");
}/* 7. 其他完善:提供设备信息,自动创建设备节点                                     */module_init(led_init);
module_exit(led_exit);MODULE_LICENSE("GPL");

ledtest.c中的代码


#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>/** ./ledtest /dev/100ask_led0 on* ./ledtest /dev/100ask_led0 off*/
int main(int argc, char **argv)
{int fd;char status;/* 1. 判断参数 */if (argc != 3) {printf("Usage: %s <dev> <on | off>\n", argv[0]);return -1;}/* 2. 打开文件 */fd = open(argv[1], O_RDWR);if (fd == -1){printf("can not open file %s\n", argv[1]);return -1;}/* 3. 写文件 */if (0 == strcmp(argv[2], "on")){status = 1;write(fd, &status, 1);}else{status = 0;write(fd, &status, 1);}close(fd);return 0;
}

程序设计思想和文件结构

希望写出的程序能给分层,即把真正对各单板的LED操作的实现最为最底层,然后有个中间层,是驱动程充中各单板实现LED操作都需要的代码,这样将来有个新板子只需要修改下最底层的代码就能用了。比如下面的工程文件结构:
在这里插入图片描述
以board为前缀的文件都是每个单板具体的对LED操作的底层代码,然后中间层代码放在文件leddrv.c中。
IMX6ULL开发板的底层代码在文件board_fire_imx6ull-pro.c中。
测试程序代码放在ledtest.c中。

实现分层思想的具体方法

概述

每个单板都有各自的LED控制的在硬件层面上的初始化函数、控制函数、释放(关闭)函数,我们只需要把这些函数集合到我们自定义的名为led_operations结构体中,然后在驱动程序的文件操作结构体(file_operations 结构体)中的open、read、write、release函数中去调用led_operations结构体实例中对应的函数即可,用led_operations结构体的一个实例就相当于是一个单板。

具体实现分析

定义结构体led_operations用来集合各个单板硬件层面操作LED的函数

首先在led_opr.h中定义一个名为led_operations结构体,用于存储各个单板具体的LED的硬件层面的操作函数,代码如下:

struct led_operations {int num; /* 表示有几个LED,实际上也是LED的 */int (*init) (int which); /* 初始化LED, which-哪个LED */int (*ctl) (int which, char status); /* 控制LED, which-哪个LED, status:1-亮,0-灭 */int (*close) (int which); /* 主要是对映射的寄存器作解除映射操作 */
};

定义具体的某个单板的操作LED的各个具体函数,并作为结构体led_operations的一个实例的成员函数

然后在文件board_fire_imx6ull-pro.c中定义具体的IMX6ULL开发板操作LED的硬件层面的函数,然后把这些函数作为led_operations结构体的一个实例的成员。相关代码如下:

static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */       
{unsigned int val;//printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);if (which == 0){if (!CCM_CCGR1){CCM_CCGR1                               = ioremap(0x20C406C, 4);IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x2290014, 4);GPIO5_GDIR                              = ioremap(0x020AC000 + 0x4, 4);GPIO5_DR                                = ioremap(0x020AC000 + 0, 4);}/* GPIO5_IO03 *//* a. 使能GPIO5* set CCM to enable GPIO5* CCM_CCGR1[CG15] 0x20C406C* bit[31:30] = 0b11*/*CCM_CCGR1 |= (3<<30);/* b. 设置GPIO5_IO03用于GPIO* set IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3*      to configure GPIO5_IO03 as GPIO* IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3  0x2290014* bit[3:0] = 0b0101 alt5*/val = *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;val &= ~(0xf);val |= (5);*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = val;/* b. 设置GPIO5_IO03作为output引脚* set GPIO5_GDIR to configure GPIO5_IO03 as output* GPIO5_GDIR  0x020AC000 + 0x4* bit[3] = 0b1*/*GPIO5_GDIR |= (1<<3);}return 0;
}static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
{//printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");if (which == 0){if (status) /* on: output 0*/{/* d. 设置GPIO5_DR输出低电平* set GPIO5_DR to configure GPIO5_IO03 output 0* GPIO5_DR 0x020AC000 + 0* bit[3] = 0b0*/*GPIO5_DR &= ~(1<<3);}else  /* off: output 1*/{/* e. 设置GPIO5_IO3输出高电平* set GPIO5_DR to configure GPIO5_IO03 output 1* GPIO5_DR 0x020AC000 + 0* bit[3] = 0b1*/ *GPIO5_DR |= (1<<3);}}return 0;
}static int board_demo_led_close(int which) /* whice表示具体是要操作哪个LED*/
{if (which == 0){if (CCM_CCGR1) {iounmap(CCM_CCGR1);CCM_CCGR1 = NULL;}if (IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3) {iounmap(IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3);IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = NULL;}if (GPIO5_GDIR) {iounmap(GPIO5_GDIR);GPIO5_GDIR = NULL;}if (GPIO5_DR) {iounmap(GPIO5_DR);GPIO5_DR = NULL;}}return 0;
}static struct led_operations board_demo_led_opr = {.num  = 1,.init = board_demo_led_init,.ctl  = board_demo_led_ctl,.close = board_demo_led_close,
};

定义函数get_board_led_opr返回结构体led_operations的一个实例的指针

最后,提供一个实例化结构体board_demo_led_opr的函数供上层代码调用,从而获得一个单板LED操作实例:

struct led_operations *get_board_led_opr(void)
{return &board_demo_led_opr;
}

注意:这个函数需要在头文件中声明,因为它是在文件leddrv.c中被调用的。
在这里插入图片描述

上层文件(leddrv.c)的模块初始化函数中调用函数get_board_led_opr获得单板的底层操作实例

在上层文件(leddrv.c)的模块初始化函数中调用函数get_board_led_opr获得单板的底层操作的led_operations结构体的实例的指针,比如下面这句代码:

p_led_opr = get_board_led_opr();

在这里插入图片描述

在驱动程序中去调用底层函数对硬件的操作

在驱动程序的文件操作结构体(struct file_operations led_drv)的成员函数中去调用底层的对硬件的操作,说句题外话对底层硬件的操作其实就是对寄存器的操作,比如下面的代码:

static int led_drv_open (struct inode *node, struct file *file)
{int minor = iminor(node);printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);/* 根据次设备号初始化LED */p_led_opr->init(minor);return 0;
}

在这里插入图片描述

static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{int err;char status;struct inode *inode = file_inode(file);int minor = iminor(inode);printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);err = copy_from_user(&status, buf, 1);/* 根据次设备号和status控制LED */p_led_opr->ctl(minor, status);return 1;
}

在这里插入图片描述

static int led_drv_close (struct inode *node, struct file *file)
{int minor = iminor(node);int err;/* 根据次设备号解除相关LED的寄存器映射关系 */err = p_led_opr->close(minor);printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);printk(KERN_INFO "LED driver: resources released.\n");return 0;
}

在这里插入图片描述

本篇博文略去对驱动程序代码的分析,重点分析底层对硬件操作的代码

本篇博文略去对驱动程序代码的分析,因为在前面的三篇博文中我已经把Linux驱动程序的框架和结构搞得比较清楚了,具体是下面三篇博文。
https://blog.csdn.net/wenhao_ir/article/details/144888989
https://blog.csdn.net/wenhao_ir/article/details/144901797
https://blog.csdn.net/wenhao_ir/article/details/144881830
所以,本文重点去关注硬件层面的实现,也就是文件board_fire_imx6ull-pro.c中的代码。

驱动程序代码虽然略去分析,但是模块初始化函数中注册驱动程序的方法和之前的有所不同【函数register_chrdev()的介绍】

led_init函数的代码如下:

static int __init led_init(void)
{int err;int i;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);major = register_chrdev(0, "imx6ull_led", &led_drv);  /* /dev/led */led_class = class_create(THIS_MODULE, "imx6ull_led_class");err = PTR_ERR(led_class);if (IS_ERR(led_class)) {printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);unregister_chrdev(major, "led");return -1;}p_led_opr = get_board_led_opr();for (i = 0; i < p_led_opr->num; i++)device_create(led_class, NULL, MKDEV(major, i), NULL, "imx6ull_led%d", i); /* /dev/imx6ull_led0,1,... */return 0;
}

从中我们可以看出,在整个设备的注册过程中,并没有用到博文 https://blog.csdn.net/wenhao_ir/article/details/144888989中提到的函数alloc_chrdev_regioncdev_initcdev_add,也没有像博文https://blog.csdn.net/wenhao_ir/article/details/144881830那样去使用函数alloc_chrdev_regioncdev_initcdev_add,而是使用了快速注册函数register_chrdev完成这几个函数的功能,下面是其详细介绍:

register_chrdev 是 Linux 驱动程序开发中用于注册字符设备的函数。它的主要作用是向内核注册一个字符设备,从而允许用户空间程序通过设备文件与驱动程序交互。

register_chrdev函数原型

int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);

参数详解

  1. major
    指定设备的主设备号。如果为 0,内核会自动分配一个主设备号,并返回分配的值。主设备号用于区分设备的类别。

  2. name
    设备的名称,用于标识设备。可以通过 /proc/devices 文件查看注册的字符设备名称和对应的主设备号。

  3. fops
    指向 struct file_operations 的指针,用于定义该字符设备支持的操作集。例如,openreadwrite 等函数。

返回值

  • 如果成功,返回分配的主设备号(major 参数)。
  • 如果失败,返回一个负数错误码。

示例代码

int major;
struct file_operations led_drv = {.owner = THIS_MODULE,.open = led_open,.release = led_release,.write = led_write,
};major = register_chrdev(0, "imx6ull_led", &led_drv);
if (major < 0) {printk(KERN_ERR "Failed to register device: %d\n", major);return major;
}
printk(KERN_INFO "Registered device with major number %d\n", major);

解释

  1. register_chrdev(0, "imx6ull_led", &led_drv)

    • 0: 让内核自动分配一个主设备号。
    • "imx6ull_led": 驱动程序名称(主设备号用来找到驱动程序),便于识别。
    • &led_drv: 定义字符设备操作的函数指针集合。
  2. struct file_operations led_drv
    包含设备的操作方法,例如 openreleasewrite。这些方法在应用程序访问设备时被调用。

  3. 返回的主设备号
    如果注册成功,major 变量会保存分配的主设备号,可以用于创建设备节点。例如:

    mknod /dev/imx6ull_led c <major> 0
    
  4. 错误检查
    如果注册失败,打印错误信息并返回错误码。


卸载驱动时对应的函数

在卸载驱动时,需要调用 unregister_chrdev 注销设备:

unregister_chrdev(major, "imx6ull_led");

小结

register_chrdev 是字符设备驱动程序中快速注册设备的方法。尽管较为简单,但在复杂场景中可能不够灵活,因此通常在学习或简单驱动开发中使用,推荐更复杂的场景使用 cdevalloc_chrdev_region

在硬件层面操作的代码的分析(board_fire_imx6ull-pro.c中的代码)

定义用于存储寄存器映射地址值的指针

static volatile unsigned int *CCM_CCGR1                              ;
static volatile unsigned int *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
static volatile unsigned int *GPIO5_GDIR                             ;
static volatile unsigned int *GPIO5_DR                               ;

关键字volatile的含义

这些定义里面首先要理解关键字volatile的含义,详细解释请参见我的另一篇博文:
https://blog.csdn.net/wenhao_ir/article/details/144983848

LED控制初始化操作函数board_demo_led_init分析

源代码

static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */       
{unsigned int val;//printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);if (which == 0){if (!CCM_CCGR1){CCM_CCGR1                               = ioremap(0x20C406C, 4);IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x2290014, 4);GPIO5_GDIR                              = ioremap(0x020AC000 + 0x4, 4);GPIO5_DR                                = ioremap(0x020AC000 + 0, 4);}/* GPIO5_IO03 *//* a. 使能GPIO5* set CCM to enable GPIO5* CCM_CCGR1[CG15] 0x20C406C* bit[31:30] = 0b11*/*CCM_CCGR1 |= (3<<30);/* b. 设置GPIO5_IO03用于GPIO* set IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3*      to configure GPIO5_IO03 as GPIO* IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3  0x2290014* bit[3:0] = 0b0101 alt5*/val = *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;val &= ~(0xf);val |= (5);*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = val;/* b. 设置GPIO5_IO03作为output引脚* set GPIO5_GDIR to configure GPIO5_IO03 as output* GPIO5_GDIR  0x020AC000 + 0x4* bit[3] = 0b1*/*GPIO5_GDIR |= (1<<3);}return 0;
}

ioremap函数分析

相关代码如下:

CCM_CCGR1                               = ioremap(0x20C406C, 4);
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x2290014, 4);
GPIO5_GDIR                              = ioremap(0x020AC000 + 0x4, 4);
GPIO5_DR                                = ioremap(0x020AC000 + 0, 4);

这些代码显然就是把寄存器的物理地址映射到Linux的虚拟地址中,在前面的博文
IMX6ULL开发板基础实验:Framebuffer驱动程序的简单应用实例代码详细分析中,用到了函数mmap,也是把地址映射到虚拟地址,不同的是mmap映射的是设备的内存,而这里咱们需要映射的是CPU的物理地址。
对于函数mmap的讲解详见我的另一篇博文
嵌入式Linux开发中的内存映射函数mmap详解?什么叫内存映射?什么叫对设备的显式和隐式读写操作?什么叫程序的虚拟内存?

对这里遇到的函数 ioremap()的详细分析,见博文 https://blog.csdn.net/wenhao_ir/article/details/144985338

GPIO操作的第01步:使能对应GPIO口的时钟(CCGR寄存器)

GPIO操作的第01步例是使能对应GPIO口的时钟。

相关的控制模块是CCM(Clock Controller Module-时钟控制模块) 。

首先我们要知道我们要操作哪个GPIO口。

从下面这幅开发板的原理图截图我们可以知道:
在这里插入图片描述
我们需要操作的对象是第5组GPIO口的第3个GPIO口,即GPIO5_IO03,别人的经验告诉我们GPIO的时钟信号由CCM(Clock Controller Module)寄存器控制,所以去IMX6ULL的的操作手册中去查找相关的CCM(Clock Controller Module)寄存器,不妨直接在手册中搜索“gpio5”,发现有下面的内容:
在这里插入图片描述
点击一看,发现正是CCM模块中的内容:
在这里插入图片描述
上面红框中的内容显示GPIO5的时钟控制为CCGR1[CG15],即CCGR1寄存器的CG15位,然后我们就去查CCGR1寄存器,先搜索PDF左边的书签:
在这里插入图片描述
通过PDF的书签搜索到了,点击查看:
在这里插入图片描述
从这幅截图中我们可以看出,CG15位是指CCGR1寄存器的第30位和31位,其初始值为11,但在Description描述中却为Reserved,Reserved的意思是保留未用的意思,那这样看来,这个操作手册有点前后矛盾了,可能是文档的疏漏吧,别人的经验告诉我们,其实这两位就是控制GPIO5的时钟的,具体意义如下:

  • 00: 时钟关闭。
  • 01: 时钟仅在硬件需要时使能。
  • 10: 时钟保留,通常不用。
  • 11: 时钟始终使能。

这里它的Reset值为11,说明它默认就是使能的,所以其实这里不需要设置,不过为了程序的健壮性和兼容性,有可能这个系列别的CPU需要设置嘛,所以不忍这里也设置一下其值。

从上面这幅截图我们还可以看出,CCGR1的物理地址为20C_406Ch,即20C406C,这样我们就获得了其物理地址,所以便可以写出下面的代码了:

CCM_CCGR1 = ioremap(0x20C406C, 4);/* GPIO5_IO03 */
/* a. 使能GPIO5的时钟信号* set CCM to enable GPIO5* CCM_CCGR1[CG15] 0x20C406C* bit[31:30] = 0b11*/
*CCM_CCGR1 |= (3<<30);

注意:*CCM_CCGR1 |= (3<<30);只设置了31位和32位为1,寄存器别的位的值没有变 ,这个或运算表达式应该是比较好懂的。

GPIO操作的第02步:设置对应的GPIO口引脚为GPIO功能【】,而不是别的复用功能

在这里,我们需要设置GPIO5_IO03,即第5组GPIO口的第3个引脚为输出功能。

别人的经验告诉我们,这涉及到 IOMUXC(I/O Multiplex Controller),所以去书签中搜索IOMUXC
在这里插入图片描述
可见是第32章的内容,所以就浏览这一章的内容,然后浏览起来才一个麻烦,不妨借助搜索引擎或chatgpt,找到最有可能的寄存器,然后去操作手册中去确认。

问chatgpt:
我想去控制IMX6ULL的GPIO5的第3个引脚为GPIO功能,该设置IOMUXC中的哪个寄存器?

回答:
IMX6ULL 中,控制 GPIO5 的第 3 个引脚(GPIO5_IO03) 为 GPIO 功能时,需要设置 IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 寄存器。


寄存器名称与地址

  • 寄存器名称IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3
  • 寄存器地址0x2290014

寄存器功能说明
该寄存器用于控制引脚 SNVS_TAMPER3(复用为 GPIO5_IO03)的复用功能。寄存器的低 4 位(bit[3:0])决定该引脚的功能。


复用模式设置
为了将引脚配置为 GPIO 功能,需要将低 4 位设置为 0101(ALT5 模式)

  • 低 4 位值说明
    • 0000(ALT0):复用为主功能(例如某外设)。
    • 0101(ALT5):复用为 GPIO 功能。

chatgpt说是寄存器IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3的低 4 位(bit 3:0)决定该引脚的功能,并且说这个寄存器的物理地址为0x2290014

我们可以搜索TAMPER3,也可以按操作手册的格式搜索地址229_0014h,去找到操作手册中对应的内容(从下面的截图来看,还是搜索地址是最精确的):
在这里插入图片描述
对应的内容为:
在这里插入图片描述
注意,这个寄存器的第3位SION的意义如下:
在 IMX6ULL 的 IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 寄存器中,第3 位(SION)用于控制引脚的输入路径是否始终有效。
以下是详细说明:


SION 位的作用

  1. 名称

    • SION: Software Input On
  2. 位置

    • 寄存器的第 3 位(从 0 开始计数)。
  3. 功能描述

    • 0:正常模式(默认值)。
      • 引脚的输入路径根据复用功能(MUX_MODE)的设置和引脚状态进行控制。
    • 1:强制输入模式。
      • 无论复用功能(MUX_MODE)的设置如何,始终保持引脚的输入路径打开。
  4. 影响

    • 在复用为 GPIO 功能时,如果引脚仅用于输出,可以关闭输入路径(即 SION = 0),以节省功耗。
    • 如果引脚需要作为输入或具有输入功能(如读取状态、实现中断等),需要打开输入路径(即 SION = 1)。

SION 位的使用场景

  • GPIO 作为输入引脚
    如果引脚配置为 GPIO 输入,或者需要读取引脚的状态,必须将 SION 设置为 1,确保输入路径打开。

  • GPIO 作为输出引脚
    如果引脚仅用作 GPIO 输出,可以将 SION 设置为 0,避免不必要的输入路径开销。

  • 复用其他功能时

    • 某些复用功能(如 UART、I2C 等)可能需要强制打开输入路径,即设置 SION = 1。
    • 如果复用功能本身不需要输入,通常可以关闭 SION(设置为 0)。

从上面的截图中可以看出,chatgpt是对的,所以可以写出下面的代码:

IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x2290014, 4);/* b. 设置GPIO5_IO03用于GPIO* set IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3*      to configure GPIO5_IO03 as GPIO* IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3  0x2290014* bit[3:0] = 0b0101 alt5*/
val = *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
val &= ~(0xf);
val |= (5); //5的二进制表示为0101
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = val;

其中val &= ~(0xf);的作用是清零低 4 位,防止之前的值对当前配置产生干扰。
其清零低4位的详细过程如下

  1. 位操作分析

    • 0xf 的二进制表示是 0000 1111,表示低 4 位全为 1。
    • ~(0xf) 进行按位取反后,结果是 1111 0000,即低 4 位全为 0,其他位全为 1。
  2. 与运算(&

    • val &= ~(0xf); 等价于 val = val & ~(0xf);
    • 这会将 val 的低 4 位强制清零,同时保留其他位的原始值。
  3. 效果
    假设 val 的原始值为 0b1011_1111,执行 val &= ~(0xf); 后:

    原始值: 1011 1111
    ~(0xf): 1111 0000
    按位与: 1011 0000
    

    结果是 0b1011_0000,即低 4 位清零,其他位保持不变。

GPIO操作的第03步:设置对应的GPIO口为输出引脚(IO口中的Output)【GDIR寄存器】

还是按第02步中的方法,直接先问chatgpt,然后再去手册中查阅。

问:已经设置好了IMX6ULL的GPIO5的第3个引脚为GPIO功能,现在想把它设为output,该设置哪个寄存器,怎么设置?

IMX6ULL 中,将 GPIO5 的第 3 个引脚(GPIO5_IO03) 配置为 输出,需要设置 GPIO5_GDIR(GPIO Direction Register) 寄存器。


寄存器名称与地址

  • 寄存器名称GPIO5_GDIR
  • 寄存器地址0x020AC004(GPIO5 基地址 0x020AC000 + 偏移 0x4

寄存器功能说明

  1. GDIR(GPIO Direction Register)

    • 每个 GPIO 引脚都有一个对应的方向位。
    • 位值:
      • 0:输入(Input)。
      • 1:输出(Output)。
  2. GPIO5_IO03 对应的位

    • GPIO5 的第 3 个引脚对应 GDIR 寄存器的 第 3 位(bit[3])。

所以我们去操作手册中搜索20A_C004h,结果没有搜索到结果…

那就去搜索GPIO5_GDIR,有一个结果:
在这里插入图片描述
在这里插入图片描述
从上需截图可以看出,其地址为20AC004,所以以后我们第一次搜索地址的时候如果找不到结果,不妨去掉最后的h再试
最后面的蓝色的字表明了详细介绍是在多少页,我们可以看到是在1359页。
在这里插入图片描述
这个介绍是个通用的介绍,从中我们可以看出你要操作某个组的GPIO的第几个GPIO口,就设置哪个位,比如我要操作GPIO5的第3位,我就去设置GPIO5_GDIR的第3位就行了,当值为0时是输入口,当值为1时为输出口。
据此,可以写出代码如下:

GPIO5_GDIR = ioremap(0x020AC000 + 0x4, 4);/* b. 设置GPIO5_IO03作为output引脚* set GPIO5_GDIR to configure GPIO5_IO03 as output* GPIO5_GDIR  0x020AC000 + 0x4* bit[3] = 0b1*/
*GPIO5_GDIR |= (1<<3);

GPIO操作的第04步:找到对应的GPIO口的数据寄存器并映射到虚拟地址空间【(GPIOx_DR寄存器】

先要找到目标寄存器,还是先问chatgpt,然后去应证。

问:现在我把GPIO5_IO03已经设为输出引脚了,我到哪个寄存器中去设置它的输出值?

在 IMX6ULL 中,GPIO5_IO03 配置为输出引脚后,设置其输出值需要操作 GPIO5 的数据寄存器 GPIO5_DR(GPIO Data Register)


GPIO5_DR 寄存器

  1. 寄存器名称GPIO5_DR
  2. 寄存器地址0x020AC000(GPIO5 基地址 0x020AC000)。
  3. 功能描述
    • 用于设置 GPIO 引脚的输出值。
    • 每个 GPIO 引脚对应寄存器中的一位:
      • 0:引脚输出低电平。
      • 1:引脚输出高电平。
    • 对于 GPIO5_IO03,对应 GPIO5_DR 的第 3 位(bit[3])。

设置 GPIO 输出值的方法

  1. 将引脚输出低电平

    • 清除 GPIO5_DR 的第 3 位(bit[3])。
      *GPIO5_DR &= ~(1 << 3);
      
  2. 将引脚输出高电平

    • 设置 GPIO5_DR 的第 3 位(bit[3])。
      *GPIO5_DR |= (1 << 3);
      

完整代码示例
以下是将 GPIO5_IO03 输出高电平或低电平的代码:

#define GPIO5_BASE  (0x020AC000)   // GPIO5 基地址
#define GPIO5_DR    (GPIO5_BASE + 0x0) // GPIO5_DR 寄存器地址void set_gpio5_io03_output_value(int value)
{volatile unsigned int *dr;unsigned int val;// 映射 GPIO5_DR 寄存器dr = ioremap(GPIO5_DR, 4);if (!dr) {printk("Failed to ioremap GPIO5_DR register\n");return;}// 读取当前 GPIO5_DR 的值val = readl(dr);if (value) {// 设置 GPIO5_IO03 输出高电平 (bit[3] = 1)val |= (1 << 3);} else {// 设置 GPIO5_IO03 输出低电平 (bit[3] = 0)val &= ~(1 << 3);}// 写回 GPIO5_DR 寄存器writel(val, dr);// 解除映射iounmap(dr);
}

注意事项

  1. 引脚功能

    • 确保 GPIO5_IO03 已经通过 IOMUXC 配置为 GPIO 功能。
    • 确保 GPIO5_IO03 的方向已设置为输出(通过 GPIO5_GDIR 配置)。
  2. 输出控制的实际效果

    • 输出低电平:物理引脚拉低,输出电压为 0V
    • 输出高电平:物理引脚拉高,输出电压为供电电压(通常为 3.3V1.8V,具体取决于硬件设计)。

接下来,我们去手册中应证一下,看下chatgpt说的对不对,搜索020A_C000
只找到下面这个结果:
在这里插入图片描述
看不出什么,只知道这个地址与GPIO5有关…
再继续搜索书签:GPIO5_DR 也没有结果
全文搜索 GPIO5_DR,有了:
在这里插入图片描述
可见,其物理地址为 20A_C000 详细介绍在1358页的28.5.1节:
在这里插入图片描述
可见你要使GPIO5_IO03输出0和1,只需要写GPIO5_DR的第3位就行了。
所以可以写出下面的代码:

 GPIO5_DR = ioremap(0x020AC000 + 0, 4);/* b. 设置GPIO5_IO03作为output引脚* set GPIO5_GDIR to configure GPIO5_IO03 as output* GPIO5_GDIR  0x020AC000 + 0x4* bit[3] = 0b1*/
*GPIO5_GDIR |= (1<<3);

LED控制函数board_demo_led_ctl分析

源代码如下:

static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
{//printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");if (which == 0){if (status) /* on: output 0*/{/* d. 设置GPIO5_DR输出低电平* set GPIO5_DR to configure GPIO5_IO03 output 0* GPIO5_DR 0x020AC000 + 0* bit[3] = 0b0*/*GPIO5_DR &= ~(1<<3);}else  /* off: output 1*/{/* e. 设置GPIO5_IO3输出高电平* set GPIO5_DR to configure GPIO5_IO03 output 1* GPIO5_DR 0x020AC000 + 0* bit[3] = 0b1*/ *GPIO5_DR |= (1<<3);}}return 0;
}

这个代码就没啥好说了,只要清楚了LED控制的初始化函数board_demo_led_init,这里理解起来就很简单了。

LED控制的关闭函数(释放内存映射)

static int board_demo_led_close(int which) /* whice表示具体是要操作哪个LED*/
{if (which == 0){if (CCM_CCGR1) {iounmap(CCM_CCGR1);CCM_CCGR1 = NULL;}if (IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3) {iounmap(IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3);IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = NULL;}if (GPIO5_GDIR) {iounmap(GPIO5_GDIR);GPIO5_GDIR = NULL;}if (GPIO5_DR) {iounmap(GPIO5_DR);GPIO5_DR = NULL;}}return 0;
}

这段代码也没啥好说的,只是在释放前先判断下对应的指针是否为NULL,如果不是NULL,再去执行释放操作。这段代码是我自己写的,不是教程中提供的。

Makfile文件的编写

这个Makfile文件和之前的驱动程序的Makefile文件相比(https://blog.csdn.net/wenhao_ir/article/details/144881830),不同的是这里的驱动程序为两个C文件,具体是board_fire_imx6ull-pro.cleddrv.c,所以难度相当于增加了,不过大部分Makefile语句还是和(https://blog.csdn.net/wenhao_ir/article/details/144881830)中的代码相同,所以要了解这里的这个Makefile文件,可以先看下(https://blog.csdn.net/wenhao_ir/article/details/144881830)中对Makefile文件的解读。具体来说,你需要打开链接 https://blog.csdn.net/wenhao_ir/article/details/144958830 查看。

具体的Makefile文件的内容如下:

# 使用不同的Linux内核时, 一定要修改KERN_DIR,KERN_DIR代表已经配置、编译好的Linux源码的根目录KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88all:make -C $(KERN_DIR) M=`pwd` modules $(CROSS_COMPILE)gcc -o led_driver_test ledtest.c clean:make -C $(KERN_DIR) M=`pwd` cleanrm -rf modules.orderrm -f ledtest# 参考内核源码drivers/char/ipmi/Makefile
# 要想把a.c, b.c编译成ab.ko, 可以这样指定:
# ab-y := a.o b.o
# obj-m += ab.o# leddrv.c board_demo.c 编译成 led_driver.ko
led_driver-y := leddrv.o board_fire_imx6ull-pro.o
obj-m	+= led_driver.o

其实关键的就是最后两行语句:

# leddrv.c board_demo.c 编译成 led_driver.ko
led_driver-y := leddrv.o board_fire_imx6ull-pro.o
obj-m	+= led_driver.o

这其实没啥好说的,照这个格式去填写就行了,这里由Linux源码中的Makefile定义的多文件模块构建规则。再比如,如果我要用 leddrv.oboard_fire_imx6ull-pro.o生成模块文件suwenhao.ko,那我就像下面这样写:

suwenhao-y := leddrv.o board_fire_imx6ull-pro.o
obj-m	+= suwenhao.o

交叉编译

代码复制到Ubuntu中…
在这里插入图片描述

make 

在这里插入图片描述
在这里插入图片描述
复制上面两个红框中的文件到NFS文件系统中以备用…

在这里插入图片描述

上板测试

cd /mnt/led_driver

加载驱动:

insmod led_driver.ko

在这里插入图片描述
运行测试程序:

首先打开LED

./led_driver_test /dev/imx6ull_led0 on

在这里插入图片描述
上面打印输出的语句:

[  294.737362] LED driver: resources released.

是下面这句代码产生的:
在这里插入图片描述
当主函数执行到close(fd);时例会调用函数led_drv_close

发现板子上编号为2的LED打开了:

在这里插入图片描述
然后关闭LED:

./led_driver_test /dev/imx6ull_led0 off

在这里插入图片描述
发现灯关了~

卸载驱动程序模块:

卸载前先看下系统中当前存在的驱动程序模块:

cat /proc/devices

在这里插入图片描述
然后再执行卸载驱动模块的命令:

rmmod led_driver

再查看系统中当前存在的驱动程序模块:

cat /proc/devices

在这里插入图片描述
发现正常卸载了…

测试结束…一切OK~

附工程压缩文件和PDF文档

工程压缩文件:
https://pan.baidu.com/s/1LzdbQ1LDcAxbv2T7akvnxQ?pwd=ixqv

开发板的原理图文件:
https://pan.baidu.com/s/1CEwPbcNUIicA1HVGwG6e5A?pwd=m9wb
IMX6ULL-CPU芯片的操作手册:
https://pan.baidu.com/s/1hZWmgmvYAA9mvzAHsPiw4g?pwd=eiuh

相关文章:

Linux驱动学习之第二个驱动程序(LED点亮关闭驱动程序-分层设计思想,使其能适应不同的板子-驱动程序模块为多个源文件怎么写Makefile)

目录 看这篇博文前请先掌握下面这些博文中的知识需要的PDF资料完整源代码board_fire_imx6ull-pro.c中的代码leddrv.c中的代码ledtest.c中的代码 程序设计思想和文件结构实现分层思想的具体方法概述具体实现分析定义结构体led_operations用来集合各个单板硬件层面操作LED的函数定…...

手写@EnableTransactionalManagement

定义一个注解&#xff0c;用于标注于方法上&#xff0c;标志着此方法是一个事务方法。 Target({ElementType.METHOD,ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) public interface MyTransaction {}定义一个开启事务功能的注解 Component Import(TransActionBean…...

【Vue】:解决动态更新 <video> 标签 src 属性后视频未刷新的问题

问题描述 在 Vue.js 项目&#xff0c;当尝试动态更新 <video> 标签的 <source> 元素 src 属性来切换视频时&#xff0c;遇到了一个问题&#xff1a;即使 src 属性已更改&#xff0c;浏览器仍显示旧视频。具体表现为用户选择新视频后&#xff0c;视频区域继续显示之…...

网络基础1 http1.0 1.1 http/2的演进史

http1.0 1.1 http/2的演进史&#x1f60e; &#xff08;连接复用 队头阻塞 服务器推送 2进制分帧&#xff09; 概述 我们主要关注的是应用层 传输层 http协议发展历史 http的报文结构&#xff1a;起始行 Header Body http的典型特征 http存在的典型问题 Keep Alive机制 chun…...

Python 通过命令行在 unittest.TestCase 中运行单元测试

文章目录 unittest 模块简介编写单元测试在命令行中运行所有测试在命令行中运行单个测试使用装饰器跳过测试总结常用断言方法 unittest 模块简介 unittest是Python标准库中的一个模块&#xff0c;用于编写和运行单元测试。它提供了一个单元测试框架&#xff0c;使得编写测试用…...

源代码编译安装X11及相关库、vim,配置vim(2)

一、编译安装vim 编译时的cofigure选项如下.只有上一步的X11的包安装全了&#xff08;具体哪些是必须的&#xff0c;哪些是多余的没验证&#xff09;&#xff0c;configure才能认为X的库文件和头文件是可以用的。打开多个编程语言的支持特性。 ./configure --prefixpwd/mybui…...

设计模式 行为型 观察者模式(Observer Pattern)与 常见技术框架应用 解析

观察者模式&#xff08;Observer Pattern&#xff09;是一种行为设计模式&#xff0c;它定义了一种一对多的依赖关系&#xff0c;让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时&#xff0c;会通知所有观察者对象&#xff0c;使它们能够自动更新。 一…...

【25考研】川大计算机复试情况,重点是啥?怎么准备?

24年进入复试的同学中&#xff0c;有10位同学的复试成绩为0分。具体是个人原因还是校方原因&#xff0c;还尚不明确。但是C哥提醒&#xff0c;一定要认真复习&#xff01;复试完后不要跟任何人讨论有关复试的题目及细节&#xff01; 一、复试内容 四川大学复试内容较多&#xf…...

C#调用Lua

目录 xLua导入 打包工具导入 单例基类导入与AB包管理器导入 Lua解析器 文件加载与重定向 Lua解析器管理器 全局变量获取 全局函数获取 对于无参数无返回值 对于有参数有返回值 对于多返回值 对于变长参数 完整代码 List与Dictionary映射Table 类映射Table 接口映射…...

LeetCode100之组合总和(39)--Java

1.问题描述 给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target &#xff0c;找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 &#xff0c;并以列表形式返回。你可以按 任意顺序 返回这些组合。 candidates 中的 同一个 数字可以 无限制重复…...

NTN学习笔记之术语和缩写词解析

去发现&#xff0c;去努力&#xff0c;去表达。 参考&#xff1a;3GPP TR 38.811&#xff08;R15&#xff09;&#xff0c;TR 38.821&#xff08;R16&#xff09; 目录 0. NTN典型架构图1. 术语2. 缩写 0. NTN典型架构图 为了方便对术语场景的理解&#xff0c;先放上两张NTN典…...

Yolo11改进:注意力改进|Block改进|ESSAformer,用于高光谱图像超分辨率的高效Transformer|即插即用

摘要 一、论文介绍 高光谱图像超分辨率的重要性:高光谱成像技术通过密集采样光谱特征,为材料区分提供丰富的光谱和空间结构信息,广泛应用于各领域。高光谱图像超分辨率(HSI-SR)旨在从低分辨率HSI生成高分辨率HSI。传统方法的局限性:传统方法依赖手工制作的先验,如低秩近…...

STM32 单片机 练习项目 LED灯闪烁LED流水灯蜂鸣器 未完待续

个人学习笔记 文件路径&#xff1a;程序源码\STM32Project-DAP&DAPmini\1-1 接线图 3-1LED闪烁图片 新建项目 新建项目文件 选择F103C8芯片 关闭弹出窗口 拷贝资料 在项目内新建3个文件夹 Start、Library、User Start文件拷贝 从资料中拷贝文件 文件路径&#xff1a;固…...

使用PyTorch实现基于稀疏编码的生成对抗网络(GAN)在CIFAR-10数据集上的应用

使用PyTorch实现基于稀疏编码的生成对抗网络(GAN)在CIFAR-10数据集上的应用 目录 使用PyTorch实现基于稀疏编码的生成对抗网络(GAN)在CIFAR-10数据集上的应用1. 引言2. 数据集介绍3. 模型网络结构3.1 网络结构3.2 编码器3.3 生成器3.4 判别器4. 模型优化器与损失函数4.1 优…...

用matlab调用realterm一次性发送16进制数

realterm采用PutString接口进行发送&#xff0c;需要注意的是发送的16进制数前面要加入0x标志。只有这样&#xff0c;realterm才能将输入的字符串识别为16进制数的形式。 另外,PutString函数支持两个参数输入&#xff0c;第一个参数为字符串&#xff0c;第二个参数为发送形式&…...

通过可穿戴外骨骼,以更灵活的方式操作你的机器人。

今天&#xff0c;我们将介绍一款专为控制 Mercury X1 和 Mercury B1 机械臂而设计的创新外骨骼。这种外骨骼以人类手臂的结构为蓝本&#xff0c;可实现直观和精确的控制。 开发这种外骨骼的动机源于人们对深度学习和机器学习等领域日益增长的兴趣。这些技术使机器人能够自主学习…...

记录将springboot的jar包和lib分离,使用docker-compose部署

本文讲诉如何把jar里的lib依赖包独立出来&#xff0c;方便更新服务时&#xff0c;缩小jar的体积&#xff0c;下面以若依的system服务为例&#xff0c;配置中的路径请酌情修改&#xff0c;主要提供大致配置逻辑 第一步&#xff1a;修改项目的pom.xml&#xff0c;调整build的配…...

JavaScript 延迟加载的方法

延迟加载&#xff08;Lazy Loading&#xff09;是一种优化网页性能的技术&#xff0c;它允许资源&#xff08;如图片、脚本等&#xff09;在需要时才被加载&#xff0c;而不是在页面初次加载时全部加载。这可以减少初始页面加载时间&#xff0c;提升用户体验&#xff0c;特别是…...

xrdp连接闪退情况之一

错误核查 首先使用命令vim ~/.xsession-errors&#xff0c;当里面的报错信息为WARNING **: Could not make bus activated clients aware of XDG_CURRENT_DESKTOPGNOME environment variable:Failed to execute child process “dbus-launch” (No such file or directory)&am…...

数据分析思维(八):分析方法——RFM分析方法

数据分析并非只是简单的数据分析工具三板斧——Excel、SQL、Python&#xff0c;更重要的是数据分析思维。没有数据分析思维和业务知识&#xff0c;就算拿到一堆数据&#xff0c;也不知道如何下手。 推荐书本《数据分析思维——分析方法和业务知识》&#xff0c;本文内容就是提取…...

AI架构师修炼之道

1 AI时代的架构革命 与传统软件开发和软件架构师相比&#xff0c;AI架构师面临着三重范式转换&#xff1a; 1.1 技术维度&#xff0c;需处理异构算力调度与模型生命周期管理的复杂性&#xff1b; 1.2 系统维度&#xff0c;需平衡实时性与资源约束的矛盾&#xff1b; 1.3 价…...

嵌入式链表操作原理详解

嵌入式链表操作原理详解 链表是嵌入式软件开发中最基础的数据结构之一&#xff0c;其设计采用嵌入式链表节点的思想&#xff0c;实现了高度通用的链表管理机制。以下是核心原理和操作的全面解析&#xff1a; 一、基础数据结构 struct list_head {struct list_head *next, *pr…...

CAN通信收发测试(USB2CAN模块测试实验)

1.搭建测试环境 电脑&#xff1a;安装 USB 驱动&#xff0c;安装原厂调试工具&#xff0c;安装cangaroo&#xff08;参考安装包的入门教程即可&#xff09; USB驱动路径&#xff1a;~\CAN分析仪资料20230701_Linux\硬件驱动程序 原厂调试工具路径&#xff1a;~\CAN分析仪资料2…...

DLL动态库实现文件遍历功能(Windows编程)

源文件&#xff1a; 文件遍历功能的动态库&#xff0c;并支持用户注册回调函数处理遍历到的文件 a8f80ba 周不才/cpp_linux study - Gitee.com 知识准备 1.Windows中的数据类型 2.DLL导出/导入宏 使用__declspec(dllexport)修饰函数&#xff0c;将函数标记为导出函数存放到…...

Hadoop 3.x 伪分布式 8088端口无法访问问题处理

【Hadoop】YARN ResourceManager 启动后 8088 端口无法访问问题排查与解决(伪分布式启动Hadoop) 在配置和启动 Hadoop YARN 模块时&#xff0c;发现虽然 ResourceManager 正常启动&#xff0c;JPS 进程中也显示无误&#xff0c;但通过浏览器访问 http://主机IP:8088 时却无法打…...

北京大学肖臻老师《区块链技术与应用》公开课:12-BTC-比特币的匿名性

文章目录 1.比特币的匿名性不是真的匿名&#xff0c;相当于化名&#xff0c;现金是真的匿名&#xff0c; 2.如果银行用化名的话和比特币的匿名哪个匿名性更好&#xff1f; 银行匿名性比比特币好&#xff0c;因为比特币的区块链的账本是完全公开的&#xff0c;所有人都可以查&am…...

学习STC51单片机30(芯片为STC89C52RCRC)

每日一言 当你感到疲惫时&#xff0c;正是成长的关键时刻&#xff0c;再坚持一下。 IIC协议 是的&#xff0c;IIC协议就是与我们之前的串口通信协议是同一个性质&#xff0c;就是为了满足模块的通信&#xff0c;其实之前的串口通信协议叫做UART协议&#xff0c;我们千万不要弄…...

服务器新建用户无法使用conda

服务器新建用户无法使用conda 1.将.bashrc文件复制到新用户家目录下 sudo cp .bashrc /home/newuser/.bashrc2.source命令激活该文件 source ~/.bashrc3.将.condarc文件复制到新用户家目录下 sudo cp .condarc/home/newuser/.condarc...

Linux 下关于 ioremap 系列接口

1、序 在系统运行时&#xff0c;外设 IO 资源的物理地址是已知的&#xff0c;由硬件的设计决定&#xff08;参考SOC的datesheet&#xff0c;一般会有memorymap&#xff09;。驱动程序不能通过物理地址访问IO资源&#xff0c;必须将其映射到内核态的虚拟地址空间。常见的接口就是…...

video-audio-extractor:视频转换为音频

软件介绍 前几天在网上看见有人分享了一个源码&#xff0c;大概就是py调用的ffmpeg来制作的。 这一次我带来源码版&#xff08;需要py环境才可以运行&#xff09;&#xff0c;开箱即用版本&#xff08;直接即可运行&#xff09; 软件特点 软件功能 视频提取音频&#xff1a…...