嵌入式基础

文章目录

  • 操作
    • 网络
    • 一些命令
  • 入门
  • Linux
    • GCC
    • Makefile
      • 函数
      • 通用Makefile使用
    • 文件IO
      • 系统调用函数怎么进入内核?
  • Linux软件架构
    • Linux启动过程
    • 如何理解Bootloader与Kernel
    • 文件系统
      • 概念
      • 虚拟文件系统、根文件系统和文件系统
      • VFS:
      • 根文件系统
      • 其他文件系统
      • uboot与根文件系统的关系
    • 总结
  • 应用编程
    • Framebuffer(ioctl mmap)
      • ioctl
      • mmap
    • 文字显示
      • 字符编码
      • ASCII字符显示
      • 中文字符显示
      • 交叉编译程序:以freetype为例
        • 库相关知识
        • 交叉编译
      • freetype
    • 输入系统
      • 框架
        • 调试技巧
        • 不用库开发应用
          • 查询方式
          • 休眠-唤醒方式
          • POLL/SELECT方式
        • 异步通知方式
      • tslib
    • 网络通信
    • 多线程
    • 串口
      • TTY体系中设备节点的差别
      • TTY驱动框架
        • 行规程
      • Linux串口应用编程
        • 串口怎么插
        • 串口API
        • 实例(回环)
    • I2C
      • I2C框架
      • 协议
        • 写操作
        • 读操作
        • 信号
        • 细节
      • SMBus协议
      • I2C系统重要结构体
      • 无需编写驱动直接访问设备_I2C-Tools介绍
        • 访问I2C设备的俩种方式
        • 源码

预览:

嵌入式基础_第1张图片

嵌入式基础_第2张图片

操作

``/etc/init.d/rcS` 是6ull开发板的开机脚本文件

echo "7 4 1 7" > /proc/sys/kernel/printk 开启内核打印信息


开发板挂载: mount -t nfs -o nolock,vers=3 192.168.2.125:/home/book/nfs_rootfs /mnt

NAT的话: mount -t nfs -o nolock,vers=3,port=2049,mountport=9999 192.x.x.x:/home/book/nfs_rootfs /mnt

mount命令用来挂载各种支持的文件系统协议到某个目录下。

mount成功之后,开发板在/mnt目录下读写文件时,实际上访问的就是Ubuntu中的/home/book/nfs_rootfs目录,所以开发板和Ubuntu之间通过NFS可以很方便地共享文件。

在开发过程中,在Ubuntu中编译好程序后放入/home/book/nfs_rootfs目录,开发板mount nfs后就可以直接使用/mnt下的文件。


网络

nmcli r wifi on
nmcli dev wifi
nmcli dev wifi connect "DRLyyds" password "19407010220" ifname wlan0

官方

移除GUI: mv /etc/init.d/S07hmi /root reboot
重新加载驱动
rmmod 8723bu.ko
modprobe 8723bu
移除其它控制
ps -ef | grep "wpa"
292 root     /usr/sbin/wpa_supplicant -u
kill -9 292
rm /etc/wpa_supplicant.conf
3.执行wifi链接操作
ifconfig wlan0 up
iw dev wlan0 scan |grep SSID
//wpa_passphrase  HUAWEI zihezihe  >> /etc/wpa_supplicant.conf
-/etc/wpa_supplicant.conf
ctrl_interface=/var/run/wpa_supplicant
ctrl_interface_group=0
update_config=1
network={
        ssid="DRLyyds"
        psk="19407010220"
}
-
wpa_supplicant -B -iwlan0 -c /etc/wpa_supplicant.conf
iw wlan0 link
udhcpc -i wlan0
ping -I wlan0 www.baidu.com

设置交叉编译工具链: vim ~/.bashrc 永久生效

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

一些命令

udhcpc : 获取ip 用网线的时候用

make之后, 可以使用make menuconfig来查看命令 状况 然后make 100ask_imx6ull_pro_ddr512m_systemV_qt5_defconfig make all

编译成功后文件输出路径为 output/images

buildroot2020.02.x 

  ├── output

​    ├── images  

​      ├── 100ask_imx6ull-14x14.dtb <--设备树文件  

​      ├── rootfs.ext2         <--ext2格式根文件系统

​      ├── rootfs.ext4 -> rootfs.ext2   <--ext2格式根文件系统 

​      ├── rootfs.tar         

​      ├── rootfs.tar.bz2       <--打包并压缩的根文件系统,用于NFSROOT启动

​      ├── 100ask-imx6ull-pro-512d-systemv-v1.img     <--完整的系统镜像(可以用来烧写emmc和sd卡)

​      ├── u-boot-dtb.imx       <--u-boot镜像

​      └── zImage         <--内核镜像


烧写裸机文件: imx是烧写EMMC, img是烧写到sdCard

parsec

入门

Linux启动流程

嵌入式基础_第3张图片

IMX6ULL启动流程

reset 开发板复位启动 -> rom 板子只读内存 ->加载UBOOT 去加载 第一段bootloader引导程序 -> uboot里有启动参数(环境变量)来启动kernel -> kernel + dtb 设备树 -> 启动Rootfs 跟文件系统 -> app

使用mount挂载

mount -t nfs -o nolock,vers=3 192.168.2.125:/home/book/nfs_rootfs /mnt

mount -t nfs -o intr,nolock,rsize=1024,wsize=1024 192.168.2.125:/home/book/nfs_rootfs /mnt

挂载ubuntu的nfs目录到开发板/mnt目录下,挂载成功后使用df -h命令查看所有挂载。

编译内核镜像-设备树

编译内核模块->nfs挂载->传输到开发板 reboot即可使用新的 zImage镜像 *.dtb设备树 /lib/modules模块

编译UBOOT

make distclean 删除之前缓存 -> make mx6ull_14x14_evk_defconfig -> make

Linux

shell会去环境变量读取 可以通过echo SPATH 查看

ls: -l(long 显示完整信息) -a(显示隐藏文件) -h(列出大小)

cp: r:recursive,递归地,即复制所有文件 f:force,强制覆盖 d:如果源文件为链接文件,也只是把它作为链接文件复制过去,而不是复制实际文件

rm: r:recursive,递归地,即删除所有文件 f:force,强制删除

chgrp:改变文件所属用户组
        -R : 进行递归的持续更改,也连同子目录下的所有文件、目录都更新成为这个用户组之意。常常用在更改			某一目录内所有文件的情况。
chown:改变文件所有者
chmod:改变文件的权限
        r:  4或0
        ② w:  2或0
        ③ x:  1或0
        这3种权限的取值相加后,就是权限的数字表示。
        例如:文件a的权限为“-rwxrwx---”,它的数值表示为:
        ① owner = rwx = 4+2+1 = 7
        ② group = rwx = 4+2+1 = 7
        ③ others = --- = 0+0 +0 = 0
        使用u、g、o三个字母代表user、group、others 3中身份。此外a代表all,即所有身份。
        范例: 
        chmod u=rwx,go=rx  .bashrc

        也可以增加或去除某种权限,“+”表示添加权限,“-”表示去除权限:
        chmod a+w  .bashrc
        chmod a-x  .bashrc

查找\搜索命令

find find 目录名 选项 查找条件 用来查找文件

例: find /home/book/dira/ -name " test1.txt "

! - 查找最近几天(几个小时)之内(之前)有变动的文件

$ find /home/book -mtime -2 //查找/home目录下两天内有变动的文件。

grep grep命令的作用是查找文件中符合条件的字符串,其格式如下: grep [选项] [查找模式] [文件名]

grep -rn "字符串" 文件名 r(recursive):递归查找 n(number):显示目标位置的行号 可以加入-w全字匹配。

可以在grep的结果中再次执行grep搜索,比如搜索包含有ABC的头文件,可执行如下命令:
$ grep  “ABC”  *  -nR  |  grep\.h”
上述命令把第1个命令“grep  “ABC”  *  -nR”通过管道传给第2个命令。
即第2个命令在第1个命令的结果中搜索。

vi 学习见 -> 日常技巧

查找命令或应用程序的所在位置

which pwd //定位到/bin/pwd whereis pwd //可得到可执行程序的位置和手册页的位置

GCC

GCC 编译选项

常用选项 描述
-E 预处理,开发过程中想快速确定某个宏可以使用“-E -dM”
-c 把预处理、编译、汇编都做了,但是不链接
-o 指定输出文件
-I 指定头文件目录
-L 指定链接时库文件目录
-l 指定链接哪一个库文件

其他有用的选项:

gcc -E main.c   // 查看预处理结果,比如头文件是哪个
gcc -E -dM main.c  > 1.txt  // 把所有的宏展开,存在1.txt里
gcc -Wp,-MD,abc.dep -c -o main.o main.c  // 生成依赖文件abc.dep,后面Makefile会用

echo 'main(){}'| gcc -E -v -  // 它会列出头文件目录、库目录(LIBRARY_PATH)

制作, 使用动态库 | 静态库

动态库:
制作、编译:
gcc -c -o main.o  main.c
gcc -c -o sub.o   sub.c
gcc -shared  -o libsub.so  sub.o  sub2.o  sub3.o(可以使用多个.o生成动态库)
gcc -o test main.o  -lsub  -L /libsub.so/所在目录/

运行:
① 先把libsub.so放到Ubuntu的/lib目录,然后就可以运行test程序。
② 如果不想把libsub.so放到/lib,也可以放在某个目录比如/a,然后如下执行:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/a  
./test

静态库:
gcc -c -o main.o  main.c
gcc -c -o sub.o   sub.c
ar  crs  libsub.a  sub.o  sub2.o  sub3.o(可以使用多个.o生成静态库)
gcc  -o  test  main.o  libsub.a  (如果.a不在当前目录下,需要指定它的绝对或相对路径)
运行:
不需要把静态库libsub.a放到板子上。
注意:执行arm-linux-gnueabihf-gcc -c -o sub.o   sub.c交叉编译需要在最后面加上 -fPIC参数。

GCC编译过程

嵌入式基础_第4张图片
  • (1)预处理

    C/C++源文件中,以“#”开头的命令被称为预处理命令,如包含命令“#include”、宏定义命令“#define”、条件编译命令“#if”、“#ifdef”等。预处理就是将要包含(include)的文件插入原文件中、将宏定义展开、根据条件编译命令选择要使用的代码,最后将这些东西输出到一个“.i”文件中等待进一步处理。

  • (2)编译

    编译就是把C/C++代码(比如上述的“.i”文件)“翻译”成汇编代码,所用到的工具为cc1(它的名字就是cc1,x86有自己的cc1命令,ARM板也有自己的cc1命令)。

  • (3)汇编

    汇编就是将第二步输出的汇编代码翻译成符合一定格式的机器代码,在Linux系统上一般表现为ELF目标文件(OBJ文件),用到的工具为as。x86有自己的as命令,ARM版也有自己的as命令,也可能是xxxx-as(比如arm-linux-as)。

    “反汇编”是指将机器代码转换为汇编代码,这在调试程序时常常用到。

  • (4)链接

    链接就是将上步生成的OBJ文件和系统库的OBJ文件、库文件链接起来,最终生成了可以在特定平台运行的可执行文件,用到的工具为ld或collect2。

编译过程常用选项:

常用选项 描述
-E 预处理,开发过程中想快速确定某个宏可以使用“-E -dM”
-c 把预处理、编译、汇编都做了,但是不链接
-o 指定输出文件
-I 指定头文件目录
-L 指定链接时库文件目录
-l 指定链接哪一个库文件

总体选项

(1)-c

预处理、编译和汇编源文件,但是不作链接,编译器根据源文件生成OBJ文件。缺省情况下,GCC通过用’.o’替换源文件名的后缀’.c’,‘.i’,`.s’等,产生OBJ文件名。可以使用-o选项选择其他名字。GCC忽略-c选项后面任何无法识别的输入文件。

(2)-S

编译后即停止,不进行汇编。对于每个输入的非汇编语言文件,输出结果是汇编语言文件。缺省情况下,GCC通过用`.s’替换源文件名后缀 ‘.c’,'.i’等等,产生汇编文件名。可以使用-o选项选择其他名字。GCC忽略任何不需要汇编的输入文件。

(3)-E

预处理后即停止,不进行编译。预处理后的代码送往标准输出。

(4)-o file

指定输出文件为file。无论是预处理、编译、汇编还是链接,这个选项都可以使用。如果没有使用-o 选项,默认的输出结果是:可执行文件为a.out;修改输入文件的名称是source.suffix,则它的OBJ文件是source.o,汇编文件是 source.s,而预处理后的C源代码送往标准输出。

(5)-v

显示制作GCC工具自身时的配置命令;同时显示编译器驱动程序、预处理器、编译器的版本号。

Makefile

demo:

hello: hello.c
gcc -o hello hello.c
clean:
rm -f  hello

完善Makefile

第1个Makefile,简单粗暴,效率低:
test : main.c sub.c sub.h
	gcc -o test main.c sub.c

第2个Makefile,效率高,相似规则太多太啰嗦,不支持检测头文件:
test : main.o sub.o
	gcc -o test main.o sub.o
main.o : main.c
	gcc -c -o main.o  main.c
sub.o : sub.c
	gcc -c -o sub.o  sub.c	
clean:
	rm *.o test -f

第3个Makefile,效率高,精炼,不支持检测头文件:
test : main.o sub.o
	gcc -o test main.o sub.o
%.o : %.c //%是通配符
	gcc -c -o $@  $<
clean:
	rm *.o test -f

第4个Makefile,效率高,精炼,支持检测头文件(但是需要手工添加头文件规则):
test : main.o sub.o
	gcc -o test main.o sub.o
%.o : %.c 
	gcc -c -o $@(代表目标文件)  $<(代表第一个依赖)
sub.o : sub.h
clean:
	rm *.o test -f

第5个Makefile,效率高,精炼,支持自动检测头文件:
objs := main.o sub.o
test : $(objs)
	gcc -o test $^(表示所有的依赖)
# 需要判断是否存在依赖文件
# .main.o.d .sub.o.d
dep_files(依赖文件) := $(foreach f, $(objs), .$(f).d)
dep_files := $(wildcard $(dep_files))

# 把依赖文件包含进来
ifneq ($(dep_files),) (如果这个变量不等于空)
  include $(dep_files)
endif
%.o : %.c
	gcc -Wp,-MD,[email protected]  -c -o $@  $<
clean:
	rm *.o test -f
distclean:
	rm  $(dep_files) *.o test -f
	
----
CFLAGS(编译参数) = -Werr (把所有警告都当作错误) -I(指定头文件目录) . -Iinclude(默认头文件目录)

**假想目标 : ** .PHONY

**即时变量 延时变量 : ** export A := xxx 定义时就确定 B = xxx 使用时才确定

?= 延时变量 只有在第一次定义才起效 += 是即时还是延时取决于前面的定义

函数

目标(target)…: 依赖(prerequiries)…
命令(command)
如果“依赖文件”比“目标文件”更加新,那么执行“命令”来重新生成“目标文件”。
命令被执行的2个条件:依赖文件比目标文件新,或是 目标文件还没生成。

$(foreach var,list,text)

对list中的每一个元素,取出来赋给var,然后把var改为text所描述的形式。

例子:
objs := a.o b.o
dep_files := $(foreach f, $(objs), .$(f).d) // 最终 dep_files := .a.o.d .b.o.d

$(wildcard pattern)

pattern所列出的文件是否存在,把存在的文件都列出来。

例子:
src_files := $( wildcard  *.c)  // 最终 src_files中列出了当前目录下的所有.c文件

$(filter pattern..., text) $(filter-out pattern..., text)

在text中取出符合patten格式的值 (不符合)

$(patsubst pattern, replacement, $(var))

从列表中取出每一个值, 如果符合pattern, 则替换为replacement

gcc -M c.c 打印出依赖

gcc -M -MF c.d c.c 把依赖写入文件c.d

gcc -c -o c.o c.c -MD -MF c.d 编译c.o, 把依赖写入文件c.d

通用Makefile使用

目录: D:\imx6ull\01_all_series_quickstart\04_嵌入式Linux应用开发基础知识\source\05_general_Makefile\Makefile_and_readme

待补充

文件IO

嵌入式基础_第5张图片
  • 如果要访问真实的文件(SD卡 等等) 需要挂载

    可以 cat /proc/mounts 看是否自动挂载

    使用mount /dev/sda1 /mnt 来手动挂载到mnt目录下

  • 有些文件是虚拟的,Linux内核提供虚拟文件系统,根据虚拟文件系统里面的文件可以查看内核的一些信息/sys

  • 其他为驱动文件, 通过函数去直接操作硬件

在Linux内核有俩种驱动,字符设备驱动’c’(ls -al 第一个字母) 块设备’b’ 主设备号 次设备号 (通过这些来确定到底是哪一个驱动 哪一个硬件)

不是通用的函数:ioctl/mmap

俩个复制文件的demo:

这个是用 write / read 方式
    
02 #include <sys/types.h>
03 #include <sys/stat.h>
04 #include <fcntl.h>
05 #include <unistd.h>
06 #include <stdio.h>
07
08 /*
09  * ./copy 1.txt 2.txt
10  * argc    = 3
11  * argv[0] = "./copy"
12  * argv[1] = "1.txt"
13  * argv[2] = "2.txt"
14  */
15 int main(int argc, char **argv)
16 {
17      int fd_old, fd_new;
18      char buf[1024];
19      int len;
20
21      /* 1. 判断参数 */
22      if (argc != 3)
23      {
24              printf("Usage: %s  \n", argv[0]);
25              return -1;
26      }
27
28      /* 2. 打开老文件 */
29      fd_old = open(argv[1], O_RDONLY);
30      if (fd_old == -1)
31      {
32              printf("can not open file %s\n", argv[1]);
33              return -1;
34      }
35
36      /* 3. 创建新文件 */
37      fd_new = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
38      if (fd_new == -1)
39      {
40              printf("can not creat file %s\n", argv[2]);
41              return -1;
42      }
43
44      /* 4. 循环: 读老文件-写新文件 */
45      while ((len = read(fd_old, buf, 1024)) > 0)
46      {
47              if (write(fd_new, buf, len) != len)
48              {
49                      printf("can not write %s\n", argv[2]);
50                      return -1;
51              }
52      }
53
54      /* 5. 关闭文件 */
55      close(fd_old);
56      close(fd_new);
57
58      return 0;
59 }


这个使用 mmap方式

02 #include <sys/types.h>
03 #include <sys/stat.h>
04 #include <fcntl.h>
05 #include <unistd.h>
06 #include <stdio.h>
07 #include <sys/mman.h>
08
09 /*
10  * ./copy 1.txt 2.txt
11  * argc    = 3
12  * argv[0] = "./copy"
13  * argv[1] = "1.txt"
14  * argv[2] = "2.txt"
15  */
16 int main(int argc, char **argv)
17 {
18      int fd_old, fd_new;
19      struct stat stat;
20      char *buf;
21
22      /* 1. 判断参数 */
23      if (argc != 3)
24      {
25              printf("Usage: %s  \n", argv[0]);
26              return -1;
27      }
28
29      /* 2. 打开老文件 */
30      fd_old = open(argv[1], O_RDONLY);
31      if (fd_old == -1)
32      {
33              printf("can not open file %s\n", argv[1]);
34              return -1;
35      }
36
37      /* 3. 确定老文件的大小 */
38      if (fstat(fd_old, &stat) == -1)
39      {
40              printf("can not get stat of file %s\n", argv[1]);
41              return -1;
42      }
43
44      /* 4. 映射老文件 */
45      buf = mmap(NULL, stat.st_size, PROT_READ, MAP_SHARED, fd_old, 0);
46      if (buf == MAP_FAILED)
47      {
48              printf("can not mmap file %s\n", argv[1]);
49              return -1;
50      }
51
52      /* 5. 创建新文件 */
53      fd_new = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
54      if (fd_new == -1)
55      {
56              printf("can not creat file %s\n", argv[2]);
57              return -1;
58      }
59
60      /* 6. 写新文件 */
61      if (write(fd_new, buf, stat.st_size) != stat.st_size)
62      {
63              printf("can not write %s\n", argv[2]);
64              return -1;
65      }
66
67      /* 5. 关闭文件 */
68      close(fd_old);
69      close(fd_new);
70
71      return 0;
72 }

系统调用函数怎么进入内核?

应用程序通过 open/read/write 进而去访问设备、文件。

进入到内核kernel, 对于普通文件,会使用文件系统去读取对应的磁盘, 对于硬件, 会去找到内核中对应的驱动程序调用相关的程序

调用过程

这些关于文件的函数一般是由glibc提供的,以及其他不同版本的C库(ucibc), open这些要去打开文件需要依赖操作系统提供的功能(怎么进入Linux内核), 可以把内核当作另外一个APP , 这个app不能去调用另外一个app里面的函数

open函数里面会放一条指令, 比如在32为cpu,会使用swi指令, 对于64位cpu, 使用svc指令, 一旦执行这些指令, 就会触发CPU的异常, 会导致CPU跳到某个地址里面去执行系统里面的代码, 这时候操作系统就会去执行对应的sys_open sys_read函数, 对于read , write 函数也是如此.

那么它怎么知道app触发这个异常是为了去调用sys_open或者是sys_read, glibc在实现sys这些系统调用的时候, 会使用这些命令(swi等)来触发异常, 并且传入不同的参数给内核, 内核根据不同的参数来分辨是想调用哪个

嵌入式基础_第6张图片

  • glibc里面怎么传递参数给内核呢? 最开始用的old abi 二进制接口(老的API), 在执行swi指令的时候可以在这条指令后面跟一个数值, 内核执行这个swi的异常处理函数时会把后面的数值取出来, 根据里面的值知道要去调用什么函数.
  • 后来又有改进,使用EABI, 不在swi里面传参数了, 它事先在 R7 寄存器(汇编) 里面存入那些值, 当发生异常的时候,内核会去R7寄存器得到这些值, 进而得到去执行sys的什么函数
  • 对于64位,使用svc指令, 通过X8寄存器传入, 内核里面有一个系统函数调用指针数组, sys_call_table内核把库函数传进来的值处理之后, 用这个值作为下标, 在这个数组里面对应的函数.(阅读glibc的源码,内核源码)

app调用openread导致内核的sys_openread被调用,那么内核的sys_openread会做什么事情呢

嵌入式基础_第7张图片

Linux软件架构

在linux系统软件架构可以分为4个层次(从低到高分别为):

1.引导加载程序

​ 引导加载程序(Bootloader)是固化在硬件Flash中的一段引导代码,用于完成硬件的一些基本配置,引导内核启动。

​ 同时,Bootloader会在自身与内核分区之间存放一些可设置的参数(Boot parameters),比如IP地址,串口波特率,要传递给内核的命令行参数。

2.系统内核

​ 系统内核(Kernel)是整个操作系统的最底层,它负责整个硬件的驱动,以及提供各种系统所需的核心功能,包括防火墙机制、是否支持LVM或Quota等文件系统等等,如果内核不认识某个最新的硬件,那么硬件也就无法被驱动,你也就无法使用该硬件。计算机真正工作的东西其实是硬件,例如数值运算要使用到CPU、数据储存要使用到硬盘、图形显示会用到显示适配器、音乐发声要有音效芯片、连接Internet 可能需要网络卡等等。内核就是控制这些芯片如何工作。

3.文件系统

​ Linux文件系统(File System)中的文件是数据的集合,文件系统不仅包含着文件中的数据而且还有文件系统的结构,所有Linux 用户和程序看到的文件、目录、软连接及文件保护信息等都存储在其中。

​ 文件系统是操作系统用于明确存储设备(常见的是磁盘,也有基于NAND Flash的固态硬盘)或分区上的文件的方法和数据结构;即在存储设备上组织文件的方法。操作系统中负责管理和存储文件信息的软件机构称为文件管理系统,简称文件系统。

4.用户程序

​ 用户应用程序(Application)为了完成某项或某几项特定任务而被开发运行于操作系统之上的计算机程序。

Linux启动过程

正常启动过程中,Bootloader首先运行,然后将内核复制到内存中(或者在固态存储设备上直接运行,但是效率较低),并在内存某个固定的地址(包括地址与参数的结构)设置好要传递给内核的参数,最后运行内核。内核启动后,挂载(mount)根文件系统(Root filesystem),启动文件系统中的应用程序。

上电 ——> Bootloader —[传递参数]—> 加载内核 ——> 内核挂载根文件系统 ——>执行应用程序

如何理解Bootloader与Kernel

操作系统内核本身就是一个裸机程序,和我们学的uboot和其他裸机程序没有本质的区别;事实上,不少U-Boot源码就是根据相应的Linux内核源程序进行简化而形成的,尤其是一些设备的驱动程序。如果我们去琢磨U-Boot源码的注释,便会轻易的发现这一情况。

区别就是操作系统运行起来后可以分为应用层(用户态)和内核层(内核态),分层后,两层的权限不同(实现的原理是基于CPU的模式切换),内存访问和设备操作的管理上更加精细(内核可以随便方位各种硬件,而应用程序只能被限制的访问硬件和内存地址)。

以ARM处理器为例,除用户模式外,其余6种工作模式都属于特权模式:

用户模式(USR):正常程序执行模式,不能直接切换到其他模式

系统模式(SYS):运行操作系统的特权任务,与用户模式类似,但具有可以直接切换到其他模式等特权

快中断模式(FIQ):支持高速数据传输及通道处理,FIQ异常响应时进入此模式

中断模式(IRQ):用于通用中断处理,IRQ异常响应时进入此模式

管理模式(SVC):操作系统保护模式,系统复位和软件中断响应时进入此模式(由系统调用执行软中断SWI命令触发)

中止模式(ABT):用于支持虚拟内存和/或存储器保护,在ARM7TDMI没有大用处

未定义模式(UND):支持硬件协处理器的软件仿真,未定义指令异常响应时进入此模式

Linux内核态是从ARM的SVC即管理模式下启动的,但在某些情况下、如:硬件中断、程序异常(被动)等情况下进入ARM的其他特权模式,这时仍然可以进入内核态(因为就是可以操作内核了);同样,Linux用户态是从ARM用户模式启动的,但当进入ARM系统模式时、仍然可以操作Linux用户态程序(进入用户态,如init进程的启动过程)。

即:Linux内核从ARM的SVC模式下启动,但内核态不仅仅指ARM的SVC模式(还包括可以访问内核空间的所有ARM模式);Linux用户程序从ARM的用户模式启动,但用户态不仅仅指ARM的用户模式。

直观来看:uboot的镜像是u-boot.bin,Linux系统的镜像是zImage,这两个东西其实都是裸机程序镜像。从系统启动的角度来讲,内核和uboot都是裸机程序。

文件系统

概念

Linux文件系统中的文件是数据的集合,文件系统不仅包含着文件中的数据而且还有文件系统的结构,所有Linux 用户和程序看到的文件、目录、软连接及文件保护信息等都存储在其中。这种机制有利于用户和操作系统的交互。

尽管内核是 Linux 的核心,但文件却是用户与操作系统交互所采用的主要工具。这对 Linux 来说尤其如此,这是因为在 UNIX 传统中,它使用文件 I/O 机制管理硬件设备和数据文件

虚拟文件系统、根文件系统和文件系统

VFS:

Linux支持多种文件系统类型,因为它将底层与应用层分隔开;而提供统一的接口支持应用层对于不同实现的文件系统的访问,这个统一的接口称为虚拟文件系统VFS。

kernel中以VFS去支持各种文件系统,如yaffs,ext3,cramfs等等。yaffs/yaffs2是专为嵌入式系统使用NAND型闪存而设计的一种日志型文件系统。在内核中以VFS来屏蔽各种文件系统的接口不同,以VFS向kernel提供一个统一的接口。

根文件系统

文件系统指文件存在的物理空间,linux系统中每个分区都是一个文件系统,都有自己的目录层次结构。以“/”为顶级目录的文件系统称为根文件系统。

Linux启动时,第一个必须挂载的是根文件系统;若系统不能从指定设备上挂载根文件系统,则系统会出错而退出启动。系统正常挂载根文件系统之后可以自动或手动挂载其他的文件系统(根文件系统是其他文件的最终挂载点)。因此,一个系统中可以同时存在不同的文件系统。

也就是说:

根文件系统可以是任何kernel支持的文件系统类型(ext4,yaffs等)。

但它必须包含linux内核启动时所必需的文件(根文件系统必需存在的目录 /dev /bin /sbin 等等),不然系统启动会失败。

根文件系统是之所以有个根(/)字,是因为它是linux系统启动时挂载(mount,所谓挂载:就是在内存中创建一个虚拟的文件对应具体的存储空间分区的过程,挂载时不会保存信息,下次启动时得重新挂载)的第一个文件系统,启动完成后可以自动(配置etc/fstab)或者手动的方式将其它文件系统(分区)挂载到根文件系统中。

其他文件系统

不同的文件系统类型有不同的特点,因而根据存储设备的硬件特性、系统需求等有不同的应用场合。在嵌入式Linux应用中,主要的存储设备为 RAM(DRAM, SDRAM)和ROM(常采用FLASH存储器),常用的基于存储设备的文件系统类型包括:jffs2, yaffs, cramfs, romfs, ramdisk,initramfs, ramfs/tmpfs,ubifs等。

uboot与根文件系统的关系

早期的uboot没有分区的概念,uboot只知道应该将什么数据烧写到存储介质的什么区间中。

(也就是说,对于uboot来看,只有起始地址结束地址等,A~B地址放内核,C~D地址放文件系统,即:规定哪个地址区间放内核或者文件系统等)

虽然此后,uboot中渐渐也有了MTD等管理分区部分的功能。尽管如此,不影响我们的学习理解。

总结

cpu首先执行位于0地址的uboot,uboot启动以后初始化一些资源,告诉内核有关的参数并引导内核,内核通过事先添加对于某种文件系统类型的支持驱动(相当于一小段程序),读取uboot等boot loader在指定的区域烧写制作好的文件系统镜像,内核解析并挂载成根文件系统,并在此基础上,通过VFS再挂载不同的文件系统类型,完成启动以后,再去管理有关的资源(包括应用程序)

应用编程

Framebuffer(ioctl mmap)

bpp: 每个像素用多少位来表示它的颜色

首地址+偏移地址=确定出位于哪里

假设fb_base是APP执行mmap后得到的Framebuffer地址

求偏移地址: y*line_width+x*pixel_width(这就时偏移地址) + fb_base = 绝对地址

可以用以下公式算出(x,y)坐标处像素对应的Framebuffer地址:

(x,y)像素起始地址=fb_base+(xres*bpp/8)y + xbpp/8

一行的宽度: xres * bpp/8 一个像素宽度: bpp/8

ioctl

ioctl是设备驱动程序中对设备的I/O通道进行管理的函数 。所谓对I/O通道进行管理,就是对设备的一些特性进行控制

ioctl函数是文件结构中的一个属性分量,就是说如果你的驱动程序提供了对ioctl的支持,用户就可以在用户程序中使用ioctl函数来控制设备的I/O通道。

头文件:#include

函数原型: int ioctl(int fd, unsigned long request, ...);

函数说明:

① fd 表示文件描述符;

② request表示与驱动程序交互的命令,用不同的命令控制驱动程序输出我们需要的数据;

③ … 表示可变参数arg,根据request命令,设备驱动程序返回输出的数据。

④ 返回值:打开成功返回文件描述符,失败将返回-1。

ioctl的作用非常强大、灵活。不同的驱动程序内部会实现不同的ioctl,APP可以使用各种ioctl跟驱动程序交互:可以传数据给驱动程序,也可以从驱动程序中读出数据。

mmap

#include 
函数原型:
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);

函数说明:
① addr表示指定映射的內存起始地址,通常设为 NULL表示让系统自动选定地址,并在成功映射后返回该地址;
② length表示将文件中多大的内容映射到内存中;
③ prot 表示映射区域的保护方式,可以为以下4种方式的组合
a. PROT_EXEC 映射区域可被执行
b. PROT_READ 映射区域可被读出
c. PROT_WRITE 映射区域可被写入
d. PROT_NONE 映射区域不能存取
④ Flags 表示影响映射区域的不同特性,常用的有以下两种
a. MAP_SHARED 表示对映射区域写入的数据会复制回文件内,原来的文件会改变。
b. MAP_PRIVATE 表示对映射区域的操作会产生一个映射文件的复制,对此区域的任何修改都不会写回原来的文件内容中。
⑤ 返回值:若成功映射,将返回指向映射的区域的指针,失败将返回-1

映射framebuffer, framebuffer时驱动程序分配的, 应用程序想要去使用必须使用mmap映射到用户空间, 应用程序得到LCD的参数并且得到framebuffer的地址之后就可以在上面操作了.

描点函数demo:

28 void lcd_put_pixel(int x, int y, unsigned int color(传入RGB888))
29 {
30      unsigned char *pen_8 = fb_base+y*line_width+x*pixel_width;
31      unsigned short *pen_16;
32      unsigned int *pen_32;
33
34      unsigned int red, green, blue;
35
36      pen_16 = (unsigned short *)pen_8;
37      pen_32 = (unsigned int *)pen_8;
38
39      switch (var.bits_per_pixel)
40      {
41              case 8:
42              {
43                      *pen_8 = color;
44                      break;
45              }
46              case 16:
47              {
48                      /* 565 */
49                      red   = (color >> 16) & 0xff;
50                      green = (color >> 8) & 0xff;
51                      blue  = (color >> 0) & 0xff;
52                      color = ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 						3);//保证绿颜色高六位 //保证蓝颜色高五位
53                      *pen_16 = color;
54                      break;
55              }
56              case 32:
57              {
58                      *pen_32 = color;
59                      break;
60              }
61              default:
62              {
63                      printf("can't surport %dbpp\n", var.bits_per_pixel);
64                      break;
65              }
66      }
67 }28行中传入的color表示颜色,它的格式永远是0x00RRGGBB,即RGB888。当LCD是16bpp时,要把color变量中的R、G、B抽出来再合并成RGB565格式。
第30行计算(x,y)坐标上像素对应的Framebuffer地址。
第43行,对于8bpp,color就不再表示RBG三原色了,这涉及调色板的概念,color是调色板的值。
第4951行,先从color变量中把R、G、B抽出来。
第52行,把red、green、blue这三种8位颜色值,根据RGB565的格式,只保留red中的高5位、green中的高6位、blue中的高5位,组合成一个新的16位颜色值。
第53行,把新的16位颜色值写入Framebuffer。
第58行,对于32bpp,颜色格式跟color参数一致,可以直接写入Framebuffer。
   
 /* 清屏: 全部设为白色 */
96      memset(fbmem, 0xff, screen_size);
97
98      /* 随便设置出100个为红色 */
99      for (i = 0; i < 100; i++)
100             lcd_put_pixel(var.xres/2+i, var.yres/2, 0xFF0000);

文字显示

字符编码

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

嵌入式基础_第8张图片

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

在中国大陆地区,ANSI的默认编码是GB2312;在港澳台地区默认编码是BIG5。以数值“0xd0d6”为例,对于GB2312编码它表示“中”;对于BIG5编码它表示“笢”。所以对于ANSI编码的TXT文件,如果你打开它发现乱码,那么还得再次细分它的具体编码。

UNICODE

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

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

UNICODE仍然向下兼容ASCII,但是对于其他字符会有对应的数值,比如对于“中”、“笢”,它们的数值分别是:0x4e2d、0x7b22

UNICODE中的数值范围是0x0000至0x10FFFF,有1,114,111即100多万个数值,可以表示100多万个字符,足够地球人使用了。

ASCII字符显示

嵌入式基础_第9张图片

中文字符显示

如果不指定“-finput-charset”,GCC就会默认C程序的编码方式为UTF-8,即使你是以ANSI格式保存,也会被当作UTF-8来对待。

对于编译出来的可执行程序,可以指定它里面的字符是以什么方式编码,可以使用以下的选项编译器:

-fexec-charset=GB231

-fexec-charset=UTF-8

如果不指定“-fexec-charset”,GCC就会默认编译出的可执行程序中字符的编码方式为UTF-8。

如果“-finput-charset”与“-fexec-charset”不一样,编译器会进行格式转换。

待补充…

交叉编译程序:以freetype为例

使用buildroot来给ARM板编译程序、编译库会很简单,这里freetype在编译 安装一些小程序很有用

库相关知识

  1. 编译程序时去哪找头文件?

    系统目录:就是交叉编译工具链里的某个include目录;

    也可以自己指定:编译时用 “ -I dir ”选项指定。

  2. 链接时去哪找库文件?

    系统目录:就是交叉编译工具链里的某个lib目录;

    也可以自己指定:链接时用 “ -L dir ”选项指定。

  3. 运行时去哪找库文件?

    系统目录:就是板子上的/lib、/usr/lib目录;

    也可以自己指定:运行程序用环境变量LD_LIBRARY_PATH指定。

  4. 运行时不需要头文件,所以头文件不用放到板子上

库的问题 这是链接程序时出现的问题

系统目录:就是交叉编译工具链里的某个lib目录, 也可以自己指定:链接时用 -L dir选项指定

怎么确定“系统目录”?执行下面命令确定目录:

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

它会列出头文件目录、库目录(LIBRARY_PATH),你编译出库文件时,可以把它放入系统库目录。

运行问题

系统目录:就是板子上的/lib、/usr/lib目录

也可以自己指定: 运行程序用环境变量LD_LIBRARY_PATH指定,执行以下的命令

export  LD_LIBRARY_PATH=/xxx_dir  ;  ./test
或
LD_LIBRARY_PATH=/xxx_dir   ./test

交叉编译

如果交叉编辑工具链的前缀是arm-buildroot-linux-gnueabihf-,比如arm-buildroot-linux-gnueabihf-gcc,交叉编译开源软件时,如果它里面有configure

./configure  --host=arm-buildroot-linux-gnueabihf(这里自己修改)   --prefix=$PWD/tmp
make
make install

就可以在当前目录的tmp目录下看见bin, lib, include等目录,里面存有可执行程序、库、头文件。

程序运行不需要头文件

freetype

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

一个文字的显示过程可以概括如下:

① 给定一个字符可以确定它的编码值(ASCII、UNICODE、GB2312);

② 设置字体大小;

③ 根据编码值,从文件头部中通过charmap找到对应的关键点(glyph),它会根据字体大小调整关键点;

④ 把关键点转换为位图点阵;

⑤ 在LCD上显示出来

如何使用freetype库,总结出下列步骤:

① 初始化:FT_InitFreetype

② 加载(打开)字体Face:FT_New_Face

③ 设置字体大小:FT_Set_Char_Sizes 或 FT_Set_Pixel_Sizes

④ 选择charmap:FT_Select_Charmap

⑤ 根据编码值charcode找到glyph_index:glyph_index = FT_Get_Char_Index(face,charcode)

⑥ 根据glyph_index取出glyph:FT_Load_Glyph(face,glyph_index)

⑦ 转为位图:FT_Render_Glyph

⑧ 移动或旋转:FT_Set_Transform

⑨ 最后显示出来。

上面的⑤⑥⑦可以使用一个函数代替:FT_Load_Char(face, charcode, FT_LOAD_RENDER),它就可以得到位图。

demo:

如果想在代码中能直接使用UNICODE值,需要使用wchar_t,宽字符, 占四个字节

注意:如果test_wchar.c是以ANSI(GB2312)格式保存,那么需要使用以下命令来编译:

gcc -finput-charset=GB2312 -fexec-charset=UTF-8 -o test_wchar test_wchar.c

显示一行文字

笛卡尔坐标系:

在LCD的坐标系中,原点在屏幕的左上角。对于笛卡尔坐标系,原点在左下角。freetype使用笛卡尔坐标系,在显示时需要转换为LCD坐标系。

从下图可知,X方向坐标值是一样的。

在Y方向坐标值需要换算,假设LCD的高度是V

在LCD坐标系中坐标是(x, y),那么它在笛卡尔坐标系中的坐标值为(x, V-y)。

反过来也是一样的,在笛卡尔坐标系中坐标是(x, y),那么它在LCD坐标系中坐标值为(x, V-y)。

嵌入式基础_第10张图片

具体函数见文档393页.

输入系统

外设都是输入设备,Linux系统为了统一管理这些输入设备,实现了一套能兼容所有输入设备的框架:输入系统。驱动开发人员基于这套框架开发出程序,应用开发人员就可以使用统一的API去使用设备。

框架

嵌入式基础_第11张图片

假设用户程序直接访问/dev/input/event0设备节点,或者使用tslib访问设备节点,数据的流程如下:

① APP发起读操作,若无数据则休眠;
② 用户操作设备,硬件上产生中断;
③ 输入系统驱动层对应的驱动程序处理中断:
	读取到数据,转换为标准的输入事件,向核心层汇报。
	所谓输入事件就是一个“struct input_event”结构体。
④ 核心层可以决定把输入事件转发给上面哪个handler来处理:
	从handler的名字来看,它就是用来处输入操作的。有多种handler,比如:evdev_handler(常用)、	kbd_handler、joydev_handler等等。
	最常用的是evdev_handler:它只是把input_event结构体保存在内核buffer等,APP来读取时就原原本本地返回。它支持多个APP同时访问输入设备,每个APP都可以获得同一份输入事件。
	当APP正在等待数据时,evdev_handler会把它唤醒,这样APP就可以返回数据。
⑤ APP对输入事件的处理:
	APP获得数据的方法有2种:直接访问设备节点(比如/dev/input/event0,1,2,...),或者通过tslib、libinput这类库来间接访问设备节点。这些库简化了对数据的处理。

内核中怎么表示一个输入设备? 使用input_dev结构体来表示输入设备,它的内容如下:

嵌入式基础_第12张图片

驱动程序上报的数据含义三项重要内容

  • type 表示数据是哪一类型的, 比如EV_KEY(表示按键类) 当type=0表示是同步事件 表示硬件已经上报了所有数据

  • code 按键类事件里面是哪一个按键? 硬件上报的是哪个按键

  • value 值, 比如0(松开) 1(按下) 2(长按)

  • 事件之间的界线

    APP读取数据时,可以得到一个或多个数据,比如一个触摸屏的一个触点会上报X、Y位置信息,也可能会上报压力值。

    APP怎么知道它已经读到了完整的数据?

    驱动程序上报完一系列的数据后,会上报一个“同步事件”,表示数据上报完毕。APP读到“同步事件”时,就知道已经读完了当前的数据。

得到一系列的输入事件,就是一个一个“struct input_event”,它定义如下:

嵌入式基础_第13张图片

每个输入事件input_event中都含有发生时间:timeval表示的是“自系统启动以来过了多少时间”,它是一个结构体,含有“tv_sec、tv_usec”两项(即秒、微秒)。

调试技巧

入设备的设备节点名为/dev/input/eventX(也可能是/dev/eventX,X表示0、1、2等数字)。查看设备节点,可以执行以下命令: ls /dev/input/* -lls /dev/event* -l

怎么知道这些设备节点对应什么硬件呢?可以在板子上执行以下命令: cat /proc/bus/input/devices

这条指令的含义就是获取与event对应的相关设备信息,可以看到类似以下的结果:

嵌入式基础_第14张图片

这里的I、N、P、S、U、H、B对应的每一行是什么含义:

I:id of the device(设备ID) 该参数由结构体struct input_id来进行描述
N:name of the device 设备名称
P:physical path to the device in the system hierarchy 系统层次结构中设备的物理路径。
S:sysfs path 位于sys文件系统的路径
U:unique identification code for the device(if device has it) 设备的唯一标识码
H:list of input handles associated with the device. 与设备关联的输入句柄列表。
B:bitmaps(位图)
	PROP:device properties and quirks(设备属性)
	EV:types of events supported by the device(设备支持的事件类型)
	KEY:keys/buttons this device has(此设备具有的键/按钮)
	MSC:miscellaneous events supported by the device(设备支持的其他事件)
	LED:leds present on the device(设备上的指示灯)

比如上图中“B: EV=b”用来表示该设备支持哪类输入事件。b的二进制是1011,bit0、1、3为1,表示该设备支持0、1、3这三类事件,即EV_SYN、EV_KEY、EV_ABS。

再举一个例子,“B: ABS=2658000 3”如何理解?

它表示该设备支持EV_ABS这一类事件中的哪一些事件。这是2个32位的数字:0x2658000、0x3,高位在前低位在后,组成一个64位的数字:“0x2658000,00000003”,数值为1的位有:0、1、47、48、50、53、54,即:0、1、0x2f、0x30、0x32、0x35、0x36,对应一些宏.

命令读取

调试输入系统时,直接执行类似下面的命令,然后操作对应的输入设备即可读出数据: hexdump /dev/input/event0

嵌入式基础_第15张图片

上图中的type为3,对应EV_ABS;code为0x35对应ABS_MT_POSITION_X;code为0x36对应ABS_MT_POSITION_Y。

上图中还发现有2个同步事件:它的type、code、value都为0。表示电容屏上报了2次完整的数据。

不用库开发应用

输入系统支持完整的API操作 : 支持这些机制:阻塞、非阻塞、POLL/SELECT、异步通知。

APP访问硬件的几种方式

① 时不时进房间看一下:查询方式
简单,但是累
② 进去房间陪小孩一起睡觉,小孩醒了会吵醒她:休眠-唤醒
不累,但是妈妈干不了活了
③ 妈妈要干很多活,但是可以陪小孩睡一会,定个闹钟:poll方式
要浪费点时间,但是可以继续干活。
妈妈要么是被小孩吵醒,要么是被闹钟吵醒。
④ 妈妈在客厅干活,小孩醒了他会自己走出房门告诉妈妈:异步通知
妈妈、小孩互不耽误。

通过ioctl获取设备信息,ioctl的参数如下: int ioctl(int fd, unsigned long request, ...);

查询方式

APP调用open函数时,传入“O_NONBLOCK”表示“非阻塞”。

APP调用read函数读取数据时,如果驱动程序中有数据,那么APP的read函数会返回数据,否则也会立刻返回错误。

休眠-唤醒方式

APP调用open函数时,不要传入“O_NONBLOCK”。

APP调用read函数读取数据时,如果驱动程序中有数据,那么APP的read函数会返回数据;否则APP就会在内核态休眠,当有数据时驱动程序会把APP唤醒,read函数恢复执行并返回数据给APP。

POLL/SELECT方式

POLL机制、SELECT机制是完全一样的,只是APP接口函数不一样。在调用poll、select函数时可以传入“超时时间”。在这段时间内,条件合适时(比如有数据可读、有空间可写)就会立刻返回,否则等到“超时时间”结束时返回错误。

APP先调用open函数时。APP不是直接调用read函数,而是先调用poll或select函数,这2个函数中可以传入“超时时间”。它们的作用是:如果驱动程序中有数据,则立刻返回;否则就休眠。在休眠期间,如果有人操作了硬件,驱动程序获得数据后就会把APP唤醒,导致poll或select立刻返回;如果在“超时时间”内无人操作硬件,则时间到后poll或select函数也会返回。

poll/select函数可以监测多个文件,可以监测多种事件:

事件类型 说明
POLLIN 有数据可读
POLLRDNORM 等同于POLLIN
POLLRDBAND Priority band data can be read,有优先级较较高的“band data”可读 Linux系统中很少使用这个事件
POLLPRI 高优先级数据可读
POLLOUT 可以写数据
POLLWRNORM 等同于POLLOUT
POLLWRBAND Priority data may be written
POLLERR 发生了错误
POLLHUP 挂起
POLLNVAL 无效的请求,一般是fd未open

在调用poll函数时,要指明:

① 你要监测哪一个文件:哪一个fd

② 你想监测这个文件的哪种事件:是POLLIN、还是POLLOUT

最后,在poll函数返回时,要判断状态。

代码补充

异步通知方式

所谓异步通知,就是APP可以忙自己的事,当驱动程序用数据时它会主动给APP发信号,这会导致APP执行信号处理函数。

① 谁发:驱动程序发
② 发什么:信号
③ 发什么信号:SIGIO
④ 怎么发:内核里提供有函数
⑤ 发给谁:APP,APP要把自己告诉驱动
⑥ APP收到后做什么:执行信号处理函数
⑦ 信号处理函数和信号,之间怎么挂钩:APP注册信号处理函数

除了注册SIGIO的处理函数,APP还要做什么事?想想这几个问题:

① 内核里有那么多驱动,你想让哪一个驱动给你发SIGIO信号?

APP要打开驱动程序的设备节点。

② 驱动程序怎么知道要发信号给你而不是别人?

APP要把自己的进程ID告诉驱动程序。

③ APP有时候想收到信号,有时候又不想收到信号:

应该可以把APP的意愿告诉驱动:设置Flag里面的FASYNC位为1,使能“异步通知”。

tslib

tslib是一个触摸屏的开源库,可以使用它来访问触摸屏设备,可以给输入设备添加各种“filter”(过滤器,就是各种处理),地址是:http://www.tslib.org/。

编译tslib后,可以得到libts库,还可以得到各种工具:较准工具、测试工具。

具体待补充.

网络通信

数据传输,都有三个要素 :源、目的、长度。

待补充

多线程

待补充

串口

UART:通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),简称串口

参数

  • 波特率:一般选波特率都会有9600,19200,115200等选项。其实意思就是每秒传输这么多个比特位数(bit)。

  • 起始位:先发出一个逻辑”0”的信号,表示传输数据的开始。

  • 数据位:可以是5~8位逻辑”0”或”1”。如ASCII码(7位),扩展BCD码(8位)。小端传输。

  • 校验位:数据位加上这一位后,使得“1”的位数应为偶数(偶校验)或奇数(奇校验),以此来校验数据传送的正确性。

  • 停止位:它是一个字符数据的结束标志。

如何发送数据

嵌入式基础_第16张图片

要发送数据时,CPU控制(UART单元)内存要发送的数据通过FIFO传给UART里面的移位器(诸位发送),依次将数据发送出去,在发送完成后产生中断提醒CPU传输完成。

接收数据时,获取接收引脚的电平,逐位放进接收移位器,再放入FIFO,程序再从FIFO中把数据取出来写入内存。在接收完成后产生中断提醒CPU传输完成。

TTY体系中设备节点的差别

设备节点 含义
/dev/ttyS0、/dev/ttySAC0 串口
/dev/tty1、/dev/tty2、/dev/tty3、…… 虚拟终端设备节点
/dev/tty0 前台终端
/dev/tty 程序自己的终端,可能是串口、也可能是虚拟终端
/dev/console 控制台,又内核的cmdline参数确定

TTY /Terminal /Console /UART

下图中两条红线之内的代码被称为TTY子系统。它既支持UART,也支持键盘、显示器,还支持更复杂的功能(比如伪终端)

ctrl + alt +f4(x) 切换

嵌入式基础_第17张图片

关于 /dev/ttyN 通过内核的cmdline来指定,比如: console=ttyS0 console=tty

更多:

TTY驱动框架

嵌入式基础_第18张图片

TTY这一层帮你屏蔽了不同输入输出设备的差别,串口 键盘等的驱动程序肯定会向上注册某个结构体, 再TTY这层使用同一个接口访问不同的设备

虚拟终端: 对于同一套键盘和显示器, 它可以对应不同的个虚拟终端, 对于虚拟终端又抽象出了一层 virtual terminal(串口没有这个) , 有很多个虚拟终端, 驱动程序得到键盘信息时会发送给前排的虚拟终端, 应用程序要显示的数据也会存入某一个buffer, 要显示时再从buffer中取出来,

行规程

嵌入式基础_第19张图片

也叫做回显 echo, 比较有意思 : 说的是在键盘上输入一个字符所发生的事情.

删除的话,删除命令(就是字节对应的编码) 发送到后,驱动上报行规成, 它把buf中的lsa删除掉a,然后将删除命令再发送回去, 所谓删除a只是显示效果, 串口工具还是接收到了4个字节的数据: l s a 退格键

回车键: 一样的,…, 将buf中的数据上传给APP(shell), 把结果再发给行规成,再发给串口驱动,然后PC显示.

Linux串口应用编程

串口怎么插

嵌入式基础_第20张图片 嵌入式基础_第21张图片

这个位置是串口,靠

串口API

在Linux系统中,操作设备的统一接口就是:open/ioctl/read/write。对于UART,又在ioctl之上封装了很多函数,主要是用来设置行规程。

所以对于UART,编程的套路就是:

open -> 设置行规程,比如波特率、数据位、停止位、检验位、RAW模式、一有数据就返回 -> read/write

怎么设置行规程?行规程的参数用结构体termios来表示,把这个结构体构造好之后再发给驱动程序.

嵌入式基础_第22张图片

这些函数在名称上有一些惯例:

`` tc:terminal contorl cf: control flflag`

函数名 作用
tcgetattr get terminal attributes,获得终端的属性(获得驱动程序参数)
tcsetattr set terminal attributes,修改终端参数
tcflflush 清空终端未完成的输入/输出请求及数据
cfsetispeed sets the input baud rate,设置输入波特率
cfsetospeed sets the output baud rate,设置输出波特率
cfsetspeed 同时设置输入、输出波特率

需要设置好termios中的参数.

实例(回环)

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

/* set_opt(fd,115200,8,'N',1) */
int set_opt(int fd,int nSpeed, int nBits, char nEvent, int nStop)
{
	struct termios newtio,oldtio;
	
	if ( tcgetattr( fd,&oldtio) != 0) { //获得驱动程序中默认的参数
		perror("SetupSerial 1");
		return -1;
	}
	//清除 使用原始模式
	bzero( &newtio, sizeof( newtio ) );
	newtio.c_cflag |= CLOCAL | CREAD; 
	newtio.c_cflag &= ~CSIZE; 

	newtio.c_lflag  &= ~(ICANON | ECHO | ECHOE | ISIG);  /*Input*/
	newtio.c_oflag  &= ~OPOST;   /*Output*/

	switch( nBits )
	{
	case 7:
		newtio.c_cflag |= CS7;
	break;
	case 8:
		newtio.c_cflag |= CS8;
	break;
	}

	switch( nEvent )
	{
	case 'O':
		newtio.c_cflag |= PARENB;
		newtio.c_cflag |= PARODD;
		newtio.c_iflag |= (INPCK | ISTRIP);
	break;
	case 'E': 
		newtio.c_iflag |= (INPCK | ISTRIP);
		newtio.c_cflag |= PARENB;
		newtio.c_cflag &= ~PARODD;
	break;
	case 'N': 
		newtio.c_cflag &= ~PARENB;
	break;
	}

	switch( nSpeed )
	{
	case 2400:
		cfsetispeed(&newtio, B2400);
		cfsetospeed(&newtio, B2400);
	break;
	case 4800:
		cfsetispeed(&newtio, B4800);
		cfsetospeed(&newtio, B4800);
	break;
	case 9600:
		cfsetispeed(&newtio, B9600);
		cfsetospeed(&newtio, B9600);
	break;
	case 115200:
		cfsetispeed(&newtio, B115200);
		cfsetospeed(&newtio, B115200);
	break;
	default:
		cfsetispeed(&newtio, B9600);
		cfsetospeed(&newtio, B9600);
	break;
	}
	
	if( nStop == 1 )
		newtio.c_cflag &= ~CSTOPB;
	else if ( nStop == 2 )
		newtio.c_cflag |= CSTOPB;
	
	newtio.c_cc[VMIN]  = 1;  /* 读数据时的最小字节数: 没读到这些数据我就不返回! */
	newtio.c_cc[VTIME] = 0; /* 等待第1个数据的时间: 
	                         * 比如VMIN设为10表示至少读到10个数据才返回,
	                         * 但是没有数据总不能一直等吧? 可以设置VTIME(单位是10秒)
	                         * 假设VTIME=1,表示: 
	                         *    10秒内一个数据都没有的话就返回
	                         *    如果10秒内至少读到了1个字节,那就继续等待,完全读到VMIN个数据再返回
	                         */

	tcflush(fd,TCIFLUSH);
	
	if((tcsetattr(fd,TCSANOW,&newtio))!=0)
	{
		perror("com set error");
		return -1;
	}
	//printf("set done!\n");
	return 0;
}

int open_port(char *com)
{
	int fd;
	//fd = open(com, O_RDWR|O_NOCTTY|O_NDELAY);
	fd = open(com, O_RDWR|O_NOCTTY);//打开某个设备节点  O_NOCTTY:程序打开串口之后不要把这个串口用作控制终端(serial连接就是控制终端,输入字符转换成某些信号去控制)
    if (-1 == fd){
		return(-1);
    }
	
    //fcntl(fd,F_SETEL, FNDELAY); 读数据时不等待, 没有数据就返回0
	  if(fcntl(fd, F_SETFL, 0)<0) /* 设置串口为阻塞状态*/ //读写数据不成功就会休眠
	  {
			printf("fcntl failed!\n");
			return -1;
	  }
  
	  return fd;
}


/*
 * ./serial_send_recv (设备节点)
 */
int main(int argc, char **argv)
{
	int fd;
	int iRet;
	char c;

	/* 1. open *///打开

	/* 2. setup 
	 * 115200,8N1
	 * RAW mode
	 * return data immediately
	 *///设置

	/* 3. write and read */
	
	if (argc != 2)
	{
		printf("Usage: \n");
		printf("%s \n", argv[0]);
		return -1;
	}

	fd = open_port(argv[1]);
	if (fd < 0)
	{
		printf("open %s err!\n", argv[1]);
		return -1;
	}

	iRet = set_opt(fd, 115200, 8, 'N', 1);//设置参数波特率11520 数据位8 不用校验位 停止位1
	if (iRet)
	{
		printf("set port err!\n");
		return -1;
	}

	printf("Enter a char: ");
	while (1)
	{
		scanf("%c", &c);
		iRet = write(fd, &c, 1);
		iRet = read(fd, &c, 1);
		if (iRet == 1)
			printf("get: %02x %c\n", c, c);
		else
			printf("can not get data\n");
	}

	return 0;
}

读数据时的最小字节数设置为0: 为什么第一遍打印俩遍错误呢?

输入a, 将a保存在行规程, 回车键继续发送至行规程, 才会去唤醒应用程序, scanf被唤醒得到了a, 把a发给另一个串口, 串口1开始接收(一位一位), 所以write发送了,但是read没有接受, 返回0, 第二个字符时enter, 又没有读到数据, 输入字符b, read读到a(行规程里第一个数据)

主要问题是write确实可以发送出去, 但是串口没有得到数据呢(没有上报给驱动程序), read返回0, 所以要设置读到最小字节为1, 没有数据的话永远等待(等待时间设置)

I2C

I2C框架

嵌入式基础_第23张图片

一个芯片里面有一个或者多个I2C控制器, 一个I2C控制器上面可以挂载一个或者多个I2C设备, 访问这些设备需要去寻址, 所以使用I2C总线来操作设备时, 首先得知道设备的设备地址, 知道后就可以跟他收发数据了.

I2C总线只需要2条线:时钟线SCL、数据线SDA. 在I2C总线的SCL、SDA线上,都有上拉电阻

嵌入式基础_第24张图片

APP(open/read/write) 设备驱动程序(例如EEPROM存储设备 TS触摸屏设备) 控制驱动程序

协议

控芯片引出两条线SCL(SCK),SDA线,在一条I2C总线上可以接很多I2C设备,我们还会放一个上拉电阻

简单的例子,来解释一下IIC的传输协议:
	老师说开始了,表示开始信号(start)
	老师提醒某个学生要发球,表示发送地址和方向(address/read/write)
	老师发球/接球,表示数据的传输
	收到球要回应:回应信号(ACK)
	老师说结束,表示IIC传输结束(P)

写操作

主芯片要发出一个start信号

然后发出一个设备地址(用来确定是往哪一个芯片写数据),方向(读/写,0表示写,1表示读)

从设备回应(用来确定这个设备是否存在),然后就可以传输数据

主设备发送一个字节数据给从设备,并等待回应

每传输一字节数据,接收方要有一个回应信号(确定数据是否接受完成),然后再传输下一个数据。

数据发送完之后,主芯片就会发送一个停止信号。

下图:白色背景表示"主→从",灰色背景表示"从→主"

嵌入式基础_第25张图片

读操作

主芯片要发出一个start信号

然后发出一个设备地址(用来确定是往哪一个芯片写数据),方向(读/写,0表示写,1表示读)

从设备回应(用来确定这个设备是否存在),然后就可以传输数据

从设备发送一个字节数据给主设备,并等待回应

每传输一字节数据,接收方要有一个回应信号(确定数据是否接受完成),然后再传输下一个数据。

数据发送完之后,主芯片就会发送一个停止信号。

下图:白色背景表示"主→从",灰色背景表示"从→主"

嵌入式基础_第26张图片

信号

I2C协议中数据传输的单位是字节,也就是8位。但是要用到9个时钟:前面8个时钟用来传输8数据,第9个时钟用来传输回应信号。传输时,先传输最高位(MSB)。

开始信号(S):SCL为高电平时,SDA山高电平向低电平跳变,开始传送数据。

结束信号(P):SCL为高电平时,SDA由低电平向高电平跳变,结束传送数据。

响应信号(ACK):接收器在接收到8位数据后,在第9个时钟周期,拉低SDA

SDA上传输的数据必须在SCL为高电平期间保持稳定,SDA上的数据只能在SCL为低电平期间变化

嵌入式基础_第27张图片

细节

如何在SDA上实现双向传输?
主芯片通过一根SDA线既可以把数据发给从设备,也可以从SDA上读取数据,连接SDA线的引脚里面必然有两个引脚(发送引 脚/接受引脚)。

主、从设备都可以通过SDA发送数据,肯定不能同时发送数据,怎么错开时间?
在9个时钟里,
前8个时钟由主设备发送数据的话,第9个时钟就由从设备发送数据;
前8个时钟由从设备发送数据的话,第9个时钟就由主设备发送数据。

双方设备中,某个设备发送数据时,另一方怎样才能不影响SDA上的数据?
设备的SDA中有一个三极管,使用开极/开漏电路(三极管是开极,CMOS管是开漏,作用一样),如下图:

嵌入式基础_第28张图片
当某一个芯片不想影响SDA线时,那就不驱动这个三极管
想让SDA输出高电平,双方都不驱动三极管(SDA通过上拉电阻变为高电平)
想让SDA输出低电平,就驱动三极管

举例(双向传输) 主设备发送(8bit)给从设备

  • 前8个clk

    从设备不要影响SDA,从设备不驱动三极管

    主设备决定数据,主设备要发送1时不驱动三极管,要发送0时驱动三极管

  • 第9个clk,由从设备决定数据

    主设备不驱动三极管

    从设备决定数据,要发出回应信号的话,就驱动三极管让SDA变为0

    从这里也可以知道ACK信号是低电平

从上面的例子,就可以知道怎样在一条线上实现双向传输,这就是SDA上要使用上拉电阻的原因。

为何SCL也要使用上拉电阻?
在第9个时钟之后,如果有某一方需要更多的时间来处理数据,它可以一直驱动三极管把SCL拉低。
当SCL为低电平时候,大家都不应该使用IIC总线,只有当SCL从低电平变为高电平的时候,IIC总线才能被使用。
当它就绪后,就可以不再驱动三极管,这是上拉电阻把SCL变为高电平,其他设备就可以继续使用I2C总线了。

对于IIC协议它只能规定怎么传输数据,数据是什么含义由从设备决定。

SMBus协议

SMBus: System Management Bus,系统管理总线。

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

跟一般的I2C协议的差别

 VDD的极限值不一样
I2C协议:范围很广,甚至讨论了高达12V的情况
SMBus:1.8V~5V
 最小时钟频率、最大的Clock Stretching
Clock Stretching含义:某个设备需要更多时间进行内部的处理时,它可以把SCL拉低占住I2C总线
I2C协议:时钟频率最小值无限制,Clock Stretching时长也没有限制
SMBus:时钟频率最小值是10KHz,Clock Stretching的最大时间值也有限制
 地址回应(Address Acknowledge)
一个I2C设备接收到它的设备地址后,是否必须发出回应信号?
I2C协议:没有强制要求必须发出回应信号
SMBus:强制要求必须发出回应信号,这样对方才知道该设备的状态:busy,failed,或是被移除了

 REPEATED START Condition(重复发出S信号)
比如读EEPROM时,涉及2个操作:
把存储地址发给设备 and 读数据
在写、读之间,可以不发出P信号,而是直接发出S信号:这个S信号就是REPEATED START
嵌入式基础_第29张图片

 SMBus协议明确了数据的传输格式
I2C协议:它只定义了怎么传输数据,但是并没有定义数据的格式,这完全由设备来定义
SMBus:定义了几种数据格式(下面分析)

I2C Block Write 和 I2C SMBus Block Write 的区别L: 传输过程会不会传输Byte Count值

I2C系统重要结构体

使用一句话概括I2C传输:APP通过I2C Controller与I2C Device传输数据。

在Linux中:

  • 怎么表示I2C Controller

    • 一个芯片里可能有多个I2C Controller,比如第0个、第1个、……

    • 对于使用者,只要确定是第几个I2C Controller即可

    • 使用i2c_adapter表示一个I2C BUS,或称为I2C Controller(使用面向对象的方式)

    • 里面有2个重要的成员:

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

      嵌入式基础_第30张图片
    • i2c_algorithm

嵌入式基础_第31张图片
  • 怎么表示I2C Device

    • 一个I2C Device,一定有设备地址

    • 它连接在哪个I2C Controller上,即对应的i2c_adapter是什么

    • 使用i2c_client来表示一个I2C Device

    嵌入式基础_第32张图片
  • 怎么表示要传输的数据

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

    • i2c_msg

      嵌入式基础_第33张图片
    • i2c_msg中的flags用来表示传输方向:bit 0等于I2C_M_RD表示读,bit 0等于0表示写

    • 一个i2c_msg要么是读,要么是写

    • 举例:设备地址为0x50的EEPROM,要读取它里面存储地址为0x10的一个字节,应该构造几个i2c_msg?

      • 要构造2个i2c_msg

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

      • 第二个i2c_msg表示读操作

      • 代码如下

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;

内核怎么传输数据 使用一句话概括I2C传输:

  • APP通过I2C Controller与I2C Device传输数据

  • APP通过i2c_adapter与i2c_client传输i2c_msg

  • 内核函数i2c_transfer

    • i2c_msg里含有addr,所以这个函数里不需要i2c_client

      int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)

无需编写驱动直接访问设备_I2C-Tools介绍

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

嵌入式基础_第34张图片

使用一句话概括I2C传输:APP通过I2C Controller与I2C Device传输数据。

所以使用I2C-Tools时也需要指定:

  • 哪个I2C控制器(或称为I2C BUS、I2C Adapter)

  • 哪个I2C设备(设备地址)

  • 数据:读还是写、数据本身

AP3216C是红外、光强、距离三合一的传感器,以读出光强、距离值为例,步骤如下:
复位:往寄存器0写入0x4
使能:往寄存器0写入0x3
读光强:读寄存器0xC、0xD得到2字节的光强
读距离:读寄存器0xE、0xF得到2字节的距离值
AP3216C的设备地址是0x1E,假设节在I2C BUS0上,操作命令如下:
i2cdetect 
查看当前i2c设备
用法: i2cdetect 0 或者 i2cdetect -l
使用SMBus协议
i2cset -f -y 0 0x1e 0 0x4 
i2cset -f -y 0 0x1e 0 0x3 
i2cget -f -y 0 0x1e 0xc w 
i2cget -f -y 0 0x1e 0xe w 
使用I2C协议
i2ctransfer -f -y 0 w2@0x1e 0 0x4 
i2ctransfer -f -y 0 w2@0x1e 0 0x3 
i2ctransfer -f -y 0 w1@0x1e 0xc r2 
i2ctransfer -f -y 0 w1@0x1e 0xe r2

访问I2C设备的俩种方式

I2C-Tools可以通过SMBus来访问I2C设备,也可以使用一般的I2C协议来访问I2C设备。

使用一句话概括I2C传输:APP通过I2C Controller与I2C Device传输数据。

在APP里,有这几个问题:

  • 怎么指定I2C控制器?

    • i2c-dev.c提供为每个I2C控制器(I2C Bus、I2C Adapter)都生成一个设备节点:/dev/i2c-0、/dev/i2c-1等待

    • open某个/dev/i2c-X节点,就是去访问该I2C控制器下的设备

  • 怎么指定I2C设备?

    • 通过ioctl指定I2C设备的地址

    • ioctl(fifile, I2C_SLAVE, address)

      • 如果该设备已经有了对应的设备驱动程序,则返回失败
    • ioctl(fifile, I2C_SLAVE_FORCE, address)

      • 如果该设备已经有了对应的设备驱动程序

      • 但是还是想通过i2c-dev驱动来访问它

      • 则使用这个ioctl来指定I2C设备地址

  • 怎么传输数据?

    • 两种方式

    • 一般的I2C方式:ioctl(fifile, I2C_RDWR, &rdwr)

    • SMBus方式:ioctl(fifile, I2C_SMBUS, &args)

源码

i2c方式

嵌入式基础_第35张图片

SMBus方式

嵌入式基础_第36张图片
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "i2cbusses.h"
#include 


/* ./at24c02  w "100ask.taobao.com"
 * ./at24c02  r
 */

int main(int argc, char **argv)
{
	unsigned char dev_addr = 0x50;//设备地址
	unsigned char mem_addr = 0;//存储空间地址
	unsigned char buf[32];

	int file;
	char filename[20];
	unsigned char *str;

	int ret;

	struct timespec req;
	
	if (argc != 3 && argc != 4)
	{
		printf("Usage:\n");
		printf("write eeprom: %s  w string\n", argv[0]);
		printf("read  eeprom: %s  r\n", argv[0]);
		return -1;
	}

	file = open_i2c_dev(argv[1][0]-'0', filename, sizeof(filename), 0);
	if (file < 0)
	{
		printf("can't open %s\n", filename);
		return -1;
	}

	if (set_slave_addr(file, dev_addr, 1))
	{
		printf("can't set_slave_addr\n");
		return -1;
	}

	if (argv[2][0] == 'w')
	{
		// write str: argv[3]
		str = argv[3];

		req.tv_sec  = 0;
		req.tv_nsec = 20000000; /* 20ms */
		
		while (*str)
		{
			// mem_addr, *str
			// mem_addr++, str++
			ret = i2c_smbus_write_byte_data(file, mem_addr, *str);
			if (ret)
			{
				printf("i2c_smbus_write_byte_data err\n");
				return -1;
			}
			// wait tWR(10ms)
			nanosleep(&req, NULL);
			
			mem_addr++;
			str++;
		}
		ret = i2c_smbus_write_byte_data(file, mem_addr, 0); // string end char
		if (ret)
		{
			printf("i2c_smbus_write_byte_data err\n");
			return -1;
		}
	}
	else
	{
		// read
		ret = i2c_smbus_read_i2c_block_data(file, mem_addr, sizeof(buf), buf);
		if (ret < 0)
		{
			printf("i2c_smbus_read_i2c_block_data err\n");
			return -1;
		}
		
		buf[31] = '\0';
		printf("get data: %s\n", buf);
	}
	
	return 0;
	
}

你可能感兴趣的:(小白探险之旅,ubuntu,linux,嵌入式,imx6ull)