【嵌入式】总结——Linux驱动开发(三)

鸽了半年,几乎全忘了,幸亏前面还有两篇总结。出于快速体验嵌入式linux的目的,本篇与前两篇一样,重点在于使用、快速体验,uboot、linux、根文件系统不作深入理解,能用就行。

重新梳理一下脉络,本章学习的是linux驱动开发,主要内容涉及到怎么编写linux驱动怎么编译、怎么加载卸载。在此之前,还需要准备两件事,其一制作SD卡启动,因为由于时间有些久,实在是忘了。其二为以太网连接,先前由于以太网被占用,所以无论是移植还是驱动开发,使用的都是SD卡,没有用nfs挂载,但以太网终究要学习且更加方便。

那么如今目的很简单,先简单制作一张能用的SD卡启动,对前面内容的巩固与复习。然后再测试以太网通信,以便后面从虚拟机编译的模块.ko文件可以传输到开发板上的根文件系统里。接着开始正式编写模块驱动,使用到的是linux内核的头文件,再熟悉一些开发驱动用到的宏、函数、命名规则以及开发规则即可。最后是加载并测试驱动。

驱动(模块)开发是我们的主要目的,驱动开发有两种方式,一种是动态加载驱动模块 ,一种是静态编译驱动到内核。前者开发的是.ko文件,可以随时使用insmod加载到正在运行的linux上。后者如其名,在linux工程里创建驱动的.c文件,然后编译成镜像。由于linux编译时间过长,前者可以快速开发并测试驱动,节约大量编译时间,测试完毕后就可以通过后者添加到linux驱动里了

本应如此,遇到问题解决问题,但实际上遇到一些大坑,而不得不学习更多的内容,虽麻烦些,但印象却也深刻了许多。注意!本文的叙述顺序并非依据标准的知识点总结框架,而是按照笔者个人的学习历程展开。

一、制作SD卡启动

1,删除所有分区

①使用ls命令确认SD卡设备

先插上SD卡,并选择连接到虚拟机上

接着使用ls命令来列出所有sd设备,然后再拔掉SD卡设备,列出所有sd设备

bash 复制代码
ls /dev/sd*

缺少哪一个,哪个就是SD卡,此处缺失/dev/sdb和/dev/sdb1,那么/dev/sdb就是SD卡设备

②使用fdisk命令删除SD卡所有分区

使用fdisk命令,进入fdisk工具界面

bash 复制代码
sudo fdisk /dev/sdb

使用命令p打印所有分区

输入d命令删除分区,由于只有一个分区,默认直接删除。

2,制作分区

如同前一篇博客所言,SD卡的起始位置需要先空10MB,供裸机程序uboot存放,还需再制作两个分区,一个存放linux镜像,另一个存放根文件系统。

①制作linux分区

在刚才的fdisk工具界面内,输入命令n来创建分区,接着按回车选择默认分区格式p,再输入20480(20480*512Byte=10MB)设置起始扇区,最后输入+500M确定创建分区的大小

②制作根文件系统分区

输入命令p打印分区,可以看到分区1所占扇区的位置为20480~1044479,所以第二个分区的起始扇区可以设为1044480,紧挨着第一个分区

除了起始扇区的位置外,其余按回车选择默认

从最后打印的分区可以看到,被薅走的1.5GB存储

③保存退出

输入命令w即可。不过这里出了一点点小意外

不过却并不影响分区创建

④为分区设定文件系统格式

先使用ls命令列出所有sd设备

bash 复制代码
ls /dev/sd*

刚列出来时,并没有分区2(/dev/sdb2),重新插拔SD卡设备后再重新列出,就有了

使用下面命令,分别为两个分区设置格式FAT和EXT4

bash 复制代码
sudo mkfs.vfat /dev/sdb1
bash 复制代码
sudo mkfs.ext4 /dev/sdb2

第二个分区需要等待一段时间,制作好后,左侧就会出现两个USB一样的图标

3,烧录uboot

①编译uboot

②烧录uboot

使用烧录工具,下图使用的烧录工具是基于正点原子提供的工具的改版imxdownload烧写工具

出现下面错误是因为这个工具在编写时,使用的是C++来创建文件而非本地的linux命令,故而需要在前面添加sudo命令

bash 复制代码
sudo ./imx_download -b u-boot.bin -s /dev/sdb

4,拷贝linux镜像和设备树

①编译linux

在Makefile已经指定架构和编译器的情况下,运行脚本

bash 复制代码
#!/bin/bash

# 通过chmod +x build.sh赋予权限

# 函数定义,用于执行不同的make命令
make_distclean() {
  echo "执行 make distclean"
  make distclean
}

make_imx_v7_defconfig() {
  echo "执行 make imx_v7_defconfig -j16"
  make imx_v7_defconfig -j16
}

make_all() {
  echo "执行 make -j16"
  make -j16
}

make_menuconfig() {
  echo "执行 make menuconfig"
  make menuconfig
}

# 当没有参数时,执行所有命令
if [ $# -eq 0 ]; then
  echo "没有参数,执行所有命令"
  make_distclean
  make_imx_v7_defconfig
#  make_menuconfig
  make_all

else
  # 主逻辑,根据输入参数调用相应的函数
  case "$1" in
    c)
      make_distclean
      ;;
    d)
      make_imx_v7_defconfig
      ;;
    a)
      make_all
      ;;
    m)
      make_menuconfig
      ;;
    *)
      echo "无效的参数: $1"
      echo "用法: $0 [{c|d|a|m}]"
      exit 1
      ;;
  esac
fi

根据提示找到镜像指定路径

②拷贝

为了把镜像和设备树拷贝到SD卡中,先创建一个目录/mnt,把SD卡挂载到上面,然后再把镜像和设备树复制到/mnt目录

bash 复制代码
sudo mount /dev/sdb1 /mnt

挂载后,左边的USB图标就会少掉一个

bash 复制代码
sudo cp arch/arm/boot/zImage /mnt

设备树就在arch/arm/boot/dts目录,进入后寻找到匹配的dtb文件,然后复制到/mnt目录中

bash 复制代码
sudo cp ./imx6ull-14x14-emmc-7-1024x600-c.dtb /mnt

使用sync后,然后再取消挂载

bash 复制代码
sudo umount /dev/sdb1

5,拷贝根文件系统

①传输根文件系统

使用FileZilla传输文件,调了好半天:NAT是给虚拟机上网用的,桥接是给以太网用的。虚拟机能ping主机不行,控制面板启用VMnet8

开发资料A盘里有些根文件系统不能正常使用,不过笔者没有一一尝试,下面这个根文件系统是正常的

②拷贝

把传输的压缩包复制到已经挂载SD卡第二个分区的/mnt里,然后解压

bash 复制代码
sudo tar -xvjf rootfs.tar.bz2

解压后删除压缩包,然后使用sync同步,最后取消挂载

6、启动开发板

串口连接至电脑,插上SD卡后,拨码选择SD启动。然后进入uboot,设置启动命令

bash 复制代码
setenv bootcmd 'load mmc 0:1 0x83000000 zimage; load mmc 0:1 0x83800000 imx6ull-14x14-emmc-7-1024x600-c.dtb; bootz 0x83000000 - 0x83800000'

保存启动命令后,重新复位

bash 复制代码
saveenv

本来想自行动态计算地址,结果发现uboot的&运算有问题,格式正确也会报语法错误。后来用/0x1000和*0x1000来代替,但会一直卡在启动内核步骤。最后还是用回了以前的命令,这个先搁置 。

最终效果如下,除了壁纸中部细看略微有些条纹外一切正常(可能这是特点?):

二、网络连接

1,设置ip和子网掩码

①测试uboot

开发板上电后,按下任意键进入uboot里,通过下面命令设置开发板的ip、子网掩码和MAC(MAC地址不能重复)

bash 复制代码
setenv ipaddr 192.168.1.254
bash 复制代码
setenv netmask 255.255.255.0
bash 复制代码
setenv ethaddr 00:11:22:33:44:55

然后设置主机的ip

bash 复制代码
setenv serverip 192.168.1.255

最后保存

bash 复制代码
saveenv

这里为了避免ip抢占,就把开发板和主机的ip设置得比较远,当然也可以不用192.168.1这个网段。

非常奇怪的是,无论去ping虚拟机还是ping开发板自身,都会出现下面数据错误,使用的是同一个u-boot,以前并未发生过。

不过还是找到了相关博客uboot下出现data abort错误导致重启解决办法

在uboot工程里的arch/arm/cpu/armv7/start.S 中,第130行左右,按照博客里的去修改。不得不说,大佬就是大佬,错误直接解决了

②设置linux

修改下面文件,设置eth0为静态IP,IP地址随意(需要注意,本篇后面其实使用的其实都是192.168.1.127,但图是192.168.1.254)

bash 复制代码
sudo vi /etc/network/interfaces

进入后把iface etho inet dhcp改为下面,都是vim的基本操作

bash 复制代码
auto eth0
iface eth0 inet static
    address 192.168.1.127     # 开发板的静态IP
    netmask 255.255.255.0     # 子网掩码
    gateway 192.168.1.1       # 网关
    dns-nameservers 8.8.8.8   # DNS服务器

(图中乱码可能是显示的问题)

修改完后,使用下面命令来重启网络

bash 复制代码
sudo /etc/init.d/networking restart

此时ip地址已被正确设置

ping虚拟机,可以看到一切正常(此时虚拟机的ip设为192.168.1.128,因为192.168.1.255是广播地址,还需要加上-b参数)

按Ctrl+C可以暂停操作。

可以看到虚拟机也能ping通开发板

2,建立连接

这里能使用的方法有很多

这里使用的是NFS,主机和开发板需要各自配置后,才能进行正常通信

①主机

先安装nfs服务

bash 复制代码
sudo apt-get install nfs-kernel-server

创建一个共享目录并赋予权限,比如在用户目录里创建,user自行替换

bash 复制代码
mkdir /home/user/nfs_share
bash 复制代码
chmod 777 /home/user/nfs_share

编辑NFS配置文件 /etc/exports,添加共享目录和权限

bash 复制代码
sudo vim /etc/exports
bash 复制代码
/home/user/nfs_share 192.168.1.0/24(rw,sync,no_subtree_check,no_root_squash)

重启NFS服务

bash 复制代码
sudo systemctl restart nfs-kernel-server

检查NFS共享是否生效

bash 复制代码
sudo exportfs -v

②主机给开发板联网

开发板需要下载nfs客户端,需要联网。联网可以使用IP转发,但这个有些麻烦(以后再说),可以直接使用Windows的网络共享功能,参考博客开发板和笔记本网线连接

到控制面板里,找到网络和Internet,再点击网络和共享中心进入下面步骤,点击更改适配器设置

右键WLAN(笔记本的一个网口已经通过以太网线与开发板连接,所以用的是WiFi),按如下设置

共享之后,使用ifconifg查看ip

设置完后,在开发板的linux里,ping百度网址

bash 复制代码
ping www.baidu.com

联网是没有问题,但是这个ping出来的结果很慢,需要耐心等待

③开发板

既然可以联网,那么接下来需要开发板下载nfs客户端。不过这个根文件系统的apt没有资源列表,需要手动创建

bash 复制代码
touch /etc/apt/sources.list

然后是添加网址,这里使用的是阿里镜像源,可自行替换需要的源

bash 复制代码
vi /etc/apt/sources.list
bash 复制代码
deb http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiverse

deb http://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiverse

deb http://mirrors.aliyun.com/ubuntu/ bionic-backports main restricted universe multiverse

deb-src http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiverse

deb-src http://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiverse

deb-src http://mirrors.aliyun.com/ubuntu/ bionic-backports main restricted universe multiverse

现在使用apt-get update就正常多了,不再会任何列表都没有了

到这一步都还挺顺利的,但无法使用apt下载。询问技术客服,他们说正点原子的根文件系统使用的是Yocto构建的,对apt的支持并不完善。前面为了快速体验开发, 只学习了怎么移植,并没有学习怎么制作根文件系统。

询问了DeepSeek,虽然apt功能强大,但对于嵌入式来说OPKG和RPM更适合(Yocto支持)。既如此,制作根文件系统先放一放,学Qt制作桌面时应该会用到。

使用下面命令检查根文件系统是否支持NFS,从结果来看,是支持的,应该是正点原子在已经提前移植过这些库了。那么就不需要安装nfs客户端了,包管理器下载先放一放。

bash 复制代码
which mount.nfs

需要注意的是,前面为了联网启用了网络共享功能,现在要尝试nfs挂载,那么就要关闭共享。同时使用ifconfig来查看ip是否正确,如果不正确,那么重启一下网络,再检查ip地址,确保开发板和虚拟机能互ping。开发板每次重启还需要输入下面命令来重启网络

bash 复制代码
sudo /etc/init.d/networking restart

前面在主机上使用的ip是192.168.1.128,路径是/home/user/nfs_share,下面就可以据此来建立nfs通信了,现在开发板上创建一个用于挂载的目录,比如/mnt/nfs

bash 复制代码
mkdir /mnt/nfs

本来应该使用下面这个命令建立连接的(用户名和ip自行替换),但需要指定版本

bash 复制代码
mount -t nfs 192.168.1.128:/home/fairy/nfs_share /mnt/nfs

在此之前,我们可以先试用下面命令来查看挂载点

bash 复制代码
df -h

再使用添加了版本的挂载命令(如果版本3不行,试试4),该命令没有任何提示

bash 复制代码
mount -t nfs -o nfsvers=3 192.168.1.128:/home/fairy/nfs_share /mnt/nfs

再使用df -h,可以看到已经成功挂载了

3,测试nfs

刚才在联网的情况下顺便又测试了下包管理器,没想到opkg、rpm、dpkg一个能用的都没有,技术客服说需要手动管理。

使用ls查看虚拟机和开发板的挂载点,可以看到任何内容都没有

在虚拟机的挂载目录里,随便创建一个文件

可以看到开发板的挂载点里确实多了一个文件,挂载成功!


启用ip转发(废稿)

编辑开发板的网络配置文件 /etc/network/interfaces

bash 复制代码
sudo vi /etc/network/interfaces

把网关改为虚拟机ip 192.168.1.128(这里把开发板的ip改为了192.168.1.127)

bash 复制代码
auto eth0
iface eth0 inet static
    address 192.168.1.127
    netmask 255.255.255.0
    gateway 192.168.1.128
    dns-nameservers 8.8.8.8

重启网络驱动(开发板每次重启后还得手动重启网络)

bash 复制代码
sudo /etc/init.d/networking restart

编辑 /etc/resolv.conf 文件,修改DNS配置

bash 复制代码
sudo vi /etc/resolv.conf

修改为下面内容(开发板每次重启都会覆盖掉下面内容)

bash 复制代码
nameserver 8.8.8.8
nameserver 114.114.114.114

三、驱动编写_基础

回顾一下,前面折腾了那么久,无论是制作SD卡启动,还是使用nfs挂载,本质上都是为驱动编写提供便利条件,本篇最终目标"驱动编写"并没有变。

事实上学到现在这个程度,对linux的使用和搭建都有了一些基本的了解和熟悉,看视频不再是首选,文档是更推荐的选择(正点原子的文档质量很高)。可以从"跟随式"学习转为"主动学习",知道要实现什么样的应用(或解决什么样的问题),为此需要学习哪些内容,学习过程遇到问题怎么解决怎么取舍。发问,那么问题就已经解决了一半,遇到问题解决问题,那么学习路径就确立了。

1,动态加载_基础方式

文档是先做字符设备开发,再做LED驱动开发,循序渐进。不过直接做LED驱动也行,可以更快地看到实验结果,两者区别并不大,没有太大的难度壁垒。

这个过程可以分为两个步骤,其一,编写驱动、生成.ko文件、加载卸载驱动;其二为测试,编写一个应用程序,生成elf文件,通过运行程序来观察结果。注意多翻阅文档手册!


①从源码入手

找到开发盘里的led驱动,通过filezila传输到虚拟机中

在虚拟机中,用自己的IDE打开刚才传输的工程

需要修改一下Makefile里的路径KERNELDIR ,换成自己linux内核的目录。如果是CLion的话,根据错误提示,把构建目标all换成build,或者在Makefile里把build改为all

直接构建没有任何问题

②分析源码框架

下面是正点原子的led源码

cpp 复制代码
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名		: led.c
作者	  	: 左忠凯
版本	   	: V1.0
描述	   	: LED驱动文件。
其他	   	: 无
论坛 	   	: www.openedv.com
日志	   	: 初版V1.0 2019/1/30 左忠凯创建
***************************************************************/
#define LED_MAJOR		200		/* 主设备号 */
#define LED_NAME		"led" 	/* 设备名字 */

#define LEDOFF 	0				/* 关灯 */
#define LEDON 	1				/* 开灯 */

/* 寄存器物理地址 */
#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;

/*
 * @description		: LED打开/关闭
 * @param - sta 	: LEDON(0) 打开LED,LEDOFF(1) 关闭LED
 * @return 			: 无
 */
void led_switch(u8 sta)
{
    u32 val = 0;
    if (sta == LEDON)
    {
        val = readl(GPIO1_DR);
        val &= ~(1 << 3);
        writel(val, GPIO1_DR);
    } else if (sta == LEDOFF)
    {
        val = readl(GPIO1_DR);
        val |= (1 << 3);
        writel(val, GPIO1_DR);
    }
}

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
    return 0;
}

/*
 * @description		: 从设备读取数据
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    return 0;
}

/*
 * @description		: 向设备写数据
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    int retvalue;
    unsigned char databuf[1];
    unsigned char ledstat;

    retvalue = copy_from_user(databuf, buf, cnt);
    if (retvalue < 0)
    {
        printk("kernel write failed!\r\n");
        return -EFAULT;
    }

    ledstat = databuf[0]; /* 获取状态值 */

    if (ledstat == LEDON)
    {
        led_switch(LEDON); /* 打开LED灯 */
    } else if (ledstat == LEDOFF)
    {
        led_switch(LEDOFF); /* 关闭LED灯 */
    }
    return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int led_release(struct inode *inode, struct file *filp)
{
    return 0;
}

/* 设备操作函数 */
static struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .read = led_read,
    .write = led_write,
    .release = led_release,
};

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init led_init(void)
{
    int retvalue = 0;
    u32 val = 0;

    /* 初始化LED */
    /* 1、寄存器地址映射 */
    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);

    /* 2、使能GPIO1时钟 */
    val = readl(IMX6U_CCM_CCGR1);
    val &= ~(3 << 26); /* 清楚以前的设置 */
    val |= (3 << 26); /* 设置新值 */
    writel(val, IMX6U_CCM_CCGR1);

    /* 3、设置GPIO1_IO03的复用功能,将其复用为
     *    GPIO1_IO03,最后设置IO属性。
     */
    writel(5, SW_MUX_GPIO1_IO03);

    /*寄存器SW_PAD_GPIO1_IO03设置IO属性
     *bit 16:0 HYS关闭
     *bit [15:14]: 00 默认下拉
     *bit [13]: 0 kepper功能
     *bit [12]: 1 pull/keeper使能
     *bit [11]: 0 关闭开路输出
     *bit [7:6]: 10 速度100Mhz
     *bit [5:3]: 110 R0/6驱动能力
     *bit [0]: 0 低转换率
     */
    writel(0x10B0, SW_PAD_GPIO1_IO03);

    /* 4、设置GPIO1_IO03为输出功能 */
    val = readl(GPIO1_GDIR);
    val &= ~(1 << 3); /* 清除以前的设置 */
    val |= (1 << 3); /* 设置为输出 */
    writel(val, GPIO1_GDIR);

    /* 5、默认关闭LED */
    val = readl(GPIO1_DR);
    val |= (1 << 3);
    writel(val, GPIO1_DR);

    /* 6、注册字符设备驱动 */
    retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
    if (retvalue < 0)
    {
        printk("register chrdev failed!\r\n");
        return -EIO;
    }
    return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
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);
}

module_init(led_init);

module_exit(led_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");

结合文档和源码,我们可以看到,一个驱动模块开发应该包含一下内容:

  • 包含内核头文件

  • 定义设备操作函数结构体

  • 实现设备操作函数

  • 定义模块初始化和退出函数

  • 定义模块信息

此外还需要遵循一些特定的规范,比如:

  • 模块信息通常放在文件的末尾,紧挨着模块的初始化和退出函数。

  • 必须定义 MODULE_LICENSE,其他模块信息(如作者、描述、版本号)是可选的,但建议尽量提供。

  • ......

一个简单的模块示例如下:

cpp 复制代码
#include <linux/module.h>
#include <linux/init.h>


static int my_open(void)
{
    /*......*/
}

/*......*/

static struct file_operations my_fops = {
    .owner = THIS_MODULE,  // 指向当前模块
    .open = my_open,       // 打开设备
    .read = my_read,       // 读取设备
    .write = my_write,     // 写入设备
    .release = my_release, // 关闭设备
};



static int __init my_init(void) {
    printk(KERN_INFO "Module loaded\n");
    return 0;
}

static void __exit my_exit(void) {
    printk(KERN_INFO "Module unloaded\n");
}

module_init(my_init);
module_exit(my_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple example module");
MODULE_VERSION("1.0");

③分析源码细节(人机对战)

  • 为什么要用到ioremapiounmap?

Linux 内核运行在虚拟地址空间(MMU),无法直接访问物理地址。ioremap 的作用就是将硬件的物理地址映射到内核的虚拟地址空间,使得内核可以通过虚拟地址访问硬件寄存器。ioremap 返回的是虚拟地址指针。

ioremap 的作用是释放映射的虚拟地址空间,当模块卸载时,需要释放之前映射的虚拟地址空间,以避免内存泄漏。ioremapiounmap 确保内核能够安全地访问硬件资源,并在模块卸载时释放资源。

所谓映射可以理解为MMU分配页表什么的,所以才需要释放虚拟地址,不然虚拟地址空间就会被消耗殆尽。

  • 注册字符驱动设备这个步骤是干什么的?

字符设备注册是 将驱动程序与设备号关联,并在内核中注册设备。注册字符设备后,用户空间程序可以通过设备文件(如 /dev/led)访问设备。

  • 设备号是干什么的?为什么要分为主次两种?这样做是为了解决什么问题?

设备号用于在内核中唯一标识一个设备,内核通过设备号管理设备,确保多个设备可以共存,用户空间程序通过设备文件(如 /dev/led)访问设备时,内核根据设备号找到对应的驱动程序。

主设备号可以标识设备类型(如 LED、键盘、串口等),主设备号是全局唯一的,由内核或开发者分配,一个主设备号对应一个驱动程序。

次设备号用于标识具体设备,用于区分同一类型的多个设备。比如主设备号为 200 的 LED 设备可能有多个次设备号:次设备号 0 表示第一个 LED、次设备号 1 表示第二个 LED。

通过主次设备号的设计,Linux 内核能够高效地管理大量设备,并为用户空间程序提供统一的访问接口。开发者只需实现一个驱动程序,即可支持多个设备实例,同一类型的多个设备可以共享一个驱动程序,减少了内核资源的浪费。

  • 也就是在驱动模块开发时分配设备号,那么加载驱动模块时,就可以产生设备文件(如/dev/led)?那么分配设备号还有哪些方式?

在 Linux 驱动模块开发中,设备号的分配和设备文件的创建是两个关键步骤。加载驱动模块时,设备文件(如 /dev/led)并不会自动生成,而是需要手动创建或通过其他机制自动创建。

设备号的分配分为静态和动态两种,动态分配是通过使用 alloc_chrdev_region 函数由内核动态分配设备号。一般可以使用混合分配的方式,**优先动态分配,失败时回退到静态分配,**这样就可以兼具动态分配的灵活性和静态分配的稳定性。

  • 为什么还要手动或自动创建设备文件?为什么不在加载驱动模块时创建呢?既然创建设备文件需要主次设备号,那我明明在驱动模块开发过程中分配了,为何还要在创建设备文件时再分配一次呢?

驱动模块加载时,内核会初始化驱动程序并分配设备号,但并不会自动创建设备文件。设备文件的创建通常由用户空间工具(如 mknod)或内核机制(如 udevdevtmpfs)完成。

设备文件的创建可能涉及用户空间的配置(如权限、所有者等),这些配置不适合在内核中处理。设备文件的创建时机可能需要根据系统状态动态调整。除此之外还有一部分历史原因,早期的 Linux 内核没有自动创建设备文件的机制,开发者需要手动使用 mknod 创建设备文件。现代 Linux 内核引入了 udevdevtmpfs,可以自动创建设备文件,但仍然保留了手动创建的选项。

两者分离便于职责分离,有更大的灵活性。设备号的分配是内核的职责,用于管理设备和驱动程序。设备文件的创建是用户空间的职责,用于提供用户访问接口。两者分离后,设备文件的创建可以根据系统配置动态调整(如权限、所有者等),可以延迟到设备实际使用时(如热插拔设备)。

现代 Linux 内核提供了自动创建设备文件的机制,开发者无需手动使用 mknod,使用 udevdevtmpfs 可以自动创建设备文件。udev 是 Linux 的用户空间设备管理器,负责管理 /dev 目录下的设备文件,当内核检测到新设备时,udev 会根据规则自动创建设备文件。驱动程序需要在初始化时调用 class_createdevice_create 函数,向 udev 提供设备信息。

  • open和write函数有什么区别?
  • 在开发stm32的驱动时,可以调用各种库函数来辅助开发,在嵌入式Linux的驱动开发中,只能从寄存器级别开发、不能使用NXP官方提供的库函数吗?

在嵌入式 Linux 驱动开发中,与 STM32 的开发方式有所不同。STM32 的开发通常依赖于厂商提供的库函数(如 HAL 库或标准外设库),而在嵌入式 Linux 驱动开发中,通常不会直接使用厂商提供的库函数(如 NXP 提供的 SDK 库),而是通过以下方式操作硬件:

寄存器级别开发

使用内核提供的 API

使用设备树(Device Tree)

**使用现成的驱动框架,**Linux 内核提供了许多现成的驱动框架(如 I2C、SPI、USB 等),开发者可以基于这些框架实现驱动,而无需从零开始。

厂商提供的库函数通常是为裸机或 RTOS 环境设计的,而 Linux 内核运行在内核空间,对内存管理、中断处理等有严格的要求。如果直接使用厂商库函数可能导致内核崩溃或资源冲突。Linux 内核提供了丰富的 API 来操作硬件,这些 API 是专门为内核空间设计的,能够更好地与内核的其他部分协同工作。直接操作寄存器或使用内核 API 可以提高驱动的可移植性,使其更容易适配不同的内核版本和硬件平台。

......

......

④编译驱动模块和测试程序

稍微修改一下write代码,编译时发现了一个警告,万万没想到会出现C90标准

稍微查了一下,这是历史原因

但我有点不太相信,现代Linux都有使用Rust编写的部分了,不可能这般守旧才对

为了兼容性,沉重的历史包袱是难免的。在Makefile里添加这一句

bash 复制代码
# 添加 C11 标准支持
ccflags-y := -std=gnu11 -Wno-declaration-after-statement

为了测试驱动模块,还需要编写应用程序,此处即ledApp,为此,编译还需要添加一个目标ledApp

bash 复制代码
KERNELDIR := /home/fairy/Embedded/program/Alientek_Uboot_Linux/linux
CURRENT_PATH := $(shell pwd)

obj-m := led.o

# 添加 C11 标准支持
ccflags-y := -std=gnu11 -Wno-declaration-after-statement

all: kernel_modules ledApp

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules

# 编译应用程序
ledApp: ledApp.c
	arm-linux-gnueabihf-gcc -o $@ $<

clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

测试程序的代码很清晰,先打开文件,再写入数据,最后关闭。main函数里的两个参数,前者为参数个数,后者为参数指针数组。编译的程序为ledApp.elf(在linux里elf后缀一般不写),运行程序时传入参数是指在命令行中输入程序名称,并且在名称后输入一些内容(参数),比如下面,传入的第二个参数是1,第一个参数默认都是程序自身的名称

bash 复制代码
./ledApp 1

放在程序里下面这一步就是把传入的参数"1"存放到数组databuf里

⑤测试驱动模块

把驱动模块和测试应用程序都传输到虚拟机的nfs挂载点

在开发板的挂载点里可以看到文件已经成功传入

加载驱动模块

bash 复制代码
insmod /mnt/nfs/led.ko

列出设备

bash 复制代码
lsmod

查看设备

bash 复制代码
cat /proc/devices

查看设备节点

bash 复制代码
ls -l /dev/

可以看到是没有led设备节点的,因为没有创建设备节点,驱动模块使用的还是例程源码,并没有添加自动创建设备节点udev需要的相关函数。

创建字符设备节点

bash 复制代码
mknod /dev/led c 200 0

可以看到led设备节点已经创建成功了测试应用程序

测试之前需要关闭led自动闪烁功能

bash 复制代码
echo none > /sys/class/leds/sys-led/trigger

此时输入命令,才发现架构不对,虚拟机使用的是x86_x64,而开发板是arm32,应使用交叉编译工具链,也就是说前面的Makefile编译ledApp时,需要把gcc改为arm-linux-gnueabihf-gcc(已改)

此时使用下面0和1两个参数测试,实验结果与预期相符

bash 复制代码
./ledApp /dev/led 1
bash 复制代码
./ledApp /dev/led 0

使用rmmod卸载模块时,设备节点/dev/led并不会消失,还需要使用rm来手动删除

bash 复制代码
rmmod led.ko
bash 复制代码
rm /dev/led

⑥尝试新方法

前面和DeepSeek对话中,可以获知混合分配设备号更推荐,udev自动创建设备文件更现代。继续提问,还有更多更现代的做法,比如驱动和硬件分离,不过这要用到设备树,一些做法可以先放一放。

下面的代码只要用到了1、5和6,其他需要设备树配合

cpp 复制代码
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/io.h>
#include <linux/slab.h>
#include <linux/uaccess.h>


#define LED_MAJOR        200        /* 主设备号 */
#define LED_NAME        "led"      /* 设备名字 */

#define LEDOFF         0           /* 关灯 */
#define LEDON          1           /* 开灯 */

/* 寄存器物理地址 */
#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 dev_t devno;
static struct cdev led_cdev;
static struct class *led_class;
static struct device *led_device;

/*
 * @description     : LED打开/关闭
 * @param - sta     : LEDON(0) 打开LED,LEDOFF(1) 关闭LED
 * @return          : 无
 */
static void led_switch(u8 sta)
{
    u32 val;
    if (sta == LEDON)
    {
        val = readl(GPIO1_DR);
        val &= ~(1 << 3);
        writel(val, GPIO1_DR);
    } else if (sta == LEDOFF)
    {
        val = readl(GPIO1_DR);
        val |= (1 << 3);
        writel(val, GPIO1_DR);
    }
}

/*
 * @description     : 打开设备
 * @param - inode   : 传递给驱动的inode
 * @param - filp    : 设备文件,file结构体有个叫做private_data的成员变量
 *                    一般在open的时候将private_data指向设备结构体。
 * @return          : 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
    return 0;
}

/*
 * @description     : 从设备读取数据
 * @param - filp    : 要打开的设备文件(文件描述符)
 * @param - buf     : 返回给用户空间的数据缓冲区
 * @param - cnt     : 要读取的数据长度
 * @param - offt    : 相对于文件首地址的偏移
 * @return          : 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    return 0;
}

/*
 * @description     : 向设备写数据
 * @param - filp    : 设备文件,表示打开的文件描述符
 * @param - buf     : 要写给设备写入的数据
 * @param - cnt     : 要写入的数据长度
 * @param - offt    : 相对于文件首地址的偏移
 * @return          : 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    unsigned char databuf[1];

    int retvalue = copy_from_user(databuf, buf, cnt);
    if (retvalue < 0)
    {
        pr_err("kernel write failed!\r\n");
        return -EFAULT;
    }

    led_switch(databuf[0]);

    return 0;
}

/*
 * @description     : 关闭/释放设备
 * @param - filp    : 要关闭的设备文件(文件描述符)
 * @return          : 0 成功;其他 失败
 */
static int led_release(struct inode *inode, struct file *filp)
{
    return 0;
}

/* 设备操作函数 */
static struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .read = led_read,
    .write = led_write,
    .release = led_release,
};

/*
 * @description     : 驱动入口函数
 * @param           : 无
 * @return          : 无
 */
static int __init led_init(void)
{
    u32 val;
    int retvalue;

    /* 1、寄存器地址映射 */
    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);

    if (!IMX6U_CCM_CCGR1 || !SW_MUX_GPIO1_IO03 || !SW_PAD_GPIO1_IO03 || !GPIO1_DR || !GPIO1_GDIR)
    {
        pr_err("ioremap failed!\r\n");
        retvalue = -ENOMEM;
        goto err_ioremap;
    }

    /* 2、使能GPIO1时钟 */
    val = readl(IMX6U_CCM_CCGR1);
    val &= ~(3 << 26); /* 清除以前的设置 */
    val |= (3 << 26); /* 设置新值 */
    writel(val, IMX6U_CCM_CCGR1);

    /* 3、设置GPIO1_IO03的复用功能 */
    writel(5, SW_MUX_GPIO1_IO03);

    /* 4、设置GPIO1_IO03的IO属性 */
    writel(0x10B0, SW_PAD_GPIO1_IO03);

    /* 5、设置GPIO1_IO03为输出功能 */
    val = readl(GPIO1_GDIR);
    val &= ~(1 << 3); /* 清除以前的设置 */
    val |= (1 << 3); /* 设置为输出 */
    writel(val, GPIO1_GDIR);

    /* 6、默认关闭LED */
    val = readl(GPIO1_DR);
    val |= (1 << 3);
    writel(val, GPIO1_DR);

    /* 7、设备号混合分配 */
    retvalue = alloc_chrdev_region(&devno, 0, 1, LED_NAME);
    if (retvalue < 0)
    {
        pr_err("dynamic alloc chrdev failed, try static alloc!\r\n");
        devno = MKDEV(LED_MAJOR, 0);
        retvalue = register_chrdev_region(devno, 1, LED_NAME);
        if (retvalue < 0)
        {
            pr_err("static alloc chrdev failed!\r\n");
            goto err_alloc_chrdev;
        }
    }

    /* 8、初始化 cdev */
    cdev_init(&led_cdev, &led_fops);
    led_cdev.owner = THIS_MODULE;

    /* 9、添加 cdev 到内核 */
    retvalue = cdev_add(&led_cdev, devno, 1);
    if (retvalue < 0)
    {
        pr_err("cdev_add failed!\r\n");
        goto err_cdev_add;
    }

    /* 10、创建设备类 */
    led_class = class_create(THIS_MODULE, LED_NAME);
    if (IS_ERR(led_class))
    {
        pr_err("create class failed!\r\n");
        retvalue = PTR_ERR(led_class);
        goto err_class_create;
    }

    /* 11、创建设备节点 */
    led_device = device_create(led_class, NULL, devno, NULL, LED_NAME);
    if (IS_ERR(led_device))
    {
        pr_err("create device failed!\r\n");
        retvalue = PTR_ERR(led_device);
        goto err_device_create;
    }

    pr_info("LED driver initialized\n");
    return 0;

err_device_create:
    class_destroy(led_class);
err_class_create:
    cdev_del(&led_cdev);
err_cdev_add:
    unregister_chrdev_region(devno, 1);
err_alloc_chrdev:
    iounmap(IMX6U_CCM_CCGR1);
    iounmap(SW_MUX_GPIO1_IO03);
    iounmap(SW_PAD_GPIO1_IO03);
    iounmap(GPIO1_DR);
    iounmap(GPIO1_GDIR);
err_ioremap:
    return retvalue;
}

/*
 * @description     : 驱动出口函数
 * @param           : 无
 * @return          : 无
 */
static void __exit led_exit(void)
{
    /* 销毁设备节点 */
    device_destroy(led_class, devno);

    /* 销毁设备类 */
    class_destroy(led_class);

    /* 删除 cdev */
    cdev_del(&led_cdev);

    /* 释放设备号 */
    unregister_chrdev_region(devno, 1);

    /* 取消映射 */
    iounmap(IMX6U_CCM_CCGR1);
    iounmap(SW_MUX_GPIO1_IO03);
    iounmap(SW_PAD_GPIO1_IO03);
    iounmap(GPIO1_DR);
    iounmap(GPIO1_GDIR);

    pr_info("LED driver exited\n");
}

module_init(led_init);

module_exit(led_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("fairy");
MODULE_DESCRIPTION("LED Driver");

使用udev后,仅仅使用insmod加载驱动,就可以自动创建设备文件,使用ledApp测试时,实验结果如预期

卸载只需要使用rmmod,不必在手动使用rm删除设备节点

2,动态加载_新方式

继续翻阅文档,发现下一节,新字符设备驱动实验的观点与AI不谋而合

不过对于设备号的分配却不相同,文档是先静态后动态,而AI是先动态再静态。重新问了几次,它自己推翻了自己,问及原因时,它这样答道:

那么就遵循现代Linux驱动开发的推荐做法,使用动态分配。

同时文档里使用了"设置文件私有数据",这种做法在现代 Linux 驱动开发中也是非常常见且推荐的,因为它可以方便地在驱动的其他操作函数(如 readwriterelease 等)中访问设备相关的数据。

优化后的代码如下

cpp 复制代码
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/io.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

#define LED_NAME        "led"      /* 设备名字 */

#define LEDOFF         0           /* 关灯 */
#define LEDON          1           /* 开灯 */

/* 寄存器物理地址 */
#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)

/* 设备结构体 */
struct led_dev
{
    dev_t devno; /* 设备号 */
    struct cdev cdev; /* 字符设备 */
    struct class *class; /* 设备类 */
    struct device *device; /* 设备实例 */
    void __iomem *reg_base; /* 寄存器基地址 */
    int led_state; /* LED 状态 */
};

static struct led_dev *led_devices; /* 设备实例 */

/*
 * @description     : LED打开/关闭
 * @param - sta     : LEDON(0) 打开LED,LEDOFF(1) 关闭LED
 * @param - reg_base: 寄存器基地址
 * @return          : 无
 */
static void led_switch(u8 sta, void __iomem *reg_base)
{
    u32 val;
    if (sta == LEDON)
    {
        val = readl(reg_base);
        val &= ~(1 << 3);
        writel(val, reg_base);
    } else if (sta == LEDOFF)
    {
        val = readl(reg_base);
        val |= (1 << 3);
        writel(val, reg_base);
    }
}

/*
 * @description     : 打开设备
 * @param - inode   : 传递给驱动的inode
 * @param - filp    : 设备文件,file结构体有个叫做private_data的成员变量
 *                    一般在open的时候将private_data指向设备结构体。
 * @return          : 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
    struct led_dev *dev;

    /* 获取设备结构体 */
    dev = container_of(inode->i_cdev, struct led_dev, cdev);
    filp->private_data = dev; /* 设置私有数据 */

    pr_info("Device opened\n");
    return 0;
}

/*
 * @description     : 从设备读取数据
 * @param - filp    : 要打开的设备文件(文件描述符)
 * @param - buf     : 返回给用户空间的数据缓冲区
 * @param - cnt     : 要读取的数据长度
 * @param - offt    : 相对于文件首地址的偏移
 * @return          : 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    return 0;
}

/*
 * @description     : 向设备写数据
 * @param - filp    : 设备文件,表示打开的文件描述符
 * @param - buf     : 要写给设备写入的数据
 * @param - cnt     : 要写入的数据长度
 * @param - offt    : 相对于文件首地址的偏移
 * @return          : 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    struct led_dev *dev = filp->private_data;
    unsigned char databuf[1];

    int retvalue = copy_from_user(databuf, buf, cnt);
    if (retvalue < 0)
    {
        pr_err("kernel write failed!\r\n");
        return -EFAULT;
    }

    /* 使用设备私有数据 */
    led_switch(databuf[0], dev->reg_base);

    return 0;
}

/*
 * @description     : 关闭/释放设备
 * @param - filp    : 要关闭的设备文件(文件描述符)
 * @return          : 0 成功;其他 失败
 */
static int led_release(struct inode *inode, struct file *filp)
{
    pr_info("Device released\n");
    return 0;
}

/* 设备操作函数 */
static struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .read = led_read,
    .write = led_write,
    .release = led_release,
};

/*
 * @description     : 驱动入口函数
 * @param           : 无
 * @return          : 无
 */
static int __init led_init(void)
{
    u32 val;
    int retvalue;

    /* 动态分配设备结构体 */
    led_devices = kzalloc(sizeof(struct led_dev), GFP_KERNEL);
    if (!led_devices)
    {
        pr_err("Failed to allocate device data\n");
        return -ENOMEM;
    }

    /* 1、寄存器地址映射 */
    led_devices->reg_base = ioremap(GPIO1_DR_BASE, 4);
    if (!led_devices->reg_base)
    {
        pr_err("ioremap failed!\r\n");
        retvalue = -ENOMEM;
        goto err_ioremap;
    }

    /* 2、使能GPIO1时钟 */
    val = readl(ioremap(CCM_CCGR1_BASE, 4));
    val &= ~(3 << 26); /* 清除以前的设置 */
    val |= (3 << 26); /* 设置新值 */
    writel(val, ioremap(CCM_CCGR1_BASE, 4));

    /* 3、设置GPIO1_IO03的复用功能 */
    writel(5, ioremap(SW_MUX_GPIO1_IO03_BASE, 4));

    /* 4、设置GPIO1_IO03的IO属性 */
    writel(0x10B0, ioremap(SW_PAD_GPIO1_IO03_BASE, 4));

    /* 5、设置GPIO1_IO03为输出功能 */
    val = readl(ioremap(GPIO1_GDIR_BASE, 4));
    val &= ~(1 << 3); /* 清除以前的设置 */
    val |= (1 << 3); /* 设置为输出 */
    writel(val, ioremap(GPIO1_GDIR_BASE, 4));

    /* 6、默认关闭LED */
    val = readl(led_devices->reg_base);
    val |= (1 << 3);
    writel(val, led_devices->reg_base);

    /* 7、动态分配设备号 */
    retvalue = alloc_chrdev_region(&led_devices->devno, 0, 1, LED_NAME);
    if (retvalue < 0)
    {
        pr_err("Failed to allocate device number\n");
        goto err_alloc_chrdev;
    }

    /* 8、初始化 cdev */
    cdev_init(&led_devices->cdev, &led_fops);
    led_devices->cdev.owner = THIS_MODULE;

    /* 9、添加 cdev 到内核 */
    retvalue = cdev_add(&led_devices->cdev, led_devices->devno, 1);
    if (retvalue < 0)
    {
        pr_err("cdev_add failed!\r\n");
        goto err_cdev_add;
    }

    /* 10、创建设备类 */
    led_devices->class = class_create(THIS_MODULE, LED_NAME);
    if (IS_ERR(led_devices->class))
    {
        pr_err("create class failed!\r\n");
        retvalue = PTR_ERR(led_devices->class);
        goto err_class_create;
    }

    /* 11、创建设备节点 */
    led_devices->device = device_create(led_devices->class, NULL, led_devices->devno, NULL, LED_NAME);
    if (IS_ERR(led_devices->device))
    {
        pr_err("create device failed!\r\n");
        retvalue = PTR_ERR(led_devices->device);
        goto err_device_create;
    }

    pr_info("LED driver initialized\n");
    return 0;

err_device_create:
    class_destroy(led_devices->class);
err_class_create:
    cdev_del(&led_devices->cdev);
err_cdev_add:
    unregister_chrdev_region(led_devices->devno, 1);
err_alloc_chrdev:
    iounmap(led_devices->reg_base);
err_ioremap:
    kfree(led_devices);
    return retvalue;
}

/*
 * @description     : 驱动出口函数
 * @param           : 无
 * @return          : 无
 */
static void __exit led_exit(void)
{
    /* 销毁设备节点 */
    device_destroy(led_devices->class, led_devices->devno);

    /* 销毁设备类 */
    class_destroy(led_devices->class);

    /* 删除 cdev */
    cdev_del(&led_devices->cdev);

    /* 释放设备号 */
    unregister_chrdev_region(led_devices->devno, 1);

    /* 取消映射 */
    iounmap(led_devices->reg_base);

    /* 释放设备结构体 */
    kfree(led_devices);

    pr_info("LED driver exited\n");
}

module_init(led_init);

module_exit(led_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("fairy");
MODULE_DESCRIPTION("LED Driver");

实验结果如预期,接下来可以使用设备树来尝试更新的方法

3,设备树的初步了解

关于设备树的介绍,先初步浏览了解一下,知道设备树与驱动开发相互配合,有函数可以访问设备树节点信息就行,再通过后面的例子进行深入学习,加深印象。

浏览了文档的后面内容,驱动这一章节的篇幅是真的大,如果一章一章地学,实在不符合我想要快速上手的目的。后面的章节有SPI、I2C什么的,这个还是等到需要用到的时候再专门查文档学习吧。


先尝试搭建Qt环境吧,如果成功的话,那么后续可以边开发驱动,边开发对应的界面来实现复杂功能的控制

四、Qt环境搭建尝试

尝试了许久,各种混账的兼容性问题频出,最终发现还是不如考古。正点原子资料盘里的虚拟机光盘里已经搭建好了所有环境,可以直接使用,可以根据目录跳转到本章的第3个的第④个。前面的内容少儿不宜,埋藏着笔者深深的怨气。

1、Qt安装_Windows(可跳过)

这里选用的是Qt5.15.2,这是Qt5的最后一个版本,同时也是LTS。从这个镜像网站里下载Qt Downloads

安装时,遵循一般博客里的做法即可,首先是要创建账号的。不过直接进入这个程序,下载还是会失败(贼他宝贝的麻烦),需要让Qt下载程序用镜像网站下载,参考博客windows安装QT时出现"无法下载存档......"解决办法 - lmore - 博客园

注意,腾讯的镜像网站里只有6.8以上的版本(一共就三个),不要用。清华的镜像可以下载5.12.2,但下载6.8.1也会报什么文档下载失败的错误。试了几个镜像,就清华的这个比较全,但这些镜像下载旧版要比官方好,但新版就不行了

在你Qt下载程序所在的目录,打开终端,输入下面命令,左边是你的Qt下载程序,输入前面./qt,然后按Tab键,一般就可以自动补全了。

bash 复制代码
.\qt-online-installer-windows-x64-4.8.1_2.exe --mirror https://mirrors.tuna.tsinghua.edu.cn/qt/

当选择版本时,一开始是没有Qt5之类的版本,把右边的Archive勾选上,然后再点击筛选

2,使用正点原子项目(可跳过)

①转移项目

把资料盘里的Qt应用程序复制到一个不含中文和特殊符号的路径

②编译项目

这里先测试一下这个Qt程序是什么样子的,选择MinGW64bit这个编译工具链

找到刚才那个项目里的pro文件

出现这个界面后,先勾选MinGW64bit(下图为32bit,都差不多),然后向下滑动,点击configure program

进入到下面项目

点击上方的构建栏,里面有运行

点击运行后,就可以编译出Qt应用程序了,一切如预期那样。不过要注意,此时编译的程序是x86_x64架构的,后缀名为exe,而非是开发板arm32架构(后缀名为elf文件)

3,交叉编译

①下载交叉编译工具链(可跳过)

Downloads | 9.2-2019.12 -- Arm Developer下载9.2的交叉编译工具链,如果是Linux使用,那么就下载下面这个

如果是Windows,那么就下载这个

下载后,把它解压在一个合适的目录(不能含有中文)。

不过考虑到这个编译器暂时不会与其他编译器的名称起冲突,那么就先添加环境变量。按下Win+X,选择【系统】,再点击【高级系统设置】,再点击【环境变量】。要编辑的是下面这个Path

在里面把刚才的路径复制过去,下面是参考,自行修改

bash 复制代码
E:\Tools\Develop\ToolsKits\ARM\arm-gnu-toolchain-14.2.rel1-mingw-w64-i686-arm-none-linux-gnueabihf\bin

一路点击确定,最后重启电脑。重启后,打开终端,输入下面语句,观察是否有版本信息

bash 复制代码
 arm-none-linux-gnueabihf-gcc -v

②交叉编译Qt源码库_Windows(失败的)

下载5.12.2的Qt源码

Index of /archive/qt/5.15/5.15.2/single

下载后,找到一个不含中文的目录,解压

找到如下路径

由于我们的目标是编译arm32平台的linux程序,所以这里选择linux-arm-gnueabi-g++,用记事本打开,可以看到这里的编译器与我们下载的编译器基本是匹配的,而且前面也将环境变量添加上去了,就不需要再这里添加路径了。

所以只需要把arm-linux前缀改为arm-none-linux,gnueabi改为gnueabihf即可

在Qt源码目录,打开终端输入下面命令

bash 复制代码
./configure.bat -release -opensource -prefix E:\Tools\Develop\ToolsKits\Qt\qt-5.15.2-arm -xplatform linux-arm-gnueabi-g++ -nomake tests -nomake examples -no-opengl -skip qtvirtualkeyboard -skip qtwebengine
  • -prefix:指定Qt库的安装路径,自行选择

  • -xplatform:指定交叉编译平台

  • -nomake:跳过不需要的模块以加快编译速度

如果出现下面错误,在环境变量里添加MSVC的bin目录即可

这个nmake的路径比较复杂,首先找到安装的VS的目录,如下,2022是版本号,按此路径最终找到下面目录(自行替换)

bash 复制代码
C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.41.34120\bin\Hostx64\x64
bash 复制代码
C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.41.34120\include

不过运行这个bat脚本中可能会出现下面这个沙雕错误(荼毒无穷),添加环境变量也没有任何作用

根据VS安装目录,找到下面路径,在这里打开终端,执行下面命令

bash 复制代码
 .\vcvarsall.bat x64

然后你就会发现,啥用没有。后续找了许久,发现忘了制定平台

bash 复制代码
./configure.bat -release -opensource -prefix E:\Tools\Develop\ToolsKits\Qt\qt-5.15.2-arm -xplatform linux-arm-gnueabi-g++ -nomake tests -nomake examples -no-opengl -skip qtvirtualkeyboard -skip qtwebengine -platform win32-g++

但我他宝贝的没高兴多久又寄了

找到下面路径

用记事本打开qglobal.h ,添加这个头文件

cpp 复制代码
#include <limits>

诸如此类,尝试了许多种方法,最终带着深深的怨气总算找到了疑似靠谱的方法(实用MSVC)。按Win打开菜单,找到VS的命令行,x64和x86随意,这里用的是

x64 Native Tools Command Prompt for VS 2022

在打开的cmd窗口中,使用cd命令跳转到Qt源码的目录,不过要注意的是在cd命令后加上/d参数,才能执行跨盘操作

bash 复制代码
cd /d E:/Tools/Develop/ToolsKits/Qt/qt-everywhere-src-5.15.2/

然后输入下面命令,不用加platform选项

bash 复制代码
configure.bat -release -opensource -prefix E:\Tools\Develop\ToolsKits\Qt\qt-5.15.2-arm -xplatform linux-arm-gnueabi-g++ -nomake tests -nomake examples -no-opengl -skip qtvirtualkeyboard -skip qtwebengine -platform win32-g++

输入y即可

如果出现下图,说明qmake.tconf里的编译工具集的名称没有写对,或者环境变量没有生效,自行检查

然后使用make构建,电脑有几核就输入几

bash 复制代码
mingw32-make -j16

然后就没有然后了,会报一些C++错误

③交叉编译Qt源码库_Linux(失败的)

下载5.12.2的Qt源码,选择下面那个tar.xz

Index of /archive/qt/5.15/5.15.2/single

然后下载工具链,Arm GNU Toolchain Downloads -- Arm Developer

通过Filezila传输到虚拟机里

工具链的解压用下面命令,Qt源码也是如此

bash 复制代码
tar -xvJf arm-gnu-toolchain-14.2.rel1-x86_64-arm-none-linux-gnueabihf.tar.xz

添加环境变量,需要修改下面文件

bash 复制代码
sudo vim /etc/environment

在原变量里加上冒号,后面再添加路径,路径改为自己的工具链路径

bash 复制代码
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/fairy/Embedded/Toolkits/toolchain/arm-gnu-toolchain-14.2.rel1-x86_64-arm-none-linux-gnueabihf/bin"

重启后,环境变量就会生效。

接着我们安装依赖,先更新apt

bash 复制代码
sudo apt update

然后安装依赖

bash 复制代码
sudo apt install build-essential libgl1-mesa-dev libxkbcommon-dev libxcb-xinerama0-dev libxcb-xinput-dev libfontconfig1-dev libfreetype6-dev libdbus-1-dev libicu-dev libssl-dev libjpeg-dev libpng-dev libpcre3-dev libz-dev

由于配置过长,需要配置一个脚本,路径/path/to/install/qt5.12.2自行替换

bash 复制代码
#!/bin/bash
./configure -prefix /path/to/install/qt5.12.2 \
    -opensource \
    -confirm-license \
    -release \
    -xplatform linux-arm-gnueabi-g++ \
    -no-opengl \
    -no-sse2 \
    -no-xcb \
    -qt-libjpeg \
    -qt-libpng \
    -qt-zlib \
    -qt-pcre \
    -qt-freetype \
    -qt-harfbuzz \
    -no-openssl \
    -no-cups \
    -no-dbus \
    -no-glib \
    -no-iconv \
    -no-icu \
    -no-eglfs \
    -no-linuxfb \
    -no-kms \
    -no-gtk \
    -no-xkbcommon \
    -no-xcb-xlib \
    -no-xinput2 \
    -no-xcb-xinput \
    -no-xcb-randr \
    -no-xcb-shape \
    -no-xcb-sync \
    -no-xcb-xfixes \
    -no-xcb-xkb \
    -no-xkbcommon-x11 \
    -no-xrender \
    -no-xi \
    -no-xext \
    -no-fontconfig \
    -no-freetype \
    -no-harfbuzz \
    -no-pcre \
    -no-zlib \
    -no-jpeg \
    -no-png \
    -no-gif \
    -no-sqlite \
    -no-libudev \
    -no-evdev \
    -no-mtdev \
    -no-tslib \
    -no-libinput \
    -no-gstreamer \
    -no-pulseaudio \
    -no-alsa \
    -no-vulkan \
    -no-qml-debug \
    -no-compile-examples \
    -nomake examples \
    -nomake tests \
-no-tslib 

使用chmod赋予脚本权限,假设脚本为autoConfig.sh

bash 复制代码
chmod 777 autoConfig.sh

然后在源码目录里运行脚本,执行脚本之后就会报缺少limits什么的错误,到下面目录,找到qglobal.h 添加这个头文件即可

cpp 复制代码
#include <limits>

构建成功后

然后使用make

bash 复制代码
make -j16

④交叉编译_正点原子

事实证明,有些护城河就不是河,简直就是天堑。知道C++的abi不稳定,但没想到会是这般不稳定,尝试了一天,在已有体系上5.12实在编译不了,我都快准备放弃Qt,使用LVGL了。最后,只能强忍着不适,继续尝试下去。

可能是MinGW版本不对,可能是GCC版本不对,可能是Qt配置的某些选项不对,可能是不同厂商的gcc对某些特定abi不兼容,可能是Ubuntu版本不对,......,可能性太多了,Windows平台是不寄予希望了,还是考古吧(已老实,求放过)。

按照文档指示,安装正点原子的ubuntu2016

在原有网卡基础上,再添加一个NAT模式,用于联网

设置好之后,打开虚拟机,进入设置,找到NetWork

进入NetWork后,可以看到两个Wired(有线连接),第一个往往是桥接模式(eth0),第二个是NAT模式(eth1)。为了与外界进行交互,我们修改第一个(桥接模式)的ip,把它设置为静态IP。

点击Options,找到IPv4 Settings,然后切换为手动模式(静态)

使用ifconfig查看,ip已被正确设置

记住下面这个ens37这个ip,这是NAT模式的,我们使用Filezila与虚拟机传输,用的ip就是它。此外,正点原子已经为这个Ubuntu安装好了FTP服务,并且已经配置好了。

如果出现乱码,在站点管理器里,把字符集设置为强制使用UTF-8

编译一二十分钟,然后报错,这一点我是万万没想到的。报了一个override错误

又重试了一遍,终于成功了。使用的脚本是正点原子里的。此步骤可以省略,因为正点原子虚拟光盘里已经有编译好的Kits

bash 复制代码
./configure -prefix /home/alientek/Qt/arm-qt \
-opensource \
-confirm-license \
-release \
-strip \
-shared \
-xplatform linux-arm-gnueabi-g++ \
-optimized-qmake \
-c++std c++11 \
--rpath=no \
-pch \
-skip qt3d \
-skip qtactiveqt \
-skip qtandroidextras \
-skip qtcanvas3d \
-skip qtconnectivity \
-skip qtdatavis3d \
-skip qtdoc \
-skip qtgamepad \
-skip qtlocation \
-skip qtmacextras \
-skip qtnetworkauth \
-skip qtpurchasing \
-skip qtremoteobjects \
-skip qtscript \
-skip qtscxml \
-skip qtsensors \
-skip qtspeech \
-skip qtsvg \
-skip qttools \
-skip qttranslations \
-skip qtwayland \
-skip qtwebengine \
-skip qtwebview \
-skip qtwinextras \
-skip qtx11extras \
-skip qtxmlpatterns \
-make libs \
-make examples \
-nomake tools -nomake tests \
-gui \
-widgets \
-dbus-runtime \
--glib=no \
--iconv=no \
--pcre=qt \
--zlib=qt \
-no-openssl \
--freetype=qt \
--harfbuzz=qt \
-no-opengl \
-linuxfb \
--xcb=no \
-tslib \
--libpng=qt \
--libjpeg=qt \
--sqlite=qt \
-plugin-sql-sqlite \
-I/home/alientek/tslib-1.21/arm-tslib/include \
-L/home/alientek/tslib-1.21/arm-tslib/lib \
-recheck-all
bash 复制代码
time (make -j16)

简直就是神迹!

bash 复制代码
time (make install)

后续又试了一下,同样编译器和构建命令的情况下,Ubuntu2024会出现下面错误

后续测试了一下Ubunt2024、Ubuntu2016与gcc9.2、gcc14.2的排列组合,只有Ubuntu2016和gcc9.2的组合可以正常编译。

换成Windows平台,使用同样的gcc编译器(9.2),同样的命令,只会编译出下面结果。也许是MinGW版本不对,MinGW32(gcc9.2)和MinGW64(gcc14.2)都不行

bash 复制代码
./configure.bat -prefix E:\Tools\Develop\ToolsKits\Qt\qt-5.15.2-arm-gcc -opensource -confirm-license -release -strip -shared -xplatform linux-arm-gnueabi-g++ -optimized-qmake -c++std c++11 --rpath=no -pch -skip qt3d -skip qtactiveqt -skip qtandroidextras -skip qtcanvas3d -skip qtconnectivity -skip qtdatavis3d -skip qtdoc -skip qtgamepad -skip qtlocation -skip qtmacextras -skip qtnetworkauth -skip qtpurchasing -skip qtremoteobjects -skip qtscript -skip qtscxml -skip qtsensors -skip qtspeech -skip qtsvg -skip qttools -skip qttranslations -skip qtwayland -skip qtwebengine -skip qtwebview -skip qtwinextras -skip qtx11extras -skip qtxmlpatterns -make libs -make examples -nomake tools -nomake tests -gui -widgets -dbus-runtime --glib=no --iconv=no --pcre=qt --zlib=qt -no-openssl --freetype=qt --harfbuzz=qt -no-opengl -linuxfb --xcb=no --libpng=qt --libjpeg=qt --sqlite=qt -plugin-sql-sqlite -recheck-all -platform win32-g++

MinGW64 8.1.0会报满屏的缺少定义的错误

⑤添加编译工具链

这些乱七八糟的的构建体系,给人一种"生命总会找到出路,甭管路子有多野"的恶感,linux(或者说Qt、C++)的护城河远比想象的牢固。

在虚拟机里安装Linux下的Qt,网址为Index of /official_releases/online_installers,与Windows下的安装基本一致

++这个还是遵循正点原子文档,使用这个命令在Ubuntu2016里下载,在Ubuntu2024里使用编译好的Qt模块,会提示缺少positioning模块++

好吧,正点原子这个光盘里什么都有,不用安装。

bash 复制代码
wget http://download.qt.io/archive/qt/5.12/5.12.9/qt-opensource-linux-x64-5.12.9.run
bash 复制代码
chmod u+x qt-unified-linux-x64-online.run
bash 复制代码
sudo ./qt-unified-linux-x64-online.run --mirror https://mirrors.tuna.tsinghua.edu.cn/qt/

安装的过程与正点原子相同,目录就默认在/opt/Qt, 安装过程可能出现下面提示 安装这个库即可,不然无法打开程序

bash 复制代码
sudo apt install libxcb-cursor0 libxcb-cursor-dev

安装完成后,可以输入下面命令来打开,或者直接到该目录下运行sh脚本

bash 复制代码
/opt/Qt/Tools/QtCreator/bin/qtcreator.sh &

点击左上角这个图表

输入一个Q即可看到安装好的Qt,不过这里最好不要使用这个图标直接运行程序,可以使用下面这个命令来运行,避免后面诸多环境变量不一致

bash 复制代码
/opt/Qt5.12.9/Tools/QtCreator/bin/qtcreator.sh &

把QtDesktop工程传输到虚拟机里,打开前删除.pro.user这个文件

使用Qt打开,选择第一个Kits(ATK-I.MX6U)和Desktop那个Kits(用于生成桌面应用程序观察效果),然后点击配置工程。这里没用之前编译的Qt库是有原因的,因为使用的时候又他宝贝的寄了。我只好非常可耻地使用正点原子编译好的Qt库,不折腾了

点击左侧的Projects,再次进入配置Kits的界面,左侧已经有两个Kits,点击哪个Kits的Build或者Run,该Kits的名称就会加粗,表示该工程使用这个Kits。

回到工程里,左下角也可以选择哪个Kits

选择桌面那个Kits,然后去点击运行,可以生成下图桌面程序

只能说还是厂商的靠谱, 虽说版本旧了,但至少能用,不会有那么多奇奇怪怪的问题

4,远程调试

①构建arm-linux程序

选择另一套kits,然后构建(不要运行),构建完成后,会在工程同级目录下有一个build目录,里面有arm架构的elf程序

②连接开发板

连接开发板的串口,输入下面命令,确保开发板的根文件系统版本大于v1.9。或者直接输入rysnc,如果出现一堆提示,那么就说明有rsync命令

bash 复制代码
cat /etc/version

回到虚拟机,在Tools栏里找到最后一个选项,点击设备。这里是默认设置好的,把里面的Host的IP改为实际开发板里的(记得在开发板里输入ifconfig确定ip),点击OK

然后点击Kits,可以看到这里已经配置好了rsync

在Projects下,点击刚才的rsync套件

再点击里面的run,由于这个工程名为Desktop,会与开发板里的Desktop程序起冲突

所以需要勾选上面的复选框,左面的路径可以到Build里去查找

添加一个SSH命令(图里多打了一个空格)

bash 复制代码
-p %{Device:SshPort} %{Device:UserName}@%{Device:HostAddress} 'mkdir -p %{CurrentRun:Executable:Path}'

再添加一个scp(传输程序用的)

bash 复制代码
-P %{Device:SshPort} %{CurrentRun:Executable:FileName}
%{Device:UserName}@%{Device:HostAddress}:%{CurrentRun:Executable:FilePath}

下面这个路径也要改

再添加一个,设备就是刚才Remote Diretory下的程序,要勾选那个复选框

bash 复制代码
/opt/test/bin/QDesktop

回到Edit界面,点击运行,除了下方有一些红字外,一切正常

这个桌面程序是要比开发板自带的画质要低一些

在开发板的串口输入top命令,可以看到有两个QDesktop在运行(难怪刚才那么卡,图片还会一闪一闪的),应该是部署时没有正确沙掉进程

在这个界面下,输入k命令,后面跟着PID就可以沙雕对应进程了,这里保留时间短的(刚才烧录的程序)。输入q可以退出

不用担心开发板里的程序,开发板重启后会自动运行自带的桌面

可以看到虚拟机里的这个桌面,左下角是没有图片的

基本的Qt环境已经搭建好了,下一步开发Qt时会轻松不少,最主要的是能看到最终要实现的效果近在眼前。不过在虚拟机里开发着实不方便,后续准备尝试把开发界面的任务迁移至主机平台,使用CLion配合Qt Design什么的开发,部署调试再放到虚拟机里。

这个方案之所以可行,还是因为Qt强大的跨平台,不同平台相同接口。只不过不同的平台需要不同的库,而这个Qt库的编译是相当折磨,与java的"一次编写,到处运行"完全不一样。

之前还觉得LVGL使用纯C语编写,开发界面很麻烦,现在看来真的是很棒的设计!不会有那么多烦人的兼容性问题,而且界面的开发完全可以使用C++等来封装一些基本的lvgl接口,达到类"Qt"的那种开发效果。或者用别的语言来调用C编译的库,总之移植起来相当方便。

五、Linux驱动开发

有设备树的驱动开发,才算完整的Linux驱动开发嘛

1,设备树下的LED驱动

①初识设备树

每个节点(无论是根节点还是子节点)都是用一个花括号包起来。花括号中,上面是属性,下面是子节点(也可能没有)。这种写法很递归,也有点像C++的类,上面是"成员变量",下面是"成员函数定义",属性部分有些像标签语言。反正怎么好理解就怎么记,不讨论先有儿子还是先有爸爸的问题

以此类推(就不水了)

bash 复制代码
后面我们就以LED设备为例来讲解,下面我有这些问题:
1,compatible怎么用于匹配?名称是根据前面已经出现的,还是我自己随便起?还是说有固定的规则?
2,这里面默认触发模式是什么?有哪些模式?各个模式有什么用?
3,gpios属性被定义为<&gpio1 5 GPIO_ACTIVE_HIGH>,那么&gpio1是不是需要已经出现过的引脚?
4,我想要添加新设备,是不是可以自己再创建一个dts,然后使用include包含前面的dts文件,然后就可以在这个新文件里使用根节点追加的方式?
5,我暂时想不到什么问题了,你就以一名初学者的角度来帮我想想还有哪些问题值得问,然后解答它

②设备树编写

结合正点原子示例代码和文档,准备新建一个dts文件,比如mx6ull-alientek-emmc.dts,在里面引用前面的那个完备的dts

cpp 复制代码
#include "imx6ull-14x14-emmc-7-1024x600-c.dts"

依次往上找到被包含的dtsi文件ixm6ull-14×14-evk.dtsi,我们可以在一个dts文件里找到pinctrl里的gpio-leds(535行左右),这是属于iomuxc节点的

往上我们可以看到leds所在节点(107行左右),这些都是写好的。leds节点中没有state属性,那么默认就是okay(启用)

也就是说如果前面的dts没有定义这些节点,我们可以通过类似于下面这种方式来追加相关内容,这是一般的开发步骤。但evk板既然给了,那就不写了吧,因为我们知道它是怎么来的

根据文档,使用pinctrl后,还需要检查引脚是否冲突!文档中特别提到,阿尔法板是没有用到tsc这个接口的,我们需要把它注释掉

通过搜索功能,可以看到在650行附近有tsc的定义,我们注释掉即可

搜索gpio 3,可以看到外设节点tsc里也会用到GPIO1的3号引脚,这里的状态是disabled,所以不会冲突。也注释掉,比较阿尔法板并没有用到这个接口

可以看到上面三个dts是层层嵌套的,左边依赖且只依赖一个右边

为了方便测试,我们可以让新建的dts只包含imx6ull-14×14-evk,把左边两个定义的节点复制过来

cpp 复制代码
#include "imx6ull-14x14-evk.dts"

&usdhc2 {
	pinctrl-names = "default", "state_100mhz", "state_200mhz";
	pinctrl-0 = <&pinctrl_usdhc2_8bit>;
	pinctrl-1 = <&pinctrl_usdhc2_8bit_100mhz>;
	pinctrl-2 = <&pinctrl_usdhc2_8bit_200mhz>;
	bus-width = <8>;
	non-removable;
	status = "okay";
};


&i2c2 {
	goodix_ts@5d {
		reg = <0x5d>;
	};
};

&lcdif {
	display0: display {
		bits-per-pixel = <16>;
		bus-width = <24>;

		display-timings {
			native-mode = <&timing0>;
			timing0: timing0 {
				clock-frequency = <51000000>;
				hactive = <1024>;
				vactive = <600>;
				hfront-porch = <160>;
				hback-porch = <140>;
				hsync-len = <20>;
				vback-porch = <20>;
				vfront-porch = <12>;
				vsync-len = <3>;

				hsync-active = <0>;
				vsync-active = <0>;
				de-active = <1>;
				pixelclk-active = <0>;
			};
		};
	};
};

找到dts目录下的Makefile文件,看看是否有新建的dts文件(注意这里添加的是dtb),如果没有就找到对应位置添加

一切就绪后,使用下面命令来编译dtb

bash 复制代码
make dtbs

③驱动编写

把原先的led工程复制一份,打开后开始编写驱动。这里先定义一个设备结构体

cpp 复制代码
/* 设备结构体 */
struct led_dev
{
    dev_t devno; /* 设备号 */
    struct cdev cdev; /* 字符设备 */
    struct class *class; /* 设备类 */
    struct device *device; /* 设备实例 */
    int led_gpio; /* GPIO 引脚 */
    int led_state; /* LED 状态 */
};

在init函数里分配这个设备结构体

cpp 复制代码
    /* 动态分配设备结构体 */
    led_devices = kzalloc(sizeof(struct led_dev), GFP_KERNEL);
    if (!led_devices)
    {
        pr_err("Failed to allocate device data\n");
        return -ENOMEM;
    }

从这里可以看出led1是在/leds/led1这个路径上

那么就可以使用of_*函数来获取对应的设备树节点

cpp 复制代码
    struct device_node *np;

    /* 从设备树中获取LED的GPIO */
    np = of_find_node_by_path("/leds");
    if (!np)
    {
        pr_err("Failed to find LED node in device tree\n");
        retvalue = -ENODEV;
        goto err_find_node;
    }

接着获取LED的引脚

cpp 复制代码
    /* 获取 LED GPIO 引脚 */
    led_device->led_gpio = of_get_named_gpio(np, "led1", 0);
    if (led_device->led_gpio < 0) {
        pr_err("Failed to get LED GPIO\n");
        ret = led_device->led_gpio;
        goto err_get_gpio;
    }

申请GPIO,这里的ret变量只是获取返回值状态

cpp 复制代码
    /* 申请 GPIO */
    ret = gpio_request(led_device->led_gpio, "led1");
    if (ret) {
        pr_err("Failed to request LED GPIO\n");
        goto err_gpio_request;
    }

调用GPIO函数,来设置GPIO状态,根据设备树可以知道它是低电平有效,那么高电平就是关闭LED

cpp 复制代码
    /* 设置 GPIO 方向 */
    gpio_direction_output(led_device->led_gpio, 1);  // 默认关闭 LED

可以通过下面函数来设置GPIO的引脚值,其余与之前无异

cpp 复制代码
gpio_set_value(dev->led_gpio, 0);

完整代码为

cpp 复制代码
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/gpio.h>

#define DEVICE_NAME "led"  // 设备名称

/* 设备结构体 */
struct led_dev {
    dev_t devno;                // 设备号
    struct cdev cdev;           // 字符设备
    struct class *class;        // 设备类
    struct device *device;      // 设备实例
    int led_gpio;               // LED GPIO 引脚
};

static struct led_dev *led_device;  // 设备实例

/*
 * @description : 打开设备
 */
static int led_open(struct inode *inode, struct file *filp)
{
    struct led_dev *dev = container_of(inode->i_cdev, struct led_dev, cdev);
    filp->private_data = dev;  // 设置私有数据
    pr_info("Device opened\n");
    return 0;
}

/*
 * @description : 从设备读取数据
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    return 0;
}

/*
 * @description : 向设备写数据
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    struct led_dev *dev = filp->private_data;
    unsigned char databuf[1];
    int ret;

    ret = copy_from_user(databuf, buf, cnt);
    if (ret < 0) {
        pr_err("Failed to copy data from user\n");
        return -EFAULT;
    }

    /* 控制 LED */
    if (databuf[0] == 1) {
        gpio_set_value(dev->led_gpio, 0);  // 点亮 LED
    } else if (databuf[0] == 0) {
        gpio_set_value(dev->led_gpio, 1);  // 关闭 LED
    }

    return 0;
}

/*
 * @description : 关闭设备
 */
static int led_release(struct inode *inode, struct file *filp)
{
    pr_info("Device released\n");
    return 0;
}

/* 设备操作函数 */
static struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .read = led_read,
    .write = led_write,
    .release = led_release,
};

/*
 * @description : 驱动入口函数
 */
static int __init led_init(void)
{
    int ret;
    struct device_node *np;

    /* 动态分配设备结构体 */
    led_device = kzalloc(sizeof(struct led_dev), GFP_KERNEL);
    if (!led_device) {
        pr_err("Failed to allocate device data\n");
        return -ENOMEM;
    }

    /* 查找设备树节点 */
    np = of_find_node_by_path("/leds/led1");
    if (!np) {
        pr_err("Failed to find LED node in device tree\n");
        ret = -ENODEV;
        goto err_find_node;
    }

    /* 获取 LED GPIO 引脚 */
    led_device->led_gpio = of_get_named_gpio(np, "gpios", 0);
    if (led_device->led_gpio < 0){
        pr_err("Failed to get LED GPIO\n");
        ret = led_device->led_gpio;
        goto err_get_gpio;
    }

    /* 申请 GPIO */
    ret = gpio_request(led_device->led_gpio, "my-led");
    if (ret){
        pr_err("Failed to request LED GPIO\n");
        goto err_gpio_request;
    }

    /* 设置 GPIO 方向 */
    gpio_direction_output(led_device->led_gpio, 1);  // 默认关闭 LED

    /* 动态分配设备号 */
    ret = alloc_chrdev_region(&led_device->devno, 0, 1, DEVICE_NAME);
    if (ret < 0) {
        pr_err("Failed to allocate device number\n");
        goto err_alloc_chrdev;
    }

    /* 初始化 cdev */
    cdev_init(&led_device->cdev, &led_fops);
    led_device->cdev.owner = THIS_MODULE;

    /* 添加 cdev 到内核 */
    ret = cdev_add(&led_device->cdev, led_device->devno, 1);
    if (ret < 0) {
        pr_err("Failed to add cdev\n");
        goto err_cdev_add;
    }

    /* 创建设备类 */
    led_device->class = class_create(THIS_MODULE, DEVICE_NAME);
    if (IS_ERR(led_device->class)) {
        pr_err("Failed to create class\n");
        ret = PTR_ERR(led_device->class);
        goto err_class_create;
    }

    /* 创建设备节点 */
    led_device->device = device_create(led_device->class, NULL, led_device->devno, NULL, DEVICE_NAME);
    if (IS_ERR(led_device->device)) {
        pr_err("Failed to create device\n");
        ret = PTR_ERR(led_device->device);
        goto err_device_create;
    }

    pr_info("LED driver initialized\n");
    return 0;

err_device_create:
    class_destroy(led_device->class);
err_class_create:
    cdev_del(&led_device->cdev);
err_cdev_add:
    unregister_chrdev_region(led_device->devno, 1);
err_alloc_chrdev:
    gpio_free(led_device->led_gpio);
err_gpio_request:
err_get_gpio:
err_find_node:
    kfree(led_device);
    return ret;
}

/*
 * @description : 驱动出口函数
 */
static void __exit led_exit(void)
{
    /* 销毁设备节点 */
    device_destroy(led_device->class, led_device->devno);

    /* 销毁设备类 */
    class_destroy(led_device->class);

    /* 删除 cdev */
    cdev_del(&led_device->cdev);

    /* 释放设备号 */
    unregister_chrdev_region(led_device->devno, 1);

    /* 释放 GPIO */
    gpio_free(led_device->led_gpio);

    /* 释放设备结构体 */
    kfree(led_device);

    pr_info("LED driver exited\n");
}

module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("LED Driver");

④驱动测试

由于我们没有对设备树的节点做任何修改,在不更换前面编译后的dtb的情况下,理应实现相同的效果。如果重新把设备树拷贝到SD卡的第一个分区里,记得在Uboot启动时把bootcmd也改了,因为两个设备树文件名称不同。

这里通过NFS挂载,把编译后的驱动模块传输到开发板上

bash 复制代码
/etc/init.d/networking restart
bash 复制代码
mount -t nfs -o nfsvers=3 192.168.1.128:/home/fairy/nfs_share /mnt/nfs

如果在设备树里就把default-trigger设置为none,那么就不需要通过下面这个设置了,否则灯还会闪

bash 复制代码
echo none > /sys/class/leds/sys-led/trigger

加载模块时可能会出现申请失败的问题,把申请GPIO的代码给注释就行了,原因在下面有。

可以通过下面命令来查看GPIO3(LED的GPIO引脚)是否被占用

bash 复制代码
cat /sys/kernel/debug/gpio

如前面提到,LED的引脚莫名其妙被占用,就是gpio-3旁边被一个"?"占用

后来试了一下,只要在设备树里定义了这个引脚,那么它就显示被"?"占用,实际上谁也没占用它。查了一下,发现内核会在启动时自动解析该节点,并调用 gpio_request 申请 gpio1_io03,也就是如果在设备树中已经定义了某个 GPIO 引脚,那么内核会优先占用该引脚,以便我们可以在用户空间中访问,比如前面通过下面命令改变LED的触发状态

bash 复制代码
echo none > /sys/class/leds/sys-led/trigger

比如亮灭

bash 复制代码
echo 1 > /sys/class/leds/sys-led/brightness  # 点亮 LED
echo 0 > /sys/class/leds/sys-led/brightness  # 关闭 LED

这其实是"compatible = "gpio-leds";"所致,意思是交由内核管理,可以换成别的名称,比如compatible ="atkalpha-gpioled";

2,梳理

有第一个使用设备树开发linux驱动的经验后,学习linux驱动的脉络清晰了一些,希望其他的多少也能照葫芦画瓢。

下一步,可以尝试在正点原子创建的桌面上添加一个按钮,用于控制LED或蜂鸣器,试一下GUI与驱动的交互。等GUI与驱动开发都熟练时,或许可以更进一步学习linux内核、构建根文件系统等,尝试使用imx6ull官方最新适配的linux(或者Android),以及配套的uboot和根文件系统,再尝试Qt6.8.1?

嵌入式Linux入门难,各种环境搭建、兼容性问题就拦了一部分人。想精进也难,各种工具、命令、规则、协议栈、C语言的奇思妙法等,想想头都大。

希望大家都能有所收获!

相关推荐
bohu834 小时前
亚博microros小车-原生ubuntu支持系列:8-脸部检测与人脸特效
linux·opencv·ubuntu·dlib·microros·亚博
小池先生7 小时前
grafana+prometheus监控linux指标
linux·grafana·prometheus
远方 hi8 小时前
linux如何修改密码,要在CentOS 7系统中修改密码
linux·运维·服务器
练小杰8 小时前
Linux系统 C/C++编程基础——基于Qt的图形用户界面编程
linux·c语言·c++·经验分享·qt·学习·编辑器
勤又氪猿8 小时前
【问题】Qt c++ 界面 lineEdit、comboBox、tableWidget.... SIGSEGV错误
开发语言·c++·qt
mcupro10 小时前
提供一种刷新X410内部EMMC存储器的方法
linux·运维·服务器
不知 不知10 小时前
最新-CentOS 7 基于1 Panel面板安装 JumpServer 堡垒机
linux·运维·服务器·centos
人才程序员10 小时前
【C++拓展】vs2022使用SQlite3
c语言·开发语言·数据库·c++·qt·ui·sqlite
BUG 40411 小时前
Linux--运维
linux·运维·服务器