. Peripherals
Each pizza glides into a slot like a circuit board into a computer, clicks into place as the smart box interfaces with the onboard system of the car. The address of the customer is communicated to the car, which computes and projects the optimal route on a heads-up display.
—Neal Stephenson, Snow Crash
In addition to the processor and memory, most embedded systems contain a handful of other hardware devices. Some of these devices are specific to the application domain, while others—like timers and serial ports—are useful in a wide variety of systems. The most generically useful of these are often included within the same chip as the processor and are called internal, or on-chip, peripherals. Hardware devices that reside outside the processor chip are, therefore, said to be external peripherals. In this chapter well discuss the most common software issues that arise when interfacing to a peripheral of either type.
7.1 Control and Status Registers
The basic interface between an embedded processor and a peripheral device is a set of control and status registers. These registers are part of the peripheral hardware, and their locations, size, and individual meanings are features of the peripheral. For example, the registers within a serial controller are very different from those in a timer/counter. In this section, Ill describe how to manipulate the contents of these control and status registers directly from your C/C programs.
Depending upon the design of the processor and board, peripheral devices are located either in the processors memory space or within the I/O space. In fact, it is common for embedded systems to include some peripherals of each type. These are called memory-mapped and I/O-mapped peripherals, respectively. Of the two types, memory-mapped peripherals are generally easier to work with and are increasingly popular.
Memory-mapped control and status registers can be made to look just like ordinary variables. To do this, you need simply declare a pointer to the register, or block of registers, and set the value of the pointer explicitly. For example, if the P2LTCH register from Chapter 2, were memory-mapped and located at physical address 7205Eh, we could have implemented toggleLed entirely in C, as shown below. A pointer to an unsigned short—a 16-bit register—is declared and explicitly initialized to the address 0x7200:005E. From that point on, the pointer to the register looks just like a pointer to any other integer variable:
unsigned short * pP2LTCH = (unsigned short *) 0x7200005E;
void
toggleLed(void)
{
*pP2LTCH ^= LED_GREEN; /* Read, xor, and modify. */
} /* toggleLed() */
Note, however, that there is one very important difference between device registers and ordinary variables. The contents of a device register can change without the knowledge or intervention of your program. Thats because the register contents can also be modified by the peripheral hardware. By contrast, the contents of a variable will not change unless your program modifies them explicitly. For that reason, we say that the contents of a device register are volatile, or subject to change without notice.
The C/C keyword volatile should be used when declaring pointers to device registers. This warns the compiler not to make any assumptions about the data stored at that address. For example, if the compiler sees a write to the volatile location followed by another write to that same location, it will not assume that the first write is an unnecessary use of processor time. In other words, the keyword volatile instructs the optimization phase of the compiler to treat that variable as though its behavior cannot be predicted at compile time.
Heres an example of the use of volatile to warn the compiler about the P2LTCH register in the previous code listing:
volatile unsigned short * pP2LTCH = (unsigned short *) 0x7200005E;
It would be wrong to interpret this statement to mean that the pointer itself is volatile. In fact, the value of the variable pP2LTCH will remain 0x7200005E for the duration of the program (unless it is changed somewhere else, of course). Rather, it is the data pointed to that is subject to change without notice. This is a very subtle point, and it is easy to confuse yourself by thinking about it too much. Just remember that the location of a register is fixed, though its contents might not be. And if you use the volatile keyword, the compiler will assume the same.
The primary disadvantage of the other type of device registers, I/O-mapped registers, is that there is no standard way to access them from C or C . Such registers are accessible only with the help of special machine-language instructions. And these processor-specific instructions are not supported by the C or C language standards. So it is necessary to use special library routines or inline assembly (as we did in Chapter 2) to read and write the registers of an I/O-mapped device.
7.2 The Device Driver Philosophy
When it comes to designing device drivers, you should always focus on one easily stated goal: hide the hardware completely. When youre finished, you want the device driver module to be the only piece of software in the entire system that reads or writes that particular devices control and status registers directly. In addition, if the device generates any interrupts, the interrupt service routine that responds to them should be an integral part of the device driver. In this section, Ill explain why I recommend this philosophy and how it can be achieved.
Of course, attempts to hide the hardware completely are difficult. Any programming interface you select will reflect the broad features of the device. Thats to be expected. The goal should be to create a programming interface that would not need to be changed if the underlying
剩余内容已隐藏,支付完成后下载完整资料
外设
每个比萨饼滑入一个插槽就像电路板插入计算机一样,正好嵌入就像智能盒与汽车的车载系统接口相连接一样。将客户的地址传达给一辆在平视显示器上计算和投影最佳路线汽车。
——斯蒂芬森,斯诺夸纳徐
除了处理器和内存之外,大多数嵌入式系统还包含少数其他硬件设备。这些设备中的一部分应用于特定的程序域,而其他类似的定时器和串行端口在各种各样的系统中是有用的。这些中最普遍有用的通常包含在与处理器相同的芯片内,称为内部或片上外设。因此,位于处理器芯片之外的硬件设备被认为是外部外设。在本章中,我们将讨论在连接到任一类型的外设时出现的最常见的软件问题。
7.1控制和状态寄存器
嵌入式处理器和外围设备之间的基本接口是一组控制和状态寄存器。这些寄存器是外围硬件的一部分,它们的位置,大小和个别含义都是外设的特征。例如,串行控制器中的寄存器与定时器/计数器中的寄存器差距很大。在本节中,我将介绍如何直接从C / C 程序中处理这些控件和状态寄存器的内容。
根据处理器和电路板的设计,外围设备位于处理器的存储空间或I / O空间内。事实上,嵌入式系统通常包括每种类型的一些外设。这些分别称为内存映射和I / O映射外设。在两种类型中,内存映射外设通常更容易使用,并且越来越受欢迎。
内存映射控制和状态寄存器可以看成普通变量。为此,您需要简单地声明一个指向寄存器或寄存器块的指针,并明确地设置指针的值。例如,如果第2章中的P2LTCH寄存器被存储映射并位于物理地址7205Eh,我们可以在C中完全实现toggleLed,如下所示。指向无符号短16位寄存器的指针被声明并明确初始化为地址0x7200:005E。从那时起,指向寄存器的指针就像一个指向任何其他整数变量的指针:
unsigned short * pP2LTCH = (unsigned short *) 0x7200005E;
void
toggleLed(void)
{
*pP2LTCH ^= LED_GREEN; /* 阅读,异或和修改。 */
} /* toggleLed() */
但是请注意,器件寄存器和普通变量之间存在一个非常重要的区别。设备寄存器的内容可以在没有您的程序的知识或干预的情况下更改。这是因为寄存器内容也可以被外设硬件修改。相比之下,变量的内容不会改变,除非你的程序明确地修改它。因此,我们说设备寄存器的内容是不稳定的,如有更改,恕不另行通知。
当声明指向设备寄存器的指针时,应使用C / C 关键字volatile。这警告编译器不要对存储在该地址的数据做任何假设。例如,如果编译器看到对易失性位置的写入,然后对同一位置进行另一次写入,则不会假定第一次写入是处理器时间的不必要的使用。换句话说,关键字volatile指示编译器的优化阶段来对待该变量,就好像在编译时不能预测它的行为。
以下是使用volatile在上一个代码列表中警告编译器关于P2LTCH寄存器的示例:
volatile unsigned short * pP2LTCH = (unsigned short *) 0x7200005E;
这是个错误来解释这个语句意味着指针本身是不稳定。实际上,在程序的持续时间内,变量pP2LTCH的值将保持为0x7200005E(当然,除非另外改变)。相反,指向的数据如有变更,恕不另行通知。这是一个非常微妙的一点,很容易通过思考太多来混淆自己。请记住,寄存器的位置是固定的,尽管它的内容可能不是。如果使用volatile关键字,编译器将承担相同的操作。
其他类型的器件寄存器I / O映射寄存器的主要缺点是没有标准的方法可以从C或C 访问它们。只有借助特殊的机器语言指令才能访问此类寄存器。并且C或C 语言标准不支持这些处理器特定的指令。因此,有必要使用特殊库例程或内联汇编(如第2章所述)来读写I / O映射器件的寄存器。
7.2设备驱动程序的理念
在设计设备驱动程序时,您应该始终有一个简单明了的目标:完全隐藏硬件。完成后,您希望设备驱动程序模块成为整个系统中唯一可以直接读取或写入特定设备的控制和状态寄存器的软件。此外,如果设备产生任何中断,则响应它们的中断服务程序应该是设备驱动程序的组成部分。在本节中,我将解释为什么我推荐这个原理,以及如何实现。
当然,完全隐藏硬件的尝试是很困难的。您选择的任何编程界面将反映设备的广泛功能。这是可以预料的。目标应该是创建一个编程接口,如果底层的外围设备在其一般类别中被替换为另一个,则不需要更改。例如,所有闪存设备共享扇区的概念(尽管扇区大小在芯片之间可能不同)。只能在整个扇区上执行擦除操作,一旦擦除,可以重写单个字节或单词。所以最后一章Flash驱动程序示例提供的编程接口应该与任何闪存设备配合使用。根据需要,AMD 29F010的特定功能从该级别隐藏。
用于嵌入式系统的设备驱动程序与其工作站对应方案截然不同。在现代计算机工作站中,设备驱动程序最常见于满足操作系统的要求。例如,工作站操作系统通常对自己和网卡之间的软件接口施加严格的要求。特定网卡的设备驱动程序必须符合该软件界面,无论底层硬件的功能和功能如何。想要使用网卡的应用程序被强制使用由操作系统提供的网络API,并且不能直接访问卡本身。在这种情况下,完全隐藏硬件的目标很容易。
相比之下,嵌入式系统中的应用软件可以轻松访问您的硬件。事实上,由于所有的软件都连接在一起成为一个二进制图像,所以在应用软件,操作系统和设备驱动程序之间很少有区别。绘制这些行和执行硬件访问限制纯粹是软件开发人员的职责。两者都是开发人员有意识的设计决策。换句话说,嵌入式软件的实现者可以比非嵌入式对等人更容易欺骗软件设计。
良好的设备驱动程序设计的好处有三个方面。首先,由于模块化,整体软件的结构更容易理解。第二,由于只有一个模块直接与外设的寄存器进行交互,硬件的状态可以更准确地跟踪。而且,最后但并非最不重要的是,由硬件更改导致的软件更改会本地化到设备驱动程序。这些好处可以帮助您减少嵌入式软件的总数量。但是,为了实现这样的节省,你必须在设计时花费一些额外的努力。
如果您同意在设备驱动程序中隐藏所有硬件细节和交互的理念,它通常由以下列表中的五个组件组成。为了使驱动程序实现尽可能简单和增量,这些元素应按照它们呈现的顺序进行开发。
1.覆盖设备的存储器映射控制和状态寄存器的数据结构
驱动程序开发过程的第一步是创建一个C风格的结构,其外观与设备的内存映射寄存器一样。这通常涉及研究外设的数据手册,并创建一个控制和状态寄存器及其偏移量表。然后,从最小偏移量的寄存器开始,开始填充结构体。 (如果一个或多个位置未使用或保留,请确保在其中放置虚拟变量以填补额外空间。)
这样的数据结构的示例如下所示。该结构描述了80188EB处理器之一的片上定时器/计数器单元之一的寄存器。该器件具有三个寄存器,如下面的TimerCounter数据结构所示排列。每个寄存器为16位宽,应被视为无符号整数,尽管其中一个控制寄存器实际上是单独有效位的集合。
struct TimerCounter
{
unsigned short count; // 当前计数,偏移量为0x00
unsigned short maxCountA; //最大计数,偏移量0x02
unsigned short _reserved; // 未使用空间,偏移0x04
unsigned short control; // 控制位,偏移量为0x06
};
为了使控制寄存器中的位更容易单独读写,我们还可以定义以下位掩码:
#define TIMER_ENABLE 0xC000 //启用定时器。
#define TIMER_DISABLE 0x4000 //禁用定时器。
#define TIMER_INTERRUPT 0x2000 //启用定时器中断。
#define TIMER_MAXCOUNT 0x0020 //计时器完成?
#define TIMER_PERIODIC 0x0001 //定期定时器?
2.一组变量来跟踪硬件和设备驱动程序的当前状态
驱动程序开发过程的第二步是找出需要跟踪硬件和设备驱动程序状态的变量。例如,在之前描述的定时器/计数器单元的情况下,我们可能需要知道硬件是否已被初始化。如果已经有,我们也可能想知道运行倒计时的长度。
某些设备驱动程序创建多个软件设备。这是一种纯逻辑的设备,可以在基础外围硬件的顶层实现。例如,很容易想到可以从单个定时器/计数器单元创建多个软件定时器。定时器/计数器单元将被配置为产生周期性的时钟脉冲,并且设备驱动器然后通过维护每一个的状态信息来管理各种长度的一组软件定时器。
3.将硬件初始化为已知状态的例程
一旦知道如何跟踪物理和逻辑设备的状态,现在是开始编写实际与之交互并控制设备的功能的时候了。最好从硬件初始化程序开始。您首先需要一个,这是熟悉设备交互的好方法。
4.一组程序集合在一起,为设备驱动程序的用户提供一个API
成功初始化设备后,您可以开始向驱动程序添加其他功能。希望您已经确定了各种例程的名称和目的,以及各自的参数和返回值。现在要做的就是实现和测试每一个。我们将在下一节中看到这样的例程。
5.一个或多个中断服务程序
在首次启用中断之前,最好设计,实施和测试大多数设备驱动程序。找出中断相关问题的根源可能相当具有挑战性。而且,如果您在其他驱动程序模块中添加可能的错误,甚至可能无法进行。使用轮询来获得驾驶员的勇气更好。这样,当您开始寻找中断问题的根源时,您将了解该设备的工作原理(确实有效)。而且几乎肯定会有一些。
7.3简单的定时器驱动
我们即将讨论的设备驱动程序示例旨在控制80188EB处理器中包含的定时器/计数器单元之一。我已经选择实现这个驱动程序 - 以及本书中所有剩下的例子 - 在C 中。虽然C 在访问硬件寄存器时没有提供C的额外帮助,但是有很多很好的理由将其用于此类型的抽象。最值得注意的是,C 类允许我们比任何C特性或编程技术更完全地隐藏实际的硬件接口。例如,每次声明新的定时器对象时,都可以包括构造函数来自动配置硬件。这消除了从应用软件到驱动程序初始化程序的显式调用的需要。此外,可以隐藏与相关类的私有部分内的设备寄存器对应的数据结构。这有助于防止应用程序员意外地从程序的其他部分读取或写入设备寄存器。
Timer类的定义如下:
enum TimerState { Idle, Active, Done };
enum TimerType { OneShot, Periodic };
class Timer
{
public:
Timer();
~Timer();
int start(unsigned int nMilliseconds,TimerType = OneShot);
int waitfor();
void cancel();
TimerState state;
TimerType type;
unsigned int length;
unsigned int count;
Timer * pNext;
private:
static void interrupt Interrupt();
};
在讨论这个类的实现之前,我们来看一下以前的声明,并考虑设备驱动程序的整体结构。我们看到的第一件事是两个枚举类型,TimerState和TimerType。这些类型的主要目的是使其余的代码可读。从他们那里我们得知每个软件定时器都有一个当前状态 - 空闲,活跃或完成 - 以及类型 - OneShot或Periodic。定时器的类型告诉驱动程序在定时器到期时该怎么办?那么一个周期性定时器将被重新启动。
Timer类的构造函数也是设备驱动程序的初始化程序。它确保定时器/计数器硬件每1毫秒主动产生一个时钟脉冲。区别启动,等待和取消的其他公共方法为易于使用的软件定时器提供API。这些方法允许应用程序员分别启动一次性和定期的定时器,等待它们过期,并取消运行定时器。这是一个比80188EB芯片内的定时器/计数器硬件提供的界面要简单得多的通用接口。一方面,定时器硬件不知道人类的时间单位,如毫秒。但是由于定时器驱动程序隐藏了这个特定硬件的细节,所以应用程序员甚至不需要知道这一点。
该类的数据成员还应该帮助您深入了解设备驱动程序的实现。前三个项目是回答有关此软件计时器的以下问题的变量:
什么是定时器的当前状态(空闲,活动或完成)?
什么类型的定时器是(单次或周期性的)?
计时器的总长度(以刻度为单位)是多少?
以下是另外两个数据成员,它们都包含特定于定时器驱动程序实现的信息。 count和pNext的值仅在活动软件定时器的链接列表的上下文中有意义。此链接列表按每个计时器剩余的刻度数排序。所以count包含有关软件定时器设置为过期之前剩余的刻度数的信息,[1],pNext是指向软件定时器的指针,该指针
剩余内容已隐藏,支付完成后下载完整资料
资料编号:[139762],资料为PDF文档或Word文档,PDF文档可免费转换为Word
您可能感兴趣的文章
- 饮用水微生物群:一个全面的时空研究,以监测巴黎供水系统的水质外文翻译资料
- 步进电机控制和摩擦模型对复杂机械系统精确定位的影响外文翻译资料
- 具有温湿度控制的开式阴极PEM燃料电池性能的提升外文翻译资料
- 警报定时系统对驾驶员行为的影响:调查驾驶员信任的差异以及根据警报定时对警报的响应外文翻译资料
- 门禁系统的零知识认证解决方案外文翻译资料
- 车辆废气及室外环境中悬浮微粒中有机磷的含量—-个案研究外文翻译资料
- ZigBee协议对城市风力涡轮机的无线监控: 支持应用软件和传感器模块外文翻译资料
- ZigBee系统在医疗保健中提供位置信息和传感器数据传输的方案外文翻译资料
- 基于PLC的模糊控制器在污水处理系统中的应用外文翻译资料
- 光伏并联最大功率点跟踪系统独立应用程序外文翻译资料
