如何使驱动程序通用、简洁、可维护、标准化是每个驱动开发人员应思考的问题。
1) 避免将精力花在写ioctl上
ARM+FPGA异构驱动(如AD数据采集)通常实现为字符驱动。项目开发过程中通常会增加定制功能,这时FPGA需要增加相关功能寄存器,驱动需要编写接口供应用调用。由于这些定制功能的存在,造成不同项目的AD驱动不能复用,增加了驱动维护的工作量。需要避免将精力花在写ioctl上。
借鉴UIO驱动的思想,将I/O放在用户空间,应用程序通过/dev/mem直接配置FPGA寄存器,这样保证驱动的简洁性和通用性,实际上应用更乐意直接读写寄存器。需要解决的问题是应用程序无法处理中断和DMA。所以驱动需要关注的是中断和DMA代码的实现。
2) ioctl or sysfs
传感器驱动通常需要获取几个数值,如温度、电压等等,这些数值代表着设备的属性,在之前的开发过程中,通常使用ioctl来给应用传递这些属性。为了验证驱动程序是否正确,开发人员通常需要编写一个简单APP程序进行验证。
sysfs具有更规范的目录层级,作为一个新生力量使用的人并不是很多。sysfs可以创建设备属性文件,上述我们也讲到用需要通过ioctl来获取例如温度、电压等属性,这样一拍即合,使用sysfs替代ioctl是更优的选择。显而易见的优点是可直接用cat/echo对文件进行读写。但这并不代表ioctl已经被废除了,ioctl 和 sysfs 有不同的优势,ioctl 更适合在用户空间和驱动程序之间传递二进制信息。此外,如果多个线程会同时读写sysfs,需要使用互斥锁保护读写过程。
使用sysfs创建一个文件示例:
dev_set_drvdata(p_dev->p_class_dev, priv);
ret = device_create_file(p_dev->p_class_dev, &dev_attr_voltage0);
if (ret) {
dev_info(p_dev->p_class_dev, "device_create_file dev_attr_voltage0 fail!\n");
return -EINVAL;
}
- dev_set_drvdata函数设置设备私有数据
- device_create_file函数在sysfs下创建设备属性文件
3) 使用dev_dbg替代printk
目前在kernel驱动代码中,都不再建议直接使用printk直接添加打印信息,而是使用dev_info,dev_dbg,dev_err之类的函数代替,虽然这些dev_xxx函数的本质还是使用printk打印的,但是相比起printk:支持打印模块信息、dev信息、支持动态调试(dynamic debug)方式。
打印类型 | 特性 |
---|---|
dev_dbg() | 默认关闭,支持动态打开打印,用于输出调试信息 |
dev_err() | printk马甲,一般使用在严重错误 |
dev_info() | printk马甲,用于驱动加载过程中打印 |
在内核开启动态调试开关:
CONFIG_DYNAMIC_DEBUG
CONFIG_DEBUG_FS
开启调试开关的命令如下:
echo "func omap_i2c_xfer_msg +p" > /sys/kernel/debug/dynamic_debug/control
echo "file drivers/user/iic/adapter/ab01_iic.c +p" > /sys/kernel/debug/dynamic_debug/control
- func开启单个函数调试
- file开启整个文件调试
- “=_” 变为 “=p”,说明改打印信息已开启
- 使用dmesg才能查看dev_dbg输出的调试信息
4) proc和debugfs的区别
proc文件系统最初是为了提供有关系统中进程的信息,后来也用来获取cpu、内存、设备驱动等各种信息。
debugfs和proc文件系统十分相似,正如其名debugfs强调用于调试,默认情况下系统并不挂载debugfs。执行以下命令挂载debugfs。
mount -t debugfs none /sys/kernel/debug
5) vscode+clangd的超级牛力
clangd 可以实现代码语义分析、代码补全、跳转等,能做到代码精准跳转、精准自动补全;其原理是通过读取工程编译自动生成的compile_commands.json 文件来索引其中包含的源文件和关联的头文件,因此能避免索引非编译的代码造成解析时语义混乱,也能带来更快的解析速度。compile_commands.json文件是每个源文件的编译参数、路径等信息组成的一个json文件,clangd 通过这个文件可以准确定位源文件需要引用的头文件从而准确的找到各种宏定义、函数、变量声明的准确值。
Linux源代码已包含可以直接生成compile_commands.json文件的python脚本,在编译后Linux kernel代码路径下执行“python3 scripts/clang-tools/gen_compile_commands.py ”命令即可生成compile_commands.json文件。笔者使用这种方式生成的json被clangd识别错误,可能是clangd版本原因。
另一种方法是在编译源码时,使用bear命令生成compile_commands.json文件。修改编译脚本如下:
bear -a make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j4
要使电脑能识别compile_commands.json文件,需要运行如下命令安装clangd软件包
sudo apt install clangd-11
在vscode上,安装clangd插件,提供前端显示功能,并在vscode配置文件中添加clangd配置:
{
"clangd.arguments": [
"--background-index",
"--compile-commands-dir=${workspaceFolder}/clangd",
"-j=4",
"--all-scopes-completion",
"--completion-style=detailed",
"--header-insertion=iwyu",
"--pch-storage=memory",
"--cross-file-rename",
"--fallback-style=WebKit",
"--pretty",
"--clang-tidy"
],
"clangd.path": "clangd-11",
"C_Cpp.formatting": "clangFormat",
"editor.tabSize": 8,
"git.ignoreLimitWarning": true,
"editor.renderWhitespace": "all"
}
- linux源码使用TAB缩进,并且TAB是8个空格大小
6) jffs2和squashfs
如何在flash上合理部署文件系统,参考大名鼎鼎的OpenWRT就行啦。OpenWRT 采用 OverlayFS,是一种堆叠文件系统。可以将多个挂载点叠加为一个目录。常见的应用是在一个只读的分区上叠加可读写的另一个分区。squashfs是一种压缩过的只读文件系统,jffs2是一种可读写的文件系统。因此在squashfs文件系统上叠加jffs2文件系统,在节省flash空间的同时,保证了文件可读写。
制作squashfs和jffs2镜像脚本如下:
\#!/bin/bash
if [ "$1"x == "clean"x ]; then
rm -rf *.img
elif [ "$1"x == "build"x ]; then
mkfs.jffs2 -s 0x100 -e 0x10000 --pad=0x1EF0000 -o userfs.img -d userfs
mkfs.jffs2 -s 0x100 -e 0x10000 --pad=0x100000 -o eeprom.img -d eeprom
rm -rf rootfs.img
./mksquashfs rootfs rootfs.img
cp *.img ../../output
else
echo "-------build project command list-------"
echo "./build.sh build build all"
echo "./build.sh clean clean all"
fi
- jffs2文件系统需要将空白区域填充满,所以需将—pad设置成此分区大小
7) 使用Fully Preemptible Kernel 抢占模式
之前笔者介绍了通过打real-time补丁的方法,实现内核硬实时。但实际上当前主线linux已添加CONFIG_PREEMPT_RT内核配置符号,但该符号目前是隐藏的,另外主线linux只合并部分RT补丁,打补丁方式实时性更强。
CONFIG_PREEMPT_RT依赖ARCH_SUPPORTS_RT,修改arch/Kconfig文件,将ARCH_SUPPORTS_RT符号配置成y,使CONFIG_PREEMPT_R可见。这样即可在menuconfig中看见Fully Preemptible Kernel (Real-Time)选项。
config ARCH_SUPPORTS_RT
def_bool y
8) 使用__stringify宏
__stringify(x…)这个宏的实际展开为#x,其作用实际上就是把x直接转换为字符串。使用示例如下:
- 用于打印代码行数,另一种方法是是使用%d
print_hex_dump_debug("ctx.key@" __stringify(__LINE__)": ",
DUMP_PREFIX_ADDRESS, 16, 4, key_in, keylen, 1);
- 用于设置uboot环境变量,可重复利用KERNEL_RAM_ADDR等宏定义,使代码更简洁
\#define CONFIG_BOOTCOMMAND "sf probe;sf read "__stringify(KERNEL_RAM_ADDR)" "__stringify(FLASH_KERNEL_OFFSET)" "__stringify(FLASH_KERNEL_SIZE)";" \
"sf read 0x6100000 "__stringify(FLASH_DTB_OFFSET)" "__stringify(FLASH_DTB_SIZE)";" \
"unzip "__stringify(KERNEL_RAM_ADDR)" 0x7100000;" \
"booti
关注博主公众号,优质文章不断更新