嵌入式Linux_字符设备驱动开发流程_最简单|最完整|最入门|零基础|保姆级|驱动开发案例_LED_(通过TFTP启动Linux内核和NFS挂载根文件系统+LED驱动编写+亮灭应用测试)

一、学习目的

如果你想速成嵌入式Linux的驱动开发,那么学习嵌入式Linux的驱动开发的第一步,便是先学到你能做一个简单的东西出来,如简单的LED驱动开发。然后再弄清楚与STM32等Cortex M3系列点灯的区别。
(本案例是通过直接操作寄存器方式,方便从32过度,现在更多的是使用设备树进行开发,设备树开发的案例后面的帖子再更新)

二、学习平台与准备工作

1、学习平台

本帖学习平台基于【正点原子的阿尔法IMX6ULL开发板(EMMC版)】。

2、准备工作

(在前面的学习中,已经启用过TFTP服务器和NFS的可以跳过这个部分)
想要在开发板中用Linux系统操作设备,需要一些准备工作。首先为了方便调试,我们选择通过TFTP启动Linux内核和NFS挂载根文件系统。启动内核就好比启动windows,首先我们得装个系统。而c盘的基础文件,就类似于linux的根文件系统。有了这些才能进行下一步的驱动开发。当然了本贴的学习是建立在读者已经掌握linux的基础知识前提下。

①TFTP配置

我们选择通过TFTP启动Linux内核。简单说就是通过网线,通过网络将linux启动需要的镜像文件和设备树文件从Ubuntu发送到开发板。Ubuntu主机作为 TFTP服务器。因此需要在 Ubuntu上搭建 TFTP服务器。
在Ubuntu下输入如下:

sudo apt-get install tftp-hpa tftpd-hpa
sudo apt-get install xinetd

TFTP需要一个文件夹来存放文件,先在用户根目录下创建一个名为“ linux”的文件夹,再在linux目录下新建一个目录tftpboot,命令如:

cd
mkdir linux
cd linux
mkdir tftpboot

这里的/wangyongyang/是作者的用户名,根据自己的用户名不同进行更改。

该文件夹需要权限,通过以下命令给予权限:

chmod 777 /home/wangyongyang/linux/tftpboot

还需要配置 tftp,需要在目录/etc/xinetd.d下新建文件 tftp,没有这个目录的话需要自己创建。

cd
touch /etc/xinetd.d/tftp
vim tftp

输入以上命令后,进入编辑模式,输入以下内容:(记得修改用户目录)

server tftp
{
	socket_type = dgram
	protocol = udp
	wait = yes
	user = root
	server = /usr/sbin/in.tftpd
	server_args = -s /home/wangyongyang/linux/tftpboot/
	disable = no
	per_source = 11
	cps = 100 2
	flags = IPv4
}

/home/wangyongyang/linux/tftpboot/ 是新建的tftpboot文件的目录,记得修改成读者自己的。
然后,启动tftp服务。

sudo service tftpd-hpa start

再继续配置tftp。打开 /etc/default/tftpd-hpa文件,在原有的内容上修改为以下内容:

# /etc/default/tftpd-hpa
TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/home/wangyongyang/linux/tftpboot"
TFTP_ADDRESS=":69"
TFTP_OPTIONS="-l -c -s"

最后输入命令, 重启 tftp服务器:

sudo service tftpd-hpa restart

到这里,TFTP服务器我们就搭载好了,通过网络传输文件,一定得配置ip地址。
那么后面还有网络ip的相关配置。

②NFS配置

我们选择通过NFS挂载根文件系统。是为了方便直接在Ubuntu上操作调试文件。
使用如下命令安装 NFS服务:

sudo apt-get install nfs-kernel-server rpcbind

NFS同样需要一个文件夹来存放文件,也在linux目录下新建一个目录nfs,命令如:

cd
cd linux
mkdir nfs

配置nfs,使用如下命令打开 nfs配置文件 /etc/exports

sudo vi /etc/exports

输入你的用户密码,之后在打开 /etc/exports文件最后面添加一行如下所示内容:

/home/wangyongyang/linux/nfs *(rw,sync,no_root_squash)

然后,重启NFS服务:

sudo /etc/init.d/nfs-kernel-server restart

到这里,NFS服务就开启成功了。

另外,插入一个可能存在的问题解决
---------------------------如果使用的是ubuntu16.04之后版本,使用nfs时会报错------问题解决----------------------------------
NFS配置没问题:NFS服务开启与使用、目录—挂载与卸载
使用 NFS 挂载文件系统的时候报错:

原因:
以前使用的16.04版本的ubuntu没问题,现在更换了18.04.4版本会报错。因为从Ubuntu17.04开始,nfs默认只支持协议3和协议4,而kernel中默认支持协议2。

如果是高版本,按照以下命令继续
修改 NFS 配置文件:

sudo vim /etc/default/nfs-kernel-server

在末尾添加:

RPCNFSDOPTS="--nfs-version 2,3,4 --debug --syslog"

重启 NFS 服务:

sudo /etc/init.d/nfs-kernel-server restart

③串口助手

串口助手用作我们开发板系统的终端。
配置启动的环境变量和操作命令的输入都在串口助手中进行。
串口助手的软件大家自行选择自己习惯使用的,这里推荐SecureCRT(收费|试用30天)和MobaXterm(免费)。

④FileZilla

在开发的过程中会频繁的在 Windows和 Ubuntu下进行文件传输。
FileZilla就是两个不同的系统之间传输文件的媒介,我们要在windows中下载并安装这个软件。
Windows和 Ubuntu下的文件互传我们需要使用 FTP服务,在ubuntu下输入一些命令进行配置:
安装 FTP服务:

sudo apt-get install vsftpd

配置:

sudo vim /etc/vsftpd.conf

通过vim编辑器打开 vsftpd.conf文件 以后找到如下两行:

local_enable=YES
write_enable=YES

确保上面两行没有被注释掉,也就是前面没有 “#”。
重启FTP服务:

sudo /etc/init.d/vsftpd restart

FileZilla的下载安装:
官网下载链接:FileZilla下载
客户端要连接到服务器上。点击文件—站点管理器。
在这里插入图片描述
站点管理器中跟我一样配置,主机(H):的ip地址要填ubuntu上的ip。
在ubuntu中输入命令ifconfig可以查看ubuntu的主机ip。

ifconfig

在这里插入图片描述
防止中文乱码,将站点管理器里的字符集改成UTF-8。
在这里插入图片描述
连接成功后如下:
在这里插入图片描述
至此,准备工作完成,我们就可以正式启动系统了。
后面配置网络ip的时候,可能主机ip会有变动,FileZilla有可能会连不上,没关系,如果遇到这个问题,到时候使用igconfig命令查看,再更改就是了。

三、正式启动

1、提供所需文件

启动需要的mfgtool、linux内核镜像文件zImage、设备树文件.dtb、根文件系统。这些所需资源我都整理和调试后放在云盘中,有需要的自行下载。当然,有能力的读者自行移植的这些文件,若能兼容则更好。
链接:https://pan.baidu.com/s/1ilt6Fa3FCoYYjHZPu7Ghmg?pwd=0621
提取码:0621
在这里插入图片描述

2、U-Boot的烧录

Linux系统要启动就必须需要一个 bootloader程序,也就说芯片上电以后先运行一段bootloader程序。U-Boot(Universal Boot Loader)就是一个bootloader软件。也有一些其他的软件。既然是入门,我们一切从简,这个通过uboot启动的过程,就不详细说了。直接按照步骤来。我们通过mfgtool工具来烧录uboot进开发板。
第一步:
从云盘中下载工具后解压。双击mfgtool,将mfgtool(study)复制到非中文路径下!!!
在这里插入图片描述
在这里插入图片描述
这一步很重要,最好磁盘名称也不要有中文,若所在路径包含中文可能会遇到如下问题:
在这里插入图片描述
第二步:
用usb数据线将开发板的USB_OTG接口连接至电脑上。
在这里插入图片描述
第三步:
拨码开关拨到 USB下载模式。并打开开发板的电源,若已经打开,按下复位键,以USB模式启动。
以一个USB设备连接到电脑时,电脑会叮咚一声,跟插入U盘一样的声音提示,有些电脑还会弹窗提示。
注意:(有的同学学过裸机开发,用SD卡下载程序,所以还需要检查一下不要有SD卡插入开发板)
在这里插入图片描述
第四步:
打开我提供的文件夹mfgtool,找到如下文件双击运行。
在这里插入图片描述
如已经放在非中文路径,将会成功打开,显示如下画面。
我们先不着急点击开始,开始之前打开串口助手以便观察烧写的过程。
在这里插入图片描述
第五步:
那么此时,我们将一根USB数据线将开发板的USB_TTL接口接在电脑上,用作串口。然后,我们打开一个串口助手(SecurCRT和MobaXterm都可以,后面会以MobaXterm为例),为了方便观察烧写uboot的进度和后续的环境变量的配置。
在这里插入图片描述
串口打开后显示这个样子。这个串口助手的使用非常简单。这里附上(原子)的教程图片。
在这里插入图片描述
在这里插入图片描述
选则串口,找到自己的ch340的com口(如下图,原子教程是com15,我的是com11),如果你没安装过ch340驱动:
下载链接:
  CH340/CH341
    Windows 驱动链接:https://www.wch.cn/download/CH341SER_EXE.html/
波特率配置成:115200
在这里插入图片描述
这就是打开串口之后的样子:
在这里插入图片描述
第六步:
mfgtool就可以点击start了。
在这里插入图片描述
等待下载完成后,点击stop。再点击exit退出即可。
在这里插入图片描述
第七步:
将开发板上的拨码开关拨到 EMMC启动模式。按下开发班上的复位按钮,就能够启动uboot。
在这里插入图片描述
第八步:
启动后,在倒计时结束之前,按下回车。进入uboot命令行模式。
(如果倒计时前不按回车就会启动linux内核,但由于我们还没配置好,也是无法启动的,这里我们要进行环境变量的配置。)
按下回车后,串口助手中会显示如下内容,这是打印的开发板的信息:
注:(我这是配置完成后的,与刚下载uboot后打印的信息略有出入,不重要,接着步骤往下进行即可)
在这里插入图片描述
这里的信息,关键的部分做一下简单的解释:

U-Boot 2016.03 (Jan 03 2023 - 12:08:37 +0800)

CPU: Freescale i.MX6ULL rev1.1 69 MHz (running at 396 MHz)
CPU: Industrial temperature grade (-40C to 105C) at 40C
Reset cause: POR
Board: ALIENTEK ALPHA EMMC
I2C: ready
DRAM: 512 MiB
MMC: FSL_SDHC: 0, FSL_SDHC: 1
Display: TFT7016 (1024x600)
Video: 1024x600x24
In: serial
Out: serial
Err: serial
switch to partitions #0, OK
mmc1(part 0) is current device
Net: FEC1
Normal Boot
Hit any key to stop autoboot: 0
=>

U-Boot 2016.03 (Jan 03 2023 - 12:08:37 +0800)uboot版本号和编译时间
CPU: Freescale i.MX6ULL rev1.1 69 MHz (running at 396 MHz)CPU是飞思卡尔的 I.MX6ULL I.MX, 频率为 69MHz,此时频率位 396MHz。
Board: ALIENTEK ALPHA EMMC 板子名称:ALIENTEK ALPHA EMMC(原子左盟主移植时起的名字)
I2C: ready I2C准备就绪
DRAM: 512 MiB当前板子的 DRAM(内存)为 512MB
MMC: FSL_SDHC: 0, FSL_SDHC: 1 正点原子的 I.MX6ULL EMMC核心板上 FSL_SDHC(0)接的 SD(TF)卡,FSL_SDHC(1)接的 EMMC。
Display: TFT7016 (1024x600) 显示屏型号:TFT7016 (1024x600)
In: serial Out: serial Err: serial 标准输入、标准输出和标准错误所使用的终端,都是串口 (serial)。
mmc1(part 0) is current deviceemmc的第 0个分区。
Error: FEC1 address not set.FEC1刚下载应该会显示这个,网卡地址没有设置。
Hit any key to stop autoboot: 0 倒计时提示,默认倒计时 3秒(可设置长短),倒计时结束之前按下回车键就会进入 uboot命令行模式。如果在倒计时结束以后没有按下回车键,那么 Linux内核就会启动, Linux内核一旦启动, uboot的作用就结束了。

3、网络配置

①网络环境搭建

需要用到:一根网线
连接方式:用网线将开发板的 ENET2接口和电脑连接起来, I.MX6U-ALPHA开发板有两个网口: ENET1和 ENET2,一定要连接 ENET2。
在这里插入图片描述

一般的同学应该都是在实验室使用自己的笔记本电脑,连接实验室的无线网进行学习。
那么,你属于【电脑WiFi 上网,开发板和电脑直连】可以放心跟着本教程一步一步操作。
如果你是【电脑和开发板直连同个路由器】或【电脑和开发板直连同个交换机】建议你先观看原子的网络配置教程。
我建议采用与我相同的网络搭建方式【电脑WiFi 上网,开发板和电脑直连】,可以无脑跟着教程走。
具体的网络拓扑结构如图,建议和我配置成一样的IP。
在这里插入图片描述

【傻瓜式步骤】:
在这里插入图片描述
这里要设置两个网络适配器。网上有很多帖子的解决方案有误导,只有一个网络适配器,为了虚拟机ubuntu–本地主机windows—开发板—虚拟机ubuntu其中一个ping通(ping指的是在两个网络点之间请求消息来确定目标主机是否可达),却牺牲了另一个网络点。

①一个适配器设置成桥接模式,让ubuntu作为服务器,供开发板连接。(IP可设定)
②一个适配器设置成NAT模式,用来主机和虚拟机传文件,和供虚拟机上网。(IP自动生成)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
点击管理员更改设置按钮后一般会出现vmnet0。
在这里插入图片描述
但如果你没有vmnet0
--------------------------------->跳转到【问题】执行步骤。
但如果你一切正常
--------------------------------->跳转到【正常】执行步骤。
【问题】应该是之前跟着别的帖子删掉了,建议手动添加,或者还原默认设置。

手动添加:
在这里插入图片描述
或还原默认设置:
在这里插入图片描述
【正常】如果你一切正常,就不需要还原默认,就继续下面的步骤。
开发板是通过一根网线接到电脑的网络接口上的,所以需要虚拟网络编辑器里的网络适配器1(即VMnet0)桥接到有线网卡上。
在这里插入图片描述
把vmnet0设置成桥接到realtek PCIe …。
在这里插入图片描述
在这里插入图片描述
配置好之后试是这样的。
要记住自己的VMnet8随机生成的地址:
比如我的是 子网地址 = 192.168.225.0
在这里插入图片描述
此时,还需要在ubuntu中继续相关设置。
打开ubuntu中的设置,点击网络,能够查看到两个有线网络。(我截图的是已经设置好的,略有区别没关系,继续操作)
在这里插入图片描述
点击“小齿轮”,设置按钮,可以查看详细信息,其中一个网络是NAT模式的网络适配器2,已经自动分配好IP了。
可以看到此时的IP = 192.168.225.128
细心的同学观察到,这与VMnet8随机生成的地址 子网地址 = 192.168.225.0在同一个网段下的。
这样就是没问题的。
在这里插入图片描述
另一个也点击“小齿轮”,设置按钮,可以查看详细信息,这个还没有设置IP的,是我们的网络适配器1,用来桥接到开发板和Windows的。
所以我们需要手动设置一个IP。
在这里插入图片描述
按照我一样的设置。
将地址设置为:IP = 192.168.10.100
子网掩码设置为:子网掩码 = 255.255.255.0
记者这个IP地址和子网掩码,这个IP地址就是
在这里插入图片描述
配置好之后点击应用,并打开这两个有线网络,使其配置生效。

在这里插入图片描述
到这里ubuntu对网络的设置已经完成了。

现在设置Windows的以太网IP的相关信息。在Windows主机打开控制面板 -> 网络和 Internet -> 查看网络状态->更改适配器设置,找到以太网。右键更改属性。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
将以太网的地址设置:IP = 192.168.10.200
要和虚拟机的VMnet0在同一网段。

如此一来,
ubuntu用来和windows互传以及上网的虚拟网卡 IP = 192.168.255.128
ubuntu的地址为 IP = 192.168.10.100
本地主机的以太网 IP = 192.168.10.200
还需要将开发板是IP地址也设置到同一网段下,才能实现相互ping通。完成我们想要的网络环境搭建。
我们先预想,将开发板的地址设置为: IP = 192.168.10.50
那么开发板的IP如何进行配置呢,这就需要在前面按下回车之后进入的uboot命令行中进行设置了。
所以下一步:在uboot命令行中进行环境变量设置。

②环境变量设置

回到这个界面。
输入

print

按下回车后,可以显示当前默认的环境变量。
在这里插入图片描述
下面显示的是我配置了几个环境变量之后print打印的所有环境变量信息,未配置显示的没有这么多,没关系,我们后面傻瓜式配置。

在这里插入图片描述
先说一下简单命令的使用方法:(不然有人写错都不知道怎么改)
设置环境变量:

setenv+环境变量名字+变量值

写错了,想删除环境变量:

setenv+环境变量名字+(不写变量值)

保存环境变量的设置:

saveenv

接下来,跟着一步一步设置:
/home/wangyongyang/linux/nfs/rootfs记得更改成自己nfs的目录。

setenv bootargs 'console=ttymxc0,115200 root=/dev/nfs rw nfsroot=192.168.10.100:/home/wangyongyang/linux/nfs/rootfs ip=192.168.10.50:192.168.10.100:192.168.10.1:255.255.255.0::eth0:off'

解释一下:
这是配置根文件系统系统的。设置为从NFS服务器挂载(也就是直接把ubuntu里面的那个)。

setenv bootcmd 'tftp 80800000 zImage;tftp 83000000 imx6ull-alientek-emmc.dtb;bootz 80800000 - 83000000;'

setenv ethaddr b8:ae:1d:01:00:01这里使用同一个路由器的如果有同学也用的原子的开发板,可能会有冲突,可以设置成最后来两位改成05/04/08等等都行。

setenv ipaddr 192.168.10.50
setenv ethaddr b8:ae:1d:01:00:01
setenv gatewayip 192.168.10.1
setenv netmask 255.255.255.0
setenv serverip 192.168.10.100

看到这几个IP地址应该有点印象。
简单解释一下:
setenv ipaddr 192.168.10.50 开发板的IP地址
setenv ethaddr b8:ae:1d:01:00:01 MAC
setenv netmask 255.255.255.0 子网掩码
setenv gatewayip 192.168.10.1 网关地址
setenv serverip 192.168.10.100 服务器的IP(Ubuntu的IP)

saveenv

重启一下,或者按下复位键,倒计时结束前按一下回车,我们检查一下是否写成功了。
ping一下服务器试一下:

ping 192.168.10.100

在这里插入图片描述
ping主机需要关闭主机防火墙,我使用的是公网就不方便关闭了。

还要测试一下,ubuntu和windows通过FileFilla传输文件是否可行:
打开FileFilla,这个时候站点的IP地址应该修改为:ubuntu用来和windows互传以及上网的虚拟网卡 IP = 192.168.255.128
在这里插入图片描述
在这里插入图片描述
连接成功了,ubuntu和windows之间的互通就没有问题了。

那么,到这网络配置就可以了,已经可以实现使用TFTP和NFS来挂载内核、设备树和文件系统了。

4、放置内核、设备树文件、根文件系统

从网络启动 linux系统的唯一目的就是为了方便调试以及熟悉网络配置的过程。
不管是为了调试 linux系统还是 linux下的驱动。每次修改 linux系统文件或者 linux下的某个驱动以后都要将其烧写到 EMMC中去测试,这样太麻烦了。我们将 linux镜像文件、设备树文件和根文件系统都放到 Ubuntu下某个指定的文件夹中,这样每次重新编译 linux内核或者某个 linux驱动以后只需要使用 cp命令将其拷贝到这个指定的文件夹中即可,这样就不用需要频繁的烧写 EMMC这样就加快了开发速度。
所以--------------我们可以通过 TFTP从 Ubuntu中下载 zImage和设备树文件,根文件系统通过 NFS挂载。

那么,接下来,我们先将我提供的文件夹里的:

imx6ull-alientek-emmc.dtb设备树文件】
zImage系统镜像文件】
rootfs.tar.bz2根文件系统的压缩包】
通过FileFilla传到我们的ubuntu中。
在这里插入图片描述
在这里插入图片描述
将这两个放在tftpboot目录下:(在ubuntu中要给予这两个文件权限)
在这里插入图片描述
进入到这个目录并给与这两个文件权限,输入命令:

cd linux/tftpboot
chmod 777 imx6ull-alientek-emmc.dtb
chmod 777 zImage

在这里插入图片描述

将这个放在nfs目录下:(这是个压缩包,我们需要在ubuntu中使用命令进行解压)
在这里插入图片描述
解压输入命令:

cd linux/nfs
tar -vxjf rootfs.tar.bz2

在这里插入图片描述
完成之后,我们就拥有了linux系统启动所需要的:

imx6ull-alientek-emmc.dtb设备树文件】
zImage系统镜像文件】
rootfs根文件系统】
---------------------------------------------------------------------->
imx6ull-alientek-emmc.dtb设备树文件】这两个通过TFTP被下载到开发板的 DRAM中,启动linux系统。
zImage系统镜像文件】
---------------------------------------------------------------------->
rootfs根文件系统】这个则通过NFS挂载根文件系统,即不需要本地的根文件系统,而是通过网络直接从NFS服务器启动。实际上是利用ubuntu作为NFS服务器来提供内核和根文件系统的NFS共享。类似于共享文件夹。

接下来就可以正式启动内核了。

5、启动Linux内核

我们再次按下开发板的复位键,这次不用在倒计时结束前按回车,直接等待他自己启动。
一切按照流程顺利操作的话,应该会显示如下内容:
在这里插入图片描述
等待他启动完成后,会显示让你按下回车:
我们按下回车后,就像看到了ubuntu下操作linux系统一样。
输入:

ls

能看到,这就是根文件系统的所有文件夹。
(这里的根文件系统是原子提供的移植后的,比自己一步一步移植的文件要多)用来学习教程。
在这里插入图片描述
到这里,我们的linux系统就能够通过网络完成启动了,接下来是编写一个LED的驱动和简单的应用APP。

四、字符设备驱动开发

1、字符设备简介

Linux系统将设备划分为:字符设备、块设备、网络设备
字符设备驱动
– 字符设备驱动适用于那些以字符流(一个一个字节)方式进行数据传输的设备。操作这些设备时,数据是一个字符一个字符地被处理的。
– 举例:LED、键盘、串口、IIC等
块设备驱动
– 块设备驱动适用于那些以块为单位进行数据传输的设备。每次操作都是以块(通常是512字节或更大的多倍数)为单位。
– 举例:硬盘、SSD
网络设备驱动
– 网络设备驱动适用于那些处理网络数据传输的设备。它们专门处理网络接口的数据包。
– 举例:网卡(Network Interface Card, NIC)

2、LED驱动开发程序编写

①编译Linux内核源码

学过STM32的同学都知道,在编写驱动的时候,通常要调用官方的API函数,需要包含很多固件库提供的头文件。
编写Linux驱动开发的程序,自然也是类似,那么这些头文件来自于哪里呢--------由Linux内核源码编译得到。
我将在以下链接中提供Linux-4.1.15版本的源码(已经过原子移植)。
链接:Linux内核源码
提取码:0621
下载完成后,得到一个压缩文件linux-imx-rel_imx_4.1.15_2.1.1_ga_alientek_v2.2.tar.bz2
在这里插入图片描述
将该文件使用FileFilla传输到ubuntu中。
放在ubuntu的哪个位置呢,为了保持和我操作的一致性,建议在ubuntu用户目录下创建目录:

cd
cd linux
mkdir IM6ULL
cd IM6ULL
mkdir linux
cd linux

此时就创建好了目录
pwd看一下当前目录是否正确:

pwd

得到:(你自己的用户名)

/home/wangyongyang/linux/IM6ULL/linux

然后将linux-imx-rel_imx_4.1.15_2.1.1_ga_alientek_v2.2.tar.bz2使用FileFilla传输到ubuntu的该目录中。
接下来解压和编译:

tar -vxjf linux-imx-rel_imx_4.1.15_2.1.1_ga_alientek_v2.2.tar.bz2
sudo apt-get install lzop
make clean
make imx_v7_mfg_defconfig
make -j16

按照傻瓜式指令,一步一步等待完成。
接下来就可以开始编写驱动代码了。

②VS Code安装

为了方便代码的编写,我们在ubuntu下安装vscode作为代码编辑器。
具体的安装步骤可以参考别人的贴子:
链接:Ubuntu安装VS Code的三种方式
VS Code可以安装插件,根据原子哥提供的 必备插件列表,我们安装以下插件:
1)、 C/C++,这个肯定是必须的。
2)、 C/C++ Snippets,即 C/C++重用代码块。
3)、 C/C++ Advanced Lint,即 C/C++静态检测 。
4)、 Code Runner,即代码运行。
5)、 Include AutoComplete,即自动头文件包含。
6)、 Rainbow Brackets,彩虹花括号,有助于阅读代码。
7)、 One Dark Pro VSCode的主题。
8)、 GBKtoUTF8,将 GBK转换为 UTF8。
9)、 ARM,即支持 ARM汇编语法高亮显示。
10)、 Chinese(Simplified),即中文环境。
11)、 vscode-icons VSCode图标插件,主要是资源管理器下各个文件夹的图标。
12)、 compareit,比较插件,可以用于比较两个文件的差异。
13)、 DeviceTree,设备树语法插件。

③编写驱动代码和测试APP代码

首先创建工程。
先创建存放工程的目录:(为了保持一致性)

cd linux/IM6ULL
mkdir Drivers
cd Drivers
mkdir Linux_drivers
cd Linux_drivers
mkdir LED_TEST

然后在ubuntu下打开vscode下,打开文件夹
选择LED_TEST:
在这里插入图片描述
然后–文件–将工作区另存为:
在这里插入图片描述
选择刚才的空文件夹,命名,保存。
在这里插入图片描述
就变成了这样:
在这里插入图片描述
新建两个文件:led.c和ledapp.c
在这里插入图片描述
然后我们需要在 VSCode中添加 Linux源码中的头文件路径。打开 VSCode,按下 Crtl+Shift+P”打开 VSCode的控制台,然后输入C/C++: Edit configurations(JSON) ”,打开 C/C++编辑配置文件:
在这里插入图片描述
点击后默认是这个样子:
在这里插入图片描述
我们要改成:(复制粘贴,记得将用户名改成自己的)

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**",
                "/home/wangyongyang/linux/IM6ULL/linux/include", 
                "/home/wangyongyang/linux/IM6ULL/linux/arch/arm/include", 
                "/home/wangyongyang/linux/IM6ULL/linux/arch/arm/include/generated/"
            ],
            "defines": [],
            "compilerPath": "/usr/bin/clang",
            "cStandard": "c11",
            "cppStandard": "c++17",
            "intelliSenseMode": "clang-x64"
        }
    ],
    "version": 4
}

配置好之后,准备编写led.c。

在这里插入图片描述
我们先直接复制粘贴实现功能,再思考为什么。
慢慢解释代码和详解字符设备驱动开发的具体步骤。

led.c

#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 <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

/**********************************************************
Copyright © WangYongyang Co., Ltd. All rights reserved.
文件名:	newchrled.c
作者:	汪永阳
版本:	v1.0
描述:	字符设备驱动开发__点亮LED(来源:正点原子)
日志:	v1.0 2024/5/27 汪永阳创建
************************************************************/

#define LED_WYY_CNT 1		  	/* 设备号个数 */
#define LED_WYY_NAME "led_wyy"
#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;

struct led_wyy_dev{
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;		/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
};

struct led_wyy_dev led_wyy;	
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);
	}	
}

static int led_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &led_wyy; 
	return 0;
}

static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

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);
	} else if(ledstat == LEDOFF) {
		led_switch(LEDOFF);	
	}
	return 0;
}

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

static struct file_operations led_wyy_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.read = led_read,
	.write = led_write,
	.release = 	led_release,
};



static int __init led_init(void)
{
	u32 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);
	val &= ~(3 << 26);	
	val |= (3 << 26);	
	writel(val, IMX6U_CCM_CCGR1);

	writel(5, SW_MUX_GPIO1_IO03);

	writel(0x10B0, SW_PAD_GPIO1_IO03);

	val = readl(GPIO1_GDIR);
	val &= ~(1 << 3);	
	val |= (1 << 3);	
	writel(val, GPIO1_GDIR);

	val = readl(GPIO1_DR);
	val |= (1 << 3);	
	writel(val, GPIO1_DR);

	if (led_wyy.major) {
		led_wyy.devid = MKDEV(led_wyy.major, 0);
		register_chrdev_region(led_wyy.devid, LED_WYY_CNT, LED_WYY_NAME);
	} else {
		alloc_chrdev_region(&led_wyy.devid, 0, LED_WYY_CNT, LED_WYY_NAME);
		led_wyy.major = MAJOR(led_wyy.devid);	
		led_wyy.minor = MINOR(led_wyy.devid);	
	}

	printk("led_wyy major=%d,minor=%d\r\n",led_wyy.major, led_wyy.minor);	
	led_wyy.cdev.owner = THIS_MODULE;
	cdev_init(&led_wyy.cdev, &led_wyy_fops);
	cdev_add(&led_wyy.cdev, led_wyy.devid, LED_WYY_CNT);
	led_wyy.class = class_create(THIS_MODULE, LED_WYY_NAME);
	if (IS_ERR(led_wyy.class)) {
		return PTR_ERR(led_wyy.class);
	}
	led_wyy.device = device_create(led_wyy.class, NULL, led_wyy.devid, NULL, LED_WYY_NAME);
	if (IS_ERR(led_wyy.device)) {
		return PTR_ERR(led_wyy.device);
	}
	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);
	
	cdev_del(&led_wyy.cdev);
	unregister_chrdev_region(led_wyy.devid, LED_WYY_CNT);
	device_destroy(led_wyy.class, led_wyy.devid);
	class_destroy(led_wyy.class);
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("wangyongyang");
MODULE_DESCRIPTION("A simple Linux char driver");
MODULE_VERSION("1.0");

ledapp.c

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

/**********************************************************
Copyright © WangYongyang Co., Ltd. All rights reserved.
文件名:	xxx
作者:	汪永阳
版本:	v1.0
描述:	LED_APP_test
日志:	v1.0 2024/5/27 汪永阳创建
************************************************************/

#define LEDOFF 	0
#define LEDON 	1

int main(int argc, char *argv[])
{
	int fd, retvalue;
	char *filename;
	unsigned char databuf[1];
	
	if(argc != 3){
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];

	fd = open(filename, O_RDWR);
	if(fd < 0){
		printf("file %s open failed!\r\n", argv[1]);
		return -1;
	}

	databuf[0] = atoi(argv[2]);

	retvalue = write(fd, databuf, sizeof(databuf));
	if(retvalue < 0){
		printf("LED Control Failed!\r\n");
		close(fd);
		return -1;
	}

	retvalue = close(fd); 
	if(retvalue < 0){
		printf("file %s close failed!\r\n", argv[1]);
		return -1;
	}
	return 0;
}

需要编辑的文件就这两个,一个是驱动代码,一个是用户空间的测试APP。
然后,我们需要编译,为了方便编译,我们编写一个Makefile。
在这里插入图片描述
Makefile
(记得修改用户目录)

KERNELDIR :=/home/wangyongyang/linux/IM6ULL/linux
CURRENT_PATH := $(shell pwd)

obj-m := led.o

build: kernel_modules

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

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

不知道什么是Makefile或者想弄懂的可以看我以往的帖子:
链接:一个简单的Makefile案例
保存。OK,可以开始编译了,跟着我输入指令。

编译成“ led.ko”的驱动模块文件

make -j32

在编译项目时,输入 make -j32 中的 -j32 是一个选项,用来指定 make 命令并行执行的最大任务数量。32:紧随 -j 之后的数字,表示可以并行执行的任务的数量,32 表示最多可以同时运行32个任务。

编译用户空间的测试APP。

arm-linux-gnueabihf-gcc ledapp.c -o ledapp

编译成功后,左侧生成的文件应该有这些。
在这里插入图片描述

3、最后的测试–点亮LED

首先,将编译生成的这两个文件led.ko ledapp复制到根文件系统下模块文件夹中。(没有这个文件夹就需要自己创建modules/4.1.15/)
在当前目录下输入命令:

sudo cp led.ko ledapp  /home/wangyongyang/linux/nfs/rootfs/lib/modules/4.1.15/ -f

在这里插入图片描述

接下来,回到我们的串口。接下来的操作和指令都是在开发板上进行的,在串口终端输入命令。
在这里插入图片描述
在串口终端中,查看这个目录下面有没有成功复制进这两个文件:

ls
cd lib/modules/4.1.15
ls

发现,已经有这两个文件:
在这里插入图片描述
第一步:第一次加载驱动,先运行一下管理模块的组件:

depmod

输入该命令后,生成了三个文件 modules.alias、modules.dep 和 modules.symbols
在 Linux 系统中,modules.alias、modules.dep 和 modules.symbols 这些文件是由 depmod 命令生成的。它们是内核模块系统的一部分,用于管理和加载内核模块。
modules.alias:这个文件包含了模块的别名信息,通常用于设备驱动程序的自动加载。它映射了设备 ID 到对应的内核模块。
modules.dep:这个文件包含了模块的依赖关系。在加载一个模块时,系统会根据这个文件来确定还需要加载哪些其他模块。
modules.symbols:这个文件包含了模块导出的符号信息。它主要用于模块之间的符号解析和依赖管理。
在这里插入图片描述

第二步:加载驱动:

modprobe led.ko

自动分配了设备号主:249,次:0
在这里插入图片描述

第三步(确保正确的一步,可省略):输入如下命 令查看 /dev/led这个设备节点文件是否存在:(我们是自动分配设备号)

cd
ls /dev/led_wyy -l

在这里插入图片描述

第四步(确保正确的一步,可省略):输入“ lsmod”命令即可查看当前系统中存在的模块

lsmod

在这里插入图片描述
能够查看到led这个模块了。

第五步(确保正确的一步,可省略):输入如下命令查看当前系统中有没有目标设备(查看设备号):

cat /proc/devices

在这里插入图片描述
找到了,自动分配的设备号249。
第六步:接下来测试:
在这里插入图片描述

开灯:

./ledapp /dev/led_wyy 1

大家最喜欢的点灯,亮了!
请添加图片描述

关灯:

./ledapp /dev/led_wyy 0

请添加图片描述

该驱动使用完了,现在不需要了,还可以卸载。
卸载驱动输入如下命令:

rmmod led.ko

我们在一个Cortex A7的板子的 linux系统上就行的 完整的点灯过程就完成了。
接下来详细的了解字符设备驱动开发的步骤,了解完后,再进行代码的详解,就更容易理解了。

五、字符设备驱动开发步骤总结

Ⅰ、字符设备驱动开发程序编写步骤:
一、定义字符设备结构体(包含当前设备信息)
二、定义文件操作函数及其结构体
①定义文件操作函数
②定义文件操作函数结构体
三、注册字符设备(模块入口部分函数)
①分配设备号
a、静态分配设备号(主动定义)
b、动态分配设备号(申请自动分配)
②初始化字符设备(初始化字符设备结构体并绑定文件操作函数)
③注册字符设备到内核
④创建字符设备类
⑤在该类下创建字符设备节点
四、注销字符设备部分(模块出口部分函数)
①从内核中删除字符设备
②注销设备号
③销毁设备节点
④销毁设备类
五、模块的入口出口函数绑定
①module_init(模块入口部分函数)
②module_exit(模块出口部分函数)
六、结尾处添加许可证|作者信息|描述|版本信息

Ⅱ、测试APP程序编写步骤:
一、检查参数数量是否正确
二、获取设备文件名
三、打开设备文件
四、将控制命令写入设备文件
五、关闭设备文件

完整的测试过程:
Ⅰ、字符设备驱动开发程序编写步骤
Ⅱ、测试APP程序编写步骤
Ⅲ、编写Makefile编译驱动和APP
Ⅳ、在开发板中加载驱动
Ⅴ、使用APP测试驱动功能
Ⅵ、卸载驱动

本次入门教程的内容就到此结束了。
我们实现了通过TFTP启动Linux内核和NFS挂载根文件系统,编写了一个简单的字符设备驱动-LED驱动,并且编写了一个简单的用户测试APP,来使用我们编写的驱动,尝试点亮led成功。
驱动和测试APP编写的代码详解放在我的下一个帖子。
链接:嵌入式Linux_字符设备驱动开发_驱动开发案例_LED编写代码详解(直接操作寄存器版)

本人也是小白入门,纯手码教程作为笔记,感谢大家的观看,欢迎大家在评论区纠错和提出问题。

  • 29
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

善于伴随

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值