完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
DSP处理器是一种嵌入式处理器(embedded microprocessor),它专门用于数字信号处理,其在系统结构和指令算法方面进行了特殊设计,具有很高的编译效率和指令执行速度。
|
|
相关推荐
3个回答
|
|
嵌入式系统在当今是一种非常活跃的应用,在工业、服务业、消费电子等领域的应用范围都不断扩大。为了方便嵌入式系统的开发,人们研发了许多嵌入式操作系统,如WinCE、uc/OS、嵌入式Linux、VxWorks、pSOS、QNX、Palm OS等,由于嵌入式系统往往用于一些较为实时性的用途,这些操作系统也往往被称为实时多任务操作系统(RTOS, Real Time Operation System)。这些系统往往被用在通用嵌入式处理器上(如ARM等)。
DSP系统和通用嵌入式系统的区别 虽然说DSP也是一种嵌入式系统,但是由于其“专用于数字信号处理”的特点,其系统架构也会同通用嵌入式系统略有区别(当然,只是“略有区别”而已)。DSP往往用来跑高速的数学算法,而不牵涉到人机界面、数据库、高层应用等功能(从PC角度来理解的话,DSP在一个嵌入式系统中的功能类似于底层驱动,例如3D图象的演算、环绕声的演算、网络协议处理等)。 因此,从这样的认识角度来看,DSP中的各种任务的调度在过程上相对“单纯”些。DSP中的任务更加侧重于“实时性”和“并行性”。实际上,对于单核的DSP芯片来说,并不可能存在真正的“并行计算”,所谓的并行只不过是通过高速切换几个“串行的线程”来实现。而对于“实时性”,则要求尽可能多的将CPU时间用于计算,并且不同的线程间不能有阻塞的现象发生(从软件角度看,就是执行任务的代码执行时间要短,如果是复杂的算法,就需要对算法进行优化使得算法可以“分步执行”)。 |
|
|
|
DSP实时多任务调度的解决方案
根据以上的分析,我们可以大致的得出一个简单的DSP RTOS的雏形,它的核心就是创建一种可以实时执行的线程。这种线程被称为“PRD Task”(period task),指的是这种线程一旦创建,就由系统内核自动的周期性调用,而调度周期可以保证相当高的时间精度。管理这个机制的部件叫做“PRD模块”。 创建PRD任务的方法是在初始化的时候向内核注册一个PRD任务。 int Thread_PRD_Append(long TimeSlinceCount/*执行周期 */,void (*CallBackHandle)()/* 函数句柄*/) 返回值: TRUE / FALSE 函数功能:向系统注册一个新的线程,指定执行周期以及需要调度的函数句柄,系统便会在指定的时间间隔自动调用这个函数。 这个注册任务等待执行的方式叫做“回调”,注册的时候向内核提交一个指定的执行周期和任务函数的入口函数指针。内核在通过计时,在达到执行周期的时候通过函数指针调用任务函数。 内核为了管理这个功能,需要一张“PRD任务表”。它的定义如下: typedef struct { long TimeSlice_Current; //当前时间片 long TimeSlice_Count; //总时间片 void (*Callback_Handle)(); //调用句柄 } Type_PRD_Table; Type_PRD_Table PRD_Table[SYS__THREAD_PRD_TABLE_SIZE];//PRD表 int PRD_Table_ItemCount=0;//当前的PRD表最大项目数 TimeSlice_Current和TimeSlice_Count两个变量构成了一个软件定时器,内核通过对于TimeSlice_Current的操作即可知道何时可以调用相关的任务函数。 相关的代码如下。这是一个典型的减法计时器的代码。 for(i=0;i if(PRD_Table.TimeSlice_Current > 0) { PRD_Table.TimeSlice_Current --; //递减时间片 }} 而在另外一处,需要判断时间片计数器是否已经计到0。 if((PRD_Table.TimeSlice_Current) == 0) {//调用相应的句柄 PRD_Table.TimeSlice_Current=PRD_Table.TimeSlice_Count;//恢复时间片 (*(PRD_Table.Callback_Handle))();//调用任务入口函数指针 } 接着,需要将减法计时器的代码放入一个硬件定时器中断中,这样便能保证这个减法计时的高度精确性。这个硬件定时器依赖于硬件,因此要求DSP芯片硬件上必须提供这样一个定时器,否则这个DSP内核便无法在这个DSP芯片上执行(幸好基本上不太会存在没有硬件定时器的DSP)。 而对于第二段代码,判别时间片计数是否“已经到点”的代码则应 当放在一个死循环中。例如在main函数中放置一个死循环。 Void Main() { … for(;;) { …..判断时间片计数器是否已经计到0的代码} …} 显然,除了计时是对“到点”的判别以外,我们还需要初始化和添加任务的函数。 int Thread_PRD_Append(long TimeSlinceCount,void *CallbackHandle) {//给PRD表添加任务 if(PRD_Table_ItemCount { PRD_Table_ItemCount++; PRD_Table[PRD_Table_ItemCount-1].TimeSlice_Current=TimeSlinceCount; PRD_Table[PRD_Table_ItemCount-1].TimeSlice_Count=TimeSlinceCount; PRD_Table[PRD_Table_ItemCount-1].Callback_Handle=CallbackHandle; return TRUE; } else {//任务表满,不能继续添加 return FALSE; }} void Thread_Init() {//PRD管理模块初始化 int i=0; for(i=0;i PRD_Table.TimeSlice_Current=0; PRD_Table.TimeSlice_Count=0; PRD_Table.Callback_Handle=0;} PRD_Table_ItemCount=0;//当前的PRD表最大项目数} 至此,一个想当精简的实时调度内核完成了。它是一种非抢占式内核,不同线程之间不需要考虑互斥的问题。 |
|
|
|
消息/事件管理器
消息/事件是现代操作系统很流行的一种机制,我们熟悉的Windows操作系统就采取了这种机制,因此,我也仿照这种机制,在我的DSP内核中加入这个部件。以增强应用代码的对象化。消息/事件机制的大致架构是这样的。 1、首先,在系统中建立一张“消息映射表”,也就是一张用来申明“某个函数可以处理某种消息”的表格。在系统初始化时,应用程序调用一个系统函数向系统提交这种映射关系以声称这样一张表。 2、由某一方的程序调用一个系统函数,向系统提交一个整数(也就是“消息”),系统接收消息,将消息存入一个队列中。这个队列称为“消息队列”。 3、系统从消息队列中取出一条消息,然后检索“消息映射表”,从中找出这条消息的处理函数并调用该函数,接着便丢弃该消息。如果无法从“消息映射表”中检索到相关的处理函数,同样丢弃该消息。 typedef struct {//消息映射表的定义 unsigned int MessageID; //消息ID void (*Callback_Handle)(long lParam);//消息处理函数指针 } Type_Message_Map;// 消息映射表,也就是说明哪个函数可以处理哪种消息 typedef struct {//消息队列,是一个FIFO的循环队列 unsigned int MessageID;//消息ID long lParam;//参数 } Type_Message_FIFO_Element; Type_Message_FIFO_Element MSG_FIFO[__MESSAGE_QUEUE_SIZE]; typedef struct { Type_Message_FIFO_Element *base; long front; long rear; } MSG_FIFO_SqQueue; MSG_FIFO_SqQueue MSG_Queue; 消息队列是一个循环队列,它的基本操作包括InitQueue,EnQueue和DeQueue,分别是初始化队列,添加记录和取出记录。其方式是始终向队尾添加新记录,从队头取出记录。 接着,让我们看一下给消息映射表添加消息映射,这是一个线性表的操作。 其中MessageID是一个整数,表示了“消息ID”,而CallbackHandle是一个函数指针,指向消息处理函数。 int Message_Map_Append(unsigned int MessageID,void *CallbackHandle) {//给消息映射表添加任务 if(Message_Map_ItemCount Message_Map_ItemCount++;//消息映射表的当前长度 Message_Map[Message_Map_ItemCount-1].MessageID=MessageID; Message_Map[Message_Map_ItemCount-1].Callback_Handle=CallbackHandle; return TRUE; } else {/*映射表满,不能继续添加*/ return FALSE; }} 接着是给消息队列添加记录的代码。 int Message_Post(unsigned int MessageID/*消息名称*/,long lParam/*相关参数*/) {//向消息队列中添加新的消息 Type_Message_FIFO_Element e; e.MessageID=MessageID;e.lParam=lParam;return Message_EnQueue(e);} 通过调用Message_EnQueue(e)函数,将一个消息存入消息队列。 最后,是处理消息的代码。 void Message_Process(){//从消息队列中取出消息并处理 Type_Message_FIFO_Element e; int i; if(Message_DeQueue(&e)) {//搜索整个“消息映射表”,找出对应的处理函数,如果没有相应的处理函数则抛弃该消息 for(i=0;i (*(Message_Map.Callback_Handle))(e.lParam);/*执行相关的处理函数*/break;}}}} 至此,便基本上完成了一个简单的DSP实时多任务调度内核。代码在TI的DSP TMS320F24xx系列下调试通过。 基于本内核的软件架构建议 1、虽然说DSP的主要任务是跑数学算法,但是由于当今DSP芯片往往除了运算内核以外,还在片上封装了大量的外设(比如说GPIO、串口、总线设备、PWM发生设备、网络外设等)。这些外设是需要驱动程序的,而这些驱动程序可以作为PRD任务注册进入PRD模块中。这是PRD模块的主要用途。 2、“应用程序”是我们很熟悉的一种程序,它往往用于描述特定的功能性用途。相应的,前面所述的“驱动程序”是一种“系统服务”,一般可以通过API接口由应用程序调用。在Windows环境下,我们很熟悉的一种应用程序架构叫做“面向对象”,而对象之间的沟通则通过“消息/事件”机制。这便是在内核中加入消息事件管理器的原因。在实际应用时,可以将解决问题的模型描述成一个个对象,在它们之间通过一个个消息推动整个流程。这个架构可以有效的降低对象模块之间的耦合性,从而增加单独模块的黑盒性,对应用程序的开发大有好处。 例如,向PRD模块提交一个“键盘驱动程序”,其代码内容是不断的扫描GPIO口以获得键盘的按键情况;一旦驱动程序扫描到了一个特定的键盘事件,便调用Message_Post函数,向系统提交一个键盘消息(例如Key_Press之类)。驱动程序并不会知道究竟哪一个“应用程序”会收到这个消息并处理,而实际上驱动程序也并不用关心这个问题。于是“驱动程序”同“应用程序”便成为了两个黑盒,其耦合性非常小。 另一方面,假设有一个应用程序MyFun1,它在初始化的时候注册了一个消息映射,说明MyFun1_OnKeyPress函数可以处理Key_Press消息,于是在收到了Key_Press消息后,内核便会调用MyFun1_OnKeyPress函数以处理该消息。这样,应用程序MyFun1同样并不知道究竟是谁提交了Key_Press消息(实际上,这个消息完全可以是“虚拟的”,它并不一定要由键盘驱动程序提交,也可以由其他的驱动甚至由其他的应用程序提交),MyFun1也完全不需要关心这一点,因此黑盒性便得到了保障。 本文作者创新点:操作系统的存在可以加速嵌入式系统的开发速度,降低成本,一个免费的操作系统可以更加有效的降低软件成本。本文所阐述的DSP操作系统,架构简洁,易于移植,易于理解,易于进行二次开发,且拥有高度可控的实时性。适合于DSP系统。 |
|
|
|
只有小组成员才能发言,加入小组>>
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-12-27 09:51 , Processed in 0.618398 second(s), Total 50, Slave 42 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号