本文隶属于AVR单片机教程系列。html
在系列教程的最后一篇中,我将向你推荐3个能够深造的方向:RTOS、C++、事件驱动。掌握这些技术能够帮助你更快、更好地开发更大的项目。git
本文涉及到许多概念性的内容,若是你有不一样意见,欢迎讨论。编程
这一篇教程叫做“走向高层”。什么是高层?函数
我认为,若是寥寥几行代码就能实现一个复杂功能,或者一行代码能够对应到几百句汇编,那么你就站在高层。高层与底层是相对的概念,没有绝对的界限。工具
站得高,看得远,这一样适用于编程,咱们要走向高层。高层是对底层的封装,是对现实的抽象,高层相比于底层更加贴近应用。站在高层,你能够看到不少底层看不到的东西,主要有编程工具和思路。合理利用工具,能够简化代码,下降工做量;用合适的思路编程,更能够事半功倍。学习
可是,掌握高层并不意味着忽视甚至鄙视底层,高层创建在底层基础之上。其一,有些高层出现的诡异现象能够追溯到底层,这样的debug任务只有通晓底层与高层的开发者才能胜任;其二,为了让高层实现复杂功能的同时得到可接受的运行效率,底层必须设计地更加精致,这就对底层提出了更高的要求。测试
相信你通过一期和二期的教程,已经至关熟悉AVR编程的底层了。跟我一块儿走上高层吧!ui
实时操做系统(RTOS)是一类操做系统。带有操做系统的计算机系统相比不带有的,最显著的特色是支持多任务。咱们以前写的程序,在监控按键的同时,开了一个定时器中断用于数码管动态扫描,两个任务同时进行,是多任务吗?不彻底是。监控按键与动态扫描两个任务只有一个能够占据main
函数,另外一个必须放在中断里,中断里的任务不能执行太长时间,不然就会干扰main
函数的运行。而操做系统中的任务调度器能够给每一个任务分配必定的运行时间,CPU一会执行这个,一会执行那个,每一个任务都好像独占了CPU连续执行同样。操作系统
RTOS与其余操做系统的主要区别在于任务调度器的设计。在RTOS中,全部任务都有优先级,优先级高的被调度器保证优先执行,以得到最短的响应时间。在与现实世界打交道的嵌入式系统中,这样的功能每每是必要的。debug
操做系统一般须要中档的硬件,8位的AVR稍差了一点,主频和存储容量达不到一些操做系统的要求,不过仍是有可选项的。咱们来试着在开发板上运行FreeRTOS。FreeRTOS是一个免费的、为单片机设计的RTOS,是目前嵌入式市场占有率第二的操做系统,仅次于Linux。
首先去官网下载代码。下载的是一个.zip
压缩包,找到FreeRTOS
文件夹,目录下Demo
和Source
中的部分代码是须要使用的。做为一个跨平台的系统,大多数代码平台无关,只存一份,其余平台相关的代码,每一个平台都有独立的实现,源码是demo都是如此,这使得代码组织有些复杂,你能够参考官方文档。
官方提供了ATmega323单片机的demo,为了在开发板上运行,须要作一些修改。demo基于WinAVR平台,它与Atmel Studio同样,都是基于avr-gcc的。若是你有WinAVR的话,直接用makefile
就能够编译;Atmel Studio虽然也提供了make
,但有些微区别,无法直接用makefile
,所以咱们本身创建项目来编译。
新建项目,而后在Solution Explorer中建3个文件夹:source
、port
和demo
。
拷贝一些文件到这些目录下:
source
:\Source\include\
全部文件、\Source\
下的tasks.c
、queue.c
、list.c
和croutine.c
;
port
:\Source\portable\GCC\ATmega323\
全部文件和\Source\portable\MemMang`下的heap_1.c
;
demo
:\Demo\Common\include\
全部文件、\Demo\Common\Minimal\
下的crflash.c
、integer.c
、PollQ.c
和comtest.c
、\Demo\AVR_ATMega323_WinAVR\
除makefile
之外的全部文件,再把ParTest.c
和serial.c
拎出来,main.c
拎到外面。
我是怎么知道的呢?我参考了官方文档和makefile
文件。
在Solution Explorer中Add Existing Item,在项目属性->Toolchain->AVR/GNU C Compiler->Directories中添加这三个目录。
修改代码,使之适用于咱们的开发板:
修改的理由有如下几种:
ATmega323和ATmega324的寄存器略有不一样;
WinAVR和Atmel Studio提供的工具链中的一些定义方式不一样;
硬件配置与链接不一样。
因此须要作如下修改:
port.c
中:TIMSK
改成TIMSK1
;SIG_OUTPUT_COMPARE1A
改成TIMER1_COMPA_vect
;54行改成0x02
;
FreeRTOSConfig.h
中:48行改成25000000
;
serial.c
中:UDR
、UCSRB
、UCSRC
、UBRRL
、UBRRH
分别改成UDR0
、UCSR0B
、UCSR0C
、UBRR0L
、UBRR0H
;67行改成0x00
;188行改成ISR(USART0_RX_vect)
;207行改成ISR(USART0_UDRE_vect)
;
comtest.c
中:71行改成4
;72行改成2
;
ParTest.c
中:DDRB
改成DDRC
;PORTB
改成PORTC
;49行改成0x00
;50行改成3
;72和99行把uxLED
改成(4 + uxLED)
;76行把if
和else
的大括号中的语句对调;
main.c
中:删除81和84行;111行改成0
;117行改成3
;127行改成2
;153行返回类型改成int
。
不出意外的话,如今代码能够经过编译了(我这里有3个warning)。下载到单片机上,链接TX
和RX
,你会发现红灯和黄灯分别以300ms和400ms为周期闪烁,绿灯和串口黄灯一块儿闪烁,蓝灯不亮。
实际上,程序建立了1个整数计算、2个串口收发、2个队列收发、2个寄存器测试、1个错误检查和1个空闲共9个任务,以及2个LED闪烁协程。每过一毫秒,定时器产生一次中断,任务调度器暂停当前任务,换一个任务开始运行。为了理解这个过程,咱们先介绍上下文这个概念。
一个任务在执行的过程当中,须要一些临时变量,它们有的保存在栈上(栈是内存中的一块区域,寄存器SP
指向栈顶),有的在寄存器中;此外,条件分支语句还要用到寄存器SREG
中的位,这些位在以前的语句中被置位或清零;还有记录当前程序执行到哪的程序计数器。这些一块儿构成了任务执行的上下文:寄存器r0
到r31
、SREG
、SP
和PC
。不一样任务的上下文是不共享的,但它们却要占用相同的位置,为此,在切换任务时须要把前一个上下文保存起来,并恢复要切换到的任务的上下文,这个过程称为上下文切换,而后才能继续这个任务。
咱们来结合代码分析一下这个过程。
void TIMER1_COMPA_vect( void ) __attribute__ ( ( signal, naked ) ); void TIMER1_COMPA_vect( void ) { vPortYieldFromTick(); asm volatile ( "reti" ); } void vPortYieldFromTick( void ) __attribute__ ( ( naked ) ); void vPortYieldFromTick( void ) { portSAVE_CONTEXT(); if( xTaskIncrementTick() != pdFALSE ) { vTaskSwitchContext(); } portRESTORE_CONTEXT(); asm volatile ( "ret" ); } typedef void TCB_t; extern volatile TCB_t * volatile pxCurrentTCB; #define portSAVE_CONTEXT() \ asm volatile ( "push r0 \n\t" \ "in r0, __SREG__ \n\t" \ "cli \n\t" \ "push r0 \n\t" \ "push r1 \n\t" \ "clr r1 \n\t" \ "push r2 \n\t" \ "push r3 \n\t" \ "push r4 \n\t" \ "push r5 \n\t" \ "push r6 \n\t" \ "push r7 \n\t" \ "push r8 \n\t" \ "push r9 \n\t" \ "push r10 \n\t" \ "push r11 \n\t" \ "push r12 \n\t" \ "push r13 \n\t" \ "push r14 \n\t" \ "push r15 \n\t" \ "push r16 \n\t" \ "push r17 \n\t" \ "push r18 \n\t" \ "push r19 \n\t" \ "push r20 \n\t" \ "push r21 \n\t" \ "push r22 \n\t" \ "push r23 \n\t" \ "push r24 \n\t" \ "push r25 \n\t" \ "push r26 \n\t" \ "push r27 \n\t" \ "push r28 \n\t" \ "push r29 \n\t" \ "push r30 \n\t" \ "push r31 \n\t" \ "lds r26, pxCurrentTCB \n\t" \ "lds r27, pxCurrentTCB + 1 \n\t" \ "in r0, 0x3d \n\t" \ "st x+, r0 \n\t" \ "in r0, 0x3e \n\t" \ "st x+, r0 \n\t" \ ); #define portRESTORE_CONTEXT() \ asm volatile ( "lds r26, pxCurrentTCB \n\t" \ "lds r27, pxCurrentTCB + 1 \n\t" \ "ld r28, x+ \n\t" \ "out __SP_L__, r28 \n\t" \ "ld r29, x+ \n\t" \ "out __SP_H__, r29 \n\t" \ "pop r31 \n\t" \ "pop r30 \n\t" \ "pop r29 \n\t" \ "pop r28 \n\t" \ "pop r27 \n\t" \ "pop r26 \n\t" \ "pop r25 \n\t" \ "pop r24 \n\t" \ "pop r23 \n\t" \ "pop r22 \n\t" \ "pop r21 \n\t" \ "pop r20 \n\t" \ "pop r19 \n\t" \ "pop r18 \n\t" \ "pop r17 \n\t" \ "pop r16 \n\t" \ "pop r15 \n\t" \ "pop r14 \n\t" \ "pop r13 \n\t" \ "pop r12 \n\t" \ "pop r11 \n\t" \ "pop r10 \n\t" \ "pop r9 \n\t" \ "pop r8 \n\t" \ "pop r7 \n\t" \ "pop r6 \n\t" \ "pop r5 \n\t" \ "pop r4 \n\t" \ "pop r3 \n\t" \ "pop r2 \n\t" \ "pop r1 \n\t" \ "pop r0 \n\t" \ "out __SREG__, r0 \n\t" \ "pop r0 \n\t" \ );
在定时器中断TIMER1_COMPA_vect
中,vPortYieldFromTick
被调用,其中依次调用portSAVE_CONTEXT
、xTaskIncrementTick
、vTaskSwitchContext
(可能不调用)和portRESTORE_CONTEXT
,执行汇编语句ret
;最后执行reti
。
在介绍中断的时候,咱们提到过编译器添加的额外代码,把用到的寄存器都push进栈。可是,编译器只会保护该中断用到的寄存器,而上下文包括全部寄存器,须要手动地编写代码,那么也就无需编译器添加多余的代码了。函数TIMER1_COMPA_vect
被添加attributenaked
,表示无需添加任何代码,把用户编写的原本来本地编进去就够了。
进入中断时,PC
被push进栈(这是硬件作的),PC
内容变为TIMER1_COMPA_vect
的地址,随后开始执行,PC
再次push进栈(没有在图片中表示出来),开始执行portSAVE_CONTEXT
保存上下文。因为它是宏,就没有PC
进栈的过程。
而后,r0
、SREG
、r1
到r31
依次进栈,上下文的内容保存完成,其位置还须要另存。SP
指向栈顶,表明着上下文的位置,它被复制到pxCurrentTCB
所指的位置中。pxCurrentTCB
其实是结构体TCB_t
指针,该结构体保存着当前执行的任务的信息,前两个字节保存栈指针。这样,上下文就保存完成了。
xTaskIncrementTick
把软件计数器加1,并检查是否须要任务切换。为了讲解,咱们假定它须要,那么vTaskSwitchContext
就会被调用,pxCurrentTCB
指向另外一个TCB_t
变量,那里保存着另外一个任务的上下文,咱们要恢复它。
恢复过程是,先用pxCurrentTCB
取出SP
,再按相反的顺序出栈,上下文中就只剩PC
没有恢复了(ret
和vPortYieldFromTick
的调用抵消,一块儿忽略)。最后执行reti
,该汇编语句从栈顶取两个字节放进PC
,并跳转到其位置继续执行。此时,PC
的内容就是该任务以前被中断时执行到的位置,如今从PC
开始继续执行,也就是继续执行该任务。上下文切换完成。
在对FreeRTOS稍有了解后,咱们动手写一个基于FreeRTOS的程序。在学习数码管的时候,你极可能考虑过,在后台建立一个任务,执行数码管的扫描。如今,FreeRTOS给了你这个机会。咱们建立两个任务,一个每一毫秒显示数码管的一位,另外一个每200毫秒更新显示的数字。
#include <stdlib.h> #include "FreeRTOS.h" #include "task.h" #include "semphr.h" #include <ee2/segment.h> SemaphoreHandle_t mutex; portTASK_FUNCTION(segment_scan, pvParameters) { while (1) { static uint8_t digit = 0; xSemaphoreTake(mutex, 1000); segment_display(digit); xSemaphoreGive(mutex); if (++digit == 2) digit = 0; vTaskDelay(1); } } portTASK_FUNCTION(segment_set, pvParameters) { while (1) { static uint8_t number = 0; xSemaphoreTake(mutex, 1000); segment_dec(number); xSemaphoreGive(mutex); if (++number == 100) number = 0; vTaskDelay(200); } } int main() { segment_init(PIN_8, PIN_9); mutex = xSemaphoreCreateMutex(); xTaskCreate(segment_scan, "scan", configMINIMAL_STACK_SIZE, NULL, 1, NULL); xTaskCreate(segment_set, "set", configMINIMAL_STACK_SIZE, NULL, 2, NULL); vTaskStartScheduler(); return 0; }
两个任务都须要使用数码管这一资源。若是一个任务正在调用segment_dec
,还没返回时,定时器中断发生,切换到另外一个任务,其中调用了segment_display
,就会发生冲突。咱们用一个互斥量mutex
来解决。当一个任务调用了xSemaphoreTake
后,在它调用xSemaphoreGive
前,mutex
会进入锁定状态,若是另外一个任务试图调用xSemaphoreTake
,则会阻塞住,切换到另外一个任务。这样就保证两个任务不会冲突。资源共享是并行程序要着重处理的问题之一。
FreeRTOS还有不少功能等待你去发掘,RTOS就更多了。最后,咱们来谈谈RTOS的长处和短处。
RTOS是多任务的,这是对代码顺序执行的编程模型的颠覆,使程序能够实现更多功能,好比两个连续的(不调用delay
之类的函数的)任务同时执行。即便是大多数状况下中断能够解决的问题,RTOS的引入也能让你更快地实现相同功能,这既体如今编程思路的改进,还有现成API可供使用,提升开发效率。若是涉及到程序在平台间的移植,RTOS能提供的帮助就更多了。
RTOS是事件驱动的,尽管表面上不太看得出来。这也能带来一些收益,咱们将在本文最后一节进行分析。
然而,RTOS的运行负担较大,包括时间和空间,好比在AVR平台上,一次任务调度至少须要100多个指令周期。在应用自己不太复杂的状况下,这一点尤其严重,须要根据应用决定是否使用。我把RTOS安排到了最后一篇,显然是建议在AVR单片机开发中,尽量不要使用RTOS。
最后,RTOS对我的发展是有好处的。Linux尽管不是RTOS,做为安装量最大的操做系统内核,是嵌入式开发者必须精通的。各类RTOS与Linux同样都是操做系统,无非是调度策略不一样(Linux也有实时的),不少内容都是相通的。学习RTOS对学习Linux有很大帮助,这对你的嵌入式道路是有益无害的。
未完待续……