最近学习linux,嵌入式硬件中,树莓派是比较好的一个linux开发和学习平台,记下来进行总结

目录

树莓派学习

Linux目录

显著区别之一就是其不同的目录结构,并不仅仅是格式上的不同,而是不同位置上保存的东西区别很大。

在Windows中,典型的路径可能是这样的 D:\Folder\subfolder\file.txt,而在Linux中,路径则是这样的 /Folder/subfolder/file.txt。

斜线倾斜的方向不同,并且,在Linux中,也没有C盘D盘的概念,Linux系统启动之后,根分区 就”挂载”在了在了 / 的位置,并且所有的文件、文件夹、设备以及不同的硬盘光驱之类的,也都挂载在了 /。

虽然可能在下面这个例子中并不明显,但是Linux系统对文件或者文路径的名称中的大小写字符是敏感的。

比如 /Folder/subfolder/file.txt 与 /folder/subfolder/file.txt并不是同一个文件。

Linux系统目录说明

Linux的目录结构是一个统一的目录结构,所有的目录和文件最终都统一到”/“根文件系统下。文件系统是无论是不是挂载过来的,最终都分层排列到以”/“为起始的文件系统之下。 Linux目录结构遵循”文件系统层次结构(Filesystem Hierarchy Structure,FHS)”,这标准是由“自由标准组织(Free Standards Group)”进行维护的,然而大多数LINUX发行版都有意或者无意的与这一规范背离。

“/” 根路径

这是Linux系统的“根”目录,也是所有目录结构的最底层。在UNIX以及和它兼容的系统中,”/“是一个单独的目录。

/boot

这个目录下包含系统启动文件(boot loader),例如Grub,Lilo或者Kernel,以及initrd,system.map等配置文件。

Initrd ramdisk或者””initrd””是指一个临时文件系统,它在启动阶段被Linux内核调用。initrd主要用于当“根”文件系统被挂载之前,进行准备工作。

/boot下挂载了FAT32格式的启动分区,里面的文件用于树莓派的开机启动。

计算机启动是一个神秘而有趣的过程,先来看计算机常见的启动方式。

当我们打开一台普通计算机的电源时,计算机一般会自动从主板的 BIOS上读取其中所存储的程序。BIOS知道直接连接在主板上的硬件。 它从默认存储设备中读取最开始512字节的数据,即所谓的 MBR(Master Boot Record)。用户也可以通过BIOS配置,从其他数据 存储设备中找到MBR。通过MBR,计算机知道要从该存储设备的哪个分区来找引导加载程序。引导加载程序储存有操作系统的相关信息,比 如操作系统名称、内核所在位置等。随后引导加载程序加载内核,操作 系统开始工作。

树莓派的开机方式有别于普通计算机。树莓派是一块集成电路板, 没有主板,也没有BIOS。树莓派电路板上携带着启动程序。板载启动 程序会挂载FAT32的启动分区,并运行其中的引导程序bootcode.bin。它 负责下一阶段的启动工作,会从SD卡上找到GPU固件start.elf,将固件 写入GPU。GPU在start.elf的指挥下,会读取系统配置文件config.txt和内核配置文件cmdline.txt,并启动内核文件kernel.img。当内核加载成功时,处理器开始工作,系统启动正式开始。

普通计算机的启动流程有更多可选项。而树莓派通过板载程序来固 化启动流程,让启动更可控。但无论是哪种开机流程,我们看到操作系 统是通过小程序加载大程序,并相继唤醒硬件的过程。内核加载成功之后,操作系统正式开始工作。Linux系统还会进行一系列的准备工作, 来让操作系统更好用。

内核会首先预留运行所需的内存空间,然后通过驱动程序检测计算 机硬件。这样,操作系统就可以知道自己有哪些硬件可用。随后,内核 会启动init进程。到此,内核完成了启动阶段的工作。进程init会运行一 系列初始脚本,这些脚本用于准备操作系统。

· 设置计算机名称、时区等。

· 检测存储器。

· 挂载存储器。

· 清空临时文件。

· 设置网络。

· 启动其他后台进程。

这些初始脚本运行完毕,操作系统就准备好了,只是,还没有人可 以登录。进程init会给出登录对话框,或是图形化的登录界面。登录之 后,就是Shell或图形化的用户界面了。

/sys

这个目录下包含内核、固件以及系统相关文件。

/sbin

保存了系统启动、修复和恢复所必需的应用程序

包含系统操作和运作所必需的二进制文件以及管理工具,主要就是可执行文件。类似WINDOWS下的EXE文件。

/bin

这里保存着Linux系统运行必须的应用程序。

包含单用户模式下的二进制文件以及工具程序,比如cat,ls,cp这些命令。

bash cat chmod cp date echo expr kill ln ls mkdir mv pwd rm rmdir sleep test unlink 

fdisk  hwclock ifconfig reboot shutdown

/lib

包含/sbin和/bin目录下二进制文件运行所需要的库文件。

/dev

内含必需的系统文件和驱动器。 /dev目录中保存着设备文件。

每个设备文件对应着一个设备,比如 存储器和UART接口。通过这些设备文件,设备还可以是没有硬件实物的虚拟设备,比如终端。我们可以以文件操作的形式直接和设备进行交流,通过读写/dev/ttyAMA0来与UART接口通信。

Linux的设备有主编号(Major Number)和副编号(Minor Number)。主编号说明了设备的类型,在/dev中对应为一个名字,比 如”ttyAMA”。副编号就是后面跟的”0”,即该类型下编号为0的设备。通过man命令来找出某种设备的主编号,比如:

man 4 ttysAMA

/root

/root是root的用户目录。该目录文件的拥有者和拥有组都是root。

/etc

内含系统配置文件,其下的目录,比如 /etc/hosts, /etc/resolv.conf, nsswitch.conf, 以及系统缺省设置,网络配置文件等等。以及一些系统和应用程序的配置文件。

/etc中有很多配置文件。这些配置文件可以影响系统和应用程序的行为。之前已经见过很多/etc下的配置文件,借助这些已经见过的文件来说明/etc下文件的类型。

/etc保存着关键的操作系统配置文件,这些配置文件可以改变操作系统级别的行为,例如

路径 功能 备注
/etc/default/local 本地设置,入语言,字符编码
/etc/default/keyboard 键盘设置
/etc/localtime 时间和时区配置
/etc/moudles 可加载模块配置

操作系统启动时的init进程及init调用的脚本也在/etc下。这些脚本在 启动阶段执行,并最终决定呈现给我们的操作系统。路径与功能如下所示。

路径 功能 备注
/etc/init.d/ 初始化相关文件
/etc/rc.local 初始化脚本

我们在Linux用户中看到,/etc保存着用户和用户组的相关信息。增 加或删除用户的操作,实际上就是修改这些文件,如下所示。

路径 功能 备注
/etc/passwd 用户列表
/etc/group 用户组列表
/etc/passwd 用户密码

上面提到的配置文件都是操作系统级别的。/etc不仅有操作系统级 别的配置文件,还包括了应用程序的配置文件。这些配置文件是全局的,对所有用户都有效,如下所示。

/etc/motion/motion.conf motion的配置文件

/etc/apt/sources.list apt-get软件源配置

/etc/wpa_supplicant/wpa_supplicant.conf wifi setting

应用程序也可以有自己的初始化脚本

路径 功能 备注
/etc/bashrc motion 初始化设置
/etc/nanorc nano初始化设置
/etc/virc vi初始化设置

/home

每一个用户的在这个目录下,都会单独有一个以其用户名命令的目录,在这里保存着用户的个人设置文件,尤其是以 profile结尾的文件。但是也有例外,root用户的数据就不在这个目录中,而是单独在根路径下,保存在单独的/root文件夹下。

/media

一个给所有可移动设备比如光驱、USB外接盘、软盘提供的常规挂载点。

Linux下的/mnt用于挂载额外的文件系统,比如网络硬盘、光驱和 额外的硬盘。对于/mnt下的存储设备,通常要手动挂载或者在挂载文件 里增加对应条目。

/mnt

临时文件系统挂载点。比如,你并不想长期挂载某个驱动器,而是只是临时挂载一会U盘烤个MP3之类的,那么应该挂载在这个位置下。

Linux下的/mnt用于挂载额外的文件系统,比如网络硬盘、光驱和 额外的硬盘。对于/mnt下的存储设备,通常要手动挂载或者在挂载文件 里增加对应条目。

/opt

在Linux系统中,这个目录用到的并不多,opt是 可选系统程序包(Optional Software Packages)的简称。这个目录在UNIX系统,如Sun Solaris用途要广泛的多。

/usr

用户数据目录,包含了属于用户的实用程序和应用程序。这里有很多重要的,但并非关键的文件系统挂载这个路径下面。在这里,你会重新找到一个 bin、sbin 和 lib目录,其中包含非关键用户和系统二进制文件以及相关的库和共享目录,以及一些库文件。

/usr/sbin

包含系统中非必备和并不是特别重要的系统二进制文件以及网络应用工具。

同理,/usr/sbin保存的也是次要的系统维护程序,比如任务规划程序cron

/usr/bin

包含用户的非必备和并不是特别重要的二进制文件。

/usr/bin保存了次要一些的应用程序。在大多数Linux发行版本中,/usr/bin中包含的应用 程序比/bin中多得多。Raspbian中apt-get安装的程序,大多也会出现在这里。虽然/usr/bin程序对于Linux程序的运行不是那么关键,但很可能是常用的应用程序,比如文本编辑程序、编译器、数据库等

cut diff free gcc head locate man make nano nice sftp ssh tail uniq vi wc which who

/usr/lib

保存着/usr/sbin以及/usr/bin中二进制文件所需要的库文件。

/usr/share

“平台无关”的共享数据目录。

/usr/local

是/usr下的二级目录,这里主要保存着包含系统二进制文件以及运行库在内的本地系统数据。

其中,/usr/local/bin和/usr/local/sbin也是保存应用程序的地方。 这里通常保存了用户自己编写或手动编译安装的应用程序。

/var

这个路径下通常保存着包括系统日志、打印机后台文件(spool files)、定时任务(crontab)、邮件、运行进程、进程锁文件等。这个目录尤其需要注意进行日常的检查和维护,因为这个目录下文件的大小可能会增长很快,以致于很快占满硬盘,然后导致系统便会出现各种奇奇怪怪的问题。

/var用于保存系统中会动态增长的数据,比如/var/log下的系统日志和应用程序日志。此外,每个应用程序也会产生动态增长的数据。就拿 邮件程序来说,其可执行文件是一个大小不变的静态文件,但电子邮件的相关文字和图片会随着用户使用快速增长。

因此,电子邮件常常归档 保存在/var下。缓存数据占据的空间经常浮动变化,因此也保存在/var 下。由于/var的动态变化性,它经常挂载有独立的存储器。

/tmp

顾名思义,这是一个临时文件夹,专门用来保存临时文件,每次系统重启之后,这个目录下的”临时”文件便会被清空。同样,/var/tmp 也同样保存着临时文件。两者唯一的不同是,后者 /var/tmp目录保存的文件会受到系统保护,系统重启之后这个目录下的文件也不会被清空。

应用程序运行的过程中可能会有一些临时数据需要保存到文件系统 中,比如数学运算的中间结果。如果应用程序不想持久保存这些文件, 就会把这些文件放在/tmp文件下。因为应用程序可能依赖这些临时文 件,所以随意修改/tmp下的文件可能造成应用程序的崩溃。幸好,/tmp 下的文件会自动清空,因此/tmp下的文件基本不需要维护。不同版本的 Linux系统会选择不同的时间来清空临时文件。Raspbian会在开机后清 空/tmp文件夹。由于临时文件的增长很难预知,因此/tmp也经常位于额 外的存储器中,以免临时文件和系统文件竞争空间

/proc

这个目录是驻留在系统内存中的虚拟(psuedo,伪)文件系统,其中保存的都是文本格式的系统内核和进程信息。

内核直接管理的硬件信息可以在/proc下查询。/proc其实是一个虚 拟文件系统,直接对应了内存上的内核空间。

通过/proc,内核给用户提 供了一个查询内核信息的简易窗口。

/proc/cpuinfo中保存着CPU信息。

/proc/meminfo中保存着内存使用信息。

因为内核直接管理的设备对于计算机运行至关重要,所以/proc下的文件大多是只读的,不允许用户 直接进行写入操作。内核还保存着进程的信息。这些原本在内核空间的 信息也以文件的形式呈现在/proc目录下。

LINUX系统目录结构图

tu1

需要注意的是,不同LINUX发行版本的目录结构会有一些差异,这对LINUX新手来说比较纠结,但是大体上,所以LINUX的不同发行版本,都符合上面这幅图片中的路径结构。

树莓派gpio结构

下面的图表展示了3B树莓派的GPIO连接器的针脚,有的40针。

tu2

管脚功能如下: tu3

其中每一个针脚都有Pin#和NAME字段。Pin代表的是该针脚的编号,其中01和02针脚对应第一张图中GPIO最右边竖排的两个针脚。而NAME代表的是该针脚的BCM名称,当然NAME也可以直接看得出针脚的默认功能。比如 3.3v和5v代表着该针脚会输出3.3v和5v的电压,Ground代表着该针脚是接地的,GPIO0*则是一些待用户开发的针脚。每个针脚都可以使用程序进行控制操作。

控制GPIO

想用python来控制GPIO,最便捷的办法就是使用一些python类库,比如树莓派系统本身集成的RPi.GPIO。

导入RPi.GPIO模块

可以用下面的代码导入RPi.GPIO模块。

import RPi.GPIO as GPIO

引入之后,就可以使用GPIO模块的函数了。如果你想检查模块是否引入成功,也可以这样写:

try:
    import RPi.GPIO as GPIO
except RuntimeError:
    print("引入错误")

针脚编号

在RPi.GPIO中,同时支持树莓派上的两种GPIO引脚编号。第一种编号是BOARD编号,这和树莓派电路板上的物理引脚编号相对应。使用这种编号的好处是,你的硬件将是一直可以使用的,不用担心树莓派的版本问题。因此,在电路板升级后,你不需要重写连接器或代码。

第二种编号是BCM规则,是更底层的工作方式,它和Broadcom的片上系统中信道编号相对应。在使用一个引脚时,你需要查找信道号和物理引脚编号之间的对应规则。对于不同的树莓派版本,编写的脚本文件也可能是无法通用的。

可以使用下列代码(强制的)指定一种编号规则:

GPIO.setmode(GPIO.BOARD)
  # or
GPIO.setmode(GPIO.BCM)

下面代码将返回被设置的编号规则

mode = GPIO.getmode()
  • 警告 如果RPi.GRIO检测到一个引脚已经被设置成了非默认值,那么你将看到一个警告信息。你可以通过下列代码禁用警告:

    GPIO.setwarnings(False)
    

引脚设置

在使用一个引脚前,你需要设置这些引脚作为输入还是输出。配置一个引脚的代码如下:

# 将引脚设置为输入模式
GPIO.setup(channel, GPIO.IN)

# 将引脚设置为输出模式
GPIO.setup(channel, GPIO.OUT)

# 为输出的引脚设置默认值
GPIO.setup(channel, GPIO.OUT, initial=GPIO.HIGH)

释放

一般来说,程序到达最后都需要释放资源,这个好习惯可以避免偶然损坏树莓派。释放脚本中的使用的引脚:

GPIO.cleanup()

注意,GPIO.cleanup()只会释放掉脚本中使用的GPIO引脚,并会清除设置的引脚编号规则。

输出

要想点亮一个LED灯,或者驱动某个设备,都需要给电流和电压他们,这个步骤也很简单,设置引脚的输出状态就可以了,代码如下:

GPIO.output(channel, state)

状态可以设置为0 / GPIO.LOW / False / 1 / GPIO.HIGH / True。如果编码规则为,GPIO.BOARD,那么channel就是对应引脚的数字。

如果想一次性设置多个引脚,可使用下面的代码:

chan_list = [11,12]
GPIO.output(chan_list, GPIO.LOW)
GPIO.output(chan_list, (GPIO.HIGH, GPIO.LOW))   

你还可以使用Input()函数读取一个输出引脚的状态并将其作为输出值,例如:

GPIO.output(12, not GPIO.input(12))

读取

我们也常常需要读取引脚的输入状态,获取引脚输入状态如下代码:

GPIO.input(channel)

低电平返回0 / GPIO.LOW / False,高电平返回1 / GPIO.HIGH / True。

如果输入引脚处于悬空状态,引脚的值将是漂动的。换句话说,读取到的值是未知的,因为它并没有被连接到任何的信号上,直到按下一个按钮或开关。由于干扰的影响,输入的值可能会反复的变化。 使用如下代码可以解决问题:

GPIO.setup(channel, GPIO.IN, pull_up_down=GPIO.PUD_UP)
  # or
GPIO.setup(channel, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

需要注意的是,上面的读取代码只是获取当前一瞬间的引脚输入信号。

如果需要实时监控引脚的状态变化,可以有两种办法。最简单原始的方式是每隔一段时间检查输入的信号值,这种方式被称为轮询。如果你的程序读取的时机错误,则很可能会丢失输入信号。轮询是在循环中执行的,这种方式比较占用处理器资源。另一种响应GPIO输入的方式是使用中断(边缘检测),这里的边缘是指信号从高到低的变换(下降沿)或从低到高的变换(上升沿)。

  1. 轮询方式

    while GPIO.input(channel) == GPIO.LOW:
    time.sleep(0.01)  # wait 10 ms to give CPU chance to do other things
    
  2. 边缘检测 边缘是指信号状态的改变,从低到高(上升沿)或从高到低(下降沿)。通常情况下,我们更关心于输入状态的该边而不是输入信号的值。这种状态的该边被称为事件。 先介绍两个函数:

wait_for_edge() 函数。

wait_for_edge()被用于阻止程序的继续执行,直到检测到一个边沿。也就是说,上文中等待按钮按下的实例可以改写为:

channel = GPIO.wait_for_edge(channel, GPIO_RISING, timeout=5000)
if channel is None:
    print('Timeout occurred')
else:
    print('Edge detected on channel', channel)

add_event_detect() 函数

该函数对一个引脚进行监听,一旦引脚输入状态发生了改变,调用event_detected()函数会返回true,如下代码:

GPIO.add_event_detect(channel, GPIO.RISING)  # add rising edge detection on a channel
do_something()
// 下面的代码放在一个线程循环执行。
if GPIO.event_detected(channel):
    print('Button pressed')

上面的代码需要自己新建一个线程去循环检测event_detected()的值,还算是比较麻烦的。

不过可采用另一种办法轻松检测状态,这种方式是直接传入一个回调函数:

def my_callback(channel):
    print('This is a edge event callback function!')
    print('Edge detected on channel %s'%channel)
    print('This is run in a different thread to your main program')

GPIO.add_event_detect(channel, GPIO.RISING, callback=my_callback)

如果你想设置多个回调函数,可以这样:

def my_callback_one(channel):
    print('Callback one')

def my_callback_two(channel):
    print('Callback two')

GPIO.add_event_detect(channel, GPIO.RISING)
GPIO.add_event_callback(channel, my_callback_one)
GPIO.add_event_callback(channel, my_callback_two)

注意:回调触发时,并不会同时执行回调函数,而是根据设置的顺序调用它们。

综合例子:点亮LED灯

好了,上面说明了一大堆函数库的用法,那么现在就应该来个简单的实验了。这个实验很简单,点亮一个LED灯。

编写代码之前,首先你需要将led灯的针脚通过杜邦线连接到树莓派的引脚上,比如你可以连接到11号引脚。 新建一个main.py文件,写入如下代码:

import RPi.GPIO as GPIO  //引入函数库
import time

RPi.GPIO.setmode(GPIO.BOARD)  //设置引脚编号规则
RPi.GPIO.setup(11, RPi.GPIO.OUT)    //将11号引脚设置成输出模式

while True
    GPIO.output(channel, 1)   //将引脚的状态设置为高电平,此时LED亮了
    time.sleep(1)   //程序休眠1秒钟,让LED亮1秒
    GPIO.output(channel, 0)   //将引脚状态设置为低电平,此时LED灭了
    time.sleep(1)   //程序休眠1秒钟,让LED灭1秒

GPIO.cleanup()    //程序的最后别忘记清除所有资源

保存,并退出文件。执行python3 main.py,即可观看效果。Ctrl+C可以关闭程序。 此外,不妨也试试其它的函数吧,增强印象。 使用PWM 这个python函数库还支持PWM模式的输出,我们可以利用PWM来制作呼吸灯效果。详情看代码:

import time
import RPi.GPIO as GPIO   //引入库
GPIO.setmode(GPIO.BOARD)  //设置编号方式
GPIO.setup(12, GPIO.OUT)  //设置12号引脚为输出模式

p = GPIO.PWM(12, 50)  //将12号引脚初始化为PWM实例 ,频率为50Hz
p.start(0)    //开始脉宽调制,参数范围为: (0.0 <= dc <= 100.0)
try:
    while 1:
        for dc in range(0, 101, 5):
            p.ChangeDutyCycle(dc)   //修改占空比 参数范围为: (0.0 <= dc <= 100.0)
            time.sleep(0.1)
        for dc in range(100, -1, -5):
            p.ChangeDutyCycle(dc)
            time.sleep(0.1)
except KeyboardInterrupt:
    pass
p.stop()    //停止输出PWM波
GPIO.cleanup()    //清除

结语

利用树莓派,学习linux,从我做起,加油鸭!!!