标签:官方 clr 没有 list 状态 false ram GNU C 语句
本文隶属于AVR单片机教程系列。
?
在系列教程的最后一篇中,我将向你推荐3个可以深造的方向:RTOS、C++、事件驱动。掌握这些技术可以帮助你更快、更好地开发更大的项目。
本文涉及到许多概念性的内容,如果你有不同意见,欢迎讨论。
这一篇教程叫作“走向高层”。什么是高层?
我认为,如果寥寥几行代码就能实现一个复杂功能,或者一行代码可以对应到几百句汇编,那么你就站在高层。高层与底层是相对的概念,没有绝对的界限。
站得高,看得远,这同样适用于编程,我们要走向高层。高层是对底层的封装,是对现实的抽象,高层相比于底层更加贴近应用。站在高层,你可以看到很多底层看不到的东西,主要有编程工具和思路。合理利用工具,可以简化代码,降低工作量;用合适的思路编程,更可以事半功倍。
但是,掌握高层并不意味着忽视甚至鄙视底层,高层建立在底层基础之上。其一,有些高层出现的诡异现象可以追溯到底层,这样的debug任务只有通晓底层与高层的开发者才能胜任;其二,为了让高层实现复杂功能的同时获得可接受的运行效率,底层必须设计地更加精致,这就对底层提出了更高的要求。
相信你经过一期和二期的教程,已经相当熟悉AVR编程的底层了。跟我一起走上高层吧!
实时操作系统(RTOS)是一类操作系统。带有操作系统的计算机系统相比不带有的,最显著的特点是支持多任务。我们之前写的程序,在监控按键的同时,开了一个定时器中断用于数码管动态扫描,两个任务同时进行,是多任务吗?不完全是。监控按键与动态扫描两个任务只有一个可以占据main
函数,另一个必须放在中断里,中断里的任务不能执行太长时间,否则就会干扰main
函数的运行。而操作系统中的任务调度器可以给每个任务分配一定的运行时间,CPU一会执行这个,一会执行那个,每个任务都好像独占了CPU连续执行一样。
RTOS与其他操作系统的主要区别在于任务调度器的设计。在RTOS中,所有任务都有优先级,优先级高的被调度器保证优先执行,以获得最短的响应时间。在与现实世界打交道的嵌入式系统中,这样的功能往往是必要的。
操作系统通常需要中档的硬件,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有很大帮助,这对你的嵌入式道路是有益无害的。
未完待续……
标签:官方 clr 没有 list 状态 false ram GNU C 语句
原文地址:https://www.cnblogs.com/jerry-fuyi/p/12482580.html