嵌入式系统开发与应用
嵌入式系统开发与应用
基本概念
可追溯到 20 世纪 60 年代
嵌入式系统是以应用为中心,以计算机技术为基础,并且软硬件可剪裁,适用于对功能、可靠性等有严格要求的 专用计算机系统。
嵌入式系统的三个基本要素:
- 嵌入性
- 专用性
- 计算机系统
- BSP Board Support Package 板级支持包
- Soc System on Chip 嵌入式片上系统
- HAL 硬件抽象层
- ARM Advanced RISC Machine 先进精简指令集设计
- RTOS real time operation system 实时操作系统
- MMU memory management unit 内存管理单元
嵌入式系统进行开发时,通常采用 交叉编译(宿主机上开发,目标机上运行)
常见嵌入式设备、操作系统
嵌入式硬件体系结构
基础知识
通常微处理器在处理一条指令要经过三个步骤(ARM7 流水线):
- 取指(从存储器装载一条指令)
- 译码(识别要被执行的指令)
- 执行(处理指令并将结果写回寄存器)
微处理器架构:
- RISC
Reduced 精简
指令集中的指令为频率最高的简单指令,长度固定,以控制逻辑为主 - CISC Complex 复杂
ARM 核有两个指令集:
- ARM 指令集
- Thumb 指令集
状态寄存器 CPSR 的 T 位反映了处理器运行不同指令的当前状态。
系统总线频率称为 外频
总线架构:
- 冯诺依曼架构
- 哈佛架构
字数据存储格式:
- 小端格式 数据高字节保存在内存的高地址中
- 大端格式 数据高字节保存在内存的低地址中
嵌入式系统硬件平台
嵌入式微处理器芯片不能独立工作,需要必要的外围设备给它提供基本的工作条件。
- 嵌入式处理器
- 电源
- 输入及显示设备 键盘、LCD、LED
- I/O 接口 串口、USB 口、以太网口、JTAG 口
- 存储器 Flash、SDRAM
存储设备
Flash
Flash 是一种非易失闪存,和 ROM 一样具有掉电后数据不会丢失的特性。
特点:
- 按整体/扇区擦除
- 按字节编程
- 低功耗、高密度、体积小
NOR Flash 和 NAND Flash 比较
NOR Flash
- 在芯片内执行,可以直接读取芯片内的数据
- 地址线与数据线分开,可以像 SRAM 一样连在数据线上,以「字」为基本单位操作
- 应用程序可以直接在 Flash 内运行,不用读到系统 RAM 中
- 写操作要经过 擦除和写入 两个操作;每次擦除一个扇区,不能逐字节擦除
- 随机存取速度快,适用于代码存储
- 单片容量相对小
- 最大擦写次数 10 万次
NAND Flash
- 地址/数据线复用,读取慢
- 擦除单位较小,擦除速度快,写入快
- 顺序读取速度快,随机存取速度慢,适用于数据存储
- 单片容量大
- 最大擦写次数 100 万次
ARM 微处理器体系
ARM 微处理器特点:
- 体积小、功耗低、成本低、高性能
- 支持 Thumb(16 位)/ARM(32 位)双指令集
- 大量使用寄存器(常见描述:37 个),指令执行速度块
- 大多数数据操作都在寄存器中完成
- 寻址方式灵活简单,执行效率高
- 指令长度固定
ARM 异常模式:
- 快速中断模式(FIQ)
- 外部中断模式(IRQ)
- 管理模式(Supervisor)
- 数据访问终止模式(Abort)
- 未定义指令终止模式(Undefined)
- 安全监控模式(Monitor)
ARM 特权模式:
- 系统模式
- 所有异常模式
ARM 七种工作模式:
- 用户模式
- 系统模式
- 五种异常模式
程序「跑飞」后,ARM 处理器最可能进入 数据访问终止模式
寄存器
CPSR
Current Program Status Register
ARM 处理器的当前程序状态寄存器,包含条件标志位、中断禁止位(I 和 F)、处理器模式位等。
R14(LR) 链接寄存器,主要用于保存子程序或异常处理的返回地址
R15(PC) 程序计数器,存储当前正在取指的指令地址,决定程序执行流程
嵌入式 Linux 操作系统
Linux 的故事始于 1991 年,芬兰赫尔辛基大学的计算机科学学生 Linus Torvalds,在自己的 386 PC 上,着手编写最初的内核。
嵌入式 Linux 的内核有 5 部分:
- 进程调度
- 内存管理
- 文件管理
- 设备控制
- 网络功能
vi:
- 工作模式 命令行模式、插入模式、底行模式
工作界面:
- 字符界面
- 图形界面
文件
- 文件权限 第一段表示文件类型 第二段表示用户权限 第三段 用户组权限 第四段 其他用户权限
- 权限管理 chmod
- dev 目录 存放外部设备文件,如 U 盘,鼠标等
相关函数
pipe() 创建管道
lseek()
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int filedes, off_t offset, int whence) ;
// 打开的每个文件都有一个与其相关联的“当前文件位移量”。它是一个非负的整数,用以度量从文件开始处计算的字节数。
// 通常,读、写操作都从当前文件位移量处开始,并使位移量增加所读或写的字节数。按系统默认,当打开一个文件时,
// 除非指定O_A P P E N D选择项,否则该位移量被设置为0。
参数说明:
第一个参数是文件描述符;第二个参数是偏移量,int 型的数,正数是向后偏移,负数向前偏移;第三个参数是有三个选项:
1.SEEK_SET:将文件指针偏移到传入字节数处(文件头开始偏移)
2.SEEK_CUR:将文件指针偏移到当前位置加上传入字节数处;((当前位置开始偏移)
3.SEEK_END:将文件指针偏移到文件末尾加上传入字节数处(作为拓展作用,必须再执行一次写操作)
嵌入式程序开发基础
编辑器(vi)、编译链接器(GCC)、调试器(GDB)、项目管理器(make)
GCC
gcc 的编译可分为四个阶段:
- 预处理
- 编译
- 汇编
- 链接
常用编译命令
| 命令选项 | 作用 | 示例 |
|---|---|---|
-c | 只编译不链接,生成目标文件(.o) | gcc -c main.c |
-o | 指定输出文件名 | gcc main.c -o main |
-g | 生成调试信息 | gcc -g main.c -o main |
-O | 优化编译 | gcc -O2 main.c -o main |
-Wall | 显示所有警告信息 | gcc -Wall main.c |
-Werror | 将警告当作错误处理 | gcc -Werror main.c |
-I | 指定头文件搜索路径 | gcc -I/usr/include main.c |
-L | 指定库文件搜索路径 | gcc -L/usr/lib main.c |
-l | 链接指定库 | gcc main.c -lm |
-D | 定义宏 | gcc -DDEBUG main.c |
-E | 只进行预处理,输出到标准输出 | gcc -E main.c |
-S | 只编译到汇编代码,不汇编 | gcc -S main.c |
-v | 显示编译过程的详细信息 | gcc -v main.c |
-static | 静态链接 | gcc -static main.c |
-shared | 生成共享库 | gcc -shared -o lib.so file.c |
-fPIC | 生成位置无关代码 | gcc -fPIC -c file.c |
优化级别
| 选项 | 说明 |
|---|---|
-O0 | 不优化(默认) |
-O1 | 基本优化 |
-O2 | 常用优化(推荐) |
-O3 | 高级优化 |
-Os | 针对代码大小优化 |
-Ofast | 最高级别优化(可能不遵循标准) |
调试选项
| 选项 | 说明 |
|---|---|
-g | 生成调试信息 |
-g0 | 不生成调试信息 |
-g1 | 生成最少调试信息 |
-g2 | 生成默认调试信息 |
-g3 | 生成最详细调试信息 |
-ggdb | 生成 GDB 专用调试信息 |
GDB
需要 gcc 编译阶段使用 -g 命令把调试信息加载到可执行文件中
常用调试命令
| 命令 | 作用 | 示例 |
|---|---|---|
gdb <program> | 启动 GDB 调试器 | gdb ./test |
run 或 r | 运行程序 | r |
break 或 b | 设置断点 | b main, b 10 |
list 或 l | 显示源代码 | l, l 10 |
next 或 n | 单步执行(不进入函数) | n |
step 或 s | 单步执行(进入函数) | s |
continue 或 c | 继续执行 | c |
print 或 p | 打印变量值 | p var, p *ptr |
info | 查看信息 | info locals, info registers |
backtrace 或 bt | 显示调用栈 | bt |
finish | 执行完当前函数 | finish |
until | 执行到指定行 | until 20 |
watch | 设置观察点 | watch var |
delete | 删除断点 | delete 1 |
disable | 禁用断点 | disable 1 |
enable | 启用断点 | enable 1 |
set | 设置变量值 | set var = 10 |
display | 自动显示变量 | display var |
undisplay | 取消自动显示 | undisplay 1 |
frame | 切换栈帧 | frame 1 |
up | 向上切换栈帧 | up |
down | 向下切换栈帧 | down |
quit 或 q | 退出 GDB | q |
help | 显示帮助信息 | help break |
make
makefile 文件
- 命令必须用制表符(Tab)缩进,不能用空格
- 大小写敏感
自动变量
makefile 规则
隐式规则
模式规则:
% 表示一个或多个字符
make 命令
- 在执行时,会自动检查时间戳
- 发现有源程序文件更新了,则只编译修改的文件,并重新生成最终的程序
- 如果仅修改了 makefile 文件,而源程序文件没有修改,则 make 文件不被执行 需要 make clean 再 make
make -C <dir> 读入指定目录下的 Makefile
shell
执行前,添加权限命令
变量:
- 环境变量
- 用户自定义变量
- 系统变量
用户变量
变量中的 $ '' "" \
| 符号 | 作用 | 说明 | 示例 |
|---|---|---|---|
$ | 变量引用 | 获取变量的值 | echo $name |
'' | 单引号 | 原样输出,不解析变量和特殊字符 | echo '$name' 输出 $name |
"" | 双引号 | 解析变量但保护空格和特殊字符 | echo "$name" 输出变量值 |
\ | 转义符 | 转义下一个字符的特殊含义 | echo \$name 输出 $name |
shell 编写 1+2+ +100 的和,并输出所有能被 13 整除的数。
嵌入式开发环境建立
交叉编译,宿主机一般是指 有指令操作系统的 PC 机
交叉编译:
在一种计算机环境中运行编译程序,能编译出在另一种环境下运行的代码。这个编译过程叫交叉编译。
(在一个平台上生成另一个平台上的可执行代码)
大多数目标板没有编译代码的功能
文件处理与进程通信
Linux 系统调用文件操作的主要函数:
- open 打开或创建文件
- close 关闭一个打开的文件,并释放文件描述符
- read 从打开的文件读取数据
- write 在打开的文件中读取数据
- lseek 将文件指针定位到响应的位置,返回值为最终的偏移量
缓冲区文件:
缓冲区文件在读写时先将数据写入缓冲区,缓冲区满后再写入磁盘,减少磁盘读写次数,延长寿命。
文件描述符:
Linux 内核利用文件描述符来访问文件
文件描述符是非负整数,是一个索引值,它指向内核中每个进程打开文件的记录表。
当打开一个现存文件或新建一个文件时,内核会向进程返回一个文件描述符。
当读写文件时,需要使用该文件描述符来指定读写的文件。
该值为 0 代表标准输入。
进程
基础概念
getpid() 获取当前进程的 ID 号
current 获取当前进程的进程描述符
get_task_pid(): 根据进程 ID 获取进程描述符
task_tgid(): 获取进程的线程组 ID
进程
Linux 中每个正在运行的程序都被称为进程。
父进程与子进程 一个进程使用 fork 创建子进程。 判断 fork()函数返回情况,返回值为 0 的是子进程,非 0 的是父进程。
进程描述符:
一个进程的所有信息,一个定义的名为 task_struct 的结构体。
僵尸进程 一个进程使用 fork 创建子进程,如果子进程退出,而父进程并没有调用 wait 或 waitpid 获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。
孤儿进程:
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。
进程的三种状态
就绪态
阻塞态 执行态
就绪态 - 进程调度 -> 执行态
执行态 - 时间片完 -> 就绪态
执行态 - I/O请求 -> 阻塞态
阻塞态 - I/O完成 -> 就绪态
fork() 函数
- 功能是创建调用进程的副本
- fork 函数执行一次返回两个值 父进程返回子进程 PID 子进程返回 0
- 子进程从 fork()的下一条指令开始执行
wait() 函数
等待子进程
嵌入式系统中进程间通信的主要方式
- 管道 管道(Pipe)及命名管道(named pipe):管道可用于具有亲缘关系进程间的通信;命名管道,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
- 共享内存 可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种通信方式需要依靠某种同步机制,如互斥锁和信号量等。
- FIFO(命名管道)
- 消息队列 消息队列是消息的链接表,包括 Posix 消息队列 SystemV 消息队列。它克服了前两种通信方式中信息量有限的缺点,具有写权限的进程可以按照一定的规则向消息队列中添加新消息;对消息队列有读权限的进程则可以从消息队列中读取消息。
- 信号 信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一样的。
- 信号量 主要作为进程之间以及同一进程的不同线程之间的同步和互斥手段。
- 套接字 一种更为一般的进程间通信机制,它可用于网络中不同机器间的进程间通信,应用非常广泛
进程间通信时,使用一个公共的缓冲区来进行信息交换,该缓冲区位于 内核 中。
system()函数及 exec 函数族
均可在进程中开始其它进程
// 不同点
system()函数可以在进程中开始某一进程,并使用系统函数库来创建新进程。
system()会调用fork()产生子进程,由子进程来调用/bin/sh –c string 来执行参数 string 字符串所代表的命令,此命令执行完后随即返回原调用的进程。
当进程调用一种exec 函数时,该进程执行的程序完全替换为新程序,而新程序从其main 函
数开始执行。调用exec 并不创建新进程,所以前后的进程ID并不变化,只是将当前进程重
新初始化了数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其
他全部被新的进程替换了。
嵌入式 Linux 网络开发应用
端口号 0-65535
socket 类型
- 字节流 Socket 基于 TCP,提供可靠字节流传输
- 数据报 Socket 基于 UDP,提供不可靠报文传输
- 原始套接字 基于 IP,允许用户直接对 IP 操作
TCP 协议编程中,server 端程序的步骤是什么?
Socket()->bind()->listen()->accept()->recv()->send()->recv()->close()
创建套接字,绑定 ip 地址和端口,监听,建立连接,接受数据,写数据,关闭
当服务器与客户端建立连接后,如果在客户端和服务器中都调用了 recv()函数,通信如何进
行?为什么?
recv()一直等待,直到协议把数据读取出来。但是因为两边都在等待,所以通信会阻塞。
嵌入式驱动程序设计
设备文件分为三种:
- 字符设备
- 块设备
- 网络接口设备
insmod 加载 Linux 驱动程序
rmmod 卸载 Linux 驱动程序
硬件抽象层 将系统软件和硬件部分完全隔离开,大大提高了系统的可移植性
RTOS:实时操作系统
IPC:进程间通信
GPIO:通用 I/O 接口
设备驱动程序的作用:
驱动程序是内核的一部分,是操作系统内核和机器硬件之间的接口,它由一组函数和一些私有数据组成,是连接应用程序与具体硬件的桥梁。
驱动程序负责将应用程序如读、写等操作正确无误地传递给相关的硬件,并使硬件能够做出正确反应的代码。驱动程序像一个黑盒子,它隐藏了硬件的工作细节,应用程序只需要通过一组标准化的接口实现对硬件的操作。
字符设备驱动程序开发的流程主要是什么?
① 编写设备驱动程序及 makefile 文件
② 执行 make 命令,生成驱动程序文件(*.ko)
③ 进入 root,执行 mknod 设置设备进入点(在/dev 目录中)
④ 执行 insmod 命令加载驱动程序(lsmod 可查看)
⑤ 编写用户验证程序,并编译生成可执行程序
⑥ 执行用户验证程序,查看运行结果
⑦ 运行 dmesg 命令查看内核中驱动程序的输出
⑧ 执行 rmmod 命令卸载驱动,rm 命令删除设备进入点
题目
四、简答题
- 什么是僵尸进程?什么是孤儿进程?
- 简述 Linux 系统进程的状态,并绘画出它们之间转换的关系。
- 简述冯·诺依曼结构和哈佛结构的区别。
- Linux 系统中的缓冲区文件与非缓冲区文件有何区别?
- 为什么需要系统调用?
系统调用(System Call)是操作系统内核提供给用户程序的接口,用于访问底层硬件和核心资源(如文件、内存、设备等)。
通过开放的接口来让用户实现对应的操作,将用户态与内核态隔离,防止用户直接操作内核,增强安全性
安全、性能优化、抽象、跨平台兼容 - 简述 ARM 微处理器支持的工作模式以及其特点。
- ARM 设计思想是什么?
- TCP 协议编程中,server 端程序的步骤是什么?
- 简述 Linux 对文件处理的方式,并说明它们之间的区别。 系统调用 无缓冲,每次调用触发内核态切换,适合高频、小数据量或实时性要求高的操作。 标准库 带缓冲(用户空间缓冲区),减少系统调用次数,适合大文件或频繁读写场景。
- 简述 ARM 和 Thumb 状态的区别及如何进行状态切换。 ARM 状态:32 位指令,功能完整,性能高。 Thumb 状态:16 位指令(Thumb-2 扩展为混合 16/32 位),代码密度高,功耗低。 使用 BX(分支交换)指令,通过目标地址最低位判断状态(0=ARM,1=Thumb)
- 简述 NOR Flash 和 NAND Flash 的特点。
- 试比较 CISC 体系结构和 RISC 体系结构的特点。
- 比较嵌入式系统与通用计算机的区别。 专用与通用、资源与性能
- 简述嵌入式系统的定义。
- ARM 微处理器的特点。
- 若寄存器 R1=0x12345678,分别按小端模式和大端模式存储在 0x2000 字单元中,试分别画出两种模式下内存的存储内容,并标出内存地址。
五、编程题和分析题 复习范围: (1)第三章:Linux 常用的命令(主要以 PPT 上面的例题和练习题为主) (2)第四章:make 命令和 Makefile 文件、shell 编程(主要以 PPT 上面的例题和练习题、书上练习题为主) (3)第六章:嵌入式 Linux 的文件处理、进程与进程控制、进程间通信(主要以 PPT 上面的例题和练习题、书上例题为主) (4)第七章:基于 TCP 的 Socket 网络编程。 (5)第九章:驱动 LED 点亮的例子
Related notes
- 所属 MOC: 学科知识MOC