Android驱动基础知识


注意:本网站所有内容为自己学习期间记录的笔记,很多都是原作者那边摘抄过来的,不作商业用途也不插播广告。(如有侵权,请联系我删除,15909440083)

作者:Gao秋

Gyang

git常用命令,作者:少糖加水

【2024最新Linux驱动开发教程】

一、Android驱动开发基础知识

1. adb 常用好用的几个命令

1、adb devices						 # (----比较常用----)查看设备列表0000
查看电脑已连接Android 设备列表,多行显示表示连接多个Android设备,每行前面的字符串表示Android设备的SN号

2、adb  get-state 					 # 获取连接状态 device offline unkown

3、adb install -r APK路径 			#  安装应用(----比较常用----)
# apk文件所在的文件路径,包括apk,如D:/hello.apk,注意需要打开的cmd路径不要带有中文,否则,部分电脑可能会提示安装失败

4、adb uninstall 应用包名,应用卸载   查看自己安装的包名,adb uninstall -k hello.apk 下载应用,但保留缓存数据

5、adb connect 设备ip地址,			# 如果电脑与设备在同一局域网内,Android设备的连接ip 地址,可连接成功

6、adb install -r  APK路径 安装应用

7、adb push 电脑上的文件 路径 sdcard/  将电脑的文件输入到手机上
	如文件D:/hello.apk,    adb push D:/hello.apk  /sdcard、hello.apk
	
8、adb pull  /sdcard/文件路径 指定的pc目录,将文件拷贝到pc,# 文件保存的路径直接与cmd打开的路径相同(----比较常用----)

9、adb reboot 或加参数-p设备进行重启

10、adb shell pm clear 包名 清除应用数据
11、adb shell am force-stop 包名 关闭应用

12、adb shell screencap /sdcard/screen.png  截屏幕

13、adb shell screenrecord /sdcard/hello.mp4 屏幕视频录制,测试时可使用

14、adb logcat 抓取日志
# 如果要过滤日志,可通过adb logcat | findstr "输入过滤的内容"

15、adb shell wm size 查看屏幕大小

16、 adb shell getprop查看配置信息
# 如:adb shell getprop ro.build.version.sdk 查看api版本

17、adb shell input keyevent 4 相当于返回键,返回上一页

18、adb shell df 查看手机存储信息

19、adb shell  pm disable-user com.android.launcher3 禁用系统应用

20、 adb shell pm enable com.android.launcher3 启用系统 ,需要root权限
21、adb start-server | kill-server //启动或关闭adb服务进程

22、adb shell cat /sys/class/net/wlan0/address   //获取mac地址

23、adb shell monkey -p 包名 --throttle 100 --ignore-crashes --ignore-timeouts --ignore-security-exceptions --ignore-native-crashes --monitor-native-crashes -v -v -v –s 1540475754297 100  monkey测试

24、adb shell ls /system/bin  查看当前设备可以使用的所有命令

25、adb shell input text "www.baidu.com",在编辑的文本框中输入编辑文字

26、adb shell svc wifi enable | disable   打开或关闭wifi

27、adb shell svc data enable |disable 打开或关闭移动网络

28、adb shell input swipe 760 500 600 320 点击屏幕,根据实际坐标点击

29、adb shell mkdir   /sdcard/创建目录

30、cat /proc/cpuinfo // 查看CPU信息,如果要看内存,则cat/proc/meminfo
31、cat /data/misc/wifi/*.conf 查看wifi密码,需要root权限

32mount -o remount,rw / 当root权限之后,仍提示file system read only时,先执行adb root ,接着执行adb remount, mount -o remount,rw / 然后执行该指令

33(1)setprop service.adb.tcp.port 5555 (2)stop adbd (3)start adbd 使用wifi连接失败时,

34、adb shell dumpsys activity | findstr “mFocus” 获取当前的activity 

35、adb shell settings put global policy_control immersive.full=*

36、仅隐藏状态栏:adb shell settings put global policy_control immersive.status=*

37、仅隐藏虚拟键:adb shell settings put global policy_control immersive.navigation=*

38、恢复:adb shell settings put global policy_control null

39、完全隐藏  adb shell wm overscan 0,0,0,0

40、旋转屏幕 adb shell content insert --uri content://settings/system --bind name:s:user_rotation --bind value:i:1
41、设置输入法  adb shell ime enable  com.iflytek.inputmethod/.FlyIME
    adb shell ime set com.iflytek.inputmethod/.FlyIME

42、Android tcp抓包  tcpdump -C 10 -i any -w /sdcard/capture.pcap
    -C 10 是指单个文件10MB
    -i any 是所有端口
    -w xxx 是存储路径

43、制作的bootanimation 压缩,注意需要将图片至于part0上,使用时在新建目录:mkdir makeLogo
放入文件:desc.txt文件和part0文件,part0存放的全是图片
执行指令:zip -r -X -Z store bootanimation part*/* desc.txt
导出  bootanimatio.zip

44、dumpsys iponesubinfo
service call iphonesubinfo 1 //查看iccid

11、adb shell pm list packages -3 		# 查看第三方安装的应用包名,卸载应用前,一般可通过该指令查看包名

1 重启

adb reboot					   # 正常重启						
adb reboot bootloader			# 重启到 bootloader (刷机模式)		用fastboot命令开始针对某个分区刷机
adb reboot recovery				# 重启到 recovery (恢复模式)		   
adb reboot edl				    # 进入9008模式整机刷机		

2 安装APK

adb install xxx.apk				# 安装
adb install -r xxx.apk			# 覆盖安装(保留缓存和数据)

3 抓NTC数据

adb root
adb shell 
cd sdcard
sh batteryCheck &
导出log:adb pull /sdcard/power_info C:\Users\dell\Desktop\battery_log"

bat.sh是丘工编好的批处理, 先把bat.sh放到sdcard根目录:adb push C:\Users\dell\Desktop\APK\batteryCheck.sh /sdcard

注意:不能带中文的文件夹路径 加&表示后台运行

4 Nibiru软件抓log

adb shell setprop persist.nibiru.logcat.flag 0 

Nibiru的3D软件默认没有打开log,需要手动打开

5 设置永不休眠

adb shell 
settings put system screen_off_timeout 999999999

6 Nibiru软件查看渠道号

adb shell getprop ro.build.version.vr.level

7 Nibiru 3D软件获取root权限

adb shell setprop nibiru.root.allowed 1
adb root
adb diasble-verity
adb reboot
adb shell setprop nibiru.root.allowed 1
adb root
adb remount

8 获取SN号

adb shell
getprop ro.serialno 

9 抓FPS

adb shell dumpsys SurfaceFlinger

10 检查电池

adb shell dumpsys battery

11 VR901 截图命令

adb shell "setprop debug.atw.capture 1"
adb pull /sdcard/disp-1.bmp d:\

数字1是截图文件的命名,多次导出每次数字要不一样,而且导出时需要选择对应的数字

12 导出系统APP的adb 命令

adb root
adb remount
adb shell
cd data
cd app
ls
cd 包名(例如:com.ktcp.vidio-1)
ls
exit
adb pull /data/app/com.ktcp.video-1/base.apk D:/123
adb shell dumpsys SurfaceFlinger

13 adb 关机指令

adb shell reboot -p

14 adb服务

adb start-server		# 开启服务 
adb kill-server			# 关闭服务

15 查看系统当前内存使用情况

adb shell cat /proc/meminfo

16 删除文件

adb shell
rm -rf
adb shell
cd /sdcard/
ls
rm -rf testreport.txt(文件名)

17 打印log

adb logcat					# 上层log:
adb shell dmesg				# 底层log:

18 push文件

adb root
adb remount
adb push

adb push "D:\temporary\test tool\4K生态微观50mbps.mp4" /sdcard/ 将D:\temporary\test tool\目录下的4K生态微观50mbps.mp4文件推送至设备本地/sdcard/根目录

19 从设备本地pull文件

adb root
adb remount
adb pull

adb pull /sdcard/emdoor/testreport.txt C:\Users\dell\Desktop\0522 将设备本地/sdcard/emdoor/目录下的testreport.txt文件拉取到C:\Users\dell\Desktop\0522目录下

20 2D截图

adb shell screencap -p 
adb shell screencap -p /sdcard/screen.png  #(保存路径)

21 3D截图

adb shell /system/bin/screencap -p 
adb pull /sdcard/screenshot.png F:\\mvp

adb shell /system/bin/screencap -p /sdcard/screenshot.png		  #(保存路径)
adb pull /sdcard/screenshot.png F:\\mvp							#(电脑保存路径)"

22 录屏

adb shell screenrecord 
adb shell screenrecord /sdcard/demo.mp4 	#(保存路径)

23 连接WIFI adb,不占用usb口

adb tcpip 5555				# PC端和安卓端连接同一个WIFI地址,cmd输入

打开Vysor设置IP连接(连接成功之前不能拔掉连接线,或者使用adb命令:adb connect 设备IP

注意:首先必须测试机连接电脑,先用adb tcpip 5555打开5555端口,然后再用adb connect 设备IP即可

24 查看ufs内存

adb shell
df

25 PC连接多台设备时指定某台设备

adb devices 
adb -s (加要连接设备名称,加要执行的命令)

adb devices (获取设备名称)
14121578
adb -s 14121578 shell

26 替换开机动画

adb root
adb remount
adb push C:\Users\dell\Desktop\bootanimation\bootanimation.zip /system/media/"

bootanimation是在动画文件界面压缩,选择ZIP格式,存储方式

27 查看屏幕分辨率

adb shell wm size

28 抓实时电压

adb shell
> while true
> do
> dumpsys battery|grep voltage
> sleep 1
> done"xxxxxxxxxx "adb shell> while true> do> dumpsys battery|grep voltage> sleep 1> done"adb shelldf

进入adb shell 进入死循环 获取电池信息|greg 电压/电流/温度等 PS:如果需要同时抓几组参数,就多打一行指令 每秒抓一次数据 done代表以上命令输入完成

电压:voltage 百分比:level 温度:temperature

29 查看主板SN

adb root
adb shell cat /mnt/vendor/persist/snPcba.bin

30 刷modem- fastboot命令

adb reboot bootloader
fastboot flash modem +modem文件路径
fastboot reboot

31 刷boot

adb reboot bootloader
fastboot flash boot C:\Users\dell\Desktop\boot.img
fastboot reboot

32 抓log,保存在内部存储

adb logcat -v time > D:\Logcat\logcat.log

33 查WiFi MAC

adb shell
cat /sys/class/net/wlan0/address

34 查系统 RTC

while true;
do
date
sleep 1
done

**35 获得当前界面的在哪个Activity **

adb shell dumpsys activity | grep "mFocus"
adb shell dumpsys activity | grep "ResumedActivity"

36 adb打印出错信息

adb logcat *:E

**37 查看一个进程的内存变化 **

adb shell dumpsys meminfo com.android.dialer

38 dumpsys这个工具可以查看当前设备系统服务信息

adb shell dumpsys power
adb shell dumpsys battery
adb shell dumpsys activity top | findstr ACTIVITY #查看顶部activity
adb shell dumpsys SurfaceFlinger

**39 根据进程号PID,反查进程信息 **

adb shell ps | grep 1490

40 查看某个进程的权限申请

adb shell appops get com.android.dialer

41 查看一个应用进程的包清单配置信息

adb shell dumpsys package com.android.dialer

42 查看APK签名命令:

keytool -printcert -v -file CERT.RSA
  1. keytool为java命令行工具,位于jdk或jre的bin目录.
  2. 用压缩工具打开APK在META-INF目录可找到CERT.RSA文件。

43 查看中断异常

adb shell cat /proc/interrupts
150236 mt-eint 1 home
0 mt-eint 2 ALS-eint
22 mt-eint 3 FINGERPRINT-eint
0 mt-eint 6 accdet-eint
5924 mt-eint 10 TOUCH_PANEL-eint
199 mt-eint 206 pmic-eint

操作复现问题后,adb shell查看中断, 红色字体在增加,代表有频繁中断发生

44 怎么用adb命令录制视频

adb shell screenrecord /sdcard/file.mp4			# 录制视频:

45 怎么用adb清除应用缓存和数据

adb shell pm clear com.android.dialer

46 怎么根据进程名查看应用的安装目录

adb shell pm path com.android.dialer

**47 怎么获取当前手机屏幕分辨率 **

#手机分辨率越高,内存占用这块会更多,对于低内存手机来讲影响较大。
adb shell wm size
Physical size: 1080x1920 
adb shell dumpsys window displays
init=1080x1920 480dpi cur=1080x1920 app=1080x1920 rng=1080x1008-1920x1848"

48 句柄泄露,查看句柄数目变化

# 操作几次看看前后文件句柄变化主要是哪些
adb shell
ps | grep com.android.dialer (查询某进程pid)
ls -a -l /proc/pid/fd (pid对应应用进程号, 句柄变化)
ps -t | grep pid (pid对应应用进程号,HandlerThread变化)

**49 怎么删掉数据库值 **

adb shell settings delete system button_enable_float_incoming_bar_key

50 查看运行内存相关信息

adb shell dumpsys meminfo

**51 屏幕刷新率 **

adb shell
dumpsys SurfaceFlinger | grep refresh

低配置手机上内存是主要瓶颈之一,当剩余内存在100MB以下时,整体性能会相对较差;

52 adb快速重启手机

adb shell "stop && start"

**53 全日志抓取到文件 **

adb logcat -b main -b system -b radio -b events -v time >d:/all.log
adb logcat -b all >d:/all.log	 (all含义 : main,system,events,radio,crash,kernel)

54 查看当前任务栈列表

adb shell am stack list

55 启动Activity

adb shell am stack list

2. adb环境配置

首先到官网下载 Downloads - ADB Shell,下载后解压,下面以windows为例,对adb环境进行配置,解压后文件保存到D盘D:\adb(要找到adb.exe的那个路径)

进入计算机,右击此电脑 -> 属性 -> 高级系统设置 -> 环境变量 -> Path -> 在里面添加就好了,点击高级系统设置之后,

202407180943355

3. fastboot及刷机的相关命令

3.1 刷modem

adb reboot bootloader
fastboot flash modem +modem文件路径
fastboot reboot

3.2 刷boot

adb reboot bootloader
fastboot flash boot C:\Users\dell\Desktop\boot.img
fastboot reboot

3.3 刷单个分区操作

adb reboot bootloader							#进入刷机模式
fastboot  flash modem NON-HLOS.bin	# 这个是文件的路径,已经在当下的目录可以直接输入,不在当前目录可以拖拽,会显示绝对路径
fastboot  flash modem_b NON-HLOS.bin
fastboot flash xbl xbl.elf
fastboot flash xbl_a xbl.elf
fastboot flash xbl_b xbl.elf
fastboot flash dtbo_a dtbo.img
fastboot flash dtbo_b dtbo.img
fastboot flash boot_a boot.img
fastboot flash boot_b boot.img
fastboot flash xbl_config_a xbl_config.elf
fastboot flash xbl_config_b xbl_config.elf
fastboot flash abl_a abl.elf
fastboot flash abl_b abl.elf
fastboot flash imagefv imagefv.elf
fastboot reboot									#刷完之后要重启

3.4 刷整个系统

adb reboot edl								    # 进入9008模式整机刷机模式

在QEIL软件中可以进行整机刷新。点击download后就等待刷机成功,它自己会重新启动

863b4aca-df24-479c-b601-0466e45c573a

4. git常用命令

一、git安装后-指定名称和邮箱

$ git config --global user.name "Your Name"
$ git config --global user.email "email@example.com"

二、创建版本库

$ mkdir learngit	//创建
$ cd learngit	//使用
$ pwd	//查看当前目录
$ git init	//初始化,生成.git文件(若该文件隐藏,则使用ls -ah)

*三、把文件添加add和提交commit到版本库

$ git add test.txt	//添加
$ git add .	//添加该目录下的所有文件
$ git commit -m "wrote a test file"	//提交
$ git commit -m "add 3 files."		//一次性提交多个文件

注意:必须在当前版本库和当前目录下
*四、版本控制

$ git log	//查看提交历史记录,从最近到最远,可以看到3次
$ git log --pretty=oneline	//加参,简洁查看
$ git reflog	//查看每一次修改历史
$ cat test.txt	//查看文件内容
$ git status	//查看工作区中文件当前状态
$ git reset --hard HEAD^(HEAD~100)(commit id)	//回退版本
$ git checkout -- test.txt	//丢弃工作区的修改,即撤销修改
$ git reset HEAD test.txt	//丢弃暂存区的修改(若已提交,则回退)

五、删除文件

$ rm test.txt
//直接删除
$ git rm test.txt
$ git commit -m "remove test.txt"
//删错了,恢复
$ git checkout -- test.txt

*六、远程仓库

$ ssh-keygen -t rsa -C "youremail@example.com"	//创建SSH Key
$ git remote add origin git@github.com:Daisy/AKgit.git	//关联
$ git push -u origin master	//将本地内容推送到远程仓库(第一次)
$ git push origin master	//将本地内容推送到远程仓库(之后)
$ git remote -v        //查看远程仓库信息
$ git remote rm origin	//删除远程仓库(解绑)
$ git clone git@github.com: Daisy/AKgit.git	//克隆远程仓库
//克隆之后使用和查看
$ cd gitskills
$ ls
$ git remote	//查看远程库的信息
$ git remote -v	//查看远程库的详细信息

*七、多人协作

$ git checkout -b dev	//创建并切换到分支dev
# 创建并切换到分支dev,同上
$ git branch dev	//创建
$ git checkout dev	//切换
# 新版本
$ git switch -c dev	//创建并切换到分支dev
$ git switch master	//直接切换分支
$ git branch		//查看当前分支
$ git merge dev	(--no-ff)(-m)//合并,把dev分支的工作成果合并到master分支上
$ git branch -d dev	//删除dev分支 

$ git stash	//将现场储藏起来
$ git stash list	//查看储存的工作现场
# 恢复和删除
$ git stash apply
$ git stash drop
# 恢复并删除
$ git stash pop
$ git cherry-pick 4c805e2	//复制修改

$ git push origin master(dev)	//推送分支
$ git checkout -b dev origin/dev	//创建远程origin的dev分支到本地
$ git pull	//抓取分支(解决冲突)
$ git branch --set-upstream-to=origin/dev dev//指定本地与远程dev的链接
$ git rebase	//把本地未push的分叉提交历史整理成直线

八、标签管理

$ git tag v1.0	//打标签
$ git tag -a v0.1 -m "version 0.1 released" 1094adb //指定标签名和说明文字
$ git tag	//查看所有标签
# 若是忘记打,则查找历史提交commit id ,再打上
$ git log --pretty=oneline --abbrev-commit
$ git tag v0.9 f52c633
$ git show v0.9		//查看标签详细信息
$ git tag -d v0.1	//删除标签
$ git push origin v1.0	//推送标签到远程
$ git push origin –tags	//一次性推送全部本地标签
# 删除标签,(若已推送到远程,先从本地删除,从远程删除)
$ git tag -d v0.9
$ git push origin :refs/tags/v0.9 

九、自定义git

$ git config –global color.ui true //让git显示颜色

//忽略特殊文件
//.gitignore文件
# Windows:
Thumbs.db
ehthumbs.db
Desktop.ini
# Python:
*.py[cod]
*.so
*.egg
*.egg-info
dist
build
# My configurations:
db.ini
deploy_key_rsa
//把该文件也提交到git
$ git add -f App.class		//强制添加被忽略的特殊文件
$ git check-ignore -v App.class	//检查哪个规则出错
# 排除所有.开头的隐藏文件:
.*
# 排除所有.class文件:
*.class
# 不排除.gitignore和App.class:
!.gitignore
!App.class

$ git config --global alias.st status	//配置别名
$ git config --global alias.unstage 'reset HEAD'  //配置操作别名
$ git config --global alias.last 'log -1'	//显示最后一次提交信息
$ git last	//显示最近一次的提交
$git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"  //颜色
$ cat .git/config //查看每个仓库的git配置文件
$ cat .gitconfig  //查看当前用户的git配置文件

十、关于log

git log .						# 查看当前目录下的文件或者目录提交历史
git log							# 查看整个仓库下的文件或者目录提交历史
git log --oneline				 # 非常高效快速的浏览提交历史,前面是哈希值
git show 5a3c8b1623(哈希值)				# 直接查看某个提交的情况,
git show 5a3c8b1623 --stat					# 直接查看某个提交的情况,大概改了哪些文件
git log --all --graph						# 以分支的形式查看

5. UART、SPI、I2C等通信协议总结

该小节的原文链接:https://blog.csdn.net/m0_70435858/article/details/126123622

https://blog.csdn.net/Mr_Guan/article/details/133324610

波特率是发送二进制数据位的速率,即每秒传输二进制位的数量。两个单片机要保持通信一定要保持一致的波特率。

一、并行和串行

  • 1.并行通讯:同一时刻,可以传输多个bit位的信号,有多少个信号位就需要多少根信号线。

    • 并行通讯的效率高,但是对信号线路要求也很高,一般应用于快速设备之间采用并行通信,譬如CPU 与存储设备、存储器与存储器、主机与打印机等都采用并行通讯。
    • 就像下面的例子,允许多个小汽车同时通过公路

    image-20240722171529197

  • 2.串行通讯:同一时刻,只能传输一个bit位的信号,只需要一根信号线。

    • 串行通讯效率较低,但是对信号线路要求低,抗干扰能力强,同时成本也相对较低,一般用于与计算机与外部设备,或者长距离的数据传输。
    • 就像下面的例子,串行就是要后面的小汽车必须等第一辆车通过才能通过

    image-20240722171406360

二、异步和同步

  • 同步通信是一种连续串行传送数据的通信方式,一次通信只传送一帧信息。收发双方时钟同步。

  • 异步通信在发送字符时,所发送的字符之间的时间间隔可以是任意的。收发双方时钟不同步。

  • 同步通信与异步通信区别

    • 同步通信要求接收端时钟频率和发送端时钟频率一致,发送端发送连续的比特流;异步通信时不要求接收端时钟和发送端时钟同步发送端发送完一个字节后,可经过任意长的时间间隔再发送下一个字节。
    • 同步通信效率高;异步通信效率较低。
    • 同步通信较复杂,双方时钟的允许误差较小;异步通信简单,双方时钟可允许一定误差。
    • 同步通信可用于点对多点;异步通信只适用于点对点。

三、全双工和半双工

全双工通信:称为双向同时通信,即通信的双方可以同时发送和接收信息的信息交互方式。
半双工通信:指数据可以沿两个方向传送,但同一时刻一个半双工总线结构信道只允许单方向传送,因此又被称为双向交替通信。
单工通信:是指消息只能单方向传输的工作方式,只能有一个方向的通信而没有反方向的交互。(发送端->接收端,不能反向)

image-20240722173730462

四、UART协议(全双工、速度慢)

1.UART简介

UART的全称是通用异步收发器(Universal Asynchronous Receiver/Transmitter),是一种通用的串行、异步通信总线,该总线有两条数据线,可以实现全双工的发送和接收在嵌入式系统中常用于主机与辅助设备之间的通信。

2.UART接口

  • TXD:发送数据;
  • RXD:接收数据;
  • CTS:清除发送、允许发送;
  • RTS:请求发送。

RTS/CTS协议即请求发送/允许发送协议,相当于一种握手协议,主要用来解决”隐藏终端”问题。”隐藏终端”是指,基站A向基站B发送信息,基站C未侦测到A也向B发送,故A和C同时将信号发送至B,引起信号冲突,最终导致发送至B的信号都丢失了。”隐藏终端”多发生在大型单元中(一般在室外环境),这将带来效率损失,并且需要错误恢复机制。当需要传送大容量文件时,尤其需要杜绝“隐藏终端”现象的发生。IEEE802.11提供了如下解决方案。在参数配置中,若使用RTS/CTS协议,同时设置传送上限字节数,一旦待传送的数据大于此上限值时,即启动RTS/CTS握手协议:首先,A向B发送RTS信号,表明A要向B发送若干数据,B收到RTS后,向所有基站发出CTS信号,表明已准备就绪,A可以发送,其余基站暂时“按兵不动”,然后,A向B发送数据,最后,B接收完数据后,即向所有基站广播ACK确认帧,这样,所有基站又重新可以平等侦听、竞争信道了。

3.UART帧格式

  • 空闲位:数据线在空闲的时候,数据线的状态为高电平;
  • 起始位:表示一次通信的开始;(1位)
  • 数据位:串口协议规定,先发低位、后发高位;可以发送5-8位数据;
  • 校验位:校验数据的正确性,若数据位1的个数为偶数,则检验位为1,否则为0;检验位只能发现错误,但不能纠错。
  • 停止位:表示一次通信的结束,数据线的状态为高电平。(1位)
image-20240722174417824

4.应用场景

多用于板间通信,比如单片机和计算机,如RS-232、USB、MCU等。

image-20240722175027419

5.UART的缺点

  • (1)电气接口不统一
    • UART只是对信号的时序进行了定义,而未定义接口的电气特性;
    • UART通信时一般直接使用处理器使用的电平,即TTL电平,但不同的处理器使用的电平存在差异,所以不同的处理器使用UART通信时一般不能直接相连;
    • UART没有规定不同器件连接时连接器的标准,所以不同器件间通过UART通信时连接很不方便。
  • (2)抗干扰能力差
    • UART一般直接使用TTL信号来表示0和1,但TTL信号的抗干扰能力较差,数据在传输过程中很容易出错。
  • (3)通信距离极短
    • 因为TTL信号的抗干扰能力较差,所以其通信距离也很短,一船只能用于一个电路板上的两个不芯片之间的通信。

五、I2C协议(半双工、速度中等)

PHILIPS公司开发的一种两线式、串行、半双工同步通信总线,可以挂载多个参与通信的器件,常用于板内通信,比如单片机与外围芯片之间短距离、低速的信号传输

在 CPU 与被控 IC 之间、IC 与 IC 之间进行双向传送,高速 IIC 总线一般可达 400kbps 以上。IIC是为了与低速设备通信而发明的,所以IIC的传输速率比不上SPI

2e184aaa54594bf3ab44a3a5e1112075

所有接到I2C总线设备上的串行数据SDA都接到总线的SDA上,各设备的时钟线SCL接到总线的SCL上。I2C总线上的每个设备都自己一个唯一的地址,来确保不同设备之间访问的准确性。

1.I2C物理层特点

  • (1)它是一个支持设备的总线。“总线”指多个设备共用的信号线。在一个 I2C 通讯总线中,可连接多个I2C通讯设备,支持多个通讯主机及多个通讯从机。
  • (2)一个I2C总线只使用两条总线线路,**一条双向串行数据线(SDA) ,一条串行时钟线(SCL)**。数据线即用来表示数据,时钟线用于数据收发同步。
  • (3)每一个连接总线的设备都有一个7位的独立地址,主机可以通过这个地址进行选择连接总线的设备与之通信。
  • (4)总线通过上拉电阻接到电源。当I2C设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。
  • (5)多个主机同时使用总线时,为了防止多个设备发送数据冲突,会利用仲裁方式决定由哪个设备占用总线。
  • (6)具有三种传输模式:标准模式传输速率为100kbit/s,快速模式为400kbit/s,快速模式为1Mbit/s,高速模式下可达3.4Mbit/s,但目前大多I2C设备尚不支持高速模式。
  • (7)连接到相同总线的 IC 数量受到总线的最大电容 400pF限制。

IIC的一个优点是它支持多主控(multimastering), 其中任何一个能够进行发送和接收的设备都可以成为主总线。一个主控能够控制信号的传输和时钟频率。当然,在任何时间点上只能有一个主控。支持不同速率的通讯速度,标准速度(最高速度100kHZ),快速(最高400kHZ)。

SCL和SDA都需要接上拉电阻 (大小由速度和容性负载决定一般在3.3K-10K之间) 保证数据的稳定性,减少干扰。

寻址

image-20240723111555101

发数据

image-20240723112208413

2.字节格式

SDA数据线上的每个字节必须是8位,每次传输的字节数量没有限制。每个字节后必须跟一个响应位(ACK)。首先传输的数据是最高位(MSB),SDA上的数据必须在SCL高电平周期时保持稳定,数据的高低电平翻转变化发生在SCL低电平时期。

3. 7-bit寻址数据传输

常见的传输方向及格式有如下两种:

  • (1)主机写数据——从机接收,传输方向不变
    • 要进行数据写入从机,首先主机发送START条件+从机地址+R/W=0(写操作,设置为0),从机读取到该地址后回应ACK,主机将继续发送需要操作的寄存器地址,从机继续回应ACK,表示从机准备完毕。之后主机发送寄存器的数据(可能是1byte也可能是多个byte),每个byte从机都会回应ACK,发送完成后,主机发送STOP命令,将总线释放,完成写操作。如下图示意:
  • (2)主机读数据-从机发送,传输方向改变
    • 读数据与写数据相似,但读数据会多几个步骤。要想从从机读取数据,首先要知道从机地址以及寄存器地址,这两部需要进行读操作来实现,和写操作一致。读操作完成后,主机发送重复开始+从机地址+R/W=1(读操作,设置为1),从机返回ACK,此时主机释放SDA线转由从机控制,主机读取SDA总线进行数据接收,每发送1 byte数据,主机会响应ACK表示还需要再接收数据。当主机接收完想要的数据后,主机将会返回NACK,告诉从机释放SDA总线,随后主机发送STOP命令,将总线释放,完成读操作。如下图示意:

4.IIC常见问题

(1)为什么Open-Drain开漏输出需要上拉电阻

​ 开漏Pin不连接外部的上拉电阻,则只能输出低电平。当输出电平为低时,N沟道MOS管是导通的,这样在Vcc和GND之间有一个持续的电流流过上拉电阻R和三极管Q1。这会影响整个系统的功耗。采用较大值的上拉电阻可以减小电流。但是大的阻值会使输出信号的上升时间变慢。即上拉电阻R pull-up的阻值 决定了逻辑电平转换的沿的速度。阻值越大,速度越低功耗越小。反之亦然。

(2)为什么IIC需要漏极开路

防止短路
如果不设为开漏,而设为推挽,几个设备连在同一条总线上,这时某一设备的某个IO输出高电平,另有一台设备的某一个IO输出低电平,这时你会发现这两个IO的VCC和GND短路了;但是开漏就不会有这个问题。

增强端口扇出能力、降低功耗
  IC为增强端口扇出能力而设计为漏极开路样式,使用时将该端口设为低电平有效的灌电流模式,能得到最大输出电流同时IC功耗最低。此类端口当输出高电平则需要外接上拉电阻。

利用“线与”判断总线占用状态
  可以将多个开漏输出的Pin脚,连接到一条线上,形成“与逻辑”关系,即“线与”功能,任意一个变低后,开漏线上的逻辑就为0了。这也是I2C,SMBus等总线判断总线占用状态的原理。
如果总线上的一个A设备将SDA拉高,这时总线上另一个B设备已将SDA拉低,这时由于1&0=0,所以A设备检查SDA的时候会发现不是高电平而是低电平,这就表明总线上已经有其他设备占用总线了,A只好放弃,如果检测是高电平那就可以使用。

增加驱动能力
如果在漏极drain_output接上拉电阻,则可以进行电平转换,且驱动能力较强。
利用外部电路的驱动能力,减少IC内部的驱动。当IC内部MOSFET导通时,驱动电流是从外部的VCC流经R pull-up ,MOSFET到GND。IC内部仅需很下的栅极驱动电流。

控制输出高电平大小
可以利用改变上拉电源的电压,改变传输电平。
(3)软件IIC和硬件IIC的区别

软件IIC:软件IIC通信指的是用单片机的两个I/O端口模拟出来的IIC,用软件控制管脚状态以模拟I2C通信波形,软件模拟寄存器的工作方式。

硬件IIC:一块硬件电路,硬件I2C对应芯片上的I2C外设,有相应I2C驱动电路,其所使用的I2C管脚也是专用的,硬件(固件)I2C是直接调用内部寄存器进行配置。

硬件I2C的效率要远高于软件的,而软件I2C由于不受管脚限制,接口比较灵活。

5.12C通信一般流程与软件模拟I2C协议

  • 主机发送起始位并进行从机寻址
  • 得到应答后主机开始发送/读取数据位
  • 数据发送/读取完成主机发送停止位结束此次通信

软件模式I2C协议

实验目的

STM32作为主机向从机EEPROM存储器写入256个字节的数据
STM32作为主机向从机EEPROM存储器读取写入的256个字节的数据

读写成功亮绿灯,读写失败亮红灯

实验原理

61adc6161e3ff927ef13c791ad28d8ee

软件模式I2C由我们CPU来控制引脚产生I2C时序,所以我们随便选引脚都可以,不过你选择的引脚肯定要连接到通信的EEPROM的SCL,SDA引脚上。这里是用了PC12,PC11充当主机STM32SCL,SDA引脚。

主机产生起始信号

8bb2632fad2554a2d6bb8282364be13d

主机产生停止信号

d1477f39105680213a067d3a2dbb324d

主机产生应答信号或非应答信号

d89ca2888a7eab2fba91d8d6a02088e4

38a8f5e6563ec9ad3cb9f6d1568bc185

等待从机EEPROM应答

539b5331516a43156e65ec65a94b26b4

主机发送一个字节给从机

a426e4da5ae62647f3818f44616ef5f8

主机向EEPROM接收一个字节

95cd35cebf49d6a0f0348a55803001d9

value应该初始化为0

i2c_gpio.h

#ifndef _I2C_GPIO_H
#define _I2C_GPIO_H


#include "stm32f10x.h"

#define EEPROM_I2C_WR	0		/* 写控制bit */
#define EEPROM_I2C_RD	1		/* 读控制bit */

#define EEPROM_GPIO_PORT_I2C         GPIOB
#define EEPROM_RCC_I2C_PORT          RCC_APB2Periph_GPIOB
#define EEPROM_I2C_SCL_PIN           GPIO_Pin_6
#define EEPROM_I2C_SDA_PIN           GPIO_Pin_7

/*当 STM32 的 GPIO 配置成开漏输出模式时,它仍然可以通过读取
GPIO 的输入数据寄存器获取外部对引脚的输入电平,也就是说它同时具有浮空输入模式的
功能*/

#define EEPROM_I2C_SCL_1()  EEPROM_GPIO_PORT_I2C->BSRR |= EEPROM_I2C_SCL_PIN		/* SCL = 1 */
#define EEPROM_I2C_SCL_0()  EEPROM_GPIO_PORT_I2C->BRR  |= EEPROM_I2C_SCL_PIN		/* SCL = 0 */
	
#define EEPROM_I2C_SDA_1()  EEPROM_GPIO_PORT_I2C->BSRR |= EEPROM_I2C_SDA_PIN		/* SDA = 1 */
#define EEPROM_I2C_SDA_0()  EEPROM_GPIO_PORT_I2C->BRR  |= EEPROM_I2C_SDA_PIN		/* SDA = 0 */

#define EEPROM_I2C_SDA_READ()  ((EEPROM_GPIO_PORT_I2C->IDR & EEPROM_I2C_SDA_PIN)!=0 )	/* 读SDA口线状态 */


void i2c_Start(void);
void i2c_Stop(void);
void i2c_Ack(void);
void i2c_NAcK(void);
uint8_t i2c_WaitAck(void);
void i2c_SendByte(uint8_t data);
uint8_t i2c_ReadByte(void);
uint8_t i2c_CheckDevice(uint8_t Address);
#endif  /* _I2C_GPIO_H */

i2c_gpio.c

#include "i2c_gpio.h"

#include  "stm32f10x.h"

void I2c_gpio_config(void)
{
	GPIO_InitTypeDef  GPIO_InitStructure;
	RCC_APB2PeriphClockCmd(EEPROM_RCC_I2C_PORT, ENABLE);
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
	GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SCL_PIN | EEPROM_I2C_SDA_PIN;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(EEPROM_GPIO_PORT_I2C, &GPIO_InitStructure);
	
	/* 给一个停止信号, 复位I2C总线上的所有设备到待机模式 */
	i2c_Stop();
}

static void i2c_Delay(void)
{
	uint8_t i;
	for(i=0;i<10;i++)
	{
	}
}


void i2c_Start(void)
{
	EEPROM_I2C_SCL_1();
	EEPROM_I2C_SDA_1();
	i2c_Delay();
	EEPROM_I2C_SDA_0();
	i2c_Delay();
	EEPROM_I2C_SCL_0();
	i2c_Delay();
}

void i2c_Stop(void)
{
	EEPROM_I2C_SDA_0();
	EEPROM_I2C_SCL_1();
	i2c_Delay();
	EEPROM_I2C_SDA_1();
	i2c_Delay();
}

void i2c_Ack(void)
{
	EEPROM_I2C_SCL_0();
	i2c_Delay();
	EEPROM_I2C_SDA_0();
	i2c_Delay();
	EEPROM_I2C_SCL_1();
	i2c_Delay();
	EEPROM_I2C_SCL_0();
	i2c_Delay();
	EEPROM_I2C_SDA_1();
	i2c_Delay();

}

void i2c_NAcK(void)
{
	EEPROM_I2C_SDA_1();
	i2c_Delay();
	EEPROM_I2C_SCL_1();
	i2c_Delay();
	EEPROM_I2C_SCL_0();
	i2c_Delay();

}

uint8_t i2c_WaitAck(void)
{
	uint8_t ret;
	EEPROM_I2C_SDA_1();
	EEPROM_I2C_SCL_1();
	i2c_Delay();
	if( EEPROM_I2C_SDA_READ() )
	{
		ret=1;
	}
	else
	{
		ret=0;
	}
	EEPROM_I2C_SCL_0();
	i2c_Delay();
  return ret;

}
	

void i2c_SendByte(uint8_t data)
{
	uint8_t i;
	for(i=0;i<8;i++)
	{
		if( data&0x80 )
	 {
		  EEPROM_I2C_SDA_1();
	 }
	 else
	 {
		  EEPROM_I2C_SDA_0();
	 }
	 i2c_Delay();
	 EEPROM_I2C_SCL_1();
	 i2c_Delay();
	 EEPROM_I2C_SCL_0();
	 i2c_Delay();
	 if( i==7 )
	 {
		 EEPROM_I2C_SDA_1();
		 i2c_Delay();
	 }
	 data=data<<1;
	}
	
}

uint8_t i2c_ReadByte(void)
{
	uint8_t value=0;
	uint8_t i;
	for(i=0;i<8;i++)
	{
		value=value<<1;
		EEPROM_I2C_SCL_1();
	  i2c_Delay();
		if( EEPROM_I2C_SDA_READ() )
	  {
	 	  value++;
	  }
	  EEPROM_I2C_SCL_0();
	  i2c_Delay();
	}
	return value;
}

uint8_t i2c_CheckDevice(uint8_t Address)
{
	uint8_t ucACK;
	I2c_gpio_config();
	i2c_Start();
	i2c_SendByte(Address|EEPROM_I2C_WR);
	ucACK=i2c_WaitAck();
	i2c_Stop();
  return ucACK;	
	
}

i2c_ee.h

#ifndef _I2C_EE_H
#define _I2C_EE_H
#include "stm32f10x.h"

#define EEPROM_DEV_ADDR			0xA0		/* 24xx02的设备地址 */
#define EEPROM_PAGE_SIZE		  8			  /* 24xx02的页面大小 */
#define EEPROM_SIZE				  256			  /* 24xx02总容量 */

uint8_t ee_Checkok(void);
uint8_t  ee_ReadByte( uint8_t *pReaddata,uint16_t Address,uint16_t num );
uint8_t  ee_WriteByte( uint8_t *Writepdata,uint16_t Address,uint16_t num );
uint8_t ee_WaitStandby(void);
uint8_t ee_WriteBytes(uint8_t *_pWriteBuf, uint16_t _usAddress, uint16_t _usSize);
uint8_t ee_ReadBytes(uint8_t *_pReadBuf, uint16_t _usAddress, uint16_t _usSize);
uint8_t ee_Test(void) ;
#endif  /* _I2C_EE_H*/

i2c_ee.c

#include "i2c_ee.h"
#include "i2c_gpio.h"

//检测EEPORM是否忙碌
uint8_t ee_Checkok(void)
{
	if(i2c_CheckDevice(EEPROM_DEV_ADDR)==0)
	{
		return 1;
	}
	else
	{
    i2c_Stop();  
		return 0;
 	}
}	
//检测EEPROM写入数完成
uint8_t ee_WaitStandby(void)
{
	uint32_t wait_count = 0;
	
	while(i2c_CheckDevice(EEPROM_DEV_ADDR))
	{
		//若检测超过次数,退出循环
		if(wait_count++>0xFFFF)
		{
			//等待超时
			return 1;
		}
	}
	//等待完成
	return 0;
}


//向EEPROM写入多个字节
uint8_t ee_WriteBytes(uint8_t *_pWriteBuf, uint16_t _usAddress, uint16_t _usSize)
{
	uint16_t i,m;
	uint16_t addr;
	addr=_usAddress;
  for(i=0;i<_usSize;i++)
	{
		  //当第一次或者地址对齐到8就要重新发起起始信号和EEPROM地址
		  //为了解决8地址对齐问题
			if(i==0 || (addr % EEPROM_PAGE_SIZE)==0 )
			{
				 //循环发送起始信号和EEPROM地址的原因是为了等待上一次写入的一页数据\
				写入完成
				 for(m=0;m<1000;m++)
				 {
					 //发送起始地址
					 i2c_Start();
					 //发送设备写地址
					 i2c_SendByte(EEPROM_DEV_ADDR|EEPROM_I2C_WR);
					 //等待从机应答
					 if( i2c_WaitAck()==0 )
					 {
						break;
					 }
				 } 
				  //若等待的1000次从机还未应答,等待超时
				  if( m==1000 )
			  	{
					goto cmd_fail;
			   	}	
				//EEPROM应答后发送EEPROM的内部存储器地址
				i2c_SendByte((uint8_t)addr);
				//等待从机应答
				if( i2c_WaitAck()!=0 )
				{
					goto cmd_fail;
					
				}	
			}
		 //发送数据
		 i2c_SendByte(_pWriteBuf[i]);
		 //等待应答
	   if( i2c_WaitAck()!=0 )
	   {
		  goto cmd_fail;			
     }
		 //写入地址加1
		 addr++;		
	}
	
	i2c_Stop();
	return 1;
	
	cmd_fail:
	i2c_Stop();
	return 0;
}


uint8_t ee_ReadBytes(uint8_t *_pReadBuf, uint16_t _usAddress, uint16_t _usSize)
{
	uint16_t i;
	
	  i2c_Start();
		i2c_SendByte(EEPROM_DEV_ADDR|EEPROM_I2C_WR);
	 if( i2c_WaitAck()!=0 )
	 {
			 goto cmd_fail;		
	  }
		i2c_SendByte((uint8_t)_usAddress);
	 if( i2c_WaitAck()!=0 )
	 {
			  goto cmd_fail;
	  }
		i2c_Start();
		i2c_SendByte(EEPROM_DEV_ADDR|EEPROM_I2C_RD);
		 if( i2c_WaitAck()!=0 )
		 {
				  goto cmd_fail;				
	   }
	 for(i=0;i<_usSize;i++)
	{	
		_pReadBuf[i]=i2c_ReadByte();
		/* 每读完1个字节后,需要发送Ack, 最后一个字节不需要Ack,发Nack */
		if (i != _usSize - 1)
		{
//			i2c_NAcK();	/* 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1) */
			i2c_Ack();	/* 中间字节读完后,CPU产生ACK信号(驱动SDA = 0) */
		}
		else
		{
			i2c_NAcK();	/* 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1) */
		}
	}
	i2c_Stop();
	return 1;
	
	cmd_fail:
	i2c_Stop();
	return 0;
}

uint8_t ee_Test(void) 
{
  uint16_t i;
	uint8_t write_buf[EEPROM_SIZE];
  uint8_t read_buf[EEPROM_SIZE];
  
/*-----------------------------------------------------------------------------------*/  
  if (i2c_CheckDevice(EEPROM_DEV_ADDR) == 1)
	{
		/* 没有检测到EEPROM */
		printf("没有检测到串行EEPROM!\r\n");
				
		return 0;
	}
/*------------------------------------------------------------------------------------*/  
  /* 填充测试缓冲区 */
	for (i = 0; i < EEPROM_SIZE; i++)
	{		
		write_buf[i] = i;
	}
/*------------------------------------------------------------------------------------*/  
  if (ee_WriteBytes(write_buf, 0, EEPROM_SIZE) == 0)
	{
		printf("写EEPROM出错!\r\n");
		return 0;
	}
	else
	{		
		printf("写EEPROM成功!\r\n");
	}  

/*-----------------------------------------------------------------------------------*/
  if (ee_ReadBytes(read_buf, 0, EEPROM_SIZE) == 0)
	{
		printf("EEPROM出错!\r\n");
		return 0;
	}
	else
	{		
		printf("EEPROM成功,数据如下:\r\n");
	}
/*-----------------------------------------------------------------------------------*/  
  for (i = 0; i < EEPROM_SIZE; i++)
	{
		if(read_buf[i] != write_buf[i])
		{
			printf("0x%02X ", read_buf[i]);
			printf("错误:EEPROM读出与写入的数据不一致");
			return 0;
		}
    printf(" %02X", read_buf[i]);
		
		if ((i & 15) == 15)
		{
			printf("\r\n");	
		}		
	}
  printf("EEPROM读写测试成功\r\n");
  return 1;
}

main

#include "stm32f10x.h"
#include "led.h"
#include  "usart.h"
#include  <string.h>
#include "i2c_ee.h"
#include "i2c_gpio.h"

#define SOFT_DELAY Delay(0x0FFFFF);

void Delay(__IO u32 nCount); 


int main(void)
{	

	/* LED 端口初始化 */
	LED_GPIO_Config();	

  /*初始化USART 配置模式为 115200 8-N-1,中断接收*/
  USART_Config();

		printf("EEPROM 软件模拟i2c测试例程 \r\n");		
	 
  if(ee_Test() == 1)
  	{
			LED_G(NO);
    }
    else
    {
      LED_R(NO);
    }
	 
while(1)
{  
}
	
  }

void Delay(__IO uint32_t nCount)	 //简单的延时函数
{
	for(; nCount != 0; nCount--);
}

六、SPI协议(全双工、速度快)

1.SPI简介

SPI是串行外设接口,也是一种单片机外设芯片串行扩展接口,是一种高速、全双工、同步通信总线,所以可以在同一时间发送和接收数据,SPI没有定义速度限制,通常能达到甚至超过10M/bps。

SPI有主、从两种模式,通常由一个主模块和一个或多个从模块组成(SPI不支持多主机),主模块选择一个从模块进行同步通信,从而完成数据的交换。提供时钟的为主设备(Master),接收时钟的设备为从设备(Slave),SPI接口的读写操作,都是由主设备发起,当存在多个从设备时,通过各自的片选信号进行管理。

  • MISO:Master input slave output 主机输入,从机输出(数据来自从机);
  • MOSI:Master output slave input 主机输出,从机输入(数据来自主机);
  • SCLK :Serial Clock 串行时钟信号,由主机产生发送给从机;
  • SS:Slave Select 片选信号,由主机发送,以控制与哪个从机通信,通常是低电平有效信号。

SPI主设备和从设备都有一个串行移位寄存器,主设备通过向它的SPI串行寄存器写入一个字节来发起一次传输。

2.SPI数据通信的流程

  • 1、主设备发起信号,将CS/SS拉低,启动通信。
  • 2、主设备通过发送时钟信号,来告诉从设备进行写数据或者读数据操作(采集时机可能是时钟信号的上升沿(从低到高)或下降沿(从高到低),因为SPI有四种模式,后面会讲到),它将立即读取数据线上的信号,这样就得到了一位数据(1bit)。
  • 3、主机(Master)将要发送的数据写到发送数据缓存区(Menory),缓存区经过移位寄存器(缓存长度不一定,看单片机配置),串行移位寄存器通过MOSI信号线将字节一位一位的移出去传送给从机,同时MISO接口接收到的数据经过移位寄存器一位一位的移到接收缓存区。
  • 4、从机(Slave)也将自己的串行移位寄存器(缓存长度不一定,看单片机配置)中的内容通过MISO信号线返回给主机。同时通过MOSI信号线接收主机发送的数据,这样,两个移位寄存器中的内容就被交换。

SPI只有主模式和从模式之分,没有读和写的说法,外设的写操作和读操作是同步完成的。若只进行写操作,主机只需忽略接收到的字节(虚拟数据);反之,若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输。也就是说,你发一个数据必然会收到一个数据;你要收一个数据必须也要先发一个数据。

3.SPI时钟特点

主要包括:时钟速率、时钟极性和时钟相位三方面。

时钟速率

SPI总线上的主设备必须在通信开始时候配置并生成相应的时钟信号。从理论上讲,只要实际可行,时钟速率就可以是你想要的任何速率,当然这个速率受限于每个系统能提供多大的系统时钟频率,以及最大的SPI传输速率。

时钟极性

根据硬件制造商的命名规则不同,时钟极性通常写为CKP或CPOL。时钟极性和相位共同决定读取数据的方式,比如信号上升沿读取数据还是信号下降沿读取数据。

CKP可以配置为1或0。可以根据需要将时钟的默认状态(IDLE)设置为高或低。极性反转可以通过简单的逻辑逆变器实现

  • CKP = 0:时钟空闲IDLE为低电平 0;
  • CKP = 1:时钟空闲IDLE为高电平1。

时钟相位

根据硬件制造商的不同,时钟相位通常写为CKE或CPHA。顾名思义,时钟相位/边沿,也就是采集数据时是在时钟信号的具体相位或者边沿;

  • CKE = 0:在时钟信号SCK的第一个跳变沿采样;
  • CKE = 1:在时钟信号SCK的第二个跳变沿采样。

4.SPI四种MODE

除了配置串行时钟速率(频率)外,SPI主设备还需要配置时钟极性和时钟相位。

时钟极性 CKP/Clock Polarit

根据硬件制造商的命名规则不同,时钟极性通常写为CKP或CPOL。时钟极性和相位共同决定读取数据的方式,比如信号上升沿读取数据还是信号下降沿读取数据;

CKP可以配置为1或0。这意味着您可以根据需要将时钟的默认状态(IDLE)设置为高或低。极性反转可以通过简单的逻辑逆变器实现。您必须参考设备的数据手册才能正确设置CKP和CKE。CKP = 0:时钟空闲IDLE为低电平 0;
CKP = 1:时钟空闲IDLE为高电平1;

时钟相位 CKE /Clock Phase (Edge)

除配置串行时钟速率和极性外,SPI主设备还应配置时钟相位(或边沿)。根据硬件制造商的不同,时钟相位通常写为CKE或CPHA;

顾名思义,时钟相位/边沿,也就是采集数据时是在时钟信号的具体相位或者边沿;

  • CKE = 0:在时钟信号SCK的第一个跳变沿采样;
  • CKE = 1:在时钟信号SCK的第二个跳变沿采样;

d5e56155435a4edeb89f38a268cba3cf-1

此处附上一组软件模拟SPI 通信传输数据的代码

//SPI发送数据,val为要发送的数据
void SPI_Send(u8 val)  
{ 
 
	u8 recv_data = 0, i = 0;//将接受数据清零方便直接或运算接受数据
	
	SCK = 0;//时钟线拉低
 
	for(i=0; i<8; i++) //传输数据,先发高位
	{
		//准备数据
		if(val  & (1<<(7-i))) //通过1左移7位然后相与,判断数据最高位是1还是0
		{
			MOSI = 1; //数据为1
		}
		else
		{
			MOSI = 0; //数据为0
		}
		delay_us(5);
		
		SCK = 1; //时钟线拉高准备接收数据
		delay_us(5);
 
		//高电平区间接收数据,此处不重要但必须有
		if(MISO == 1)	//收到的为1
		{
			recv_data |= (1<<(7-i));//将数据位通过或运算保存。先保持高位
		}
		SCK = 0;//时钟线拉低
	}
}
 
//SPI接收数据
u8 SPI_Receive(void)  
{ 
	u8 recv_data = 0, i = 0;
	
//	u8 val = 0x00;
	
	RC522_SCK = 0;
	for(i=0; i<8; i++) //发送数据,此处不重要但必须有
	{
		//准备数据
//		if(val  & (1<<(7-i))) //1
//		{
//			MOSI = 1; //数据为1
//		}
//		else
//		{
//			MOSI = 0; //数据为0
//		}
		
        MOSI = 0; //输出线清零
		delay_us(5);
		
		SCK = 1; //时钟线拉高准备接收数据
		delay_us(5);
 
		//高电平区间接收数据
		if(MISO == 1)	//收到的为1
		{
			recv_data |= (1<<(7-i));//将数据位通过或运算保存。先保持高位
		}
 
		SCK = 0;
	}
	return recv_data; //返回接收到的数据
}

5.数据传输流程代码

首先主机和从机都选择同一传输模式。然后主机片选拉低,选中从机。接着在时钟的驱动下, MOSI发送数据,同时MISO读取接收数据。最后完成传输,取消片选。

 /*
* 函数名: void SPI_WriteByte(uint8_t data)
* 输入参数: data -> 要写的数据
* 输出参数:无  
* 返回值:无
* 函数作用:模拟 SPI 写一个字节
*/ SPI写1 Byte,循环8次,每次发送1 Bit;
void SPI_WriteByte(uint8_t data)  {
    uint8_t i = 0;  
    uint8_t temp = 0;  
    for(i=0; i<8; i++) {
        temp = ((data&0x80)==0x80)? 1:0;  //将data最高位保存到temp;
        data = data<<1;                   //data左移一位,将次高位变为最高位,用于下次取最高位;
        SPI_CLK(0); //CPOL=0              //拉低时钟,即空闲时钟为低电平, CPOL=0;
        SPI_MOSI(temp);                   //根据temp值,设置MOSI引脚的电平;
        SPI_Delay();                      //简单延时,可以定时器或延时函数实现
        SPI_CLK(1); //CPHA=0  //拉高时钟, W25Q64只支持SPI模式0或1,即会在时钟上升沿采样MOSI数据;
        SPI_Delay();  
     }
     SPI_CLK(0);                          //最后SPI发送完后,拉低时钟,进入空闲状态;
}
 
/*
* 函数名: uint8_t SPI_ReadByte(void)
* 输入参数:
* 输出参数:无
* 返回值:读到的数据
* 函数作用:模拟 SPI 读一个字节
*/  SPI读1 Byte,循环8次,每次接收1 Bit;  
uint8_t SPI_ReadByte(void) {
    uint8_t i = 0;
    uint8_t read_data = 0xFF;
    for(i=0; i<8; i++) {
        read_data = read_data << 1;  //“腾空” read_data最低位,8次循环后,read_data将高位在前;  
        SPI_CLK(0);                  //拉低时钟,即空闲时钟为低电平;  
        SPI_Delay();
        SPI_CLK(1);
        SPI_Delay();
        if(SPI_MISO()==1) { 
           read_data = read_data + 1;
        }
    }
    SPI_CLK(0);   //最后SPI读取完后,拉低时钟,进入空闲状态  
    return read_data;
}  

前面提到SPI传输可以看作一个虚拟的环形拓扑结构,即输入和输出同时进行。在前面“ SPI_WriteByte()”函数里,发送了1 Byte,也应该接收1 Byte,只是代码中忽略了接收引脚MISO的状态; 在前面“ SPI_ReadByte()”函数里,接收了1 Byte,也应该发送1 Byte,只是代码中忽略了发送引脚MOSI的内容。有些场景, SPI需要同时读写,因此还需要编写SPI同时读写函数。

/*
* 函数名: uint8_t SPI_WriteReadByte(uint8_t data)
* 输入参数: data -> 要写的一个字节数据
* 输出参数:无
* 返回值:读到的数据
* 函数作用:模拟 SPI 读写一个字节
SPI读和写1 Byte,循环8次,每次发送和接收1 Bit;  */
uint8_t SPI_WriteReadByte(uint8_t data) {
    uint8_t i = 0;
    uint8_t temp = 0;
    uint8_t read_data = 0xFF;
    for(i=0;i<8;i++) {
        temp = ((data&0x80)==0x80)? 1:0; //将data最高位保存到temp;  
        data = data<<1;                  //data左移一位,将次高位变为最高位,用于下次取最高位;  
        read_data = read_data<<1;        //“腾空” read_data最低位,8次循环后,read_data将高位在前;  
        SPI_CLK(0);
        SPI_MOSI(temp);
        SPI_Delay();
        SPI_CLK(1);
        SPI_Delay();
        if(SPI_MISO()==1) {             //读取MISO上的数据,保存到当前read_data最低位;  
            read_data = read_data + 1;
        }
    }
    SPI_CLK(0);
    return read_data;
}  

6.SPI优缺点

优点:

  • 无起始位和停止位,因此数据位可以连续传输而不会被中断;
  • 没有像I2C这样复杂的从设备寻址系统;
  • 数据传输速率比I2C更高(几乎快两倍);
  • 分离的MISO和MOSI信号线,因此可以同时发送和接收数据;
  • 极其灵活的数据传输,不限于8位,它可以是任意大小的字;
  • 非常简单的硬件结构。从站不需要唯一地址(与I2C不同)。从机使用主机时钟,不需要精密时钟振荡器/晶振(与UART不同)。不需要收发器(与CAN不同)。

缺点:

  • 使用四根信号线(I2C和UART使用两根信号线);
  • 无法确认是否已成功接收数据(I2C拥有此功能);
  • 没有任何形式的错误检查,如UART中的奇偶校验位;
  • 只允许一个主设备;
  • 没有硬件从机应答信号(主机可能在不知情的情况下无处发送);
  • 没有定义硬件级别的错误检查协议;
  • 与RS-232和CAN总线相比,只能支持非常短的距离。

七、三种通讯方式区别

623c99072ef941b1adadc101d0c0b8cf

  • SPI(Serial Peripheral Interface)是一种同步的串行通信协议,需要4根线:MISO、MOSI、SCLK和CS。SPI通信速度快,但只能在短距离内通信,且只能支持单主设备和多从设备的通信方式。
  • IIC(Inter-Integrated Circuit)是一种同步的串行通信协议,需要2根线:SCL和SDA。IIC通信速度较慢,但可以在长距离内通信,且可以支持多主设备和多从设备的通信方式。
  • UART(Universal Asynchronous Receiver/Transmitter)是一种异步的串行通信协议,需要2根线:TX和RX。UART通信速度较慢,但可以在长距离内通信,且可以支持点对点的通信方式。

6.设备树相关知识

原文链接:拉依达的嵌入式小屋

原文链接:嵌功尽弃

6.1 设备树的定义

Device Tree是一种描述硬件的数据结构,以便于操作系统的内核可以管理和使用这些硬件,包括CPU或CPU,内存,总线和其他一些外设。

Linux内核从3.x版本之后开始支持使用设备树,可以实现驱动代码与设备的硬件信息相互的隔离,减少了代码中的耦合性

  • 引入设备树之前:一些与硬件设备相关的具体信息都要写在驱动代码中,如果外设发生相应的变化,那么驱动代码就需要改动。
  • 引入设备树之后:通过设备树对硬件信息的抽象,驱动代码只要负责处理逻辑,而关于设备的具体信息存放到设备树文件中。如果只是硬件接口信息的变化而没有驱动逻辑的变化,开发者只需要修改设备树文件信息,不需要改写驱动代码。

描述设备树的文件叫做 DTS(Device Tree Source) ,这个 DTS 文件采用树形结构描述板级设备,也就是开发板上的设备信息,比如 CPU 数量、 内存基地址、 IIC 接口上接了哪些设备、 SPI 接口上接了哪些设备等等,如图

9bd28993f265d7533e642b701e2a19b6

树的主干就是系统总线,IIC 控制器、 GPIO 控制器、 SPI 控制器等都是接 到系统主线上的分支。 IIC 控制器有分为 IIC1 和 IIC2 两种,其中 IIC1 上接了 FT5206 和 AT24C02 这两个 IIC 设备, IIC2 上只接了 MPU6050 这个设备。 DTS 文件的主要功能就是按照图所示的结构来描述板子上的设备信息, DTS 文件描述设备信息是有相应的语法规则要求的。

6.2 DTS、DTB和DTC

08ccae554e82fb18fc2b2e1cc12d1af0

  • DTS
    • 设备树源码文件,硬件的相应信息都会写在.dts为后缀的文件中,每一款硬件可以单独写一份xxxx.dts
  • DTSI
    • 对于一些相同的dts配置可以抽象到dtsi文件中,然后可以用include的方式到dts文件中
    • 同一芯片可以做一个dtsi,不同的板子不同的dts,然后include同一dtsi
    • 对于同一个节点的设置情况,dts中的配置会覆盖dtsi中的配置
  • DTC
    • dtc是编译dts的工具
  • DTB
    • dts经过dtc编译之后会得到dtb文件,设备树的二进制执行文件
    • dtb通过Bootloader引导程序加载到内核。

通过make dtbs编译所有的dts文件。如果要编译指定的dtbs

make xxx.dtb

6.3 设备树框架

6.3.1 dtsi 头文件

设备树也有头文件,扩展名为.dtsi。可以将一款SOC的其他所有设备/平台的共有的信息提出来,作为一个通用的.dtsi文件。 在.dts 设备树文件中,可以通过 “ #include ”来引用 .h.dtsi.dts 文件。只是,我们在编写设备树头文件的时候最好选择 .dtsi 后 缀。 一般.dtsi 文件用于描述 SOC 的内部外设信息,比如 CPU 架构、主频、外设寄存器地址范围,比如 UART 、 IIC 等等。

  • 1)后缀名一般为dts和dtsi,可以被include,甚至可以include那些C语言的头文件
  • 2)dtsi一般写soc共性部分,而dts一般写目标单板特性部分,所以一般dts包含并重写部分dtsi
  • 3)注释用/* */,注意#开头的不是注释
  • 4)分号是段落块之间的分隔符,丹和[]是段落块的封装符号,和C语言语言类似
  • 5)/dts-v1/节点,表示dts的版本号,目前都是v1
  • 6)/{}是根节点root node,理论上只应该有一个根节点,有说法dtc会合并所有root node为同一个
  • 7)dts是树状的多节点组织,基本单元是node,除root外其他node都有parent,还可以有child

6.3.2 设备节点格式

设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点

每个节点都通过一些属性信息来描述节点信息,属性就是键—值对

(1)格式定义

[label:] <node-name>[@<unit-address>]{
	[property]
	[child nodes]
	[child nodes]
	. . .
}

(2)格式解读

  • [ ]: 表示该项可以省略,<>表示不可省略
  • [label] :标签,为了方便访问节点,后面可以直接通过&label来访问该节点。
  • node-name: 节点名称。根节点的名称必须是
  • [@unit-address]: 地址,如cpu node就是0、1这种,reg node就是0x12010000这种。设备的地址或寄存器首地址,若某个节点没有地址或者寄存器,可以省略

(3)设备树源码中常用的几种数据形式

1.字符串:  compatible = "arm,cortex-a7";设置 compatible 属性的值为字符串“arm,cortex-a7”
2.32位无符号整数:reg = <0>; 设置reg属性的值为0,   reg 的值也可以设置为一组值,比如: reg = <0 0x123456 100>;
3.字符串列表:字符串和字符串之间采用“,”隔开
compatible = "fsl,imx6ull-gpmi-nand", "fsl, imx6ul-gpmi-nand";
设置属性 compatible 的值为“fsl,imx6ull-gpmi-nand”和“fsl, imx6ul-gpmi-nand”。

属性

  • compatible属性(兼容属性)
    cpp "manufacturer,model" manufacturer:厂商名称 model:模块对应的驱动名字
    例:
    imx6ull-alientekemmc.dts 中 sound 节点是 音频设备节点,采用的欧胜(WOLFSON)出品的 WM8960, sound 节点的 compatible 属性值如下:
    cpp compatible = "fsl,imx6ul-evk-wm8960","fsl,imx-audio-wm8960";
    属性值有两个,分别为“fsl,imx6ul-evk-wm8960”和“fsl,imx-audio-wm8960”,其中“fsl”表示厂商是飞思卡尔,“imx6ul-evk-wm8960”和“imx-audio-wm8960”表示驱动模块名字。

    sound这个设备首先使用第一个兼容值在 Linux 内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到的话就使用第二个兼容值查

    一般驱动程序文件会有一个 OF 匹配表,此 OF 匹配表保存着一些 compatible 值,如果设备节点的 compatible 属性值和 OF 匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动

    在根节点来说,Linux 内核会通过根节点的 compoatible 属性查看是否支持此设备,如果支持的话设备就会启动 Linux 内核。如果不支持的话那么这个设备就没法启动 Linux 内核。

  • model属性
    model 属性值是一个字符串,一般 model 属性描述设备模块信息。比如名字什么的,比如: model = "wm8960-audio";

  • status属性
    status 属性和设备状态有关的, status 属性值是字符串,描述设备的状态信息。

    描述
    “okay 表明设备是可操作的
    “disabled” 表明设备当前是不可操作的,但是在未来可以变为可操作的,比如热插拔设备插入以后
    “fail” 表明设备不可操作,设备检测到了一系列的错误,而且设备也不大可能变得可操作
    “fail-sss 含义和“fail”相同,sss 部分是检测到的错误内容
  • #address-cells#size-cells 属性

    用于描述子节点的地址信息,reg属性的address 和 length的字长。

    • #address-cells 属性值决定了子节点 reg 属性中地址信息所占用的字长(32 位),
    • #size-cells 属性值决定了子节点 reg 属性中长度信息所占的字长(32 位)。
    • 子节点的地址信息描述来自于父节点的#address-cells#size-cells的值,而不是该节点本身的值(当前节点的信息是描述子节点的,自己的信息在父节点里)
    //每个“address length”组合表示一个地址范围,
    //其中 address 是起始地址, length 是地址长度,
    //#address-cells 表明 address 这个数据所占用的字长,
    // #size-cells 表明 length 这个数据所占用的字长.
    reg = <address1 length1 address2 length2 address3 length3……>
  • reg属性
    reg 属性一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息, reg 属性的值一般是(address, length)对.

    uart1: serial@02020000 {
        compatible = "fsl,imx6ul-uart",
            "fsl,imx6q-uart", "fsl,imx21-uart";
        reg = <0x02020000 0x4000>;
        interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
        clocks = <&clks IMX6UL_CLK_UART1_IPG>,
            <&clks IMX6UL_CLK_UART1_SERIAL>;
        clock-names = "ipg", "per";
        status = "disabled";
    };

    uart1 的父节点 aips1: aips-bus@02000000 设置了#address-cells = <1>、 #sizecells = <1>,因此 reg 属性中address=0x02020000, length=0x4000。都是字长为1.

  • ranges属性

    • ranges属性值可以为或者**按照( child-bus-address , parent-bus-address , length )**格式编写的数字
    • ranges 是一个地址映射/转换表, ranges 属性每个项目由子地址、父地址和地址空间长度这三部分组成。
    • 如果 ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换
    child-bus-address:子总线地址空间的物理地址,由父节点的#address-cells 确定此物理地址所占用的字长
    parent-bus-address: 父总线地址空间的物理地址,同样由父节点的#address-cells 确定此物理地址所占用的字长
    length: 子地址空间的长度,由父节点的#size-cells 确定此地址长度所占用的字长
  • name 属性
    name 属性值为字符串, name 属性用于记录节点名字, name 属性已经被弃用,不推荐使用 name 属性,一些老的设备树文件可能会使用此属性。

  • device_type 属性
    device_type 属性值为字符串, IEEE 1275 会用到此属性,用于描述设备的 FCode ,但是设备树没有 FCode ,所以此属性也被抛弃了。此属性只能用于 cpu 节点或者 memory 节点。 imx6ull.dtsi 的 cpu0 节点用到了此属性,内容如下所示:

cpu0: cpu@0 {
	compatible = "arm,cortex-a7";
	device_type = "cpu";
	reg = <0>;
	......
};
  • 特殊节点

    在根节点“/”中有两个特殊的子节点: aliaseschosen

    1. aliases

      aliases {
      can0 = &flexcan1;
      can1 = &flexcan2;

      usbphy0 = &usbphy1;
      usbphy1 = &usbphy2;
      };

    aliases 节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。

    ​ 但是,一般会在节点命名的时候会加上 label,然后通过&label来访问节点。

    1. chosen
      chosen 不是一个真实的设备, chosen 节点主要是为了 uboot 向 Linux 内核传递数据(bootargs 参数)。

6.4 OF操作函数

Linux 内核提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_” (称为OF 函数)

6.4.1 查找节点

Linux 内核使用 device_node 结构体来描述一个节点:

struct device_node {
    const char *name; 					/* 节点名字 */
    const char *type; 					/* 设备类型 */
    phandle phandle;
    const char *full_name; 				/* 节点全名 */
    struct fwnode_handle fwnode;

    struct property *properties; 		/* 属性 */
    struct property *deadprops; 		/* removed 属性 */
    struct device_node *parent; 		/* 父节点 */
    struct device_node *child; 			/* 子节点
    ...
}
  • 通过节点名字查找指定的节点:of_find_node_by_name
struct device_node *of_find_node_by_name(struct device_node *from,const char *name)

​ from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
​ name:要查找的节点名字。
​ 返回值: 找到的节点,如果为 NULL 表示查找失败。

  • 通过 device_type 属性查找指定的节点:of_find_node_by_type
struct device_node *of_find_node_by_type(struct device_node *from, const char *type)

​ from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
​ type:要查找的节点对应的 type 字符串, device_type 属性值。
​ 返回值: 找到的节点,如果为 NULL 表示查找失败

  • 通过device_typecompatible两个属性查找指定的节点:of_find_compatible_node
struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compatible)

​ from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
​ type:要查找的节点对应的 type 字符串,device_type 属性值,可以为 NULL
​ compatible: 要查找的节点所对应的 compatible 属性列表。
​ 返回值: 找到的节点,如果为 NULL 表示查找失败

  • 通过 of_device_id 匹配表来查找指定的节点:of_find_matching_node_and_match
struct device_node *of_find_matching_node_and_match(struct device_node *from,
                                            const struct of_device_id *matches,
                                            const struct of_device_id **match)

​ from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
​ matches: of_device_id 匹配表,在此匹配表里面查找节点。
​ match: 找到的匹配的 of_device_id。
​ 返回值: 找到的节点,如果为 NULL 表示查找失败

  • 通过路径来查找指定的节点:of_find_node_by_path
inline struct device_node *of_find_node_by_path(const char *path)

​ path:设备树节点中绝对路径的节点名,可以使用节点的别名
​ 返回值: 找到的节点,如果为 NULL 表示查找失败

6.4.2 获取属性值

Linux 内核中使用结构体 property 表示属性

struct property {
    char *name; /* 属性名字 */
    int length; /* 属性长度 */
    void *value; /* 属性值 */
    struct property *next; /* 下一个属性 */
    unsigned long _flags;
    unsigned int unique_id;
    struct bin_attribute attr;
}
  • 查找指定的属性:of_find_property
property *of_find_property(const struct device_node *np, const char *name, int *lenp)

​ np:设备节点。
​ name: 属性名字。
​ lenp:属性值的字节数,一般为NULL
​ 返回值: 找到的属性。

  • 获取属性中元素的数量(数组):of_property_count_elems_of_size
int of_property_count_elems_of_size(const struct device_node *np,
                                    const char *propname
                                    int elem_size)

​ np:设备节点。
​ proname: 需要统计元素数量的属性名字。
​ elem_size:元素长度。
​ 返回值: 得到的属性元素数量

  • 从属性中获取指定标号的 u32 类型数据值:of_property_read_u32_index
int of_property_read_u32_index(const struct device_node *np, const char *propname, u32 index, u32 *out_value)

​ np:设备节点。
​ proname: 要读取的属性名字。
​ index:要读取的值标号。
​ out_value:读取到的值
​ 返回值: 0 读取成功;
​ 负值: 读取失败,
​ -EINVAL 表示属性不存在
​ -ENODATA 表示没有要读取的数据,
​ -EOVERFLOW 表示属性值列表太小

  • 读取属性中 u8、 u16、 u32 和 u64 类型的数组数据
of_property_read_u8_array
of_property_read_u16_array 
of_property_read_u32_array 
of_property_read_u64_array 
int of_property_read_u8_array(const struct device_node *np, const char *propname, u8 *out_values, size_t sz)

​ np:设备节点。
​ proname: 要读取的属性名字。
​ out_value:读取到的数组值,分别为 u8、 u16、 u32 和 u64。
​ sz: 要读取的数组元素数量。
​ 返回值: 0:读取成功;
​ 负值: 读取失败
​ -EINVAL 表示属性不存在
​ -ENODATA 表示没有要读取的数据
​ -EOVERFLOW 表示属性值列表太小

  • 读取属性中字符串值:of_property_read_string
int of_property_read_string(struct device_node *np, const char *propname, const char **out_string)

​ np:设备节点。
​ proname: 要读取的属性名字。
​ out_string:读取到的字符串值。
​ 返回值: 0,读取成功,负值,读取失败

  • 获取 #address-cells 属性值:of_n_addr_cells ,获取 #size-cells 属性值:of_size_cells
int of_n_addr_cells(struct device_node *np)
int of_n_size_cells(struct device_node *np)

​ np:设备节点。
​ 返回值: 获取到的#address-cells 属性值。
​ 返回值: 获取到的#size-cells 属性值。

  • 内存映射of_iomap

of_iomap 函数用于直接内存映射,前面通过 ioremap 函数来完成物理地址到虚拟地址的映射,采用设备树以后就可以直接通过 of_iomap 函数来获取内存地址所对应的虚拟地址。这样就不用再去先获取reg属性值,再用属性值映射内存。

of_iomap 函数本质上也是将 reg 属性中地址信息转换为虚拟地址,如果 reg 属性有多段的话,可以通过 index 参数指定要完成内存映射的是哪一段, of_iomap 函数原型如下:

void __iomem *of_iomap(struct device_node *np,  int index)

​ np:设备节点。
​ index: reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0。
​ 返回值: 经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败。

例如:

#if 1
	/* 1、寄存器地址映射 */
	IMX6U_CCM_CCGR1 = ioremap(regdata[0], regdata[1]);
	SW_MUX_GPIO1_IO03 = ioremap(regdata[2], regdata[3]);
  	SW_PAD_GPIO1_IO03 = ioremap(regdata[4], regdata[5]);
	GPIO1_DR = ioremap(regdata[6], regdata[7]);
	GPIO1_GDIR = ioremap(regdata[8], regdata[9]);
#else   //第一对:起始地址+大小 -->映射 这样就不用获取reg的值
	IMX6U_CCM_CCGR1 = of_iomap(dtsled.nd, 0); 
	SW_MUX_GPIO1_IO03 = of_iomap(dtsled.nd, 1);
  	SW_PAD_GPIO1_IO03 = of_iomap(dtsled.nd, 2);
	GPIO1_DR = of_iomap(dtsled.nd, 3);
	GPIO1_GDIR = of_iomap(dtsled.nd, 4);
#endif
  • of 函数在 led_init() 中应用
int ret;
u32 regdate[14];
const char *str;
struct property *proper;
/* 1 、获取设备节点: */
dtb_led.nd = of_find_node_by_path("/songwei_led");
if(dtb_led.nd == NULL){
    printk("songwei_led node can not found!\r\n");
    return -EINVAL;
}
else{
    printk("songwei_led node has been found!\r\n");
}

/* 2 、获取 compatible  属性内容 */
proper = of_find_property(dtb_led.nd ,"compatible",NULL);
if(proper == NULL) {
    printk("compatible property find failed\r\n");
} 
else {
    printk("compatible = %s\r\n", (char*)proper->value);
}

/* 3 、获取 status  属性内容 */
ret = of_property_read_string(dtb_led.nd, "status", &str);
if(ret < 0){
    printk("status read failed!\r\n");
}
else {
    printk("status = %s\r\n",str);
}

/* 4 、获取 reg  属性内容 */
ret = of_property_read_u32_array(dtb_led.nd, "reg", regdate, 10);
if(ret < 0) {
    printk("reg property read failed!\r\n");
}
else {
    u8 i = 0;
    printk("reg data:\r\n");
    for(i = 0; i < 10; i++)
        printk("%#X ", regdate[i]);
    printk("\r\n");
}

7.GPIO子系统

GPIO 是 “General Purpose Input/Output” 的缩写,中文意为通用输入/输出。GPIO 是一种在数字电子电路中常见的接口,允许数字设备(如微控制器、处理器、FPGA 等)与外部世界进行通信和交互。

每个 GPIO 引脚通常可以在输入和输出之间切换。作为输入时,GPIO 引脚可以接收来自外部设备或电路的电平信号,例如开关、传感器等。作为输出时,GPIO 引脚可以发送电平信号到外部设备或电路,例如驱动 LED、控制继电器等。

当使用 pinctrl 子系统将引脚的复用设置为 GPIO,可以使用 GPIO 子系统来操作GPIO

7.1 GPIO子系统工作内容

通过 GPIO 子系统功能要实现:

  • 引脚功能的配置(设置为 GPIO,GPIO 的方向, 输入输出模式,读取/设置 GPIO 的值)
  • 实现软硬件的分离(分离出硬件差异, 有厂商提供的底层支持; 软件分层。 驱动只需要调用接口 API 即可操作 GPIO)
  • iommu 内存管理(直接调用宏即可操作 GPIO)

按键输入:左边是接地,右边有那个按键按下的话,哪个GPIO的端口就能接收到低电平信号

image-20240731105411230

按键输出:控制LED灯,当GPIO输出高电平就可以点亮这个LED。

image-20240731105757711

7.2 GPIO子系统设备树设置

在具体设备节点中添加GPIO信息

gpioled {
		#address-cells = <1>;
		#size-cells = <1>;
		compatible = "songwei-gpioled";
		pinctrl-names = "default";
		pinctrl-0 = <&pinctrl_led>;
		//gpio信息
		led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
		status = "okay";
	};
  • led-gpio 属性指定了 LED 灯所使用的 GPIO,在这里就是 GPIO1 的 IO03,低电平有效。
  • 稍后编写驱动程序的时候会获取 led-gpio 属性的内容来得到 GPIO 编号,因为 gpio 子系统的 API 操作函数需要 GPIO 编号

7.3 API函数(gpio是旧的函数,gpiod是新的函数)

  1. gpio_is_valid检测GPIO引脚是否有效函数:
#include <linux/gpio.h>			//GPIO的标准接口函数包含头文件
bool gpio_is_valid(int number);
// int number:GPIO编号
// 返回值:有效返回true,无效返回false。
  1. gpio_request 函数用于申请一个 GPIO 管脚,在使用一个 GPIO 之前一定要使用 gpio_request进行申请。
int gpio_request(unsigned gpio, const char *label);
int gpio_request_array(const struct gpio *array, size_t num); //申请多个GPIO资源函数:
// const struct gpio *array :所要申请的GPIOID
// size_t num :申请的GPIO资源个数
// 返回值:成功返回0,失败返回一个负的错误码。
  • gpio:要申请的 gpio 标号,使用 of_get_named_gpio 函数从设备树获取指定 GPIO 属性信息,此函数会返回这个 GPIO 的标号

  • label:给 gpio 设置个名字。

  • 返回值: 0,申请成功;其他值,申请失败。

  1. gpio_free如果不使用某个 GPIO ,需要调用 gpio_free 函数进行释放。
void gpio_free(unsigned gpio);  // gpio:要释放的 gpio 标号。
void gpio_free_array(const struct gpio *array, size_t num);//释放多个GPIO资源函数:
// const struct gpio *array :所要释放的GPIOID
// size_t num :释放的GPIO资源个数
// 返回值:无返回值。
  1. gpio_direction_input设置某个 GPIO 为输入
int gpio_direction_input(unsigned gpio) //gpio:要设置为输入的 GPIO 标号。
// unsigned gpio:GPIO对应的"ID"
// 返回值:成功返回0,失败返回一个负的错误码。
  1. gpio_direction_output设置某个 GPIO 为输出,并且设置默认输出值。
int gpio_direction_output(unsigned gpio, int value)
  • gpio:要设置为输出的 GPIO 标号”ID”。
  • value: GPIO 默认输出值。初始电平,0为低电平,1为高电平
  • 返回值: 0,设置成功;负值,设置失败
  1. gpio_get_value获取某个 GPIO 的值(0 或 1) (原子操作)
#define gpio_get_value __gpio_get_value
int __gpio_get_value(unsigned gpio)
  • gpio:要获取的 GPIO 标号”ID”。

  • 返回值: 非负值,得到的 GPIO 值(0表示低电平,非零整数表示高电平);负值,获取失败

  1. gpio_set_value 设置某个 GPIO 的值 (原子操作)
#define gpio_set_value __gpio_set_value
void __gpio_set_value(unsigned gpio, int value)
  • gpio:要设置的 GPIO 标号。

  • value: 要设置的值,设置GPIO口的高低电平

  • 返回值:无返回值

以上两种函数均属原子操作,能作用于中断程序
以下两种函数,在队列中等待访问引脚,可能会进入睡眠,不能作用于中断

访问必须通过消息总线比如I2C或者SPI,这些需要在队列中访问。

  1. gpio_get_value_cansleep通过消息总线获取GPIO的输入值函数
int gpio_get_value_cansleep(unsigned gpio);
// GPIO对应的"ID"
// 返回值:成功返回非负数(0表示低电平,非零整数表示高电平),失败返回一个负的错误码。
  1. gpio_set_value_cansleep通过消息总线获设置GPIO的输出值函数
void gpio_set_value_cansleep(unsigned gpio, int value);
// unsigned gpio:GPIO对应的"ID"
// int value:设置GPIO口的高低电平
// 返回值:无返回值
  1. gpiod_cansleep检测引脚是否需要通过消息总线访问函数
int gpiod_cansleep(unsigned gpio);
// unsigned gpio:GPIO对应的"ID"
// 返回值: 返回0表示需要通过消息总线访问,否则返回负数
  1. gpio_to_irq将GPIO设置为中断口函数
int gpio_to_irq(unsigned gpio);
// unsigned gpio:GPIO对应的"ID"
// 返回值:request_irq()使用的中断号
  1. gpio_export导出GPIO端口到用户空间函数
int gpio_export(unsigned gpio,bool direction_may_change);
// unsigned gpio:GPIO对应的"ID"
// direction_may_change:表示用户程序是否允许修改gpio的方向,如果可以则该参数为真
// 返回值:成功返回0。
  1. gpio_unexport撤销GPIO的导出函数
void gpio_unexport(unsigned gpio);
// unsigned gpio:GPIO对应的"ID"
// 返回值:无返回值

7.4 GPIO相关OF函数

  1. of_gpio_named_count
    获取设备树某个属性里面定义了几个GPIO 信息。
int of_gpio_named_count(struct device_node *np, const char *propname)
  • np:设备节点。
  • propname:要统计的 GPIO 属性。
  • 返回值: 正值,统计到的 GPIO 数量;负值,失败
  1. of_gpio_count
    此函数统计的是gpios属性的 GPIO 数量,而 of_gpio_named_count 函数可以统计任意属性的GPIO 信息
int of_gpio_count(struct device_node *np)
  • np:设备节点。
  • 返回值: 正值,统计到的 GPIO 数量;负值,失败
  1. of_get_named_gpio
    获取 GPIO 编号,在Linux 内核中关于 GPIO 的 API 函数都要使用 GPIO 编号,此函数会将设备树中类似<&gpio5 7 GPIO_ACTIVE_LOW>的属性信息转换为对应的 GPIO 编号。
int of_get_named_gpio(struct device_node *np, const char *propname, int index)
  • np:设备节点。
  • propname:包含要获取 GPIO 信息的属性名。
  • index: GPIO 索引,因为一个属性里面可能包含多个 GPIO,此参数指定要获取哪个 GPIO的编号,如果只有一个 GPIO 信息的话此参数为 0。
  • 返回值: 正值,获取到的 GPIO 编号;负值,失败。

7.5 pinctrlgpio子系统使用程序框架

int ret = 0;
/* 1 、获取设备节点:alphaled */
gpio_led.nd = of_find_node_by_path("/gpioled");
if(gpio_led.nd == NULL)
{
    printk("songwei_led node can not found!\r\n");
    return -EINVAL;
}else
{
    printk("songwei_led node has been found!\r\n");
}

/* 2、 获取设备树中的 gpio 属性,得到 LED 所使用的 LED 编号 */
gpio_led.led_gpio = of_get_named_gpio(gpio_led.nd,"led-gpio",0);
if(gpio_led.led_gpio < 0) 
{
    printk("can't get led-gpio");
    return -EINVAL;
}
printk("led-gpio num = %d\r\n", gpio_led.led_gpio);

/* 3、设置 GPIO1_IO03 为输出,并且输出高电平,默认关闭 LED 灯 */
ret = gpio_direction_output(gpio_led.led_gpio, 1);
if(ret < 0) 
{
    printk("can't set gpio!\r\n");
}

8.sys系统节点的创建与使用

sysfs属性节点可以实现用户空间与硬件交互,如设置管教电平,设置寄存器值等,控制驱动的具体功能。

sysfs是一种基于ram文件系统和proc一样。Sysfs文件系统是一个类似于proc文件系统的特殊文件系统,用于将系统中的设备组织成层次结构,并向用户模式程序提供详细的内核数据结构信息。

8.1 sys创建节点函数接口

int sysfs_create_file(struct kobject *kobj, const struct attribute *attr);
// 作用:通过kobject创建sysfs节点
// 参数:	  @kobj:kobject结构指针
//  		@attr:设备属性描述符
// 返回值:成功返回0,否则返回负数
int device_create_file(struct device *, const struct device_attribute *attr);
// 作用:创建sysfs节点    // 创建读写字符串的节点:
// 参数:@dev:设备
//  	@attr:设备属性描述符
// 返回值:成功返回0,否则返回负数
struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt);
// 作用:创建设备并将其注册到系统
// 参数:@class:class结构体指针
//  	@parent:设备节点父节点
 // 	@devt:设备号
 // 	@drvdata:设备的私有数据
//  	@fmt:sys/class/节点下的设备名
// 返回值:struct device结构体类型的指针
struct class *class_create(owner, name);
// 作用:创建class结构体
// 参数:@owner:指向要"拥有"此结构类的模块的指针
//  	@name:指向此类名称的字符串的指针。
// 返回值:struct class结构类型的指针
void kobject_put(struct kobject *kobj);
// 作用:递减kobj内核对象的引用计数 当为0的时候调用在kobject_init()中传入的kobj_type{}结构中包含的kobj释放函数
// 参数:@kobj:递减的内核对象
// 返回值:递减引用计数,如果为0,则调用 kobject_cleanup()。
int sysfs_create_group(struct kobject *kobj, const struct attribute_group *grp);
// 作用:为kobject目录创建一个属性组
// 参数:@kobj:需要创建属性组的kobject
//  @grp:属性组
// 返回值:成功时返回 0,失败时返回错误代码。

下面的 Linux sysfs使用方法随笔引用丨可乐猫丨

8.2 概述

Linux 2.6以后的内核引入了sysfs文件系统, sysfs被看成是与proc、 devfs和devpty同类别的文件系统,该文件系统是一个虚拟的文件系统, 它可以产生一个包括所有系统硬件的层级视图, 与提供进程和状态信息的proc文件系统十分类似。

sysfs把连接在系统上的设备和总线组织成为一个分级的文件, 它们可以由用户空间存取, 向用户空间导出内核数据结构以及它们的属性。 sysfs的一个目的就是展示设备驱动模型中各组件的层次关系, 其顶级目录包括block、 bus、 dev、 devices、 class、 fs、 kernel、 power和firmware等。

8.3 目录结构

image-20240801195239791

8.4 创建sysfs节点

创建读写字符串的节点:device_create_file

这里涉及一个结构体:
device_attribute

struct device_attribute {
	struct attribute        attr;
	ssize_t (*show)(struct device *dev, struct device_attribute *attr, char *buf);
	ssize_t (*store)(struct device *dev, struct device_attribute *attr, const char *buf, size_t count);
};

#define DEVICE_ATTR(_name, _mode, _show, _store) \
	struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)
#define DEVICE_ATTR_RW(_name) \
	struct device_attribute dev_attr_##_name = __ATTR_RW(_name)
#define DEVICE_ATTR_RO(_name) \
	struct device_attribute dev_attr_##_name = __ATTR_RO(_name)
#define DEVICE_ATTR_WO(_name) \
	struct device_attribute dev_attr_##_name = __ATTR_WO(_name)

以一个可以进行字符串读写的AA节点为例:
在代码中需要如下定义:

DEVICE_ATTR_RW(AA) 
#这样就已经定义好了名为 dev_attr_AA 的结构体对象
#同时也定义好了两个方法名称:AA_show()AA_store()

在probe函数中调用创建函数:

device_create_file(&client->dev, &dev_attr_AA);

然后实现AA_show()和AA_store()实体

static ssize_t vmute_show(struct device *dev, struct device_attribute *attr, char *buf){
	unsigned int value;
	......
	......
	return scnprintf(buf, PAGE_SIZE, "%d\n", value);
}

static ssize_t vmute_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count){
	......
	......
	return count;
}

有几点注意项:
1.show()方法应该总是使用snprintf 进行返回
2.store()方法应该返回实际使用的字节数
3.show()和store()方法出错时返回相应的错误码
4.show()和store()方法都是同步通信
5.节点的缓冲区总是为1页,为PAGE_SIZE个字节,PAGE_SIZE=4096

创建读写bin类型的节点:

#函数原型
int __must_check device_create_bin_file(struct device *dev, const struct bin_attribute *attr);
#因为是__must_check,所以该函数的返回值必须进行判定

这里涉及一个结构体:

bin_attribute

struct bin_attribute {
        struct attribute        attr;
		size_t                  size;
		void                    *private;		
		ssize_t (*read)(struct file *, struct kobject *, struct bin_attribute *, char *, loff_t, size_t);		
		ssize_t (*write)(struct file *, struct kobject *, struct bin_attribute *, char *, loff_t, size_t);		
		int (*mmap)(struct file *, struct kobject *, struct bin_attribute *attr, struct vm_area_struct *vma);
};	

#define BIN_ATTR(_name, _mode, _read, _write, _size) struct bin_attribute bin_attr_##_name = __BIN_ATTR(_name, _mode, _read, _write, _size)
#define BIN_ATTR_RO(_name, _size) struct bin_attribute bin_attr_##_name = __BIN_ATTR_RO(_name, _size)
#define BIN_ATTR_WO(_name, _size) struct bin_attribute bin_attr_##_name = __BIN_ATTR_WO(_name, _size)
#define BIN_ATTR_RW(_name, _size) struct bin_attribute bin_attr_##_name = __BIN_ATTR_RW(_name, _size) 

以一个可以进行二进制读写的BB节点为例:
在代码中需要如下定义:

BIN_ATTR_RW(BB, 0x400) 
#这样就已经定义好了名为 bin_attr_BB 的结构体对象
#同时也定义好了两个方法名称:BB_read()BB_write()

在probe函数中调用创建函数:

err = device_create_bin_file(&client->dev, &bin_attr_BB);
if (err)
	......

然后实现BB_read()BB_write()实体

static ssize_t BB_read(struct file *filp,
							struct kobject *kobj, struct bin_attribute *attr,
							char *buf, loff_t offset, size_t count)
{
	......
	......
	return count;
}

static ssize_t BB_write(struct file *filp,
							struct kobject *kobj, struct bin_attribute *attr,
							char *buf, loff_t offset, size_t count)
{
	......
	......
	return count;
}

sysfs_notify 如果内核想主动上报状态,可以使用sysfs_notify方法

#函数原型
void sysfs_notify(struct kobject *kobj, const char *dir, const char *attr);

直接唤醒该字符串

sysfs_notify(&dev->client->dev.kobj, NULL, "AA");	

/sys/module中的parameter

可以用如下方式向模块中传入参数,例:

static bool xx;
#将模块外传进来的参数yy映射到代码中的xx变量,代码中使用xx进行操作
module_param_named(yy, xx, bool, 0644);		
#这句是对yy参数的描述,在#:modinfo mm.ko时,会打印出来,用来描述参数的作用等
MODULE_PARM_DESC(yy, "Force Off");

8.5 用户空间使用sysfs节点

比如:

#第一种
cat  AA			#会调用 AA_show() 方法
echo "xxx" > AA		#会调用 AA_store()方法

#第二种
int fd = open("BB", O_RDWR);
# offset = 1, size = 1
ret = pread(fd, readbuf, 1, 1);		#会调用 BB_read()方法,ret返回读取到的个数,readbuf中是读取到的值
# offset = 1, size = 1
ret = pwrite(fd, writebuf, 1, 1);		#会调用 BB_write()方法,ret返回写入的个数,writebuf中是待写入的值

#第三种
while(1) {
	err = epoll_wait(epfd, pEvent, 1, -1);	#sysfs_notify会唤醒epoll
	if(err) {
		ret = read(fd, read_value, 1);	#会调用 AA_show()
		write(fd, writebuf, sizeof(writebuf)); #会调用 AA_store() 
	}
}

8.6 sysfs节点创建原理

105a481f3f6f801eb440d206751e35e7

二、linux驱动学习

第00章 Linux设备模型的基本概念

Bus, Class, Device和Device Driver的概念

下图是嵌入式系统常见的硬件拓扑的一个示例: 引用来自蜗窝科技

e960493a2c0e405ae9b2fefaf869334220140227080147

硬件拓扑描述Linux设备模型中四个重要概念中三个:Bus,Class和Device(第四个为Device Driver,后面会说)。

  • Bus(总线):Linux认为(可以参考include/linux/device.h中struct bus_type的注释),总线是CPU和一个或多个设备之间信息交互的通道。而为了方便设备模型的抽象,所有的设备都应连接到总线上(无论是CPU内部总线、虚拟的总线还是“platform Bus”)。
  • Class(分类):在Linux设备模型中,Class的概念非常类似面向对象程序设计中的Class(类),它主要是集合具有相似功能或属性的设备,这样就可以抽象出一套可以在多个设备之间共用的数据结构和接口函数。因而从属于相同Class的设备的驱动程序,就不再需要重复定义这些公共资源,直接从Class中继承即可。
  • Device(设备):抽象系统中所有的硬件设备,描述它的名字、属性、从属的Bus、从属的Class等信息。
  • Device Driver(驱动):Linux设备模型用Driver抽象硬件设备的驱动程序,它包含设备初始化、电源管理相关的接口实现。而Linux内核中的驱动开发,基本都围绕该抽象进行(实现所规定的接口函数)。

第01章 Linux内核模块

1.1 Linux内核模块简介

1.什么是内核模块

Linux 模块是一种可加载内核模块,可以在运行时动态地插入和删除,扩展 Linux 内的功能和特性。以下是几个 Linux 模块的特性:

  • 1.动态加载:Linux 模块可以在运行时动态地加载和卸载,而无需重新编译内核或重启系统。这使得开发人员可以更快速地开发和测试
    新的内核功能。
  • 2.代码独立:Linux 模块的代码是独立的,与内核的其余部分分开编译。这意味着即使一个模块崩溃了,也不会影响内核的其余部分。
  • 3,模块参数:Linux 模块可以带有一些参数,这些参数可以在加载模块时由用户指定。这些参数允许用户在不改变内核源代码的情况下修改模块的行为。
  • 4.模块依赖:Linux 模块可以依赖其他模块,这些模块必须在当前模块之前加载,这使得开发人员可以更容易地管理和维护模块之间的依赖关系。
  • 5.模块版本:Linux 模块有一个版本号,这个版本号可以与内核版本号进行比较,以确保模块与内核兼容

linux 模提供了一种录活,高效的方来扩展 nux 内核的特性,使得开发人员可以更读地开发和测计的内核功能,并以在不影响系统稳定性的情况下动态地加载和卸载模块。

2.内核模块与应用程序区别

内核模块用于扩展和定制操作系统的核心功能.

  • 运行在内核空间,具有更高的权限和更广泛的访问权限
  • 运行在用户空间,应用程序则是为了满足用户需求而设计的,受到操作系统的保护
image-20240718161420945

1.2 模块加载和卸载

1.模块开发头文件

模块开发必不可少的3个头文件

#include<linux/module.h>
#include<linux/kerne1.h>
#include<linux/init.h>

模块文件名是以 *.ko文件结束

2.安装模块 insmod

sudo su 						# 必须先获得root权限
insmod *.ko
insmod hello.ko

会执行当前目录下的*.ko 文件内的init_module(linux2.4内核)函数,如果*.ko 文件内没有init _module,就会找module_init(linux2.6以后)函数,执行函数内部的功能,进行驱动的加载。

3.查看模块信息 dmesg

显示安装/卸载信息 dmesg显示后不清除,dmesg -c 显示后清除信息。

4.卸载模块 rmmod

rmmod 模块去除*.ko之前的名称

rmmod hello

会执行当前目录下的*.ko文件,会行文件内的cleanup_module(llinux2.4内核)函数,如果*.ko文件内没有cleanup_module会找
module_exit(linux2.6以后)函数,执行函数内部的功能,进行驱动的卸载

5.查看模块 Ismod

lsmod 显示系统中所有的已经安装的模块信息。
lsmod | grep 名称 用来显示特定模块信息。

lsmod
lsmod | grep hello

6.static关键字

static 修饰函数时和全局变量时,这个函数只能在本文件中使用,不能被其他文件调用,防止函数名重名。
static 修饰局部变量时,这个变量放到静态区,放到程序的data段内,默认值为0。

7.__init__exit 宏的理解

#define _init    __section(.init.text)

如果用_init 关键字修饰时,在连接时,会把这段代码放到.init.text ,这个段内放的是各个驱动的加载函数部分。

#define __exit  __section(.exit.text)

如果用_exit关键字修饰时,在连接时,会把这段代码放到.exit.text,这个段内放的是各个驱动的卸载函数部分

  • .init.text
    • 静态编译:obj-y 静态编译: 编程内核固定的一部分,不可拆卸。
      动态编译:obi-m : 动态编译,编程模块,动态编译: 可拆可卸
    • 静态编译内核时:驱动加载时,会把,.init.text段内的所有加载函数执行一遍,在执行后,系统会把这段内存释放掉,驱动加载代码只执行一次
    • 动态编译(*.KO)内核时: 这个功能没有意义
  • .exit.text
    • 静态编译内核时:直接把这段代码不编译进内核,这段代码就会扔掉,因为静态编译不会卸载模块
    • 动态编译(*.KO)内核时: 这个功能没有意义

8.通用许可

通用许可 MODULE_LICENSE("Dual BSD/GPL")

9.实例1——最简单模块

前提环境得安装了gcc

rpm -q gcc							# 首先可以看看有没有 gcc
sudo apt update
apt list --upgradable

# 安装gcc,这里我们实际上安装的是"build-essential",它包含了 GNU 编辑器集合,GNU 调试器,和其他编译软件所必需的开发库和工具。下面这个命令将会安装一系列软件包,包括gcc,g++,和make。
sudo apt install build-essential
gcc --version 						# 首先可以看看有没有 gcc

最简单模块文件

//hello.c文件
/* Linux内核模块程序示例 */
#include <linux/module.h> /* 包含模块所需的头文件 */
#include <linux/kernel.h> /* 包含向内核打印日志的函数 */
#include <linux/init.h>   /* 包含模块初始化和清理函数的头文件 */

/* 初始化模块时调用的函数 */
/* 该函数没有输入参数和返回值 */
int init_module(void)
{
    /* 向内核日志打印消息,表示模块已加载 */
    printk(KERN_INFO "Hello, world!\n");
    /* 成功加载模块,返回0 */
    return 0;
}

/* 清理模块时调用的函数 */
/* 该函数没有输入参数和返回值 */
void cleanup_module(void)
{
    /* 向内核日志打印消息,表示模块正在卸载 */
    printk(KERN_INFO "Goodbye, world!\n");
}

/*或者用下面的代码替换上面的*/
int __init init_module(void){
	printk(KERN_INFO "Hello, world!\n");
	return 0;
}

void __exit cleanup_module(void){
	printk(KERN_INFO "Goodbye, world!\n");
}
# Makefile文件
$(warning KERNELRELEASE=$(KERNELRELEASE))
ifeq ($(KERNELRELEASE),)

KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

clean:
	rm -rfv *.o *~ core .depend .*.cmd *.ko *.mod.c *.mod .tmp_versions Module* modules*

.PHONY: modules modules_install clean

else
	obj-m:=hello.o

endif

10.查看模块内信息 modinfo

modinfo *.ko
modinfo hello.ko

例如:

filename:       /home/linux/work/01-driver/hello.ko
license:        Dual BSD/GPL
srcversion:     1CCA9D77406976EC4260817
depends:        
retpoline:      Y
name:           hello
vermagic:       5.4.0-150-generic SMP mod_unload modversions 

11.模块作者

MODULE_AUTHOR("GeYangwen");					//添加到头文件的下面就可以显示作者信息

12.模块描述

MODULE_DESCRIPTION("A simple Module");		//添加描述信息

13.内核模块的程序结构

  • 模块加载函数 (必须)
  • 模块卸载函数 (必须)
  • 模块许可证声明 (必须)
  • 模块参数 (可选)
  • 模块导出符号(可选)
  • 模块作者等信息声明 (可选)

14.动态加载模块 modprobe

modprobeinsmod 都是 Linux 内核模块相关的命令行工具,但它们的功能有所不同。
modprobe 用于动态加载和卸载内核模块,并自动解析和加载依赖的模块
insmod 则是一个更底层的工具,它用于手动加内核模块。与 modprobe 不同的是,insmod 不会自动解析和加载依赖的模块,也不
会配置模块参数。因此,通常需要手动加载所有依赖的模块,并手动设置模块参数才能保证模块正常工作。
由于 insmod 不会自动解析和加载依赖的模块,因此在加载模块之前,必须确保所有依赖的模块都已经加载并处于正确的状态.
如果依赖的模块没有被加载或者版本不兼容,可能会导致模块加载失败或产生其他问题。

15.模块Makefile

Makefile组成如下:

# Makefile
# 使用$(warning)函数输出变量KERNELRELEASE的值
$(warning KERNELRELEASE=$(KERNELRELEASE))

# 判断条件,如果KERNELRELEASE为空,则执行以下代码块
ifeq ($(KERNELRELEASE),)

# 定义变量KERNELDIR,如果未定义,则默认值为/lib/modules/当前内核版本/build
# 使用$(shell)函数获取当前内核版本
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
# 使用$(shell)函数获取当前目录路径
PWD := $(shell pwd)

# 定义目标modules,通过$(MAKE)命令在$(KERNELDIR)目录下编译当前目录下的模块
modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
# 清理编译过程中生成的临时文件和目标文件
	rm -rfv *.o *~ core .depend .*.cmd *.mod.c *.mod .tmp_versions Module* modules*

# 定义目标modules_install,通过$(MAKE)命令在$(KERNELDIR)目录下安装当前目录下的模块
modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

# 定义目标clean,用于清理编译过程中生成的临时文件和目标文件
clean:
	rm -rfv *.o *~ core .depend .*.cmd *.ko *.mod.c *.mod .tmp_versions Module* modules*

# 定义伪目标.PHONY,用于标识以下目标为伪目标,即使文件系统中存在同名文件,也不会被认为是该目标的产物
.PHONY: modules modules_install clean

# 条件语句之外的代码块,用于定义obj-m变量,指向当前目录下的hello.o模块文件
else
	obj-m:=hello.o

endif
root@ubuntu:/home/linux/work/01-driver# make

# 第一次
Makefile:3: KERNELRELEASE=
make -C /lib/modules/5.4.0-150-generic/build M=/home/linux/work/01-driver modules
make[1]: 进入目录“/usr/src/linux-headers-5.4.0-150-generic”

# 第二次
/home/linux/work/01-driver/Makefile:3: KERNELRELEASE=5.4.0-150-generic
  CC [M]  /home/linux/work/01-driver/hello.o						# 这个把.c文件编程.o文件
  
# 第三次
/home/linux/work/01-driver/Makefile:3: KERNELRELEASE=5.4.0-150-generic
  Building modules, stage 2.
  MODPOST 1 modules
  CC [M]  /home/linux/work/01-driver/hello.mod.o
  LD [M]  /home/linux/work/01-driver/hello.ko
make[1]: 离开目录“/usr/src/linux-headers-5.4.0-150-generic”
# 清理编译过程中生成的临时文件和目标文件
rm -rfv *.o *~ core .depend .*.cmd *.mod.c *.mod .tmp_versions Module* modules*
已删除'hello.mod.o'
已删除'hello.o'
已删除'.hello.ko.cmd'
已删除'.hello.mod.cmd'
已删除'.hello.mod.o.cmd'
已删除'.hello.o.cmd'
已删除'hello.mod.c'
已删除'hello.mod'
已删除'Module.symvers'
已删除'modules.order'

ll /lib/modules/5.4.0-150-generic/build			# 这个是软连接
lrwxrwxrwx 1 root root 40 519  2023 /lib/modules/5.4.0-150-generic/build -> /usr/src/linux-headers-5.4.0-150-generic/

Makefile运行过程分析,
这个Makefile一共执行了3次

  • 第1次执行Makefile
# 在当前目录下输入make 后,会执行当前目录下的Makefile
# 第一次执行Makefile
$(warning KERNELRELEASE=S(KERNELRELEASE))		#打印变量内的值
# 因为没有没有赋值,KERNELRELEASE =空
# MakefiTe:1: KERNELRELEASE=

# 条件为真
ifeq ($(KERNELRELEASE),)	
KERNELDIR ?= /lib/modules/$(she11 uname -r)/build		# (本地开发) 等于
# /lib/modules/5.4.0-150-generic/build 这个目录存放的是编译内核必须的一些头文件和库

PWD := /home/1inux/1inux-driver/01-day/02-char-module_init
# 在输入make后默认会执行第一个目标,第一目标:modules:
# 进而执行: S(MAKE) -C $(KERNELDIR) M=S(PWD) modules
# $(MAKE) = make
# make -c : 进入子目录(/lib/modules/5.4.0-150-generic/build) 下,去执行子目录下的Makefile
# M =$(PWD) :/home/linux/work/01-driver   # 这个是我们所在的路径
# modules : 告诉内核我要编译模块
  • 第2次执行Makefile
# 第二次: 执行子目录下的Makefile后,会再次调用当前目录下的Makefile
# 在执行子目录(/lib/modules/5.4.0-150-generic/build)下的Makefile后,会对KERNELRELEASE 进行赋值KERNELRELEASE=5.4.0-150-generic
# ifeq(S(KERNELRELEASE),条件为假
# 执行e1se分支
# obj-m := hello.o(obj-m 表示的要编译成模块所依赖的*.o文件)(动态编译)
# obj-y :静态编译
# 执行后会把hello.c编译程hello.o文件
  • 第3次执行Makefile
# 执行后会把hello.o编译为hello.ko文件

16.实例2——模块安装与卸载

实现模块的安装与卸载
module_init()使用
module_exit()使用
源文件和源代码

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h> 

/* 
 * 设置模块的许可协议。
 * 设置模块的作者信息。
 * 设置模块的描述信息。
 */
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("GeYangwen");
MODULE_DESCRIPTION("A simple Hello world module");
int __init hello_int(void)
{
	printk(KERN_INFO "Hello, world!\n");
	return 0;
}

void __exit hello_exit(void)
{
	printk(KERN_INFO "Goodbye, world!\n");
}

module_init(hello_int);
module_exit(hello_exit);

程序解释

oot@ubuntu:/home/linux/work/02-driver# insmod hello.ko 
root@ubuntu:/home/linux/work/02-driver# ls
hello.c  hello.ko  Makefile
root@ubuntu:/home/linux/work/02-driver# dmesg
[13230.030136] Hello, world!
root@ubuntu:/home/linux/work/02-driver# lsmod | grep hello
hello                  16384  0
root@ubuntu:/home/linux/work/02-driver# rmmod hello
root@ubuntu:/home/linux/work/02-driver# dmesg
[13230.030136] Hello, world!
[13293.781785] Goodbye, world!
root@ubuntu:/home/linux/work/02-driver# lsmod | grep hello

insmod hello.ko 会执行module_init函数,进而去执行hello_init函数,最后执行printk(KERN_INFO "Hello, world!\n");
rmmod hello 会执行module_exit函数,进而去执行hello_exit函数,最后执行printk(KERN_INFO "Goodbye, world!\n");

1.3 模块参数

模块参数是指内核模块中可以被用户动态设置的变量。内核模块需要根据不同的应用场景和用户需求来进行定制和配置。这时就需要一些参数来进行调整设置。
如果需要在内核模块中支持参数,需要使用module_param()宏进行注册。这个宏接受三个参数:参数名、参数类型和权限

1.module_param 注册模块参数

  • 函数原型

/*
convenience many standard types are provided but
you can create your own by defining those variables.

standard types are:
byte, short, ushort, int, uint, long, ulong
charp: a character pointer
boo1: a boo1, values 0/1, y/n, Y/N.
invboo1: the above, only sense-reversed (N = true)
*/
#define module_param(name,type,perm)
module_param_named(name,name,type,perm)	
  • 函数功能
    • 注册一个模块参数
  • 函数参数
    • name:变量的名称
    • type:变量的类型可以是 short int long charp uint ushort ulong bool invbool
    • perm:权限,表示变量的权限(一般的权限是 SIRUGO 是用户、组、其它用户都可以读,但是不可以写)
#define S_IRUGO	(S_IRUSR | S_IRGRP | S_IROTH)				// 这是用户| 所在组| 其他   的权限

2.MODULE_PARAM_DESC参数描述

  • 函数原型

    #define MODULE_PARM_DESC(_parm,desc)
  • 函数功能

    • 参数描述
  • 函数参数I

    • _parm:表示参数的名称
    • desc:表示参数的描述

例如如:

// short类型:			
module_param(myshort, short, S_IRUGO);			//注册一个参数
MODULE_PARM_DESC(myshort, "A short integer");
// 运行时:
insmod hello.ko myshort=100

// int 类型:
module_param(myint, int, S_IRUGO);
MODULE_PARM_DESC(myint, "An int integer");
// 运行时:
insmod hello.ko myint=100

3.module_param_array传递数组

  • 函数原型

    #define module_param_array(name,type,nump,perm)

    函数功能

    • 传入一个数组
  • 函数参数

    • name:传入的数组名称

    • type: 数组的类型

    • nump:数组成员个数的指针

    • perm:权限,表示变量的权限
      一般的权限是 S_IRUGO ,用户、组、其它用户都可以读,但是不可以写

      #define S_IRUGO(S_IRUSR|S_IRGRP|S_IROTH)

案例

/* hello.c*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h> 

/* 
 * 设置模块的许可协议。
 * 设置模块的作者信息。
 * 设置模块的描述信息。
 */
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("GeYangwen");
MODULE_DESCRIPTION("A simple Hello world module");

static short myshort = 1;
static int myint = 4200;
static long mylong = 1000000;
static char *mycharp = "a char pointer";
static int myint_array[6] = {2, 4, 8, 16, 32, 64};
static int num = sizeof(myint_array) / sizeof(int);

module_param(myshort, short, S_IRUGO);
MODULE_PARM_DESC(myshort, "A short integer");

module_param(myint, int, S_IRUGO);
MODULE_PARM_DESC(myint, "An int integer");

module_param(mylong, long, S_IRUGO);
MODULE_PARM_DESC(mylong, "A long integer");

module_param(mycharp, charp, S_IRUGO);						//传参字符串
MODULE_PARM_DESC(mycharp, "A charp integer");

module_param_array(myint_array, int, &num, S_IRUGO);		//传参数组
MODULE_PARM_DESC(myint_array, "A myint_array integer");



static int __init hello_int(void){
	printk(KERN_INFO "hello, driver install sucessed!\n");
	int i;
	printk(KERN_INFO "myshort = %d\n",myshort);
	printk(KERN_INFO "myint = %d\n",myint);
	printk(KERN_INFO "mylong = %ld\n",mylong);
	printk(KERN_INFO "mycharp = %s\n",mycharp);
	for(i = 0 ; i < num ; i++){
		printk(KERN_INFO "myint_array[%d] = %d\n",i,myint_array[i]);
	}

	return 0;
}
static void __exit hello_exit(void){
	printk(KERN_INFO "hello, driver uninstall sucessed!\n");
}

module_init(hello_int);			//宏函数做关联
module_exit(hello_exit);

加载命令如下:

sudo su
dmesg -c
dmesg -c
insmod hello.ko		#直接加载
dmesg
rmmod hello
dmesg -c
dmesg -c

insmod hello.ko myshort=1 myint=2 mylong=3 mycharp="Geyangwen" myint_array=1,2,3,4		#带参数加载
dmesg
rmmod hello
dmesg -c
dmesg -c

1.4 导出符号

在Linux 内核中,导出符号是指将函数、变量或常量等代码标记为可由其他模块使用的符号。如果一个模块中定义了一个函数或变量,并希望其他模块能够调用或引用它,那么就需要将该符号导出。

在内核中,通过 EXPORT_SYMBOLEXPORT_SYMBOL_GPL 宏来实现符号导出。这两个宏的定义如下

void EXPORT_SYMBOL(symbol_name):
void EXPORT_SYMBOL_GPL(symbo1_name);

其中,symbol_name 是要导出的符号名称,其类型可以是函数、变量或常量等.

相比而言,EXPORT_SYMBOL_GPL 宏只允 GPL协议的模块使用该符号。这是因为许多内核开发人员认为,只有遵循 GPL协议的模块才应该使用内核中的某些符号,以确保内核的代码质量和安全性。

需要注意的是,在内核开发中,不建议随意导出符号,因为这可能会对内核的安全性和稳定性造成不利影响。只有在必要的情况下才应该将符号导出。
另外,导出符号的过程也需要仔细考虑,以确保导出的符号不会被误用或滥用。

第02章 Linux字符设备驱动

2.1 设备驱动介绍

1.设备驱动分类

设备驱动可以分为下面三类

  • 字符设备驱动
  • 块设备驱动(简单地理解就是储存类的设备)
  • 网络设备驱动

2.设备驱动在内核中的结构

。视图1

image-20240722105304506

  • 视图2

image-20240722105345028

2.2 字符设备驱动原理

1.设备号理解

  • 包括主设备号和次设备号

  • 主设备号: 表示是哪一类的驱驱动 usb、adc、led、uart。

  • 通过 cat /proc/devices 显示系统设备号。

  • 字符设备和块设备的设备号是独立的

  • 次设备号 用来表示这一类设备中的哪一个设备,用这些命令可以查看 ls /dev/tty ,ls /dev/tty* -l

  • ```shell
    crw-rw-rw- 1 root tty 5, 0 7月 22 10:20 /dev/tty # 5表示主设备号,0表示次设备号
    crw–w—- 1 root tty 4, 0 7月 22 09:14 /dev/tty0
    crw–w—- 1 linux tty 4, 1 7月 22 09:14 /dev/tty1
    crw–w—- 1 root tty 4, 10 7月 22 09:14 /dev/tty10
    crw–w—- 1 root tty 4, 11 7月 22 09:14 /dev/tty11
    crw–w—- 1 root tty 4, 12 7月 22 09:14 /dev/tty12

    
    - 设备号内核用一个变量来表示 `dev_t(u32)`来表示一个设备号       u32表示无符号的32位的变量
    
    - 高20为来表示主设备号 低12为表示次设备号
      主设备号: 表示范围 0~ (1M-1)
      次设备号: 表示范围 0~ (4096-1)
    
    - 可以使用一个宏函数去生成一个设备号
    
    ```c
    #define MINORBITS 20
    #define MKDEV(ma,mi)   (((ma) << MINORBITS)|(mi))		//ma表示住设备号,mi表示次设备号, 主设备号左移20位与上次设备号
    dev_t devno = MKDEV(500,0)		//生成一个设备号	

2.register_chrdev_region静态申请设备号

。函数原型

#include<linux/fs.h>
int register_chrdev_region(dev_t from, unsigned count, const char *name)
  • 函数功能

    • 静态申请一个范围内的设备号
  • 函数参数

    • form:从哪一个设备号开始向内核去申请设备号,向内核去申请一个或多个设备号

      • 如果这个设备号内核没有占用,可以申请到。

      • 否则申请失败。

    • count:表示要申请设备的个数

    • name:要申请设备号的名称
      函数返回值:
      成功返回0,失败返回负数(错误码)

例如:

int ret;
dev_t devno = MKDEV(hello_major, hello_minor);
ret = register_chrdev_region(devno, 1, "hello");
if(ret < 0){
    printk(KERN_WARNING "hello, can't get major %d\n", hello_major);
    return ret;
}

使用命令去查看申请到的设备号 cat /proc/devices

3.alloc_chrdev_region动态申请设备号

函数原型

函数原型

#include<linux/fs.h>
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name)
  • 函数功能
    • 动态申请设备号 I
    • 随机分配一个设备号,一个可用的设备号
  • 函数参数
    • dev: 设备号变量的指针
    • baseminor:次设备号开始于那一个数字
    • count:需要的多少个设备
    • name: 要申请设备的名称
  • 函数返回值:
    成功返回0,失败负数

4.unregister_chrdev_region注销设备号

函数原型

#include<linux/fs.h>
void unregister_chrdev_region(dev_t from, unsigned count);
  • 函数功能
    • 注销设备号
  • 函数参数
    • form:要释放的设备号
    • count: 要释放几个设备号
  • 函数返回值:
    • 无返回值

补充 tags 索引搜索系统

在内核源码路径下使用命令生成索引

make tags

在内核源码的顶层目录下查找索引

vi -t 函数名/结构体名/变量名
vi -t MKDEV
vi -t unregister_chrdev_region

可以找到函数的定义和结果体定义

  • 可以使用ctrl +]进行函数跟踪或查找系统所有定义
  • 使用ctrl + t 返回上一级目录

5.cdev_init字符设备初始化

  • 函数原型 cdev——char devices
#include<linux/cdev.h>

struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;       /* 这些函数定义了字符设备的具体行为,如读、写、打开、关闭等操作 */
    struct list_head list;                  /* list用于将cdev结构体实例链接到一个列表中 */
    dev_t dev;                               /* dev存储了字符设备的设备号 */
    unsigned int count;                      /* count用于记录引用这个字符设备的次数。当count为0时,表示没有进程正在使用这个设备,可以进行安全的删除操作 */
}

void cdev_init(struct cdev *cdev, const struct file_operations *fops)
  • 函数功能

    • cdev_init()初始化一个cdev结构体,把cdev这个结构体和file_operations结构体进行关联
  • 函数参数

    • cdev: 要初始化的cdev结构体指
    • fops: 要初始化的file_operations结构体指针
  • 函数返回值:
    无返回值

  • 例如:

    cdev.owner = THIS_MODULE: 		// 表示的是指向本模块的指针
    #define THIS_MODULE (&__this_module)

6.file_operations结构体

file_operations结构体是字符设备驱动所支持的文件操作

struct file_operations { 
    struct module *owner;//拥有该结构的模块的指针,一般为THIS_MODULES 
    loff_t (*llseek) (struct file *, loff_t, int);//用来修改文件当前的读写位置 
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);//从设备中同步读取数据 
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);//向设备发送数据
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);//初始化一个异步的读取操作 
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);//初始化一个异步的写入操作 
    int (*readdir) (struct file *, void *, filldir_t);//仅用于读取目录,对于设备文件,该字段为NULL 
    unsigned int (*poll) (struct file *, struct poll_table_struct *); //轮询函数,判断目前是否可以进行非阻塞的读写或写入
    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); //执行设备I/O控制命令 
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); //不使用BLK文件系统,将使用此种函数指针代替ioctl 
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long); //在64位系统上,32位的ioctl调用将使用此函数指针代替 
    int (*mmap) (struct file *, struct vm_area_struct *); //用于请求将设备内存映射到进程地址空间
    int (*open) (struct inode *, struct file *); //打开 
    int (*flush) (struct file *, fl_owner_t id); 
    int (*release) (struct inode *, struct file *); //关闭 
    int (*fsync) (struct file *, struct dentry *, int datasync); //刷新待处理的数据 
    int (*aio_fsync) (struct kiocb *, int datasync); //异步刷新待处理的数据 
    int (*fasync) (int, struct file *, int); //通知设备FASYNC标志发生变化 
    int (*lock) (struct file *, int, struct file_lock *); 
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); 
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); 
    int (*check_flags)(int); 
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); 
    int (*setlease)(struct file *, long, struct file_lock **); 
};

用户进程利用在对设备文件进行诸如read/write操作的时候,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数,这是Linux的设备驱动程序工作的基本原理。

例如:

struct cdev cdev;
struct file_operations hello_fops = {
    .owner = THIS MODULE
}

//表示的是指向本模块的指针
//cdev.owner = THIS_MODULE
//#define THIS_MODULE (&__this_module)
static void char_reg_setup_cdev (void){
    int error, devno = MKDEV (he11o_major, he11o_minor);
    cdev_init (&cdev,&hello_fops);			//把上面定义的两个结构体关联到一起,我用我的指针保存你的地址
    cdev.owner = THIS_MODULE;
    cdev.ops = &hello_fops;
    error = cdev_add (&cdev, devno ,1);
}

if (error < 0 ){
    printk (KERN_NOTICE "Error %d adding char_reg_setup_cdev", error);
}

7.cdev_add字符设备添加到内核链表内

  • 函数原型
#include<linux/cdev.h>
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
  • 函数功能
    • 字符设备添加到内核链表内
  • 函数参数
    • cdev: 要初始化的结构体指针
    • fops: 要初始化的结构体指针
  • 函数返回值:
    • 成功返回0;
    • 失败返回负数的错误码

8.cdev_del从系统链表中删除一个设备

  • 函数原型

    #include<linux/cdev.h>
    void cdev_de1(struct cdev *p)
  • 函数功能

    • 从系统链表中删除一个字符设备
  • 函数参数

    • cdev: 要删除的结构体指针
  • 函数返回值:

    • 返回值

9.mknod 手动创建设备节点

  • 创建设备文件,把设备号和文件进行关联

    mknod /dev/hello c 500 0
    crw-r--r-- 1 root root 250,0 730 16:46 /dev/hello

10.实例3——字符设备实现

字符设备实现,对设备进行打开与关闭操作
源文件

/*hello.c*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h> 
#include <linux/fs.h> 
#include <linux/cdev.h> 

/* 
 * 设置模块的许可协议。
 * 设置模块的作者信息。
 * 设置模块的描述信息。
 */
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("GeYangwen");
MODULE_DESCRIPTION("A simple Hello world module");

int hello_major = 500;
int hello_minor = 0;
static int hello_open(struct inode *inode, struct file *filp){
	printk(KERN_INFO "hello_driver打开成功!\n");
	return 0;
}
static int hello_release(struct inode *inode, struct file *filp){
	printk(KERN_INFO "hello_driver关闭成功!\n");
	return 0;
}

struct cdev cdev = {
	.owner = THIS_MODULE						//表示的是指向本模块的指针
};
struct file_operations hello_fops = {
	.owner = THIS_MODULE,
	.open = hello_open,
	.release = hello_release
};
static int __init hello_int(void){
	int ret;
	dev_t devno = MKDEV(hello_major, hello_minor);
	ret = register_chrdev_region(devno, 1, "hello");
	if(ret < 0){
		printk(KERN_WARNING "错误:设备号申请失败: %d\n", hello_major);
		return ret;
	}
	
	memset(&cdev, 0, sizeof(struct cdev));		// 使用memset()函数初始化cdev结构体
	cdev_init(&cdev, &hello_fops);
	ret = cdev_add(&cdev, devno, 1);			/* 向内核链表添加一个cdev结构体 */
	if(ret < 0){
		printk(KERN_WARNING "错误:添加cdev失败\n");
		return ret;
	}

	printk(KERN_INFO "hello_driver installed sucessed!\n");
	return 0;
}


static void __exit hello_exit(void){
	dev_t devno = MKDEV(hello_major, hello_minor);
	cdev_del(&cdev);				/* 从内核链表中删除指定的cdev */
	unregister_chrdev_region(devno, 1);
	printk(KERN_INFO "hello_driver uninstall sucessed!\n");
}

module_init(hello_int);			//宏函数做关联
module_exit(hello_exit);
/*main.c*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>  
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char *argv[]){
    int fd = open("/dev/hello", O_RDWR);
    if(fd == -1){
        perror("open");
        exit(-1);
    }
    printf("fd = %d\n",fd);
    close(fd);

    return 0;
}
# Makefile
$(warning KERNELRELEASE=$(KERNELRELEASE))		# 使用$(warning)函数输出变量KERNELRELEASE的值

ifeq ($(KERNELRELEASE),)	# 判断条件,如果KERNELRELEASE为空,则执行以下代码块

# 定义变量KERNELDIR,如果未定义,则默认值为/lib/modules/当前内核版本/build
# 使用$(shell)函数获取当前内核版本
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
# 使用$(shell)函数获取当前目录路径
PWD := $(shell pwd)

# 定义目标modules,通过$(MAKE)命令在$(KERNELDIR)目录下编译当前目录下的模块
modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
# 清理编译过程中生成的临时文件和目标文件
	gcc main.c -o main
	rm -rfv *.o *~ core .depend .*.cmd *.mod.c *.mod .tmp_versions Module* modules*

# 定义目标modules_install,通过$(MAKE)命令在$(KERNELDIR)目录下安装当前目录下的模块
modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

# 定义目标clean,用于清理编译过程中生成的临时文件和目标文件
clean:
	rm -rfv *.o *~ core .depend .*.cmd *.ko *.mod.c *.mod .tmp_versions Module* modules*
	rm -rfv main

# 定义伪目标.PHONY,用于标识以下目标为伪目标,即使文件系统中存在同名文件,也不会被认为是该目标的产物
.PHONY: modules modules_install clean

# 条件语句之外的代码块,用于定义obj-m变量,指向当前目录下的hello.o模块文件
else
	obj-m:=hello.o

endif
# 执行的步骤
make
dmesg -c
insmod hello.ko 
dmesg 
cat /proc/devices | grep hello
# 500 hello
lsmod | grep hello
# hello                  16384  0
mknod /dev/hello c 500 0				# 创建一个设备,让它和500 0这个设备号关联起来
ls -l /dev/hello 
crw-r--r-- 1 root root 500, 0 723 16:41 /dev/hello
./main
dmesg
rmmod hello
dmesg 
rm -rfv /dev/hello 
已删除'/dev/hello'

11.字符设备驱动调用原理

image-20240723164959660

1.在驱动程序中,首先要创建一个cdev结构

struct cdev{
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;
    struct list_head list;
    dev_t dev;
    unsigned int count;
};
// 可以手动创建一个cdev结构体加上cdev_init,也可以使用cdev_al1oc函数去创建
struct cdev my_cdev;
cdev_init(&my_cdev,&fops);
my_cdev.owner = THIS_MODULE:

2.静态创建结构体后有要对结构体进行初始化,使用cdev_init函数进行,cdev_init有两个参数

void cdev_init(struct cdev *cdev, const struct file_operations *fops){	
    memset(cdev,0sizeof *cdev);
    INIT_LIST_HEAD(&cdev->list);
    kobject_init(&cdev->kobj,&ktype_cdev_default);
    cdev->ops = fops;
}
//把结构体清除为0,然后赋了一个 cdev->ops 的值,因此cdev->ops 指向ops结构体

3.使用cdev_add把设备好加入到cdev组成的链表当中。

int cdev_add(struct cdev *p, dev_t dev, unsigned count){
    p->dev = dev;
    p->count = count;
    return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);	//往链表里面插入节点
}
// 把设备号加入到cdev结构体中,因此cdev结构体中的成员ops指向驱动当中的ops结构体,dev成员是保存的设备号

4.mknod 时会在内核中创建一个inode的结构体,这里只写inode重要的成员信息

struct inode f{
    dev_t i_rdev;//该成员表示设备文件的inode结构,它包含了真正的设备编号。
    
    // 该成员表示字符设备的内核的 内部结构。当inode指向一个字符设备文件时,该成员包含了指向struct cdev结构的
    // 其中cdev结构是字符设备结构体。
    struct cdev *i_cdev;
    
}

//执行mknod时会对这个结构体进行赋值,怎么赋值的呢?
//首先会对i_rdev 这个成员赋值,会对成员赋值为相应的设备号。
//然后对i_cdev这个指针赋值,这个是通过查询系统的cdev链表实现的,根据主设备号和次设备号找到cdev这个结构
//因此i_cdev= &cdev.

5.应用程序调用 open("/dev/hello")这个函数时

/*
应用程序调用 open("/dev/he11o")这个函数时,内核会调用sys_open,继续向下调用
chrdev_open(struct inode * inode, struct file * filp)
内核会在调用open函数时创建struct fie 结构体,并且会对fe结构体的成员f_op赋值为驱动中的file_operatior结构体(通过设备号可以找到inode结构体,inode结构体中有i_cdev指针,指向系统的cdev的结构体,cdev的成员ops指向驱动的file_operation成员hel1o_ops),应用程序调用open,驱动中也会调用fie_operation的open函数,故系统正确的调用应用程序调用open后,对于驱动来说,filep->f_op = hel1o_ops,最终会调用驱动的open函数
*/

struct file {
	struct path f_path;
    const struct file operations *f_op;
    spinlock_t 		f_lock;
    atomic_long_t 	f_count;
    struct inode 	*f_inode;
};

2.3 字符设备读写数据

1.file_operationsread接口

  • 函数原型

#include<linux/uaccess.h>
ssize_t (*read) (struct file *filep, char __user *to, size_t count, loff_t *off);//从设备中同步读取数据 
  • 函数功能
    • 把驱动中的数据复制到应用程序内
  • 函数参数
    • filep: 指向内核创建的file结构体指针
    • to: 要向用户空间发送数据的指针,应用程序的地址
      • __user: 表示应用程序的地址
    • count: 用户空间要读取的字节数
    • off :文件指针的偏移量
  • 函数返回值
    • 成功返回:发送到应用程序的字节数
    • 失败返回:负数

2. copy_to_user

  • 函数原型
#include<linux/uaccess.h>
extern inline long copy_to_user(void __user *to, const void *from, long n){
    return __copy_tofrom_user((__force void *)to, from, n, to);
}
  • 函数功能
    把驱动中的数据复制到应用程序内
  • 函数参数
    。 to:数据的目的,应用程序的地址
    。 from: 数据的源,驱动的地址
    。n:数据的大小
  • 函数返回值
    成功返回:0
    失败返回:负数

3.file_operationswrite接口

  • 函数原型
#include<linux/uaccess.h>
ssize_t (*write) (struct file *, const char __user *from, size_t count, loff_t *off);
  • 函数功能
    • 把应用程序内的数据写入到驱动内
  • 函数参数
    • filep: 指向内核创建的file结构体指针
    • from :要接受用户空间发给内核空间数据的指针,应用程序的地址
    • count:用户空间要写的字节数
    • off: 文件指针的偏移量
  • 函数返回值
    • 成功返回写入到驱动的字节数
    • 失败返回负数

4.copy_from_user

  • 函数原型
#include<linux/uaccess.h>
extern inline long copy_from_user(void *to, const void __user *from, long n){
    return __copy_tofrom_user(to, (__force void *)from, n, from);
}
  • 函数功能
    • 把应用程序中的数据复制到驱动内
  • 函数参数
    • to: 数据的目的,驱动中的地址
    • from: 数据的源,应用程序的地址
    • n:数据的大小
  • 函数返回值:
    • 成功返回写入到驱动中的字节数
    • 失败返回负数

5.实例4——字符设备的读写

/*hello.c*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h> 
#include <linux/fs.h> 
#include <linux/cdev.h> 
#include <linux/uaccess.h>

int hello_major = 500;
int hello_minor = 0;
char dribuf[128] = {0};

static int hello_open(struct inode *inode, struct file *filp){
	printk(KERN_INFO "hello_driver打开成功!\n");
	return 0;
}

static int hello_release(struct inode *inode, struct file *filp){
	printk(KERN_INFO "hello_driver关闭成功!\n");
	return 0;
}

static ssize_t hello_read(struct file *filp, char __user *usrbuf, size_t count, loff_t *f_pos){
	ssize_t ret = 0;
	ret = copy_to_user(usrbuf, dribuf, count);
	if(ret<0){
		printk(KERN_WARNING "错误:应用程序读取驱动内的数据失败\n");
		return ret;
	}
	else{
		printk(KERN_INFO "应用程序读取驱动内的数据成功, 读取了%ld bytes\n",count);
	}
	return ret;
}

static ssize_t hello_write(struct file *filp, const char __user *usrbuf, size_t count, loff_t *f_pos){
	ssize_t ret = 0;
	ret = copy_from_user(dribuf, usrbuf, count);
	if(ret<0){
		printk(KERN_WARNING "错误:应用程序写给驱动数据失败\n");
		return ret;
	}
	else{
		printk(KERN_INFO "应用程序写给驱动数据成功, 用户把数据写进驱动程序了,写了%ld bytes\n",count);
	}
	return ret;
}

struct cdev cdev = {
	.owner = THIS_MODULE						//表示的是指向本模块的指针
};
struct file_operations hello_fops = {
	.owner = THIS_MODULE,
	.open = hello_open,
	.release = hello_release,
	.read = hello_read,
	.write = hello_write,
};

static int __init hello_int(void){
	int ret;
	dev_t devno = MKDEV(hello_major, hello_minor);
	ret = register_chrdev_region(devno, 1, "hello");
	if(ret < 0){
		printk(KERN_WARNING "错误:设备号申请失败: %d\n", hello_major);
		return ret;
	}
	
	memset(&cdev, 0, sizeof(struct cdev));		// 使用memset()函数初始化cdev结构体
	cdev_init(&cdev, &hello_fops);
	ret = cdev_add(&cdev, devno, 1);			/* 向内核链表添加一个cdev结构体 */
	if(ret < 0){
		printk(KERN_WARNING "错误:添加cdev失败\n");
		return ret;
	}

	printk(KERN_INFO "hello_driver installed sucessed!\n");
	return 0;
}

static void __exit hello_exit(void){
	dev_t devno = MKDEV(hello_major, hello_minor);
	cdev_del(&cdev);				/* 从内核链表中删除指定的cdev */
	unregister_chrdev_region(devno, 1);
	printk(KERN_INFO "hello_driver uninstall sucessed!\n");
}

module_init(hello_int);			//宏函数做关联
module_exit(hello_exit);

/* 
 * 设置模块的许可协议。
 * 设置模块的作者信息。
 * 设置模块的描述信息。
 */
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("GeYangwen");
MODULE_DESCRIPTION("A simple Hello world module");
/*main.c*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>  
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char *argv[]){
    char buf[] = "hello world";

    int fd = open("/dev/hello", O_RDWR);
    if(fd == -1){
        perror("open");
        exit(-1);
    }
    printf("fd = %d\n",fd);

    memset(buf, '0', sizeof(buf)/sizeof(buf[0])-1);                          //留一个'\0'
    int ret = write(fd, buf, sizeof(buf)/sizeof(buf[0]));
    if(ret == -1){
        perror("write");
        exit(-1);
    }
    memset(buf, 0, sizeof(buf)/sizeof(buf[0]));                          //清空buf内的东西

    ret = read(fd, buf, sizeof(buf)/sizeof(buf[0]));
    if(ret == -1){
        perror("read");
        exit(-1);
    }
    printf("buf = %s\n", buf);
    
    close(fd);
    return 0;
}

makefile文件继续沿用上面的案列

以下是相关的命令调试

dmesg -c
make
insmod hello.ko 
dmesg 
cat /proc/devices | grep hello
lsmod | grep hello
ll /dev/hello 

./main
fd = 3
buf = 00000000000

dmesg
[ 4734.210647] hello_driver installed sucessed!
[ 4819.517357] hello_driver打开成功!
[ 4819.517447] 应用程序写给驱动数据成功, 用户把数据写进驱动程序了,写了12 bytes
[ 4819.517448] 应用程序读取驱动内的数据成功, 读取了12 bytes
[ 4819.517452] hello_driver关闭成功!

ll /dev/hello 
rmmod hello
dmesg
dmesg -c

2.4 字符设备的控制

1.ioctl 控制设备

  • 函数原型
#include <sys/ioctl.h>										//这是应用层的
int ioct1(int fd, unsigned long request, ...);
  • 函数功能
    • 给设备发送控制命令,是linux的系统调用函数
  • 函数参数
    • fd:要操作的文件描述符
    • request:要发送的命令
  • 函数返回值:
    • 成功返回: 0
    • 失败返回 :-1

2. file_operationsunlocked_ioctl接口

  • 函数原型
#include<linux/fs.h>
#include<asm/ioct1.h>
long (*unlocked_ioctl) (struct file * fd, unsigned int cmd, unsigned long arg);
  • 函数功能
    • 应用程序调用ioctl, 则驱动调用unlocked_ioctl接口函数
  • 函数参数
    • fd:要操作的文件描述符
    • cmd:应用程序要发送的命令
    • arg: ioct1传递的第三个参数(可变的)
  • 函数返回值:
    • 成功返回 : 0
    • 失败返回 : 负数的错误码

3.ioctl命令定义格式

  • 函数原型
#define _IO(type,nr) 			_IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size)  	_IOC(_IOC_READ,(type),(nr),sizeof(size))
#define _IOW(type,nr,size)  	_IOC(_IOC_WRITE,(type),(nr),sizeof(size))
#define _IOWR(type,nr,size) 	_IOC(_IOC_READ | _IOC_WRITE,(type),(nr),sizeof(size))

设备类型			序列号				方向				数据尺寸
type				nr				  IO				size
8 bit				8 bit			  2 bit				8~14 bit
  • 函数功能
    • 驱动中和应用程序中关于ioctl的命令定义格式
  • 函数参数
    • type : 表示类型,是命令的类别(组),这个数的表示范围是0x0 ~ 0xff
    • nr :这一组命令的第几个,led控制的方式如ledon,ledoff
    • size : 读写的数据
    • IO:读设备还是写设备
      • _IO : 只控制设备,不读写设备
      • _IOR : 读设备,读取1个字节的数据
      • _lOW : 写设备,写1个字节的数据
      • _IOWR : 读写设备,读写1个字节的数据

例如,定义1个命令

#define LEDON _IO('A',0)
#define LEDOFF _IO('A',1)
#define LEDSET _IOW('A',2,1)

4.案例5——控制设备

/*main.c*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>  
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>	

#define LEDON 	 _IO('L',0)
#define LEDOFF	 _IO('L',1)
#define LEDSET	 _IO('L',2)
int main(int argc, char *argv[]){
    char buf[128] = {0};
    int ret = 0;
    int fd = open("/dev/hello", O_RDWR);
    if(fd == -1){
        perror("open");
        exit(-1);
    }
    printf("fd = %d\n",fd);

    ret = ioctl(fd, LEDON);
    if(ret < 0){
        perror("ioctl");
        exit(-1);
    }
    printf("ioctl LEDON\n");

    ret = ioctl(fd, LEDOFF);
    if(ret < 0){
        perror("ioctl");
        exit(-1);
    }
    printf("ioctl LEDOFF\n");

    ret = ioctl(fd, LEDSET);
    if(ret < 0){
        perror("ioctl");
        exit(-1);
    }
    printf("ioctl LEDSET\n");

    close(fd);

    return 0;
}
/*hello.c*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h> 
#include <linux/fs.h> 
#include <linux/cdev.h> 
#include <linux/uaccess.h>
#include <asm/ioctl.h>	

#define LEDON 	 _IO('L',0)
#define LEDOFF	 _IO('L',1)
#define LEDSET	 _IO('L',2)

int hello_major = 500;
int hello_minor = 0;
char dribuf[128] = {0};
static int hello_open(struct inode *inode, struct file *filp){
	printk(KERN_INFO "hello_driver打开成功!\n");
	return 0;
}
static int hello_release(struct inode *inode, struct file *filp){
	printk(KERN_INFO "hello_driver关闭成功!\n");
	return 0;
}
static ssize_t hello_read(struct file *filp, char __user *usrbuf, size_t count, loff_t *f_pos){
	ssize_t ret = 0;
	ret = copy_to_user(usrbuf, dribuf, count);
	if(ret<0){
		printk(KERN_WARNING "错误:应用程序读取驱动内的数据失败\n");
		return ret;
	}
	else{
		printk(KERN_INFO "应用程序读取驱动内的数据成功, 读取了%ld bytes\n",count);
	}
	return ret;
}
static ssize_t hello_write(struct file *filp, const char __user *usrbuf, size_t count, loff_t *f_pos){
	ssize_t ret = 0;
	ret = copy_from_user(dribuf, usrbuf, count);
	if(ret<0){
		printk(KERN_WARNING "错误:应用程序写给驱动数据失败\n");
		return ret;
	}
	else{
		printk(KERN_INFO "应用程序写给驱动数据成功, 用户把数据写进驱动程序了,写了%ld bytes\n",count);
	}
	return ret;
}

static long hello_unlocked_ioctl(struct file *filp, unsigned int cmd, unsigned long arg){
	long ret = 0;
	printk(KERN_INFO "cmd = %d\n",cmd);
	printk(KERN_INFO "LEDON =  %d\n",LEDON);
	printk(KERN_INFO "LEDOFF = %d\n",LEDOFF);
	printk(KERN_INFO "LEDSET = %d\n",LEDSET);
	printk(KERN_INFO "arg = %ld\n",arg);

	switch(cmd){
		case LEDON:
			printk(KERN_INFO "hello_driver ioctl LEDON\n");
			break;
		case LEDOFF:
			printk(KERN_INFO "hello_driver ioctl LEDOFF\n");
			break;
		case LEDSET:
			printk(KERN_INFO "hello_driver ioctl LEDSET\n");
			break;
		default:
			break;
	}
	return ret;
}

struct cdev cdev = {
	.owner = THIS_MODULE						//表示的是指向本模块的指针
};
struct file_operations hello_fops = {
	.owner = THIS_MODULE,
	.open = hello_open,
	.release = hello_release,
	.read = hello_read,
	.write = hello_write,
	.unlocked_ioctl = hello_unlocked_ioctl,
};
static int __init hello_int(void){
	int ret;
	dev_t devno = MKDEV(hello_major, hello_minor);
	ret = register_chrdev_region(devno, 1, "hello");
	if(ret < 0){
		printk(KERN_WARNING "错误:设备号申请失败: %d\n", hello_major);
		return ret;
	}
	
	memset(&cdev, 0, sizeof(struct cdev));		// 使用memset()函数初始化cdev结构体
	cdev_init(&cdev, &hello_fops);
	ret = cdev_add(&cdev, devno, 1);			/* 向内核链表添加一个cdev结构体 */
	if(ret < 0){
		printk(KERN_WARNING "错误:添加cdev失败\n");
		return ret;
	}

	printk(KERN_INFO "hello_driver installed sucessed!\n");
	return 0;
}
static void __exit hello_exit(void){
	dev_t devno = MKDEV(hello_major, hello_minor);
	cdev_del(&cdev);				/* 从内核链表中删除指定的cdev */
	unregister_chrdev_region(devno, 1);
	printk(KERN_INFO "hello_driver uninstall sucessed!\n");
}

module_init(hello_int);			//宏函数做关联
module_exit(hello_exit);

/* 
 * 设置模块的许可协议。
 * 设置模块的作者信息。
 * 设置模块的描述信息。
 */
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("GeYangwen");
MODULE_DESCRIPTION("A simple Hello world module");

makefile文件继续沿用上面的案列

以下是相关的命令调试

make
insmod hello.ko 
cat /proc/devices 
cat /proc/devices |grep hello
lsmod | grep hello
ll /dev/hello 
./main
dmesg 
rmmod hello
dmesg -c

2.5字符设备新方法

1.字符设备注册方法 register_chrdev

//函数所在文件路径/include/linux/fs.h
#include <linux/fs.h> 
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops){
	return __register_chrdev(major, 0, 256, name, fops);
}
  • 函数作用
    • 向内核注册某字符设备的 file_operations
    • register_chrdev = register_chrdev_region + cdev_init + cdev_add
  • 返回值
    • major如果设置为0,则返回自动分配的主设备号;
    • 如果设置为指定的主设备号,成功则返回值为0,失败返回负数。
  • 参数说明
    • major,表示当前设备的主设备号,范围是1~254,这个范围应该不准 也可设置500。可以自己指定,也可以设置为0让内核自动分配。犹如学号。
    • name,表示当前设备的驱动名称,犹如名字。
    • fops,是file_operations结构体指针。
    • inline修饰符说明

2.字符设备注销方法 unregister_chrdev

/*字符设备注销的函数原型*/
#include <linux/fs.h> 
static inline void unregister_chrdev(unsigned int major, const char *name){};

/*major:主设备号,Limnux下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分。
name:设备名字,指向一串字符串。
*/
  • 函数功能
    • 从系统中注销一个字符设备
    • unregister_chrdev =unregister_chrdev_region + cdev_del
  • 函数参数
    • major:主设备号
    • name:设备的名称
  • 函数返回值:
    • 成功返回: 0
    • 失败返回 :负的错误码

2.6 多文件与多模块编译

1.多文件编译

obj-m := hello.o
hello-objs := hello1.o hello2.o

2.实例06—— 多文件编译

/*hello1.c*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h> 
#include <linux/fs.h> 
#include <linux/cdev.h> 
#include <linux/uaccess.h>
#include <asm/ioctl.h>	

#define LEDON 	 _IO('L',0)
#define LEDOFF	 _IO('L',1)
#define LEDSET	 _IO('L',2)

int hello_major = 500;
// int hello_minor = 0;
char dribuf[128] = {0};

//引入外部函数
extern void display(void);
static int hello_open(struct inode *inode, struct file *filp){
	display();
	printk(KERN_INFO "hello_driver打开成功!\n");
	return 0;
}
static int hello_release(struct inode *inode, struct file *filp){
	printk(KERN_INFO "hello_driver关闭成功!\n");
	return 0;
}
static ssize_t hello_read(struct file *filp, char __user *usrbuf, size_t count, loff_t *f_pos){
	ssize_t ret = 0;
	ret = copy_to_user(usrbuf, dribuf, count);
	if(ret<0){
		printk(KERN_WARNING "错误:应用程序读取驱动内的数据失败\n");
		return ret;
	}
	else{
		printk(KERN_INFO "应用程序读取驱动内的数据成功, 读取了%ld bytes\n",count);
	}
	return ret;
}
static ssize_t hello_write(struct file *filp, const char __user *usrbuf, size_t count, loff_t *f_pos){
	ssize_t ret = 0;
	ret = copy_from_user(dribuf, usrbuf, count);
	if(ret<0){
		printk(KERN_WARNING "错误:应用程序写给驱动数据失败\n");
		return ret;
	}
	else{
		printk(KERN_INFO "应用程序写给驱动数据成功, 用户把数据写进驱动程序了,写了%ld bytes\n",count);
	}
	return ret;
}

static long hello_unlocked_ioctl(struct file *filp, unsigned int cmd, unsigned long arg){
	long ret = 0;
	printk(KERN_INFO "cmd = %d\n",cmd);
	printk(KERN_INFO "LEDON =  %d\n",LEDON);
	printk(KERN_INFO "LEDOFF = %d\n",LEDOFF);
	printk(KERN_INFO "LEDSET = %d\n",LEDSET);
	printk(KERN_INFO "arg = %ld\n",arg);

	switch(cmd){
		case LEDON:
			printk(KERN_INFO "hello_driver ioctl LEDON\n");
			break;
		case LEDOFF:
			printk(KERN_INFO "hello_driver ioctl LEDOFF\n");
			break;
		case LEDSET:
			printk(KERN_INFO "hello_driver ioctl LEDSET\n");
			break;
		default:
			break;
	}
	return ret;
}

struct cdev cdev = {
	.owner = THIS_MODULE						//表示的是指向本模块的指针
};
struct file_operations hello_fops = {
	.owner = THIS_MODULE,
	.open = hello_open,
	.release = hello_release,
	.read = hello_read,
	.write = hello_write,
	.unlocked_ioctl = hello_unlocked_ioctl,
};
static int __init hello_int(void){
	int ret;
#if 0
	dev_t devno = MKDEV(hello_major, hello_minor);
	ret = register_chrdev_region(devno, 1, "hello");
	if(ret < 0){
		printk(KERN_WARNING "错误:设备号申请失败: %d\n", hello_major);
		return ret;
	}
	
	memset(&cdev, 0, sizeof(struct cdev));		// 使用memset()函数初始化cdev结构体
	cdev_init(&cdev, &hello_fops);
	ret = cdev_add(&cdev, devno, 1);			/* 向内核链表添加一个cdev结构体 */
	if(ret < 0){
		printk(KERN_WARNING "错误:添加cdev失败\n");
		return ret;
	}
#endif
	ret = register_chrdev(hello_major,"hello",&hello_fops);
	if(ret < 0){
		printk(KERN_WARNING "错误:注册字符设备失败\n");
		return ret;
	}

	printk(KERN_INFO "hello_driver installed sucessed!\n");
	return 0;
}
static void __exit hello_exit(void){
#if 0
	dev_t devno = MKDEV(hello_major, hello_minor);
	cdev_del(&cdev);				/* 从内核链表中删除指定的cdev */
	unregister_chrdev_region(devno, 1);
#endif
	unregister_chrdev(hello_major,"hello");
	printk(KERN_INFO "hello_driver uninstall sucessed!\n");
}

module_init(hello_int);			//宏函数做关联
module_exit(hello_exit);

/* 
 * 设置模块的许可协议。
 * 设置模块的作者信息。
 * 设置模块的描述信息。
 */
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("GeYangwen");
MODULE_DESCRIPTION("A simple Hello world module");
/*hello2.c*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h> 
void display(void){
	printk(KERN_INFO "hello_driver:display\n");
}
# Makefile
$(warning KERNELRELEASE=$(KERNELRELEASE))		
ifeq ($(KERNELRELEASE),)	
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
	gcc main.c -o main
	rm -rfv *.o *~ core .depend .*.cmd *.mod.c *.mod .tmp_versions Module* modules*

modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

clean:
	rm -rfv *.o *~ core .depend .*.cmd *.ko *.mod.c *.mod .tmp_versions Module* modules*
	rm -rfv main

.PHONY: modules modules_install clean

else
	obj-m := hello.o
	hello-objs := hello1.o hello2.o
endif

3.实例07—— 多模块编译

多模块就是多驱动,可以编译出来好几个驱动,这个案例就是复制一下hello.c文件修改为char.c,然后再Makefile文件中修改下面的目标值,这样就可以生成两个驱动文件

obj-m += hello.o
obj-m += char.o

image-20240724165640294

2.7 cdev、misc及device的联系和区别

引用:leochen_career

2.7.1从/dev目录说起

从事Linux嵌入式驱动开发的人,都很熟悉下面的一些基础知识

  • 比如对于一个char类型的设备,我想对其进行read wirteioctl操作,那么我们通常会在内核驱动中实现一个file_operations结构体,然后分配主次设备号,调用cdev_add函数进行注册。从/proc/devices下面找到注册的设备的主次设备号,在用mknod /dev/char_dev c major minor 命令行创建设备节点。在用户空间open /dev/char_dev这个设备,然后进行各种操作。
  • OK,字符设备模型就这么简单,很多ABC教程都是一个类似的实现。
  • 然后我们去看内核代码时,突然一脸懵逼。。。怎么内核代码里很多常用的驱动的实现不是这个样子的?没看到有file_operations结构体,我怎么使用这些驱动?看到了/dev目录下有需要的char设备,可是怎么使用呢?我还是习惯用/dev的形式,那我该怎么办?

2.7.2 Linux驱动模型的一个重要分界线

linux2.6版本以前,普遍的用法就像我上面说的一样。但是linux2.6版本以后,引用了Linux设备驱动模型,开始使用了基于sysfs的文件系统,让我们不是太明白的那些Linux内核驱动了。 也就是说,我们熟悉的那套驱动模式是2.6版本以前的(当然这是基础,肯定要会的) 我们不熟悉的驱动模型是2.6版本以后的。

2.7.3 cdev、misc以及device

cdev和device的区别和联系

struct  cdev {
     struct  kobject kobj;
     struct  module *owner;
     const   struct  file_operations *ops;
     struct  list_head list;
     dev_t  dev;
     unsigned  int  count;
};
 
struct  device {
     struct  device      *parent;
     struct  device_private  *p;
     struct  kobject kobj;
     const   char      *init_name;  		/* initial name of the device */
     struct  device_driver *driver; 		/* which driver has allocated this device */
     void         * driver_data ;   		/* Driver data, set and get with dev_set/get_drvdata */
     dev_t            devt;   				/* dev_t, creates the sysfs "dev" */
     u32         id; 						/* device instance */
     void     (*release)( struct  device *dev);
     ......
};

通过看这两个结构体发现,其实cdevdevice之间没啥直接的联系,只有一个 dev_tkobject 是相同的。 dev_t 这个是联系两者的一个纽带了 。通常可以这么理解: cdev是传统驱动的设备核心数据结构,device是linux设备驱动模型中的核心数据结构。
要注册一个device设备,需要调用核心函数device_register()(或者说是device_add()函数) ; 要注册一个cdev设备,需要调用核心函数register_chrdev()(或者说是cdev_add()函数)

miscdevice和device的区别和联系

struct miscdevice  {
    int minor;                              // 次设备号
    const char *name;                       //名字 
    const struct file_operations *fops; 	// file_operations 结构体指针
    struct list_head list;                  // 作为一个链表节点挂接到misc设备维护的一个链表头上去misc_list
    struct device *parent;                  // 次设备的父设备
    struct device *this_device;            	// 本设备的device 结构体指针
    const struct attribute_group **groups;	//用于定义设备属性的指针数组。属性组定义了设备的属性,例如 sysfs 上的属性。
    const char *nodename;          //杂项设备节点的名称,通常与 name 字段相同。此字段用于在 /sys 文件系统中创建设备节点。
    mode_t mode;						// 设备节点的权限模式,指定了用户对设备节点的访问权限。
};

从定义可以看出,miscdevicedevice的子类,是从device派生出来的结构体,也是属于device范畴的,也就是该类设备会统一在/sys目录下进行管理了。

miscdevicecdev的区别和联系
通过上面的数据结构可以看到,两者都有一个file_operationsdev_tmisdevice由于主设备号固定,所以结构体里只有一个minor)。从数据结构上看,miscdevicedevicecdev的结合。

2.7.4 device_register()—理解上面三者关系的关键函数

device_register()—>device_add(),然后会调用两个关键函数
device_create_file() —-将相关信息添加到/sys文件系统中
devtmpfs_create_node() —-将相关信息添加到/devtmpfs文件系统中
第一个调用不做详细解析,因为devices本来就是/sys文件系统中的重要概念
关键是**devtmpfs_create_node()**
先了解下Devtmpfs 的概念

Devtmpfs lets the kernel create a tmpfs very early at kernel initialization ,  before any driver core device  is  registered .  Every device with a major / minor will have a device node created  in  this tmpfs instance .  After the rootfs  is  mounted by the kernel ,  the populated tmpfs  is  mounted at  / dev . 

devtmpfs_create_node()函数的核心是调用了内核的 vfs_mknod()函数,这样就在devtmpfs系统中创建了一个设备节点,当devtmpfs被内核mount/dev目录下时,该设备节点就存在于/dev目录下,比如/dev/char_dev之类的。
vfs_mknod()函数中会调用一个init_special_inode(),该函数实现如下:

void  init_special_inode( struct  inode *inode, umode_t mode, dev_t rdev){
     inode->i_mode = mode;
     if  (S_ISCHR(mode)) {
         inode->i_fop = &def_chr_fops;
         inode->i_rdev = rdev;
     }  else   if  (S_ISBLK(mode)) {
         inode->i_fop = &def_blk_fops;
         inode->i_rdev = rdev;
     }  else   if  (S_ISFIFO(mode))
         inode->i_fop = &pipefifo_fops;
     else   if  (S_ISSOCK(mode))
         ;   /* leave it no_open_fops */
     else
         printk(KERN_DEBUG  "init_special_inode: bogus i_mode (%o) forinode %s:%lu\n" , mode, inode->i_sb->s_id, inode->i_ino);
}
/*
  * Dummy default file-operations: the only thing this does
  * is contain the open that then fills in the correct operations
  * depending on the special file...
  */
const   struct   file_operations  def_chr_fops = {
     .open =  chrdev_open ,
     .llseek = noop_llseek,
}; 

如果node是一个char设备,会给i_fop 赋值一个默认的def_chr_fops,也就是说对该node节点,有一个默认的操作。在open一个字符设备文件时,最终总会调用chrdev_open,然后调用各个char设备自己的file_operations 定义的open函数。

static   int  chrdev_open( struct  inode *inode,  struct  file *filp){
  ret = -ENXIO;
     fops = fops_get(p->ops);
     if  (!fops)
         goto  out_cdev_put;
 
     replace_fops (filp, fops);
     if  (filp->f_op->open) {
         ret = filp->f_op-> open (inode, filp);
         if  (ret)
             goto  out_cdev_put;
     }
 
     return  0;
  out_cdev_put:
     cdev_put(p);
     return  ret;
}

上面分析的核心意思是:device_register()函数除了注册在/sys下面外,还做了一件重要的事情,通过 devtmpfs_create_node()/dev目录下创建了一个设备节点(inode),这个设备节点有一个默认的file_operations操作。

2.7.5 misc杂项设备简介

引用:iriczhao

Linux 内核中的杂项设备(Miscellaneous Devices)是一种通用的设备类型,用于表示那些不适合其他设备类型的设备。这些设备通常是不规则的,没有标准的通信协议或接口。杂项设备提供了一种灵活的机制,允许我们将不同类型的设备注册为杂项设备,并通过统一的接口在用户空间访问它们。

在Linux 中,把无法归类的五花八门的设备定义成杂项设备。相较于字符设备,杂项设备有以下两个优点:

  • (1)节省主设备号:杂项设备的主设备号固定为10,而字符设备不管是动态分配还是静态分配设备号,都会消耗一个主设备号,进而造成了主设备号浪费。当系统中注册了多个misc 设备驱动时,只需使用子设备号进行区分即可
  • (2)使用简单:当使用普通的字符设备驱动时,如果开发人员需要导出操作接口给用户空间,就需要注册对应的字符驱动,并创建字符设备class 从而自动在/dev 下生成设备节点,而misc驱动只需要将基本信息通过结构体传递给相应处理函数即可。

在驱动中使用miscdevice 结构体描述misc 设备, 该结构体定义在“ 内核源码/include/linux/miscdevice.h”文件中(在下面的实验代码中需要加入该头文件的引用),具体内容如下所示:

struct miscdevice  {
    int minor;                              // 次设备号
    const char *name;                       //名字 
    const struct file_operations *fops; 	// file_operations 结构体指针
    struct list_head list;                  // 作为一个链表节点挂接到misc设备维护的一个链表头上去misc_list
    struct device *parent;                  // 次设备的父设备
    struct device *this_device;            	// 本设备的device 结构体指针
    const struct attribute_group **groups;	//用于定义设备属性的指针数组。属性组定义了设备的属性,例如 sysfs 上的属性。
    const char *nodename;          //杂项设备节点的名称,通常与 name 字段相同。此字段用于在 /sys 文件系统中创建设备节点。
    mode_t mode;						// 设备节点的权限模式,指定了用户对设备节点的访问权限。
};

struct miscdevice 提供了一种将设备注册为杂项设备的机制,并指定了设备的名称、操作以及其他相关属性。杂项设备在 /dev 目录下创建,但是它们的名字与其驱动程序关联,而不是与设备类型直接关联。因此,同一类型的设备可能具有不同的名称,取决于它们所使用的驱动程序。

杂项设备API

在内核中,关于杂项设备提供的驱动API较少,仅包含两个API。

注册杂项设备

int misc_register(struct miscdevice *misc)			//struct miscdevice *misc:杂项设备结构

该函数用于向内核注册一个杂项设备。如果次设备号设置为 MISC_DYNAMIC_MINOR,则会分配一个次设备号,并将其放置在结构体的 minor 字段中。对于其他情况,使用请求的次设备号。

传递的结构体被链接到内核中,并且在注销之前可能不会被销毁。默认情况下,对设备的 open() 系统调用会将 file->private_data 设置为指向该结构体。驱动程序不需要在 fops 中包含 open 函数。

成功时返回零,失败时返回负的 errno 代码。

注销杂项设备

int misc_deregister(struct miscdevice *misc)

该函数用于注销之前使用 misc_register() 成功注册的杂项设备。

杂项设备初始化

在Linux内核中都会支持杂项设备,在内核启动过程中,会调用misc_init()完成杂项设备相关的初始化操作:

static int __init misc_init(void)
{
	int err;

#ifdef CONFIG_PROC_FS
	proc_create("misc", 0, NULL, &misc_proc_fops);
#endif
	misc_class = class_create(THIS_MODULE, "misc");
	err = PTR_ERR(misc_class);
	if (IS_ERR(misc_class))
		goto fail_remove;

	err = -EIO;
	if (register_chrdev(MISC_MAJOR,"misc",&misc_fops))
		goto fail_printk;
	misc_class->devnode = misc_devnode;
	return 0;

fail_printk:
	printk("unable to get major %d for misc devices\n", MISC_MAJOR);
	class_destroy(misc_class);
fail_remove:
	remove_proc_entry("misc", NULL);
	return err;
}
subsys_initcall(misc_init);

上述代码主要完成以下三个操作:

  • 使用proc_create()创建一个名为 “misc” 的 proc 文件,使用指定的文件操作 misc_proc_fops
  • 使用class_create()创建一个名为 “misc” 的设备类。
  • register_chrdev()注册字符设备

上述代码的目的是初始化杂项设备,包括创建 proc 文件、创建设备类、注册字符设备等。

杂项设备示例

我们通常通过以下步骤来创建和注册杂项设备:

  • 1、定义和初始化 miscdevice 结构。
  • 2、调用 misc_register() 函数注册杂项设备。
  • 3、在驱动模块的退出函数中调用 misc_deregister() 函数注销杂项设备。

例如下例代码:

#include <linux/init.h>		  //初始化头文件
#include <linux/module.h>	  //最基本的文件,支持动态添加和卸载模块。
#include <linux/miscdevice.h> //注册杂项设备头文件
#include <linux/fs.h>		  //注册设备节点的文件结构体

struct file_operations misc_fops = {
	// 文件操作集
	.owner = THIS_MODULE ////将owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模
};
struct miscdevice misc_dev = {
	// 杂项设备结构体
	.minor = MISC_DYNAMIC_MINOR, 		// 动态申请的次设备号
	.name = "hello",				 	// 杂项设备名字是hello
	.fops = &misc_fops,			 		// 文件操作集
	.nodename = "hello_dev",			// 设备节点名字
	.mode = 0666						// 设备节点权限
};

static int __init misc_init(void){
	int ret;
	ret = misc_register(&misc_dev); 			// 在初始化函数中注册杂项设备
	if (ret < 0)
	{
		printk("misc registe is error \n"); 	// 打印注册杂项设备失败
	}
	printk("misc registe is succeed \n"); 		// 打印注册杂项设备成功
	return 0;
}

static void __exit misc_exit(void){
	misc_deregister(&misc_dev); 				// 在卸载函数中注销杂项设备
	printk(" misc goodbye! \n");
}
module_init(misc_init);
module_exit(misc_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

运行测试

将编译生成的驱动模块hello.ko 拷贝到开发板上,输入以下命令加载驱动模块。

insmod hello.ko 

可以看到驱动加载之后,打印“misc registe is succeed”,说明misc 驱动注册成功。输入以下命令查看加载的驱动模块,驱动加载成功如下(图13-5)所示:

lsmod | grep hello
hello                  16384  0

然后来到/sys/class/misc 目录下,可以看到名为“test”的文件夹已经被创建了,在/sys/class/misc 目录下有misc 类的所有设备,每个注册的杂项设备对应一个文件夹目录,如下所示:

ls /sys/class/misc/
agpgart  cpu_dma_latency  ecryptfs  hello  hw_random     mcelog  rfkill    tun      uinput  vga_arbiter  vsock
autofs   device-mapper    fuse      hpet   loop-control  psaux   snapshot  udmabuf  vfio    vmci

cd /sys/class/misc/hello
ls
dev  power  subsystem  uevent

驱动加载成功之后会生成/dev/hello_dev 设备驱动文件,这个是通过.nodename = "hello_dev",设置的,输入以下命令查看杂项设备的主次设备号。

ll /dev/hello_dev
crw-rw-rw- 1 root root 10, 57 82 09:35 /dev/hello_devl

从上图可以看出,/dev/hello_dev这个杂项设备的主设备号为10,次设备号为57,最后可以使用以下命令对驱动进行卸载,卸载完成如下所示:

rmmod hello
lsmod | grep hello
dmesg 
[ 1489.104972] misc registe is succeed 
[ 2114.187939]  misc goodbye! 

杂项设备和字符设备

在 Linux 内核中,杂项设备(misc device)和字符设备(character device)是两种不同类型的设备。在内核中的实现和使用方式略有不同。

  • 杂项设备(Miscellaneous Devices):

    • 杂项设备是一种比较通用的设备类型,用于表示不适合其他设备类型的设备。
    • 这些设备通常是不规则的,没有标准的通信协议或接口。
    • 杂项设备在 /dev 目录下创建,但是它们的名字与其驱动程序关联,而不是与设备类型直接关联。
    • 在内核中,杂项设备通过 miscdevice 结构来表示,该结构包含了设备的名称、设备号、文件操作等信息。
    • 创建和注册杂项设备通常涉及使用 misc_register()misc_deregister() 等函数。
  • 字符设备(Character Devices):

    • 字符设备是一种基本的设备类型,用于处理以字符为单位的数据流。
    • 这些设备通常是顺序访问的,没有固定的大小或边界。
    • 字符设备在 /dev 目录下创建,并且其名称直接与设备类型相关联。
    • 在内核中,字符设备通过 cdev 结构来表示,该结构包含了设备的文件操作等信息。
    • 创建和注册字符设备通常涉及使用 cdev_add()cdev_del() 等函数。

总的来说,杂项设备适用于那些不符合标准设备模型的设备,而字符设备则适用于以字符流形式进行通信的设备。在编写内核驱动程序时,我们可以根据设备的特性选择适当的设备类型。

第03章 Linux内核的并发机制

3.1 并发控制的概念

1.并发控制的概念

并发(concurrency)指的是多个执行单元同时、并行被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态 (race conditions)

2.竞态发生的情况

对称多处理器 (SMP) 的多个CPU

image-20240724170746468
  • 单CPU内进程与抢占它的进程
    • Linux 2.6内核支持抢占调度,一个进程在内核执行的时候可能被另一高优先级进程打新
  • 中断与进程之间
    • 中断可以打断正在执行的进程,如果中断处理程序访问进程正在访问的资源,则竞态也会发生

3.解决竞态问题的方法

保证对共享资源的互斥访问,所谓互斥访问是指一个执行单元在访问共享资源的时候,其他的执行单元被禁止访问
访问共享资源的代码区域称为临界区 (critical sections) ,临界区需要被以某种互斥机制加以保护

4.互斥机制

  • 中断屏蔽
  • 原子量,不能被拆分的操作,比如运算过程不能被拆分
  • 自旋锁
  • 信号量 多值信号量
  • 互斥锁(互斥体,也称二值信号量)

3.2 中断屏蔽

在Linux 中,屏蔽中断是指禁止处理器响应特定类型的硬件中断信号。屏蔽中断可以用于控制处理器对来自硬件设备的中断请求的响应

在 Linux 中,你可以使用以下方法来屏蔽中断:

  • 1.屏蔽特定中断线: Linux 内核提供了一组函数来操作 CPU 的中断控制器,例如disable_irg()enable_irq()。你可以使用这些函数来屏蔽或恢复特定的硬件中断线。
  • 2.屏蔽所有中断: Linux 内核还提供了 local_irg_disable()local_irq_enable() 函数,用于完全屏蔽所有中断信号。当你调用 local_irg_disable() 函数时,处理器将不再响应任何中断请求,而是将所有中断信号置于屏蔽状态。相应地,当你调用 local_irq_enable() 函数时,处理器将恢复对中断请求的响应.

屏蔽中断可能会导致系统出现一些问题,例如延迟或不可预测的行为。在屏蔽中断之前,你应该仔细考虑并确保这是你需要的操作

屏蔽中断是一个高级操作,通常在编写驱动程序中使用。对于一般用户和应用程序开发者来说,直接屏蔽中断通常不是必需的。

例如:

1ocal_irq_disable()				/*屏蔽中断*/
critical section				/*临界区*/
local_irq_enable()				/*开中断*/

3.3 原子量

1.原子量概念

  • 在 Linux 内核中,原子操作被广泛应用于多线程或多进程环境下对共享资源的访问和修改;
  • Linux 内核提供了一些原子操作函数,这些函数能够保证在执行期间不会被中断或并发操作所打断.;
  • 原子量可以分为整型原子量和位原子量;

2.整型原子量

包含头文件

#include <asm/atomic.h>

定义原子量
Linux 内核中的原子变量使用 atomic_t 类型来表示

atomic_t v;

设置原子变量的值

void atomilc_set(atomic_t *v,int i); // 设置原子变量的值为i

例如:

atomic_t v = ATOMIC_INIT(1); // 定义原子变量v并初始化为0
atomic_set(&v,1) ;// 设置原子变量的值为1

获取原子变量的值

atomic_read(atomic_t *v);// 返回原子变量的值

原子变量加/减

void atomic_add(int i, atomic_t *v);// 原子变量增加i
void atomic_sub(int i, atomic_t *v);// 原子变量减少i

原子变量自增/自减

void atomic_inc(atomic_t *v);// 原子变量增加1
void atomic_dec(atomic t *v);// 原子变量减少1

操作并测试

int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(int i, atomic_t *v);	

上述操作对原子变量执行自增、自减和减操作后 (注意没有加) 测试其是否为0,为0返回true,否则返回fals

操作并返回

int atomic_add_return(int i, atomic_t *v);
int atomic_sub_return(int i, atomic_t *v);
int atomic_inc_return(atomic_t *v);
int atomic_dec_return(atomic_t *v);

上述操作对原子变量进行加/减和自增/自减操作,并返回新的值

3.实例09 整型原子量

使用原子量实现设备只能被一个进程打开
源文件
源代码
运行结果

3.3 自旋锁

1.自旋锁概念

Linux 中的自旋锁是一种保护共享资源的同步机制。自旋锁的目的是防止多个线程同时访问共享资源,从而导致数据不一致或其他问题.

自旋锁的特点是在获取锁的时候,如果锁已经被其他线程持有,则当前线程会一直”自旋”,也就是循环等待,直到锁被释放为止。这种方
式比较适用于短时间内多个线程竞争同一个锁的情况,因为自旋等待的时间比较短,不会造成线程的长时间阻塞。

自旋锁不会睡眠,信号量会睡眠

  • 包含头文件
#include <linux/spinlock.h>
  • 定义自旋锁
spinlock_t lock;
  • 初始化自旋锁
spin_lock_init(&lock)
  • 获得自旋锁
spin_lock(&lock)

如果能够立即获得锁,它就马上返回,否则,它将自旋在那里

  • 尝试获得自旋锁
spin_trylock(&lock)

该宏尝试获得自旋锁lock,如果能立即获得锁,它获得锁并返回真,否则立即返回假

。释放自旋锁

spin_unlock(&lock)
  • 自旋锁注意事项
    • 自旋锁实际上是忙等锁
    • 自旋锁可能导致系统死锁,一段代码在自己获得了锁之后,自己又想第二次获得这个锁时(这个锁没有被释放时)
    • 自旋锁锁定期间不能调用可能引起进程调度的函数,如果系统要多个进程要获得自旋锁,如果在自旋锁中睡眠,会照成其它讲程会一直的忙等待,cpu的使用率会下降
  • 什么情况会引发系统调度
    • 每隔一个固定的时间间隔,任务状态会刷新一次,linux默认定义这个频率200HZ,也就是5ms刷新一次。
    • 在执行到睡眠函数时,会引发系统的一次调度

2.实例10——自旋锁

3.读写自旋锁

定义和初始化读写自旋锁

rwlock_t my_rwlock;
rwlock_t my_rwlock = RW_LOCK_UNLOCKED;		// 静态初始化
rwlock_init(&my_rwlock);				   // 动态初始化

读锁定

void read_Tock(rwlock_t *Tock);
void read_lock_irqsave(rwlock_t *lock, unsigned long flags);
void read_1ock_irq(rwlock_t *lock);
void read_Tock_bh(rwlock_t *lock);

读解锁

void read_unlock(rwlock t *1ock);
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void read_unlock_irq(rwlock_t *lock);
void read_unlock_bh(rwlock_t *lock);

写锁定

void write_lock(rwlock_t *lock);
void write_lock_irqsave(rwlock_t *lock, unsigned long flags);
void write_lock_irq(rwlock_t *lock);
void write_lock_bh(rwlock_t *lock);
int write_trylock(rwlock_t *lock):

写解锁

void write_un1ock(rwlock_t *lock);
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void write_unlock_irq(rwlock_t *lock);
void write_unlock_bh(rwlock_t *lock);

读写自旋锁使用

rwlock_t lock;				/* 定义rwlock */
rwlock_init(&lock);			/* 初始化rwlock*/

/* 读时获取锁*/
read_lock(&lock);
/* 临界资源*/
read_unlock(&lock);

/*写时获取锁*/
write_lock_irqsave(&lock, flags);
/*临界资源*/
write_unlock_irqrestore(&lock, fags);

4.顺序锁

写顺序锁操作

  • 获得顺序锁
void write_seqlock(seqlock_t *s1);
int write_tryseqlock(seqlock_t *s1);
write_seglock_irqsave(lock,flags);
write_seqlock_irq(lock);
write_seqlock_bh(lock);
  • 释放顺序锁
void write_sequnlock(seqlock_t *s1);
write_sequnlock_irqrestore(lock, flags)
write_sequnlock_irq(lock) ;
write_sequnlock_bh(lock) ;

读顺序锁操作

  • 读开始
unsigned read_seqbegin(const seqlock_t *s1);
read_seqbegin_irqsave(lock,flags)
  • 判断是否需要重读
int read_seqretry(const seqlock_t *s1, unsigned iv);
read_seqretry_irqrestore(lock,iv,flags)

3.4 信号量

1.信号量概念

内核区动中使用互斥体的典型场景是对设备进行独占性访问,避免多个线程同时访问设备导致竞争和冲突。通过在关键代码段前后加锁和
解锁操作,可以确保只有一个线程可以执行关键代码段

信号量分为多值信号量和二值信号量,二值信号量就是互斥体 (互斥锁)

  • 包含头文件

    #include <linux/semaphore.h>
  • 定义信号量

    struct semaphore sem:
  • 初始化信号量

    void sema_init(struct semaphore *sem, int va1) ;
  • 信号量的获取

    // 获取信号量,如果可以获取,函数直接返回
    // 如果不能获取则进程进入睡眠,进入阻塞,等待被唤醒后,继续执行
    // 睡眠与唤醒的机制
    void down(struct semaphore * sem);
    int down_interruptible(struct semaphore * sem);			// 睡眠时可以中断
    int down_trylock(struct semaphore * sem);				// 尝试获得信号量
  • 信号量的释放

    void up(struct semaphore * sem);

2.实例11——信号量

使用信号量保存临界资源
源文件

struct semaphore sem;

sema_init(&sem, 1) ;

down(&sem);				// 用这个也可以 down_interruptible(&sem);   获得信号量,信号量的值减1 ,P操作
/*邻接资源*/
up(&sem);				// 释放信号量 V操作,信号量的值加1

3.5 互斥体

1.互斥体概念

互斥体是一种二进制信号量,用于保护共享资源,使得只有一个线程可以访问临界区。当一个线程获取了与斥体后,其他线程无法获取该
互斥体,只能等待互斥体被释放。

驱动中使用互斥体的典型场景是对设备进行独占性访问,避免多个线程同时访问设备导致竞争和冲突。通过在关键代码段前后加锁和解锁
操作,可以确保只有一个线程可以执行关键代码段。

互斥锁是睡眠与唤醒的机制,得不到睡眠,被唤醒后继续执行获得锁

  • 包含头文件

    #include <linux/mutex.h>
  • 初始化

    struct mutex my_mutex;
    mutex_init(&my_mutex);
  • 获取互斥体

    void mutex_lock(struct mutex *lock);
    int mutex_trylock(struct mutex *lock);		//有返回值  得到锁返回真1 反之返回0
  • 释放互斥体

    void __sched mutex_unlock(struct mutex *lock);

    例如,mutex的使用方法

    struct mutex mutex		/* 定义mutex */
    mutex_init(&mutex);		/* 初始化mutex */
    mutex_lock(&mutex);		/* 获取mutex 		只能被一个进程获取 ,另外一个进程需要等待	*/
    /* 临界资源 */
    mutex_un1ock(&mutex);	/* 释放mutex */

2.实例12——互斥体

使用互斥体实现设备只能被一个进程打开
源文件

源代码

运行结果

第04章 Linux内核的IO模型

4.1 IO模型介绍

在Linux 中,IO 模型用于描述应用程序与输入输出设备之间的数据传输方式和机制。Linux 提供了多种IO 模型,常见的包括阻塞式IO、
非阻塞式IO 、IO 复用 (select/poll/epoll) 、信号驱动IO 和异步IO 。

  1. 阻塞式IO (Blocking IO ):在阻塞式IO 中,当应用程序发起一个IO 操作时,它会一直等待直到操完成。在这个过程中,应用程
    序的执行将被阻塞。只有当数据准备好并成功传输后,IO 操作才会返回。
  2. 非阻塞式IO (Non-blocking IO : 在非阻塞式IO 中,应用程序在发起一个IO操作后,立即返回并继续执行其他任务,而不必等待
    操作完成。如果数据没有准备好或无法立即传输,IO操作将返回一个错误码,应用程序可以通过轮询来检查数据是否准备好。
  3. IO复用 (IO Multiplxing) :IO 复用使用 selet,poll或 epoll等机制来同时监听多人IO事件应用程可以将多个文件描待符注册到一个IO复用对象中,并通过阻塞等待该对象上的任何事件发生。一旦有事件发生,应用程序就可以处理该事件,而不必阻塞等待。
  4. 信号驱动IO(Signal-driven IO) : 信号动IO 使用信号机制来通知应用程序IO事件的发生。应用程序通过调用 sigaction 函数来注册一个信号处理函数,并在接收到指定信号后进行相应的处理操作。
  5. 异步IO(Asynchronous IO): 异步IO 中,应用程序发起一个IO操作后,可以立即返回并继续执行其他任务。当数据准备好并传输完成时,操作系统会通知应用程序,应用程序可以通过回调函数来处理完成的IO事件。

每种IO 模型都有其适用的场景和特点。在选择IO模型时,需要考虑应用程序的需求、性能要求以及平台支持等因素

4.2 等待队列 waitqueue

1.等待队列概念

等待队列 以队列为基础数据结构,与进程调度机制紧密结合,能够用于实现内核中的异步事件通知机制,也可以用来同步对系统资源的访问 (如信号量)

包含头文件

#include <linux/wait.h>
#include <linux/sched.h>

等待队列的定义

struct __wait_queue_head{
    spinlock_t lock;
    struct list_head task_1ist;
};
typedef struct __wait_queue_head wait_queue_head_t;

定义等待队列头

wait_queue_head_t wq;

初始化等待队列头

init_waitqueue_head(&wq);

等待事件发生(睡眠)

/*
wait_event - sleep until a condition gets true
@wq: the waitqueue to wait on
@condition: a C expression for the event to wait for int flag = 0 ;
*/

// 传递等待队列的结构体,而不传递结构体地址
#define wait_event(queue,condition)

wait_event_interruptible(queue, condition)
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)

例如:

wait_event(wq,(flag !=0))
// 返回:如果condition表达为真,这个函数成功返回
// 如果condition表达为假,这个函数在此睡眠     过一段时间后,使用wake_up唤醒了等待队列,再次condition的结果

。唤醒等待队列

void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t queue);

。直接睡眠(不判断条件的睡眠)

sleep_on(wait_queue_head_t *q );
interruptible_sleep_on(wait_queue_head_t *q );

4.3 Linux内核阻塞IO

1阻塞IO概念

阻塞操作指在执行设备操作时,若不能获得资源则进程睡眠。当满足可操作的条件后,内核唤醒进程继续执行。

2.实例13 阻塞IO

使用等待队列实现设备读阻塞

源文件和源代码

/*hello.c*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <asm/ioctl.h>
#include <linux/spinlock.h>
#include <linux/semaphore.h>
#include <linux/atomic.h>
#include <linux/mutex.h>
#include <linux/wait.h>
#include <linux/sched.h>

int hello_major = 500;
#define MAX_BUF_COUNT 128
char dribuf[MAX_BUF_COUNT] = {0};
atomic_t v;					  // 原子变量
spinlock_t lock;			  // 自旋锁
struct semaphore sem;		  // 信号量
struct mutex mutex;			  // 互斥锁
wait_queue_head_t wait_queue; // 等待队列的头

// 设置一个标志位,表示设备的使用情况
// busy_flag == 0:not busy
// busy_flag > 0:busy
int busy_flag = 0;
int dribuf_len = 0;		//表示dribuf中的字节数,dribuf_len==0 ,表示没数据
static int hello_open(struct inode *inode, struct file *filp)
{
	printk(KERN_INFO "hello_driver打开成功!\n");
	return 0;
}
static int hello_release(struct inode *inode, struct file *filp)
{
	printk(KERN_INFO "hello_driver关闭成功!\n");
	return 0;
}
static ssize_t hello_read(struct file *filp, char __user *usrbuf, size_t count, loff_t *f_pos)
{
	ssize_t ret = 0;
	if(dribuf_len == 0){
		// dribuf_len == 0 表示驱动中没有数据
		// dribuf_len != 0 表示驱动中有数据
		// 此时 (dribuf_len != 0) 条件为假,wait_event interruptible 进入睡眠状态,造成进程读阻塞
		// 有讲程向驱动中写入了数据,此时dribuf_len != 0  (dribuf len != 0)条件为真,函数返回
		wait_event_interruptible(wait_queue, (dribuf_len!=0));
	}
	down_interruptible(&sem);			// 睡眠时可以中断  P操作,信号量的值减1
	ret = copy_to_user(usrbuf, dribuf, count);
	if (ret < 0)
	{
		printk(KERN_WARNING "错误:应用程序读取驱动内的数据失败\n");
		return ret;
	}
	else
	{
		printk(KERN_INFO "应用程序读取驱动内的数据成功, 读取了%ld bytes\n", count);
	}
	up(&sem);						// 释放信号量 V操作,信号量的值加1
	dribuf_len = 0;
	ret = count;
	return ret;
}
static ssize_t hello_write(struct file *filp, const char __user *usrbuf, size_t count, loff_t *f_pos)
{
	ssize_t ret = 0;
	if(count > MAX_BUF_COUNT){
		count = MAX_BUF_COUNT;
	}
	down_interruptible(&sem);			// 睡眠时可以中断  P操作,信号量的值减1
	ret = copy_from_user(dribuf, usrbuf, count);
	if (ret < 0)
	{
		printk(KERN_WARNING "错误:应用程序写给驱动数据失败\n");
		return ret;
	}
	else
	{
		printk(KERN_INFO "应用程序写给驱动数据成功, 用户把数据写进驱动程序了,写了%ld bytes\n", count);
	}
	up(&sem);						// 释放信号量 V操作,信号量的值加1
	dribuf_len = count;				// 应用数据写给驱动的数据量
	wake_up(&wait_queue);	
	ret = count;

	return ret;
}
static long hello_unlocked_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	long ret = 0;
	return ret;
}

struct cdev cdev = {
	.owner = THIS_MODULE // 表示的是指向本模块的指针
};
struct file_operations hello_fops = {
	.owner = THIS_MODULE,
	.open = hello_open,
	.release = hello_release,
	.read = hello_read,
	.write = hello_write,
	.unlocked_ioctl = hello_unlocked_ioctl,
};
static int __init hello_int(void)
{
	int ret;
	ret = register_chrdev(hello_major, "hello", &hello_fops);
	if (ret < 0)
	{
		printk(KERN_WARNING "错误:注册字符设备失败\n");
		return ret;
	}
	atomic_set(&v, 0);	   // 初始化原子变量为0,表示没有资源有效
	spin_lock_init(&lock); // 初始化自旋锁,初始化后锁的状态是开的
	sema_init(&sem, 1) ;	// 初始化信号量,初始化后信号量的状态是1
	init_waitqueue_head(&wait_queue);

	printk(KERN_INFO "hello_driver installed sucessed!\n");
	return 0;
}
static void __exit hello_exit(void)
{
	unregister_chrdev(hello_major, "hello");
	printk(KERN_INFO "hello_driver uninstall sucessed!\n");
}

module_init(hello_int); // 宏函数做关联
module_exit(hello_exit);

/*
 * 设置模块的许可协议。
 * 设置模块的作者信息。
 * 设置模块的描述信息。
 */
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("GeYangwen");
MODULE_DESCRIPTION("A simple Hello world module");
/*test_read.c*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>  
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>	

#define MAX_BUF_COUNT 128
char buf[MAX_BUF_COUNT]={0};
int main(int argc, char *argv[]){
    int ret = 0;
    int fd = open("/dev/hello", O_RDWR);
    if(fd == -1){
        perror("open");
        exit(-1);
    }
    printf("fd = %d\n",fd);

    ret = read(fd, buf, MAX_BUF_COUNT);
    if(ret < 0){
        perror("read");
        exit(-1);
    }
    printf("read %d bytes, data = %s\n", ret, buf);
    // getchar();

    close(fd);

    return 0;
}
/*test_write.c*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>  
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>	

#define MAX_BUF_COUNT 128
char buf[MAX_BUF_COUNT]={0};
int main(int argc, char *argv[]){
    int ret = 0;
    int fd = open("/dev/hello", O_RDWR);
    if(fd == -1){
        perror("open");
        exit(-1);
    }
    printf("fd = %d\n",fd);

    // memset(buf, 0, MAX_BUF_COUNT);                           //清空buf内的东西
    strcpy(buf, "hello world\0");
    ret = write(fd, buf, strlen(buf));
    if(ret == -1){
        perror("write");
        exit(-1);
    }
    memset(buf, 0, MAX_BUF_COUNT);                          //清空buf内的东西

    close(fd);

    return 0;
}
# Makefile
$(warning KERNELRELEASE=$(KERNELRELEASE))		

ifeq ($(KERNELRELEASE),)

KERNELDIR ?= /lib/modules/$(shell uname -r)/build

PWD := $(shell pwd)

modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

	gcc test_write.c -o test_write
	gcc test_read.c -o test_read
	rm -rfv *.o *~ core .depend .*.cmd *.mod.c *.mod .tmp_versions Module* modules*

modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

clean:
	rm -rfv *.o *~ core .depend .*.cmd *.ko *.mod.c *.mod .tmp_versions Module* modules*
	rm -rfv test_write test_read

.PHONY: modules modules_install clean

else
	obj-m += hello.o

endif

运行结果

image-20240726112136526

4.4 Linux内核非阻塞IO

1.非阻塞IO概念

非阻塞操作 指进程在不能进行设备操作时并不睡眠而是立刻返回结果

设置非阻塞属性 O_NONBLOCK
应用层打开文件时,要设置一个属性

fd = open("/dev/he11o",O_RDWR | O_NONBLOCK )

应用层会把宏的值赋值给file结构体的成员f_flags
要在驱动中,判断这个标志位

if(filep ->f_flags & O_NONBLOCK)	return -EAGAIN;

2.实例14 非阻塞I0

使用O_NONBLOCK实现设备读非阻塞
源文件

/*hello.c*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <asm/ioctl.h>
#include <linux/spinlock.h>
#include <linux/semaphore.h>
#include <linux/atomic.h>
#include <linux/mutex.h>
#include <linux/wait.h>
#include <linux/sched.h>

int hello_major = 500;
#define MAX_BUF_COUNT 128
char dribuf[MAX_BUF_COUNT] = {0};
atomic_t v;					  // 原子变量
spinlock_t lock;			  // 自旋锁
struct semaphore sem;		  // 信号量
struct mutex mutex;			  // 互斥锁
wait_queue_head_t wait_queue; // 等待队列的头

// 设置一个标志位,表示设备的使用情况
// busy_flag == 0:not busy
// busy_flag > 0:busy
int busy_flag = 0;
int dribuf_len = 0;		//表示dribuf中的字节数,dribuf_len==0 ,表示没数据
static int hello_open(struct inode *inode, struct file *filp)
{
	printk(KERN_INFO "hello_driver打开成功!\n");
	return 0;
}
static int hello_release(struct inode *inode, struct file *filp)
{
	printk(KERN_INFO "hello_driver关闭成功!\n");
	return 0;
}
static ssize_t hello_read(struct file *filp, char __user *usrbuf, size_t count, loff_t *f_pos)
{
	ssize_t ret = 0;
	if(dribuf_len == 0){

		if(filp->f_flags & O_NONBLOCK){	// 判断应用程序是否使用了O_NONBLOCK 标志
			return -EAGAIN; // 非阻塞模式下,如果驱动中没有数据,则返回-EAGAIN
		}

		// dribuf_len == 0 表示驱动中没有数据
		// dribuf_len != 0 表示驱动中有数据
		// 此时 (dribuf_len != 0) 条件为假,wait_event interruptible 进入睡眠状态,造成进程读阻塞
		// 有讲程向驱动中写入了数据,此时dribuf_len != 0  (dribuf len != 0)条件为真,函数返回
		wait_event_interruptible(wait_queue, (dribuf_len!=0));
	}

	if(count > dribuf_len){
		count = dribuf_len;
		// count =  strlen(dribuf);			//有错
	}
	down_interruptible(&sem);			// 睡眠时可以中断  P操作,信号量的值减1
	ret = copy_to_user(usrbuf, dribuf, count);
	if (ret < 0)
	{
		printk(KERN_WARNING "错误:应用程序读取驱动内的数据失败\n");
		return ret;
	}
	else
	{
		printk(KERN_INFO "应用程序读取驱动内的数据成功, 读取了%ld bytes\n", count);
	}
	up(&sem);						// 释放信号量 V操作,信号量的值加1
	dribuf_len -= count;			// 驱动减少的字节数
	ret = count;
	return ret;
}
static ssize_t hello_write(struct file *filp, const char __user *usrbuf, size_t count, loff_t *f_pos)
{
	ssize_t ret = 0;
	if(count > MAX_BUF_COUNT){
		count = MAX_BUF_COUNT;
	}
	down_interruptible(&sem);			// 睡眠时可以中断  P操作,信号量的值减1
	ret = copy_from_user(dribuf, usrbuf, count);
	if (ret < 0)
	{
		printk(KERN_WARNING "错误:应用程序写给驱动数据失败\n");
		return ret;
	}
	else
	{
		printk(KERN_INFO "应用程序写给驱动数据成功, 用户把数据写进驱动程序了,写了%ld bytes\n", count);
	}
	up(&sem);						// 释放信号量 V操作,信号量的值加1
	dribuf_len = count;				// 应用数据写给驱动的数据量
	wake_up(&wait_queue);	
	ret = count;

	return ret;
}
static long hello_unlocked_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	long ret = 0;
	return ret;
}

struct cdev cdev = {
	.owner = THIS_MODULE // 表示的是指向本模块的指针
};
struct file_operations hello_fops = {
	.owner = THIS_MODULE,
	.open = hello_open,
	.release = hello_release,
	.read = hello_read,
	.write = hello_write,
	.unlocked_ioctl = hello_unlocked_ioctl,
};
static int __init hello_int(void)
{
	int ret;
	ret = register_chrdev(hello_major, "hello", &hello_fops);
	if (ret < 0)
	{
		printk(KERN_WARNING "错误:注册字符设备失败\n");
		return ret;
	}
	atomic_set(&v, 0);	   // 初始化原子变量为0,表示没有资源有效
	spin_lock_init(&lock); // 初始化自旋锁,初始化后锁的状态是开的
	sema_init(&sem, 1) ;	// 初始化信号量,初始化后信号量的状态是1
	init_waitqueue_head(&wait_queue);

	printk(KERN_INFO "hello_driver installed sucessed!\n");
	return 0;
}
static void __exit hello_exit(void)
{
	unregister_chrdev(hello_major, "hello");
	printk(KERN_INFO "hello_driver uninstall sucessed!\n");
}

module_init(hello_int); // 宏函数做关联
module_exit(hello_exit);

/*
 * 设置模块的许可协议。
 * 设置模块的作者信息。
 * 设置模块的描述信息。
 */
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("GeYangwen");
MODULE_DESCRIPTION("A simple Hello world module");
/*test_read.c*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>  
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>	

#define MAX_BUF_COUNT 128
char buf[MAX_BUF_COUNT]={0};
int main(int argc, char *argv[]){
    int ret = 0;
    int fd = open("/dev/hello", O_RDWR | O_NONBLOCK);
    if(fd == -1){
        perror("open");
        exit(-1);
    }
    printf("fd = %d\n",fd);

    ret = read(fd, buf, MAX_BUF_COUNT);
    if(ret < 0){
        perror("read");
        exit(-1);
    }
    printf("read %d bytes, data = %s\n", ret, buf);
    // getchar();

    close(fd);

    return 0;
}
/*test_write.c*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>  
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>	

#define MAX_BUF_COUNT 128
char buf[MAX_BUF_COUNT]={0};
int main(int argc, char *argv[]){
    int ret = 0;
    int fd = open("/dev/hello", O_RDWR);
    if(fd == -1){
        perror("open");
        exit(-1);
    }
    printf("fd = %d\n",fd);

    // memset(buf, 0, MAX_BUF_COUNT);                           //清空buf内的东西
    strcpy(buf, "hello world\0");
    ret = write(fd, buf, strlen(buf));
    if(ret == -1){
        perror("write");
        exit(-1);
    }
    memset(buf, 0, MAX_BUF_COUNT);                          //清空buf内的东西

    close(fd);

    return 0;
}

运行结果

root@ubuntu:/home/linux/work/14-nonblocking# ./test_read 
fd = 3
read: Resource temporarily unavailable
root@ubuntu:/home/linux/work/14-nonblocking# ./test_write 
fd = 3
root@ubuntu:/home/linux/work/14-nonblocking# ./test_read 
fd = 3
read 11 bytes, data = hello world
root@ubuntu:/home/linux/work/14-nonblocking# rmmod hello
root@ubuntu:/home/linux/work/14-nonblocking# dmesg 
[ 9516.139380] hello_driver installed sucessed!
[ 9523.698601] hello_driver打开成功!
[ 9523.698742] hello_driver关闭成功!
[ 9529.340597] hello_driver打开成功!
[ 9529.340679] 应用程序写给驱动数据成功, 用户把数据写进驱动程序了,写了11 bytes
[ 9529.340681] hello_driver关闭成功!
[ 9532.396614] hello_driver打开成功!
[ 9532.396695] 应用程序读取驱动内的数据成功, 读取了11 bytes
[ 9532.396699] hello_driver关闭成功!
[ 9539.604237] hello_driver uninstall sucessed!

4.5 Linux内核的多路复用IO

1多路复用IO概念

在 Linux 内核中,多路复用(Multiplexing)是一种机制,允许进程同时监视和等待多个文件描述符的事件。它是实现高效IO的关键技
术之一,常见的多路复用机制有select、poll、epoll。

  1. select :
    seect 是最古老的多路复用机制之一,在早期UNIX系统上广泛使用。它使用三个位图参数来表示要监视的文件描述符集合,并等
    待其中任意一个文件描述符就绪。但select存在一些性能问题,它需要遍历整个文件描述符集合以找到就绪的文件描述符。
  2. poll :
    poll是对 select 的改进,它使用一个包含文件描述符和事件的结构体数组来表示要监视的文件描述符集合,并等待其中任意一人
    文件描述符就绪。相比于 selectpoll不需要遍历整个文件描述符集合,因此性能更好。
  3. epoll :
    epoll 是 Linux 内核提供的高性能多路复用机制。与 selectpoll不的是,epoll中的文件描述符集合是由内核来管理的
    而不是用户空间。这使得 epoll 在大规模并发场景下具有更高的性能和可扩展性

2.应用层的select

// sizeof(fd_set) = 128 1024
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
    - 参数:
        - nfds : 委托内核检测的最大文件描述符的值 + 1
        - readfds : 要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读的属性
            - 一般检测读操作
            - 对应的是对方发送过来的数据,因为读是被动的接收数据,检测的就是读缓冲区
            - 是一个传入传出参数
        - writefds : 要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写的属性
        	- 委托内核检测写缓冲区是不是还可以写数据(写缓冲区不满的就可以写)
        - exceptfds : 检测发生异常的文件描述符的集合
        - timeout : 设置的超时时间
            struct timeval {
                long tv_sec; /* seconds */
                long tv_usec; /* microseconds */
            };
            - NULL : 永久阻塞,直到检测到了文件描述符有变化
            - tv_sec = 0 tv_usec = 0, 不阻塞
            - tv_sec > 0 tv_usec > 0, 阻塞对应的时间
                
        - 返回值 :
            - -1 : 失败
            - >0(n) : 检测的集合中有n个文件描述符发生了变化
                
// 将参数文件描述符fd对应的标志位设置为0
void FD_CLR(int fd, fd_set *set);
// 判断fd对应的标志位是0还是1, 返回值 : fd对应的标志位的值,0返回0, 1返回1
int FD_ISSET(int fd, fd_set *set);
// 将参数文件描述符fd 对应的标志位,设置为1
void FD_SET(int fd, fd_set *set);
// fd_set一共有1024 bit, 全部初始化为0
void FD_ZERO(fd_set *set);

3.驱动层调用poll函数

static unsigned int xxx_poll(struct file *filp, poll_table *pollt){
    unsigned int mask = 0;
    poll_wait(filp,&dev->r_wait,pollt);		//加读等待队列头
    poll_wait(filp,&dev->w_wait,pollt);		//加写等待队列头
    if (...){//可读
    	mask|= POLLIN POLLRDNORM; /*标示数据可获得*/
    }
    if (...){ //可写
    	mask|= POLLOUT POLLWRNORM; /标示数据可写入*/
    }
    return mask;
}

例如:

unsigned int mypoll (struct file *filep, struct poll_table_struct * pollt){
    unsigned int mask =0;
    po11_wait(filep, &wq, pollt); // 把等待队列加入到可读的轮寻表中去
    if(len != 0){ // buffer 内是有数据可读
        mask |= POLLIN | POLLRDNORM;
	}
    return mask;
}

4.poll_wait 函数的功能

#include <linux/poll.h>
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p);

把等待队列加入到监控集合中去

4.6 Linux内核信号驱动的I/0

1.信号驱动I/O概念

异步通知的意思是:一旦设备就绪,则主动通知应用程序,这样应用程序根本就不需要查询设备状态,这一点非常类似于硬件上中断的概
念,比较准确的称谓是信号驱动的异步IO

包含头文件

#include <linux/fs.h>

。异步队列定义

struct fasync_struct{
    spinlock_t fa_lock;
    int magic;
	int	fa_fd;
    struct fasync_struct *fa_next; 		/* singly linked list */
    struct file *fa_file;
    struct rcu_head fa_rcu;
}

。定义一个异步队列的指针

struct fasync_struct *async_queue;

2.生成异步队列 fasync_helper

函数原型

int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
  • 函数功能

    • 用于帮助实现异步通知机制。它在文件描述符上注册/取消异步通知,并在异步事件发生时触发相应的操作.
  • 函数参数

    • fd :文件表述符,应用程序传递

    • filep: 指向file结构体的指针

    • on:

      • 为真时,表示要安装这个异步队列
      • 为假时,表示要卸载这个异步队列
    • fapp: 异步队列的指针的地址 ,传递一级指针的地址。

  • 函数返回值:

    • 成功返回:0
    • 失败返回 : 负的错误码

3.发送信号 kill_fasync()

函数原型

void kill_fasync(struct fasync_struct **fp, int sig, int band)
  • 函数功能
    • 资源可以获得时发送信号
  • 函数参数
    • fp: 异步队列的指针,一级指针地址
    • sig:发送的型号, 这个信号是 SIGIO
    • band :
      • POLL_IN
      • POLL_OUT
  • 函数返回值: 无

例如:

kill_fasync(&async_queue,SIGIO,POLL_IN);

4.实现信号驱动IO流程

1.应用程序要设置信号捕捉函数

void handler(int num){
	printf("num = %d n",num) ;
    read(fd, buf, N);
    printf("buf=%s\n",buf) ;
}

signal(SIGIO, handler);

2.对file结构体的成员f_owner赋值,把file结构体的f_owner与pid进行捆绑


fcntl(fd,F_SETOWN,getpid()); 	// f_owner = getpid();

3.应用程序调用fcntl函数,设置FASYNC标志,在驱动中生成异步队列

int flag = fcntl(fd,F_GETFL);
flag = fcntl(fd,F_SETFL ,flag|FASYNC);		//启用异步标志

驱动会调用一次fasync
4.驱动中会调用 fasync,在驱动fasync函数中,生成一个异步队列

int myfasync (int fd, struct file *filep, int on){
    int ret = 0;
    printk("fd = %d\n",fd);
    printk("on = %d\n",on) ;
    fasync_helper(fd,filep ,on ,&async_queue) ;
    return ret;
}

5.等待资源可以获得是发送信号

kill_fasync(&async_queue,SIGIO,POLL_IN);

5.实例16——信号驱动异步IO

使用kill_fasync实现设备的信号驱动IO

源文件

第05章 Linux内核的中断机制

5.1 Linux内核中断系统

1.中断概述

中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的CPU暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续运行被暂时中断的程序。

2.Linux中断类型

Linux 中断系统支持多种不同类型的中断,其中包括

  1. IRQ 中断:最常见的中断类型,用于处理硬件设备产生的中断请求。IRQ 中断被广泛应用于各种设备,例如网络卡、键盘、鼠标等,
  2. NMI 中断:非屏蔽中断,通常用于处理硬件故和系统异常情况。与RQ 中断不同,NMI 中断无法被蔽或禁止。
  3. 软中断:由软件产生的中断事件,通常用于在内核空间中触发一个函数或任务。软中断是一种快速目可靠的方式,用于在内核空间中
    执行某些操作,例如更新内核状态、清除缓存等。
  4. 时钟中断:由硬件定时器产生的中断事件,用于实现时间片轮转和调度等功能。时钟中断是操作系统中最重要的中断之一,它能够保
    证系统的时间精度、稳定性和可靠性。
  5. 系统调用:也称为软中断,用于触发用户空间程序和内核空间之间的交互,系统调用通常是通过软中断指令实现的.
  6. 异常中断:由 CPU 发生的异常事件,例如除零、页错误、非法指令等。通常会导致系统崩溃或进入死循环,需要谨慎处理.

3.Linux中断数据处理结构

中断处理流程图

image-20240726164845970

Linux 中断数据处理的结构主要由以下几个组成部分:

  1. 中断描述符表(interrupt Descriptor Table,IDT): DT 是一个存储中断向量和中断门描述符的表。每个中断都有一个唯一的中断向量,用于标识该中断。中断向量是一个索引值,用于在DT 中查找相应的中断门描述符。中断门描述符包含了中断服务程序的入口地址、特权级别、中断类型等信息。

    和stm32的中断向量表类似。

  2. 中断服务程序(lnterrupt Service Routine,ISR): 中断服务程序是用于处理特定中断的函数或代码块。当系统发生中断时,CPU 会根据中断向量从IDT 中获取对应的中断门描述符,并跳转到中断服务程序的入口地址执行。中断服务程序负责处理中断事件,例如遗取设备状态、更新数据、发送通知等。

  3. 中断控制器(lnterrupt (ontroller) : 中断控制器是硬件设备,用于管理和分发中断请求。它可以处理来自多个设备的中断请求,并将其传递给 CPU,在Linux中,常见的中断控制器包括 GIC Generic lnterrupt Controller) 和APIC (Advanced Programmable Interrupt Controller)

  4. 中断外理程序(lnterrupt Handler) :中新外理程房是内核中用于外理中断的代码。它负责协调和管理中断事件的处理过程。当中事件发生时,中断处理程序会进行必要的初始化和上下文切换,并调用相应的中断服务程序来处理中断事件。

  5. 中断上下文 (lnterrut ontext) :中断上下文是指在中新事件发生时,CPU 保存的当前执行现场和寄存器值。中断处理程序需要使用中断上下文来恢复中断前的执行状态,并在中断处理完成后正确返回到中断发生的地方。

5.2 Linux内核中断接口函数

1.request_irq 注册IRQ号

函数原型

#incTude<linux/interrupt.h>
typedef irgreturn_t (*irq_handler_t)(int,void *);
static inline int __must_check request_irg(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name,void *dev)
  • 函数功能

    • 向内核注册一个irq中断号
  • 函数参数

    • irg: 要申请的中断号

    • handler: 中断理函数

    • flags: 中断的标志位

      • 中断触发方式

        #define IRQF_TRIGGER_NONE 0x00000000
        #define IRQF_TRIGGER_RISING 0x0000001
        #define IRQF_TRIGGER_FALLING 0x00000002
        #define IRQF_TRIGGER_HIGH 0x00000004
        #define IRQF_TRIGGER_LOW 0x00000008

        中断标志

        
        #define IRQF_SHARED			0x00000080
        #define IRQF_PROBE_SHARED	0x00000100
        #define_IRQF_TIMER			0x00000200
        #define IRQF_PERCPU			0x00000400
        #define IRQF_NOBALANCING	0x00000800
        #define IRQF_IRQPOLL		0x00001000
        #define IRQF_ONESHOT		0x00002000
        #define IRQF_NO_SUISPEND	0x00004000
        #define IRQF_FORCE_RESUME	0x00008000
        #define IRQF_NO_THREAD		0x00010000
        #define IRQF_EARLY_RESUME	0x00020000
        #define IRQF_COND_SUSPEND	0x00040000
        • ​ name: 中断号的名称,和设备号类似。

          • dev : 给中断函数传递参数
        • 函数返回值
          成功返回 0

2.free_irq 释放IRQ号

函数原型

#include<Tinux/interrupt.h>
const void *free_irq(unsigned int irq,void * devid);
  • 函数功能
    • 从内核中注销一个irq中断号。
  • 函数参数
    • irq: 要注销的中断号
    • devid :
      如果是共享中断,需要传递共享中断的参数
      如果不是共享中断,这里传参为NULL即可.
  • 函数返回值:
    • 无返回值

注:IRQ线资源非常宝贵,我们在使用时必须先注册,不使用时必须释放IRQ资源

3.查看系统的中断号

cat /proc/interrupts

例如:

         CPU0       CPU1       CPU2       CPU3       
 0:          6          0          0          0   IO-APIC    2-edge      timer
 1:         37          0          0          0   IO-APIC    1-edge      i8042
 8:          0          1          0          0   IO-APIC    8-edge      rtc0
 9:          0          0          0          0   IO-APIC    9-fasteoi   acpi
12:       1436          0          0         16   IO-APIC   12-edge      i8042
14:          0          0          0          0   IO-APIC   14-edge      ata_piix
15:          0          0          0          0   IO-APIC   15-edge      ata_piix
16:          0       1693          0        233   IO-APIC   16-fasteoi   vmwgfx, snd_ens1371
17:      50329          0          0          0   IO-APIC   17-fasteoi   ehci_hcd:usb1, ioc0
18:          0         43          0          0   IO-APIC   18-fasteoi   uhci_hcd:usb2
19:          0          0        141     363744   IO-APIC   19-fasteoi   ens33
24:          0          0          0          0   PCI-MSI 344064-edge      PCIe PME, pciehp
25:          0          0          0          0   PCI-MSI 346112-edge      PCIe PME, pciehp

4.中断使能与禁止

激活当前CPU中断

local_irq_enable();

。禁止当前CPU中断

local_irq_disable();

激活指定中断线

void enable_irq(unsigned int irq);

禁止指定中断线

void disable_irg(unsigned int irg);

5.中断处理函数的注意事项

  • cpu在向量响应irg中断时,默认不在响应系统的其他中断,因此中断响应必须快
  • 中断处理函数中不能调用引起睡眠的函数,例如: read,accept,connect。
  • 中断处理函数中不能调用 和用户空间交换数据的函数(copy_from_user/copy_to_user),耗时时间久。
  • 不能调用schedule(),会引发系统的调度,让任务刷新,可能会让进程挂载
  • 实现中断处理有一个原则,就是尽可能快地处理并返回。

文章作者: 葛杨文
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 葛杨文 !
评论
  目录