码迷,mamicode.com
首页 > 其他好文 > 详细

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

时间:2020-03-20 21:51:04      阅读:65      评论:0      收藏:0      [点我收藏+]

标签:官方   clr   没有   list   状态   false   ram   GNU C   语句   

本文隶属于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. 拷贝一些文件到这些目录下:

    • source\Source\include\所有文件、\Source\下的tasks.cqueue.clist.ccroutine.c

    • port\Source\portable\GCC\ATmega323\所有文件和\Source\portable\MemMang`下的heap_1.c

    • demo\Demo\Common\include\所有文件、\Demo\Common\Minimal\下的crflash.cinteger.cPollQ.ccomtest.c\Demo\AVR_ATMega323_WinAVR\makefile以外的所有文件,再把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                      \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_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单片机教程——走向高层

标签:官方   clr   没有   list   状态   false   ram   GNU C   语句   

原文地址:https://www.cnblogs.com/jerry-fuyi/p/12482580.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!