EagleBear2002 的博客

这里必须根绝一切犹豫,这里任何怯懦都无济于事

嵌入式系统概论-05-基于主线的计算系统

本文主要内容来自 SpriCoder的博客,更换了更清晰的图片并对原文的疏漏做了补充和修正。

输入输出

I/O 设备

  1. 通常包括一些非数字组件。
  2. 典型的 CPU 数字接口:

应用:8251 UART

  1. 通用异步接收器发送器(UART):提供串行通信。
  2. 允许对许多通信参数进行编程。

串行通讯

  1. 字符分别传输:

串行通讯参数:

  1. 波特率(Baud (bit) rate)
  2. 每个字符的比特数
  3. 奇偶校验/无奇偶校验:奇偶校验
  4. 停止位的长度(1、1.5、2 位)

CPU 接口

I/O 设备分类

从属关系分类

  1. 系统设备:操作系统启动时已经在系统中注册的标准设备。
    1. 示例包括 NOR/NAND 闪存,触摸板等。
    2. 操作系统中有用于这些设备的设备驱动程序和管理程序。
    3. 用户应用程序只需调用操作系统提供的标准命令或功能即可使用这些设备。
  2. 用户设备:操作系统启动时未在系统中注册的非标准设备。
    1. 通常,设备驱动程序由用户提供。用户必须以某种方式将这些设备的控制权转移到 OS 进行管理。
    2. 典型的设备包括 SD 卡,U 盘等。

使用分类

  1. 专用设备:同一时间只能被一个进程使用的设备。对于多个并发进程,每个进程使用设备是互斥的。一旦操作系统将设备分配给一个特定的进程,它将被该进程独占,直到该进程使用后释放它。
  2. 共享设备:可被多个进程同时寻址的设备。共享设备必须是可寻址的,并且是随机寻址的。共享设备机制可以提高每个设备的利用率。
  3. 虚拟设备:通过虚拟技术将一台独占设备虚拟成多台逻辑设备,供多个用户进程同时使用, 通常把这种经过虚拟的设备称为虚拟设备。

特征分类

  1. 存储设备:用于存储信息的设备。嵌入式系统中的典型 例子包括硬盘、固态硬盘、NOR/NAND 闪存。
  2. I/O 设备:
  3. 输入设备:输入设备负责将信息从外部源输入到内部系统,例如触摸面板,条形码扫描仪等。
  4. 输出设备:输出设备负责将嵌入式系统处理的信息输出到外部世界,例如 LCD 显示器,扬声器等。

信息传递单位类别

  1. 块设备:这种类型的设备以数据块为单位组织和交换数据。
    1. 它是一种结构装置。
    2. 典型的设备是硬盘。
    3. 在 I/O 操作中,即使只是单字节读/写,也应该读取或写入整个数据块。
  2. 字符设备:这种类型的设备以字符为单位组织和交换数据。
    1. 它是一种非结构性的设备。
    2. 字符设备的类型很多,例如串行端口,触摸面板,打印机。
    3. 字符设备的基本特征是传输速率低且无法寻址。当字符设备执行 I/O 操作时,通常使用中断。

I/O 设备组成

通常,I/O 设备由两部分组成:机械部分和电子部分。电子部分称为设备控制器或适配器。

可编程 I/O

  1. 通信期间选择控制寄存器或数据缓冲区的三种方法
    1. 独立的 I/O 端口:需要专门的指令来完成。
    2. 内存映射的 I/O。
    3. 混合解决方案(统一编址)。混合模型包括内存映射的 I/O 数据缓冲区和用于控制寄存器的单独的 I/O 端口。
  2. 英特尔 x86 提供了输入输出说明。大多数其他 CPU 使用内存映射的 I/O。
  3. I/O 指令不排除内存映射的 I/O
(a)独立的 I/O 和内存空间、(b)内存映射的 I/O、(c)混合解决方案

内存映射的 I/O

内存映射 I/O 的优点可总结为:

  1. 在内存映射的 I/O 模式下,设备控制寄存器只是内存中的变量,并且可以与其他变量一样用 C 寻址。因此,可以完全用 C 语言编写 I/O 设备驱动程序。
  2. 在这种模式下,不需要特殊的保护机制即可阻止用户进程执行 I/O 操作。

内存映射 I/O 模式的缺点可以总结为:

  1. 当前大多数嵌入式处理器都支持内存缓存。缓存设备控制寄存器将导致灾难。为了防止这种情况,必须为硬件提供选择性禁用缓存的功能。这将增加嵌入式系统中硬件和软件的复杂性。
  2. 如果只有一个地址空间,则所有内存模块和所有 I/O 设备都必须检查所有内存引用,以便确定要响应的内存引用。这会严重影响系统性能。

ARM 内存映射的 I/O:

  1. 定义设备地址:DEV1 EQU 0x1000
  2. 读写代码:
1
2
3
4
LDR r1,#DEV1 ; set up device adrs
LDR r0,[r1] ; read DEV1
LDR r0,#8 ; set up value to write
STR r0,[r1] ; write value to device
Peek and poke

传统 HLL 接口:

1
2
3
4
5
6
int peek(char *location) {
return *location;
}
void poke(char *location, char newval) {
(*location) = newval;
}
忙等(busy/wait)输出

对设备进行编程的最简单方法。使用指令测试设备何时准备就绪:

1
2
3
4
5
6
current_char = mystring;
while (*current_char != '\0') {
poke(OUT_CHAR,*current_char);
while (peek(OUT_STATUS) != 0);
current_char++;
}
同步忙等输入输出:
1
2
3
4
5
6
7
8
9
while (TRUE) {
/* read */
while (peek(IN_STATUS) == 0);
achar = (char)peek(IN_DATA);
/* write */
poke(OUT_DATA,achar);
poke(OUT_STATUS,1);
while (peek(OUT_STATUS) != 0);
}

中断 I/O

忙等是非常低效的。而中断 I/O 是相对高效的。

  1. CPU 在测试设备时不能做其他工作。
  2. 很难实现同步 I/O

中断允许设备更改 CPU 中的控制流:导致子例程调用以处理设备。

中断接口

中断行为
  1. 基于子程序的调用机制
  2. 中断迫使下一条指令成为子程序调用到预定位置。
  3. 返回地址被保存以恢复执行前台程序。
中断物理接口
  1. CPU 和设备通过 CPU 总线连接。
  2. CPU 和设备握手
  3. 设备声明中断请求(interrupt request)
  4. CPU 可以处理中断时会声明中断确认(interrupt acknowledge)
字符 I/O 处理
1
2
3
4
5
6
7
void input_handler() {
achar = peek(IN_DATA);
gotchar = TRUE;
poke(IN_STATUS,0);
}
void output_handler() {
}
主程序
1
2
3
4
5
6
7
8
9
10
void main() {
while (TRUE) {
if (gotchar) {
poke(OUT_DATA,achar);
poke(OUT_STATUS,1);
gotchar = FALSE;
}
}
// other processing....
}
例子:带缓冲区的中断 I/O

字节队列如下:

基于缓冲区的输入处理程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
void input_handler() {
char achar;
if (full_buffer()) error = 1;
else {
achar = peek(IN_DATA);
add_char(achar);
}
poke(IN_STATUS, 0);
if (nchars == 1) {
poke(OUT_DATA,remove_char());
poke(OUT_STATUS,1);
}
}
I/O 时序图

优先级和向量

两种机制让我们可以使中断更具体:

  1. 优先级:确定什么中断首先获取 CPU。
  2. 中断向量:确定每种中断类型需要调用什么代码。

这两种机制是正交的:大多数 CPU 都提供两者。

优先级例子

中断嵌套受到中断嵌套允许位控制

中断优先级
  1. 屏蔽:优先级低于当前优先级的中断直到待处理的中断完成后才被识别。
  2. 不可屏蔽中断(NMI):最高优先级,从不屏蔽。经常情况下被用于关闭电源。

中断向量
  1. 允许使用不同的代码处理不同的设备。
  2. 中断向量表:可以通过 ISR 寻找到,放置在起始地址。

中断向量获取

通用状态机

中断序列
  1. CPU 确认请求。
  2. 设备发送引导程序。
  3. CPU 调用处理程序。
  4. 软件流程请求。
  5. CPU 将状态恢复到前台程序。
中断开销的来源
  1. 处理程序执行时间
  2. 中断机制开销
  3. 注册保存/恢复
  4. 管道相关的罚时
  5. 与缓存有关的罚时

中断设计准则

虽然糟糕的代码很难调试,但糟糕的 ISR 几乎是无法调试的。

如何调试?

  1. 首先,在您布置中断映射之前,不要考虑为您的新嵌入式系统编写一行代码。列出每个中断,并给出例程应该做什么的英文描述。
  2. 地图是预算。它可以让您评估中断时间将花在哪里。
  3. 估计每个 ISR 的复杂性。
  4. ISR 的基本规则是使处理程序保持简短。
  5. 当然,短是用时间而不是代码大小来衡量的。避免循环。避免冗长的复杂指令(重复动作、可怕的数学等)。
  6. 在 ISR 中尽快重新启用中断。预先做硬件关键和不可重入的事情,然后执行中断启用指令。给其他 ISR 一个战斗的机会来做他们的事情。

中断图

用指向空例程的指针填充所有未使用的中断向量

C 还是汇编?

  1. 如果例程将使用汇编语言,则将时间转换为大致的指令数。如果一条平均指令需要 x 微秒(取决于时钟速率、等待状态等),那么很容易获得对代码允许复杂度的关键估计。
  2. C 更成问题。事实上,没有办法科学地用 C 编写中断处理程序! 你不知道一行 C 需要多长时间。您甚至无法进行估算,因为每条线路的时间变化很大。
    • 字符串比较可能会导致运行时库调用,结果完全不可预测。
    • FOR 循环可能需要一些简单的整数比较或大量的处理开销。
  3. 您可能会发现一个编译器在处理中断时慢得可怜。尝试另一个或切换到汇编。

调试中断代码

  1. 如果您忘记更改寄存器怎么办?
    1. 前台程序可能会显示神秘的错误。
    2. 错误将很难重复:取决于中断的时间。

调试 INT/INTA 周期

  1. 设备硬件产生中断脉冲。
  2. 中断控制器(如果有)优先处理多个同时请求并向处理器发出单个中断。
  3. CPU 以中断确认周期进行响应。
  4. 控制器在数据总线上丢弃一个中断向量。
  5. CPU 读取向量并计算用户存储向量在内存中的地址。然后它获取这个值。
  6. CPU 推送当前上下文,禁用中断,并跳转到 ISR。

查找丢失的中断

  1. 您可以使用一个递增/递减计数器建立一个小的电路,该计数器对每个中断进行计数,并减少每个中断应答的计数。如果计数器始终显示零或一的值,则一切正常。
  2. 一种设计经验法则将有助于最大程度地减少丢失的中断:在最早的安全位置重新启用 ISR 中的中断。

避免 NMI

  1. 为像天启这样的灾难预留 NMI(不可屏蔽的中断)。电源故障、系统关闭和即将发生的灾难都是使用 NMI 监控的好东西。定时器或 UART 中断不是。
  2. NMI 甚至会破坏编码良好的中断处理程序,因为大多数 ISR 在硬件服务的前几行代码中是不可重入的。NMI 也会阻碍您的堆栈管理工作。
  3. NMI 与大多数工具的混合很差。调试任何 ISR(NMI 或其他)充其量是令人恼火的。很少有工具能很好地在 ISR 内执行单步执行和设置断点。

断点问题

尽管断点确实是很棒的调试辅助工具,但它们就像海森堡的不确定性原则:观察系统的行为会改变它。您可以使用实时跟踪(至少在所有调试器和某些智能逻辑分析器中都可用)来欺骗海森堡(至少在调试嵌入式代码中!)。

一个简单 ISR 调试

调试 ISR 的最快方法是什么?

  • 不要调试
  • 如果您的 ISR 只有 10 或 20 行代码,请通过检查进行调试。不要启动各种复杂且不可预测的工具。
  • 保持处理程序简单而简短。

可重用代码

在嵌入式世界中,例程必须满足以下条件才能重新进入:

  1. 它以原子方式使用所有共享变量,除非每个共享变量都分配给该函数的特定实例。
  2. 它不调用非可重入函数。
  3. 它不会以非原子方式使用硬件。

可重用,即被中断之后还可以再次进入

原子变量

第一个规则和最后一个规则都使用“原子”一词,该词源于希腊语,意思是“不可分割”。在计算机世界中,“原子”是指不能中断的操作。

1
2
3
4
5
6
7
8
9
10
mov ax, bx 原子式
temp = foobar; temp += 1; foobar = temp; 非原子
foobar += 1; 非原子?先加法,再移动

mov ax,[foobar]
inc ax
mov [foobar],ax ; 非原子

inc [foobar] ; 非原子,先从寄存器中读,然后加载进来两步骤
lock inc [foobar] ; 将总线锁死

规则 2 告诉我们调用函数继承了被调用者的重入问题

规则 3 是唯一嵌入的警告。硬件看起来很像一个变量。如果处理一个设备需要多个 I/O 操作,则会产生重入问题。

保持代码可重入

消除非重入代码的最佳选择是什么?

  1. 经验法则是避免共享变量。全局变量是无止境的调试难题和失败的代码。使用自动变量或动态分配的内存。
  2. 最常见的方法是在非重入代码期间禁用中断。
  3. 信号量

递归

如果函数调用自身,则该函数是递归的。因此,所有递归函数都必须是可重入的……但并非所有可重入函数都是递归的。

嵌入式不建议使用递归,核心栈空间可能不足、执行时间比较复杂。

异步硬件/防火墙

1
2
3
4
5
6
7
8
9
10
11
12
13
// QAR 公司 RTEMS
int timer_hi;
interrupt timer(){
// 中断,让高时间位自增 1
++timer_hi;
}
long timer_read(void){
unsigned int low, high;
// 这里有问题,读硬件的时候值是 ffffff
low = inword(hardware_register); // 存在被 ISR 抢断的情况,比如产生了溢出
high = timer_hi;
return (high << 16 + low);
}

比赛条件 Race Conditions

timer_read 的错误条件之一可能是:(顺序变化导致的结果变化,重要)

  1. 它读取硬件,并获得 0xffff 的值
  2. 在有机会从变量 timer_hi 检索大部分时间之前,硬件将再次递增至 0x0000
  3. 溢出触发中断。ISR 运行。timer_hi 现在为 0
  4. 0001,而不是 0,因为它只是十亿分之一秒。
  5. ISR 返回;我们无所畏惧的 timer_read 例程,不知道发生了中断,巧妙地将新的 0 连接起来
  6. 0001 加上以前读取的计时器值为 0Xffff,并返回 0X1ffff,这是一个非常不正确的值。

选项

最简单的方法是在尝试读取计时器之前将其停止:失去时间。在此期间关闭中断将消除不必要的任务,但会增加系统延迟和复杂性。

另一种解决方案是先读取 timer_hi 变量,然后读取硬件计时器,然后重新读取 timer_hi。如果两个变量值都不相同,则会发生中断。迭代直到两个变量读取相等。

  1. 好处是:正确的数据,中断持续存在,并且系统不会丢失计数。
  2. 缺点:在负载繁重的多任务环境中,该例程可能会循环很长时间才能获得两次相同的读取。函数的执行时间不确定。我们已经从非常简单的计时器读取器转变为可以运行几毫秒而不是微秒的更为复杂的代码。

另一种选择是简单地禁用读取周围的中断。

1
2
3
4
5
6
7
8
9
10
11
12
13
long timer_read(void){
unsigned int low, high;
push_interrupt_state;
disable_interrupts;
low = inword(Timer_register);
high = timer_hi;
if(inword(timer_overflow)){
++high;
low = inword(timer_register);
}
pop_interrupt_state;
return (((ulong)high) << 16 + (ulong)low);
}

总线

CPU 总线

总线允许 CPU,内存,设备进行通信:共享的通讯媒体。

总线是:

  1. 一组电线
  2. 通信协议

总线协议

  1. 总线协议决定了设备是如何进行通信的
  2. 总线上的设备会经历状态序列.协议是由状态机指定的,协议中的每个参与者都有一个状态机。
  3. 可能包含异步逻辑行为。

四周期握手

  1. 设备 1 引发 enq。
  2. 设备 2 以 ack 响应。
  3. 完成后,设备 2 降低确认。
  4. 设备 1 降低 enq。

微处理器总线

  1. 时钟提供同步
  2. 读时 R/W 为 true(读时 R/W' 为 false)
  3. 地址是地址线的比特捆绑
  4. 数据是 n 位数据线束
  5. 当 n 位数据就绪时,数据就绪信号发出

总线复用

总线复用

系统总线配置

多个总线允许并行处理:慢设备在其中一条总线上,快设备位于另一条单独的总线上,桥连接两条总线。

PC 端类似,使用南桥和北桥来完成,北桥连接 CPU

桥状态图

ARM AMBA 总线

两个类别:

  1. AHB 是高性能的。
  2. APB 是低速,低成本的。

AHB 支持流水线,突发传输,拆分事务,多个总线主控(H:高速)

所有设备都是 APB 上的从设备。

  1. APB 是外部
  • USB3.0 是高速设备
  • SPI 和 I2C 也是非常多的
    • SPI:点对点星型设备
    • I2C:串行设备
    • 开源硬件一般同时支持这两个
  • 市面上的开源硬件支持 USB、SPI 和 I2C

Arduino Uno R3