您的位置  > 互联网

嵌入式系统驱动程序的开发有别于或或Linux

驱动程序通常分为两层:层和API层。 前者是真正驱动硬件设备的程序,后者是负责与系统或应用程序交互的接口,向外界隐藏了硬件的特性和细节。 如果以后要更换硬件,只需要修改驱动层,API层不变,整个应用程序和系统程序不需要修改。

下面是一个简单的驱动示例,为上层应用提供关闭中断的功能。

// 驱动程序内部函数(Driver层)
void hw_clear_interrupt_flag(void)
{
    //设定CPU状态寄存器内的某位
    //
    asm("pushn  %r0");
    asm("ld.w   %r0, %psr");
    asm("xand   %r0, 0xffffffef");
    asm("ld.w   %psr, %r0");
    asm("popn   %r0");
}
// 驱动程序对外接口(API层)
void drv_clear_interrupt_flag(void)
{
    hw_clear_interrupt_flag();
}

固件组根据硬件特性和最终产品需求定义驱动API的初稿,然后提交给系统组参考。 然后根据系统组的反馈修改、添加或删除API。

1、开发驱动程序前的准备工作

在整个系统的开发中,驱动程序是一个没有机会展现个人创造力的工作。 必须按照CPU或外围IC规定的步骤进行开发和编写。

(1) 收集可用资源

以下是嵌入式驱动程序开发应寻求获得的资源:

(2)用C语言编写驱动程序

驱动程序开发一般用C语言编写,可以在需要的地方内联汇编:

//其他一些CPU将9分配给寄存器r1。 语法 ldw 9, %r1 ```

(3)API设计

除了操作系统之外,其他系统程序和应用程序只能通过该层提供的API接口来间接操作硬件。 因此,在嵌入式系统项目开发的早期阶段,驱动设计者必须根据产品规格和硬件架构开始设计API。 我们应该关注系统和应用需求; 对于硬件设计,我们只需要确保它能够实现这些API的功能即可,电路细节不会影响API的设计。

其实我们可以先编写包含空函数的.h头文件和.c源文件,这样就不会影响系统的编译,可以同时开发其他程序。 该层也成为硬件抽象层HWL,因为它的上层程序与硬件无关。

2.控制CPU

目前日全食系统中使用的CPU一般会集成许多外围器件(LCD、NAND Flash、SDRAM、USB控制器等),以降低成本和设计难度。 下图是嵌入式CPU的内部架构图:

因此,CPU是驱动工程师首先处理的最重要的设备。 下面将CPU需要设置的功能一一说明。

(1) 内部寄存器

同一CPU核的内部寄存器和汇编语言规则是相同的,但不同型号的外围设备可能不同。 下图是一些嵌入式系统的CPU寄存器列表:

CPU内部寄存器通常可分为可直接赋值的寄存器(如通用寄存器、堆栈指针寄存器、状态寄存器)和只读寄存器(程序计数器寄存器),只能通过跳转或调用来改变执行顺序。

(2) 注册

外围设备分为CPU外部的分立设备和集成在CPU内的片内外围设备。 前者通过CPU的引脚相互传输控制或数据信息; 而后者则需要通过CPU内部特定的寄存器(寄存器)来传递控制和数据信息。 这里的寄存器与CPU内部的寄存器本质上完全不同。 内部寄存器有自己的名字,而内部外设的寄存器只有一个地址,这种控制内置芯片的方式称为meory I/O! 例如,一个内置外围设备寄存器的地址是,那么对该寄存器赋值的语句可以写为: (char *) = 0x0;

CPU除了控制内置外设的寄存器和操作CPU Core的基本功能外,还可以控制CPU的Pin引脚(I/O端口)。 一般为了方便读写,在头文件中将这些特殊地址定义为宏常量:

// ALE与CLE是CPU与NAND Flash chip连接的两根PIN脚
// 在控制NAND Flash时要频繁地将其拉高或拉低。
//
#define SET_ALE_H   *(volatile unsigned char*)0x402d9 |=0x20
#define SET_ALE_L   *(volatile unsigned char*)0x402d9 &=0x20

#define SET_CLE_H   *(volatile unsigned char*)0x402d9 |=0x10
#define SET_CLE_L   *(volatile unsigned char*)0x402d9 &=0xef

但也存在争议点。 有些书推荐直接使用地址和注释来编写代码。 这些人有不同的看法,智者也有不同的看法。 因为有些内置IC的寄存器太多,很难一一命名,别人读起来可能无法理解你的名字的含义。 还是要通过名称查头文件来获取地址,然后再查地址来了解寄存器。 意思是,所以最好直接在代码中使用该地址。

除了寄存器之外,在一些高级CPU(如X86系列)中,还使用Port I/O。 所谓Port I/O也是一个连续的地址空间,但它的寻址独立于CPU的内存空间。 CPU无法以地址或指针的形式对其进行访问,而必须使用特殊指令(由X86系列in和out指令提供)从特定端口读写数据。 例如,PC上的RS232使用端口0x3F8读取数据。

(3)中断处理器

驱动程序控制中断系统时应注意的事项:

(4)时钟

在将板子交给固件工程师之前,硬件工程师需要验证各个模块的供电电压是否正确,以及输入CPU的时钟频率是否正确。 这两个条件是CPU运行的基本条件。

通常,板上会有两个振荡电路,每个振荡电路产生一个固定频率的定时输入到CPU。 较慢的频率(通常)用于为处于待机状态的 CPU 提供计时。 较快的频率(48MHz左右)会通过CPU内部的PLL电路来增减频率,为其他模块提供时钟。 有些外部芯片的时序不需要与CPU同步。 此时,外部芯片可以有另一个独立的振荡源。 此时CPU与外部芯片可以使用串行、并行传输、IIC、IIS等通信协议进行通信。

(5) 总线与芯片

一般来说,总线分为数据总线和地址总线,所有存储器的对应端口分别连接到这两条总线上。 同时CPU会将整个地址空间划分为若干个域。 不同的域代表不同的地址范围和连接的存储器类型。 不同的PIN引脚也被指定为存储器的片选信号: 下图显示了某种CPU地址空间划分:

当然,不同的存储器有不同的存储模式,CPU也有专门的内部寄存器来设置外部存储器的行为模式,如下图所示:

理论上CPU在操作某块内存之前需要插入几个等待周期,是可以计算出来的。 CPU中有相关信息。 只要知道CPU的时序(如48MHz)和内存的执行速度(如90ns),就可以得到理论上的等待周期()数。 但实际上,我们通常会先把这个值设置得较大(CPU访问内存的速度会变慢),等系统稳定后再慢慢调整系统各模块的时序。

(6)GPIO端口

在使用CPU的各个引脚之前,硬件工程师首先要提供CPU所有PIN引脚的“配置与使用”表。 CPU的每个GPIO都可以设置为输入或输出端口,也可以设置为具有其他功能的端口。

输入引脚可分为两种类型:中断启动和非中断启动。 不产生中断的输入引脚相对简单。 程序只需通过CPU寄存器读取PIN引脚当前的状态(高电平或低电平)即可。 如果是可以产生中断的输入引脚,则需要找出中断向量表中哪个中断对应该PIN引脚,然后为其调用ISR。

触发中断的属性有两种:一种是边沿触发(Edge),一种是电位触发。

GPIO通过不同的状态和时序组合来达到控制外部IC的目的。 下面是某一次NAND Flash读操作的时序图:

编写驱动程序时,必须按照NAND Flash中的时序图,先依次改变各个PIN引脚的点位并向NAND Flash发出指令,然后在规定的时间内一一读取输入引脚的电位在时序图中。 获取NAND Flash的返回值。

下面是IIC接口设备的架构图。 由于它是串行通信协议,因此只需要两个引脚(一个负责时钟,一个负责数据交换)。 系统中的每个IIC设备都有自己的ID号,CPU通过这个号来确定选择进行通信的设备。

(7) Nop和延迟期

考虑以下简单的序列:

当CPU将CS从高电平拉至低电平时,IC开始动作。 T1时间过后,IC会将数据放到P#1端口上,维持时间为T2。 那么程序如何在正确的时间从P#1获取正确的数据呢?

通常执行数据检索的时间很短,不会超过T2。 假设T1=10ms,我们应该如何编写相应的驱动程序以及如何确定10ms的时间段?

/****************************************************
Function: drv_read_XXXIC_status
          return 0 if P#1 is low,1 if P#1 is high
*****************************************************/
int drv_read_XXXIC_status(void)
{
    int i;
    int P1_status;
    //将CS PIN设为低电平
    //
    drv_set_XXXIC_CS_Low();
    //等待10ms
    //
    for(i=0; i<10, i++)
        drv_wait_1ms();
    // 读出P#1 PIN的状态
    //
    P1_status = drv_get_P1_status();
    //根据时序图,将CS PIN恢复为高电平
    //
    drv_set_XXXIC_CS_High();
    return P1_status;
}

如何写一个延迟1ms的程序? 每个CPU都有一条NOP指令。 当执行这条指令时,CPU什么也不做,只是消耗一个或多个时钟。 我们可以循环执行该指令,以实现短暂的延迟,而不影响任何其他模块。 一般来说,CPU 都会有一张类似于下图所示的表。 它会告诉你CPU执行各种指令所需的时钟数,然后根据CPU执行频率计算出每条指令所花费的时间。

因此,一般会写成如下的延迟函数:

#define COUNTER_PER_1MS     25000

void drv_wait_1ms(void)
{
    int counter;
    //执行该函数首先要确保不会产生中断

    for(counter=0; counter<COUNTER_PER_1MS; counter++)
    {
        asm("nop");
    }
}

如何确定上述程序中的值?

我们首先将上面用C语言编写的程序转换为汇编代码(可以通过添加-S编译器来实现):

;不同的CPU有不同的汇编指令集
;以下程序旨在表达流程
    R0 = COUNTER_PER_1MS    ; 2个时钟周期
loop_start:
    nop                     ; 1个时钟周期
    R0 = R0 -1              ; 1个时钟周期
    jump to "loop_start" if R0 != 0 ;4个时钟周期

从上面的汇编代码我们可以看出,每个循环需要6个时钟周期,因此根据CPU的主频,我们可以很容易地计算出执行一个循环需要多少时间。

(8)电源管理

根据不同的产品特性可区分不同的功耗级别,一般可分为以下三个级别:

一般来说,省电可以从以下几个方面入手:

CPU:降低频率; 降低电压; 通过停止或睡眠指令; 关闭暂时不使用的设备的电源; 切换内部/外部设备的工作模式(Full Run Mode或Mode)或工作电压; 将CPU的各个PIN引脚切换为更省电模式,特别是一些F复合功能引脚。 通常GPIO会比其他设置(AD、数据总线等)省电; 输出引脚比输入引脚省电; 将输出引脚设置为低电平比保持高电平更省电; 如果GPIO被电路阻止上拉或下拉,逆势通常会消耗更多的电量; 以上原理不一定使用所有的CPU,请参考或代码中的说明。

固件设计者必须与硬件设计者沟通电路图的设计,了解电源管理相关的硬件设计:哪些器件有独立的电源、哪些器件的电流消耗是否可以独立测量、用哪个PIN脚来供电等。打开设备的电源开关。 例如:如果硬件设计者设计了一个控制IC电源开关在低电平关闭的PIN,但在CPU内部, PIN 被设计为上拉电阻,因此必须保持上拉电位。 在低电位时关闭外部IC会消耗更多电量。

总结:基于软件需求和硬件限制,系统设计人员必须总结产品应包含的所有电源模式,并在系统中设计一个电源管理模块进行统一管理和控制,以确定系统在什么情况下应该进行切换。 进入哪种电源模式,以及在某种电源模式下,CPU和系统中各个设备的功耗设置。 总体电源管理框架如下图所示:

驱动程序提供电源管理接口供电源管理模块调用。 系统和应用程序必须根据需要设置系统的功耗状态,但不需要知道所有设备的详细信息。 也就是说,电源管理模块必须提供抽象的电源模式和接口,供其他系统模块或程序调用。 同时,请注意以下情况:

(9) 停电前的处理

无论是低电压还是突然断电,系统可以确定的硬件事件是电压已经下降到某个临界值(电平或致命电平)。 此时,电池驱动程序会向系统发送低电压事件(系统可以定期检测电压并利用CPU内外的硬件功能来检测Br​​own-Out)。 通常,低电压事件发生后需要做什么取决于系统。 这取决于目的,但无论什么系统,最后一件事就是执行CPU的复位,无论是通过复位引脚还是复位指令。

(10)()控制

电池充电管理的主要目的是避免过充和过放:为了避免过充,快充满时必须放慢充电速度,当电池电压达到某一临界值时必须停止充电。 当电池几乎耗尽时,电池电压会突然急剧下降。 此时,系统必须停止或自动关闭,以避免功耗过大而造成永久性损坏。

正确的充电周期是:

小电流启动:为了防止电池电压过低,大电流充电会对电池产生影响。 恒流充电(CC):采用固定电流将电池快速充电至特定电压,此时充电速度应减慢。 恒压充电(CV):当使用恒流将电池充电到一定电压值时,必须使用小电流缓慢充电。 否则,由于电池内阻效应,大电流时会产生电压差,对电池造成影响。 的饱和度。 通常需要将充电电压控制在电池额定电压的正负50mV以内。

为了避免人身事故(电池爆炸),软件和硬件的设计必须尽可能的好。 除了基本的CC\CV模式切换之外,还必须检测有问题的充电器(输出电压过高或过低)。 ,不稳定),必要时停止充电并发出警告!

(11)ADC & DAC

ADC:只要外接芯片可以将模拟输入(温度、速度、磁场强度、压力、体积、湿度、振动强度、亮度等)转换成不同的电压输出,然后连接到AD口CPU中,程序可以计算输入到外部模拟。

DAC:当系统使用DAC IC时,只需关注DAC IC与CPU之间的接口(IIC或IIS)以及数据传输协议即可。 一般一些简单的应用(电机控制、亮度调节、语音输出等)可以直接让CPU模拟信号方式——PWM(脉冲宽度调制)。 如果主控IC不提供PWM输出,也可以使用定时器来控制某个IO引脚的输出周期(占空比)来模拟。

(12)看门狗

如果系统在硬件设计中没有复位按钮,那么就需要在软件中安装一个看门狗,以随时监控系统是否崩溃。 如果驱动程序发起复位中断,该中断的ISR会主动复位系统。 但本质上看门狗是一个定时器。 它将从系统设置的某个值开始倒计时。 当计数到零时,将产生最高优先级的中断或直接复位CPU。

为了防止看门狗重置系统,我们必须设置一个喂狗程序,在计数器倒数到零之前重置计数器的值。 因此,这个初始值的大小必须仔细规划。 如果太小,一些正常运行但耗时的程序可能会被看门狗错误地重置,因为系统在运行完成之前就崩溃了。

Linux系统内置了一个功能,用于监控系统的运行情况。 一旦应用程序调用了dev/虚拟设备,就相当于指示内核启动一个1分钟的定时器。 应用程序必须在 1 分钟内将数据写入该设备。 每次写入数据时,都会将其重置。 如果不写的话,会导致系统超时。 另外,Linux还提供了一个名为 的应用程序,它可以定期检测系统的以下方面:

如果某个测试出现问题,应用程序会触发软重启(Soft),也可以通过dev/触发。

(13)CPU初始化

CPU也是系统设备之一,当然也需要驱动程序,驱动程序必须在其他程序开始执行之前初始化CPU。 主要包括:

设置CPU内部寄存器(PSR、SP等); 设置中断向量表; 设置CPU内部各单元(CPU核心和内置设备)的时钟; BUS设置:设置每个外部存储器的特性,包括要插入多少次、操作存储器的基本单元宽度(16bit或32bit)等。设置CPU每个PIN引脚的用途。 设置CPU的执行模式。 首先应该是在完全运行模式下对其他内部设备进行初始化。

这些在驱动中必须区分为独立的模块,如Timer、Timer等,对于上层应用来说,它们应该是独立的模块,只需要调用这些模块提供的API即可。

(14)CPU内部还有哪些其他RTC:Timer定时器一般用于精确计时(毫秒或微秒级别),但RTC是用于粗粒度计时(单位是百分之一秒)的定时器,程序可以通过循环将其调整为1秒甚至1分钟,然后再产生中断,以实现时钟或日历的功能。 DMA:运行外部设备(硬盘控制器、显卡、网卡、声卡等)直接与内存通信,无需CPU的全面干预。 用于嵌入式系统的CPU将具有内置DMA控制器,因此可以通过寄存器来控制它们。 在数据传输之前,驱动程序必须首先设置源地址、目标地址和数据大小。 MMU:内存管理单元,实现虚拟存储空间、页映射等内存管理功能。 一般能运行大型操作系统(Linux等)的CPU都需要具备该功能。 其他内置IP:一些针对特定应用领域设计的CPU会内置需要使用的功能模块。 例如MP3 CPU会集成解码器、LCD控制器、NAND Flash控制器、USB控制器等,此时一般不需要控制CPU的IO口。 制造商将指定特殊寄存器来控制这些设备。3. 记忆

在嵌入式设计中,出现内存问题的机会比您想象的要多,因为程序依赖内存来执行。 一般来说,记忆可以分为以下几类:

地址可读只读存储器(Mask ROM):用于存储执行过程中不会改变的程序和数据。 程序可以直接在上面运行,除非有性能要求,否则不需要加载到内存中。 可按地址读写的RAM:RAM的内容在执行过程中可以改变,分为两类: RAM:需要持续供电来保存SRAM中存储的数据,功耗低,接线简单但价格较高。 RAM:需要不断动态刷新来保存DRAM中存储的数据。 因此,需要额外的DRAM控制器来设置DRAM容量、存取时序和其他特性以正常使用DRAM。 一般出于成本考虑,嵌入式系统会选择SDRAM(动态随机存取存储器)。 CPU只需向SDRAM提供连续稳定的时钟,而且比DRAM便宜,一般用于系统缓冲区。 NOR Flash,可以用地址读,但必须用命令写:虽然可以改变里面的数据,但不像写RAM那么简单(可以通过地址和数据总线来完成)。 CPU需要向它发出命令。 并且对NOR Flash的写入必须以块(Block)为单位,通常大小为16KB或8KB,并且必须先擦除块。 正因为如此,NOR Flash的写入性能很差,一般用来作为ROM的替代品,以避免ROM无法更改的缺陷。 另外,它还可以用来存储或备份执行期间的数据,因为断电后数据不会丢失。 例如用户配置、记录数据等。 只能用命令读写的NAND Flash:不能直接读写,必须使用特定的命令。 读取的单位为页(Page),大小通常为0.5KB或2KB; 写入和擦除的单位是块(块是多个页的集合)。 但NAND Flash的一个问题是它会像硬盘一样产生坏扇区(Bad Block),因此它可以用作更小的硬盘,因为它的成本比NOR Flash小得多。 因此,NOR Flash通常用于执行程序,而NAND Flash用于存储大量数据。 4. 控制其他芯片

根据应用类型,控制外部 IC 有多种标准:

5、编写ISR的注意事项。 尽量不要在 ISR 中使用全局变量。 如果必须使用,请保护它们(关键部分/ ),即进入前端中断。 ISR的第一个动作必须是按顺序将所有CPU内部寄存器压入堆栈,ISR的最后一个动作必须是按相反的顺序弹出堆栈。 恢复CPU内部寄存器的值。 调用外部函数时,确认是否是“可重入”(可以被多个任务同时调用的程序,而不用担心数据损坏。即可重入函数可以随时中断,然后继续执行,而无需担心数据损坏)。函数中的数据会受到影响,因为函数中断时会被其他任务重新调用)。 如果必须在 ISR 内调用不可重入函数,则一般程序在使用同一模块中的函数时也必须受到保护。 ISR 程序越简单越好。 执行时间应尽可能短,并且尽量不涉及复杂的算法。 它的任务只是通知(通过-queue数据结构,列出与该数据结构相关的所有函数或操作)上层应用程序发生了哪些硬件事件,留给上层应用程序来决定如何处理它们; 在ISR中调用系统函数时一定要小心,特别是与任务切换相关的函数。 如果要调用,就调用系统提供给ISR的专用系统函数。 .6. 驱动调试

证明驱动器运行正常最直接的方法是用示波器在正确的PIN引脚上测量与时序图相同的信号。 另外,如果条件允许,还可以使用逻辑分析仪记录一段时间内多个PIN引脚的信号。