高通驱动开发参考(二)

来源:互联网 发布:linux 清除arp缓存 编辑:程序博客网 时间:2024/04/30 19:52

第1章 Driver相关模块介绍

1.1 REX简介

虽说目前QSC60x5平台上采用L4操作系统,REX只是L4上面的一个Task。但高通为了开发的兼容性,提供的API仍然采用老的一套接口(可能内部实现不一样),很容易将老的代码移植到新的架构中。那么我还试必要介绍一下REX

REX是一个操作系统的名字,它是Real-time Executive的缩写。它是美国的QUALCOMM开发的,REX是一个简单的、高效的、抢占式的、多任务的、嵌入式实时操作系统。它最初是为应用于Intel80186而设计的,如今它已经被移植到了ARM微处理器上。

下面是它比较重要的几个概念(其实和其他操作系统一样):

1、 Task

REX把任务当作独立运作的实体,每一个任务都有自己独立的堆栈和优先级。每个任务都有一个数据结构,我们称为任务控制块”(TCB)REX允许在运行时,在任何时间动态的创建任意数量的任务。但是创建的任务越多,REX的性能便稍微递降,这是因为你创建的任务越多,任务列表越长。所以你需要确保你创建的任务数量尽量小。另外,REX任务的负载量还跟你选择的处理器有关。

下面的图描述了REX任务的启动过程:

TCB也是一个比较重要的结构体,各个task都有一个全局变量tcb,这个tcb包含了很多重要信息如,任务优先级、堆栈指针,信号量等,这些有时对查死机问题很有帮助。

2、 任务调度

REX总是选择处于就绪态的最高优先级的任务进行调度,如果任务的优先级不是唯一的,REX可能选择任何一个处于就绪态的最高优先级的任务进行调度。被选择的任务会一直执行,直到它自愿挂起或者中断发生而激活了更高优先级的任务。 

当被挂起的任务的等待条件得到满足时,该任务就变成了就绪态。如果所有的认为都挂起的时候,那么空闲认为就被执行。 

REX提供了一种机制,允许任务可以动态的提高或降低自己或其它任务的优先级。 

3、 信号量

信号量是任务之间进行通信和同步的桥梁。信号量是REX提供的一种任务间通信的机制。每个任务都有一套与之相关联的通用信号量,信号量通常作为任务的TCB的一部分,它们被用来通知任务某种事件的发生,任务的信号量可以被任何其它任务或中断设置或清除。 

信号量实际上是一种约定机制,在多任务内核中普遍使用,信号量用于: 

��� 控制共享资源的使用权(满足互斥条件); 

��� 标志某事件的发生; 

��� 使2个任务的行为同步。 

4、 中断

REX是一个可剥夺的内核。当从中断返回的时候,控制权就被传递给处于最高优先级的就绪任务,而没有必要返回到被ISR中断了的任务处。 

一般我们只会挂载GPIO中断,后续会介绍。

5、 定时器

定时器,英文是timer。和其它内核一样,REX要求给用户提供定时时间,来实现超时控制等功能。REX中有些信号是事件触发引起,也可以由timer引起。一旦由于某种原因,等待的事件信号一直等不到,这时可以用timer定时来产生信号量。 

关于以上几个概念,高通都提供了相关接口操作,具体参看代码!

1.2 Boot

高通平台系统Memory组成可以分为:NAND+SDRAMNOR+PSRAM 。两者配置不能并存,这也就决定了有两种boot模式,具体决定采用哪种Boots方式是Boot_mode引脚决定。

高通60x5系列采用了Multi-Image-Boot技术,这可以从高通的代码看出,


从上述枚举变量中可以看出,flash中有多个image.其中QCSBL+OEMSBL =SBLSBL就相当于bootloader, Bootloader主要是在Nand启动方式中起作用!原因后续介绍!

其中每个Image又是由三个部分组成:

其中签名段和证书段是用来校验代码段的完整性与合法性的。 在启动过程中就可以看到校验的相关步骤。代码段就不需要我说了吧。

1.2.1 Nor Boot

Nor flash有地址线,存在Nor Flash的代码可以本地执行,变量存在PSRAM,所以从NOR启动不需要Bootloader

NOR启动模式太简单,一般只要将Nor Flash挂载到EBI1上,然后做些简单的操作,就可以直接运行。在高通代码中关于这方面的注释也很少。

1.2.2 Nand Boot

Nand Flash没有地址线,代码不能直接运行,因此需要Bootloader,那么bootloader的作用:

可以看出相对NOR boot,Nand boot复杂很多。它需要将falsh中的Image复制到SDRAM,然后才能执行。并且要对各个image进行校验。

1.2.3 Boot流程

Boot是一个很复杂的过程,尤其是Nand Boot,需要你对ARM架构很深的了解,这边只是简单示意相关流程,具体大家可以参考代码!

1.3 Gpio

1.3.1 GPIO的作用

GPIO 作为QSC60X5 与外界沟通的桥梁,主要有四个作用:

1. 通过GPIO 向外部输出一个高/低电平,控制外部的器件或者通知外部器件某事件的发生。

例如我们可以通过GPIO 输出一个高电平点亮一个LED, 或者输出低电平关掉一个LED

2. 通过GPIO 读入一个外界的高低电平输入,检测外部器件的当前状态。例如键盘按键是否

按下的探测。

3. 将GPIO 口作为外部中断信号的一个输入口,实时检测外部事件的发生。

4. 将GPIO 用作其他特定用途。例如用作I2C 通信、数据线、地址线等。

1.3.2 GPIO的用法

1. 配置一个GPIO 口。通常一个GPIO 有多种功能,我们可以将GPIO 配置为符合我们当前需要的功能;同时我们也可以将GPIO配置为内部具有上拉电阻、下拉电阻或者没有任何上下拉电阻。文件GPIO_60x5.c 中定义了配置GPIO 的函数接口

void gpio_tlmm_config(GPIO_SignalType gpio_signal)

{

}

gpio_signal 指定要配置成的功能,文件GPIO_60x5.h 枚举出了每个GPIO 的可配置的功能选项:

typedef enum

{

GPIO_OUTPUT_3          = GPIO_OUT(3,0),

GPIO_INPUT_3           = GPIO_IN(3,0,GPIO_PULL_DOWN),

SDCC_DATA0             = GPIO_ALT(3,0,1,GPIO_PULL_UP),

DBG_BUS_IN_7           = GPIO_ALT(3,0,2,GPIO_PULL_DOWN),…

}

例子:

SDCC_DATA0 = GPIO_ALT(3,0,1,GPIO_PULL_UP) gpio_tlmm_config(SDCC_DATA); /* GPIO3 配置为SD卡 的数据线1*/ gpio_tlmm_config(GPIO_INTPUT_3); /* GPIO26 配置为通用的有下拉电阻的输入端口*/ /*2个参数0表示配置成通用功能,generic i/o*/

2. 从GPIO 输出高/低电平。首先应该通过函数接口gpio_out来确定GPIO输出高电平或者低电平,然后通过函数接口gpio_tlmm_config把该GPIO配置为通用功能(非特定功能),在该配置函数中调用一个宏函数接口BIO_TRISTATE 打开GPIO 使能,将此电平输出出去。

void gpio_out(GPIO_SignalType gpio_signal,GPIO_ValueType  gpio_value) /*GPIO 寄存器设置输出的电平*/ BIO_TRISTATE(io, mask, val) /*GPIO 寄存器中的值输出出去:输出使能*/

例子:使GPIO 31 输出低电平

gpio_out(GPIO_OUTPUT_31, GPIO_LOW_VALUE);

gpio_tlmm_config(GPIO_OUTPUT_31);     

3. 从GPIO 读入外部器件输入的高/低电平,检测外部的事件或者状态。只能从一个输出已被disable GPIO 读入输入的高/低电平,也就是说只能从一个已被设置为输入模式的GPIO 读入高/低电平。从GPIO 读入外部输入的高/低电平的宏函数接口:

GPIO_ValueType gpio_in

(

GPIO_SignalType gpio_signal

), 

gpio_signal解释见上面的说明,而返回值

typedef enum

{

    GPIO_LOW_VALUE  = 0,

    GPIO_HIGH_VALUE = 1

} GPIO_ValueType;

例:判断GPIO3的当前状态 

gpio_tlmm_config(GPIO_INTPUT_3);

if (gpio_in(GPIO_INTPUT_3) == GPIO_LOW_VALUE)

{

…..

}

4. 将GPIO 设置为某个中断信号的输入口。这样外部器件一旦有中断信号(高电平或者低电平)输入到此GPIO 端口,将直接触发一个中断,指定的ISR 将被调用,处理中断事件。中断的好处在于中断事件可以实时得到处理,无论系统是否处于睡眠状态。接口函数有两个:

A)gpio_int_set_detect(gpio_int_type which_group_int, gpio_int_detect_type detect) /*指定边沿触发还是电平触发*/

B) boolean gpio_int_set_handler

(

gpio_int_type which_group_int,

gpio_int_polarity_type polarity,

gpio_int_handler_type handler

) /*指定ISR 以及触发极性*/

typedef enum

{

/* GPIO_GROUP 1*/

GPIO_INT_0 = 0,

GPIO_INT_1,

GPIO_INT_2,

GPIO_INT_3,

GPIO_INT_4,

} gpio_int_type;

typedef enum

{

DETECT_LEVEL = 0,

DETECT_EDGE

} gpio_int_detect_type;

typedef enum

{

ACTIVE_LOW = 0,

ACTIVE_HIGH

} gpio_int_polarity_type;

A) 参数which_group_int 指定GPIO , detect 指定中断是电平触发方式还是边沿触发方式。

B) 参数which_group_int 指定GPIO, polarity gpio_int_set_detect 中指定的detect 值相关, 若detect 指定电平触发方式,polarity 就表示指定中断是高电平触发还是低电平触发;若detect 指定边沿触发方式,polarity 就表示指定中断是上升沿触发还是下降沿触发。handler 指定该中断的ISR,若handle NULL 则表示取消此GPIO 的中断处理,

也就是说以后此GPIO 输入任何信号都不会触发中断了。

例子:/*GPIO 3 设置为中断高电平触发*/

gpio_int_set_detect (GPIO_INT_3, DETECT_LEVEL );

gpio_int_set_handler(GPIO_INT_3, ACTIVE_HIGH, &GPIO_3_isr);

1.3.3 GPIO注意事项

1. 必须保证我们要用的GPIO 没有已被其他地方用作其它用途,否则可能会出现一些莫名其妙的现象。

2. 确保GPIO 已被正确配置。每个GPIO 都有缺省的配置,如果缺省的配置不符合我们的要求,我们就需要对GPIO 进行重新配置。

3. 注意GPIO 的上拉电阻或者下拉电阻的设置情况。例如:将GPIO 设置为有下来电阻输入口,若外部输入的高电平电压不够高,就可能导致读入电平介于高低电平电压之间,从而无法准确辨别出高电平还是低电平。

1.4 内存管理

高通平台上内存管理机制由三个部分组成:队列(Queue)、DS存储池、Watermark。其中真正存放数据的DS存储池,其他两个则是管理机制。

1.4.1 队列(Queue

• Single-linked list of data blocks (数据块单链表) 

• Each data block has a link field to the next data block (每个数据块有一个指针域(link field)指向下一个数据块) 

• Always use queue functions to use the queues (总是使用队列函数来操作队列) 

– Declared in queue.h 

– q_init( ) – initializes the queue (初始化队列) 

– q_link( ) – initializes the link field of the item (初始化队列的指针域)

– q_get() – removes the element from the head of the queue (从队列头部取出一个元素并将其从队列中去除) 

– q_check() – returns the pointer to the first element; does not remove (返回队列的第一个元素的指针,但不删除它) 

– q_put() – puts the element at the tail of the queue (添加一个元素到队列尾部) 

– q_cnt() – counts the number of elements in the queue (计算队列中有几个元素) 

The model of Queue 

队列广泛使用在DS的存储池中,贯穿于整个DS的相关代码中。 

1.4.2 DS存储池

在高通软件平台上,它的数据存储方式比较特别。事先划分一块区域用于数据存储,并且为不同应用再细分一个Pool(池),这个Pool是由固定大小item(用于存储数据块)组成。应用不同,这个item的数据容量也不一样。每个用户申请的就是这个item.它的具体特点:

• Statically allocated memory in RAM (静态分配于RAM中) 

• A number of DSM pools (不止一个DSM pool) 

– Each pool is allocated as a static array(每一个pool都是以一个静态数组的形式分配空间的) 

– Flow control is built into each pool based on the number of free items many, few, dont_exceed (流控制被构造于每个DSM pool中) 

• Each pool has a fixed number of items (called DSM items)(每个存储池都有固定数量的item成为DSM items) 

– Each DSM item in each pool has the same fixed number of payload bytes (在同一个存储池中的所有DSM item都有相同的有效字节数) 

– All DSM items across all pools have the same overhead bytes(所有存储池中的所有DSM item都有相同的字节数?) 

• Can link each DSM two dimensionally (每个DSM都可以以二维方式连接构造) 

– link_ptr for queues (队列方式) 

– pkt_ptr for items (条目方式)

使用时,只要将数据Copydata_ptr,并将数据长度赋给used,其他成员在申请,系统已经初始化好了。注:Copy时要注意每个Item最大数据容量—size.如果一个不够,再申请一个,强制copy一个超出范围的数据,会将Pool污染。导致后续莫名死机。

1.4.3 Watermark –flow control point

Watermark用于提供通用数据流控制解决方案,具体怎样实现可以从它的结构体和代码入手分析:

可以看出,一个watermark中包含了字节数(current_cnt)、底限(lo_watermark)、上限(hi_watermark)以及各种call back指针(hiwater_func_ptr)和队列Q的指针等。

当上层应用(比如DS)通过底层设备来进行数据传输时(具体流程我们会在SIO章节中介绍),他们共同维护watermark,高通提供一些使用watermark的函数。

以下是代码中提供的几个watermark相关函数接口: 

dsm_enqueue(wmark, item) 

– Adds an item to the watermark(添加一条记录到watermark中) 

– Calls each_item_func(), if any(调用函数each_item_func()) 

– If this is the first item in the watermark, calls non_empty_func_ptr(如果这是watermark的第一条记录(队头),则调用non_empty_func_ptr) 

– If highwater mark in bytes is reached, calls hiwater_func_ptr(deassert CTS)(如果字节数到达了watermark上线则调用hiwater_func_ptr) 

– If it does not_exceed_cnt bytes in the watermark, drops the item(如果item中的字节数没有达到要求则忽略这个item) 

dsm_dequeue(wmark) 

– Removes the first item from the watermark(从watermark中去掉队头(出队)) 

– If no other items are left in the watermark, calls gone_empty_func_ptr(如果watermark中已经没有item了,则调用gone_empty_func_ptr) 

– If low-water mark is reached, calls lowater_func_ptr(assert CTS)(如果到达watermark底线,则调用lowater_func_ptr) 

注意: watermark counts是指byte数而不是item数。 

可以看出,dsm watermark就是通过对队列的出队入队的管理来实现流控制的。

DSM入队操作中,其中有这么一句:

这里就是对队列进行watermark监测,下面看watermark监测做了什么事情。 

上面的代码可以看出,在入队时,根据队列的情况进行了相应情况的call back调用。换句话讲,就是当队列发生变化时,watermark将调用不同的函数以响应这些变化。 

可见,watermark可以看成是一种基于队列操作的队列管理机制。示意图如下

 

1.5 Sleep

1.5.1 Sleep硬件相关知识

一般我们直观观察高通是否进入睡眠可以通过数字电源来观察,那么有必要对睡眠相关的Sleep电流概念有所了解:

CDMA 手机的Idle 状态的电流如上。有个概念描述如下:

Average sleep current: 0.4-0.9mA.根据手机功能,显示屏的要求不同而不同。

Average Rx current:手机处于睡眠状态时,需要定时从基站接收消息。此刻手机接收系统唤醒。正常的手机paging 应是规则的。

Idle 电流: 是指手机 sleep current Rx current 的时间平均值。影响手机Idle 电流的主要因素是sleep 底电流。RX 电流的宽度和高度在芯片组和协议栈定好以后,一般开发者没有能力改变。

1.5.2 Sleep软件相关知识

在高通平台上,Sleepsleep task进行管理, sleep任务的优先级最低(优先级2). 当没有其他任务使用CPU调度器才会让sleep task获得CPU的使用权限, sleep task会根据当前的任务调用状态决定CPU是否可以进入睡眠状态如果判定当前状态可以进入sleep状态则通过sleep_power_down_and_halt关闭主晶振TCXO及其他LDO电源以获得省电效果。具体流程如下图所示:

Sleep鉴权:

sleep_power_down_and_halt能否关闭TCXO及其他LDO电源以获得省电效果由sleep鉴权机制决定

Sleep鉴权机制大体可以分为两块鉴权部分:

1) sleep任务本身是否容许进行睡眠

2) 是否有其他任务不容许进行睡眠

1.5.3 查Sleep电流方法

1. 首先确保所有 task 都可以进sleep

Sleep.c 里面添加代码,把所有not_okts task log 打出来如果这时只有SLEEP_DIAG_OKTS_SIG SLEEP_USB_OKTS_SIG 则正常.

再把上面加的代码去掉用示波器测量TCXO, 看是否有周期性关闭如果有则正常,没有则需查diag usb task 是否有问题.

2. 在 TCXO 关闭基础上,查板子是否存在漏电

测量板上所有电阻两端是否存在压降;

把板子一些无关器件吹掉;

测试PM 输出的几个电压值是否正常

1.6 SIO

1.6.1 SIO模块的架构

在高通平台,SIO模块的整体架构如下图所示:

Service layer主要实现相关服务,如DIAGDATA等等,Control layer主要负责设备与服务的管理,并且加上相应管理机制。Device layer可以分为两层,SIO Interface向上层提供统一的接口,隐藏了硬件的相关操作。SIO Device driver实现相关硬件驱动。

开机时,在tmc_init函数中,会依次调用rdm_get_prev_portrdm_issue_open函数,rdm_get_prev_ports()的作用在于读取保存在efs中的设备映射表。在设备映射表中保存了上次断电前的设备映射表。读出设备映射表内容将其放入到rdm_current_device_map[ RDM_DEV_MAX ]之中。 接着调用rdm_issue_opens(),根据读出的rdm_current_device_map[ RDM_DEV_MAX ]的内容,打开相应的port。 

RDM的作用在于动态的修改设备映射关系,那么在应用中可以动态修改设备映射关系。只要调用相关接口rdm_assign_port就可以。

1.6.2 SIO数据处理流程

在上一节介绍了高通平台中SIO模块的整体架构,但我们经常涉及到其中一部分(device layer),通常也是修改这一部分。在这一节我们将详细介绍SIO的数据处理流程。

SIO数据接收: 

RX FIFO中如果有数据,则会从DSM Pool中申请得到一个DSM item,然后将RX FIFO中的数据装载到这个DSM item中,然后将这个DSM item入队到对应的watermark中,watermark在有新的DSM item加入时会进行监测,看数据是否达到要求以调用不同的call back,发出信号通知application来处理这些数据。 当然也可以直接通过回调函数rx_fun直接处理DSM item

SIO数据发送: 

数据发送前会先申请一个DSM item,然后将要发送的数据填充到dsm item中,如果TX FIFO为空,则直接将这个dsm item中的数据传送到TX FIFO中,如果TX FIFO不为空(发送器正忙),则将这个dsm item入队到相应的water mark中,watermark会在适当的时候将这些数据发送到TX FIFO中。

1.7 重启问题分析

万事皆有根源。 重起及异常无非就是软件和硬件的原因;我们在分析的过程中通常需要排除,定位。 硬件上的异常现象通常表现在: 电压过低(或限流过低)2. 程序跑飞,没有规律(这通常是flash的原因,时序太紧了);等等这些原因用调试器,软件的方法是没法查找的,是一些经验问题,在驱动调试阶段一般可以解决。 还有就是软件上的问题, 这些问题我们一般都可以寻根溯源, 在异常向量表的地方打断点,逐步尝试, 一般可以定位到:指令没定义,指令预取异常, data abort, 或者就是reset。 这些原因我们都可以根据r14 的值来回溯, 查找; 这些方法技巧做嵌入式的软件工程师都应该有所了解,加以掌握。

1.7.1 软件狗检测异常

通常重起时,我们跟踪会在0地址打断点, 然后根据r14(或者r144r148)的值进行回溯, 一般可以找发生重起的地方, 然后再分析原因。 通常有两种情况:

1. 就是dog task 跑起来之前重起, 通常是在初始化过程中没有及时踢狗, 这时要审查代码在适当的地方(处理费时的地方)加入硬件踢狗, 多试几处, 通常可以解决问题;

2. 就是dog task 跑进之后重起,就是下面所描述的。

QSC60x5系列的平台里面有一个优先级最高的任务Dog task,它用来监测其它task是否在正常运行,如果某一任务陷入了死循环,或者长时间占有系统资源不释放,导致其它任务不能及时的得到调度,这时候dog task会认为当前的任务调度出现了问题,当dog task认为在规定的时间之内某一个task 还没有给自己做dog_report()的动作时,它就认为该任务已经死了,所以dog task接下去会去调用:err_fatal_put_log ( 0,(char*)&(dog_task_info[loop_index].task))

发生这种情况时,我们需要先确认是否真的是由于dog task检测的原因导致重启的,通过调试器在spider上可以跟踪。步骤如下:

如上图,将断点设置到dog.c文件的

err_fatal_put_log ( 0,(char*)&(dog_task_info[loop_index].task))处,如果设置了你需要的debug操作后,程序进入到这里,那么很可能就是某一个task没有给dog taskdog_report()导致的原因。具体还可以定位到是哪一个task没有做dog reprot导致超时的。这可以再深入研究里面的代码。

1.7.2 发生data abort导致重启

ARM中有一种异常称之为data abort。当PC指针指向一段未定义区域并访问(读或者写)的时候,系统会发生data abort异常。这时候PC指针会自动跳转到0x10的地方执行data abort的处理程序.目前QSC60x5平台同样在这个时候设置了一个死循环,导致硬件狗超时并重启。Data abort的情况在实际开发过程中也经常发生,数组越界,指针乱指,都有可能导致data abort异常发生。

发生了data abort的时候可以将断点直接设置到0x10 data abort的程序入口处,通过回溯R14寄存器来找到发生data abort的地方:R14 – 8的地方就是发生data abort的地方。

1.7.3 指针未初始化引起的系统重启

下面这个代码说明了一个极为常见的错误:

int  *a;

*a  =  12;

这个声明创建了一个名叫a的指针变量,后面那 条赋值语句把12存储在a所指向的内存位置。但是a指向那里呢?我们声明了这个变量,但从未对它进行初始化,所以我们没有办法预测12这个值将存储于什么地方。从这一点看,指针变量和其他变量并无区别。如果变量是静态的,它会被初始化为0;但如果变量是自动的,它根本不会被初始化。无论是那种情况,声明一个指向整型的指针都不会创建用于存储整型值的内存空间。由于指针没有被初始化所以这个指针包含一个随机的合法的地址。接下来的事情很简单:位于该随机的合法的地址位置的值被修改,虽然操作者并无意去修改它。这种类型的错误非常难以捕捉,因为引发的错误可能与原先用于操作那个值的代码完全布相干。所以,在呢对指针进行间或访问之前,必须非常小心,确保它们已被初始化。

1.7.4 注意事项

1、将使用到的比较大的栈变量使用static修饰,或者改为MALLOC,使用堆空间。

2、避免使用递归。

3、函数调用的参数使用结构指针,不要使用结构体做为参数。

4、使用指针变量之前必须初始化

5、在写数组或者MEMSET中,检测边界

6、在大段程序执行注意踢狗

总之最重要的是仔细!

 

原创粉丝点击