uboot-命令和环境变量

1.1 help或者?#

image

1.1.1 help+具体命令#

? bootz 或 help bootz
image

1.2 信息查询#

1.2.1 bdinfo#

image

1.2.2 printenv#

image
image
前确保 uboot 中的环境变量 bootargs 内容如下:
console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw

1
2
3
4
5
cp arch/arm/boot/zImage /home/zuozhongkai/linux/tftpboot/ -f
cp arch/arm/boot/dts/imx6ull-14x14-evk.dtb /home/zuozhongkai/linux/tftpboot/ -f
tftp 80800000 zImage
tftp 83000000 imx6ull-14x14-evk.dtb
bootz 80800000 - 83000000

1.2.3 version#

image

1.3 环境变量#

1.3.1 setenv设定环境变量#

设置bootdelay时间为5s.

1
2
setenv bootdelay 5
saveenv

image

1
2
setenv author zuozhongkai
saveenv

新建环境变量也是用setenv。

1.3.2 saveenv保存环境变量#

1.3.3 setenv删除环境变量#

1
2
setenv author
saveenv

设置变量为空表示删除掉该环境变量,重启该环境变量就不会存在了。

1.3.4 环境变量原理#

include/env_default.h定义了很多环境变量,如bootargs,bootdelay,bootcmd等
image
由 于 没 有 定 义DEFAULT_ENV_INSTANCE_EMBEDDED和CONFIG_SYS_REDUNDAND_ENVIRONMENT,因此 uchar default_environment[]数组保存环境变量。

1.3.4.1 bootcmd展开#

CONFIG_BOOTCOMMAND等一系列宏都是定义在include/configs/mx6ull_alientek_emmc.h:
image
uboot 使用了类似 shell 脚本语言的方式来编写的,首先run findfdt, findfdt 是用来查找开发板对应的设备树文件(.dtb),IMX6ULL EVK 的设备树文件为imx6ull-14x14-evk.dtb,findfdt内容如下:

1
2
3
4
5
6
7
8
9
"findfdt="\
"if test $fdt_file = undefined; then " \
"if test $board_name = EVK && test $board_rev = 9X9; then " \
"setenv fdt_file imx6ull-9x9-evk.dtb; fi; " \
"if test $board_name = EVK && test $board_rev = 14X14; then " \
"setenv fdt_file imx6ull-14x14-evk.dtb; fi; " \
"if test $fdt_file = undefined; then " \
"echo WARNING: Could not determine dtb to use; fi; " \
"fi;\0" \

那么run findfdt就等同于执行setenv fdt_file imx6ull-14x14-evk.dtb。设置fdt_file环境变量等于imx6ull-14x14-evk.dtb。
mmc dev ${mmcdev}用于切换 mmc 设备,mmcdev 为 1,因此这行代码就是:mmc dev 1,也就是切换到 EMMC 上。
先执行 mmc dev ${mmcdev}切换到 EMMC 上,然后使用命令 mmc rescan 扫描看有没有 SD 卡或者 EMMC 存在,如果没有的话就直接跳到else,执行 run netboot,netboot也是一个自定义的环境变量,这个变量是从网络启动 Linux 的。
扫描到EMMC后,run loadbootscript
loadbootscript=fatload mmc ${mmcdev}:${mmcpart} ${loadaddr} ${script};
其中 mmcdev=1,mmcpart=1,loadaddr=0x80800000,script= boot.scr,因此展开以后就是:
loadbootscript=fatload mmc 1:1 0x80800000 boot.scr;
loadbootscript 就是从 mmc1 的分区 1 中读取文件 boot.src 到 DRAM 的 0X80800000 处。但是 mmc1 的分区 1 中没有 boot.src 这个文件,可以使用命令ls mmc 1:1查看一下 mmc1 分区1 中的所有文件,看看有没有 boot.src 这个文件。再展开bootscript:

1
2
bootscript=echo Running bootscript from mmc ...;
source

由于boot.src 文件不存在,所以 bootscript 也就不会运行。就运行环境变量 loadimage:
loadimage=fatload mmc ${mmcdev}:${mmcpart} ${loadaddr} ${image}
loadimage展开:
loadimage=fatload mmc 1:1 0x80800000 zImage
fatload就是从 mmc1 的分区中读取 zImage 到内存的 0X80800000 处,而 mmc1的分区 1 中存在 zImage。
loadimage执行完,执行mmcboot环境变量,run mmcboot:
image
①打印Booting from mmc …
②运行环境变量 mmcargs,mmcargs 用来设置 bootargs,后面分析 bootargs 的时候在学习。
③判断boot_fdt是否为yes或者try,根据uboot输出的环境变量信息可知boot_fdt=try。因此执行loadfdt。
④执行loadfdt环境变量,如下:
loadfdt=fatload mmc ${mmcdev}:${mmcpart} ${fdt_addr} ${fdt_file}
展开以后就是:
loadfdt=fatload mmc 1:1 0x83000000 imx6ull-14x14-evk.dtb
因此 loadfdt 的作用就是从 mmc1 的分区 1 中读取 imx6ull-14x14-evk.dtb 文件并放到 0x83000000处。
⑤loadfdt加载dtb成功后,调用命令 bootz 启动 linux:
bootz ${loadaddr} - ${fdt_addr};
展开:
bootz 0x80800000 - 0x83000000 (注意‘-’前后要有空格)

总结一下bootcmd展开:

1
2
3
4
mmc dev 1 //切换到 EMMC
fatload mmc 1:1 0x80800000 zImage //读取 zImage 到 0x80800000 处
fatload mmc 1:1 0x83000000 imx6ull-14x14-evk.dtb //读取设备树到 0x83000000 处
bootz 0x80800000 - 0x83000000 //启动 Linux

我们可以将bootcmd环境变量进行简化:

1
2
3
4
5
#define CONFIG_BOOTCOMMAND \
"mmc dev 1;" \
"fatload mmc 1:1 0x80800000 zImage;" \
"fatload mmc 1:1 0x83000000 imx6ull-alientek-emmc.dtb;" \
"bootz 0x80800000 - 0x83000000;"

或者:
setenv bootcmd 'mmc dev 1; fatload mmc 1:1 80800000 zImage; fatload mmc 1:1 83000000 imx6ullalientek-emmc.dtb; bootz 80800000 - 83000000;'

1.3.4.2 bootargs展开#

bootargs 保存着 uboot 传递给 Linux 内核的参数。从emmc启动时,bootargs 环境变量是由 mmcargs 设置的:
mmcargs=setenv bootargs console=${console},${baudrate} root=${mmcroot}
其中:
console=ttymxc0,baudrate=115200,mmcroot=/dev/mmcblk1p2 rootwait rw
mmcargs展开以后就是:
mmcargs=setenv bootargs console= ttymxc0, 115200 root= /dev/mmcblk1p2 rootwait rw

可以看出环境变量 mmcargs 就是设置 bootargs 的值为console= ttymxc0, 115200 root= /dev/mmcblk1p2 rootwait rw
console=ttymxc0,115200 综合起来就是设置 ttymxc0(也就是串口 1)作为 Linux 的终端,并且串口波特率设置为 115200。
root=/dev/mmcblk1p2 用于指明根文件系统存放在mmcblk1 设备的分区 2 中。在 I.MX6U-ALPHA 开发板中/dev/mmcblk1 表示 EMMC,而/dev/mmcblk1p2 表示EMMC 的分区 2。
③root 后面有rootwait rw,rootwait 表示等待 mmc 设备初始化完成以后再挂载,否则的话mmc 设备还没初始化完成就挂载根文件系统会出错的。rw 表示根文件系统是可以读写的,不加rw 的话可能无法在根文件系统中进行写操作,只能进行读操作。

1.3.4.3 bootdlelay#

uboot命令行倒计时

1.4 内存操作#

1.4.1 md#

md[.b, .w, .l] address [# of objects]

注意:uboot 命令中的数字都是十六进制的

  1. 命令中的[.b .w .l]对应 byte、word 和 long,也就是分别以 1 个字节、2 个字节、4 个字节来显示内存值.
  2. address 就是要查看的内存起始地址.
  3. [# of objects]表示要查看的数据长度:

这个数据长度单位不是字节,而是跟你所选择的显示格式有关。比如你设置要查看的内存长度为20(十六进制为 0x14),如果显示格式为.b 的话那就表示 20 个字节;如果显示格式为.w 的话就表示 20 个 word,也就是 20*2=40 个字节;如果显示格式为.l 的话就表示 20 个 long,也就是20*4=80 个字节

1
2
3
md.b 80000000 10 //读16字节
md.w 80000000 10 //读32字节
md.l 80000000 10 //读64字节

image

1.4.2 nm命令#

写内存,写成功后地址不会自增。

1
nm [.b, .w, .l] address

image
0500e031 表示地址 0x80000000 现在的数据,后面就可以输入要修改后的数据 0x12345678,输入完成以后按下回车,然后再输入‘q’即可退出。
退出后输入md.l查看。
image

1.4.3 mm命令#

写内存,写成功后地址会自增。
比如以.l 格式修改从地址 0x80000000 开始的连续 3 个内存块(3*4=12个字节)的数据为 0X05050505.
image
输入md命令查看一下:
image

1.4.4 mw命令#

写一段连续的内存。

1
mw [.b, .w, .l] address value [count]

比如使用.l 格式将以 0X80000000 为起始地址的 0x10 个内存块(0x10 * 4=64 字节)填充为 0X0A0A0A0A

1
mw.l 80000000 0A0A0A0A 10

image

1.4.5 cp命令#

数据拷贝命令,用于将 DRAM 中的数据从一段内存拷贝到另一段内存中,或者把 Nor Flash 中的数据拷贝到 DRAM 中。

cp [.b, .w, .l] source target count

我们使用.l 格式将 0x80000000 处的地址拷贝到 0X80000100 处,长度为 0x10 个内存块(0x10 * 4=64 个字节).

cp.l 80000000 80000100 10

image

1.4.6 cmp命令#

比较两段内存的数据是否相等。

cmp [.b, .w, .l] addr1 addr2 count

比较 0x80000000 和 0X80000100 这两个地址数据是否相等,比较长度为 0x10 个内存块(16 * 4=64 个字节)

cmp.l 80000000 80000100 10

image
我们再随便挑两段内存比较一下,比如地址0x80002000 和 0x800003000,长度为 0X10.
image
可以看出,0x80002000 处的数据和 0x80003000 处的数据就不一样.

1.5 网络操作命令#

环境变量 描述
ipaddr 开发板 ip 地址,可以不设置,使用 dhcp 命令来从路由器获取 IP 地址
ethaddr 开发板的 MAC 地址,一定要设置
gatewayip 网关地址
netmask 子网掩码
serverip 服务器 IP 地址,也就是 Ubuntu 主机 IP 地址,用于调试代码

预先设置号网络相关环境变量:

setenv ipaddr 192.168.1.50
setenv ethaddr b8:ae:1d:01:00:00
setenv gatewayip 192.168.1.1
setenv netmask 255.255.255.0
setenv serverip 192.168.1.253
saveenv

1.5.1 ping命令#

ping 192.168.1.100

image

1.5.2 dhcp命令(自动ip获取)#

dhcp 用于从路由器获取 IP 地址,前提得开发板连接到路由器上的,如果开发板是和电脑
直连的,那么 dhcp 命令就会失效。直接输入 dhcp 命令即可通过路由器获取到 IP 地址。

image
DHCP 不单单是获取 IP 地址,其还会通过 TFTP 来启动 linux 内核,输入? dhcp
可查看 dhcp 命令详细的信息:
image

1.5.3 nfs命令#

nfs(Network File System)网络文件系统,通过网络将编译好的 linux 镜像和设备树文件下载
到 DRAM 中,然后就可以直接运行。
虚拟机Ubuntu先搭建好nfs服务,详见:nfs服务搭建 linux搭建nfs服务 | Hexo (fuzidage.github.io)

1
nfs [loadAddress] [[hostIPaddr:]bootfilename]

loadAddress 是要保存的 DRAM 地址,[[hostIPaddr:]bootfilename]是要下载的文件地址,
我们将正点原子官方编译出来的 Linux 镜像文件 zImage 下载到开发板 DRAM 的 0x80800000这个地址处。

1
nfs 80800000 192.168.1.253:/home/zuozhongkai/linux/nfs/zImage

下载过程如图下:
image

下载完成以后查看 0x80800000 地址处的数据,使用命令 md.b 来查看前 0x100 个字节的数据。
image
我们再用UE打开编译出的zImage,对比一下说明 nfs 命令下载到的zImage 是正确的。
image

1.5.4 tftp命令#

tftp 命令的作用和 nfs 命令一样,都是用于通过网络下载东西到 DRAM 中。tftp服务搭建参考:tftp服务搭建 linux搭建tftp和ftp服务 | Hexo (fuzidage.github.io)

tftpboot [loadAddress] [[hostIPaddr:]bootfilename]

loadAddress 是文 件在 DRAM 中的 存放 地址 ,
[[hostIPaddr:]bootfilename]是要从 Ubuntu 中下载的文件。但是和 nfs 命令的区别在于,tftp 命令不需要输入文件在 Ubuntu 中的完整路径,只需要输入文件名即可。比如我们现在将 tftpboot 文件夹里面的 zImage 文件下载到开发板 DRAM 的 0X80800000 地址处。

1
tftp 80800000 zImage

image

1.6 emmc和sd卡指令#

image

mmc相关命令:

命令 描述
mmc info 输出 MMC 设备信息
mmc read 读取 MMC 中的数据
mmc wirte 向 MMC 设备写入数据
mmc rescan 扫描 MMC 设备
mmc part 列出 MMC 设备的分区
mmc dev 切换 MMC 设备
mmc list 列出当前有效的所有 MMC 设备
mmc hwpartition 设置 MMC 设备的分区
mmc bootbus…… 设置指定 MMC 设备的 BOOT_BUS_WIDTH 域的值
mmc bootpart…… 设置指定 MMC 设备的 boot 和 RPMB 分区的大小
mmc partconf…… 设置指定 MMC 设备的 PARTITION_CONFG 域的值
mmc rst 复位 MMC 设备
mmc setdsr 设置 DSR 寄存器的值

1.6.1 mmc info#

image

当前选中的 MMC 设备是EMMC,版本为 5.0,容量为 7.1GiB(EMMC为 8GB),速度为 52000000Hz=52MHz,8 位宽的总线。还有一个与 mmc info 命令相同功能的命令:mmcinfo,“mmc”和“info”之间没有空格。

1.6.2 mmc rescan#

mmc rescan 命令用于扫描当前开发板上所有的 MMC 设备,包括 EMMC 和 SD 卡。

1.6.3 mmc list#

查看当前开发板一共有几个 MMC 设备.
image

可以看出当前开发板有两个 MMC 设备:FSL_SDHC:0 和 FSL_SDHC:1 (eMMC),这是因为我现在用的是 EMMC 版本的核心板,加上 SD 卡一共有两个 MMC 设备,FSL_SDHC:0 是 SD卡,FSL_SDHC:1(eMMC)是 EMMC。默认会将 EMMC 设置为当前 MMC 设备,这就是为什么输入mmc info查询到的是 EMMC 设备信息,而不是 SD 卡。要想查看 SD 卡信息,就要使用命令mmc dev来将 SD 卡设置为当前的 MMC 设备。

1.6.4 mmc dev#

选择切换当前emmc设备。

mmc dev [dev] [part]

[dev]用来设置要切换的 MMC 设备号,[part]是分区号。如果不写分区号的话默认为分区 0。

1
mmc dev 0 //切换到 SD 卡,0 为 SD 卡,1 为 eMMC

image
切换到sd这个mmc后,输入mmc info命令既可以查看sd信息:
image

可以看到当前 SD 卡为 3.0 版本的,容量为 14.8GiB(16GB 的 SD 卡),4 位宽的总线。

image

1.6.5 mmc part#

查看mmc分区

1
2
mmc dev 1 //切换到 EMMC
mmc part //查看 EMMC 分区

image

切到核心板的emmc后,mmc part显示分区信息,此时 EMMC 有两个分区,第一个分区起始扇区为 20480,长度为 262144 个扇区;第二个分区起始扇区为 282624,长度为 14594048 个扇区。
如果 EMMC 里面烧写了 Linux 系统的话,EMMC 是有 3 个分区的,第 0 个分区存放 uboot,第 1 个分区存放Linux 镜像文件和设备树,第 2 个分区存放根文件系统。但是在上图中只有两个分区,那是因为第 0 个分区没有格式化,所以识别不出来,实际上第 0 个分区是存在的。一个新的 SD卡默认只有一个分区,那就是分区 0,所以前面讲解的 uboot 烧写到 SD 卡,其实就是将 u-boot.bin烧写到了 SD 卡的分区 0 里面.

1
mmc dev 1 2 //将 EMMC 的分区 2 设置为当前 MMC 设备

image

1.6.6 mmc read#

1
mmc read addr blk# cnt

读mmc设备中的数据。addr 是数据读取到 DRAM 中的地址,blk 是要读取的块起始地址(十六进制),一个块是 512字节,这里的块和扇区是一个意思,在 MMC 设备中我们通常说扇区,cnt 是要读取的块数量(十六进制)。比如从 EMMC 的第 1536(0x600)个块开始,读取 16(0x10)个块的数据到 DRAM 的0X80800000 地址处

1
2
mmc dev 1 0 //切换到 MMC 分区 0
mmc read 80800000 600 10 //读取数据

image
这里我们还看不出来读取是否正确,通过 md.b 命令查看 0x80800000 处的数据就行了,查看 16*512=8192(0x2000)个字节的数据。
image

可以看到baudrate=115200.board_name=EVK.board_rev=14X14等字样,
这个就是 uboot 中的环境变量。EMMC 核心板 uboot 环境变量的存储起始地址就是1536*512=786432

1.6.7 mmc write#

1
mmc write addr blk# cnt

从DRAM写数据到mmc设备。
比如通过 nfs 或者 tftp 命令将新的 u-boot.bin 下载到开发板的 DRAM 中,然后再使用命令mmc write将其写入到 MMC设备中。

1
2
3
mmc dev 0 //切换到 SD 卡
version //查看uboot版本号
tftp 80800000 u-boot.imx

image
可以看出,u-boot.imx 大小为 379904 字节,379904/512=742,所以我们要向 SD 卡中写入742 个块,如果有小数的话就要加 1 个块。使用命令mmc write从 SD 卡分区 0 第 2 个块(扇区)开始烧写,一共烧写 742(0x2E6)个块

1
2
mmc dev 0 0
mmc write 80800000 2 2E6

image

烧写成功,重启开发板(从 SD 卡启动),重启以后再输入 version 来查看版本号:
image
这样就给mmc0也就是sd卡烧录了uboot, 同理要烧录emmc也是同理,切到mmc1即可:

1
2
3
4
mmc dev 1 0 //切换到 EMMC 分区 0
tftp 80800000 u-boot.imx //下载 u-boot.imx 到 DRAM
mmc write 80800000 2 32E //烧写 u-boot.imx 到 EMMC 中
mmc partconf 1 1 0 0 //分区配置,EMMC 需要这一步!

注意:千万不要写 SD 卡或者 EMMC 的前两个块(扇区),里面保存着分区表!

1.6.7 mmc erase#

1
mmc erase blk# cnt

擦除 MMC 设备的指定块, blk 为要擦除的起始块,cnt 是要擦除的数量。

1.7 文件操作命令(fat文件系统)#

1.7.1 fatinfo#

fatinfo <interface> [<dev[:part]>]

于查询指定 MMC 设备分区的文件系统信息,interface 表示接口,比如 mmc,dev 是查询的设备号,part 是要查询的分区。比如我们要查询 EMMC 分区 1 的文件系统信息:

1
fatinfo mmc 1:1

image
上图显示mmc1也就是emmc设备的分区1的文件系统为fat32格式。

1.7.1 fatls#

fatls <interface> [<dev[:part]>] [directory]

查询设备分区的目录和文件信息。interface 是要查询的接口,比如 mmc,dev 是要查询的设备号,part 是要查询的分区,directory是要查询的目录。

1
fatls mmc 1:1   //查询 mmc1设备(EMMC 设备)中分区 1 中的所有的目录和文件

image

1.7.3 fstype#

fstype <interface> <dev>:<part>

查看 MMC 设备某个分区的文件系统格式.正点原子 EMMC 核心板上的 EMMC 默认有 3 个分区, 分区 0 格式未知,因为分区 0 存放的 uboot,并且分区 0 没有格式化,所以文件系统格式未知。分区 1 的格式为 fat,分区 1 用于存放 linux 镜像和设备树。分区 2 的格式为 ext4,用于存放 Linux 的根文件系统(rootfs)。

1
2
3
fstype mmc 1:0
fstype mmc 1:1
fstype mmc 1:2

image

1.7.4 fatload#

fatload <interface> [<dev[:part]> [<addr> [<filename> [bytes [pos]]]]]

将指定的文件读取到 DRAM.
interface 为接口,比如 mmc,dev 是设备号,part 是分区,addr 是保存在 DRAM 中的起始地址,filename 是要读取的文件名字。bytes 表示读取多少字节的数据,如果 bytes 为 0 或者省略的话表示读取整个文件。pos 是要读的文件相对于文件首地址的偏移,如果为 0 或者省略的话表示从文件首地址开始读取.

fatload mmc 1:1 80800000 zImage  //将 EMMC 分区 1 中的 zImage 文件读取到 DRAM 中的0X80800000 地址处

image

225ms 内读取了 6785272 个字节的数据,速度为 28.8MiB/s,速度是非常快的,因为这是从 EMMC 里面读取的,而 EMMC 是 8 位的,速度肯定会很快的。

1.7.5 fatwrite#

将 DRAM 中的数据写入到 MMC 设备。
uboot 默认没有使能 fatwrite 命令,需要修改板子配置头文件,比如 mx6ullevk.h、 mx6ull_alientek_emmc.h 等,需要开启宏:

1
#define CONFIG_FAT_WRITE /* 使能 fatwrite 命令 */

image

fatwrite <interface> <dev[:part]> <addr> <filename> <bytes>

interface 为接口,比如 mmc,dev 是设备号,part 是分区,addr 是要写入的数据在 DRAM中的起始地址,filename 是写入的数据文件名字,bytes 表示要写入多少字节的数据。

比如我们通过nfs or tftp命令下载镜像到DRAM后,通过fatwrite去烧写image:
image
zImage 大小为6785272(0X6788f8)个字节(注意,由于开发板系统在不断的更新中,因此zImage 大小不是固定的,一切以实际大小为准),接下来使用命令fatwrite将其写入到 EMMC 的分区 1 中,文件名字为 zImage:

fatwrite mmc 1:1 80800000 zImage 6788f8

image

完成以后使用fatls命令查看一下 EMMC 分区 1 里面的文件:
image

1.8 文件操作命令(ext文件系统)#

ext文件系统是linux常用的文件系统,一般rootfs就是典型的ext2文件系统。
ext2load、ext2ls、ext4load、ext4ls 和 ext4write。这些命令的含义和使用与 fatload、fatls 和 fatwrite一样,只是 ext2 和 ext4 都是针对 ext 文件系统的。

1
ext4ls mmc 1:2	//emmc设备分区2就是ext4文件系统,存放了rootfs

image

1.9 nandflash操作命令#

输入? nand即可查看NAND 相关命令:
image

1.9.1 nand info#

打印 NAND Flash 信息:
image

1.9.2 nand device#

用于切换 NAND Flash,如果你的板子支持多片 NAND 的话就可以使用此命令来设置当前所使用的 NAND。

1.9.3 nand erase#

nand erase 命令用于擦除 NAND Flash,NAND Flash 的特性(位翻转,只能由1变成0,而不能由0变成1)决定了在向 NAND Flash 写数据之前一定要先对要写入的区域进行擦除.

1
2
3
nand erase[.spread] [clean] off size //从指定地址开始(off)开始,擦除指定大小(size)的区域。
nand erase.part [clean] partition //擦除指定的分区
nand erase.chip [clean] //全篇擦除

1.9.3 nand write#

nand write addr off size

addr 是要写入的数据首地址,off 是 NAND 中的目的地址,size 是要写入的数据大小。

编译出来 NAND版本的 kernel 和 dtb 文件,在烧写之前要先对 NAND 进行分区,也就是规划好 uboot、linux kernel、设备树和根文件系统的存储区域,I.MX6U-ALPHA 开发板出厂系统 NAND 分区如下:

1
2
3
4
5
6
0x000000000000-0x0000003FFFFF : "boot"
0x000000400000-0x00000041FFFF : "env"
0x000000420000-0x00000051FFFF : "logo"
0x000000520000-0x00000061FFFF : "dtb"
0x000000620000-0x000000E1FFFF : "kernel"
0x000000E20000-0x000020000000 : "rootfs"

一共有六个分区,第一个分区存放 uboot,地址范围为 0x0~0x3FFFFF(共 4MB);第二个分区存放 env(环境变量),地址范围为 0x400000~0x420000(共 128KB);第三个分区存放 logo(启动图标),地址范围为 0x420000~0x51FFFF(共 1MB);第四个分区存放 dtb(设备树),地址范围为0x520000~0x61FFFF(共 1MB);第五个分区存放 kernel(也就是 linux kernel),地址范围为0x620000~0xE1FFFF(共 8MB);剩下的所有存储空间全部作为最后一个分区,存放 rootfs(根文件系统)。

1
2
3
tftp 0x87800000 zImage //下载 zImage 到 DRAM 中
nand erase 0x620000 0x800000 //从地址 0x620000 开始擦除 8MB 的空间
nand write 0x87800000 0x620000 0x800000 //将接收到的 zImage 写到 NAND 中

这里我们擦除了 8MB 的空间,因为一般 zImage 就是 6,7MB 左右,8MB 肯定够了,如果不够的话就再多擦除一点就行了。同理烧录dtb:

1
2
3
tftp 0x87800000 imx6ull-14x14-emmc-7-1024x600-c.dtb //下载 dtb 到 DRAM 中
nand erase 0x520000 0x100000 //从地址 0x520000 开始擦除 1MB 的空间
nand write 0x87800000 0x520000 0x100000 //将接收到的 dtb 写到 NAND 中

1.9.4 nand read#

nand read addr off size

从 NAND 中的指定地址读取指定大小的数据到 DRAM.
addr 是目的地址,off 是要读取的 NAND 中的数据源地址,size 是要读取的数据大小。

1
nand read 0x83000000 0x520000 0x19000  //读取设备树(dtb)文件到 0x83000000 地址处

image

1.10 设备树相关#

1.10.1 fdt addr#

设置设备树起始地址:

1
2
nand read 0x83000000 0x520000 0x19000 //比如把nand中dtb分区数据读到dram
fdt addr 83000000 //设置好设备树起始地址

1.10.2 fdt header#

查看设备树头部信息:

fdt header

image

1.10.3 fdt print#

解析出dts内容:
image

1.11 启动相关#

1.11.1 bootz (启动zImage)#

bootz [addr [initrd[:size]] [fdt]]

addr 是 Linux 镜像文件在 DRAM 中的位置,initrd 是 initrd 文件在DRAM 中的地址,如果不使用 initrd 的话使用‘-’代替即可,fdt 就是设备树文件在 DRAM 中的地址.

1
2
3
tftp 80800000 zImage
tftp 83000000 imx6ull-14x14-emmc-7-1024x600-c.dtb
bootz 80800000 - 83000000

image
换一张启动方式:从mmc1也就是emmc启动:

1
2
3
fatload mmc 1:1 80800000 zImage
fatload mmc 1:1 83000000 imx6ull-14x14-emmc-7-1024x600-c.dtb
bootz 80800000 - 83000000

image

1.11.1 bootm (启动uImage)#

bootm [addr [initrd[:size]] [fdt]]

bootm 命令和 bootz 类似,它是启动 uImage 镜像。uImage是U-boot专用的映像文件,它是在zImage之前加上一个长度为0x40的“头”,说明这个映像文件的类型、加载位置、生成时间、大小等信息。
bootm如果不需要启动设备树:

bootm addr

1.11.3 boot#

boot 会读取环境变量 bootcmd 来启动 Linux 系统:
比如设置bootcmd如下,从网络启动linux:

1
2
3
setenv bootcmd 'tftp 80800000 zImage; tftp 83000000 imx6ull-14x14-emmc-7-1024x600-c.dtb; bootz 80800000 - 83000000'
saveenv
boot

image

同理,如果想从emmc启动linux,设置bootcmd如下:

1
2
3
setenv bootcmd 'fatload mmc 1:1 80800000 zImage; fatload mmc 1:1 83000000 imx6ull-14x14-emmc-7-1024x600-c.dtb; bootz 80800000 - 83000000'
savenev
boot

image

1.11.4 go命令#

go addr

跳到指定的地址处执行, addr表示DRAM地址

1
2
tftp 87800000 printf.bin	//一个裸机程序,打印输入的按键,将两个整数相加
go 87800000

image

1.11.5 run#

run 命令用于运行环境变量中定义的命令,比如可以通过run bootcmd来运行 bootcmd 中的启动命令:

1
2
3
4
5
setenv mybootemmc 'fatload mmc 1:1 80800000 zImage; fatload mmc 1:1 83000000 imx6ull-14x14-emmc-7-1024x600-c.dtb;bootz 80800000 - 83000000'
setenv mybootnand 'nand read 80800000 4000000 800000;nand read 83000000 6000000 100000;bootz 80800000 - 83000000'
setenv mybootnet 'tftp 80800000 zImage; tftp 83000000imx6ull-14x14-emmc-7-1024x600-c.dtb;
bootz 80800000 - 83000000'
saveenv

设置了3个环境变量,可以run mybootemmc或者run mybootnand或者run mybootnet来分别从emmc启动,从nand启动,从网络启动。

1.12 内存测试mtest#

mtest [start [end [pattern [iterations]]]]

start 是要测试的 DRAM 开始地址,end 是结束地址,比如我们测试 0X80000000~0X80001000这段内存,输入mtest 80000000 80001000
image
可以看出,测试范围为 0X80000000~0X80001000,已经测试了 486 次,如果要结束测试就按下键盘上的“Ctrl+C”键。

Linux日志管理-dynamic_debug

1 dynamic_debug介绍#

img

这里强烈推荐驱动开发者用这种方式输出log。linux kernel space中有pr_debugdev_dbg来使用dynamic debug。可以看到当用户define DEBUG后,pr_debugdev_dbg就等于printk的KERN_DEBUG级别输出了;否则什么也不打印。

img

1.1 开启dynamic debug#

要使用dynamic_debug需要在kernel的defconfig中开启。

1
2
CONFIG_DEBUG_FS=y
CONFIG_DYNAMIC_DEBUG=y

用menuconfig去配置的话如下图:

img

1.2 dynamic debug使用#

编译好image后,需要挂载debugfs(不挂载的话将不会创建debugfs,那么/sys/kernel/debug/下是空的)。

修改etc/fstab文件,追加下面这段字符:

1
nodev      /sys/kernel/debug debugfs   defaults    0   0

img

可以用cat /sys/kernel/debug/dynamic_debug/control | grep xxx.c来查看自己想要查看的log所在文件有没有包含进去。

img

这里可以看到该文件所有用dev_dbg()打印出的讯息。

那如果不开启CONFIG_DYNAMIC_DEBUG,将不会产生/sys/kernel/debug/dynamic_debug目录, 是不能进行动态打印的。

1.2.1 开启dynamic debug#

1
2
echo "module cvi_mipi_rx +p" > /sys/kernel/debug/dynamic_debug/control
echo "file cvi_vip_cif.c +p" >/sys/kernel/debug/dynamic_debug/control

这两种方式都是开dynamic debug,第一种是对模块开启,第二种只对文件开启。

下面举一个栗子:

img

开启之后,可以看到dev_dbg()打印的log都会输出。

img

反之,关闭dynamic debug

1
2
echo "module cvi_mipi_rx -p" > /sys/kernel/debug/dynamic_debug/control
echo "file cvi_vip_cif.c -p" >/sys/kernel/debug/dynamic_debug/control

除了上面的两种方式还有一种可以只开启某个function:

1
echo "func _init_resource +p" > /sys/kernel/debug/dynamic_debug/control

img

2 dev_err/dev_info/dev_warn#

在Linux驱动代码中,有大量的调试信息,那么推荐使用dev_err/dev_info/dev_warn这一系列函数族。这一系列函数族定义在include/linux/device.h

img

其实这些函数族本质上和下面printk.h中的定义也是完全一致的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define pr_emerg(fmt, ...) \
printk(KERN_EMERG pr_fmt(fmt), ##__VA_ARGS__)
#define pr_alert(fmt, ...) \
printk(KERN_ALERT pr_fmt(fmt), ##__VA_ARGS__)
#define pr_crit(fmt, ...) \
printk(KERN_CRIT pr_fmt(fmt), ##__VA_ARGS__)
#define pr_err(fmt, ...) \
printk(KERN_ERR pr_fmt(fmt), ##__VA_ARGS__)
#define pr_warning(fmt, ...) \
printk(KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__)
#define pr_warn pr_warning
#define pr_notice(fmt, ...) \
printk(KERN_NOTICE pr_fmt(fmt), ##__VA_ARGS__)
#define pr_info(fmt, ...) \
printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)

下图是示例,可以看到err级别以下的log没有打印,那么设置printk的控制台级别可以把对应的log输出到console。

img

如何设置printk console level可以看上一篇Linux日志管理-printk和dmesg

3 可变参数宏#

##__VA_ARGS__表示可变参数宏,可以用来传递多个参数,如:

1
2
3
4
5
6
7
8
9
10
11
#define my_dbg(fmt, ...) \
do { \
printf("[%s] [%d] " fmt, __func__, __LINE__, ##__VA_ARGS__);\
} while(0)
#define my_dbg(fmt...) \
do { \
  printf("[%s] [%d] ", __func__, __LINE__); \
  printf(fmt); \
} while(0)

char *name = "robin"; int age = 18; my_dbg("this is a test. name:%s, age:%d\n", name, age);

结果如下:

img

那和下面这种写法呢本质上是完全一样的。

img

4 模块打印等级控制#

4.1 按照打印等级控制#

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
#ifndef _XXX_DEBUG_H_
#define _XXX_DEBUG_H_

#include <linux/debugfs.h>

extern u32 xxx_log_lv;

#define DBG_ERR 1 /* error conditions */
#define DBG_WARN 2 /* warning conditions */
#define DBG_NOTICE 3 /* normal but significant condition */
#define DBG_INFO 4 /* informational */
#define DBG_DEBUG 5 /* debug-level messages */

#if defined(CONFIG_LOG)
#define TRACE_XXX(level, fmt, ...) \
do { \
if (level <= xxx_log_lv) { \
if (level == DBG_ERR) \
pr_err("%s:%d(): " fmt, __func__, __LINE__, ##__VA_ARGS__); \
else if (level == DBG_WARN) \
pr_warn("%s:%d(): " fmt, __func__, __LINE__, ##__VA_ARGS__); \
else if (level == DBG_NOTICE) \
pr_notice("%s:%d(): " fmt, __func__, __LINE__, ##__VA_ARGS__); \
else if (level == DBG_INFO) \
pr_info("%s:%d(): " fmt, __func__, __LINE__, ##__VA_ARGS__); \
else if (level == DBG_DEBUG) \
printk(KERN_DEBUG "%s:%d(): " fmt, __func__, __LINE__, ##__VA_ARGS__); \
} \
} while (0)
#else
#define TRACE_XXX(level, fmt, ...)
#endif

#endif /* _xxx_DEBUG_H_ */

当级别高于DBG,即可输出高于DBG的所有级别打印。

4.2 精确控制打印等级#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
extern u32 vi_log_lv;
enum vi_msg_pri {
VI_ERR = 0x1,
VI_WARN = 0x2,
VI_NOTICE = 0x4,
VI_INFO = 0x8,
VI_DBG = 0x10,
};
#define vi_pr(level, fmt, arg...) \
do { \
if (vi_log_lv & level) { \
if (level == VI_ERR) \
pr_err("%s:%d(): " fmt, __func__, __LINE__, ## arg); \
else if (level == VI_WARN) \
pr_warn("%s:%d(): " fmt, __func__, __LINE__, ## arg); \
else if (level == VI_NOTICE) \
pr_notice("%s:%d(): " fmt, __func__, __LINE__, ## arg); \
else if (level == VI_INFO) \
pr_info("%s:%d(): " fmt, __func__, __LINE__, ## arg); \
else if (level == VI_DBG) \
pr_debug("%s:%d(): " fmt, __func__, __LINE__, ## arg); \
} \
} while (0)

可以随意开启任意级别打印,比如只开启DBG,只开启WARN。

5 用户态模块打印等级控制#

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
XXX_S32 *log_levels;
XXX_CHAR const *log_name[8] = {
(XXX_CHAR *)"EMG", (XXX_CHAR *)"ALT", (XXX_CHAR *)"CRI", (XXX_CHAR *)"ERR",
(XXX_CHAR *)"WRN", (XXX_CHAR *)"NOT", (XXX_CHAR *)"INF", (XXX_CHAR *)"DBG"
};

XXX_S32 XXX_LOG_SetLevelConf(LOG_LEVEL_CONF_S *pstConf)
{
log_levels[pstConf->enModId] = pstConf->s32Level;
return XXX_SUCCESS;
}

XXX_S32 XXX_LOG_GetLevelConf(LOG_LEVEL_CONF_S *pstConf)
{
pstConf->s32Level = log_levels[pstConf->enModId];
return XXX_SUCCESS;
}

#ifndef __XXX_DEBUG_H__
#define __XXX_DEBUG_H__

#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <syslog.h>
#include <string.h>
#include <XXX_common.h>

#ifdef __cplusplus
#if __cplusplus
extern "C" {
#endif
#endif /* End of #ifdef __cplusplus */

/*
* Debug Config
*/
#define CONFIG_XXX_GDB_NO 1
#define CONFIG_XXX_GDB "n"
#define CONFIG_XXX_LOG_TRACE_SUPPORT 1
#define CONFIG_XXX_LOG_TRACE_ALL 1
#define CONFIG_XXX_LOG_TRACE_LEVEL 4


#define XXX_DBG_EMERG 0 /* system is unusable */
#define XXX_DBG_ALERT 1 /* action must be taken immediately */
#define XXX_DBG_CRIT 2 /* critical conditions */
#define XXX_DBG_ERR 3 /* error conditions */
#define XXX_DBG_WARN 4 /* warning conditions */
#define XXX_DBG_NOTICE 5 /* normal but significant condition */
#define XXX_DBG_INFO 6 /* informational */
#define XXX_DBG_DEBUG 7 /* debug-level messages */

typedef struct _LOG_LEVEL_CONF_S {
MOD_ID_E enModId;
XXX_S32 s32Level;
char cModName[16];
} LOG_LEVEL_CONF_S;

#define XXX_PRINT printf

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-variable"

extern XXX_S32 * log_levels;
extern XXX_CHAR const *log_name[8];

#pragma GCC diagnostic pop

#define _GENERATE_STRING(STRING) (#STRING),
static const char *const MOD_STRING[] = FOREACH_MOD(_GENERATE_STRING);
#define XXX_GET_MOD_NAME(id) (id < XXX_ID_BUTT)? MOD_STRING[id] : "UNDEF"

/* #ifdef XXX_DEBUG */
#ifdef CONFIG_XXX_LOG_TRACE_SUPPORT

#define XXX_ASSERT(expr) \
do { \
if (!(expr)) { \
printf("\nASSERT at:\n" \
" >Function : %s\n" \
" >Line No. : %d\n" \
" >Condition: %s\n", \
__func__, __LINE__, #expr); \
_exit(-1); \
} \
} while (0)

#ifndef FPGA_PORTING

#define XXX_TRACE(level, enModId, fmt, ...) \
do { \
XXX_S32 LogLevel = (log_levels == NULL) ? CONFIG_XXX_LOG_TRACE_LEVEL : log_levels[enModId]; \
if (level <= LogLevel) \
syslog(LOG_LOCAL5|level, "[%s-%s] " fmt, XXX_GET_MOD_NAME(enModId), log_name[level], \
##__VA_ARGS__); \
} while (0)
#else
#define XXX_TRACE(level, enModId, fmt, ...) \
printf(fmt, ##__VA_ARGS__)
#endif
#else
#define XXX_ASSERT(expr)
#define XXX_TRACE(level, enModId, fmt...)
#endif

#define XXX_TRACE_ID(level, id, fmt, ...) \
XXX_TRACE(level, id, "%s:%d:%s(): " fmt, __FILENAME__, __LINE__, __func__, ##__VA_ARGS__)

#define XXX_TRACE_LOG(level, fmt, ...) \
XXX_TRACE(level, XXX_ID_LOG, "%s:%d:%s(): " fmt, __FILENAME__, __LINE__, __func__, ##__VA_ARGS__)

#define XXX_TRACE_SYS(level, fmt, ...) \
XXX_TRACE(level, XXX_ID_SYS, "%s:%d:%s(): " fmt, __FILENAME__, __LINE__, __func__, ##__VA_ARGS__)

#ifdef __cplusplus
#if __cplusplus
}
#endif
#endif /* __cplusplus */

#endif /* __XXX_COMM_SYS_H__ */

Linux日志管理-printk和demsg

1 printk#

printk函数主要做两件事情:第一件就是将信息记录到log中,而第二件事就是调用控制台驱动来将信息输出。printk的相关函数定义在linux/printk.h。

1.1 日志级别#

printk需要设置日志级别,用来控制printk打印的这条信息是否在终端上显示的,当printk设置的日志级别高于控制台级别时,printk要打印的信息才会在控制台打印出来。

内核日志一共有8种级别:

1
2
3
4
5
6
7
8
#define        KERN_EMERG        "<0>"        /* system is unusable                        */
#define KERN_ALERT "<1>" /* action must be taken immediately */
#define KERN_CRIT "<2>" /* critical conditions */
#define KERN_ERR "<3>" /* error conditions */
#define KERN_WARNING "<4>" /* warning conditions */
#define KERN_NOTICE "<5>" /* normal but significant condition */
#define KERN_INFO "<6>" /* informational */
#define KERN_DEBUG "<7>" /* debug-level messages */

1.2 控制台级别#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define MINIMUM_CONSOLE_LOGLEVEL 1 /*可以使用的最小日志级别*/
#define DEFAULT_CONSOLE_LOGLEVEL 7 /*默认的控制台级别*/
#define DEFAULT_MESSAGE_LOGLEVEL 4 /* 默认的日志级别 */

int console_printk[4] = {
DEFAULT_CONSOLE_LOGLEVEL,/*控制台日志级别:优先级高于该值的消息将被打印至控制台*/
DEFAULT_MESSAGE_LOGLEVEL,/*缺省的消息日志级别:将用该优先级来打印没有优先级的消息*/
MINIMUM_CONSOLE_LOGLEVEL,/*最低的控制台日志级别:控制台日志级别可被设置的最小值(最高优先级)*/
DEFAULT_CONSOLE_LOGLEVEL,/*缺省的控制台日志级别:控制台日志级别的缺省值*/
};

#define console_loglevel (console_printk[0])
#define default_message_loglevel (console_printk[1])
#define minimum_console_loglevel (console_printk[2])
#define default_console_loglevel (console_printk[3])

使用命令 cat /proc/sys/kernel/printk 来查看这四个值:

img

结果显示了 current, default, minimumboot-time-default 日志级别。

其中的 4 4 1 7,分别对应与:console_loglevel、default_message_loglevel、minimum_console_loglevel、default_console_loglevel

default_message_loglevel

缺省时的消息日志级别,因此当printk未指定优先级时,将以该默认级别输出,也就是DEFAULT_MESSAGE_LOGLEVEL =4, 对应KERN_WARNING。

也就是说printk("hello world\n");就表示printk(KERN_WARNING "hello world\n");

那如果我们将控制台级别设成<4,如:

echo 3 > /proc/sys/kernel/printk

那么printk("hello world\n");就无法输出到控制台。

名称 字符串 别名函数
KERN_EMERG “0” pr_emerg()
KERN_ALERT “1” pr_alert()
KERN_CRIT “2” pr_crit()
KERN_ERR “3” pr_err()
KERN_WARNING “4” pr_warn()
KERN_NOTICE “5” pr_notice()
KERN_INFO “6” pr_info()
KERN_DEBUG “7” pr_debug() and pr_devel() 若定义了DEBUG
KERN_DEFAULT “” KERN_WARNING
KERN_CONT “c” pr_cont()

1.3 修改控制台级别#

1
2
3
echo "n" > /proc/sys/kernel/printk
#Eg:
echo 8 > /proc/sys/kernel/printk

img

另一种方式,使用 dmesg

1
dmesg -n 8

此时所有的printk日志级别都会被输出到控制台,如下图所示:

1
2
3
4
5
6
7
8
printk ( KERN_EMERG "Hello, EMERG.\n" ) ;
printk ( KERN_ALERT "Hello, ALERT.\n" ) ;
printk ( KERN_CRIT "Hello, CRIT.\n" ) ;
printk ( KERN_ERR "Hello, ERR.\n" ) ;
printk ( KERN_WARNING "Hello, WARNING.\n" ) ;
printk ( KERN_NOTICE "Hello, NOTICE.\n" ) ;
printk ( KERN_INFO "Hello, INFO.\n" ) ;
printk ( KERN_DEBUG "Hello, DEBUG.\n" ) ;

img

再来一种方法,修改bootargs:

Uboot中修改console=ttyS0,115200改为loglevel=7 console=ttyS0,115200,表示设置内核的console_loglevel 值=7,开机cat /proc/sys/kernel/printk,可以看到控制台级别被设置成了7:

img

1.4 printk带时间戳讯息#

make menuconfig开启如下:

img

1.5 printk底层实现#

源码位于kernel\printk\printk.c

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
printk
// linux 4.9: kernel/printk/internal.h
// linux 5.4: kernel/printk/printk_safe.c
vprintk_func
vprintk_default(fmt, args);
vprintk_emit
vprintk_store // 把要打印的信息保存在log_buf中
log_output
preempt_disable();
if (console_trylock_spinning())
console_unlock();
preempt_enable();
console_unlock
for (;;) {
msg = log_from_idx(console_idx);
if (suppress_message_printing(msg->level)) {
/* 如果消息的级别数值大于console_loglevel, 则不打印此信息 */
}
printk_safe_enter_irqsave(flags);
call_console_drivers(ext_text, ext_len, text, len);
printk_safe_exit_irqrestore(flags);
}

img

1.5.1 命令行参数console#

1
2
3
/* IMX6ULL */
[root@100ask:~]# cat /proc/cmdline
console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw

命令行信息可以来自设备树或者环境变量:

1
2
3
4
5
/ {
chosen {
bootargs = "console=ttymxc1,115200";
};
};

修改环境变量:

1
2
3
4
5
6
7
/* 进入IMX6ULL的UBOOT */
=> print mmcargs
mmcargs=setenv bootargs console=${console},${baudrate} root=${mmcroot}
=> print console
console=ttymxc0
=> print baudrate
baudrate=115200

1.5.2 console驱动注册过程#

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
struct console {
char name[16]; // name为"ttyXXX",在cmdline中用"console=ttyXXX0"来匹配
// 输出函数
void (*write)(struct console *, const char *, unsigned);

int (*read)(struct console *, char *, unsigned);
// APP访问/dev/console时通过这个函数来确定是哪个(index)设备
// 举例:
// a. cmdline中"console=ttymxc1"
// b. 则注册对应的console驱动时:console->index = 1
// c. APP访问/dev/console时调用"console->device"来返回这个index
struct tty_driver *(*device)(struct console *co, int *index);
void (*unblank)(void);
// 设置函数, 可设为NULL
int (*setup)(struct console *, char *);
// 匹配函数, 可设为NULL
int (*match)(struct console *, char *name, int idx, char *options);
short flags;
// 哪个设备用作console:
// a. 可以设置为-1, 表示由cmdline确定
// b. 也可以直接指定
short index;
// 常用: CON_PRINTBUFFER
int cflag;
void *data;
struct console *next;
};

1.5.2.1 处理命令行参数#

1
2
__setup("console=", console_setup);
console=ttymxc0,115200 console=ttyVIRT0

处理u-boot通过dts传给内核的cmdline参数,比如bootparam参数。

对于这两个”console=xxx”就会调用console_setup函数两次,构造得到2个数组项:

1
2
3
4
5
6
7
8
9
10
struct console_cmdline {
char name[16]; /* Name of the driver */
int index; /* Minor dev. to use */
char *options; /* Options for the driver */
#ifdef CONFIG_A11Y_BRAILLE_CONSOLE
char *brl_options; /* Options for braille driver */
#endif
};

static struct console_cmdline console_cmdline[MAX_CMDLINECONSOLES];

在cmdline中,最后的”console=xxx”就是”selected_console”(被选中的console,对应/dev/console):

img

1.5.2.2 register_console#

1
2
3
uart_add_one_port
uart_configure_port
register_console(port->cons);

Linux下Uart子系统驱动 - fuzidage - 博客园 (cnblogs.com)

1.5.2.3 /dev/console#

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
tty_open
tty = tty_open_by_driver(device, inode, filp);
driver = tty_lookup_driver(device, filp, &index);
case MKDEV(TTYAUX_MAJOR, 1): {
struct tty_driver *console_driver = console_device(index);

/* 从console_drivers链表头开始寻找
* 如果console->device成功,就返回它对应的tty_driver
* 这就是/dev/console对应的tty_driver
*/
struct tty_driver *console_device(int *index)
{
struct console *c;
struct tty_driver *driver = NULL;

console_lock();
for_each_console(c) {
if (!c->device)
continue;
driver = c->device(c, index);
if (driver)
break;
}
console_unlock();
return driver;
}

Linux下Uart子系统驱动 - fuzidage - 博客园 (cnblogs.com)

1.6 内核信息的早期打印#

当我们注册了uart_driver、并调用uart_add_one_port后,它里面才注册console,在这之后才能使用printk。

如果想更早地使用printk函数,比如在安装UART驱动之前就使用printk,这时就需要自己去注册console。

更早地、单独地注册console,有两种方法:

early_printk:自己实现write函数,不涉及设备树,简单明了

earlycon:通过设备树传入硬件信息,跟内核中驱动程序匹配
earlycon是新的、推荐的方法,在内核已经有驱动的前提下,通过设备树或cmdline指定寄存器地址即可。

1.6.1 early_printk#

arch\arm\kernel\early_printk.c,必须实现这几点:

  • 配置内核,选择:CONFIG_EARLY_PRINTK
  • 内核中实现:printch函数
  • cmdline中添加:earlyprintk

img

1.6.2 earlycon#

2 prink.h介绍#

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
#define pr_emerg(fmt, ...) \
printk(KERN_EMERG pr_fmt(fmt), ##__VA_ARGS__)
#define pr_alert(fmt, ...) \
printk(KERN_ALERT pr_fmt(fmt), ##__VA_ARGS__)
#define pr_crit(fmt, ...) \
printk(KERN_CRIT pr_fmt(fmt), ##__VA_ARGS__)
#define pr_err(fmt, ...) \
printk(KERN_ERR pr_fmt(fmt), ##__VA_ARGS__)
#define pr_warning(fmt, ...) \
printk(KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__)
#define pr_warn pr_warning
#define pr_notice(fmt, ...) \
printk(KERN_NOTICE pr_fmt(fmt), ##__VA_ARGS__)
#define pr_info(fmt, ...) \
printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)
/* If you are writing a driver, please use dev_dbg instead */
#if defined(CONFIG_DYNAMIC_DEBUG)
#include <linux/dynamic_debug.h>

/* dynamic_pr_debug() uses pr_fmt() internally so we don't need it here */
#define pr_debug(fmt, ...) \
dynamic_pr_debug(fmt, ##__VA_ARGS__)
#elif defined(DEBUG)
#define pr_debug(fmt, ...) \
printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#else
#define pr_debug(fmt, ...) \
no_printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#endif

img

pr_emerg到pr_info都是一些基本的kernel打印函数,用来设置内核日志打印级别,可以看到它和下面这种打印本质上并无差异。

pr_debug则有3种输出方式,当开启dynamic_debug后,则走dynamic_pr_debug流程(dynamic_debug见下一节)。当用户开启了DEBUG宏,则走printk流程,否则什么都不打印。

3 dmesg命令#

3.1 /proc/kmsg#

/proc/kmsg 是一个特殊的文件,它提供了内核消息缓冲区的访问,这个缓冲区包含了内核产生的所有消息,包括各种调试和错误信息,如内核的启动打印

dmesg命令就是cat /proc/kmsg

3.2 修改内核日志缓冲区大小#

img

3.3 dmesg#

img

dmesg命令是从kernel ring buffer中读取内核日志信息。因此可以用dmesg命令查看。

1
2
3
4
5
6
7
8
9
10
-C:  直接清除ring buffer
-c: 当完成打印显示后清除环缓冲内的内容。
-s: 缓冲区大小
  定义一个大小为"缓冲区大小"的缓冲区用于查询内核环缓冲区。默认大小为 8196,如果你设置了一个大于默认值的环缓冲区,那你就可以用这个选项定义一个相当的缓冲区来查看完整的环缓冲区内容。

-n:级别

dmesg -k:打印内核信息
dmesg -u:打印用户空间信息

其实用dmesg -n也是可以设置控制台打印级别:

img

有时候在调试kernel驱动时内核panic了or死锁了,那么无法敲命令,如何查看日志呢?重启后日志就没了,那么可以敲如下命令:

1
cat /proc/kmsg > /mnt/data/ker.log & 2>&1

用后台进程将日志导入到文件。

  1. dmesg -f:根据系统打印信息:

可用系统有

kern - kernel messages(内核信息)
user - random user-level messages(随机用户信息)
mail - mail system(邮件系统信息)
daemon - system daemons(系统守护进程信息)
auth - security/authorization messages(认证授权安全信息)
syslog - messages generated internally by syslogd(系统日志信息)
lpr - line printer subsystem(打印机信息)
news - network news subsystem(网络系统信息)

img

  1. dmesg -l:根据level来打印信息:

可用的level信息有

   emerg - system is unusable(系统无法使用)
      alert - action must be taken immediately
       crit - critical conditions(临界条件)
        err - error conditions(错误条件)
       warn - warning conditions(警告条件)
     notice - normal but significant condition
       info - informational
      debug - debug-level messages(debug)

img

Linux日志管理-syslog和rsyslog

1 syslogd简介#

syslogd不仅仅是记录kernel log的服务,还能记录user space中的日志。

syslogd是Linux下的一个记录日志文件服务。新版本叫做rsyslogd。

syslogd有一系列的子服务,例如mail、auth、cron、kern等等,这些子服务提供日志记录的功能,。当程序要记录log时,可以直接调用这些子服务将日志记录到设定的地方。

syslogd是一个守护进程,配置这整个守护进程以及其子服务的地方就是/etc/syslog.conf这个文件。可以从https://www.rsyslog.com/doc/master/获取官方文档。

1.1 日志格式#

如果配置好并运行了 syslogd 或 klogd,一般所有 log的信息也会追加到 /var/log/messages。并且kernel log信息被记录在/val/log/kern.log

img

可以看到基本日志格式包含以下四列:

  1. 事件产生的时间(Jan 1 08:00:09)
  2. 发生事件的服务器的主机名 (cvitek)
  3. 产生事件的服务名或程序名 (kernel or local5)
  4. 事件的具体信息(…cif a0c2000.cif:..)

当开启rsyslogd后,不能透过/proc/kmsg来查看kernel log。

1.2 /etc/syslog.conf服务配置#

  1. /etc/rsyslog.conf 是rsyslog服务的总配置文件
  2. /etc/rsyslog.d 该目录是单独配置的rsyslog配置文件

1.2.1 总配置/etc/syslog.conf#

rsyslog记录哪些日志,到底记录了什么样的日志,是通过这个/etc/rsyslog.conf配置文件来决定的,先分析一下rsyslogd的总配置文件:

img

默认规则会定义在/etc/rsyslog.d/50-default.conf中:

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
43
#################
#### MODULES ####
#################
module(load="imuxsock") # provides support for local system logging ;加载提供对本地系统日志的支持
module(load="imklog") # provides kernel logging support;加载读取内核消息模块
#module(load="immark") # provides --MARK-- message capability
# provides UDP syslog reception (接收使用UDP 协议转发过来的日志,这里#注释掉了表示不启用)
#module(load="imudp")
#input(type="imudp" port="514") (允许514端口接收)
# provides TCP syslog reception (接收使用UDP 协议转发过来的日志)
#module(load="imtcp")
#input(type="imtcp" port="514")(允许514端口接收)
# Enable non-kernel facility klog messages
$KLogPermitNonKernelFacility on
###########################
#### GLOBAL DIRECTIVES ####
###########################
#
# Use traditional timestamp format.
# To enable high precision timestamps, comment out the following line.
#
$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat
# Filter duplicated messages
$RepeatedMsgReduction on
#
# Set the default permissions for all log files.
#
$FileOwner syslog
$FileGroup adm
$FileCreateMode 0640
$DirCreateMode 0755
$Umask 0022
$PrivDropToUser syslog
$PrivDropToGroup syslog
#
# Where to place spool and state files
#
$WorkDirectory /var/spool/rsyslog
(记录所有日志类型的info级别以及大于info级别的信息到/var/log/messages,但是mail邮件信息,authpriv验证方面的信息和cron时间任务相关的信息除外)
#*.info;mail.none;authpriv.none;cron.none /var/log/messages
# Include all config files in /etc/rsyslog.d/
#
$IncludeConfig /etc/rsyslog.d/*.conf (表示加载该目录中的所有配置)

1.2.2 rsyslog规则(/etc/rsyslog.d/*.conf)#

rsyslog规则配置文件一般由以下3部分组成,每一行表示一个项目,格式为:facility.level action,分别表示日志类型,日志等级,日志输出路径。一般系统默认的规则定义在/etc/rsyslog.d/50-default.conf

img

一般所有日志类型都会被追加在/val/log/messages。如:

1
*.info;mail.none;authpriv.none;cron.none    /var/log/messages  

1.2.2.1 facility-日志类型#

  • kern: 内核信息
  • user: 用户进程相关信息
  • mail: 电子邮件相关信息
  • Local0- local7: 为本地使用预留的服务
  • daemon: 后台进程相关信息
  • syslog: 系统日志信息

1.2.2.2 level-按严重程度由低到高排序#

  • none: 没有重要级
  • debug: 调试信息
  • info: 打印的信息
  • notice: 具有重要信息的普通条件
  • warning: 警告信息
  • err: 错误信息
  • crit: 阻止某些工具或子系统功能实现的错误条件
  • alert: 需要立即被修改的条件
  • emerg: 该系统不可用

1.2.2.3 action-表示log保存的位置#

那下面我也抄过来一份比较全面的规则定义示例供大家参考:

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
# 记录mail日志等级为error及以上日志
mail.err /var/log/mail_err.log
# 记录mail所有等级为warn级别的日志(仅记录warn级别)
mail.=warn /var/log/mail_err.log
# 记录kern所有日志
kern.* /var/log/kern.log
# 将mail的所有信息,除了info以外,其他的都写入/var/adm/mail
mail.*;mail.!=info /var/adm/mail
# 将日志等级为crit或更高的内核消息定向到远程主机finlandia
# 如果主机崩溃,磁盘出现不可修复的错误,可能无法读取存储的消息。如果有日志在远程主机上,可以尝试找出崩溃的原因。
kern.crit @finlandia
# 记录所有类型的warning等级及以上日志
*.warning /var/log/syslog_warn.log
# 记录mail的warning日志和kern的error日志,其他所有的info日志
*.info;mail.warning;kern.error /var/log/messages
# 记录kernel的info到warning日志
kern.info;kern.!err /var/adm/kernel-info
# 将mail和news的info级别日志写入/var/adminfo
mail,news.=info /var/adm/info
# 将所有系统中所有类型的info日志和notice日志存入/var/log/massages,mail的所有日志除外。
*.=info;*.=notice;\
mail.none /var/log/messages
# 紧急消息(emerg级别)将使用wall显示给当前所有登录的用户,这里用等号表示只对emerg日志级别有效
*.=emerg *
# 该规则将所有alert以及更高级别的消息定向到操作员的终端,即登录的用户“root”和“joey”的终端。
*.alert root,joey

1.3 程序如何配置syslog子服务(自定义rsyslog规则)#

问题:进程如何发送消息给rsyslog守护进程,rsyslog守护进程是如何对各种日志区分开来的?

/usr/sbin/sshd、/usr/bin/login、/usr/bin/su这些进程,它们是调用一个叫syslog的系统调用, syslog系统调用是一个用于向rsyslog守护进程发送消息的的系统函数。
/usr/sbin/sshd,/usr/bin/login、/usr/bin/su这些进程专门执行登录验证时,它们在调用syslog系统函数会一般会传入LOG_AUTH这个常量。

/usr/bin/crond和/usr/bin/at这些在调用syslog系统调用会传入LOG_CRON这个常量(具体请看**syslog()**函数),日志归类规则如下:

img

所以如果用LOG_AUTH的syslog()函数调用,那么会归类到了/val/log/secure

如果用LOG_CRON的syslog()调用则归类到了/val/log/cron。而kernel等其他log被记录在了/val/log/messages中。

那么我们可以自定义规则如下:

img

1
2
3
4
5
6
syslogfacility-text:表示facility日志类型
syslogseverity-text:表示level日志级别

这里对aisdk.conf用local7日志类型产生的大于warn日志级别的log都将记录到/val/log/aisdk,
对kern.conf用kern类型产生的所有级别日志都记录到/val/log/kern,
Local5类型的日志记录到/val/log/middleware, 并且当日志级别等于或高于4(warn)时也会追加到console.

注意busybox要开启使能syslog.conf解析:/etc/syslog.conf解析,CONFIG_FEATURE_SYSLOGD_CFG=y

img

2 syslog()函数#

img

用户空间也可以用syslog()函数来记录自己的进程的日志,所以用户进程可以自定义日志规则。
调用openlog是可选择的。如果不调用openlog,则在第一次调用syslog时,会自动调用openlog。
syslog的相关函数和宏定义一般在toolchain中都会有定义:

img

1
2
3
4
#include <syslog.h>
void openlog (char*ident,int option ,int facility);
void syslog(int priority,char*format,……)
void closelog();

2.1 openlog函数#

第1个参数为ident,该参数常用来表示信息的来源。ident信息会被固定地添加在每行日志的前面:
第2个参数 option控制标志:

option控制标志 作用
LOG_CONS 如果将信息发送给syslogd守护进程时发生错误,直接将相关信息输出到终端
LOG_PID 每条日志信息中都包括进程号

第3个参数为facility:

facility参数 syslog.conf中对应的facility取值
LOG_KERN kern
LOG_USER user
LOG_MAIL mail
LOG_DAEMON daemon
LOG_AUTH auth
LOG_SYSLOG syslog
LOG_LPR lpr
LOG_NEWS news
LOG_UUCP uucp
LOG_CRON cron
LOG_AUTHPRIV authpriv
LOG_FTP ftp
LOG_LOCAL0~LOG_LOCAL7 local0~local7

img

2.2 syslog函数#

第一个参数priority表示日志级别:

priority参数 syslog.conf中对应的level取值
LOG_EMERG emerg
LOG_ALERT alert
LOG_CRIT crit
LOG_ERR err
LOG_WARNING warning
LOG_NOTICE notice
LOG_INFO info
LOG_DEBUG debug

img

下面是具体的例子:

img

这里printf("%m")等价于printf("%s",strerror(errno));它表示把errno用string形式打印出来。

由于我这里facility为user时,是记录在/val/log/syslog中的:

img

因此打印log如下:

img

2.3 重定向log#

我们也可以把log定向到自己想要的地方。

2.3.1 方法1-修改rsyslog.conf#

img

facility=user时的所有level级别的log重定向到/val/log/user.log, 重启rsyslog服务:

img

此时log将被写入到新配置的位置/val/log/user.log, 当然/val/log/syslog也会保留一份.(因为也符合/val/log/syslog这条规则)

img

2.3.2 方法2-修改code中的facility#

img

那这里的facility被设置成了local0, 那也会记录在/val/log/syslog:

img

2.4 设置log等级#

  1. 这里新增一个app.conf,然后自定义log路径:

img

当然还可以类似于这样子写, syslogfacility-text和syslogseverity-text是rsyslog自带的系统变量。

img

  1. 重启rsyslog服务

    img

img

这里切成4个文件,每个文件记录1024k。

  1. 运行程序,查看log如下:

    img

img

  1. 那现在修改log等级为warn, 表示只有大于等于该等级的log才会记录。

    img

  2. 再次重启rsyslog服务,运行程序,可以看到”log debug”不再打印:

    img

2.5 重定向log到console#

img

再次重启rsyslog服务,运行程序,那么可以看到err级别的log打印在了console上,但是低于err级别还是会记录在/val/log/app。

img

3 dup函数介绍#

img

用来将标准输出重定向到文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int dup_fd;
static int dup_fd_bak = 1000;

static int dup_fd(void) {
dup_fd = open( "./printf_dup_log.txt ", O_CREAT | O_RDWR | O_TRUNC);
dup2(STDOUT_FILENO, dup_fd_bak);/*backup stdout*/
dup2(dup_fd, STDOUT_FILENO);
return 0;
}
static int rst_fd(void) {
dup2(dup_fd_bak, fileno(stdout));/*recover stdout*/
close(dup_fd);
return 0;
}

tslib移植使用

1 获取 tslib 源码#

https://github.com/libts/tslib
git clone https://github.com/libts/tslib.git

2 修改 tslib 源码所属用户#

sudo chown book:book tslib-1.21 -R
这一步一定要做!否则在稍后的编译中会遇到各种问题。

3 ubuntu 工具安装#

1
2
3
sudo apt-get install autoconf
sudo apt-get install automake
sudo apt-get install libtool

4 编译 tslib#

1
2
3
4
5
cd tslib-1.21/ //进入 tslib 源码目录
./autogen.sh
./configure --host=arm-linux-gnueabihf --prefix=/home/zuozhongkai/linux/IMX6ULL/tool/tslib
make //编译
make install //安装

“--host”参数指定编译器,“--prefix”参数指定编译完成以后的 tslib 文件安装到哪里.
完成以后 tslib 目录下的内容如下:
image
把所有文件拷贝到开发板的根文件系统中:
sudo cp * -rf /home/zuozhongkai/linux/nfs/rootfs

5 配置 tslib#

打开/etc/ts.conf 文件,找到下面这一行:
module_raw input
打开/etc/profile 文件,在里面加入如下内容:

1
2
3
4
5
6
export TSLIB_TSDEVICE=/dev/input/event1
export TSLIB_CALIBFILE=/etc/pointercal
export TSLIB_CONFFILE=/etc/ts.conf
export TSLIB_PLUGINDIR=/lib/ts
export TSLIB_CONSOLEDEVICE=none
export TSLIB_FBDEVICE=/dev/fb0

TSLIB_TSDEVICE 表示触摸设备文件,这里设置为/dev/input/event1,这个要根据具体情况设置,如果你的触摸设备文件为 event2 那么就应该设置为/dev/input/event2,以此类推。
TSLIB_CALIBFILE 表示校准文件,如果进行屏幕校准的话校准结果就保存在这个文件中,这里设置校准文件为/etc/pointercal,此文件可以不存在,校准的时候会自动生成。
TSLIB_CONFFILE 表示触摸配置文件,文件为/etc/ts.conf,此文件在移植 tslib 的时候会生成。
TSLIB_PLUGINDIR 表示 tslib 插件目录位置,目录为/lib/ts。
TSLIB_CONSOLEDEVICE 表示控制台设置,这里不设置,因此为 none。
TSLIB_FBDEVICE 表示 FB 设备,也就是屏幕,根据实际情况配置,我的屏幕文件为/dev/fb0,因此这里设置为/dev/fb0

6 tslib 测试#

一般电容屏可以不用校准,如果是电阻屏就要先进行校准。
输入ts_calibrate
校准完成以后如果不满意,或者不小心对电容屏做了校准,那么直接删除掉/etc/pointercal文件即可。

6.1 ts_test_mt#

此命令会打开一个触摸测试界面:
image
Drag:拖拽按钮,默认就是此功能,大家可以看到屏幕中间有一个十字光标,我们可以通过触摸屏幕来拖拽此光标。一个触摸点一个十字光标,对于 5 点电容触摸屏,如果 5 个手指都放到屏幕上,那么就有 5 个光标,一个手指一个。
Draw:绘制按钮,按下此按钮我们就可以在屏幕上进行简单的绘制,可以通过此功能检测多点触摸工作是否正常。
Quit:退出按钮,退出 ts_test_mt 测试软件。
点击“Draw”按钮,使用绘制功能,5 个手指一起划过屏幕,如果多点电容屏工作正常的话就会在屏幕上留下 5 条线:
image
可以看到有5条横线。

linux基本命令集合

1 前言-参考资料#

正点原子:http://www.openedv.com/docs/boards/arm-linux/zdyz-i.mx6ull.html

2 linux 命令#

1.1 磁盘相关#

1.1.1 fdisk#

1.1.1.1 查看分区#

fdisk -l显示磁盘分区使用情况

image

1.1.1.2 删除分区#

fdisk /dev/sdb1 用来对sdb1进行分区.

image

输入m表示获取帮助,默认有分区sdb1, 然后输入d删除分区1,p打印出分区表,i表示打印出详细分区信息,n表示新增分区信息,w表示保存,q表示退出。

image

来看dev/sd*信息,发现已经没有了sdb1.

image

1.1.1.3 创建分区#

再来看如何建立分区1:
先建立一个1GB的分区,1GB= 1024 * 1024 * 1024=1073741824 B = 2097152个sector,一个sector有512 byte,再加上2048 个sector,那么等于2099200个sector。

image

再来何建立分区2:

image

这里First sector使用默认值2101248,Last sector使用4198400(1G是2097152, 2101248 + 2097152 = 4198400),分区2也是1GB
再来何建立分区3:

image

First sector和Last sector使用默认,那么最终分区3有26.8GiB。

最后输入w保存退出,来看下分区:

image

image

image

1.1.2 磁盘格式化命令-mkfs#

mkfs命令用来对磁盘分区格式化,将格式化好的sd卡放入windows系统查看,可以看到3个盘:

image

image

1.1.2.1 mount#

image

1.1.3 du#

image

1.1.4 df#

image

1.2 文件字符操作命令#

1.2.1 xargs#

find -name *.sh |xargs grep -rn "build_all"

image

1.2.1 find#

按时间搜索:
-atime 访问时间 (单位是天,分钟单位则是-amin,以下类似)
-mtime 修改时间 (内容被修改)
-ctime 变化时间 (元数据或权限变化)

最近7天被访问过的所有文件:
find . -atime 7 -type f -print
最近7天被修改的文件:
find . -maxdepth 2 -mtime 7 -type f

通配符#

一版find命令还会伴随通配符使用:

1
2
3
4
*:匹配任意多个字符
?:匹配任意一个字符
[...]:匹配中括号内出现的任意一个字符
[!...]:不匹配中括号内出现的任意一个字符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ find . -name *.txt
$ find -name *.[ch]
$ find -name *fsi*.? #查找包含fsi字符,同时又有.c或者.h等单个字符后缀的文件
./i2c/busses/i2c-fsi.c
./spi/spi-fsi.c
./input/joystick/fsia6b.c
./fsi/fsi-sbefifo.c
./fsi/fsi-master-ast-cf.c
./fsi/fsi-master-aspeed.c
./fsi/fsi-master.h
./fsi/fsi-occ.c
./fsi/fsi-master-hub.c
./fsi/fsi-master-gpio.c
./fsi/fsi-core.c
./fsi/fsi-scom.c
./fsi/cf-fsi-fw.h

伴随执行任务#

1
2
# 找到后执行删除
find . -name "*.txt" -exec rm {} \;

1.2.2 grep#

find /path/to/directory -type f -name "*.txt" | grep "keyword"

-w 全词匹配。
-v 反向搜索
-i 不区分大小写

1
2
3
4
5
6
7
$匹配以字符串结尾的行
^ 匹配以字符串开头的行

找出空行 grep "^$" test.txt -n
找出unix开头的行grep "^unix" geekfile.txt
找出.结尾的行 grep "\.$" test.txt -n -o
找出os.结尾的行,grep "os.$" geekfile.txt

[abc]中括号

匹配abc字符中的任意一个:

image

匹配a-z:

image

下面一个脚本用grep -v排除掉不需要的行,也就是删除包含指定字符的行从一个文件。

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
# 定义要删除的特定字符
pattern="特定字符"
# 定义要处理的文件名
filename="文件名"
# 使用grep命令找到含有特定字符的行,并将结果输出到一个临时文件中
grep -v "$pattern" "$filename" > temp.txt
# 将临时文件的内容复制回原始文件
cat temp.txt > "$filename"
# 删除临时文件
rm temp.txt

uniq 删除重复行#

tr字符串替换#

1.2 awk数据流处理#

常用的就是提取文件中的列字段,比如提取file中的第二个和第三个字段。
awk '{print $2, $3}' file

1.3 网络命令#

1
2
3
4
ifconfig eth0 up/down
udhcpc -i eth0 //通过路由器分配 IP 地址
ifconfig eth0 192.168.1.251 netmask 255.255.255.0 //设置 IP 地址和子网掩码
route add default gw 192.168.1.1 //添加默认网关

3 shell脚本命令#

3.1 解释器#

  1. sh解释器

  2. bash解释器
    脚本开头用#!用来申明用什么解释器,如:

    image

3.2 段代码注释#

1
2
3
<<EOF
...
EOF

image

3.3 read命令#

image

3.4 test命令#

测试文件,数值,权限,字符串等参数。

image

image

中括号也能表示测试,里面只能用==或!=。

image

3.4.1 文件测试#

image-20240921213503247

3.4.2 比较测试#

image-20240921213551593

3.4.3 多重条件测试#

image

image

可以看到第一和第三条test都成立:

image

3.5 命令行参数#

1
2
3
4
5
6
7
8
9
$0, $1, $2, $3...
$0表示脚本文件名
$1表示第一个参数
$n表示第n个参数
$#表示一共有多少个命令行参数
$@表示所有的命令行参数集合,$0 $1 $2 ... $n
$*表示等价$@
$?表示上一条命令是否返回成功,成功为0,错误非0
$$表示当前脚本的进程号

image

3.6 条件语句#

image

3.7 case语句#

image

3.8 函数#

image

image

image

3.9 循环语句#

image

image

image

3.9 数组#

用括号来表示数组,数组元素用“空格”符号分割开:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
array_name=(value0 value1 value2 value3)
array_name=(
value0
value1
value2
value3
)
#还可以每个元素进行定义,可以不使用连续的下标,而且下标的范围没有限制。
array_name[0]=value0
array_name[1]=value1
array_name[2]=value2

#读取数组
val=${array_name[2]}
all=${array_name[*]}
all2=${array_name[@]}

# 取得数组元素的个数
length=${#array_name[@]}
# 或者
length=${#array_name[*]}
# 取得数组单个元素的长度
lengthn=${#array_name[n]}

3.10 typeset或者declare#

sh脚本默认所有变量都是字符串,比如val=1,也表示val是一个字符串“1”。那么需要如何声明一个变量类型,用typeset或者declare。

1
typeset -i data=1

3.11 unset#

清除变量值.

3.12 readonly#

只读变量.

3.13 sed#

3.13.1 目录递归替换字符#

find /path/to/dir -type f -name "*.[c-h]" -exec sed -i 's/oldstring/newstring/g' {} +

1
2
3
4
5
6
7
8
find /path/to/dir:找到指定目录/path/to/dir下的所有文件。
-type f:仅查找文件。
-name "*.txt":限制文件名以.txt结尾。
-exec:对找到的每个文件执行后面的命令。
sed -i:使用sed进行替换,-i表示直接修改文件内容。
's/oldstring/newstring/g':sed的替换表达式,g表示全局替换。
{}:表示find找到的文件名。
+:结束-exec命令,批量处理匹配到的文件。

3.13.2 去掉一行中多余的空格#

sed -i 's/[[:space:]]\+/ /g' array.sh
s/[[:space:]]\+/ /g 是替换命令,它会将一个或多个空白字符(包括空格、制表符等)替换为单个空格.

3.14 shell脚本如何查看将要执行的命令#

1
2
3
#!/bin/sh
set -x
echo "Hello, World!"

3.15 字符串中替换一个字符#

1
2
3
string="123123"
echo ${string/3/0} #用0替换第一个遇见的3
echo ${string//3/0} #用0替换串中所有3

3.16 算数运算符#

image

3.17 重定向#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$who
weiyong.luo pts/1130 2024-09-21 12:12 (xxx.xxx.xxx.xxx)
shawn.wu pts/1036 2024-09-11 17:48 (xxx.xxx.xxx.xxx)
weiyong.luo pts/1133 2024-09-21 12:12
robin.lee pts/1134 2024-09-21 15:23
chengzhi.lian pts/1135 2024-09-21 09:02
robin.lee pts/1166 2024-09-20 18:15

who >users ##输出重定向到users文件

#再看输入重定向
#从文件获取输入信息给命令。
$ wc -l < users #统计users文件的行数
12

3.17.1 保存log到文本#

make all > build_osdrv.log 2>&1

markdown语法教程

1 多级标题目录结构#

# 一级标题
## 二级标题
### 三级标题
#### 四级标题
##### 五级标题
###### 六级标题

效果如下:

一级标题#

二级标题#

三级标题#

四级标题#

五级标题#
六级标题#

2 段落#

2.1 换行#

Markdown段落的换行是使用两个以上空格加上回车,当然也可以在段落后面使用一个空行来表示重新开始一个段落。

3 字体#

*斜体*
**粗体**
***加粗斜体***

<font face="黑体">我是黑体字</font>
<font face="微软雅黑">我是微软雅黑</font>
<font face="STCAIYUN">我是华文彩云</font>

效果:

斜体
粗体
加粗斜体

我是黑体字
我是微软雅黑
我是华文彩云

4 字体颜色,大小#

<font color=#0099ff size=12 face="黑体">黑体</font>
<font color=gray size=5>gray</font>
<font color=#00ffff size=3>null</font>

效果:

黑体
gray
null

5 字体背景颜色#

<table><td bgcolor=pink> 
背景色是pink </table>

<table><td bgcolor=LightGoldenRodYellow> 
LightGoldenRodYellow </table>

<table><td bgcolor=PeachPuff> 
PeachPuff </table>

<table><td bgcolor=PapayaWhip> 
PapayaWhip </table>

<table><td bgcolor=PaleGreen> 
PaleGreen </table>

<table><td bgcolor=PaleGoldenRod> 
PaleGoldenRod </table>

<table><td bgcolor=MistyRose> 
MistyRose </table>

<table><td bgcolor=Linen> 
Linen </table>

<table><td bgcolor=LightPink> 
LightPink </table>

<table><td bgcolor=BurlyWood> 
BurlyWood </table>

效果:

背景色是pink
LightGoldenRodYellow
PeachPuff
PapayaWhip
PaleGreen
PaleGoldenRod
MistyRose
Linen
LightPink
BurlyWood

5 下划线#

写法:
<u>下划线</u>

效果如下:
下划线

6 列表项#

无序列表使用星号(*)、加号(+)或是减号(-)作为列表标记:

* 第一项
* 第二项
* 第三项

显示效果:

  • 第一项
  • 第二项
  • 第三项

有序列表使用数字并加上 . 号来表示:
1. 第一项
1. 小一项
2. 小二项
2. 第二项
3. 第三项
显示效果:

  1. 第一项
    1. 小一项
    2. 小二项
  2. 第二项
  3. 第三项

7 代码块与语法高亮#

7.1 行内代码#

用反引号 `` 来标记或插入代码区段

效果:
int main(void)

7.2 块代码#

用tab键或者用```

效果:

1
2
3
4
5
#include <stdio.h>
int main(vpod) {
puts("hello world\n");
return 0;
}

8 代码折叠#

写法:

1
2
3
4
5
6
7
8
9
10
11
<details>
<summary>点击展开代码</summary>
<pre><code>
#include<stdio.h>
int main(int argc, char **argv)
{
printf("hello world\n");
return 0;
}
</code></pre>
</details>

效果如下:

点击展开代码

    #include <stdio.h>
    int main(int argc, char **argv)
    {
        printf("hello world\n");
        return 0;
    }

9 插入链接#

1
2
用:
[fuzidage的博客](https://www.cnblogs.com/fuzidage/)

效果:

fuzidage的博客

9.1 自动链接#

只要是用<>包起来, Markdown 就会自动把它转成链接:

效果:
https://github.com/fuzidage

10 添加图片#

写法:![](https://img2018.cnblogs.com/blog/1876680/201912/1876680-20191214155002138-1798053565.png)
当然如果是本地图片写法:
![](./markdown语法教程/1.png)

引用网络效果:

引用本地图片效果:
image-20240526174126727

11 表格#

写法:

1
2
3
4
| 左对齐 | 右对齐 | 居中对齐 |
| :-----| ----: | :----:|
| 单元格 | 单元格 | 单元格 |
| 单元格 | 单元格 | 单元格 |

效果:

左对齐 右对齐 居中对齐
单元格 单元格 单元格
单元格 单元格 单元格

12 转义字符#

显示结果 描述 输入 实体编号
空格 &nbsp; &#160;
< 小于号 &lt; &#60;
> 大于号 &gt; &#62;
& 和号 &amp; &#38;
引号 &quot; &#34;
撇号 &apos; (IE不支持) &#39;
&cent; &#162;
£ &pound; &#163;
¥ 日圆 &yen; &#165;
§ &sect; &#167;
© 版权 &copy; &#169;
® 注册商标 &reg; &#174;
× 乘号 &times; &#215;
÷ 除号 &divide; &#247;

锚点#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
## 目录
[跳转到第一节](#a)
[跳转到第二节](#b)

<a name="a"></a>
### 第一节
这是第一节的内容。
...
...
...
...
...
...
...
...
...
...
...
<a name="b"></a>
### 第二节
这是第二节的内容。

目录#

跳转到第一节
跳转到第二节

第一节#

这是第一节的内容。











第二节#

这是第二节的内容。

I2C-SMBus协议和I2C Tool

1 SMBus协议#

1.1 SMBus 是 I2C 协议的一个子集#

SMBus: System Management Bus,系统管理总线。SMBus 最初的目的是为智能电池、充电电池、其他微控制器之间的通信链路而定义的。SMBus 也被用来连接各种设备,包括电源相关设备,系统传感器,EEPROM 通讯设备等等。SMBus 为系统和电源管理这样的任务提供了一条控制总线,使用 SMBus 的系 统,设备之间发送和接收消息都是通过 SMBus,而不是使用单独的控制线,这样可以节省设备的管脚数。

SMBus 是基于 I2C 协议的,SMBus 要求更严格,SMBus 是 I2C 协议的子集。

img

1.2 和I2C协议特性对比#

SMBus 有哪些更严格的要求?跟一般的 I2C 协议有哪些差别?

  1. VDD 的极限值不一样
      1.1 I2C 协议:范围很广,甚至讨论了高达 12V 的情况
                    1.2 SMBus:1.8V~5V
  2. 速率更高
      I2C 协议:时钟频率最小值无限制,Clock Stretching 时长也没有限制
                    SMBus:时钟频率最小值是 10KHz,Clock Stretching 的最大时间值也有限制
  3. 地址回应(Address Acknowledge):一个 I2C 设备接收到它的设备地址后, 是否必须发出回应信号?
      I2C 协议:没有强制要求必须发出回应信号
                    SMBus:强制要求必须发出回应信号,这样对方才知道该设备的状态: busy,failed,或是被移除了
  4. SMBus 协议明确了数据的传输格式
      I2C 协议:它只定义了怎么传输数据,但是并没有定义数据的格式,这完全由设备来定义
                    SMBus:定义了几种数据格式
  5. REPEATED START Condition(重复发出 S 信号)
      比如读 EEPROM 时,涉及 2 个操作:
                  ​ 把存储地址发给设备
                  ​ 读数据
                  在写、读之间,可以不发出 P 信号,而是直接发出 S 信号:这个 S 信号就是
                  REPEATED START

img

1.3 SMBus数据格式#

下面文档中的 Functionality flag 是 Linux 的某个 I2C 控制器驱动所支持的功能。比如 Functionality flag: I2C_FUNC_SMBUS_QUICK,表示需要I2C 控制器支持 SMBus Quick Command。

1
2
3
4
5
6
7
8
9
10
11
symbols(符号):
S (1 bit) : Start bit(开始位)
 Sr (1 bit) : 重复的开始位
 P (1 bit) : Stop bit(停止位)
 R/W# (1 bit) : Read/Write bit. Rd equals 1, Wr equals 0.(读写位)
 A, N (1 bit) : Accept and reverse accept bit.(回应位)
 Address(7 bits): I2C 7 bit address. Note that this can be expanded as usual to get a 10 bit I2C address.(地址位,7 位地址)
 Command Code (8 bits): Command byte, a data byte which often selects a register on the device.(命令字节,一般用来选择芯片内部的寄存器)
Data Byte (8 bits): A plain data byte. Sometimes, I write DataLow, DataHigh for 16 bit data.(数据字节,8 位;如果是 16 位数据的话,用 2 个字节来表示:DataLow、DataHigh)
 Count (8 bits): A data byte containing the length of a block operation.(在 block 操作总,表示数据长度)
 [..]: Data sent by I2C device, as opposed to data sent by the host adapter.(中括号表示 I2C 设备发送的数据,没有中括号表示 host adapter 发送的数据)

1.3.1 SMBus Quick Command#

只是用来发送一位数据:R/W#本意是用来表示读或写,但是在 SMBus 里可以用来表示其他含义。比如某些开关设备,可以根据这一位来决定是打开还是关闭.

img

Functionality flag: I2C_FUNC_SMBUS_QUICK

1.3.2 SMBus Receive Byte#

img

I2C-tools 中的函数:i2c_smbus_read_byte()。读取一个字节,Host adapter 接收到一个字节后不需要发出回应信号(上图中 N 表示不回应)。

Functionality flag: I2C_FUNC_SMBUS_WRITE_BYTE

1.3.3 SMBus Send Byte#

img

I2C-tools 中的函数:i2c_smbus_write_byte()。发送一个字节。

Functionality flag: I2C_FUNC_SMBUS_WRITE_BYTE

1.3.4 SMBus Read Byte#

img

I2C-tools 中的函数:i2c_smbus_read_byte_data()。先发出 Command Code(它一般表示芯片内部的寄存器地址),再读取一个字节的数据。上面介绍的 SMBus Receive Byte 是不发送 Comand,直接读取数据。

Functionality flag: I2C_FUNC_SMBUS_READ_BYTE_DATA

1.3.5 SMBus Read Word#

img

I2C-tools 中的函数:i2c_smbus_read_word_data()。先发出 Command Code(它一般表示芯片内部的寄存器地址),再读取 2 个字节的数据。

Functionality flag: I2C_FUNC_SMBUS_READ_WORD_DATA

1.3.6 SMBus Write Byte#

img

I2C-tools 中的函数:i2c_smbus_write_byte_data()。先发出 Command Code(它一般表示芯片内部的寄存器地址),再发出 1 个字节的数据。

Functionality flag: I2C_FUNC_SMBUS_WRITE_BYTE_DATA

1.3.7 SMBus Write Word#

img

I2C-tools 中的函数:i2c_smbus_write_word_data()。先发出 Command Code(它一般表示芯片内部的寄存器地址),再发出 1 个字节的数据。

Functionality flag: I2C_FUNC_SMBUS_WRITE_WORD_DATA

1.3.8 SMBus Block Read#

img

I2C-tools 中的函数:i2c_smbus_read_block_data()。先发出 Command Code(它一般表示芯片内部的寄存器地址),再发起度操作:

  1. 先读到一个字节(Block Count),表示后续要读的字节数
  1. 然后读取全部数据

Functionality flag: I2C_FUNC_SMBUS_READ_BLOCK_DATA

1.3.8 SMBus Block Write#

img

I2C-tools 中的函数:i2c_smbus_write_block_data()。先发出 Command Code(它一般表示芯片内部的寄存器地址),再发出 1 个字节的 Byte Conut(表示后续要发出的数据字节数),最后发出全部数据。

Functionality flag: I2C_FUNC_SMBUS_WRITE_BLOCK_DATA

1.3.9 SMBus Block Write - Block Read Process Call#

img

先写一块数据,再读一块数据。

Functionality flag: I2C_FUNC_SMBUS_BLOCK_PROC_CALL

1.3.10 Packet Error Checking (PEC)#

PEC 是一种错误校验码,如果使用 PEC,那么在 P 信号之前,数据发送方要发送一个字节的 PEC 码(它是 CRC-8 码)。以 SMBus Send Byte 为例,下图中,一个未使用 PEC,另一个使用 PEC:(一般很少使用)

img

1.4 I2C数据格式#

1.4.1 I2C Block Read#

在一般的 I2C 协议中,也可以连续读出多个字节。它跟 SMBus Block Read 的差别在于设备发出的第 1 个数据不是长度 N,如下图所示:

img

I2C-tools 中的函数:`i2c_smbus_read_i2c_block_data()。先发出 Command Code(它一般表示芯片内部的寄存器地址),直接读出全部数据。

Functionality flag: I2C_FUNC_SMBUS_READ_I2C_BLOCK

1.4.2 I2C Block Write#

在一般的 I2C 协议中,也可以连续发出多个字节。它跟 SMBus Block Write 的差别在于发出的第 1 个数据不是长度 N,如下图所示:

img

I2C-tools 中的函数:i2c_smbus_write_i2c_block_data()。先发出 Command Code(它一般表示芯片内部的寄存器地址),再发出 1 个字节的 data,最后发出全部数据。

Functionality flag: I2C_FUNC_SMBUS_WRITE_I2C_BLOCK

2 I2C Tools#

2.1 I2C控制器框架#

APP 访问硬件肯定是需要驱动程序的,对于 I2C 设备,linux内核提供了默认的驱动程序 drivers/i2c/i2c-dev.c,通过它可以直接使用下面的 I2C 控制器驱动程序来 访问 I2C 设备。

img

2.2 i2c数据结构#

2.2.1 i2c_adapter#

img

i2c_adapter 表示一个 I2C BUS,或称为 I2C Controller,里面有 2 个重要的成员:

  1. nr:第几个 I2C BUS(I2C Controller)
  2. i2c_algorithm,里面有该 I2C BUS 的传输函数,用来收发 I2C 数据

怎么表示 I2C Controller , 一个芯片里可能有多个 I2C Controller,比如第 0 个、第 1 个、……

2.2.3 i2c_algorithm#

img

2.3.4 I2c_device/I2c_client#

一个 I2C Device,一定有设备地址, 那它连接在哪个 I2C Controller 上,即对应的 i2c_adapter 是什么。

使用 i2c_client 来表示一个 I2C Device。

img

2.3.5 i2c_msg#

在上面的i2c_algorithm结构体中可以看到要传输的数据被称为:i2c_msg

img

flags: 用来表示传输方向:bit 0 等于 I2C_M_RD 表示 读,bit 0 等于 0 表示写。一个 i2c_msg 要么是读,要么是写
举例:设备地址为 0x50 的 EEPROM,要读取它里面存储地址为 0x10 的一个字节, 应该构造几个 i2c_msg?
要构造 2 个 i2c_msg :

  1. 第一个 i2c_msg 表示写操作,把要访问的存储地址 0x10 发给设备

  2. 第二个 i2c_msg 表示读操作,并且返回读出的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
  //例如:
  u8 data_addr = 0x10;
  i8 data;
  struct i2c_msg msgs[2];
  
  msgs[0].addr = 0x50;
  msgs[0].flags = 0;
  msgs[0].len = 1;
  msgs[0].buf = &data_addr;
  msgs[1].addr = 0x50;
  msgs[1].flags = I2C_M_RD;
  msgs[1].len = 1;
  msgs[1].buf = &data;

2.3 I2C-Tools移植#

2.3.1 源码下载#

https://mirrors.edge.kernel.org/pub/software/utils/i2c-tools/

2.3.2 工具链配置#

1
2
3
export ARCH=arm
export CROSS_COMPILE=arm-buildroot-linux-gnueabihf-
export PATH=$PATH:/home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueab ihf_sdk-buildroot/bin

2.3.3 编译#

修改 I2C-Tools 的 Makefile 指定交叉编译工具链

1
2
3
CC ?= gcc
AR ?= ar
STRIP ?= strip

改为(指定交叉编译工具链前缀, 去掉问号):

1
2
3
CC = $(CROSS_COMPILE)gcc
AR = $(CROSS_COMPILE)ar
STRIP = $(CROSS_COMPILE)strip

在 Makefile 中,“?=”在第一次设置变量时才会起效果,如果之前设置过该变量,则不会起效果。

执行 make 时,是动态链接,需要把 libi2c.so 也放到单板上。 想静态链接的话,执行:make USE_STATIC_LIB=1

img

img

img

2.4 I2C Tools使用#

2.4.1 i2cdetect: I2C 检测#

// 列出当前的 I2C Adapter(或称为 I2C Bus、I2C Controller)

i2cdetect -l

// 打印某个 I2C Adapter 的 Functionalities, I2CBUS 为 0、1、2 等整数 i2cdetect -F I2CBUS

// 看看有哪些 I2C 设备, I2CBUS 为 0、1、2 等整数

i2cdetect -y -a I2CBUS

img

img

2.4.2 i2cget: I2C 读(SMBus协议)#

img

img

使用示例:

img

img

2.4.3 i2cset: I2C 写(SMBus协议)#

img

使用示例:

img

img

2.4.4 i2ctransfer:I2C传输(I2C协议)#

img

使用示例:

img

img

从i2c总线0,往0x1e设备地址,写2个字节,第一个字节写入寄存器地址0,第二个字节表示往寄存器0地址写入0x4。
从i2c总线0,往0x1e设备地址,写2个字节,第一个字节写入寄存器地址0,第二个字节表示往寄存器0地址写入0x3。
从i2c总线0,往0x1e设备地址,写1个字节,写入寄存器地址0xc表示要读0xc里面的值,再从0xc读2个字节。

2.5 I2C-Tools 访问 I2C 设备的方式#

I2C-Tools 可以通过 SMBus 来访问 I2C 设备,也可以使用一般的 I2C 协议 来访问 I2C 设备。 使用一句话概括 I2C 传输:APP 通过 I2C Controller 与 I2C Device 传 输数据.

1
2
Open("dev/i2c-0");
ioctl(file, I2C_SLAVE, address);

如果该设备已经有了对应的设备驱动程序,则返回失败。

1
ioctl(file, I2C_SLAVE_FORCE, address);

如果该设备已经有了对应的设备驱动程序但是还是想通过 i2c-dev 驱 动来访问它,则使用这个 ioctl 来指定 I2C 设备地址。

2.5.1 数据传输方式#

2.5.1.1 使用I2C方式#

ioctl(file, I2C_RDWR, &rdwr);(使用I2C方式)

img

该结构体表示一个或者多个i2c_msg。

示例代码:i2ctransfer.c

i2ctransfer -f -y 0 w1@0x1e 0xe r2为例:

流程如下:

img

构造i2c_rdwr_ioctl_data结构,要用2个i2c_msg结构,第一个表示写入0xe寄存器地址给设备,第二个表示要从0xe寄存器读出2个字节。

2.5.1.2 使用SMBus方式#

ioctl(file, I2C_SMBUS, &args) ;(使用SMBus方式)

示例代码:i2cget.c i2cset.c

img

2.5.1.3 直接使用read()/write()方式#

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
int gc2053_read_register(VI_PIPE ViPipe, int addr) {
int ret, data;
CVI_U8 buf[8];
CVI_U8 idx = 0;

if (g_fd[ViPipe] < 0)
return CVI_FAILURE;

if (gc2053_addr_byte == 2)
buf[idx++] = (addr >> 8) & 0xff;

// add address byte 0
buf[idx++] = addr & 0xff;

ret = write(g_fd[ViPipe], buf, gc2053_addr_byte);
if (ret < 0) {
CVI_TRACE_SNS(CVI_DBG_ERR, "I2C_WRITE error!\n");
return ret;
}

buf[0] = 0;
buf[1] = 0;
ret = read(g_fd[ViPipe], buf, gc2053_data_byte);
if (ret < 0) {
CVI_TRACE_SNS(CVI_DBG_ERR, "I2C_READ error!\n");
return ret;
}

// pack read back data
data = 0;
if (gc2053_data_byte == 2) {
data = buf[0] << 8;
data += buf[1];
} else {
data = buf[0];
}

syslog(LOG_DEBUG, "i2c r 0x%x = 0x%x\n", addr, data);
return data;
}

int gc2053_write_register(VI_PIPE ViPipe, int addr, int data) {
CVI_U8 idx = 0;
int ret;
CVI_U8 buf[8];

if (g_fd[ViPipe] < 0)
return CVI_SUCCESS;

if (gc2053_addr_byte == 1) {
buf[idx] = addr & 0xff;
idx++;
}

if (gc2053_data_byte == 1) {
buf[idx] = data & 0xff;
idx++;
}

ret = write(g_fd[ViPipe], buf, gc2053_addr_byte + gc2053_data_byte);
if (ret < 0) {
CVI_TRACE_SNS(CVI_DBG_ERR, "I2C_WRITE error!\n");
return CVI_FAILURE;
}

ret = read(g_fd[ViPipe], buf, gc2053_addr_byte + gc2053_data_byte);
//syslog(LOG_DEBUG, "i2c w 0x%x 0x%x\n", addr, data);

return CVI_SUCCESS;
}

字符编码与freetype移植

1 字符编码#

1.1 ASCII编码#

ascii是“American Standard Code for Information Interchange”的缩写, 美国信息交换标准代码。

电脑毕竟是西方人发明的,他们常用字母就 26 个,区分大小写、加上标点符号也没超过 127 个,每个字符用一个字节来表示就足够了。一个字节的 7 位就可以表示 128 个数值,在 ASCII 码中最高位永远是 0。

linux-4.18.16/lib/fonts这个目录下就有对应文件。在这里我挑选font_8x16.c

img

img

1.2 ANSI#

ASNI 是 ASCII 的扩展,向下包含 ASCII。对于 ASCII 字符仍以一个字节来表示。

对于非 ASCII 字符则使用 2 字节来表示。并没有固定的 ASNI 编码。

比如在中国大陆地区, ANSI 的默认编码是 GB2312;

在港澳台地区默认编码是 BIG5。以数值“ 0xd0d6”为例,对于 GB2312 编码它表示“中”;对于 BIG5 编码它表示“ 笢”。

用ANSI编码字符’aa中’的16进制数据

img

1.3 UNICODE#

在 ANSI 标准中,很多种文字都有自己的编码标准,汉字简体字有 GB2312、繁体字有 BIG5,这难免同一个数值对应不同字符。比如数值“ 0xd0d6”,对于GB2312 编码它表示“中”;对于 BIG5 编码它表示“ 笢”。这造成了使用 ANSI 编码保存的文件,不适合跨地区交流。

UNICODE 编码就是解决这类问题:对于地球上任意一个字符,都给它一个唯一的数值。

  1. ASCII 编码中使用一个字节来表示一个字符,只用到其中的 7 位,最高位恒为 0;
  2. ANSI 编码中,对于 ASCII 字符仍使用一个字节来表示(BIT7 是 0),对于非ASCII 字符一般使用 2 个字节来表示,非 ASCII 字符的数值 BIT7 都是 1

1.3.1 UTF-16 LE#

每个 UNICODE 值用 3 字节来表示有点浪费,那只用 2 字节呢?它可以表示2^16=65536 个字符,全世界常用的字符都可以表示了。Little endian 表示小字节序,数值中权重低的字节放在前面,比如字符“ A 中”在 TXT 文件中的数值如下,其中的“ A”使用“0x41 0x00”两字节表示;“中”使用“ 0x2d 0x4e”两字节表示。文件开头的“ 0xff 0xfe”表示“UTF-16 LE”。

img

1.3.2 UTF-16 BE#

Big endian 表示大字节序,数值中权重低的字节放在后面,比如字符“ ab中”在 TXT 文件中的数值如下,其中的“ A”使用“ 0x00 0x41”两字节表示;“中”使用“ 0x4e 0x2d”两字节表示。文件开头的“ 0xfe 0xff”表示“UTF-16 BE”。

img

1.4 UTF8#

UTF8 是一种变长的编码方法,有 2 种 UTF8格式的文件:带有头部、不带头部。

img

对于 ASCII 字符用UTF-16有空间浪费、而且文件中有某个字节丢失,这会使得后面所有字符都因为错位而无法显示。UTF8则不会有这样的问题。0x41表示大写字母’A’,只用了一个字节。上图中的 3 个字节“ 0xe4 0xb8 0xad”表示的数值是 0x4e2d,对应“中”的 UNICODE 码.

img

上图中, 0xe4 的二进制是“ 11100100”,高位有 3 个 1,表示从当前字节起有 3 字节参与表示 UNICODE;
0xb8 的二进制是“10111000”,高位有 1 个 1,表示从当前字节起有 1 字节参与表示 UNICODE;
0xad 的二进制是“10101101”,高位有 1 个 1,表示从当前字节起有 1 字节参与表示 UNICODE;
除去高位的“ 1110”、“ 10”、“ 10”后,剩下的二进制数组合起来得到“ 01001110001101”,它就是 0x4e2d,即“中”的 UNICODE 值。

使用 UTF8 编码时,即使 TXT 文件中丢失了某些数据,也只会影响到当前字符的显示,后面的字符不受影响。

2 中文字库移植#

2.1 -finput-charset -fexec-charset编译选项#

我们编写 C 程序时,可以使用 ANSI 编码,或是 UTF-8 编码;在编译程序时,可以使用以下的选项告诉编译器用什么方式编码:

1
2
-finput-charset=GB2312
-finput-charset=UTF-8

如果不指定-finput-charset, GCC 就会默认 C 程序的编码方式为 UTF8。

  1. 用ANSI格式编写编码,vim浏览显示会乱码,用notepad++采用ANSI编码格式浏览。

    img

img

img

由于编译器默认用utf8编码,所以看到最终打印是乱码的。可以看到“中”的ANSI码是d6 d0。

  1. 用utf8编写代码

    img

img

最终打印是OK的。可以看到“中”的utf8码是e4 b8 ad。

2.2 GB2312 转为 UTF-8#

img

从上面的输出信息可以看出来, GB2312 的”0xd6 0xd0”可以转换为 UTF-8的“ 0xe4 0xb8 0xad”。而如果把原本就是 UTF-8 格式的 test_charset_utf8.c当作 GB2312 格式,会引起错误.

2.3 UTF-8 转为 GB2312#

img

从 上 面 的 输 出 信 息 可 以 看 出 来 , 如 果 把 原 本 就 是 GB2312 格 式 test_charset_ansi.c 当成UTF-8 格式,会引起错误。而 UTF-8 格式的“中”编码值为“ 0xe4 0xb8 0xad”,可以转换为 GB2312 的“0xd6 0xd0”

2.4 HZK16中文字库#

HZK16字库里的16×16汉字一共需要256个点来显示,也就是说需要32个字节才能达到显示一个普通汉字的目的。符合GB2312标准。

一个GB2312汉字是由两个字节编码的,范围为A1A1~FEFE。A1-A9为符号区,B0到F7为汉字区。每一个区有94个字符。

HZK16 中是以 GB2312 编码值来查找点阵的,以“中”字为例,它的编码值是“ 0xd6 0xd0”,其中的 0xd6 表示“区码”,表示在哪一个区;其中的 0xd0 表示“位码”,表示它是这个区里的哪一个字符。每一个区有 94 个汉字。区位码从 0xa1 而不是从 0 开始,是为了兼容 ASCII码。

要显示“中”字, 它的 GB2312 编码是 d6d0,它是 HZK16 里第“ (0xd6-0xa1)*94+(0xd0-0xa1)”个字符。(0xd6-0xa1)表示是哪个区,(0xd0-0xa1)表示是哪个位。

如何获取和显示汉字”中“?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fd_hzk16 = open("HZK16", O_RDONLY);
if (fd_hzk16 < 0){
printf("can't open HZK16\n");
return -1;
}
if(fstat(fd_hzk16, &hzk_stat)){
printf("can't get fstat\n");
return -1;
}
hzkmem = (unsigned char *)mmap(NULL , hzk_stat.st_size, PROT_READ, MAP_SHARED, fd_hzk16, 0);
if (hzkmem == (unsigned char *)-1){
printf("can't mmap for hzk16\n");
return -1;
}

下载HZK16字库,读取并且mmap字库:

img

img

1
2
3
该函数在 LCD 的(x,y)位置处显示汉字字符 str, str[0]中保存区码、 str[1]中保存位码。
第 4734 行确定该汉字属于哪个区;第 4735 行确实它是该区中哪一个汉字。
第 4736 行确实它的字库地址(第多少个字节):每个区中有 94 个汉字,每个汉字在字库中占据 32 字节。

根据下图来理解字库中每个像素点是如何显示的:

img

总共有十六行,因此需要一个循环 16次的大循环(第 4740 行)。

考虑到一行有两个字节, 在大循环中加入一个 2 次的循环用于区分是哪个字节(第 4741 行)。

最后使用第 3 个循环来处理一个字节中的 8 位(第 4744 行)。对于每一位,它等于 1 时对应的像素被设置为白色,它等于 0 时对应的像素被设置为黑色。需要注意的是根据 x、 y、 i、 j、 b 来计算像素坐标。

测试:

img

注意:使用上述命令时 show_chinese.c 的编码格式必须是 ANSI(GB2312),因为HZK16字库是按照GB2312编码的,否则编译时需要指定“ -fexec-charset=GB2312”。

3 freetype移植#

FreeType库是一个完全免费(开源)的、高质量的且可移植的字体引擎。Freetype 是开源的字体引擎库, 它提供统一的接口来访问多种字体格式文件,从而实现矢量字体显示。我们只需要移植这个字体引擎,调用对应的 API 接口,提供字体文件,就可以让 freetype 库帮我们取出关键点、实现闭合曲线, 填充颜色, 达到显示矢量字体的目的。

这里仅移植freetype库,freetype的使用不做具体展开。可以从 https://www.freetype.org/ 可 以 下 载 到 “ freetype-doc-2.10.2.tar.xz”。

3.1 矢量字体#

使用点阵字库显示英文字母、汉字时, 大小固定, 如果放大缩小则会模糊甚至有锯齿出现,为了解决这个问题,引用矢量字体。

1
2
3
第1步 确定关键点,
第2步 使用数学曲线( 贝塞尔曲线) 连接头键点,
第3步 填充闭合区线内部空间。

什么是关键点?以字母“ A”为例:

img

再用数学曲线(比如贝塞尔曲线)将关键点都连接起来, 得到一系列的封闭的曲线:

img

最后把封闭空间填满颜色,就显示出一个 A 字母:

img

如果需要放大或者缩小字体,关键点的相对位置是不变的, 只要数学曲线平滑,字体就不会变形。

3.2 下载freetype#

https://freetype.org/download.html

https://download.savannah.gnu.org/releases/freetype/

freetype 依赖于 libpng, libpng 又依赖于 zlib,所以我们应该:先编译安装 zlib,再编译安装 libpng,最后编译安装 freetype。 但是,有些工具链里有 zlib, 那就不用编译安装 zlib.

下载安装libpng: https://www.linuxfromscratch.org/blfs/view/svn/general/libpng.html

下载安装zlib: https://www.zlib.net/fossils/

3.3 编译freetype#

3.3.1 设置工具链#

1
2
3
4
5
export ARCH=arm
export CROSS_COMPILE=arm-buildroot-linux-gnueabihf-
export PATH=$PATH:/home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin

echo 'main(){}'| arm-buildroot-linux-gnueabihf-gcc -E -v -

得到头文件的系统目录为:

1
2
3
4
5
6
7
/media/cvitek/robin.lee/my_test/study/weidongshan/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin/../lib/gcc/arm-buildroot-linux-gnueabihf/7.5.0/include

/media/cvitek/robin.lee/my_test/study/weidongshan/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin/../lib/gcc/arm-buildroot-linux-gnueabihf/7.5.0/include-fixed

/media/cvitek/robin.lee/my_test/study/weidongshan/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin/../lib/gcc/arm-buildroot-linux-gnueabihf/7.5.0/../../../../arm-buildroot-linux-gnueabihf/include

/media/cvitek/robin.lee/my_test/study/weidongshan/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/arm-buildroot-linux-gnueabihf/sysroot/usr/include

库文件系统目录为:

1
2
3
COMPILER_PATH=/media/cvitek/robin.lee/my_test/study/weidongshan/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin/../libexec/gcc/arm-buildroot-linux-gnueabihf/7.5.0/:/media/cvitek/robin.lee/my_test/study/weidongshan/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin/../libexec/gcc/:/media/cvitek/robin.lee/my_test/study/weidongshan/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin/../lib/gcc/arm-buildroot-linux-gnueabihf/7.5.0/../../../../arm-buildroot-linux-gnueabihf/bin/

LIBRARY_PATH=/media/cvitek/robin.lee/my_test/study/weidongshan/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin/../lib/gcc/arm-buildroot-linux-gnueabihf/7.5.0/:/media/cvitek/robin.lee/my_test/study/weidongshan/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin/../lib/gcc/:/media/cvitek/robin.lee/my_test/study/weidongshan/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin/../lib/gcc/arm-buildroot-linux-gnueabihf/7.5.0/../../../../arm-buildroot-linux-gnueabihf/lib/:/media/cvitek/robin.lee/my_test/study/weidongshan/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/arm-buildroot-linux-gnueabihf/sysroot/lib/:/media/cvitek/robin.lee/my_test/study/weidongshan/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/arm-buildroot-linux-gnueabihf/sysroot/usr/lib/

3.3.2 编译zlib#

img

编译zlib库时,./configure不允许传入–host参数;不支持的话需要export CC设置为你的arm工具链

1
2
3
4
export CC=arm-buildroot-linux-gnueabihf-gcc
./configure --prefix=$PWD/tmp
make;make install
cd tmp/lib;ls

将lib和头文件拷贝到工具链目录(或者不拷贝,到时候编译用-L, -I指定即可,运行时指定LIBRARY_PATH)

1
2
cp include/* -rf /home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin/../lib/gcc/arm-buildroot-linux-gnueabihf/7.5.0/include
cp lib/* -rfd /home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin/../lib/gcc/arm-buildroot-linux-gnueabihf/7.5.0/../../../../arm-buildroot-linux-gnueabihf/lib

3.3.3 编译libpng#

1
2
3
4
./configure --host= arm-buildroot-linux-gnueabihf --prefix=$PWD/tmp
make
make install
cd tmp/lib;ls

img

将lib和头文件拷贝到工具链目录。

3.3.4 编译freetype#

1
2
3
./configure --host= arm-buildroot-linux-gnueabihf --prefix=$PWD/tmp
Make
Make install

注意:如果你的工具链路径不是 /home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot,那么make时会出现类似如下错误。

img

修改自己工具链下的$(TOOLCHAIN)/arm-buildroot-linux-gnueabihf/sysroot/usr/lib目录下编辑libfreetype.la, 替换dependency_libs和libdir的路径。来自 < http://bbs.100ask.net/question/15908>

img

libfreetype库如下:

img

将lib和头文件拷贝到工具链目录。

3.4 测试#

1
gcc freetype_show_font.c -I /media/cvitek/robin.lee/my_test/study/weidongshan/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/arm-buildroot-linux-gnueabihf/sysroot/usr/include/freetype2/ -L /media/cvitek/robin.lee/my_test/study/weidongshan/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/arm-buildroot-linux-gnueabihf/sysroot/usr/lib -lfreetype
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <linux/fb.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <wchar.h>
#include <sys/ioctl.h>
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_GLYPH_H

int fd_fb;
struct fb_var_screeninfo var; /* Current var */
int screen_size;
unsigned char *fbmem;
unsigned int line_width;
unsigned int pixel_width;

/**********************************************************************
* 函数名称: lcd_put_pixel
* 功能描述: 在LCD指定位置上输出指定颜色(描点)
* 输入参数: x坐标,y坐标,颜色
* 输出参数: 无
* 返 回 值: 会
***********************************************************************/
void lcd_put_pixel(int x, int y, unsigned int color) {
unsigned char *pen_8 = fbmem+y*line_width+x*pixel_width;
unsigned short *pen_16;
unsigned int *pen_32;
unsigned int red, green, blue;

pen_16 = (unsigned short *)pen_8;
pen_32 = (unsigned int *)pen_8;

switch (var.bits_per_pixel) {
case 8: {
*pen_8 = color;
break;
}
case 16: {
/* 565 */
red = (color >> 16) & 0xff;
green = (color >> 8) & 0xff;
blue = (color >> 0) & 0xff;
color = ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
*pen_16 = color;
break;
}
case 32: {
*pen_32 = color;
break;
}
default: {
printf("can't surport %dbpp\n", var.bits_per_pixel);
break;
}
}
}
/**********************************************************************
* 函数名称: draw_bitmap
* 功能描述: 根据bitmap位图,在LCD指定位置显示汉字
* 输入参数: x坐标,y坐标,位图指针
* 输出参数: 无
* 返 回 值: 无
***********************************************************************/
void draw_bitmap( FT_Bitmap* bitmap, FT_Int x, FT_Int y) {
FT_Int i, j, p, q;
FT_Int x_max = x + bitmap->width;
FT_Int y_max = y + bitmap->rows;

//printf("x = %d, y = %d\n", x, y);

for (j = y, q = 0; j < y_max; j++, q++) {
for (i = x, p = 0; i < x_max; i++, p++) {
if (i < 0 || j < 0 || i >= var.xres || j >= var.yres)
continue;

//image[j][i] |= bitmap->buffer[q * bitmap->width + p];
lcd_put_pixel(i, j, bitmap->buffer[q * bitmap->width + p]);
}
}
}

int main(int argc, char **argv) {
wchar_t *chinese_str = L"繁";
FT_Library library;
FT_Face face;
int error;
FT_Vector pen;
FT_GlyphSlot slot;
int font_size = 24;

if (argc < 2) {
printf("Usage : %s <font_file> [font_size]\n", argv[0]);
return -1;
}

if (argc == 3)
font_size = strtoul(argv[2], NULL, 0);

fd_fb = open("/dev/fb0", O_RDWR);
if (fd_fb < 0) {
printf("can't open /dev/fb0\n");
return -1;
}

if (ioctl(fd_fb, FBIOGET_VSCREENINFO, &var)) {
printf("can't get var\n");
return -1;
}

line_width = var.xres * var.bits_per_pixel / 8;
pixel_width = var.bits_per_pixel / 8;
screen_size = var.xres * var.yres * var.bits_per_pixel / 8;
fbmem = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0);
if (fbmem == (unsigned char *)-1) {
printf("can't mmap\n");
return -1;
}

/* 清屏: 全部设为黑色 */
memset(fbmem, 0, screen_size);

/* 显示矢量字体 */
error = FT_Init_FreeType( &library ); /* initialize library */
/* error handling omitted */

error = FT_New_Face( library, argv[1], 0, &face ); /* create face object */
/* error handling omitted */
slot = face->glyph;

FT_Set_Pixel_Sizes(face, font_size, 0);

/* 确定座标:
*/
//pen.x = 0;
//pen.y = 0;

/* set transformation */
//FT_Set_Transform( face, 0, &pen);

/* load glyph image into the slot (erase previous one) */
error = FT_Load_Char( face, chinese_str[0], FT_LOAD_RENDER );
if (error) {
printf("FT_Load_Char error\n");
return -1;
}

draw_bitmap( &slot->bitmap,
var.xres/2,
var.yres/2);
return 0;
}

ini_parse配置解析功能移植

1 ini_parse移植#

1.1 下载ini解析源码#

源码的github地址https://github.com/benhoyt/ini

1
git clone https://github.com/benhoyt/inih.git

img

1.2 使用ini_parse功能#

img

img

img

可以看到核心就是一个ini_parse函数。用户自定义一个callback函数去解析自己的配置ini。测试代码如下:

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
43
44
45
46
47
48
49
50
51
52
#include <stdio.h>
#include "ini.h"
#include <string.h>
typedef struct _SAMPLE_INI_CFG_S {
char name[100];
int bus_id;
int age;
char name2[100];
int bus_id2;
int age2;
} SAMPLE_INI_CFG_S;
static int parse_handler(void *user, const char *section, const char *name, const char *value) {
SAMPLE_INI_CFG_S *cfg = (SAMPLE_INI_CFG_S *)user;
if (strcmp(section, "person1") == 0) {
if (strcmp(name, "name") == 0) {
strcpy(cfg->name, value);
} else if (strcmp(name, "bus_id") == 0) {
cfg->bus_id = atoi(value);
} else if (strcmp(name, "age") == 0) {
cfg->age = atoi(value);
} else {
/* unknown section/name */
}
} else if (strcmp(section, "person2") == 0) {
if (strcmp(name, "name") == 0) {
strcpy(cfg->name2, value);
} else if (strcmp(name, "bus_id") == 0) {
cfg->bus_id2 = atoi(value);
} else if (strcmp(name, "age") == 0) {
cfg->age2 = atoi(value);
} else {
/* unknown section/name */
}
} else {
/* unknown section/name */
}
return 1;
}
int main(int argc, char **argv) {
SAMPLE_INI_CFG_S ini_cfg;
int ret = ini_parse("./sensor_cfg.ini", parse_handler, &ini_cfg);
if (ret > 0) {
printf("Parse err in %d line.\n", ret);
return ret;
}
if (ret == 0) {
printf("Parse incomplete, use default cfg ./sensor_cfg.ini\n");
printf("%s, %d, %d\n", ini_cfg.name, ini_cfg.bus_id, ini_cfg.age);
printf("%s, %d, %d\n", ini_cfg.name2, ini_cfg.bus_id2, ini_cfg.age2);
}
return 0;
}

1.2.1 自定义ini文件#

ini配置文件sensor_cfg.ini如下:

img

gcc test.c ini.c。我的callback定义是parse_handler,从ini中解析section,每个section会调用一次callback,解析出所有的section。

运行代码:

img

1.2.2 支持语法检测#

ini_parse还支持语法检测。但ini写的不和语法规范会报错。手工制造ini语法错误,测试结果如下:

img

img

2 minIni移植使用#

MiniINI 是一个用来解析 INI/CFG 配置文件的 C++ 库,主要特点是可移植性、性能和小体积。支持上千种 INI 格式配置,易用简单。

2.1 下载minIni#

GitHub - compuphase/minIni: A small and portable INI file library with read/write support

2.2 特征#

  • minIni 支持读取senction外部的key,因此它支持不使用section的配置文件(但在其他方面与 INI 文件兼容)。
  • 可以使用冒号分隔键和值;冒号等价于等号。也就是说,字符串“Name: Value”和“Name=Value”具有相同的含义。
  • minIni 不需要标准 C/C++ 库中的 文件 I/O 函数,且允许通过宏配置要选择文件 I/O 接口。
  • 哈希字符 (“#”) 是分号开始注释的替代方法。允许尾随注释(即在一行上的键/值对后面)。
  • key名称和val周围的前导和尾随空格将被忽略。
  • 当写入包含注释字符(“;”或“#”)的值时,该值将自动放在双引号之间;读取值时,将删除这些引号。当设置中出现双引号本身时,这些字符将被转义。
  • 支持section和key枚举。
  • 您可以选择设置 minIni 将使用的行终止符(对于文本文件)。(这是编译时设置,而不是运行时设置)。
  • 由于写入速度远低于闪存(SD/MMC 卡、U 盘)中的读取速度,因此 minIni 以双倍“文件读取”为代价将“文件写入”降至最低。
  • 内存占用是确定性的。没有动态内存分配。

2.3 INI 文件语法#

1
2
3
4
[Network]  #section
hostname=My Computer #key = val
address=dhcp
dns = 192.168.1.1

2.4 minIni支持文件系统#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

#define INI_FILETYPE FILE*
#define ini_openread(filename,file) ((*(file) = fopen((filename),"r")) != NULL)
#define ini_openwrite(filename,file) ((*(file) = fopen((filename),"w")) != NULL)
#define ini_close(file) (fclose(*(file)) == 0)
#define ini_read(buffer,size,file) (fgets((buffer),(size),*(file)) != NULL)
#define ini_write(buffer,file) (fputs((buffer),*(file)) >= 0)
#define ini_rename(source,dest) (rename((source), (dest)) == 0)
#define ini_remove(filename) (remove(filename) == 0)

#define INI_FILEPOS fpos_t
#define ini_tell(file,pos) (fgetpos(*(file), (pos)) == 0)
#define ini_seek(file,pos) (fsetpos(*(file), (pos)) == 0)

2.5 minIni的API介绍#

image-20240602144015881

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "minIni.h"
#define sizearray(a) (sizeof(a) / sizeof((a)[0]))
const char inifile[] = "example.ini";
int main(void){
char str[100];
char section[50];
long n;

n = ini_gets("Network", "address", "dummy", str, sizearray(str), inifile);
if (n >= 0) printf("Network/address=%s", str);

n = ini_getl("Network", "timeout", -1, inifile);
printf("Network/timeout=%ld\n", n);
}

example.ini如下:

1
2
3
4
5
[Network]
hostname=My Computer
address=dhcp
dns=192.168.1.1
timeout=10

运行结果:

1
2
Network/address=dhcp
Network/timeout=10

2.5.1 ini_gets()#

获取字符串类型的值.

1
2
3
4
5
6
7
8
9
10
int   ini_gets(const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *DefValue, mTCHAR *Buffer, int BufferSize, const mTCHAR *Filename);
/*
参数 1 是 Section;
参数 2 是 Key;
参数 3 是获取不到值时的默认值;
参数 4 是用于保存目标键值的 Buffer;
参数 5 是 Buffer 的长度;
参数 6 是 INI 文件的路径;
*/

image-20240602144722018

先打开文件,然后用 getkeystring() 找到目标键值,最后拷贝给调用者。

2.5.1.1 getkeystring#

image-20240602150058567

image-20240602145619533

  1. 用 fgets 进行逐行读取,用 strrchr 找到包含 ‘[‘ 和 ‘]’ 的行,然后再用 strncasecmp 找到目标 Section 所在的行。
  2. 继续用 fgets 进行逐行读取,用 strrchr 找到包含 ‘=’ 的行,然后再用 strncasecmp 找到目标 Key 所在的行。
  3. 用 strncpy 将目标 Key 的值拷贝给调用者。

大致就是这3个关键步骤,当然还有很多其他异常处理,语法检测和边界判断的逻辑,这里不做展示。

2.5.2 ini_getl()#

ini_getl() 用于获取整型类型的值,也是间接调用int_gets, 最后将字符串转换成数字。

2.5.3 ini_puts()#

写出参数到ini,保存到ini。

1
2
3
4
5
6
7
8
9
10
11
12
/** ini_puts()
* \param Section the name of the section to write the string in
* \param Key the name of the entry to write, or NULL to erase all keys in the section
* \param Value a pointer to the buffer the string, or NULL to erase the key
* \param Filename the name and full path of the .ini file to write to
*
* \return 1 if successful, otherwise 0
*/
int ini_puts(const TCHAR *Section, const TCHAR *Key, const TCHAR *Value, const TCHAR *Filename)
//eg:
ini_putl("second", "age", 20, inifile);
n = ini_puts("first", "alt", NULL, inifile);//当val等于NULL表示删除该key

2.5.4 ini_putl()#

ini_putl() 用于写出整型类型的值,也是间接调用int_puts, 将数字转换成字符串,然后保存字符串到ini。

image-20240602171648913

2.5.5 section/key enumeration#

1
2
3
4
5
6
7
8
printf("4. Section/key enumeration, file structure follows\n");

for (s = 0; ini_getsection(s, section, sizearray(section), inifile) > 0; s++) {
printf(" [%s]\n", section);
for (k = 0; ini_getkey(section, k, str, sizearray(str), inifile) > 0; k++) {
printf("\t%s\n", str);
}
}//对section和key进行枚举

2.5.6 section/key存在性检查#

1
2
3
4
5
6
/* section/key presence check */
assert(ini_hassection("first", inifile));//检查是否有first 段
assert(!ini_hassection("fourth", inifile));
assert(ini_haskey("first", "val", inifile));//检查first段是否有val这个key
assert(!ini_haskey("first", "test", inifile));
printf("5. checking presence of sections and keys passed\n");

2.5.7 配置文件阅读打印#

ini_browse用来打印出每个段的每个key的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
int Callback(const char *section, const char *key, const char *value, void *userdata) {
(void)userdata; /* this parameter is not used in this example */
printf(" [%s]\t%s=%s\n", section, key, value);
return 1;
}

/* browsing through the file */
printf("6. browse through all settings, file field list follows\n");
ini_browse(Callback, NULL, inifile);

if (access(filename, F_OK) != 0) {
perror("open %s fail", filename);
}

image-20240602173647362