单片机

从数据存储类型来说,8051系列有片内、片外程序存储器,片内、片外数据存储器,片内程序存储器还分直接寻址区和间接寻址类型,分别对应code、data、xdata、idata以及根据51系列特点而设定的pdata类型。

使用不同的存储器,将使程序执行效率不同,在编写C51程序时,最好指定变量的存储类型,这样将有利于提高程序执行效率(此问题将在后面专门讲述)。与ANSI-C稍有不同,它只分SAMLL、COMPACT、LARGE模式,各种不同的模式对应不同的实际硬件系统,也将有不同的编译结果。

在51系列中data,idata,xdata,pdata的区别:

data:固定指前面0x00-0x7f的128个RAM,可以用acc直接读写的,速度最快,生成的代码也最小。

idata:固定指前面0x00-0xff的256个RAM,其中前128和data的128完全相同,只是因为访问的方式不同。idata是用类似C中的指针方式访问的。汇编中的语句为:mox ACC,@Rx.(不重要的补充:c中idata做指针式的访问效果很好)

xdata:外部扩展RAM,一般指外部0x0000-0xffff空间,用DPTR访问。

pdata:外部扩展RAM的低256个字节,地址出现在A0-A7的上时读写,用movx ACC,@Rx读写。这个比较特殊,而且C51好象有对此BUG,建议少用。但也有他的优点,具体用法属于中级问题,这里不提。

单片机C语言unsigned char code table[] code是什么作用?

code的作用是告诉单片机,我定义的数据要放在ROM(程序存储区)里面,写入后就不能再更改,其实是相当与汇编里面的寻址MOVX(好像是),因为C语言中没办法详细描述存入的是ROM还是RAM(寄存器),所以在软件中添加了这一个语句起到代替汇编指令的作用,对应的还有data是存入RAM的意思。

程序可以简单的分为code(程序)区,和data(数据)区,code区在运行的时候是不可以更改的,data区放全局变量和临时变量,是要不断的改变的,cpu从code区读取指令,对data区的数据进行运算处理,因此code区存储在什么介质上并不重要。

像以前的计算机程序存储在卡片上,code区也可以放在rom里面,也可以放在ram里面,也可以放在flash里面(但是运行速度要慢很多,主要读flash比读ram要费时间),因此一般的做法是要将程序放到flash里面,然后load到ram里面运行的;DATA区就没有什么选择了,肯定要放在RAM里面,放到rom里面改动不了。

bdata如何使用它呢?

若程序需要8个或者更多的bit变量,如果你想一次性给8个变量赋值的话就不方便了,(举个例子说说它的方便之处,想更深入的了解请在应用中自己琢磨)又不可以定义bit数组,只有一个方法

char bdata MODE;

sbit MODE_7 = MODE^7;

sbit MODE_6 = MODE^6;

sbit MODE_5 = MODE^5;

sbit MODE_4 = MODE^4;

sbit MODE_3 = MODE^3;

sbit MODE_2 = MODE^2;

sbit MODE_1 = MODE^1;

sbit MODE_0 = MODE^0;

8个bit变量MODE_n就定义好了

这是定义语句,Keilc的特殊数据类型。记住一定要是sbit

不能bit MODE_0 = MODE^0;

赋值语句要是这么写C语言就视为异或运算。

keil生成的文件:

.plg:编译器编译结果

.hex和.bin:可执行文件

.map和.lst:链接文件

.o:目标文件

.crf、.lnp、.d和.axf:调试文件

.opt:保存工程配置信息

.bak:工程备份文件

M51文件,startup文件。



来源:网络,转载此文目的在于传递更多信息,版权归原作者所有。

围观 6

算法(Algorithm):计算机解题的基本思想方法和步骤。

算法的描述:是对要解决一个问题或要完成一项任务所采取的方法和步骤的描述,包括需要什么数据(输入什么数据、输出什么结果)、采用什么结构、使用什么语句以及如何安排这些语句等。通常使用自然语言、结构化流程图、伪代码等来描述算法。

而单片机会用到什么常用的C语言?

一、计数、求和、求阶乘等简单算法

此类问题都要使用循环,要注意根据问题确定循环变量的初值、终值或结束条件,更要注意用来表示计数、和、阶乘的变量的初值。

例:用随机函数产生100个[0,99]范围内的随机整数,统计个位上的数字分别为1,2,3,4,5,6,7,8,9,0的数的个数并打印出来。

本题使用数组来处理,用数组a[100]存放产生的确100个随机整数,数组x[10]来存放个位上的数字分别为1,2,3,4,5,6,7,8,9,0的数的个数。即个位是1的个数存放在x[1]中,个位是2的个数存放在x[2]中,……个位是0的个数存放在x[10]。

void main()
{
int a[101],x[11],i,p;
for(i=0;i<=11;i++)
x=0;
for(i=1;i<=100;i++)
{
a=rand() % 100;
printf("%4d",a);
if(i%10==0)printf("\n");
}
for(i=1;i<=100;i++)
{
p="a"%10;
if(p==0) p="10";
x[p]=x[p]+1;
}
for(i=1;i<=10;i++)
{
p="i";
if(i==10) p="0";
printf("%d,%d\n",p,x);
}
printf("\n");
}

二、求两个整数的最大公约数、最小公倍数

分析:求最大公约数的算法思想:(最小公倍数=两个整数之积/最大公约数)
(1) 对于已知两数m,n,使得m>n;
(2) m除以n得余数r;
(3) 若r=0,则n为求得的最大公约数,算法结束;否则执行(4);
(4) m←n,n←r,再重复执行(2)。

例如: 求 m="14" ,n=6 的最大公约数. m n r
14 6 2
6 2 0
void main()
{ int nm,r,n,m,t;
printf("please input two numbers:\n");
scanf("%d,%d",&m,&n);
nm=n*m;
if (m
{ t="n"; n="m"; m="t"; }
r=m%n;
while (r!=0)
{ m="n"; n="r"; r="m"%n; }
printf("最大公约数:%d\n",n);
printf("最小公倍数:%d\n",nm/n);
}

三、判断素数

只能被1或本身整除的数称为素数 基本思想:把m作为被除数,将2—INT( )作为除数,如果都除不尽,m就是素数,否则就不是。(可用以下程序段实现)
void main()
{ int m,i,k;
printf("please input a number:\n");
scanf("%d",&m);
k=sqrt(m);
for(i=2;i
if(m%i==0) break;
if(i>=k)
printf("该数是素数");
else
printf("该数不是素数");
}
将其写成一函数,若为素数返回1,不是则返回0
int prime( m%)
{int i,k;
k=sqrt(m);
for(i=2;i
if(m%i==0) return 0;
return 1;
}

四、验证哥德巴赫猜想

(任意一个大于等于6的偶数都可以分解为两个素数之和)

基本思想:n为大于等于6的任一偶数,可分解为n1和n2两个数,分别检查n1和n2是否为素数,如都是,则为一组解。如n1不是素数,就不必再检查n2是否素数。先从n1=3开始,检验n1和n2(n2=N-n1)是否素数。然后使n1+2 再检验n1、n2是否素数,… 直到n1=n/2为止。

利用上面的prime函数,验证哥德巴赫猜想的程序代码如下:

#include "math.h"
int prime(int m)
{ int i,k;
k=sqrt(m);
for(i=2;i
if(m%i==0) break;
if(i>=k)
return 1;
else
return 0;
}
main()
{ int x,i;
printf("please input a even number(>=6):\n");
scanf("%d",&x);
if (x<6||x%2!=0)
printf("data error!\n");
else
for(i=2;i<=x/2;i++)
if (prime(i)&&prime(x-i))
{
printf("%d+%d\n",i,x-i);
printf("验证成功!");
break;
}
}

五、排序问题

1.选择法排序(升序)

基本思想:
1)对有n个数的序列(存放在数组a(n)中),从中选出最小的数,与第1个数交换位置;
2)除第1 个数外,其余n-1个数中选最小的数,与第2个数交换位置;
3)依次类推,选择了n-1次后,这个数列已按升序排列。

程序代码如下:

void main()
{ int i,j,imin,s,a[10];
printf("\n input 10 numbers:\n");
for(i=0;i<10;i++)
scanf("%d",&a);
for(i=0;i<9;i++)
{ imin="i";
for(j=i+1;j<10;j++)
if(a[imin]>a[j]) imin="j";
if(i!=imin)
{s=a; a=a[imin]; a[imin]=s; }
printf("%d\n",a);
}
}

2.冒泡法排序(升序)

基本思想:(将相邻两个数比较,小的调到前头)
1)有n个数(存放在数组a(n)中),第一趟将每相邻两个数比较,小的调到前头,经n-1次两两相邻比较后,最大的数已“沉底”,放在最后一个位置,小数上升“浮起”;
2)第二趟对余下的n-1个数(最大的数已“沉底”)按上法比较,经n-2次两两相邻比较后得次大的数;
3)依次类推,n个数共进行n-1趟比较,在第j趟中要进行n-j次两两比较。

程序段如下

void main()
{ int a[10];
int i,j,t;
printf("input 10 numbers\n");
for(i=0;i<10;i++)
scanf("%d",&a);
printf("\n");
for(j=0;j<=8;j++)
for(i=0;i<9-j;i++)
if(a>a[i+1])
{t=a;a=a[i+1];a[i+1]=t;}
printf("the sorted numbers:\n");
for(i=0;i<10;i++)
printf("%d\n",a);
}

3.合并法排序(将两个有序数组A、B合并成另一个有序的数组C,升序)

基本思想:
1)先在A、B数组中各取第一个元素进行比较,将小的元素放入C数组;
2)取小的元素所在数组的下一个元素与另一数组中上次比较后较大的元素比较,重复上述比较过程,直到某个数组被先排完;
3)将另一个数组剩余元素抄入C数组,合并排序完成。

程序段如下:

void main()
{ int a[10],b[10],c[20],i,ia,ib,ic;
printf("please input the first array:\n");
for(i=0;i<10;i++)
scanf("%d",&a);
for(i=0;i<10;i++)
scanf("%d",&b);
printf("\n");
ia=0;ib=0;ic=0;
while(ia<10&&ib<10)
{ if(a[ia]
{ c[ic]=a[ia];ia++;}
else
{ c[ic]=b[ib];ib++;}
ic++;
}
while(ia<=9)
{ c[ic]=a[ia];
ia++;ic++;
}
while(ib<=9)
{ c[ic]=b[ib];
b++;ic++;
}
for(i=0;i<20;i++)
printf("%d\n",c);
}

六、查找问题

顺序查找法(在一列数中查找某数x)

基本思想:一列数放在数组a[1]---a[n]中,待查找的数放在x 中,把x与a数组中的元素从头到尾一一进行比较查找。用变量p表示a数组元素下标,p初值为1,使x与a[p]比较,如果x不等于a[p],则使p=p+1,不断重复这个过程;一旦x等于a[p]则退出循环;另外,如果p大于数组长度,循环也应该停止。(这个过程可由下语句实现)

void main()
{ int a[10],p,x,i;
printf("please input the array:\n");
for(i=0;i<10;i++)
scanf("%d",&a);
printf("please input the number you want find:\n");
scanf("%d",&x);
printf("\n");
p=0;
while(x!=a[p]&&p<10)
p++;
if(p>=10)
printf("the number is not found!\n");
else
printf("the number is found the no%d!\n",p);
}

思考:将上面程序改写一查找函数Find,若找到则返回下标值,找不到返回-1

②基本思想:一列数放在数组a[1]---a[n]中,待查找的关键值为key,把key与a数组中的元素从头到尾一一进行比较查找,若相同,查找成功,若找不到,则查找失败。(查找子过程如下。index:存放找到元素的下标。)

void main()
{ int a[10],index,x,i;
printf("please input the array:\n");
for(i=0;i<10;i++)
scanf("%d",&a);
printf("please input the number you want find:\n");
scanf("%d",&x);
printf("\n");
index=-1;
for(i=0;i<10;i++)
if(x==a)
{ index="i"; break;
}
if(index==-1)
printf("the number is not found!\n");
else
printf("the number is found the no%d!\n",index);
}

七、二分法

在一个数组中,知道一个数值,想确定他在数组中的位置下标,如数组:A[5] = {1,2,6,7,9};我知道其中的值为6,那么他的下标位置就是3。

int Dichotomy(int *ucData, int long, int num)
{
int iDataLow = 0 ;
int iDataHigh = num - 1;
int iDataMIDDLE;
while (iDataLow <= iDataHigh)
{
iDataMIDDLE = (iDataHigh + iDataLow)/2;
i f (ucData[iDataMIDDLE] > long)
{
iDataHigh = iDataMIDDLE - 1 ;
}
else if (ucData[iDataMIDDLE] < long)
{
iDataLow = iDataMIDDLE + 1 ;
} else{
return iDataMIDDLE ;
}
}
}

八、限幅滤波法

对于随机干扰 , 限幅滤波是一种有效的方法;

基本方法:比较相邻n 和 n - 1时刻的两个采样值y(n)和 y(n – 1),根据经验确定两次采样允许的最大偏差。如果两次采样值的差值超过最大偏差范围 ,认为发生可随机干扰 ,并认为后一次采样值y(n)为非法值 ,应予删除 ,删除y(n)后 ,可用y(n – 1) 代替y(n);若未超过所允许的最大偏差范围 ,则认为本次采样值有效。

下面是限幅滤波程序:( A 值可根据实际情况调整,value 为有效值 ,new_value 为当前采样值滤波程序返回有效的实际值 )

#define A 10
char value;
char filter()
{ char new_value;
new_value = get_ad();
if ( ( new_value - value > A ) || ( value - new_value > A )) return value;
return new_value;
}

九、中位值滤波法

中位值滤波法能有效克服偶然因素引起的波动或采样不稳定引起的误码等脉冲干扰;

对温度 液位等缓慢变化的被测参数用此法能收到良好的滤波效果 ,但是对于流量压力等快速变化的参数一般不宜采用中位值滤波法;

基本方法:对某一被测参数连续采样 n次(一般 n 取奇数) ,然后再把采样值按大小排列 ,取中间值为本次采样值。

下面是中位值滤波程序:

#define N 11
char filter()
{ char value_buf[N], count,i,j,temp;
for ( count=0;count
{ value_buf[count] = get_ad(); delay(); }
for (j=0;j
{ for (i=0;i
{ if ( value_buf>value_buf[i+1] )
{temp = value_buf; value_buf = value_buf[i+1]; value_buf[i+1] = temp; }
}
}
return value_buf[(N-1)/2];
}

十、算术平均滤波法

算术平均滤波法适用于对一般的具有随机干扰的信号进行滤波。这种信号的特点是信号本身在某一数值范围附近上下波动 ,如测量流量、 液位;

基本方法:按输入的N 个采样数据,寻找这样一个 Y ,使得 Y 与各个采样值之间的偏差的平方和最小。

编写算术平均滤波法程序时严格注意:

1. 为了加快数据测量的速度,可采用先测量数据 存放在存储器中 ,测完 N 点后 ,再对 N 个数据进行平均值计算;

2. 选取适当的数据格式 ,也就是说采用定点数还是采用浮点数。其程序如下所示:

#define N 12
char filter()
{int sum = 0,count;
for ( count=0;count
{ sum+=get_ad(); delay();}
return (char)(sum/N);
}

十一、递推平均滤波法

基本方法:采用队列作为测量数据存储器 , 设队列的长度为 N ,每进行一次测量,把测量结果放于队尾 ,而扔掉原来队首的一个数据,这样在队列中始终就有 N 个 “最新” 的数据。当计算平均值时,只要把队列中的 N 个数据进行算数平均,就可得到新的算数平均值。这样每进行一次测量,就可得到一个新的算术平均值。

#define N 12
char value_buf[N],i=0;
char filter()
{ char count; int sum=0;
value_buf[i++] = get_ad();
if ( i == N ) i = 0;
for ( count=0;count
sum = value_buf[count];
return (char)(sum/N);
}

十二、一阶滞后滤波法

优点:对周期性干扰具有良好的抑制作用,适用于波动频率较高的场合;

缺点:相位滞后,灵敏度低.滞后程度取决于a值大小。不能消除滤波频率高于采样频率的1/2的干扰信号。

程序如下:

#define a 50
char value;
char filter()
{ char new_value;
new_value = get_ad();
return (100-a)*value + a*new_value;
}

十三、PID控制算法

在过程控制中,按偏差的比例(P)、积分(I)和微分(D)进行控制的PID控制器(亦称PID调节器)是应用最为广泛的一种自动控制器;
对于过程控制的典型对象──“一阶滞后+纯滞后”与“二阶滞后+纯滞后”的控制对象,PID控制器是一种最优控制;
PID调节规律是连续系统动态品质校正的一种有效方法,它的参数整定方式简便,结构改变灵活(PI、PD、…)。

一 模拟PID调节器

PID调节器各校正环节的作用:

比例环节:即时成比例地反应控制系统的偏差信号e(t),偏差一旦产生,调节器立即产生控制作用以减小偏差;

积分环节:主要用于消除静差,提高系统的无差度。积分时间常数TI越大,积分作用越弱,反之则越强;

微分环节:能反应偏差信号的变化趋势(变化速率),并能在偏差信号的值变得太大之前,在系统中引入一个有效的早期修正信号,从而加快系统的动作速度,减小调节时间。

PID调节器是一种线性调节器,它将给定值r(t)与实际输出值c(t)的偏差的比例(P)、积分(I)、微分(D)通过线性组合构成控制量,对控制对象进行控制。

程序片段如下:

#include
#include
typedef struct PID {
double SetPoint; // 设定目标Desired value
double Proportion; // 比例常数Proportional Const
double Integral; // 积分常数Integral Const
double Derivative; // 微分常数Derivative Const
double LastError; // Error[-1]
double PrevError; // Error[-2]
double SumError; // Sums of Errors
} PID;

主程序:

double sensor (void)
{
return 100.0; }
void actuator(double rDelta)
{}
void main(void)
{
PID sPID;
double rOut;
double rIn;
PIDInit ( &sPID );
sPID.Proportion = 0.5;
sPID.Derivative = 0.0;
sPID.SetPoint = 100.0;
for (;;) {
rIn = sensor ();
rOut = PIDCalc ( &sPID,rIn );
actuator ( rOut );
}
}

十四、开根号算法

单片机开平方的快速算法

因为工作的需要,要在单片机上实现开根号的操作。目前开平方的方法大部分是用牛顿迭代法。我在查了一些资料以后找到了一个比牛顿迭代法更加快速的方法。不敢独享,介绍给大家,希望会有些帮助。

1. 原理

因为排版的原因,用pow(X,Y)表示X的Y次幂,用B[0],B[1],...,B[m-1]表示一个序列,其中[x]为下标。

假设:
B[x],b[x]都是二进制序列,取值0或1。

M = B[m-1]*pow(2,m-1) + B[m-2]*pow(2,m-2) + ... + B[1]*pow(2,1) + B[0]*pow(2,0)
N = b[n-1]*pow(2,n-1) + b[n-2]*pow(2,n-2) + ... + b[1]*pow(2,1) + n[0]*pow(2,0)
pow(N,2) = M

(1) N的最高位b[n-1]可以根据M的最高位B[m-1]直接求得。
设 m 已知,因为 pow(2, m-1) <= M <= pow(2, m),所以 pow(2, (m-1)/2) <= N <= pow(2, m/2)
如果 m 是奇数,设m=2*k+1,
那么 pow(2,k) <= N < pow(2, 1/2+k) < pow(2, k+1),
n-1=k, n=k+1=(m+1)/2
如果 m 是偶数,设m=2k,
那么 pow(2,k) > N >= pow(2, k-1/2) > pow(2, k-1),
n-1=k-1,n=k=m/2
所以b[n-1]完全由B[m-1]决定。
余数 M[1] = M - b[n-1]*pow(2, 2*n-2)

(2) N的次高位b[n-2]可以采用试探法来确定。
因为b[n-1]=1,假设b[n-2]=1,则 pow(b[n-1]*pow(2,n-1) + b[n-1]*pow(2,n-2), 2) = b[n-1]*pow(2,2*n-2) + (b[n-1]*pow(2,2*n-2) + b[n-2]*pow(2,2*n-4)),
然后比较余数M[1]是否大于等于 (pow(2,2)*b[n-1] + b[n-2]) * pow(2,2*n-4)。这种比较只须根据B[m-1]、B[m-2]、...、B[2*n-4]便可做出判断,其余低位不做比较。
若 M[1] >= (pow(2,2)*b[n-1] + b[n-2]) * pow(2,2*n-4), 则假设有效,b[n-2] = 1;
余数 M[2] = M[1] - pow(pow(2,n-1)*b[n-1] + pow(2,n-2)*b[n-2], 2) = M[1] - (pow(2,2)+1)*pow(2,2*n-4);
若 M[1] < (pow(2,2)*b[n-1] + b[n-2]) * pow(2,2*n-4), 则假设无效,b[n-2] = 0;余数 M[2] = M[1]。

(3) 同理,可以从高位到低位逐位求出M的平方根N的各位。
使用这种算法计算32位数的平方根时最多只须比较16次,而且每次比较时不必把M的各位逐一比较,尤其是开始时比较的位数很少,所以消耗的时间远低于牛顿迭代法。

3. 实现代码

这里给出实现32位无符号整数开方得到16位无符号整数的C语言代码。

/****************************************/
/*Function: 开根号处理 */
/*入口参数:被开方数,长整型 */
/*出口参数:开方结果,整型 */
/****************************************/
unsigned int sqrt_16(unsigned long M)
{
unsigned int N, i;
unsigned long tmp, ttp; // 结果、循环计数
if (M == 0) // 被开方数,开方结果也为0
return 0;
N = 0;
tmp = (M >> 30); // 获取最高位:B[m-1]
M <<= 2;
if (tmp > 1) // 最高位为1
{
N ++; // 结果当前位为1,否则为默认的0
tmp -= N;
}
for (i=15; i>0; i--) // 求剩余的15位
{
N <<= 1; // 左移一位
tmp <<= 2;
tmp += (M >> 30); // 假设
ttp = N;
ttp = (ttp<<1)+1;
M <<= 2;
if (tmp >= ttp) // 假设成立
{
tmp -= ttp;
N ++;
}
}
return N;
}

本文内容来源于网络,文章转载只为学术传播,如涉及侵权请联系删除。

围观 25

01:前言

全局变量简直就是嵌入式系统的戈兰高地。冲突最激烈的双方是:1. 做控制的工程师, 2. 做非嵌入式的软件工程师。

02:做控制的工程师特点

他们普遍的理解就是“变量都写成全局该有多方便”。我之前面试过一个非常有名的做控制实验室里出来的PhD/Master,前前后后陆续有快十个人。面试问题是用C写PID。到后面的几位面试的时候我都觉得没有看的意义了,因为全都写的是同一个风格。大概就是这样的:

float SetSpeed;
float err;
float err_last;
float Kp,Ki,Kd;
float integral;
float result;

float PID(float speed)
{
    err=SetSpeed-speed;
    integral+=err;
    result=Kp*err+Ki*integral+Kd*(err-err_last);
    err_last=err;
    return result;
}

代码的特点就是所有的变量一定定义在函数外面。问他们为什么,回答是“全局变量方便调试”。

事实上在学校里做搞自动控制的人最重要的根本就是控制的结果,而不是代码本身。代码只要能工作就行。变量名污染,低耦合之类的和他们就不在同一个世界。进了公司有些人代码质量会变好,但有的还是会延续之前的习惯。前公司代码库里面凡是看不懂的代码一律都是那一两个Control Engineer写的,写完了还会用自己的名字给函数命名的那种。

要成为一个资深的嵌入式工程师相当难,一方面要有非常扎实的理论知识,同时也要有相当的那种大型的、高频CPU、多层PCB板的设计经验。嵌入式硬件工程师要学的课程主要有模拟电路设计、数字电路设计、电磁波理论等。熟悉常用的放大电路、滤波电路、电源电路设计和分析。

03:做非嵌入式的软件工程师特点

代码的特点就是所有的静态变量都不可以定义在.h文件里,必须写在.c文件里以确保别的文件没法访问它们。

别的文件真要访问怎么办?那就给每一个变量写get/set函数啊!问题是静态变量写在.c文件里编译器是没法优化get/set的。结果就大面积的变量访问要花几倍的CPU时间去做get/set的函数调用。嵌入式项目很多情况下对硬件的压榨是很极端的,CPU利用率90%都不算什么,顶到97%都是有的。(注意下这些项目是实时性要求很高的,晚一个毫秒算不完都不行。不是跑在电脑上鼠标卡一卡也无所谓的。)然后为了封装性,在代码里面塞这么多get/set吗?

04:总结

总的来说嵌入式软件里大部分的代码都是中断驱动的,天生就有很多变量是没法使用参数传递的。全局变量的存在是因为正义站在这边。但是嵌入式软件远远没有特殊到不需要按照正常软件工程方法去管理的地步。要真有人认为“嵌入式软件只要能工作就成,代码丑一点无所谓的”纯粹是软件工程水平不行,不是因为控制水平太高。

全局变量一定是要用的,管理它们也很重要。一些基本的代码规则:

如果只是文件内调用,全局变量只能写在这个.c文件里,不要写进.h文件。

如果有文件外调用,全局变量要写在.h文件里。

.h里面的全局变量全局可读,但是只有本文件组可以写。别的文件要写请调用set函数。

所有的全局变量无论在.h还是.c里面都要包成同名struct。哪怕只有一个变量也要写进struct里面。比如PID.c里面有一个pid_S,PID.h里面有个PID_S。这样其他人不仅可以立即识别出一个变量是project内global/文件内static/函数内local,同时还能轻松追溯到这个函数是属于哪个文件的。

不要写函数内的static变量。函数内的static变量在实际的项目中几乎就是bug生成器,没法简单的reset。而且对unit test非常不友好。

本文系网络转载,转载此文目的在于传递更多信息,版权归原作者所有。如有不妥请联系删除。

围观 11

负电压的产生电路图原理

在电子电路中我们常常需要使用负电压,比如说我们在使用运放的时候常常需要建立一个负电压。下面就简单的以正5V电压到负电压5V为例说一下它的电路。

通常需要使用负电压时一般会选择使用专用的负压产生芯片,但这些芯片都比较贵,比如ICL7600,LT1054等。差点忘了MC34063了,这个芯片使用的最多了,关于34063的负压产生电路这里不说了,在datasheet中有的。下面请看我们在单片机电子电路中常用的两种负电压产生电路。

单片机中常用的负电压是怎样产生的?

现在的单片机有很多都带有了PWM输出,在使用单片机的时候PWM很多时候是没有用到的,用它辅助产生负压是不错的选择。

上面的电路是一个最简单的负压产生电路了。使用的原件是最少的了,只需要给它提供1kHz左右的方波就可以了,相当简单。这里需要注意这个电路的带负载能力是很弱的,同时在加上负载后电压的降落也比较大。

由于上面的原因产生了下面的这个电路:

单片机中常用的负电压是怎样产生的?

负电压产生电路分析

电压的定义:电压(voltage),也称作电势差或电位差,是衡量单位电荷在静电场中由于电势不同所产生的能量差的物理量。其大小等于单位正电荷因受电场力作用从A点移动到B点所做的功,电压的方向规定为从高电位指向低电位的方向。

说白了就是:某个点的电压就是相对于一个参考点的电势之间的差值。V某=E某-E参。一般把供电电源负极当作参考点。电源电压就是Vcc=E电源正-E电源负。

想产生负电压,就让它相对于电源负极的电势更低即可。要想更低,必须有另一个电源的介入,根本原理都是利用两个电源的串联。电源2正极串联在参考电源1的负极后,电源2负极就是负电压了。

单片机中常用的负电压是怎样产生的?

一个负电压产生电路:利用电容充电等效出一个新电源,电容串联在GND后,等效为电源2,则产生负电压。

单片机中常用的负电压是怎样产生的?

1、电容充电:当PWM为低电平时,Q2打开,Q1关闭,VCC通过Q2给C1充电,充电回路是VCC-Q2-C1-D2-GND,C1上左正右负。

单片机中常用的负电压是怎样产生的?

2、电容C1充满电。

单片机中常用的负电压是怎样产生的?

3、电容C1作为电源,C1高电势极串联在参考点。C1放电,从C2续流,产生负电压。

当PWM为低电平时,Q2关闭,Q1打开,C1开始放电,放电回路是C1-C2-D1,这实际上也是对C2进行充电的过程。C2充好电后,下正上负,如果VCC的电势为5点几伏,就可以输出-5V的电压了。

单片机中常用的负电压是怎样产生的?

产生负电压(-5V)的方案

单片机中常用的负电压是怎样产生的?

7660和MAX232输出能力有限,做示波器带高速运放很吃力,所以也得用4片并联的方式扩流。

第一版是7660两片并联的。

用普通的DC/DC芯片都可以产生负电压,且电压精确度同正电压一样,驱动能力也很强,可以达到300mA以上。

一般的开关电源芯片都能产生负电压,实在不行用开关电源输出的PWM去推电荷泵,也可以产生较大的电流,成本也很低,不知纹波要求多少,电荷泵用LC滤波之后纹波相当小的。7660是电荷泵,所以电流很小。

整个示波器的设计,数字电源的+5V和模拟电源的+5V是分开供电的,但是数字地和模拟地应该怎么处理呢?

数字地和模拟地是一定要连在一起的,不然电路没法工作。

数字部分的地返回电流不能流过模拟部分地,两个地应该在稳定的地参考点连在一起。

负电压的意义

1、人为规定。例如电话系统里是用-48V来供电的,这样可以避免电话线被电化学腐蚀。当然了,反着接电话也是可以工作的,无非是电压参考点变动而已。

2、通讯接口需要。例如RS232接口,就必须用到负电压。-3V~-15V表示1,+3~+15V表示0。这个是当初设计通讯接口时的协议,只能遵守咯。PS:MAX232之类的接口芯片自带电荷泵,可以自己产生负电压。

3、为(非轨到轨)运放提供电源轨。老式的运放是没有轨到轨输入/输出能力的,例如OP07,输入电压范围总是比电源电压范围分别小1V,输出分别小2V。这样如果VEE用0V,那么输入端电压必须超过1V,输出电压不会低于2V。这样的话可能会不满足某些电路的设计要求。为了能在接近0V的输入/输出条件下工作,就需要给运放提供负电压,例如-5V,这样才能使运放在0V附近正常工作。不过随着轨到轨运放的普及,这种情况也越来越少见了。

4、这个比较有中国特色,自毁电路。一般来说芯片内部的保护电路对于负电压是不设防的,所以只要有电流稍大,电压不用很高的负电压加到芯片上,就能成功摧毁芯片。

本文来源网络,如果原作者不支持咱们转发,请联系删除,谢谢!

围观 14

振荡器由晶振、电容、电阻组成,部分还有电感,它以晶体频率产生脉冲序列。以英锐恩单片机为例,EN8F156便拥有稳定的时钟源。但不是所有单片机的时钟源都一样,由于每条指令在一定数量的时钟周期后执行,具体取决于单片机的架构。比如,有些人支持使用PLL电路在内部进一步提高时钟速率的能力。


目前,有许多类型的时钟源。可以使用简单的RC电路作为时钟源,但是它们并不精确,因为它们会随温度而发生变化。这类时钟源适用于正常处理和SPI等同步协议。但是对于UART/RS-232,这不是一个好的选择,因为没有外部时钟与数据一起发送,而是以波特率或协议定时的形式将时钟嵌入数据(信号)中。这会降低单片机器对接收数据进行采样的能力,以及正确传输数据的能力。

有些单片机的晶振是外部的,这样可能会更加精确。除此之外,一些单片机器没有内置时钟源,但大多数都有某种RC电路,优点是这类单片机都相当便宜。

大多数单片机器都支持时钟分频器,但它们的频率可能并不精确,因此可能需要使用外部频率。比如,实时时钟通常需要外部32.768KHz晶振。这些都要求非常精确,因为它们是在计算时间。如果不准确,时间相差可能很大。有些单片机内置32KHzRC低速时钟,不过这不足以用来计算时间。

一些单片机器内部和外部支持多个时钟,这一切都取决于单片机和应用程序。

转自:畅学单片机,转载此文目的在于传递更多信息,版权归原作者所有,如涉及侵权,请联系删除。

围观 17

单片机CPU在处理某一事件A时,发生了另一事件B请求CPU迅速去处理(中断发生);CPU暂时中断当前的工作,转去处理事件B(中断响应和中断服务);待CPU将事件B处理完毕后,再回到原来事件A被中断的地方继续处理事件A(中断返回),这一过程称为中断。

例如,当你正在洗衣时,突然手机响了(中断发生),你暂时中断洗衣的工作,转去接电话(中断响应和中断服务),待你接完后,再回来继续洗衣(中断返回),这一过程就是中断。

单片机中断分为内部中断和外部中断两大类,外部中断由单片机外部设备产生,中断产生后通过单片机的外部管脚传递给单片机,传递这个中断信号最简单的方法就是规定单片机的管脚在什么状态下有外部中断产生,这样单片机通常是有一个或多个IO口,当在输入状态时可以用来检测外部中断信号。

有外部中断产生的条件通常也 就是这五种:IO口输入为高、IO口输入为低、IO口输入由高变为低、IO口输入由低变为高、IO口输入由高变低或者由低变高。

一个连接到 单片机的外部设备,如果想要使用单片机的外部中断,就必须在自己请求单片机中断响应的时候给单片机提供单片机在这五种信号中所支持的类型来触发单片机中断。程序运转中,一个中断不是只产生一次,一般都会间隔持续产生,这五种外部中断触发信号前四种都有一个问题,就是外设发出请求中断信号后如果信号请求线 状态不改变,外设会无法向单片机提供下一次中断请求信号。让我们来看看以单片机和外部设备采用负跳变触发中断为例的触发情况。

外部设备以负跳变触发单片机中断,第一次中断请求外部设备的中断请求输出脚可以从高变低,触发单片机中断,第一次中断请求发生后中断请求脚保持输出低,外部设备无法产生第二次中断的触发负跳变信号。

图1 外设只能产生一次中断请求信号示意图

将外部设备的中断请求信号做出修改,原来为需要中断时只是输出从高到低变化,现在改为输出先从高变到低,经过一小段时间后自己从低变回高,这样就可以每次需要中断时都能向单片机输出负跳变触发信号。

图2 外设可连续产生中断请求信号示意图一

或者是由外部设备提供某种接口,单片机通过该接口可以对外部设备进行中断清除操作,中断清除操作可以让外部设备的中断请求输出脚恢复到高。

图3 外设可连续产生中断请求信号示意图

外部中断触发还有一些特殊方式,比如外部脉冲宽度测量、外部脉冲计数等,这些方式都是在前面几种基本触发方式上进行功能扩展得来的,外部脉冲宽度测量就是当中断信号线跳变时会启动内部一个计时器,到下一次中断信号线跳变时通过计时器得到脉冲宽度并重新启动计时器,这些方式很少会使用到,不做详述。

内部中断是指单片机内部的功能模块产生中断信号,只要是单片机内部在CPU外围能独立工作的功能模块都会提供中断功能,常见的内部中断类型有时钟 timer、串口UART、模数转换ADC等。内部中断的工作流程和外部中断没太多区别,只是中断请求信号是在单片机内部进行传输,中断信号不是管脚上的电平状态,而是一个寄存器里面的相应标志位,通常当某个内部中断产生中断请求时就会将相应标志位置为1,CPU响应中断时将这个标志位清0。

图4 内部中断触发示意图

单片机对中断标志位的处理方法没有统一标准,具体的约定方法要看单片机文档。大部分是标志位为1有中断产生,但有少数单片机是标志位为0有中断产生;有的单片机对中断标志位是CPU写入什么就给改写成什么,有的则是规定必须通过写1或写0来实现清除操作,还有少数只要读一下中断标志位就会自动清除掉该标志位。

如果单片机不想被外部中断触发,大不了将用于连接外部中断触发信号的管脚接成不会触发中断的电压状态就可以,但内部中断无法去改变内部 连线,所以单片机为了可以选择中断是否可以被除法,在其内部会有相关的寄存器来进行选择,通过里面的控制标志位,开发人员可以根据实际情况决定是否使用中断。通常单片机里面有一个总控制位,这个位可以控制所有中断的开与关,然后每一种中断自己还有一个独立的控制位决定自己的开与关,如果想使用某个中断,就 需要将总中断开关和对应中断的开关都打开。

当单片机有中断信号产生时,就会触发对应中断,不同的中断源会需要不同的响应方法,也就是说不同 的中断产生的时候,需要单片机程序依照不同的中断源做出不同的响应,这就是中断服务程序。如果是UART收到新数据产生中断,应该是UART中断服务程序 将数据读回来并做处理,如果是ADC转换完成产生的中断,需要的则是ADC中断服务程序将数据读回来并做处理。如果需要清中断标志位动作,一般都是在中断服务程序里面完成。

不同的中断源需要与之对应的中断服务程序,实际开发中并不是所有的中断都会被用到,开发人员为了节约程序代码空间会只写 出自己要使用到的中断服务程序,也就是说会有一些中断没有与之对应的中断服务程序,如果触发了这样的中断,单片机程序会运行出错,前面中断各自独立的控制位就排上用场,将这些控制位关掉,相应中断就不会被触发。

单片机开始上电的时候,如果控制中断是否被打开的寄存器控制标志位被打开,可能会出现中断被误触发的情况,而这个中断如果没有与之相对应的中断服务程序的话程序就会跑飞,所以单片机上电的时候一般会自动将这些寄存器里面的标志位都关掉,以免误触发。

中断服务程序是单片机程序的一部分,具体内容由开发人员决定,这样中断服务程序的大小在单片机程序中的位置就不固定,当单片机的中断被触发后,单片机需要知道中断服务程序在什么位置才能执行它,单片机通过中断跳转表(中断向量表)来解决这个问题。

虽然中断服务程序的大小和在整个程序中的位置会不固定,但程序只要被烧进单片机系统,对于这个程序来说其中断服务程序的大小和在整个程序中的位置就会被固定下来,如果对单片机程序空间分配我们做出一些约定,将一个绝对固定地址专门分配给中断使用,程序编译时会将中断服务程序的起始地址(或者是跳转到中断服务 程序的指令)填到这个绝对固定地址所在的空间,当中断产生时候,单片机先将绝对固定地址所在位置里面的内容读出,根据所读内容就可以跳转到中断服务程序。

图5 中断响应示意

简单的单片机所提供的中断种类有限,为了简化程序,会给每一个中断分配一个用来存放中断服务程序地址的地址空间,这种方法其实没什么不好的地方,只是单片机 技术发展到现在遇到了瓶颈,高端单片机越来越复杂,于是一些专业厂商开始合作共享技术资源,例如ARM公司利用他们在CPU架构体系上的技术优势专门给另 外的厂商提供CPU内核,另外的厂商在ARM内核的CPU外围增加功能模块,这些功能模块大都支持中断。

图6 ARM内核单片机架构图

不同厂家在相同CPU内核基础上设计出来的单片机外围的功能模块会各不相同,从而中断的种类和个数也各不相同,而CPU处理中断的方法是一样的,如果延续简单的单片机给每个中断都分配一个地址空间的做法显然有问题,CPU无法知道到底有多少种中断需要支持,这些中断又分别对应什么模块,于是采用另外一种中断处理方法,将所有中断地址都指向同一个,并将所有中断依次编号,中断产生时候CPU会告诉中断服务程序当前中断编号是多少,然后中断服务程序根据中断编号做出相应响应。

图7 公用中断入口中断响应流程图

图8 独立中断入口中断响应流程图

所有中断使用同一个中断向量地址,然后通过中断号判断中断类别的方法虽然解决了通用CPU内核中断不能直接对应中断向量地址的问题,但把它中断处理的流程和具有独立中断向量表的单片机相比就会发现:中断的响应速度会变慢。具有独立中断向量表的单片机只要一条跳转指令就可以直接进入中断程序,而没有独立中断向量表的单片机需要先跳转到中断公共入口,然后通过代码判定中断类别,确定中断类别后才跳转到真正的中断程序中去。C语言的代码会让这种情况更加恶化,所以如果是没有独立中断向量表的单片机一般采用汇编查表的方法加快中断响应速度。

图9 汇编中断快速跳转表

中断程序执行完毕后回返回继续执行主程序,这样就要求中断不改变主程序的运行状态,所以中断响应时需要将程序当前运行的状态信息保存起来,比如程序运行到什么位置、当前CPU状态寄存器的状态等信息。当中断程序执行完毕,可以通过这些信息将CPU状态寄存器恢复原来状态,并能返回原程序继续执行。不同的单片机对此的处理方式也会有不同,一种是完全由硬件来完成,并不需要程序来进行管理;另外一种是将状态信息用相应指令保存在特定位置,返回时再用相应指令恢复原来状态。

单片机中断还有中断优先级和中断嵌套的概念,但不是所有的单片机都会支持这两种功能。中断优先级是不同的中断会有不同的优先级别,如果同时有两个中断产生,单片机会先响应优先级高的中断。中断嵌套是指在中断响应当中又有新的中断产生,单片机可以暂停当前的中断程序执行去响应新的中断,新中断程序执行完以后在接着执行当前中断程序。一般中断嵌套是高优先级的中断可以插入低优先级中断响应程序,同级或低级的中断不能插入当前中断响应程序。

图10 中断嵌套示意图


中断步骤说明
步骤①保存主程序现场,执行中断1服务程序。
步骤②保存中断1服务程序现场,执行中断2服务程序。
步骤③恢复中断1服务程序现场,继续执行中断1服务程序。
步骤④恢复主程序现场,准备继续执行主程序,有新中断不能继续执行主程序。
步骤⑤保存主程序现场,执行中断3服务程序。
步骤⑥恢复主程序现场,准备继续执行主程序,有新中断不能继续执行主程序。
步骤⑦保存主程序现场,执行中断4服务程序。
步骤⑧恢复主程序现场,无中断产生继续执行主程序。

有的单片机一进入中断函数就会自动将中断的总控制位关掉,需要开发人员在中断程序中用程序再次打开,否则一次中断后所有的中断就不能继续使用。对于中断标志位,在写单片机程序的时候要依据单片机文档进行清除标志为操作,不然有可能会一旦产生某个中断就会连续不停的反复响应这个中断,导致主程序不能继续运行。

来源:STM32嵌入式开发,转载此文目的在于传递更多信息,版权归原作者所有,如涉及侵权,请联系删除。

围观 13

计算机处理器以晶体管集成电路技术的发展而在不断前进。早期的处理器是通过二极管搭建的逻辑计算器。随着现代加工业的发展,处理器使用集成电路构建。现代计算机处理器是在一片单晶硅上,通过刻腐机雕刻并制作各种晶体管电路,实现高度集成的计算功能的电路集合体。

计算机根据功能特性主要有:中央处理器(简称CPU)、专用数字处理器(DSP)可编程门阵列(简称FPGA)、片上系统(简称SOC)、微处理器(简称MCU)等。为此作如下分析:

(1)CPU类处理器主要应用在个人电脑上,它是电脑上“枢纽中心”,主要功能是实现其他电路的沟通桥梁作用。现代CPU内部集成度较高,一般含有传感器、图形加速器、内存管理器、少量必要的随机存取存储器(简称RAM)和只读存储器(简称ROM)。CPU具有高速运算能力和处理能力,支持操作系统的移植,便于大型平台的开发。但它的价格偏高、指令复杂,故在本系统中不适宜采用。

(2)DSP类处理器主要应用在需要复杂计算处理的场景。DSP内部集成专用浮点数计算,支持高速傅里叶转换,可以高速实现复杂数据计算。最常见的应用为电脑的“显卡”。除此之外,在工业领域中具有较高的应用范围,如电源类。该处理具备RAM和ROM,支持多种外设接口。该类处理器一般用于复杂运算系统上,故在本系统中不适宜采用。

(3)SOC类处理器又叫“片上系统”,它包括基本的CPU运算处理器、RAM和ROM类的存储器。SOC由于具有良好的外设功能,减少了设计周期和成本。现代的SOC支持精简指令,支持操作系统移植,便于大型平台的开发。SOC的价格仍然偏高,故在本系统中不适宜采用。

(4)MCU微处理器和SOC功能较为相似,属于精简版的SOC,且具有独立运算功能,和支持外设。MCU有点:性价比相对特别高,指令简单,适用做小型系统的核心控制器。MCU缺点:运算处理能力差,在设计要求稳定高速的浮点数条件下,基本无法实现要求,但是在小型系统设计来说,其作为控制芯片最为合适。

MCU一般被称作“单片机”,它的体积很小,可以安装在任何小型仪器或者设备当中。它是把CPU、RAM、ROM、输入/输出端口(I/0)等主要计算机功能部件都集成在一块集成电路芯片上的微型计算机。MCU的发展给小型化电子设备发展带来可能。

版权声明:本文为CSDN博主「武力戡乱」的原创文章,
遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:
https://blog.csdn.net/st441747863/article/details/104792415

围观 12

一、五大内存分区

内存分成5个区,它们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。

1、栈区(stack):FIFO就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。

2、堆区(heap):就是那些由new分配的内存块,它们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。

3、自由存储区:就是那些由malloc等分配的内存块,它和堆是十分相似的,不过它是用free来结束自己的生命。

4、全局/静态存储区:全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。

5、常量存储区:这是一块比较特殊的存储区,它们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多)

code/data/stack

内存主要分为代码段,数据段和堆栈。代码段放程序代码,属于只读内存。数据段存放全局变量,静态变量,常量等,堆里存放自己malloc或new出来的变量,其他变量就存放在栈里,堆栈之间空间是有浮动的。数据段的内存会到程序执行完才释放。调用函数先找到函数的入口地址,然后计算给函数的形参和临时变量在栈里分配空间,拷贝实参的副本传给形参,然后进行压栈操作,函数执行完再进行弹栈操作。字符常量一般放在数据段,而且相同的字符常量只会存一份。

二、C语言程序的存储区域

1、由C语言代码(文本文件)形成可执行程序(二进制文件),需要经过编译-汇编-连接三个阶段。编译过程把C语言文本文件生成汇编程序,汇编过程把汇编程序形成二进制机器代码,连接过程则将各个源文件生成的二进制机器代码文件组合成一个文件。

2、C语言编写的程序经过编译-连接后,将形成一个统一文件,它由几个部分组成。在程序运行时又会产生其他几个部分,各个部分代表了不同的存储区域:

1)代码段(Code或Text)

代码段由程序中执行的机器代码组成。在C语言中,程序语句执行编译后,形成机器代码。在执行程序的过程中,CPU的程序计数器指向代码段的每一条机器代码,并由处理器依次运行。

2)只读数据段(RO data)

只读数据段是程序使用的一些不会被更改的数据,使用这些数据的方式类似查表式的操作,由于这些变量不需要更改,因此只需要放置在只读存储器中即可。

3)已初始化读写数据段(RW data)

已初始化数据是在程序中声明,并且具有初值的变量,这些变量需要占用存储器的空间,在程序执行时它们需要位于可读写的内存区域内,并且有初值,以供程序运行时读写。

4)未初始化数据段(BBS)

未初始化数据是在程序中声明,但是没有初始化的变量,这些变量在程序运行之前不需要占用存储器的空间。

5)堆(heap)

堆内存只在程序运行时出现,一般由程序员分配和释放。在具有操作系统的情况下,如果程序没有释放,操作系统可能在程序(例如一个进程)结束后会后内存。

6)栈(statck)

堆内存只在程序运行时出现,在函数内部使用的变量,函数的参数以及返回值将使用栈空间,栈空间由编译器自动分配和释放。


3、代码段、只读数据段、读写数据段、未初始化数据段属于静态区域,而堆和栈属于动区域。代码段、只读数据段和读写数据段将在连接之后产生,未初始化数据段将在程序初始化的时候开辟,而对堆和栈将在程序饿运行中分配和释放。

4、C语言程序分为映像和运行时两种状态。在编译-连接后形成的映像中,将只包含代码段(Text)、只读数据段(R0 Data)和读写数据段(RW Data)。在程序运行之前,将动态生成未初始化数据段(BSS),在程序的运行时还将动态生成堆(Heap)区域和栈(Stack)区域。

注:

1、一般来说,在静态的映像文件中,各个部分称之为节(Section),而在运行时的各个部分称之为段(Segment)。如果不详细区分,统称为段。

2、C语言在编译连接后,将生成代码段(TEXT),只读数据段(RO Data)和读写数据段(RW Data)。在运行时,除了上述三个区域外,还包括未初始化数据段(BBS)区域和堆(heap)区域和栈(Stack)区域。

三、C语言程序的段

1、段的分类

每一个源程序生成的目标代码将包含源程序所需要表达的所有信息和功能。目标代码中各段生成情况如下:

1)代码段(Code)

代码段由程序中的各个函数产生,函数的每一个语句将最终经过编译和汇编生成二进制机器代码

2)只读数据段(RO Data)

只读数据段由程序中所使用的数据产生,该部分数据的特点在运行中不需要改变,因此编译器会将数据放入只读的部分中。C语言的一些语法将生成只读数据数据段。

2、只读数据段(RO Data)

只读数据段(RO Data)由程序中所使用的数据产生,该部分数据的特点是在运行中不需要改变,因此编译器会将数据放入只读的部分中。以下情况将生成只读数据段。

1)只读全局变量

定义全局变量const char a[100]=”abcdefg”将生成大小为100个字节的只读数据区,并使用字符串“abcdefg”初始化。如果定义为const char a[]=”abcdefg”,没有指定大小,将根据“abcdefgh”字串的长度,生成8个字节的只读数据段。

2)只读局部变量

例如:在函数内部定义的变量const char b[100]=”9876543210”;其初始化的过程和全局变量。

3)程序中使用的常量

例如:在程序中使用printf("informationn”),其中包含了字串常量,编译器会自动把常量“information n”放入只读数据区。

注:在const char a[100]={“ABCDEFG”}中,定义了100个字节的数据区,但是只初始化了前面的8个字节(7个字符和表示结束符的‘0’)。在这种用法中,实际后面的字节米有初始化,但是在程序中也不能写,实际上没有任何用处。因此,在只读数据段中,一般都需要做完全的的初始化。

3、读写数据段(RW Data)

读写数据段表示了在目标文件中一部分可以读也可以写的数据区,在某些场合它们又被称为已初始化数据段。这部分数据段和代码,与只读数据段一样都属于程序中的静态区域,但是具有科协的特点。

1)已初始化全局变量

例如:在函数外部,定义全局的变量char a[100]=”abcdefg”

2)已初始化局部静态变量

例如:在函数中定义static char b[100]=”9876543210”。函数中由static定义并且已经初始化的数据和数组将被编译为读写数据段。

说明:

读写数据区的特点是必须在程序中经过初始化,如果只有定义,没有初始值,则不会生成读写数据区,而会定义为未初始化数据区(BSS)。如果全局变量(函数外部定义的变量)加入static修饰符,写成static char a[100]的形式,这表示只能在文件内部使用,而不能被其他文件使用。

4、未初始化数据段(BSS)

未初始化数据段常被称之为BSS(英文名为Block start by symbol的缩写)。与读写数据段类似,它也属于静态数据区。但是该段中数据没有经过初始化。因此它只会在目标文件中被标识,而不会真正称为目标文件中的一个段,该段将会在运行时产生。未初始化数据段只有在运行的初始化阶段才会产生,因此它的大小不会影响目标文件的大小。

四、在C语言的程序中,对变量的使用需要注意的问题

1、在函数体中定义的变量通常是在栈上,不需要在程序中进行管理,由编译器处理。

2、用malloc,calloc,realoc等分配分配内存的函数所分配的内存空间在堆上,程序必须保证在使用后使用后freee释放,否则会发生内存泄漏。

3、所有函数体外定义的是全局变量,加了static修饰符后的变量不管在函数内部或者外部存放在全局区(静态区)。

4、使用const定义的变量将放于程序的只读数据区。

说明:

在C语言中,可以定义static变量:在函数体内定义的static变量只能在该函数体内有效;在所有函数体外定义的static变量,也只能在该文件中有效,不能在其他源文件中使用;对于没有使用 static修饰的全局变量,可以在其他的源文件中使用。这些区别是编译的概念,即如果不按要求使用变量,编译器会报错。使用static 和没使用static修饰的全局变量最终都将放置在程序的全局去(静态去)。

五、程序中段的使用

C语言中的全局区(静态区),实际上对应着下述几个段:

只读数据段:RO Data

读写数据段:RW Data

未初始化数据段:BSS Data

一般来说,直接定义的全局变量在未初始化数据区,如果该变量有初始化则是在已初始化数据区(RW Data),加上const修饰符将放置在只读区域(RO Data).

例如:

const char ro[ ]=”this is a readonlydata”; //只读数据段,不能改变ro数组中的内容,ro存放在只读数据段。

char rw1[ ]=”this is global readwrite data”; //已初始化读写数据段,可以改变数组rw1中的内容。应为数值/是赋值不是把”this is global readwrite data” 地址给了rw1,不能改变char rw1[ ]=”this is global readwrite data”; //已初始化读写数据段,可以改变数组rw1中的内容。应为数值/是赋值不是把”this is global readwrite data” 地址给了rw1,不能改变”this is global readwrite data”的数值。因为起是文字常量放在只读数据段中

char bss_1[100];//未初始化数据段

const char *ptrconst = “constant data”; //”constant data”放在只读数据段,不能改变ptrconst中的值,因为其是地址赋值。ptrconst指向存放“constant data”的地址,其为只读数据段。但可以改变ptrconst地址的数值,因其存放在读写数据段中。

实例讲解:

int main( )

{

short b;//b放置在栈上,占用2个字节

char a[100];//需要在栈上开辟100个字节,a的值是其首地址

char s[]=”abcde”;

//s在栈上,占用4个字节,“abcde”本身放置在只读数据存储区,占6字节。s是一个地址

//常量,不能改变其地址数值,即s++是错误的。

char *p1;//p1在栈上,占用4个字节

char *p2 ="123456";//"123456"放置在只读数据存储区,占7个字节。p2在栈上,p2指向的内容不能更

//改,但是p2的地址值可以改变,即p2++是对的。

static char bss_2[100]; //局部未初始化数据段

static int c=0 ; //局部(静态)初始化区

p1 = (char *)malloc(10*sizeof(char)); //分配的内存区域在堆区

strcpy(p1,”xxx”); //”xxx”放置在只读数据存储区,占5个字节

free(p1); //使用free释放p1所指向的内存

return 0;

}

说明:

1、只读数据段需要包括程序中定义的const型的数据(如:const char ro[]),还包括程序中需要使用的数据如“123456”。对于const char ro[]和const char * ptrconst的定义,它们指向的内存都位于只读数据据区,其指向的内容都不允许修改。区别在于前者不允许在程序中修改ro的值,后者允许在程序中修改ptrconst本身的值。对于后者,改写成以下的形式,将不允许在程序中修改ptrconst本身的值:

const char * const ptrconst = “const data”;

2、读写数据段包含了已经初始化的全局变量static char rw1[]以及局部静态变量static char

rw2[]。rw1和rw2的差别在于编译时,是在函数内部使用的还是可以在整个文件中使用。对于前者,static修饰在于控制程序的其他文件时候可以访问rw1变量,如果有static修饰,将不能在其他的C语言源文件中使用rw1,这种影响针对编译-连接的特性,但无论有static,变量rw1都将被放置在读写数据段。对于后者rw2,它是局部的静态变量,放置在读写数据区;如果不使用static修饰,其意义将完全改变,它将会是开辟在栈空间局部变量,而不是静态变量。

3、未初始化数据段,事例1中的bss_1[100]和 bss_2[200]在程序中代表未初始化的数据段。其区别在于前者是全局的变量,在所有文件中都可以使用;后者是局部的变量,只在函数内部使用。未初始化数据段不设置后面的初始化数值,因此必须使用数值指定区域的大小,编译器将根据大小设置BBS中需要增加的长度。

4、栈空间包括函数中内部使用的变量如short b和char a[100],以及char *p1中p1这个变量的值。

1)变量p1指向的内存建立在堆空间上,堆空间只能在程序内部使用,但是堆空间(例如p1指向的内存)可以作为返回值传递给其他函数处理。

2)栈空间主要用于以下3类数据的存储:

a、函数内部的动态变量

b、函数的参数

c、函数的返回值

3)栈空间主要的用处是供函数内部的动态变量使用,变量的空间在函数开始之前开辟,在函数退出后由编译器自动回收。看一个例:

int main( )

{

char *p = "tiger";

p[1] = 'I';

p++;

printf("%sn",p);

}

编译后提示:段错误

分析:

char *p = "tiger";系统在栈上开辟了4个字节存储p的数值。"tiger"在只读存储区中存储,因此"tiger"的内容不能改变,*p="tiger",表示地址赋值,因此,p指向了只读存储区,因此改变p指向的内容会引起段错误。但是因为p是存放在栈上,因此p的数值是可以改变的,因此p++是正确的。

六、const的使用

1、前言:

const是一个C语言的关键字,它限定一个变量不允许被改变。使用const在一定程序上可以提高程序的健壮性,另外,在观看别人代码的时候,清晰理解const所起的作用,对理解别人的程序有所帮助。

2、const变量和常量

1)const修饰的变量,其值存放在只读数据段中,其值不能被改变。称为只读变量。

其形式为 const int a=5;此处可以用a代替5

2)常量:其也存在只读数据段中,其数值也不能被改变。其形式为"abc" ,5

3、const 变量和const限定的内容,先看一个事例:

typedef char* pStr;

int main( )

{

char string[6] = “tiger”;

const char *p1 = string;

const pStr p2 = string;

p1++;

p2++;

printf(“p1=%snp2=%sn”,p1,p2);

}

程序经过编译后,提示错误为

error:increment of read-only variable ‘p2’

1)const 使用的基本形式为:const char m;

//限定m 不可变

2)替换1式中的m,const char *pm;

//限定*pm不可变,当然pm是可变的,因此p1++是对的。

3)替换1式中的char,const newType m;

//限定m不可变,问题中的pStr是一种新类型,因此问题中p2不可变,p2++是错误的。

4、const 和指针

类型声明中const用来修饰一个常量,有如下两种写法:

1)const在前面

const int nValue;//nValue是const

const char *pContent;//*pContent是const,pConst可变

const (char *)pContent;//pContent是const,*pContent可变

char *const pContent;//pContent是const,*pContent可变

const char * const pContent;//pContent和*pContent都是const

2)const 在后面与上面的声明对等

int const nValue;// nValue是const

char const *pContent;//*pContent是const, pContent可变

(char *) constpContent;//pContent是const, *pContent可变

char* const pContent;// pContent是const, *pContent可变

char const* const pContent;//pContent和*pContent都是const

说明:const和指针一起使用是C语言中一个很常见的困惑之处,下面是两天规则:

1)沿着*号划一条线,如果const位于*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;如果const位于*的右侧,const就是修饰指针本身,即指针本身是常量。你可以根据这个规则来看上面声明的实际意义,相信定会一目了然。

2)对于const (char *) ; 因为char *是一个整体,相当于一个类型(如char),因此,这是限定指针是const。

七、单片机C语言中的data,idata,xdata,pdata,code

从数据存储类型来说,8051系列有片内、片外程序存储器,片内、片外数据存储器,片内程序存储器还分直接寻址区和间接寻址类型,分别对应code、data、xdata、idata以及根据51系列特点而设定的pdata类型,使用不同的存储器,将使程序执行效率不同,在编写C51程序时,最好指定变量的存储类型,这样将有利于提高程序执行效率(此问题将在后面专门讲述)。与ANSI-C稍有不同,它只分SAMLL、COMPACT、LARGE模式,各种不同的模式对应不同的实际硬件系统,也将有不同的编译结果。

在51系列中data,idata,xdata,pdata的区别:

data:固定指前面0x00-0x7f的128个RAM,可以用acc直接读写的,速度最快,生成的代码也最小。

idata:固定指前面0x00-0xff的256个RAM,其中前128和data的128完全相同,只是因为访问的方式不同。idata是用类似C中的指针方式访问的。汇编中的语句为:mox ACC,@Rx.(不重要的补充:c中idata做指针式的访问效果很好)

xdata:外部扩展RAM,一般指外部0x0000-0xffff空间,用DPTR访问。

pdata:外部扩展RAM的低256个字节,地址出现在A0-A7的上时读写,用movx ACC,@Rx读写。这个比较特殊,而且C51好象有对此BUG,建议少用。但也有他的优点,具体用法属于中级问题,这里不提。

单片机C语言unsigned char code table[]code 是什么作用?

code的作用是告诉单片机,我定义的数据要放在ROM(程序存储区)里面,写入后就不能再更改,其实是相当与汇编里面的寻址MOVX(好像是),因为C语言中没办法详细描述存入的是ROM还是RAM(寄存器),所以在软件中添加了这一个语句起到代替汇编指令的作用,对应的还有data是存入RAM的意思。

程序可以简单的分为code(程序)区,和data (数据)区,code区在运行的时候是不可以更改的,data区放全局变量和临时变量,是要不断的改变的,cpu从code区读取指令,对data区的数据进行运算处理,因此code区存储在什么介质上并不重要,象以前的计算机程序存储在卡片上,code区也可以放在rom里面,也可以放在ram里面,也可以放在flash里面(但是运行速度要慢很多,主要读flash比读ram要费时间),因此一般的做法是要将程序放到flash里面,然后load到 ram里面运行的;DATA区就没有什么选择了,肯定要放在RAM里面,放到rom里面改动不了。

bdata如何使用它呢?

若程序需要8个或者更多的bit变量,如果你想一次性给8个变量赋值的话就不方便了,(举个例子说说它的方便之处,想更深入的了解请在应用中自己琢磨)又不可以定义bit数组,只有一个方法

char bdata MODE;

sbit MODE_7 = MODE^7;

sbit MODE_6 = MODE^6;

sbit MODE_5 = MODE^5;

sbit MODE_4 = MODE^4;

sbit MODE_3 = MODE^3;

sbit MODE_2 = MODE^2;

sbit MODE_1 = MODE^1;

sbit MODE_0 = MODE^0;

8个bit变量MODE_n 就定义好了

这是定义语句,Keilc 的特殊数据类型。记住一定要是sbit

不能 bit MODE_0 = MODE^0;

赋值语句要是这么写C语言就视为异或运算。

Flash相对单片机里的RAM属于外部存取器,虽其结构位置装在单片机中,其实xdata是放在相对RAM的外面,而flash正是相对RAM外面。

inta变量定义在内部RAM,xdatainta定义在外部RAM或flash,uchar codea定义在flash。

uchar code duma[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x40,0x00}; //共阴的数码管段选,P2口要取的数值

若定义 uchar aa[5],aa[5]中的内容是存放在数据存储区(RAM)中的,在程序运行工程中各个数组元素的值可以被修改,掉电后aa[5]中的数据无法保存。

若定义 uchar code bb[5]中的内容是存放在程序存储区(如flash)中的,只有在烧写程序时,才能改变bb[5]中的各元素的值,在程序运行工程中无法修改,并且掉电后bb[5]中的数据不消失。

八、C语言中堆和栈的区别

C语言程序经过编译连接后形成编译、连接后形成的二进制映像文件由栈、堆、数据段(由三部分部分组成:只读数据段,已经初始化读写数据段,未初始化数据段即BBS)和代码段组成,如下图所示:


1、栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量等值。其操作方式类似于数据结构中的栈。

2、堆区(heap):一般由程序员分配释放,若程序员不释放,则可能会引起内存泄漏。注堆和数据结构中的堆栈不一样,其类是与链表。

3、程序代码区:存放函数体的二进制代码。

4、数据段:由三部分组成:

1)只读数据段:

只读数据段是程序使用的一些不会被更改的数据,使用这些数据的方式类似查表式的操作,由于这些变量不需要更改,因此只需要放置在只读存储器中即可。一般是const修饰的变量以及程序中使用的文字常量一般会存放在只读数据段中。

2)已初始化的读写数据段:

已初始化数据是在程序中声明,并且具有初值的变量,这些变量需要占用存储器的空间,在程序执行时它们需要位于可读写的内存区域内,并且有初值,以供程序运行时读写。在程序中一般为已经初始化的全局变量,已经初始化的静态局部变量(static修饰的已经初始化的变量)

3)未初始化段(BSS):

未初始化数据是在程序中声明,但是没有初始化的变量,这些变量在程序运行之前不需要占用存储器的空间。与读写数据段类似,它也属于静态数据区。但是该段中数据没有经过初始化。未初始化数据段只有在运行的初始化阶段才会产生,因此它的大小不会影响目标文件的大小。在程序中一般是没有初始化的全局变量和没有初始化的静态局部变量。

堆和栈的区别

1、申请方式

(1)栈(satck):由系统自动分配。例如,声明在函数中一个局部变量int b;系统自动在栈中为b开辟空间。

(2)堆(heap):需程序员自己申请(调用malloc,realloc,calloc),并指明大小,并由程序员进行释放。容易产生memory leak.

eg:charp;

p = (char *)malloc(sizeof(char));//但是,p本身是在栈中。

2、申请大小的限制

1)栈:在windows下栈是向底地址扩展的数据结构,是一块连续的内存区域(它的生长方向与内存的生长方向相反)。栈的大小是固定的。如果申请的空间超过栈的剩余空间时,将提示overflow。

2)堆:堆是高地址扩展的数据结构(它的生长方向与内存的生长方向相同),是不连续的内存区域。这是由于系统使用链表来存储空闲内存地址的,自然是不连续的,而链表的遍历方向是由底地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。

3、系统响应:

1)栈:只要栈的空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

2)堆:首先应该知道操作系统有一个记录空闲内存地址的链表,但系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的free语句才能正确的释放本内存空间。另外,找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

说明:对于堆来讲,对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,

4、申请效率

1)栈由系统自动分配,速度快。但程序员是无法控制的

2)堆是由malloc分配的内存,一般速度比较慢,而且容易产生碎片,不过用起来最方便。

5、堆和栈中的存储内容

1)栈:在函数调用时,第一个进栈的主函数中后的下一条语句的地址,然后是函数的各个参数,参数是从右往左入栈的,然后是函数中的局部变量。注:静态变量是不入栈的。

当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续执行。

2)堆:一般是在堆的头部用一个字节存放堆的大小。

6、存取效率

1)堆:char *s1=”hellowtigerjibo”;是在编译是就确定的

2)栈:char s1[]=”hellowtigerjibo”;是在运行时赋值的;用数组比用指针速度更快一些,指针在底层汇编中需要用edx寄存器中转一下,而数组在栈上读取。

补充:

栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

7、分配方式:

1)堆都是动态分配的,没有静态分配的堆。

2)栈有两种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的。它的动态分配是由编译器进行释放,无需手工实现。

来源:玩转单片机

围观 11

页面

订阅 RSS - 单片机