当前位置: 首页 > news >正文

网络科技网站建设甘肃新闻

网络科技网站建设,甘肃新闻,网站建设有什么价值,郑州网站建设搭建公司FreeRTOS学习笔记——四、任务的定义与任务切换的实现 0 前言1 什么是任务2 创建任务2.1 定义任务栈2.2 定义任务函数2.3 定义任务控制块2.4 实现任务创建函数2.4.1 任务创建函数 —— xTaskCreateStatic()函数2.4.2 创建新任务——prvInitialiseNewTask()函数2.4.3 初始化任务… FreeRTOS学习笔记——四、任务的定义与任务切换的实现 0 前言1 什么是任务2 创建任务2.1 定义任务栈2.2 定义任务函数2.3 定义任务控制块2.4 实现任务创建函数2.4.1 任务创建函数 —— xTaskCreateStatic()函数2.4.2 创建新任务——prvInitialiseNewTask()函数2.4.3 初始化任务栈——pxPortInitialiseStack()函数 3 实现就绪列表3.1 定义就绪列表3.2 就绪列表初始化3.3 将任务插入到就绪列表 4 实现调度器4.1 启动调度器4.1.1 调度器启动——vTaskStartScheduler()函数4.1.2 启动调度器——xPortStartScheduler()函数4.1.3 启动第一个任务——prvStartFirstTask()函数4.1.4 vPortSVCHandler()函数 4.2 任务切换4.2.1 任务切换——taskYIELD()4.2.2 中断服务函数——xPortPendSVHandler()函数4.2.3 vTaskSwitchContext()函数 5 main函数6 实验现象7 本章涉及到的汇编指令 0 前言 本章是我们真正从从0 到1 写FreeRTOS 的第一章属于基础中的基础必须要学会创建任务并重点掌握任务是如何切换因为任务的切换是由汇编代码来完成的所以代码看起来比较难懂但是会尽力把代码讲得透彻如果本章内容学不会后面的内容根本无从下手在这章中: 我们会创建两个任务并让这两个任务不断地切换任务的主体都是让一个变量按照一定的频率翻转通过KEIL 的软件仿真功能在逻辑分析仪中观察变量的波形变化最终的波形图具体见图 上图的波形图的效果并不是真正的多任务系统中任务切换的效果图这个效果其实可以完全由裸机代码来实现具体见代码 1 /* flag 必须定义成全局变量才能添加到逻辑分析仪里面观察波形 2 * 在逻辑分析仪中要设置以 bit 的模式才能看到波形不能用默认的模拟量 3 */ 4 uint32_t flag1; 5 uint32_t flag2; 6 7 8 /* 软件延时不必纠结具体的时间 */ 9 void delay( uint32_t count ) 10 { 11 for (; count!0; count--); 12 } 13 14 int main(void) 15 { 16 /* 无限循环顺序执行 */ 17 for (;;) { 18 flag1 1; 19 delay( 100 ); 20 flag1 0; 21 delay( 100 ); 22 23 flag2 1; 24 delay( 100 ); 25 flag2 0; 26 delay( 100 ); 27 } 28 }在多任务系统中两个任务不断切换的效果图应该像下图所示那样即两个变量的波形是完全一样的就好像CPU在同时干两件事一样这才是多任务的意义。 这章只是开始我们先掌握好任务是如何切换在后面章节中我们会陆续的完善功能代码加入系统调度实现真正的多任务。千里之行始于本章节不要急 1 什么是任务 在裸机系统中 系统的主体就是main 函数里面顺序执行的无限循环这个无限循环里面CPU 按照顺序完成各种事情 在多任务系统中 我们根据功能的不同把整个系统分割成一个个独立的且无法返回的函数这个函数我们称为任务任务的大概形式具体见代码1 void task_entry (void *parg) 2 { 3 /* 任务主体无限循环且不能返回 */ 4 for (;;) { 5 /* 任务主体代码 */ 6 } 7 }2 创建任务 2.1 定义任务栈 在一个裸机系统中: 如果有全局变量有子函数调用有中断发生那么系统在运行的时候全局变量放在哪里子函数调用时局部变量放在哪里中断发生时函数返回地址放哪里如果只是单纯的裸机编程它们放哪里我们不用管写一个RTOS这些种种环境参数我们必须弄清楚他们是如何存储的 在裸机系统中他们统统放在一个叫栈的地方 栈是单片机RAM 里面一段连续的内存空间栈的大小一般在启动文件或者链接脚本里面指定最后由C 库函数_main 进行初始化 在多任务系统中: 每个任务都是独立的互不干扰的要为每个任务都分配独立的栈空间 这个栈空间通常是一个预先定义好的全局数组也可以是动态分配的一段内存空间但它们都存在于RAM 中 本章我们要实现两个变量按照一定的频率轮流的翻转: 每个变量对应一个任务那么就需要定义两个任务栈具体见代码清单在多任务系统中有多少个任务就需要定义多少个任务栈 /* main.c 中定义任务栈 */ 1 #define TASK1_STACK_SIZE 128 (2) 2 StackType_t Task1Stack[TASK1_STACK_SIZE]; (1) 3 4 #define TASK2_STACK_SIZE 128 5 StackType_t Task2Stack[TASK2_STACK_SIZE];1任务栈 预先定义好的全局数据数据类型为StackType_t #define portSTACK_TYPE uint32_t;typedef portSTACK_TYPE StackType_t; 大小由TASK1_STACK_SIZE 这个宏来定义 默认为128单位为字即512字节, 是FreeRTOS 推荐的最小的任务栈 在FreeRTOS 中凡是涉及到数据类型的地方FreeRTOS 都会将标准的C 数据类型用typedef 重新取一个类型名, 经过重定义的数据类型放在portmacro.h这个头文件 0 /* portmacro.h 中的数据类型 */ 1 #ifndef PORTMACRO_H 2 #define PORTMACRO_H 3 4 /* 包含标准库头文件 */ 5 #include stdint.h 6 #include stddef.h 7 8 9 /* 数据类型重定义 */ 10 #define portCHAR char 11 #define portFLOAT float 12 #define portDOUBLE double 13 #define portLONG long 14 #define portSHORT short 15 #define portSTACK_TYPE uint32_t 16 #define portBASE_TYPE long 17 18 typedef portSTACK_TYPE StackType_t; 19 typedef long BaseType_t; 20 typedef unsigned long UBaseType_t; 21 22 23 #endif /* PORTMACRO_H */2.2 定义任务函数 任务是一个独立的函数函数主体无限循环且不能返回本章我们在main.c 中定义的两个任务具体见代码清单 0 /* main.c 中的任务函数 */ 1 /* 软件延时 */ 2 void delay (uint32_t count) 3 { 4 for (; count!0; count--); 5 } 6 /* 任务1 */ 7 void Task1_Entry( void *p_arg ) (1) 8 { 9 for ( ;; ) 10 { 11 flag1 1; 12 delay( 100 ); 13 flag1 0; 14 delay( 100 ); 15 } 16 } 17 18 /* 任务2 */ 19 void Task2_Entry( void *p_arg ) (2) 20 { 21 for ( ;; ) 22 { 23 flag2 1; 24 delay( 100 ); 25 flag2 0; 26 delay( 100 ); 27 } 28 }2.3 定义任务控制块 在裸机系统中程序的主体是CPU 按照顺序执行的在多任务系统中任务的执行是由系统调度的系统为了顺利的调度任务为每个任务都额外定义了一个任务控制块 这个任务控制块就相当于任务的身份证里面存有任务的所有信息比如任务的栈指针任务名称任务的形参等有了这个任务控制块之后以后系统对任务的全部操作都可以通过这个任务控制块来实现 定义一个任务控制块需要一个新的数据类型: 该数据类型在task.c 这C 头文件中声明具体的声明见代码清单使用它可以为每个任务都定义一个任务控制块实体 0 /* task.c 中定义任务控制块 */ 1 typedef struct tskTaskControlBlock 2 { 3 volatile StackType_t *pxTopOfStack; /* 栈顶 */ (1) 4 5 ListItem_t xStateListItem; /* 任务节点 */ (2) 6 7 StackType_t *pxStack; /* 任务栈起始地址 */ (3) 8 /* 任务名称字符串形式 */(4) 9 char pcTaskName[ configMAX_TASK_NAME_LEN ]; 10 } tskTCB; 11 typedef tskTCB TCB_t; (5)(1)栈顶指针作为TCB(TaskControlBlock) 的第一个成员(2)任务节点这是一个内置在TCB 控制块中的链表节点 通过这个节点可以将任务控制块挂接到各种链表中 (3)任务栈起始地址(4)任务名称字符串形式长度由宏configMAX_TASK_NAME_LEN来控制该宏在FreeRTOSConfig.h 中定义默认为16(5)数据类型重定义 在main.c 文件中为两个任务定义的任务控制块具体见代码清单 /* main.c 中定义任务控制块 */ 1 /* 定义任务控制块 */ 2 TCB_t Task1TCB; 3 TCB_t Task2TCB;2.4 实现任务创建函数 任务的栈任务的函数实体任务的控制块最终需要联系起来才能由系统进行统一调度这个工作由 任务创建函数 xTaskCreateStatic()来实现该函数在task.c中定义在task.h 中声明所有跟任务相关的函数都在这个文件定义 2.4.1 任务创建函数 —— xTaskCreateStatic()函数 xTaskCreateStatic()函数的实现见代码清单 1 #if( configSUPPORT_STATIC_ALLOCATION 1 ) (1) 2 3 TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode, (2) 4 const char * const pcName, (3) 5 const uint32_t ulStackDepth, (4) 6 void * const pvParameters, (5) 7 StackType_t * const puxStackBuffer, (6) 8 TCB_t * const pxTaskBuffer ) (7) 9 { 10 TCB_t *pxNewTCB; 11 TaskHandle_t xReturn; (8) 12 13 if ( ( pxTaskBuffer ! NULL ) ( puxStackBuffer ! NULL ) ) 14 { 15 pxNewTCB ( TCB_t * ) pxTaskBuffer; 16 pxNewTCB-pxStack ( StackType_t * ) puxStackBuffer; 17 18 /* 创建新的任务 */ (9) 19 prvInitialiseNewTask( pxTaskCode, /* 任务入口 */ 20 pcName, /* 任务名称字符串形式 */ 21 ulStackDepth, /* 任务栈大小单位为字 */ 22 pvParameters, /* 任务形参 */ 23 xReturn, /* 任务句柄 */ 24 pxNewTCB); /* 任务栈起始地址 */ 25 26 } 27 else 28 { 29 xReturn NULL; 30 } 31 32 /* 返回任务句柄如果任务创建成功此时xReturn 应该指向任务控制块 */ 33 return xReturn; (10) 34 } 35 36 #endif /* configSUPPORT_STATIC_ALLOCATION */(1)FreeRTOS 中任务的创建有两种方法 一种是使用动态创建一种是使用静态创建动态创建时任务控制块和栈的内存是创建任务时动态分配的任务删除时内存可以释放静态创建时任务控制块和栈的内存需要事先定义好是静态的内存 任务删除时 内存不能释放目前我们以静态创建为例来讲解configSUPPORT_STATIC_ALLOCATION 在FreeRTOSConfig.h 中定义我们配置为1 (2)任务入口即任务的函数名称 TaskFunction_t 是在projdefs.h中重定义的一个数据类型实际就是空指针具体实现见代码清单 TaskFunction_t 定义 0 /* projdefs.h 中TaskFunction_t定义 */ 1 #ifndef PROJDEFS_H 2 #define PROJDEFS_H 3 4 typedef void (*TaskFunction_t)( void * ); 5 6 #define pdFALSE ( ( BaseType_t ) 0 ) 7 #define pdTRUE ( ( BaseType_t ) 1 ) 8 9 #define pdPASS ( pdTRUE ) 10 #define pdFAIL ( pdFALSE ) 11 12 13 #endif /* PROJDEFS_H */(3)任务名称字符串形式方便调试(4)任务栈大小单位为字(5)任务形参(6)任务栈起始地址(7)任务控制块指针(8)定义一个任务句柄xReturn 任务句柄用于指向任务的TCB任务句柄的数据类型为TaskHandle_t在task.h 中定义实际上就是一个空指针具体实现见代码清单 0 /* task.h 中 TaskHandle_t 定义 */ 1 /* 任务句柄 */ 2 typedef void * TaskHandle_t;(9)调用prvInitialiseNewTask()函数创建新任务该函数在task.c 实现具体实现见代码清单(10)返回任务句柄 如果任务创建成功此时xReturn 应该指向任务控制块xReturn 作为形参传入到prvInitialiseNewTask 函数 2.4.2 创建新任务——prvInitialiseNewTask()函数 (9)调用prvInitialiseNewTask()函数创建新任务该函数在task.c 实现具体实现见代码清单 0 /* task.c 中 prvInitialiseNewTask() 定义*/ 1 static void prvInitialiseNewTask(TaskFunction_t pxTaskCode, (1) 2 const char * const pcName, (2) 3 const uint32_t ulStackDepth, (3) 4 void * const pvParameters, (4) 5 TaskHandle_t * const pxCreatedTask, (5) 6 TCB_t *pxNewTCB ) (6) 7 8 { 9 StackType_t *pxTopOfStack; 10 UBaseType_t x; 11 12 /* 获取栈顶地址 */ (7) 13 pxTopOfStack pxNewTCB-pxStack ( ulStackDepth - ( uint32_t ) 1 ); 14 /* 向下做8 字节对齐 */ (8) 15 pxTopOfStack ( StackType_t * ) \ 16 ( ( ( uint32_t ) pxTopOfStack ) ( ~( ( uint32_t ) 0x0007 ) ) ); 17 18 /* 将任务的名字存储在TCB 中 */ (9) 19 for ( x ( UBaseType_t ) 0; x ( UBaseType_t ) configMAX_TASK_NAME_LEN; x ) 20 { 21 pxNewTCB-pcTaskName[ x ] pcName[ x ]; 22 23 if ( pcName[ x ] 0x00 ) 24 { 25 break; 26 } 27 } 28 /* 任务名字的长度不能超过configMAX_TASK_NAME_LEN */ (10) 29 pxNewTCB-pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] \0; 30 31 /* 初始化TCB 中的xStateListItem 节点 */ (11) 32 vListInitialiseItem( ( pxNewTCB-xStateListItem ) ); 33 /* 设置xStateListItem 节点的拥有者 */ (12) 34 listSET_LIST_ITEM_OWNER( ( pxNewTCB-xStateListItem ), pxNewTCB ); 35 36 37 /* 初始化任务栈 */ (13) 38 pxNewTCB-pxTopOfStack pxPortInitialiseStack( pxTopOfStack, 39 pxTaskCode, 40 pvParameters ); 41 42 43 /* 让任务句柄指向任务控制块 */ (14) 44 if ( ( void * ) pxCreatedTask ! NULL ) 45 { 46 *pxCreatedTask ( TaskHandle_t ) pxNewTCB; 47 } 48 }(1)任务入口(2)任务名称字符串形式(3)任务栈大小单位为字(4)任务形参(5)任务句柄(6)任务控制块指针(7)获取栈顶地址(8)将栈顶指针向下做8 字节对齐 在Cortex-M3Cortex-M4 或Cortex-M7内核的单片机中因为总线宽度是32 位的通常只要栈保持4 字节对齐就行可这样为啥要8 字节难道有哪些操作是64 位的确实有那就是浮点运算所以要8 字节对齐但是目前我们都还没有涉及到浮点运算只是为了后续兼容浮点运行的考虑如果栈顶指针是8 字节对齐的在进行向下8 字节对齐的时候指针不会移动如果不是8 字节对齐的在做向下8 字节对齐的时候就会空出几个字节不会使用比如当pxTopOfStack 是33明显不能整除8进行向下8 字节对齐就是32那么就会空出一个字节不使用。 (9)将任务的名字存储在TCB 中(10)任务名字的长度不能超过configMAX_TASK_NAME_LEN并以’\0’结尾(11)初始化TCB 中的xStateListItem 节点即初始化该节点所在的链表为空表示节点还没有插入任何链表(12)设置xStateListItem 节点的拥有者即拥有这个节点本身的TCB(13)调用pxPortInitialiseStack()函数初始化任务栈并更新栈顶指针任务第一次运行的环境参数就存在任务栈中(14)让任务句柄指向任务控制块 2.4.3 初始化任务栈——pxPortInitialiseStack()函数 (13)调用pxPortInitialiseStack()函数初始化任务栈并更新栈顶指针任务第一次运行的环境参数就存在任务栈中 该函数在port.c中定义具体实现见代码清单任务栈初始化完毕之后栈空间内部分布图具体见图 0 /* port.c 中prvTaskExitError函数定义 */ 1 #define portINITIAL_XPSR ( 0x01000000 ) 2 #define portSTART_ADDRESS_MASK ( ( StackType_t ) 0xfffffffeUL ) 3 4 static void prvTaskExitError( void ) 5 { 6 /* 函数停止在这里 */ 7 for (;;); 8 } 9 10 StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, 11 TaskFunction_t pxCode, 12 void *pvParameters ) 13 { 14 /* 异常发生时自动加载到CPU 寄存器的内容 */ (1) 15 pxTopOfStack--; 16 *pxTopOfStack portINITIAL_XPSR; (2) 17 pxTopOfStack--; 18 *pxTopOfStack ( ( StackType_t ) pxCode ) portSTART_ADDRESS_MASK; (3) 19 pxTopOfStack--; 20 *pxTopOfStack ( StackType_t ) prvTaskExitError; (4) 21 pxTopOfStack - 5; /* R12, R3, R2 and R1 默认初始化为0 */ 22 *pxTopOfStack ( StackType_t ) pvParameters; (5) 23 24 /* 异常发生时手动加载到CPU 寄存器的内容 */ (6) 25 pxTopOfStack - 8; 26 27 /* 返回栈顶指针此时pxTopOfStack 指向空闲栈 */ 28 return pxTopOfStack; (7) 29 }(1)异常发生时CPU 自动从栈中加载到CPU 寄存器的内容 包括8个寄存器分别为R0、R1、R2、R3、R12、R14、R15 和xPSR 的位24且顺序不能变。 (2)xPSR 的bit24 必须置1即0x01000000 port.c中定义 #define portINITIAL_XPSR ( 0x01000000 )portINITIAL_XPSR是一个32位的寄存器它是ARM Cortex-M处理器中的一个特殊寄存器用于存储初始程序状态寄存器的值。它的值在启动时被加载到程序状态寄存器PSR中以设置处理器的初始状态 (3)任务的入口地址(4)任务的返回地址通常任务是不会返回的如果返回了就跳转到prvTaskExitError该函数是一个无限循环(5)R12, R3, R2 and R1 默认初始化为0(6)异常发生时需要手动加载到CPU 寄存器的内容总共有8 个分别为R4、R5、R6、R7、R8、R9、R10 和R11默认初始化为0(7)返回栈顶指针此时pxTopOfStack 指向具体见图 任务第一次运行时就是从这个栈指针开始手动加载 8 个字的内容到CPU 寄存器R4、R5、R6、R7、R8、R9、R10 和R11当退出异常时栈中剩下的8 个字的内容会自动加载 到CPU 寄存器R0、R1、R2、R3、R12、R14、R15 和xPSR 的位24此时PC 指针就指向了任务入口地址从而成功跳转到第一个任务 3 实现就绪列表 3.1 定义就绪列表 任务创建好之后我们需要把任务添加到就绪列表里面表示任务已经就绪系统随时可以调度就绪列表在task.c 中定义具体见代码清单 0 /* task.c 中定义就绪列表 */ 1 /* 任务就绪列表 */ 2 List_t pxReadyTasksLists[ configMAX_PRIORITIES ];(1)就绪列表实际上就是一个List_t 类型的数组 数组的大小由决定最大任务优先级的宏configMAX_PRIORITIES 决定 configMAX_PRIORITIES 在FreeRTOSConfig.h 中默认定义为5最大支持256 个优先级。数组的下标对应了任务的优先级同一优先级的任务统一插入到就绪列表的同一条链表中一个空的就绪列表具体见图 3.2 就绪列表初始化 就绪列表在使用前需要先初始化 就绪列表初始化的工作在函数prvInitialiseTaskLists()里面实现具体见代码清单就绪列表初始化完毕之后示意图见 1 void prvInitialiseTaskLists( void ) 2 { 3 UBaseType_t uxPriority; 4 5 6 for ( uxPriority ( UBaseType_t ) 0U; 7 uxPriority ( UBaseType_t ) configMAX_PRIORITIES; 8 uxPriority ) 9 { 10 vListInitialise( ( pxReadyTasksLists[ uxPriority ] ) ); 11 } 12 }3.3 将任务插入到就绪列表 任务控制块里面有一个xStateListItem 成员数据类型为ListItem_t我们将任务插入到就绪列表里面就是通过将任务控制块的xStateListItem 这个节点插入到就绪列表中来实现的 如果把就绪列表比作是晾衣架任务是衣服那xStateListItem 就是晾衣架上面的钩子每个任务都自带晾衣架钩子就是为了把自己挂在各种不同的链表中我们在任务创建好之后紧跟着将任务插入到就绪列表具体实现见代码清单 7-15 的加粗部分。 1 /* 初始化与任务相关的列表如就绪列表 */ 2 prvInitialiseTaskLists(); 3 4 Task1_Handle /* 任务句柄 */ 5 xTaskCreateStatic( (TaskFunction_t)Task1_Entry, /* 任务入口 */ 6 (char *)Task1, /* 任务名称字符串形式 */ 7 (uint32_t)TASK1_STACK_SIZE , /* 任务栈大小单位为字 */ 8 (void *) NULL, /* 任务形参 */ 9 (StackType_t *)Task1Stack, /* 任务栈起始地址 */ 10 (TCB_t *)Task1TCB ); /* 任务控制块 */ 11 12 /* 将任务添加到就绪列表 */ 13 vListInsertEnd( ( pxReadyTasksLists[1] ), 14 ( ((TCB_t *)(Task1TCB))-xStateListItem ) ); 15 16 Task2_Handle /* 任务句柄 */ 17 xTaskCreateStatic( (TaskFunction_t)Task2_Entry, /* 任务入口 */ 18 (char *)Task2, /* 任务名称字符串形式 */ 19 (uint32_t)TASK2_STACK_SIZE , /* 任务栈大小单位为字 */ 20 (void *) NULL, /* 任务形参 */ 21 (StackType_t *)Task2Stack, /* 任务栈起始地址 */ 22 (TCB_t *)Task2TCB ); /* 任务控制块 */ 23 /* 将任务添加到就绪列表 */ 24 vListInsertEnd( ( pxReadyTasksLists[2] ), 25 ( ((TCB_t *)(Task2TCB))-xStateListItem ) );就绪列表的下标对应的是任务的优先级但是目前我们的任务还不支持优先级有关支持多优先级的知识点我们后面会讲到所以Task1 和Task2 任务在插入到就绪列表的时候可以随便选择插入的位置在代码清单中我们选择将Task1 任务插入到就绪列表下标为1 的链表中Task2 任务插入到就绪列表下标为2 的链表中具体的示意图见图 4 实现调度器 调度器是操作系统的核心 其主要功能就是实现任务的切换即从就绪列表里面找到优先级最高的任务然后去执行该任务从代码上来看调度器无非也就是由几个全局变量和一些可以实现任务切换的函数组成全部都在task.c 文件中实现 4.1 启动调度器 调度器的启动由vTaskStartScheduler()函数来完成该函数在task.c 中定义具体实现见代码清单 4.1.1 调度器启动——vTaskStartScheduler()函数 调度器的启动由vTaskStartScheduler()函数来完成该函数在task.c 中定义具体实现见代码清单 0 /* task.c 中 vTaskStartScheduler()函数 */ 1 void vTaskStartScheduler( void ) 2 { 3 /* 手动指定第一个运行的任务 */ 4 pxCurrentTCB Task1TCB; (1) 5 6 /* 启动调度器 */ 7 if ( xPortStartScheduler() ! pdFALSE ) 8 { 9 /* 调度器启动成功则不会返回即不会来到这里 */ (2) 10 } 11 }(1)pxCurrentTCB 是一个在task.c 定义的全局指针用于指向当前正在运行或者即将要运行的任务的任务控制块 目前我们还不支持优先级则手动指定第一个要运行的任务 (2)调用函数xPortStartScheduler()启动调度器调度器启动成功则不会返回。该函数在port.c 中实现具体见代码清单 4.1.2 启动调度器——xPortStartScheduler()函数 (2)调用函数xPortStartScheduler()启动调度器调度器启动成功则不会返回。该函数在port.c 中实现具体见代码清单 0 /* port.c 中 xPortStartScheduler()函数 */ 1 /* 2 * 参考资料《STM32F10xxx Cortex-M3 programming manual》4.4.7百度搜索“PM0056”即可找到这个文档 3 * 在Cortex-M 中内核外设SCB 中SHPR3 寄存器用于设置SysTick 和PendSV 的异常优先级 4 * System handler priority register 3 (SCB_SHPR3) SCB_SHPR30xE000 ED20 5 * Bits 31:24 PRI_15[7:0]: Priority of system handler 15, SysTick exception 6 * Bits 23:16 PRI_14[7:0]: Priority of system handler 14, PendSV 7 */ 8 #define portNVIC_SYSPRI2_REG (*(( volatile uint32_t *) 0xe000ed20)) 9 10 #define portNVIC_PENDSV_PRI (((uint32_t) configKERNEL_INTERRUPT_PRIORITY ) 16UL) 11 #define portNVIC_SYSTICK_PRI (((uint32_t) configKERNEL_INTERRUPT_PRIORITY ) 24UL ) 12 13 BaseType_t xPortStartScheduler( void ) 14 { 15 /* 配置PendSV 和 SysTick 的中断优先级为最低 */ (1) 16 portNVIC_SYSPRI2_REG | portNVIC_PENDSV_PRI; 17 portNVIC_SYSPRI2_REG | portNVIC_SYSTICK_PRI; 18 19 /* 启动第一个任务不再返回 */ 20 prvStartFirstTask(); (2) 21 22 /* 不应该运行到这里 */ 23 return 0; 24 }(1)配置 PendSV 和 SysTick 的中断优先级为最低 SysTick 和PendSV 都会涉及到系统调度系统调度的优先级要低于系统的其它硬件中断优先级 即优先响应系统中的外部硬件中断所以SysTick 和PendSV 的中断优先级配置为最低portNVIC_PENDSV_PRI是一个宏定义用于设置PendSV中断的优先级。在FreeRTOS中PendSV中断用于任务切换。portNVIC_SYSTICK_PRI是一个宏定义用于设置SysTick中断的优先级。在FreeRTOS中SysTick定时器被用作系统时基因此其中断优先级需要设置为最高。 (2)调用函数prvStartFirstTask()启动第一个任务启动成功后则不再返回该函数由汇编编写在port.c 实现具体代码见代码清单 4.1.3 启动第一个任务——prvStartFirstTask()函数 (2)调用函数prvStartFirstTask()启动第一个任务启动成功后则不再返回该函数由汇编编写在port.c 实现具体代码见代码清单prvStartFirstTask()函数用于开始第一个任务 主要做了两个动作 一个是更新MSP 的值二是产生SVC 系统调用然后去到SVC 的中断服务函数里面真正切换到第一个任务 该函数的具体实现见代码清单 1 /* 2 * 参考资料《STM32F10xxx Cortex-M3 programming manual》4.4.3百度搜索“PM0056”即可找到这个文档 3 * 在Cortex-M 中内核外设SCB 的地址范围为0xE000ED00-0xE000ED3F 4 * 0xE000ED008 为SCB 外设中SCB_VTOR 这个寄存器的地址里面存放的是向量表的起始地址即MSP 的地址 5 */ 6 7 (1) 8 __asm void prvStartFirstTask( void ) 9 { 10 PRESERVE8 (2) 11 12 /* 在Cortex-M 中0xE000ED08 是SCB_VTOR 这个寄存器的地址 (3) 13 里面存放的是向量表的起始地址即MSP 的地址 */ 14 ldr r0, 0xE000ED08 (4) 15 ldr r0, [r0] (5) 16 ldr r0, [r0] (6) 17 18 /* 设置主堆栈指针msp 的值 */ 19 msr msp, r0 (7) 20 21 /* 使能全局中断 */ (8) 22 cpsie i 23 cpsie f 24 dsb 25 isb 26 27 /* 调用SVC 去启动第一个任务 */ 28 svc 0 (9) 29 nop 30 nop 31 }(1)(2)当前栈需按照8 字节对齐如果都是32 位的操作则4 个字节对齐即可。在Cortex-M中浮点运算是8 字节的。(3)在Cortex-M 中0xE000ED08 是SCB_VTOR 寄存器的地址 里面存放的是向量表的起始地址即MSP 的地址向量表通常是从内部FLASH 的起始地址开始存放那么可知memory0x00000000 处存放的就是MSP 的值这个可以通过仿真时查看内存的值证实具体见图 SCB_VTOR寄存器是ARM Cortex-M处理器中的一个系统控制寄存器用于存储向量表的地址。当处理器复位或发生异常时处理器会从该地址处读取向量表的起始地址。MSP(主堆栈指针Main Stack PointerMSP) (4)将0xE000ED08 这个立即数加载到寄存器R0(5)将0xE000ED08 这个地址指向的内容加载到寄存器R0此时R0等于SCB_VTOR寄存器的值等于0x00000000即memory 的起始地址(6)将0x00000000 这个地址指向的内容加载到R0此时R0 等于0x200008DB与图查询到的值吻合。(7)将R0 的值存储到MSP此时MSP 等于0x200008DB这是主堆栈的栈顶指针 起始这一步操作有点多余因为当系统启动的时候执行完Reset_Handler的时候向量表已经初始化完毕MSP 的值就已经更新为向量表的起始值即指向主堆栈的栈顶指针。 (8)使用CPS 指令把全局中断打开 为了快速地开关中断 Cortex-M内核 专门设置了一条 CPS 指令有 4 种用法具体见代码清单 0 /* CPS指令用法 */ 1 CPSID I ;PRIMASK1 ;关中断 2 CPSIE I ;PRIMASK0 ;开中断 3 CPSID F ;FAULTMASK1 ;关异常 4 CPSIE F ;FAULTMASK0 ;开异常PRIMASK 和FAULTMAST 是Cortex-M内核 里面三个中断屏蔽寄存器中的两个还有一个是BASEPRI有关这三个寄存器的详细用法见表格 (9)产生系统调用服务号0 表示SVC 中断接下来将会执行SVC 中断服务函数**SVCSupervisor Call**中断是一种特殊的中断通常在嵌入式系统中使用。它是由程序或硬件产生的异常或错误条件触发的可以引起程序从当前执行点跳转到预设的中断处理程序或称为异常处理程序所在地址执行特定的中断处理程序。SVC中断通常被用于以下情况 系统调用当应用程序需要操作系统提供服务时可以触发SVC中断将控制权交给操作系统然后执 行相应的系统调用服务例程完成特定的操作。异常处理当系统遇到一些异常情况时比如非法指令、栈溢出等可以触发SVC中断将控制权交给异常处理程序进行处理并决定如何恢复程序运行。任务切换在一些实时操作系统中当高优先级任务就绪时可以通过触发SVC中断来切换任务将当前任务挂起转而执行高优先级任务。中断处理在一些系统中SVC中断也可以用于处理其他中断源的中断处理程序以便在中断处理结束后返回到被中断的程序中继续执行。 4.1.4 vPortSVCHandler()函数 (9)产生系统调用服务号0 表示SVC 中断接下来将会执行SVC 中断服务函数SVC 中断要想被成功响应其函数名必须与向量表注册的名称一致 在启动文件的向量表中SVC 的中断服务函数注册的名称是SVC_Handler所以SVC 中断服务函数的名称我们应该写成SVC_Handler但是在FreeRTOS 中官方版本写的是vPortSVCHandler()为了能够顺利的响应SVC 中断我们有两个选择 改中断向量表中SVC 的注册的函数名称或者改FreeRTOS 中SVC 的中断服务名称这里我们采取第二种方法即在FreeRTOSConfig.h 中添加添加宏定义的方法来修改 具体见代码清单顺便把PendSV 和SysTick 的中断服务函数名也改成与向量表的一致 0 /* FreeRTOSConfig.h 中 修改FreeRos 中SVC、PendSV 和SysTick 中断服务函数的名称 */ 1 #define xPortPendSVHandler PendSV_Handler 2 #define xPortSysTickHandler SysTick_Handler 3 #define vPortSVCHandler SVC_HandlervPortSVCHandler()函数开始真正启动第一个任务不再返回实现具体见代码清单 0 /* port.c 中vPortSVCHandler()函数真正启动第一个任务 */ 1 __asm void vPortSVCHandler( void ) 2 { 3 extern pxCurrentTCB; (1) 4 5 PRESERVE8 6 7 ldr r3, pxCurrentTCB (2) 8 ldr r1, [r3] (3) 9 ldr r0, [r1] (4) 10 ldmia r0!, {r4-r11} (5) 11 msr psp, r0 (6) 12 isb 13 mov r0, #0 (7) 14 msr basepri, r0 (8) 15 orr r14, #0xd (9) 16 17 bx r14 (10) 18 }(1)声明 外部变量 pxCurrentTCBpxCurrentTCB 是一个在task.c 中定义的全局指针用于指向当前正在运行或者即将要运行的任务的任务控制块。(2)加载pxCurrentTCB 的地址到r3。(3)加载pxCurrentTCB 到r3。(4)加载pxCurrentTCB 指向的任务控制块到 r0任务控制块的第一个成员就是栈顶指针所以此时r0 等于栈顶指针 一个刚刚被创建还没有运行过的任务的栈空间分布具体如图所示即r0 等于图中pxTopOfStack (5)以r0 为基地址将栈中向上增长的8 个字的内容加载到CPU 寄存器r4~r11同时r0 也会跟着自增。(6)将新的栈顶指针r0 更新到psp任务执行的时候使用的堆栈指针是psp。此时psp 的指向具体见。(7)将寄存器r0 清0。(8)设置basepri 寄存器的值为0即打开所有中断。basepri 是一个中断屏蔽寄存器大于等于此寄存器值的中断都将被屏蔽。(9)当从SVC 中断服务退出前通过向r14 寄存器最后4 位按位或上0x0D使得硬件在退出时使用进程堆栈指针PSP 完成出栈操作并返回后进入任务模式、返回Thumb 状态。在SVC 中断服务里面使用的是MSP 堆栈指针是处在ARM 状态。 当r14 为0xFFFFFFFX执行是中断返回指令cortext-m3 的做法X的bit0 为1 表示返回thumb 状态bit1 和bit2 分别表示返回后sp 用msp 还是psp、以及返回到特权模式还是用户模式(10)异常返回这个时候出栈使用的是PSP 指针自动将栈中的剩下内容加载到CPU 寄存器 xPSRPC任务入口地址R14R12R3R2R1R0任务的形参同时PSP 的值也将更新即指向任务栈的栈顶具体指向见图。 4.2 任务切换 任务切换就是在就绪列表中寻找优先级最高的就绪任务然后去执行该任务 但是目前我们还不支持优先级仅实现两个任务轮流切换任务切换函数taskYIELD()具体实现见代码清单。 4.2.1 任务切换——taskYIELD() 1 /* 在task.h 中定义 */ 2 #define taskYIELD() portYIELD() 3 4 5 /* 在portmacro.h 中定义 */ 6 /* 中断控制状态寄存器0xe000ed04 7 * Bit 28 PENDSVSET: PendSV 悬起位 8 */ 9 #define portNVIC_INT_CTRL_REG (*(( volatile uint32_t *) 0xe000ed04)) 10 #define portNVIC_PENDSVSET_BIT ( 1UL 28UL ) 11 12 #define portSY_FULL_READ_WRITE ( 15 ) 13 14 #define portYIELD() \ 15 { \ 16 /* 触发PendSV产生上下文切换 */ \ 17 portNVIC_INT_CTRL_REG portNVIC_PENDSVSET_BIT; (1) \ 18 __dsb( portSY_FULL_READ_WRITE ); \ 19 __isb( portSY_FULL_READ_WRITE ); \ 20 }(1)portYIELD 的实现很简单实际就是将PendSV 的悬起位置1当没有其它中断运行的时候响应PendSV 中断去执行我们写好的PendSV中断服务函数在里面实现任务切换 4.2.2 中断服务函数——xPortPendSVHandler()函数 PendSV 中断服务函数是真正实现任务切换的地方具体实现见代码清单。 0 /* port.c 中真正实现任务切换函数实现 */ 1 __asm void xPortPendSVHandler( void ) 2 { 3 extern pxCurrentTCB; (1) 4 extern vTaskSwitchContext; (2) 5 6 PRESERVE8 (3) 7 8 mrs r0, psp (4) 9 isb 10 11 ldr r3, pxCurrentTCB (5) 12 ldr r2, [r3] (6) 13 14 stmdb r0!, {r4-r11} (7) 15 str r0, [r2] (8) 16 17 stmdb sp!, {r3, r14} (9) 18 mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY (10) 19 msr basepri, r0 (11) 20 dsb 21 isb 22 bl vTaskSwitchContext (12) 23 mov r0, #0 (13) 24 msr basepri, r0 25 ldmia sp!, {r3, r14} (14) 26 27 ldr r1, [r3] (15) 28 ldr r0, [r1] (16) 29 ldmia r0!, {r4-r11} (17) 30 msr psp, r0 (18) 31 isb 32 bx r14 (19) 33 nop 34 }(1)声明外部变量 pxCurrentTCBpxCurrentTCB 是一个在task.c 中定义的全局指针用于指向当前正在运行 或者即 将要运行的任务的任务控制块(2)声明 外部函数 vTaskSwitchContext等下会用到。(3)当前栈需按照8 字节对齐如果都是32 位的操作则4 个字节对齐即可。在Cortex-M中浮点运算是8 字节的。(4)将PSP 的值存储到r0。当进入PendSVC Handler 时上一个任务运行的环境即 xPSRPC任务入口地址R14R12R3R2R1R0任务的形参这些CPU 寄存器的值会自动存储到任务的栈中剩下的r4~r11 需要手动保存同时PSP 会自动更新在更新之前PSP 指向任务栈的栈顶此时PSP 具体指向见图 (5)加载pxCurrentTCB 的地址到r3。(6)加载r3 指向的内容到r2即r2 等于pxCurrentTCB。(7)以r0 作为基址指针先递减再操作STMDB 的DB 表示Decrease Befor将CPU 寄存器r4~r11 的值存储到任务栈同时更新r0 的值此时r0 的指向具体见。 (8)将r0 的值存储到r2 指向的内容r2 等于pxCurrentTCB。具体为将r0 的值存储到上一个任务的栈顶指针pxTopOfStack具体指向如上图的r0 指向一样。到此上下文切换中的上文保存就完成了。(9)将R3 和R14 临时压入堆栈在整个系统中中断使用的是主堆栈栈指针使用的是MSP 因为接下来要调用函数vTaskSwitchContext调用函数时返回地址自动保存到R14 中所以一旦调用发生R14 的值会被覆盖PendSV 中断服务函数执行完毕后返回的时候需要根据R14 的值来决定返回处理器模式还是任务模式出栈时使用的是PSP 还是MSP因此需要入栈保护。R3 保存的是当前正在运行的任务准确来说是上文因为接下来即将要切换到新的任务的TCB 指针(pxCurrentTCB)地址函数调用后pxCurrentTCB 的值会被更新后面我们还需要通过R3 来操作pxCurrentTCB但是运行函数vTaskSwitchContext 时不确定会不会使用R3 寄存器作为中间变量所以为了保险起见R3 也入栈保护起来。 (10)将configMAX_SYSCALL_INTERRUPT_PRIORITY 的值存储到r0该宏在FreeRTOSConfig.h 中定义用来配置中断屏蔽寄存器BASEPRI 的值高四位有效。目前配置为191因为是高四位有效所以实际值等于11即优先级高于或者等于11 的中断都将被屏蔽。在关中断方面FreeRTOS 与其它的RTOS 关中断不同而是操作BASEPRI 寄存器来预留一部分中断并不像μC/OS 或者RT-Thread 那样直接操作PRIMASK 把所有中断都关闭掉除了硬FAULT。(11)关中断进入临界段因为接下来要更新全局指针pxCurrentTCB的值。(12)调用函数vTaskSwitchContext。该函数在task.c 中定义作用只有一个选择优先级最高的任务然后更新pxCurrentTCB。目前我们还不支持优先级则手动切换不是任务1 就是任务2该函数的具体实现见代码清单 7-24 vTaskSwitchContext()函数。(13)退出临界段开中断直接往BASEPRI 写0。(14)从主堆栈中恢复寄存器r3 和r14 的值此时的sp 使用的是MSP。(15)加载r3 指向的内容到r1。r3 存放的是pxCurrentTCB 的地址即让r1 等于pxCurrentTCB。pxCurrentTCB 在上面的vTaskSwitchContext 函数中被更新指向了下一个将要运行的任务的TCB。(16)加载r1 指向的内容到r0即下一个要运行的任务的栈顶指针。(17)以r0 作为基地址先取值再递增指针LDMIA 的IA 表示Increase After将下一个要运行的任务的任务栈的内容加载到CPU 寄存器r4~r11。(18)更新psp 的值等下异常退出时会以psp 作为基地址将任务栈中剩下的内容自动加载到CPU 寄存器。(19)异常发生时R14 中保存异常返回标志包括返回后进入任务模式还是处理器模式、使用PSP 堆栈指针还是MSP 堆栈指针。此时的r14 等于0xfffffffd最表示异常返回后进入任务模式SP 以PSP 作为堆栈指针出栈出栈完毕后PSP 指向任务栈的栈顶。当调用 bx r14 指令后系统以PSP 作为SP 指针出栈把接下来要运行的新任务的任务栈中剩下的内容加载到CPU 寄存器R0任务形参、R1、R2、R3、R12、R14LR、R15PC和xPSR从而切换到新的任务。 4.2.3 vTaskSwitchContext()函数 (12)调用函数vTaskSwitchContext。该函数在task.c 中定义作用只有一个选择优先级最高的任务然后更新pxCurrentTCB。目前我们还不支持优先级则手动切换不是任务1 就是任务2该函数的具体实现见代码清单 7-24 vTaskSwitchContext()函数。 1 void vTaskSwitchContext( void ) 2 { 3 /* 两个任务轮流切换 */ 4 if ( pxCurrentTCB Task1TCB ) (1) 5 { 6 pxCurrentTCB Task2TCB; 7 } 8 else (2) 9 { 10 pxCurrentTCB Task1TCB; 11 } 12 }(1)如果当前任务为任务1则把下一个要运行的任务改为任务2。(2)如果当前任务为任务2则把下一个要运行的任务改为任务1。 5 main函数 任务的创建就绪列表的实现调度器的实现均已经讲完现在我们把全部的测试代 码都放到main.c 里面具体见代码清单。 1 /** 2 *********************************************************************** 3 * file main.c 4 * author fire 5 * version V1.0 6 * date 2018-xx-xx 7 * brief 《FreeRTOS 内核实现与应用开发实战指南》书籍例程 8 * 任务的定义与任务切换的实现 9 *********************************************************************** 10 * attention 11 * 12 * 实验平台:野火 STM32 系列 开发板 13 * 14 * 官网 :www.embedfire.com 15 * 论坛 :http://www.firebbs.cn 16 * 淘宝 :https://fire-stm32.taobao.com 17 * 18 *********************************************************************** 19 */ 20 21 /* 22 ************************************************************************* 23 * 包含的头文件 24 ************************************************************************* 25 */ 26 #include FreeRTOS.h 27 #include task.h 28 29 /* 30 ************************************************************************* 31 * 全局变量 32 ************************************************************************* 33 */ 34 portCHAR flag1; 35 portCHAR flag2; 36 37 extern List_t pxReadyTasksLists[ configMAX_PRIORITIES ]; 38 39 40 /* 41 ************************************************************************* 42 * 任务控制块 STACK 43 ************************************************************************* 44 */ 45 TaskHandle_t Task1_Handle; 46 #define TASK1_STACK_SIZE 128 47 StackType_t Task1Stack[TASK1_STACK_SIZE]; 48 TCB_t Task1TCB; 49 50 TaskHandle_t Task2_Handle; 51 #define TASK2_STACK_SIZE 128 52 StackType_t Task2Stack[TASK2_STACK_SIZE]; 53 TCB_t Task2TCB; 54 55 56 /* 57 ************************************************************************* 58 * 函数声明 59 ************************************************************************* 60 */ 61 void delay (uint32_t count); 62 void Task1_Entry( void *p_arg ); 63 void Task2_Entry( void *p_arg ); 64 65 /* 66 ************************************************************************ 67 * main 函数 68 ************************************************************************ 69 */ 70 /* 71 * 注意事项1、该工程使用软件仿真debug 需选择 Ude Simulator 72 * 2、在Target 选项卡里面把晶振Xtal(Mhz)的值改为25默认是12 73 * 改成25 是为了跟system_ARMCM3.c 中定义的__SYSTEM_CLOCK 相同 74 * 确保仿真的时候时钟一致 75 */ 76 int main(void) 77 { 78 /* 硬件初始化 */ 79 /* 将硬件相关的初始化放在这里如果是软件仿真则没有相关初始化代码 */ 80 81 /* 初始化与任务相关的列表如就绪列表 */ 82 prvInitialiseTaskLists(); 83 84 /* 创建任务 */ 85 Task1_Handle 86 xTaskCreateStatic( (TaskFunction_t)Task1_Entry, /* 任务入口 */ 87 (char *)Task1, /* 任务名称字符串形式 */ 88 (uint32_t)TASK1_STACK_SIZE , /* 任务栈大小单位为字 */ 89 (void *) NULL, /* 任务形参 */ 90 (StackType_t *)Task1Stack, /* 任务栈起始地址 */ 91 (TCB_t *)Task1TCB ); /* 任务控制块 */ 92 /* 将任务添加到就绪列表 */ 93 vListInsertEnd( ( pxReadyTasksLists[1] ), 94 ( ((TCB_t *)(Task1TCB))-xStateListItem ) ); 95 96 Task2_Handle 97 xTaskCreateStatic( (TaskFunction_t)Task2_Entry, /* 任务入口 */ 98 (char *)Task2, /* 任务名称字符串形式 */ 99 (uint32_t)TASK2_STACK_SIZE , /* 任务栈大小单位为字 */ 100 (void *) NULL, /* 任务形参 */ 101 (StackType_t *)Task2Stack, /* 任务栈起始地址 */ 102 (TCB_t *)Task2TCB ); /* 任务控制块 */ 103 /* 将任务添加到就绪列表 */ 104 vListInsertEnd( ( pxReadyTasksLists[2] ), 105 ( ((TCB_t *)(Task2TCB))-xStateListItem ) ); 106 107 /* 启动调度器开始多任务调度启动成功则不返回 */ 108 vTaskStartScheduler(); 109 110 for (;;) 111 { 112 /* 系统启动成功不会到达这里 */ 113 } 114 } 115 116 /* 117 *********************************************************************** 118 * 函数实现 119 *********************************************************************** 120 */ 121 /* 软件延时 */ 122 void delay (uint32_t count) 123 { 124 for (; count!0; count--); 125 } 126 /* 任务1 */ 127 void Task1_Entry( void *p_arg ) 128 { 129 for ( ;; ) 130 { 131 flag1 1; 132 delay( 100 ); 133 flag1 0; 134 delay( 100 ); 135 136 /* 任务切换这里是手动切换 */ 137 taskYIELD(); (注意) 138 } 139 } 140 141 /* 任务2 */ 142 void Task2_Entry( void *p_arg ) 143 { 144 for ( ;; ) 145 { 146 flag2 1; 147 delay( 100 ); 148 flag2 0; 149 delay( 100 ); 150 151 /* 任务切换这里是手动切换 */ 152 taskYIELD(); (注意) 153 } 154 }每个局部的代码均已经讲解过剩下的看代码注释即可。(注意)因为目前还不支持优先级每个任务执行完毕之后都主动调用任务切换函数taskYIELD()来实现任务的切换。 6 实验现象 本章代码讲解完毕接下来是软件调试仿真具体过程见图 本章讲解完毕。但是只是把本章的内容看完然后再仿真看看波形是远远不够的应该是把当前任务控制块指针pxCurrentTCB、就绪列表pxReadyTaskLists、每个任务的控制块和任务的栈这些变量统统添加到观察窗口然后单步执行程序看看这些变量是怎么变化的特别是任务切换时CPU 寄存器、任务栈和PSP 这些是怎么变化的让机器执行代码的过程在自己的脑子里面过一遍。下图就是我在仿真调试时的观察窗口。 7 本章涉及到的汇编指令 本章中有些函数是用汇编编写的涉及到的ARM 汇编指令具体参考表格
http://www.yayakq.cn/news/2205/

相关文章:

  • 公众号里的网站怎么做的百度快速排名系统查询
  • 网站平台建设心得学设计师需要学历吗
  • 网站开发设计素材网络 网站建设办公
  • 如何进行网络营销方式seo服务商
  • 建设部网站退休注册人员工程项目编号查询系统
  • 江西工程建设信息网站世界工厂采购网下载
  • 数据查询网站如何做永久3e38cos免费
  • 企业网站seo外包用php做网站要用什么软件
  • 永和建设集团有限公司网站广州网站设计首选柚米
  • 便宜网站制作公司wordpress内插件翻译
  • 简洁大气企业网站模板网站制作 昆明
  • 学做网站论坛课程湘潭做网站价格找磐石网络一流
  • 电子商务网站建设读书报告传奇手游网站大全9377
  • 上市公司网站建设网站开发团队需配备什么岗位
  • asp制作网站北京泰达建设有限公司网站
  • 工控人如何做自己的网站网络营销就是网上营销
  • 建网站做seo建设部监理工程师网站
  • 做网站的主题有哪些用jsp做网站用什么软件
  • 萝卜建站信息技术网站建设市场分析
  • 阿里巴巴怎么做网站小程序代理方法
  • 免费的黄冈网站有哪些平台可以用微信支付wordpress搬家出现404
  • 关于京东商城网站建设的实践报告广州定制网站开发
  • 网站开发与硬件合同wordpress 手机验证码
  • 智慧团建网站登录电脑版网页设计工具软件有哪些
  • 天津企业做网站多少钱东坑网站仿做
  • 稳定的网站建设编程app免费
  • 毕业设计网站用什么做南通网站建设果尔
  • 凡科网站怎么做友情链接桂林北站到象鼻山景区怎么坐车
  • 站长工具无内鬼放心开车禁止收费墨玉县建设工程信息网
  • 地方网站发展方向网站建设背景朝阳