普中51-单核-A2
STC89C52
Keil uVision V5.29.0.0
PK51 Prof.Developers Kit Version:9.60.0.0
问题
在Keil C51中直接使用递归会报如下警告:
recursive call to non-reentrant function
为了提高运行效率,C51采用静态分配局部变量的方式,所以一般情况下不可递归。
被中断和非中断函数调用的函数,如果在非中断状态运行,发生中断后,局部变量被破坏,中断结束后再执行就完全错误了,这个跟不能递归的原理是一样的。
如对数组arr[10] = { 7,1,3,9,2,6,1,7,3,5 }进行快速排序
#include <REGX52.H>
#include "intrins.h"
#include "stdint.h"
#include "UART.h"
void Delay500ms() //@22.1184MHz
{
unsigned char i, j, k;
_nop_();
i = 8;
j = 1;
k = 243;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
typedef int QuickSortType;
void QuickSort(QuickSortType v[], int left, int right)
{
int i, pos;
QuickSortType temp;
if (left >= right)
return;
//取第一个元素为锚定点
pos = left;
//遍历数组,将小于锚定点的数swap到数组前部
for (i = left + 1; i <= right; ++i)
{
if (v[i] < v[left])
{
++pos;
temp = v[i];
v[i] = v[pos];
v[pos] = temp;
}
}
//将锚定点的数与swap至其正确位置pos;
temp = v[left];
v[left] = v[pos];
v[pos] = temp;
//递归该过程
QuickSort(v, left, pos - 1);
QuickSort(v, pos + 1, right);
}
void main(void)
{
int i;
int arr[10] = { 7,1,3,9,2,6,1,7,3,5 };
UART_Init(57600);
QuickSort(arr, 0, 9);
while(1)
{
Delay500ms();
for(i = 0; i < 10; ++i)
printf("%d ", arr[i]);
printf("\r\n");
}
}
得到的数据可能会出错
应对措施
加上reentrant关键字后,输出结果正确。
typedef int QuickSortType;
void QuickSort(QuickSortType v[], int left, int right) reentrant
{
int i, pos;
QuickSortType temp;
if (left >= right)
return;
//取第一个元素为锚定点
pos = left;
//遍历数组,将小于锚定点的数swap到数组前部
for (i = left + 1; i <= right; ++i)
{
if (v[i] < v[left])
{
++pos;
temp = v[i];
v[i] = v[pos];
v[pos] = temp;
}
}
//将锚定点的数与swap至其正确位置pos;
temp = v[left];
v[left] = v[pos];
v[pos] = temp;
//递归该过程
QuickSort(v, left, pos - 1);
QuickSort(v, pos + 1, right);
}
但reentrant后也不能随意使用递归:
由于51稀缺的资源,当数组长度达到46后也出现了错误,因此在51上尽量不要使用递归
原理
本节摘自Keil C51对C语言的关键词扩展之十五: reentrant —— 昵称90天可改
这篇博文写的很详细模拟堆栈,可重入函数调用,参数传递 —— hplog
reentrant声明的函数为可重入函数。可重入的函数能够被多个进程同时调用。可重入函数在执行时,另外的进程可以中断当前执行的函数,并且调用同一个函数。正常情况下,C51程序中的函数不能被递归地调用,这是由于函数的参数和局部变量都被保存在固定的地址,在递归调用时操作了相同存储位置,导致数据被覆盖。
使用reentrant声明函数为可递归调用的可重入函数:
int calc (char i, int b) reentrant
{
int x;
x = table [i];
return (x * b);
}
可重入函数,能够被递归调用,也能被两个以上的进程同时调用。可重入函数通常在实时应用或者中断与非中断程序共享相同函数这两种情况下被使用。
每个可重入函数都有一个位于内部ram或外部ram的模拟堆栈:
-
SMALL内存模型下,可重入函数模拟堆栈位于idata区;
-
COMPACT内存模型下,可重入函数模拟堆栈位于pdata区;
-
LARGE内存模型下,可重入函数模拟堆栈位于xdata区;
使用reentrant声明可重入函数须遵循的规则:
-
可重入函数不支持位寻址变量,比如bit类型的参数;
-
可重入函数不能被alien函数调用;
-
可重入函数不能被声明为alien属性(alien用于使能PL/M-51参数传递约定);
-
可重入函数可以同时拥有其他属性,比如using、interrupt、small、compact、large;
-
返回地址被保存在硬件堆栈中;
-
使用不同存储模型的可重入函数能够混合,但是各自函数声明时必须指定存储模型;
-
三种内存模型的可重入函数都有自己的对战区和栈指针。比如在相同模块中,定义了small和large类型的可重入函数,则small和large类型的堆栈及其堆栈指针都被创建;
可重入堆栈模拟体系,效率比较低下,但是由于8051自身缺乏适当寻址方法的硬件特性,所以推出这种堆栈模拟体系来满足我们的可重入需求,在应用中 ,我们应该尽量不用或少用可重入函数。
可重入函数使用的模拟堆栈拥有独立于8051硬件堆栈的栈指针。堆栈和堆栈指针在STARTUP.A51文件中被定义和初始化。
8051硬件堆栈向上增长,堆栈指针先加再压栈,可重入模拟堆栈正好相反,其指针先减再压栈。
STARTUP.A51启动代码声明并初始化了模拟堆栈及其堆栈指针,如果使用可 重入函数就必须修改启动代码指出哪个模拟堆栈须要初始化。可以在启动代码中修改模拟堆栈的起始地址。
可重入函数的参数传递,通过模拟堆栈的压栈、出栈完成。
可重入函数的局部变量,也保存在模拟堆栈中,通过模拟堆栈指针访问。