嵌入式学习笔记(综合提高篇 第二章) -- FreeRTOS的移植和应用
1.1 资料准备和分析
上章节通过实现双机通讯,了解如何设计和实现自定义协议,不过对于嵌入式系统来说,当然不仅仅包含协议,还有其它很多需要深入学习了解的知识,下面将列出我在工作和学习上遇到的嵌入式方向及知识点,虽然不一定全面,也基本上覆盖大部分嵌入式应用。
- 嵌入式RTOS(包括不限于uCos,FreeRTOS,RT-Thread)的移植和应用,以及配合的文件系统,协议栈等的移植
- 图像/摄像头,音/视频流和GUI/触摸等,以及依托之上的菜单管理,图像识别,视频流压缩等功能实现
- 通讯协议/射频,如支持以太网的TCP/IP协议, 2.4GHz无线的BLE协议,基于无线网络的WIFI协议和常见的USB协议等,另外支持大部分双机之间通过不同物理接口(如RS485,I2C,SPI, USART)通讯的自定义用户协议,目前流行的物联网设备大都基于此开发。
- 低功耗设计,这里包含芯片选型,硬件的设计/调试,应用的整体设计配合,这些需要经验和对芯片的深入了解
这些都是我踏入行业后接触到的嵌入式大方向的知识,有些有丰富的经验,如RTOS操作系统的移植和应用,摄像头驱动调试/图像采集,TCP/IP协议(lwip协议移植,web,snmp,telnet服务器), 有些则浅尝辄止,只是因为项目和开发需求,有过开发经验但没有深入了解,如低功耗设备开发,BLE/WIFI模组开发/应用,图像算法的使用,GUI移植、菜单界面开发和RFID连接开发调试。说到这里,只是提醒这些并不需要全都需要深入掌握,之所以总结出来,也是希望能够对未来发展方向选择提供指导。
这里我也根据开发板硬件和外部设备的限制,规划后续的深入了解,后续将结合嵌入式RTOS,菜单界面GUI开发,TCP/IP网络协议(LWIP),USB协议开发,WIFI模块(WIFI转串口开发)来实践,包含FreeRTOS的移植和应用开发,文件系统移植,GUI移植和开发,LWIP移植,USB协议移植以及相应的应用开发,对于低功耗,CMOS,视频流则不在涉及,如果后续有机会将单独整理这部分的经验,下面开始本章的重点,FreeRTOS的移植或应用,具体要求要求如下:
- 使用FreeRTOS实现uart点亮LED灯
- 增加协议实现串口转发功能
- 支持串口远程复位
- 系统出现错误后能够自我恢复,优化串口发送
资料/设置(在之前基础上添加):
- FreeRTOSv10.2.0源代码
- FreeRTOS_Reference_Manual_V10.0.0文档
根据设计要求,在结合上述文档,首先就可以确定需要掌握的知识点包含,上位机和协议部分需要在上章基础上进行扩充,在主循环中的处理代码可以由FreeRTOS的任务单独完成,接收数据标志位由信号量替代,另外因为系统的复杂度提升,为了保证系统在出错后能够自恢复,需要硬件、软件或者窗口看门狗的引入。经过这些分析,就可以将整个软件的实现分解成以下任务:
- FreeRTOS系统的移植 ,实现测试程序并运行正常(一般以点亮LED或者打印为准)。
- 移植上章实现的Uart通讯协议,将主函数内循环改由任务完成,标志位由任务间通讯完成,并在上章协议的基础上完善添加串口转发和系统复位命令。
- 代码改进提高,引入看门狗任务,保证系统自恢复,串口发送改由DMA完成,提高代码的处理效率
1.2 FreeRTOS系统的移植
FreeRTOS的移植并不困难,从官网下载https://www.freertos.org/a00104.html最新源代码压缩包,解压后复制FreeRTOSv10.2.0/FreeRTOS下的文件夹到到当前工程下(DEMO文件夹是提供的例程,可作为参考,不需要全部添加)。
在工程界面目录下添加FreeRTOS文件夹,将FreeRTOS/Source下的所有.c文件导入到目录下,并添加portable/MemMang下的heap_4.c以及portable/RVDS/ARM_CM7/r0p1下的port.c文件;添加portable/RVDS/ARM_CM7/r0p1和Source/include到头文件路径,如下图所示:
图 1 FreeRTOS文件/路径添加
完成后编译,此时会报大量error: #5: cannot open source input file "FreeRTOSConfig.h": No such file or directory, 这表示缺少与FreeRTOS跨平台相关系统配置文件,此时不用太过担心,只要去FreeRTOS/Demo/CORTEX_M7_STM32F7_STM32756G-EVAL_IAR_Keil下复制FreeRTOSConfig.h到当前目录可访问路径下,但此时配置不能直接使用,需要注释掉与芯片/代码不相符的配置,修改后文件如下:
#ifndef FREERTOS_CONFIG_H #define FREERTOS_CONFIG_H /*----------------------------------------------------------- * Application specific definitions. * * These definitions should be adjusted for your particular hardware and * application requirements. * * THESE PARAMETERS ARE DESCRIBED WITHIN THE 'CONFIGURATION' SECTION OF THE * FreeRTOS API DOCUMENTATION AVAILABLE ON THE FreeRTOS.org WEB SITE. * * See http://www.freertos.org/a00110.html *----------------------------------------------------------*/ //board.h不存在,修改为stm32f746xx.h /* For definition of BOARD_MCK. */ #ifndef __IAR_SYSTEMS_ASM__ /* Prevent chip.h being included when this file is included from the IAR port layer assembly file. */ #include “board.h” #include "stm32f746xx.h" #endif #define configUSE_PREEMPTION 1 #define configUSE_PORT_OPTIMISED_TASK_SELECTION 1 #define configUSE_QUEUE_SETS 1 #define configUSE_IDLE_HOOK 0 #define configUSE_TICK_HOOK 0 //时钟钩子函数暂不需要 #define configCPU_CLOCK_HZ ( ( unsigned long ) 216000000 ) //系统时钟216Mhz #define configTICK_RATE_HZ ( 1000 ) #define configMAX_PRIORITIES ( 6 ) //最高优先级改大 #define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 ) #define configTOTAL_HEAP_SIZE ( ( size_t ) ( 18 * 1024 ) ) //系统堆改小 #define configMAX_TASK_NAME_LEN ( 16 ) //任务名变大 #define configUSE_TRACE_FACILITY 1 #define configUSE_16_BIT_TICKS 0 #define configIDLE_SHOULD_YIELD 1 #define configUSE_MUTEXES 1 #define configQUEUE_REGISTRY_SIZE 8 #define configCHECK_FOR_STACK_OVERFLOW 0 //栈溢出检查暂不实现 #define configUSE_RECURSIVE_MUTEXES 1 #define configUSE_MALLOC_FAILED_HOOK 0 //堆溢出检查暂不实现 #define configUSE_APPLICATION_TASK_TAG 0 #define configUSE_COUNTING_SEMAPHORES 1 /* The full demo always has tasks to run so the tick will never be turned off. The blinky demo will use the default tickless idle implementation to turn the tick off. */ #define configUSE_TICKLESS_IDLE 0 /* Run time stats gathering definitions. */ #define configGENERATE_RUN_TIME_STATS 0 /* This demo makes use of one or more example stats formatting functions. These format the raw data provided by the uxTaskGetSystemState() function in to human readable ASCII form. See the notes in the implementation of vTaskList() within FreeRTOS/Source/tasks.c for limitations. */ #define configUSE_STATS_FORMATTING_FUNCTIONS 1 /* Co-routine definitions. */ #define configUSE_CO_ROUTINES 0 #define configMAX_CO_ROUTINE_PRIORITIES ( 2 ) /* Software timer definitions. */ #define configUSE_TIMERS 1 #define configTIMER_TASK_PRIORITY ( configMAX_PRIORITIES - 1 ) #define configTIMER_QUEUE_LENGTH 5 #define configTIMER_TASK_STACK_DEPTH ( configMINIMAL_STACK_SIZE * 2 ) /* Set the following definitions to 1 to include the API function, or zero to exclude the API function. */ #define INCLUDE_vTaskPrioritySet 1 #define INCLUDE_uxTaskPriorityGet 1 #define INCLUDE_vTaskDelete 1 #define INCLUDE_vTaskCleanUpResources 1 #define INCLUDE_vTaskSuspend 1 #define INCLUDE_vTaskDelayUntil 1 #define INCLUDE_vTaskDelay 1 #define INCLUDE_eTaskGetState 1 #define INCLUDE_xTimerPendFunctionCall 1
/* Cortex-M specific definitions. */ #ifdef __NVIC_PRIO_BITS /* __BVIC_PRIO_BITS will be specified when CMSIS is being used. */ #define configPRIO_BITS __NVIC_PRIO_BITS #else #define configPRIO_BITS 3 /* 7 priority levels */ #endif /* The lowest interrupt priority that can be used in a call to a "set priority" function. */ #define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 0xF //Priority最低值为所有位为1即0xF /* The highest interrupt priority that can be used by any interrupt service routine that makes calls to interrupt safe FreeRTOS API functions. DO NOT CALL INTERRUPT SAFE FREERTOS API FUNCTIONS FROM ANY INTERRUPT THAT HAS A HIGHER PRIORITY THAN THIS! (higher priorities are lower numeric values. */ #define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 4 /* Interrupt priorities used by the kernel port layer itself. These are generic to all Cortex-M ports, and do not rely on any particular library functions. */ #define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
/* !!!! configMAX_SYSCALL_INTERRUPT_PRIORITY must not be set to zero !!!! See http://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html. */ #define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) ) /* Normal assert() semantics without relying on the provision of an assert.h header file. */ #define configASSERT( x ) if( ( x ) == 0 ) { taskDISABLE_INTERRUPTS(); for( ;; ); } /* Definitions that map the FreeRTOS port interrupt handlers to their CMSIS standard names. */ #define xPortPendSVHandler PendSV_Handler #define vPortSVCHandler SVC_Handler #define xPortSysTickHandler SysTick_Handler #endif /* FREERTOS_CONFIG_H */
然后在任务创建前添加代码SCB->AIRCR = (VECTKEYSTAT | NVIC_PriorityGroup_4);在主函数中添加用于测试的LED闪烁任务和创建即可
static void vLEDTask( void *pvParameters ) { while(1) { vTaskDelay( pdMS_TO_TICKS(500) ); led_gpio_on(); vTaskDelay( pdMS_TO_TICKS(500) ); led_gpio_off(); } } void TestTaskCreate(void) { //任务创建 xTaskCreate( vLEDTask, "LED", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY, NULL ); /* Start the scheduler. */ vTaskStartScheduler(); }
编译后下载运行后,就可以看到LED闪烁,此时FreeRTOS系统就成功运行了。对于引入的FreeRTOSConfig.h文件修改,其中替换包含头文件,时钟频率,钩子函数和堆栈检测标志位等,这些项的修改比较容易理解,看说明注释即可,这里主要讲为什么在初始化时添加SCB->AIRCR = (VECTKEYSTAT | NVIC_PriorityGroup_4);以及修改优先级,因为STM32F746支持最大4位优先级,也就是最大值(优先级最低)就是0xF, 所以configLIBRARY_LOWEST_INTERRUPT_PRIORITY的值同样需要要保持一致;另外FreeRTOS为了保证系统的实时性,要求所有被管理中断都配置为可抢占优先级中断,参考下面这段话:
If you are using an STM32 with the STM32 driver library then ensure all the priority bits are assigned to be preempt priority
这里给出来源参考http://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html以及《Cortex-M3权威指南》7.2节优先级的定义部分, 至此,我们完成了任务的第一步,FreeRTOS的移植。
1.3 串口协议移植和功能添加
串口通讯协议因为在开发时已经考虑到结构化,所以修改起来工作量不大,主要包含协议处理任务的实现,并将之前通过状态标志位传递修改为消息实现,具体实现如下。
消息的创建和管理
//信号量创建UsartProtocolInit函数 USART_STATUS.rxsem = xSemaphoreCreateBinary(); //信号量等待ReceiveCheckData函数 rtype = xSemaphoreTake(USART_STATUS.rxsem, 1000); //信号量的释放USART6_IRQHandler函数 xSemaphoreGiveFromISR( USART_STATUS.rxsem, &xHigherPriorityTaskWoken ); portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
同时保留USART_STATUS.reflag = 0; 用于保证串口接收缓存在代码处理完成后不会被后续接收覆盖导致数据出错。上述修改完成后,编译下载后测试,此时通过协议修改LED能够正常工作,如此便完成了协议的移植。
至于功能说明的添加到ProtoCmdDone函数中,在上章的将0x01定义为USART设备,则可以添加如下代码:
case CMD_USART: sendbuffer(USART1, (char *)&pdata[1], len-1); size = CreateOutBuf(RT_OK, NULL, 0); break;
添加新的代码后,就可通过串口发送数据,测试结构如图 3‑2所示
图 2 串口数据转发示意图
串口实现软件复位的实现就更简单些,参考资料文件夹下的《嵌入式协议》内,添加软件复位指令即可,代码实现如下。
if(pFrame->type == TYPE_DEVCMD)
{ size = ProtoCmdDone(USART_STATUS.rxdata, REVERSAL16(pFrame->length)); } else if (pFrame->type == TYPE_DEVRST)
{ NVIC_SystemReset(); } else size = CreateOutBuf(RT_FAIL, NULL, 0); //sendbuffer(USART6, (char *)USART_STATUS.txdata, size); sendbuffer_dma((char *)USART_STATUS.txdata, size); return RT_OK;
如此便完成了串口协议的移植以及新增加了串口转发及软件复位功能。至于发送部分的改进则要结合Uart驱动和DMA部分,首先要确认Uart对应的DMA模块和通道。
图 3 DMA通道表
这里表示数据流6和数据流7都可以选择,这里选择数据流7,就可实现如下代码。
void uart_dma_module_init(void) { DMA_InitTypeDef DMA_InitStruct; RCC_ENABLE_CLK(RCC->AHB1ENR, RCC_CLK_AHB1_DMA2); //FLASH => USART DMA_InitStruct.Channel = DMA_CHANNEL_5; DMA_InitStruct.Direction = DMA_MEM_DIR_PER; DMA_InitStruct.PeriphInc = DMA_PINC_DISABLE; DMA_InitStruct.MemInc = DMA_MINC_ENABLE; DMA_InitStruct.PeriphDataAlignment = DMA_MSIZE_1; DMA_InitStruct.MemDataAlignment = DMA_MSIZE_1; DMA_InitStruct.Mode = DMA_NORMAL; DMA_InitStruct.Priority = DMA_PL_HIGH; DMA_InitStruct.FIFOMode = DMA_FIFOMODE_DISABLE; DMA_InitStruct.FIFOThreshold = DMA_FIFOMODE_THRESHOLD_4_4; DMA_InitStruct.MemBurst = DMA_PBURST_1; DMA_InitStruct.PeriphBurst = DMA_PBURST_1; DMA_Init(DMA2_Stream7, &DMA_InitStruct); //启动USART6的DMA发送 USART6->CR3 |= (1<<7); }
至于发送代码则如下。
void sendbuffer_dma(char *ptr, int len) { #if CACHE_OPEN == 1 //发送首地址必须为4的整倍数(设计要求) SCB_InvalidateDCache_by_Addr((uint32_t *)ptr, len/4+1); #endif //DMA再次启动时需要清除状态位,否则只能发送一次 //模块使能位状态注释说明 DMA2->HIFCR |= (0xf<<24) | (0x1<<22); DMA_Start(DMA2_Stream7,(uint32_t)ptr,(uint32_t)(&(USART6->TDR)), len); if(DMA_CheckDone(DMA2_Stream7, 1) != RT_OK) { vTaskDelay( pdMS_TO_TICKS(1) ); } while((USART6->ISR&(1<<6)) == 0); }
1.4 项目程序改进
结合上位机软件的开发,我们对于本次应用的功能需求基本上已经全部完成,不过因为设计时要求系统异常时能够自复位,这里介绍下什么是自复位,为什么需要自复位。
所谓自复位很好理解,当芯片工作不正常时,能够自己进行软件复位,保证系统能够恢复初始状态继续运行。不过正常情况下运行一段时间出错是不允许的,这时一般是软件有逻辑错误或者代码错误,有可能是数组/指针越界,任务分支异常,这些是软件测试时要解决和避免的问题,不能够依靠自复位解决。不过有的情况下如突然的电流/电压异常,外部温度的急剧变化等外部环境会引起芯片不正常反应,进入正常情况不可能达到的分支,从而触发异常,这时自复位就可以保证芯片在重启后能够继续实现功能,而不是直接死机,可以一定程度上提高产品的可靠性。这里要深刻记住一点,自复位是功能的限制补充,而不是开发运行的手段,正常情况下复位首先需要的是定位和解决。
对于大部分芯片,都提供看门狗来实现芯片自复位,当看门狗配置定时时钟,启动后,就需要在指定时间写入寄存器保证内部定时器计数的刷新,否则就会软件复位,如此当芯片进入异常分支时,便能够自动复位。具体实现如下,当然在FreeRTOS下我们就可以创建任务单独管理看门狗功能。
void vIdleTask(void *pvParameters) { TickType_t delaytick = 2000; #if SYSTEM_NODEBUG == 1 iwdg_module_init(); #endif
while(1) { iwdg_module_reload(); vTaskDelay(delaytick); } } xTaskCreate(vIdleTask, "vIdleTaskk", configMINIMAL_STACK_SIZE, NULL, vIdleTaskPriority, NULL );
硬件部分的操作参考手册与IWDG相关的说明,就可以轻松的实现。
void iwdg_module_init(void) { IWDG->KR = 0x5555;
//独立看门狗时钟由内部32Khz时钟提供 //则复位时间为 计数/(时钟频率/分频系数),暂定8s //RLR值为8*(32000/64)= 4000 IWDG->PR = 0x4; IWDG->RLR = 0xFA0; IWDG->KR = 0xcccc; }
至此,自复位的功能也已经实现完毕。
1.5 总结和知识点
在本章节中,我们在上章的串口点亮LED任务代码基础上移植了FreeRTOS,扩展了串口实现,并实现了相应的上位机功能,同时增加了自复位功能,进一步优化了产品的可靠性,仔细思考全部流程,就会发现无论FreeRTOS的移植,还是串口的扩展功能实现,架构部分基本保持不变,这正是协议制定后开发的优势。此外我们也了解了自复位和看门狗存在的意义,进一步理解产品开发中细节的重要性。事实上到这步,产品开发的框架已经走向了正轨,后续在这基础上扩展功能就可以一步步实现心目中的作品,当然这并不是结束,恰恰是真正的开始,下章我们将暂时远离自定义协议,从LCD显示和GUI部分在讲诉另外的功能实现。
本章节涉及的部分细节知识点:
- FreeRTOS移植
- FreeRTOS任务创建/管理,消息通讯
- USART发送DMA模式
- 独立看门狗(IWDG)
本项目开发相关资料和代码实现见附件:
链接:https://pan.baidu.com/s/1pXkKG3y8ItXWqXLC4ODuaw
提取码:kzol