AVR单片机教程——走向高层
jerry_fuyi 人气:0
本文隶属于AVR单片机教程系列。
在系列教程的最后一篇中,我将向你推荐3个可以深造的方向:RTOS、C++、事件驱动。掌握这些技术可以帮助你更快、更好地开发更大的项目。
本文涉及到许多概念性的内容,如果你有不同意见,欢迎讨论。
### 关于高层
这一篇教程叫作“走向高层”。什么是高层?
我认为,如果寥寥几行代码就能实现一个复杂功能,或者一行代码可以对应到几百句汇编,那么你就站在高层。高层与底层是相对的概念,没有绝对的界限。
站得高,看得远,这同样适用于编程,我们要走向高层。高层是对底层的封装,是对现实的抽象,高层相比于底层更加贴近应用。站在高层,你可以看到很多底层看不到的东西,主要有编程工具和思路。合理利用工具,可以简化代码,降低工作量;用合适的思路编程,更可以事半功倍。
但是,掌握高层并不意味着忽视甚至鄙视底层,高层建立在底层基础之上。其一,有些高层出现的诡异现象可以追溯到底层,这样的debug任务只有通晓底层与高层的开发者才能胜任;其二,为了让高层实现复杂功能的同时获得可接受的运行效率,底层必须设计地更加精致,这就对底层提出了更高的要求。
相信你经过一期和二期的教程,已经相当熟悉AVR编程的底层了。跟我一起走上高层吧!
### RTOS
实时操作系统(RTOS)是一类操作系统。带有操作系统的计算机系统相比不带有的,最显著的特点是支持多任务。我们之前写的程序,在监控按键的同时,开了一个定时器中断用于数码管动态扫描,两个任务同时进行,是多任务吗?不完全是。监控按键与动态扫描两个任务只有一个可以占据`main`函数,另一个必须放在中断里,中断里的任务不能执行太长时间,否则就会干扰`main`函数的运行。而操作系统中的任务调度器可以给每个任务分配一定的运行时间,CPU一会执行这个,一会执行那个,每个任务都好像独占了CPU连续执行一样。
![](https://img2020.cnblogs.com/blog/1734552/202003/1734552-20200314232344729-1683014067.png)
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`,因此我们自己建立项目来编译。
1. 新建项目,然后在Solution Explorer中建3个文件夹:`source`、`port`和`demo`。
2. 拷贝一些文件到这些目录下:
- `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`文件。
3. 在Solution Explorer中Add Existing Item,在项目属性->Toolchain->AVR/GNU C Compiler->Directories中添加这三个目录。
4. 修改代码,使之适用于我们的开发板:
修改的理由有以下几种:
- 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`。不同任务的上下文是不共享的,但它们却要占用相同的位置,为此,在切换任务时需要把前一个上下文保存起来,并恢复要切换到的任务的上下文,这个过程称为上下文切换,然后才能继续这个任务。
![](https://img2020.cnblogs.com/blog/1734552/202003/1734552-20200314232359446-144587912.png)
我们来结合代码分析一下这个过程。
```
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`被添加attribute`naked`,表示无需添加任何代码,把用户编写的原原本本地编进去就够了。
![](https://img2020.cnblogs.com/blog/1734552/202003/1734552-20200314232406284-1631079568.png)
进入中断时,`PC`被push进栈(这是硬件做的),`PC`内容变为`TIMER1_COMPA_vect`的地址,随后开始执行,`PC`再次push进栈(没有在图片中表示出来),开始执行`portSAVE_CONTEXT`保存上下文。由于它是宏,就没有`PC`进栈的过程。
![](https://img2020.cnblogs.com/blog/1734552/202003/1734552-20200314232415684-1901545555.png)
然后,`r0`、`SREG`、`r1`到`r31`依次进栈,上下文的内容保存完成,其位置还需要另存。`SP`指向栈顶,代表着上下文的位置,它被复制到`pxCurrentTCB`所指的位置中。`pxCurrentTCB`实际上是结构体`TCB_t`指针,该结构体保存着当前执行的任务的信息,前两个字节保存栈指针。这样,上下文就保存完成了。
![](https://img2020.cnblogs.com/blog/1734552/202003/1734552-20200314232422754-285707224.png)
`xTaskIncrementTick`把软件计数器加1,并检查是否需要任务切换。为了讲解,我们假定它需要,那么`vTaskSwitchContext`就会被调用,`pxCurrentTCB`指向另一个`TCB_t`变量,那里保存着另一个任务的上下文,我们要恢复它。
![](https://img2020.cnblogs.com/blog/1734552/202003/1734552-20200314232428084-1864117395.png)
恢复过程是,先用`pxCurrentTCB`取出`SP`,再按相反的顺序出栈,上下文中就只剩`PC`没有恢复了(`ret`和`vPortYieldFromTick`的调用抵消,一起忽略)。最后执行`reti`,该汇编语句从栈顶取两个字节放进`PC`,并跳转到其位置继续执行。此时,`PC`的内容就是该任务之前被中断时执行到的位置,现在从`PC`开始继续执行,也就是继续执行该任务。上下文切换完成。
在对FreeRTOS稍有了解后,我们动手写一个基于FreeRTOS的程序。在学习数码管的时候,你很可能考虑过,在后台创建一个任务,执行数码管的扫描。现在,FreeRTOS给了你这个机会。我们创建两个任务,一个每一毫秒显示数码管的一位,另一个每200毫秒更新显示的数字。
```
#include
加载全部内容