AVR单片机教程——走向高层

Posted jerry-fuyi

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AVR单片机教程——走向高层相关的知识,希望对你有一定的参考价值。

本文隶属于AVR单片机教程系列。

?

在系列教程的最后一篇中,我将向你推荐3个可以深造的方向:RTOS、C++、事件驱动。掌握这些技术可以帮助你更快、更好地开发更大的项目。

本文涉及到许多概念性的内容,如果你有不同意见,欢迎讨论。

关于高层

这一篇教程叫作“走向高层”。什么是高层?

我认为,如果寥寥几行代码就能实现一个复杂功能,或者一行代码可以对应到几百句汇编,那么你就站在高层。高层与底层是相对的概念,没有绝对的界限。

站得高,看得远,这同样适用于编程,我们要走向高层。高层是对底层的封装,是对现实的抽象,高层相比于底层更加贴近应用。站在高层,你可以看到很多底层看不到的东西,主要有编程工具和思路。合理利用工具,可以简化代码,降低工作量;用合适的思路编程,更可以事半功倍。

但是,掌握高层并不意味着忽视甚至鄙视底层,高层建立在底层基础之上。其一,有些高层出现的诡异现象可以追溯到底层,这样的debug任务只有通晓底层与高层的开发者才能胜任;其二,为了让高层实现复杂功能的同时获得可接受的运行效率,底层必须设计地更加精致,这就对底层提出了更高的要求。

相信你经过一期和二期的教程,已经相当熟悉AVR编程的底层了。跟我一起走上高层吧!

RTOS

实时操作系统(RTOS)是一类操作系统。带有操作系统的计算机系统相比不带有的,最显著的特点是支持多任务。我们之前写的程序,在监控按键的同时,开了一个定时器中断用于数码管动态扫描,两个任务同时进行,是多任务吗?不完全是。监控按键与动态扫描两个任务只有一个可以占据main函数,另一个必须放在中断里,中断里的任务不能执行太长时间,否则就会干扰main函数的运行。而操作系统中的任务调度器可以给每个任务分配一定的运行时间,CPU一会执行这个,一会执行那个,每个任务都好像独占了CPU连续执行一样。

技术图片

RTOS与其他操作系统的主要区别在于任务调度器的设计。在RTOS中,所有任务都有优先级,优先级高的被调度器保证优先执行,以获得最短的响应时间。在与现实世界打交道的嵌入式系统中,这样的功能往往是必要的。

操作系统通常需要中档的硬件,8位的AVR稍差了一点,主频和存储容量达不到一些操作系统的要求,不过还是有可选项的。我们来试着在开发板上运行FreeRTOS。FreeRTOS是一个免费的、为单片机设计的RTOS,是目前嵌入式市场占有率第二的操作系统,仅次于Linux。

首先去官网下载代码。下载的是一个.zip压缩包,找到FreeRTOS文件夹,目录下DemoSource中的部分代码是需要使用的。作为一个跨平台的系统,大多数代码平台无关,只存一份,其他平台相关的代码,每个平台都有独立的实现,源码是demo都是如此,这使得代码组织有些复杂,你可以参考官方文档

官方提供了ATmega323单片机的demo,为了在开发板上运行,需要做一些修改。demo基于WinAVR平台,它与Atmel Studio一样,都是基于avr-gcc的。如果你有WinAVR的话,直接用makefile就可以编译;Atmel Studio虽然也提供了make,但有些微区别,没法直接用makefile,因此我们自己建立项目来编译。

  1. 新建项目,然后在Solution Explorer中建3个文件夹:sourceportdemo

  2. 拷贝一些文件到这些目录下:

    • sourceSourceinclude所有文件、Source下的tasks.cqueue.clist.ccroutine.c

    • portSourceportableGCCATmega323所有文件和SourceportableMemMang`下的heap_1.c

    • demoDemoCommoninclude所有文件、DemoCommonMinimal下的crflash.cinteger.cPollQ.ccomtest.cDemoAVR_ATMega323_WinAVRmakefile以外的所有文件,再把ParTest.cserial.c拎出来,main.c拎到外面。

    我是怎么知道的呢?我参考了官方文档和makefile文件。

  3. 在Solution Explorer中Add Existing Item,在项目属性->Toolchain->AVR/GNU C Compiler->Directories中添加这三个目录。

  4. 修改代码,使之适用于我们的开发板:

    修改的理由有以下几种:

    • ATmega323和ATmega324的寄存器略有不同;

    • WinAVR和Atmel Studio提供的工具链中的一些定义方式不同;

    • 硬件配置与连接不同。

    所以需要做以下修改:

    • port.c中:TIMSK改为TIMSK1SIG_OUTPUT_COMPARE1A改为TIMER1_COMPA_vect;54行改为0x02

    • FreeRTOSConfig.h中:48行改为25000000

    • serial.c中:UDRUCSRBUCSRCUBRRLUBRRH分别改为UDR0UCSR0BUCSR0CUBRR0LUBRR0H;67行改为0x00;188行改为ISR(USART0_RX_vect);207行改为ISR(USART0_UDRE_vect)

    • comtest.c中:71行改为4;72行改为2

    • ParTest.c中:DDRB改为DDRCPORTB改为PORTC;49行改为0x00;50行改为3;72和99行把uxLED改为(4 + uxLED);76行把ifelse的大括号中的语句对调;

    • main.c中:删除81和84行;111行改为0;117行改为3;127行改为2;153行返回类型改为int

不出意外的话,现在代码可以通过编译了(我这里有3个warning)。下载到单片机上,连接TXRX,你会发现红灯和黄灯分别以300ms和400ms为周期闪烁,绿灯和串口黄灯一起闪烁,蓝灯不亮。

实际上,程序创建了1个整数计算、2个串口收发、2个队列收发、2个寄存器测试、1个错误检查和1个空闲共9个任务,以及2个LED闪烁协程。每过一毫秒,定时器产生一次中断,任务调度器暂停当前任务,换一个任务开始运行。为了理解这个过程,我们先介绍上下文这个概念。

一个任务在执行的过程中,需要一些临时变量,它们有的保存在栈上(栈是内存中的一块区域,寄存器SP指向栈顶),有的在寄存器中;此外,条件分支语句还要用到寄存器SREG中的位,这些位在之前的语句中被置位或清零;还有记录当前程序执行到哪的程序计数器。这些一起构成了任务执行的上下文:寄存器r0r31SREGSPPC。不同任务的上下文是不共享的,但它们却要占用相同的位置,为此,在切换任务时需要把前一个上下文保存起来,并恢复要切换到的任务的上下文,这个过程称为上下文切换,然后才能继续这个任务。

技术图片

我们来结合代码分析一下这个过程。

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                      
	"                       "in     r0, __SREG__            
	"                       "cli                            
	"                       "push   r0                      
	"                       "push   r1                      
	"                       "clr    r1                      
	"                       "push   r2                      
	"                       "push   r3                      
	"                       "push   r4                      
	"                       "push   r5                      
	"                       "push   r6                      
	"                       "push   r7                      
	"                       "push   r8                      
	"                       "push   r9                      
	"                       "push   r10                     
	"                       "push   r11                     
	"                       "push   r12                     
	"                       "push   r13                     
	"                       "push   r14                     
	"                       "push   r15                     
	"                       "push   r16                     
	"                       "push   r17                     
	"                       "push   r18                     
	"                       "push   r19                     
	"                       "push   r20                     
	"                       "push   r21                     
	"                       "push   r22                     
	"                       "push   r23                     
	"                       "push   r24                     
	"                       "push   r25                     
	"                       "push   r26                     
	"                       "push   r27                     
	"                       "push   r28                     
	"                       "push   r29                     
	"                       "push   r30                     
	"                       "push   r31                     
	"                       "lds    r26, pxCurrentTCB       
	"                       "lds    r27, pxCurrentTCB + 1   
	"                       "in     r0, 0x3d                
	"                       "st     x+, r0                  
	"                       "in     r0, 0x3e                
	"                       "st     x+, r0                  
	"                   );

#define portRESTORE_CONTEXT()                                   asm volatile (  "lds    r26, pxCurrentTCB       
	"                       "lds    r27, pxCurrentTCB + 1   
	"                       "ld     r28, x+                 
	"                       "out    __SP_L__, r28           
	"                       "ld     r29, x+                 
	"                       "out    __SP_H__, r29           
	"                       "pop    r31                     
	"                       "pop    r30                     
	"                       "pop    r29                     
	"                       "pop    r28                     
	"                       "pop    r27                     
	"                       "pop    r26                     
	"                       "pop    r25                     
	"                       "pop    r24                     
	"                       "pop    r23                     
	"                       "pop    r22                     
	"                       "pop    r21                     
	"                       "pop    r20                     
	"                       "pop    r19                     
	"                       "pop    r18                     
	"                       "pop    r17                     
	"                       "pop    r16                     
	"                       "pop    r15                     
	"                       "pop    r14                     
	"                       "pop    r13                     
	"                       "pop    r12                     
	"                       "pop    r11                     
	"                       "pop    r10                     
	"                       "pop    r9                      
	"                       "pop    r8                      
	"                       "pop    r7                      
	"                       "pop    r6                      
	"                       "pop    r5                      
	"                       "pop    r4                      
	"                       "pop    r3                      
	"                       "pop    r2                      
	"                       "pop    r1                      
	"                       "pop    r0                      
	"                       "out    __SREG__, r0            
	"                       "pop    r0                      
	"                   );

在定时器中断TIMER1_COMPA_vect中,vPortYieldFromTick被调用,其中依次调用portSAVE_CONTEXTxTaskIncrementTickvTaskSwitchContext(可能不调用)和portRESTORE_CONTEXT,执行汇编语句ret;最后执行reti

在介绍中断的时候,我们提到过编译器添加的额外代码,把用到的寄存器都push进栈。但是,编译器只会保护该中断用到的寄存器,而上下文包括所有寄存器,需要手动地编写代码,那么也就无需编译器添加多余的代码了。函数TIMER1_COMPA_vect被添加attributenaked,表示无需添加任何代码,把用户编写的原原本本地编进去就够了。

技术图片

进入中断时,PC被push进栈(这是硬件做的),PC内容变为TIMER1_COMPA_vect的地址,随后开始执行,PC再次push进栈(没有在图片中表示出来),开始执行portSAVE_CONTEXT保存上下文。由于它是宏,就没有PC进栈的过程。

技术图片

然后,r0SREGr1r31依次进栈,上下文的内容保存完成,其位置还需要另存。SP指向栈顶,代表着上下文的位置,它被复制到pxCurrentTCB所指的位置中。pxCurrentTCB实际上是结构体TCB_t指针,该结构体保存着当前执行的任务的信息,前两个字节保存栈指针。这样,上下文就保存完成了。

技术图片

xTaskIncrementTick把软件计数器加1,并检查是否需要任务切换。为了讲解,我们假定它需要,那么vTaskSwitchContext就会被调用,pxCurrentTCB指向另一个TCB_t变量,那里保存着另一个任务的上下文,我们要恢复它。

技术图片

恢复过程是,先用pxCurrentTCB取出SP,再按相反的顺序出栈,上下文中就只剩PC没有恢复了(retvPortYieldFromTick的调用抵消,一起忽略)。最后执行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有很大帮助,这对你的嵌入式道路是有益无害的。

C++

未完待续……

以上是关于AVR单片机教程——走向高层的主要内容,如果未能解决你的问题,请参考以下文章

AVR单片机教程——UART进阶

AVR单片机教程——矩阵键盘

AVR单片机教程——EasyElectronics Library v2.0手册

AVR单片机教程——数字输入

AVR单片机教程——串口接收

AVR单片机教程——串口发送