本文章介绍linux下应用层访问phy寄存器的几种方式 便于开发者开发

厂家提供节点

一些厂家会直接提供类似/dev/mdio类似的节点,可以find -name 搜索一下看看。通过操作节点可以直接操作phy寄存器

uboot

uboot下可以通过mii cmd来实现读写phy寄存器

ioctl

套接字

API
1
2
3
4
5
6
7
/*	成功时返回文件描述符,失败时返回-1
domain: 套接字中使用的协议族(Protocol Family)
type: 套接字数据传输的类型信息
protocol: 计算机间通信中使用的协议信息
*/
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
协议族

image-20241009093230037

套接字类型

常用:

  • SOCK_STREAM

    流,TCP。面向连接。

    • 传输过程中数据不会消失。
    • 按序传输数据。
    • 传输的数据不存在数据边界(Boundary)。
    • 缓冲区不会因为满而丢失数据,因为有滑动窗口控制,能接收多少都会告诉对端。
  • SOCK_DGRAM

    面向消息的,不可靠的。

    • 强调快速传输而非传输有序。
    • 传输的数据可能丢失也可能损毁。
    • 传输的数据有边界。
    • 限制每次传输数据的大小。

ioctl实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
LONG skfd = -1;
struct mii_data *data;
struct ifreq ifr;

skfd = socket(PF_UNIX, SOCK_STREAM, 0); // #define PF_UNIX AF_UNIX --> Unix domain sockets 常用于本地连接
data = (struct mii_data *)&ifr.ifr_data;
data->phy_id = (unsigned short)ucMiiAddr; //phy地址
data->reg_num = (unsigned short)ucRegAddr; //寄存器地址

ioctl(skfd, SIOCGMIIREG, &ifr);
*pusData = data->val_out;

// include/uapi/linux/socketios.h中
#define SIOCGMIIREG 0x8948 /* Read MII PHY register. */
#define SIOCSMIIREG 0x8949 /* Write MII PHY register. */

底层调用实现

1
2
3
4
5
// 内核通用框架
dev_ioctl --> dev_ifsioc --> dev_do_ioctl --> ndo_do_ioctl

// 底层芯片厂商驱动实现
mstar_emac_drv_init_module --> Mstar_emac_driver --> mstar_emac_drv_probe --> MDev_EMAC_init --> MDev_EMAC_setup --> dev->do_ioctl = MDev_EMAC_ioctl;

简介

当内存发生panic的时候,需要把panic的内容以日志的方式记录下来。目前有几种方式,kdump,mtdoops,crashlog(openwrt特有),以及pstore。

kdump主要用在x86系统上,因为它使用大量的内存和硬盘信息。

mtdoops和crashlog主要用于嵌入式系统。记录文本日志。

mtdoops

mtdoop功能在发生oops时,把msg区写入特定的mt分区。写入过程,不支持文件系统。直接二进制文本写。 它需要由mtd驱动的支持,就是mtd驱动支持mtd_panic_write。也就是原子式写入。不能被中断。一般flash不支持。 在mtdoops.c文件,并在标准内核的drivers/mtd/mtdcore中。

1
2
3
mtd_panic_write
--> _panic_write
---> mtd->_panic_write = panic_nand_write / onenand_panic_write / concat_panic_write 在具体驱动实现

在初始化过程中,需要指定写入哪个分区,对应的分区名/号,可以写入的size是多少。使用kmsg_dump_register注册一个cxt->dump.dump绑定的回调函数 (eg:mmcoops_do_dump),此函数主要是读取kmsg区的内容,直接调用mtd->write的驱动进行写操作。

crashlog

在linux内核启动的时候,保留一块64K的内存,用于记录panic日志,crashlog发生在oop时候,把msg写入之前分配好的MEM区域,并加上magic,再重启后,check magic ok,则把上次日志放入到/sys/kernel/debug/crashlog。

pstore

pstore最初是用于系统发生oops或panic时,自动保存内核log buffer中的日志。不过在当前内核版本中,其已经支持了更多的功能,如保存console日志、ftrace消息和用户空间日志。同时,它还支持将这些消息保存在不同的存储设备中,如内存、块设备或mtd设备。 为了提高灵活性和可扩展性,pstore将以上功能分别抽象为前端和后端,其中像dmesg、console等为pstore提供数据的模块称为前端,而内存设备、块设备等用于存储数据的模块称为后端,pstore core则分别为它们提供相关的注册接口。

通过模块化的设计,实现了前端和后端的解耦,因此若某些模块需要利用pstore保存信息,就可以方便地向pstore添加新的前端。而若需要将pstore数据保存到新的存储设备上,也可以通过向其添加后端设备的方式完成。

image-20240924102113713

除此之外,pstore还设计了一套pstore文件系统,用于查询和操作上一次重启时已经保存的pstore数据。当该文件系统被挂载时,保存在backend中的数据将被读取到pstore fs中,并以文件的形式显示。

源码在/fs/pstore/ram_core.c

1
2
3
4
5
6
7
8
9
10
fs/pstore/
├── ftrace.c # ftrace 前端的实现
├── inode.c # pstore 文件系统的注册与操作
├── internal.h
├── Kconfig
├── Makefile
├── platform.c # pstore 前后端功能的核心
├── pmsg.c # pmsg 前端的实现
├── ram.c # pstore/ram 后端的实现,dram空间分配与管理
├── ram_core.c # pstore/ram 后端的实现,dram的读写操作

oops/panic日志位于 pstore 目录下的dmesg-ramoops-x文件中,根据缓冲区大小可以有多个文件,x从0开始。
函数调用序列日志位于 pstore 目录下的ftrace-ramoops文件中。

使用方法

内核配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CONFIG_PSTORE=y
CONFIG_PSTORE_CONSOLE=y
CONFIG_PSTORE_PMSG=y
CONFIG_PSTORE_RAM=y
CONFIG_PANIC_TIMEOUT=-1

由于log数据存放于DDR,不能掉电,只能依靠自动重启机制来查看,故而要配置:CONFIG_PANIC_TIMEOUT,让系统在 panic 后能自动重启。

mtdoops:
CONFIG_PSTORE=y
CONFIG_PSTORE_CONSOLE=y
CONFIG_PSTORE_PMSG=y
CONFIG_MTD_OOPS=y
CONFIG_MAGIC_SYSRQ=y

blkoops:
CONFIG_PSTORE=y
CONFIG_PSTORE_CONSOLE=y
CONFIG_PSTORE_PMSG=y
CONFIG_PSTORE_BLK=y
CONFIG_MTD_PSTORE=y
CONFIG_MAGIC_SYSRQ=y

设备树配置

1
2
3
4
5
6
7
8
9
10
11
12
13
ramoops_mem: ramoops_mem {
reg = <0x0 0x110000 0x0 0xf0000>;
reg-names = "ramoops_mem";
};

ramoops {
compatible = "ramoops";
record-size = <0x0 0x20000>;
console-size = <0x0 0x80000>;
ftrace-size = <0x0 0x00000>;
pmsg-size = <0x0 0x50000>;
memory-region = <&ramoops_mem>;
};

bootargs分区配置

1
2
3
4
5
6
7
8
9
10
11
12
13
方案1:
bootargs = "console=ttyS1,115200 loglevel=8 rootwait root=/dev/mtdblock5 rootfstype=squashfs mtdoops.mtddev=pstore";
(blk则为 pstore_blk.blkdev=pstore)

blkparts = "mtdparts=spi0.0:64k(spl)ro,256k(uboot)ro,64k(dtb)ro,128k(pstore),3m(kernel)ro,4m(rootfs)ro,-(data)";

方案2:
bootargs = "console=ttyS1,115200 loglevel=8 rootwait root=/dev/mtdblock5 rootfstype=squashfs mtdoops.mtddev=pstore";
在设备树中
partition@60000 {
label = "pstore";
reg = <0x60000 0x20000>;
};

挂载pstore文件系统

1
mount -t pstore pstore /sys/fs/pstore

summary

以上几种方案都使用到了kmsg_dump的注册机制。 注册很简单,就是把一个全局变量结构挂到一个全局list中。 kmsg_dump是oops时进入kmsg_dump的入口。由panic,die,oops_exit等函数调用。它会一一调用回调函数。 每一个回调函数都会用到kmsg_dump_get_buffer。它先是计算dump还有多少空间,然后把kmsg中最后的一部分写进去。

cma以及mma内存分配介绍

mstar平台为例子。

问题引入:

ssr931g在插入usb过程中会报错。

1
cma:cma_calloc:memory range at ptrval is busy,  retrying。
cma介绍

CMA –> 连续内存分配器。是一种用于申请大量的,并且物理上连续的内存块的方法。在设备驱动USB,HOST,DMA,ETH PHY中起关键作用。

内存分配 –> Sstar

image-20240913102052749

sstar的内存分配图。如上。LX_MEM有可能有好几块,例如某些SOC上会有双通道DDR,每个DDR上面会各自分配一块LX_MEM。还有某些特别的情况,一颗DDR上面可能会分配多个LX_MEM。多个LX_MEM的命名规则为LX_MEM1、LX_MEM2,以此类推。

注意:MMA Heap以及HW IP Layout分配出来的内存在物理上是连续的,但是LX_MEM分配给linux kernel的不一定是在物理上连续的。

ARM架构

cache多级相连

cache是多级的,在一个系统中会有多级cache。

image-20240910090615943

一般来说,在bit-little中,L1在core中,且L1缓存又分为 I-cache指令环境和 D-cache数据缓存。L2 cache在cluster中,L3则在BUS总线上。

当CPU计算时,首先去L1去寻找需要的数据,如果没有则去L2寻找,接着从L3中寻找,如果都没有,则从内存中读取数据。所以,如果某些数据需要经常被访问,那么这些数据存放在L1中的效率会最高。

计算机缓存行cache line

高速缓存其实就是一组称之为缓存行cache line的固定大小数据块。其大小是以突发写或者突发读的周期大小为基础的。

即使处理器只存取一个字节的存储器,高速缓存控制器也启动整个存取器访问周期并请求整个数据块。缓存行第一个字节的地址总是突发周期尺寸的倍数。缓存行的起始位置总是与突发周期的开头保持一致。

当从内存中取单元到cache中时,会一次取一个cacheline大小的内存区域到cache中,然后存进相应的cacheline中。

cache一般和MMU结合使用

很多时候cache都是和MMU一起使用的(即同时开启或关闭)。因为MMU的页表entry属性中控制着内存权限和cache缓存策略等等。

image-20240910091751607

在ARM架构中,L1 cache都是VIPT(virtual index physical tag 虚拟地址做索引,物理地址做tag)的,也就是当有一个虚拟地址送进来,MMU在开始进行地址翻译的时候,Virtual Index就可以去L1 cache中查询了,MMU查询和L1 cache的index查询是同时进行的。如果L1 Miss了,则再去查询L2,L2还找不到则再去查询L3。 注意在arm架构中,仅仅L1是VIPT,L2和L3都是PIPT。

image-20240910094036583

env小记

在完成rootfs的构建之后,通过配置内核支持Initramfs,并设置好rootfs的路径即可编译出带有文件系统的vmliunx

initramfs简介

是什么

initramfs 即 initram file system,翻译成中文意思就是 初始 ram 文件系统,基于 tmpfs,是一种大小灵活,直接作用在内存中的文件系统。initramfs 包含的工具和脚本,在正式的根文件系统的初始化脚本 init 启动之前,就被挂载。initramfs 是可选的,内核编译选项默认开启 initramfs(initrd)。那么什么情况下考虑使用 initramfs 呢?

  • 加载模块,如三方驱动
  • 定制化启动系统
  • 制作一个很小的 rescue shell
  • 内核不能,但是用户态可以完成的命令

initramfs 在内核启动的早期提供用一个户态环境,用于完成在内核启动阶段不易完成的工作。initramfs 包含的工具可以解密抽象层(用于加密的文件系统),逻辑卷管理器,软件 RAID,蓝牙驱动程序等。

一个 initramfs 至少包含一个文件,即 /init,内核将这个文件执行起来的进程设置为 main init 进程,pid = 1。内核挂载 initramfs 时,文件系统的根分区并没有挂载,所以无法访问文件系统中的文件。大多数嵌入式设备可能需要一个 shell,那么也会在 initramfs 打包进一个 shell。如果还需要其他工具或者脚本,也可以打包进 initramfs,但注意,必须包含依赖,因为 initramfs 是一个能够独立运行的 ram 文件系统

如何工作

initramfs 和我们常见的文件系统类似,可能存在 /usr、/bin 等目录。里面包含着我们的工具和脚本。initramfs 需要使用 cpio 来归档,cpio 是一个有着古老历史的文件归档解决方案,类似于 linux 中常用的 tar,或者 windows 中的 zip,主要作用是将多个文件打包成一个文件(但是没有压缩)。使用 cpio,是因为其代码易于实现,而且能够兼容更多的设备。

归档之后,需要考虑 initramfs 文件的体积,要进一步压缩,减少内存或磁盘的占用。所有文件,工具,库,配置设置(如果适用)等都放入 cpio 归档后,使用 gzip 实用程序压缩 cipo 文件,并将其与linux 内核一起存储。引导加载程序(通常是 grub 或者 uboot)将在引导时将其提供给内核,以便内核知道需要一个 initramfs。

内核一旦检测到 initramfs,会创建一个 tmpfs 文件系统,提取 gzip 中存档的 initramfs,并存入 tmpfs 中,内核启动 tmpfs 文件系统中的 init 脚本。该脚本用于挂载实际的根文件系统,当完成根文件系统和其他的一些重要的文件系统的安装之后,init 脚本会切换至真实的根文件系统,并在系统上调用 / sbin / init 二进制文件,进行后续的启动过程。

initramfs配置

使用make menuconfig进入内核菜单后。

配置General setup —> [*]Initial RAM filesystem and RAM disk (initramfs/initrd) support。

配置General setup —> ()Initramfs source file(s) [注]在括号里写入构建的rootfs路径,绝对路径和相对路径均可。此处填写的是busybox编译的rootfs.cpio。

make后重新编译内核即可

与ramdisk区别

ramdisk是在一块内存区域中创建的块设备,用于存放文件系统。ramdisk的容量是固定的,不能象ramfs一样动态增长。ramdisk需要内核的文件系统驱动程序(如ext2)来操作其上的数据,而ramfs则是内核的天然特性,无需额外的驱动程序。ramdisk也象其他文件系统设备一样, 需要在块设备和内存中的磁盘高速缓存之间复制数据,而这种数据复制实际不必要的

gcc编译过程

  • 预处理阶段,完成宏定义和include文件展开工作 (.i)
  • 根据编译参数进行不同程度的优化,编译成汇编代码 (.S)
  • 用汇编器把汇编代码进一步生成目标代码 (.o)
  • 用连接器把生成目标的代码和系统或者用户提供的库连接起来,生成可执行文件

gdb功能

  • 设置断点

  • 监视程序变量的值

  • 程序单步执行

  • 显示修改变量值

  • 显示修改寄存器

  • 查看程序堆栈情况

gdb调试使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
gcc -g  main.c                      //在目标文件加入源代码的信息
gdb a.out

(gdb) start //开始调试
(gdb) n //一条一条执行
(gdb) step/s //执行下一条,如果函数进入函数
(gdb) backtrace/bt //查看函数调用栈帧
(gdb) info/i locals //查看当前栈帧局部变量
(gdb) frame/f //选择栈帧,再查看局部变量
(gdb) print/p //打印变量的值
(gdb) finish //运行到当前函数返回
(gdb) set var sum=0 //修改变量值
(gdb) list/l 行号或函数名 //列出源码
(gdb) display/undisplay sum //每次停下显示变量的值/取消跟踪
(gdb) break/b 行号或函数名 //设置断点
(gdb) continue/c //连续运行
(gdb) info/i breakpoints //查看已经设置的断点
(gdb) delete breakpoints 2 //删除某个断点
(gdb) disable/enable breakpoints 3 //禁用/启用某个断点
(gdb) break 9 if sum != 0 //满足条件才激活断点
(gdb) run/r //重新从程序开头连续执行
(gdb) watch input[4] //设置观察点
(gdb) info/i watchpoints //查看设置的观察点
(gdb) x/7b input //打印存储器内容,b--每个字节一组,7--7组
(gdb) disassemble //反汇编当前函数或指定函数
(gdb) si // 一条指令一条指令调试 而 s 是一行一行代码
(gdb) info registers // 显示所有寄存器的当前值
(gdb) x/20 $esp //查看内存中开始的20个数

poll机制 使用方法

使用非阻塞IO的应用程序通常会使用select和poll系统调用查询是否可对设备进行无阻塞的访问,这两个系统调用最终会引发设备驱动中poll函数被执行。

atomic_t介绍

atomic_t: 这是一个用于定义原子变量的类型。原子变量是一种特殊的变量,可以在多线程环境下安全地进行操作,而不需要使用锁来防止竞争条件。atomic_t 通常用于存储整数值,并且提供了一组原子操作函数来保证操作的原子性。

ATOMIC_INIT(0): 这是一个宏,用于初始化原子变量。在这种情况下,ATOMIC_INIT(0)gstSub1gMsgFlag 初始化为 0。原子变量的初始化通常需要使用这个宏,而不是直接赋值,因为这样可以保证初始化过程的线程安全性。

DECLARE_WAIT_QUEUE_HEAD介绍

用于在linux内核编程中声明和初始化等待队列(wait queue)头的宏。等待队列用于在某些条件满足之前让线程处于睡眠状态,并且在条件满足的时候唤醒他们。常用于阻塞式等待

1
2
3
4
DECLARE_WAIT_QUEUE_HEAD(msg_wait)
等价于
wait_queue_head_t msg_wait;
init_waitqueue_head(&msg_wait);
wake_up_interruptible介绍

用于唤醒等待队列中处于可中断睡眠状态的进程。进程被唤醒后,会从睡眠状态恢复并执行对应任务(.poll绑定函数)。

poll_wait(file, &msg_wait, wait);介绍

将当前进程mgs_wait添加到指定的等待队列中。

poll函数返回事件的状态。

image-20240812150319868

linux设备驱动注册sys文件访问

device_create_file 在当前设备的sys目录下创建一个属性对应的文件。

设备属性文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)

定义一个device_attribute类型的变量,##代表将两边的名字拼接起来,因此,我们得到的变量名称是含有dev_attr的,该宏定义需要 传入四个参数,name,mode,show,store分别代表文件名,文件权限,show回调函数(cat 节点),store回调函数(echo 节 点),参数mode,可以用宏(S_IRUSR、S_IWUSR、S_IXUSR等等)来定义,表示读写权限。

extern int device_create_file(struct device *device,
const struct device_attribute *entry);
extern void device_remove_file(struct device *dev,
const struct device_attribute *attr);

device表示设备,其成员中有个bus_type变量,用于指定设备挂载在某个总线上,并且会在总线的devices目录下创建一个属于该设备的目录。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#用于在sys文件系统中创建一个设备属性

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

static ssize_t my_sysfs_show(struct device *dev, struct device_attribute *attr, char *buf) {
return sprintf(buf, "Hello, world!\n");
}

static ssize_t my_sysfs_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) {
// 处理写入的数据
return count;
}

static DEVICE_ATTR(my_sysfs_file, 0664, my_sysfs_show, my_sysfs_store);

static int __init my_driver_init(void) {
struct device *dev = /* 获取设备指针 */;
int retval;

retval = device_create_file(dev, &dev_attr_my_sysfs_file);
if (retval) {
printk(KERN_ERR "Failed to create sysfs file\n");
return retval;
}

printk(KERN_INFO "Sysfs file created\n");
return 0;

}

static void __exit my_driver_exit(void) {
struct device *dev = /* 获取设备指针 */;
device_remove_file(dev, &dev_attr_my_sysfs_file);
printk(KERN_INFO "Sysfs file removed\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");

shc加密脚本

当shell脚本中包含密码,不希望其他人查看到shell脚本中的密码等信息,可以安装使用SHC工具进行加密。

Shc可以用来对shell脚本进行加密,可以将shell脚本转换为一个可执行的二进制文件。经过shc对shell脚本进行加密后,会同时生成两种个新的文件,一个是加密后的可执行的二进制文件(文件名以.x结束),另一个是C语言的原文件(文件名以.x.c结束)。

常用参数
1
2
3
4
5
-e date (指定过期日期)
-m message (指定过期提示的信息)
-f script_name(指定要编译的shell的路径及文件名)
-r Relax security. (可以相同操作系统的不同系统中执行)
-v Verbose compilation(编译的详细情况)
使用方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# shc -v -f abc.sh
-v 是现实加密过程
-f 后面跟需要加密的文件
运行后会生成两个文件:
abc.sh.x 和 abc.sh.x.c
abc.sh.x为二进制文件,赋予执行权限后,可直接执行。更改名字mv abc.sh.x a.sh
abc.sh.x.c 是c源文件。基本没用,可以删除

另shc还提供了一种设定有效执行期限的方法,过期时间,如:
# shc -e 28/01/2012 -m "过期了" -f abc.sh
选项“-e”指定过期时间,格式为“日/月/年”;选项“-m”指定过期后执行此shell程序的提示信息。
如果在过期后执行,则会有如下提示:

# ./abc.sh.x
./abc.sh.x: has expired!
过期了
使用unshc可以解密shc加密的文件
0%