产品选型

单片机 局部变量 (建议收藏)关于单片机检查变量的方法,你会几种?

小编 2025-06-30 产品选型 23 0

(建议收藏)关于单片机检查变量的方法,你会几种?

这些单片机调试方法你真的知道吗?

导读:授人以鱼不如授人以渔,为什么那些前辈们能快捷定位问题,这个系列的文章将揭秘 KEIL 调试那些不为人知的事。

以下内容更适用于 STM32 单片机(51 也支持局部)。掌握了它们将加速你的调试速度,不信吗?试试看咯。

程序中最重要的是什么,数据。很多时候程序运行有问题和你的数据密切相关,假如你能实时观察程序中的数据,你觉得怎么样?

///插播一条:我自己在今年年初录制了一套还比较系统的入门单片机教程和毕业设计指导,想要的同学找我拿就行了免費的,私信我就可以哦~点我头像白色字体加我也能领取哦,记得口令一哥///

数据分为两种,一种是可变的,一种为不可变的。假如 RAM 数据为可变的,FLASH 数据为不可变的(实际上也能变更,不然你怎么把程序烧写到 FLASH 中呢),还有一种极其特殊的存在:寄存器数据。

1、变量查看

首先说说可变数据的查看方式,比如你声明的一些变量,可以通过 Watch 窗口查看。

通过以下方式可打开 Watch 窗口(任选一个窗口打开即可):

在这里可以查看变量(这里选择 Watch 1):

是否发现上面的显示不太对劲?,这是啥意思?这个是说明 KEIL 无法找到这个变量。就我所知,有两种情况会出现这种现象:

1)、这个变量不存在:有可能你之前声明过这个变量,后来发现没用到,删除了。

2)、使用 static 声明的变量。

比如像这样的:

如果是第二种情况,那么可以通过将程序运行到使用该变量的地方,然后停止就可以查看了。

添加变量:

那么如何添加你需要查看的变量呢?通常可以使用如下方法:

1)、光标处于变量位置,然后右击会出现一个界面,最后选择添加到你需要的窗口:

2)、直接将你的变量拖到你的 Watch 窗口(前提是你已经打开了 Watch 窗口):

3)、复制变量名,然后将变量名粘贴在窗口里面就可以了。

移除变量

能添加,也就能移除,可以通过以下方法移除你的变量(注意程序应该处于停止状态):

当然还有一种方法就是直接删除这个变量名,这也可以达到移除的效果。

如果你希望使用十进制的方式显示你的数据,那么试试去掉上面的 Hexadecimal Display 勾选吧。

2、内存查看

如果你想查看 FLASH 的数据怎么办?那么试试这个窗口:

比如说你想看看 FLASH 地址开始处是什么数据,只要把 0x0800 0000 输入进去后按回车键就可以了(注意数字中间没有空格,只是为了看起来方便才用空格分开的):

四字节显示不爽?那试试改变显示格式吧,无符号,有符号,char、int、float……任你选(如果不想用十进制表示,必须去掉 Decimal 的勾选):

如果需要修改某个地址的数据,也可以通过上面的方式在某个数据上右击后选择修改(Modify)。

事实上,除了 FLASH 数据,RAM 数据也是可以通过它观察的:

从这里能够看到,Memory 在数据显示上比 Watch 窗口更壮大,它能够对单片机上的所有数据进行查看,缺少点就是你不知道谁是谁了(没有变量名显示,只能靠地址分辨了)。

对于以上知识可能很多人都了解过,下面说一说一般人不知道的点:

对于单片机来说,片上外设决定了你单片机的功能,所以多数情况下都须要查看外设寄存器的值,那么该怎么样查看呢?

通过 Watch 窗口就能够了。怎么做?

以最为常用的串口外设为例说明:

之后你就可以看到寄存器的内容了:

有没有很方便啊。那到底添加什么标志符才能显示出来呢?实际上这个标志符就是那些外设宏定义了。怎么看?前面一哥说过搜索也算一个调试功能,那你在工程内搜索之后就会发现这个定义:

明白了吧,你输写的 USART1 其实就是一个指针,然后 KEIL 就会从这个地址里读出数据并依照你的指针构造体显示出来。知道了这个,你应该也就知道该怎么样查看 GPIO、SPI 等外设了。

其实这里还有一个额外的益处,不知道你是否发现了。我们都知道,使用宏定义虽好,但它有一个很麻烦的地方,就是不能很直观的知道这个值到底是多少,那么通过这个你也就能够知道 USART1 的值就是 0x4001 3800 了,也就是 USART1?外设基地址就是它:

事实上通过 Memory 窗口也是可以的:

只是没有 Watch 窗口那么直观而已。

那么为什么须要支持这两种方式呢?我们知道有些变量空间非常大,假如串口缓存数组,可能有好几 K,假如你通过 Watch 窗口查看的话,你会发现它会严重干扰你的程序运行,表现情况就是数据刷新缓慢,但是通过 Memory 就不一样了,相当流畅。所以假如你要看大数据的话,用 Memory 效果最好。

还有一个益处就是,它能随时更变更量的显示方式,假如说你把一个浮点数据放在了四个字节数组变量中,那么我想查看这个浮点数据是什么怎么办,我不可能通过浮点数据的存储格式手工计算一下吧?假如你能计算出来还好,说明你很厉害,但是万一不懂存储格式或者计算错了呢?使用 Memory 就不同了,你只有把这个数组的地址给它,然后设置显示方式为浮点型就能够了,相当方便。还有就是当使用宏定义时,查看这个宏定义的值非常不方便,使用 Memory 就能够轻松查看。

假如查看 USART1 的 DR 寄存器地址,在 Watch 窗口显示是这样的:

如果你要知道 DR 的地址,你就需要通过基地址 0x400 13800 和偏移地址 0x04 知道它的地址为 0x400 13804,即使用 Watch 单独查看 DR 也是一样:

但是通过 Memory 就是这样的:

这里千万要注意的是要使用取地址符 &,否则它就变成了这样:

外设地址怎么可能是 0,所以肯定错了。

事实上你用 Watch 也是可以的,但显得比较诡异,会让你觉得这是一个指针变量:

实际上它只是一个常量而已,并不是指针变量。

在这里你会发现,这些窗口支持运算符,看这个:

还有这个:

变量的查看也是如此,是不是特别方便啊。须要注意的是,Watch 窗口和 Memory 都支持在线修改数据,对于须要临时更改数据情况下非常有用。

3、临时变量查看

以上数据查看都有一个特点,那就是数据的地址都是固定的,这样通过地址就能知道你的数据是什么,但还有一种数据,只会在函数运行的时候才会创建,一旦函数运行完,变量空间也就消失了,这就是局部变量。

局部变量使用的空间是栈,在进入函数时分配,离开函数的时候就消失了,所以你没法确定一个局部变量的地址(事实上你能得到局部变量的地址,但这个地址是随时变化的,所以即便你得到了也没用,由于你只能得到这一次的内存地址,下一次又会变化的)。

那么该怎么样观察局部变量的值呢?

假如一个简略延时函数,我想知道传入函数的参数是什么,那么通过窗口 Call Stack + Locals 就能够了。这个是专门查看局部变量的,当然也能够在函数中查看局部静态变量(关于这个你能够看 【C语言之static】)。

当你把断点设置在函数内部,当程序停下在函数内部时,就能通过这个窗口查看了。

当程序停止在上面的第一个断点时,就可以在窗口上看到这个:

不知道你发现没有,nms 变量显示为,用有道词典取词后你就知道这是说变量不在范围内。什么意思?这是由于你的断点在函数的初始处,程序运行到这里时这个空间的值还没有意义,所以并没有显示出来(事实上由于 nms 为函数的第一个参数,所以这个 nms 其实是寄存器的值,而不是内存变量),但是当你的程序运行到第二个断点处你就会发现窗口变成了这样:

这是因为后面的代码将函数的参数传入到变量 nms 中了,导致这个变量有初始值了,并且可以看到这值为 0x00 0A,即传入参数为 10,事实上它传入的就是这个值:

但是你也能够看到,变量 Osprey 的值是能够看到的,为什么?由于它是局部静态变量,意味着它有固定地址,在没有初始化的时候就会被默认初始化为 0。

所以使用 Call Stack + Locals 窗口能够很方便的查看局部变量的数据。

下面再说一点关于这个窗口少有人知道的点:

1、能够查看函数的调用顺序:

为了说明这个,我构造几个函数出来:

Osprey_fun3 被 main 函数调用,而 Osprey_fun3 本身调用 Osprey_fun2,Osprey_fun2 调用函数 Osprey_fun1。

如何知道这个关系呢?

看这个:

最新调用的函数在最下面(所谓的压栈),从下往上看就是,Osprey_fun3 调用 Osprey_fun2,Osprey_fun2 调用函数 Osprey_fun1,而主函数 main 这个最上层调用者却并不显示在这里(假如你使用操作系统,假如 uCOS,你是没办法在任务函数中观察到这个的,由于任务函数的调用由操作系统负责)。

2、显示调用关系:

这个功能能够查看当前函数的上层调用函数位置,通过选中某一个函数后右击选择第一个就能够进入上一层调用者的函数内部了(在这里就会跳到 Osprey_fun2 的函数内部)。而第二个是进入你选中函数的内部。

这个功能有什么用?在这里你可能觉得很鸡肋,由于函数之间的调用关系很明显啊,但是在中断处理函数中却非常有用。假如说 USART1_IRQHandler 处理函数,由于这个中断可能在主程序运行的任何时候发生,所以可能在普通函数的任何位置中断它,进而进入到中断处理函数里面,而通过这个功能你就能知道是哪个函数被中断了。

实际上,你可能并不关怀被串口中断的代码位置在哪,但是对于一些错误中断就不一样了,一旦进入错误中断,你就必需找到错误代码位置才行,怎么找?假如常见的硬件错误中断?HardFault_Handler,假如进入这个中断,你该怎么定位?就是使用这个功能了(关于错误中断的处理我会单独用一小节详细介绍)。

4、寄存器变量查看

在单片机中,有一种及其特殊的变量,就是寄存器(不是那些外设寄存器),而能和 CPU 直接打交道的其实就是这些寄存器(所谓的变量操作其实都要首先通过这些寄存器才能进行的,有一个比喻是:CPU 是君王,寄存器就是君王身边的太监,而内存变量就是那些官员了,官员要和君主说话,首先要通过太监传话才行)。这些寄存器没有所谓的地址,所以你没有办法通过取址符&获取一个申明为 register 的变量(寄存器的存取速度超快,所以假如一个变量的使用得非常频繁,那么申明为 register 是一个明智之举,但这只是建议编译器去这么做罢了,编译器听不听就不知道了,所以即便你声明一个变量为 register,它还可能是内存变量),假如这个错误:

那么通过什么方法查看呢?看左边窗口:

所有的寄存器都在这显示,当寄存器的值在发生变化后(与上一次停下时的值比较),就会变更背景颜色(Watch 窗口也是如此)。

这些寄存器的值在一般情况下根本没啥用,但是对于汇编层面的调试却很有用。假如说一条代码,没有提示任何语法错误,但就是和你想要运行的结果不同,那么假如你懂点汇编,再配合这个寄存器调试,你就能很快的定位问题。

5、注意

这里要注意的一个问题是,为了显示窗口的变量能够实时更新数据,须要在View?里勾选这个:

为了更好的观察变量,这些窗口是可以单独关闭或打开的,当然也可以通过鼠标按住窗口后拖动到你想要的地方去(可以看到这里有多个选择的位置):

有的时候窗口弄得比较乱,怎么办?通过这个就可以复位窗口到默认状态:

接下来就是外设窗口展现局部,我把它也归为根底调试,由于它很常用,很有必要进修。敬请期待!

但是当我在后期查找BUG的时候,一哥发现自己主要的调试伎俩已经变成了它,其次才是Watch之类根本调试功能。

其次就是ITM,这个也是调试利器,一定程度上能够缓解 KEIL 命令行的缺少点之处。

所以既然各位在茫茫文章中看到了这篇文章,不如把我认为比较好的技能一起进修了吧。

切记一点,假如时长充裕的话,别收藏了,直接看完,收藏后很大可能你是不会再去看的。

但是看完之后,肯定会对你今后的软件开发提供非常大的帮助。

想要学习单片机的朋友 ,做毕业设计的同学,关注我们,口令一哥,与导师一起学习成长,共同进步,还有更多资料领取。

说了这么多,大家记得留意下方评论第一条(或者私信我)有干货~

单片机C语言基础入门第三章:变量与数据类型

大家好,上次和大家介绍了单片机的开发流程。也就是大家可以根据单片机的开发流程进行单片机程序的烧写,但这仅限于已经写好的程序(假设对C语言还没有了解),接下来这章我们开始介绍单片机中C语言的编写。

、关键字介绍

既然是编程语言那么就得有自己的规则,其实单片机C语言和标准C语言区别并不是很大,毕竟是利用的标准C语言扩展来的。既然说到规则定义,那我们从单片机C语言的关键字说起。

ANSIC的关键字有32个,其中包括9种控制语句,如下:

auto、const、signed、unsigned、static、extern、

char、long、short、int、float、double、void、volatile、register、union、struct、enum、

sizeof、typedef、

if、while、break、case、continue、defualt、do、else、for、goto、return、switch

单片机中除了上面的关键字还有自己的变量:

bit:定义为变量的关键字;

sbit:定义特殊功能寄存器的位变量;

sfr:定义特殊功能寄存器变量;

sfr16:16位特殊功能寄存器变量定义;

除此之外还有单片机特有的存储器类型:

data:直接访问内部数据数据存储器,访问速度最快;

bdata:可位寻址内部数据存储器,允许位与字节混合访问;

idata:间接访问内部数据存储器,允许访问全部内存地址;

pdata:分页访问外部数据存储器,等效汇编movx @Ri访问;

xdata:外部数据存储器访问,等同汇编的movx @DPTR;

code:程序存储器访问,等同汇编的movc @A+DPTR;

、命名规则介绍

所谓的变量就是对存储空间的区域进行命名,其内存区域存储的值可以改变。变量定义必须放在变量使用之前,一般放在函数体的开头部分。要区分变量名和变量值是两个不同的概念,其对应关系如下:

以下未命名的规则:

1. 标识符由字母(A-Z,a-z)、数字(0-9)、下划线“_”组成,并且首字符不能是数字,但可以是字母或者下划线。例如,正确的标识符:abc,a1,prog_to。

2. 不能把C语言关键字作为用户标识符,例如if ,for, while等.

3. 标识符长度是由机器上的编译系统决定的,一般的限制为8字符(注:8字符长度限制是C89标准,C99标准已经扩充长度,其实大部分工业标准都更长)。

4. 标识符对大小写敏感,即严格区分大小写。一般对变量名用小写,符号常量命名用大写。

5. 标识符命名应做到 见名知意 ,例如,长度(length)圆周率(pi)等等。

、变量定义

有了关键字和标识符的规则就可以定义变量了,一般定义变量的表达式如下:

[存储种类] 数据类型 [存储器类型] 变量名表

可以看到在定义格式中除了数据类型和变量名表是必要的,其它都是可选项。

存储种类有四种:自动(auto),外部(extern),静态(static)和寄存器(register),缺省类型为自动(auto)。

数据类型就对应前边介绍的unsigned/signed char、int 、bit、double等数据类型;

而存储器类型就是前面介绍的data、idata、pdata、code等存储器类型,值得注意的是在keil软件中如果忽略存储器类型编译器会根据选择的编译模式SMALL,COMPACT和LARGE去指定变量的存储区域;这三种模式选择可以在右键targetàoption选项中去选择,如下图:

SMALL存储模式把所有函数变量和局部数据段放在8051系统的内部数据存储区这使访问数据非常快,但SMALL存储模式的地址空间受限。在写小型的应用程序时,变量和数据放在data内部数据存储器中是很好的因为访问速度快,但在较大的应用程序中data区最好只存放小的变量、数据或常用的变量(如循环计数、数据索引),而大的数据则放置在别的存储区域。

COMPACT存储模式中所有的函数和程序变量和局部数据段定位在8051系统的外部数据存储区。外部数据存储区可有最多256字节(一页),在本模式中外部数据存储区的短地址用@R0/R1。

LARGE存储模式所有函数和过程的变量和局部数据段都定位在8051系统的外部数据区外部数据区最多可有64KB,这要求用DPTR数据指针访问数据。

四、 几种常用变量的定义方法示例

Ø 常量的定义

常量可以作为一种特殊的变量存在于内存中,只是在程序执行过程中其数值不发生变化,因此通常将称量定义在程序存储器中(有时候有很多常量定义成表格,固化到程序存储器)。

常量的定义有直接定义常量和用预编译的命令来代替的符号常量:

直接定义的常量有直接出现在程序中的数据,比如“2”“a”等。另一种是定义常量比如:const char code a =10;在程序存储器空间定义一个常量a,其值为10,在以后的程序中a的值不能发生变化。

还有一种是利用宏定义的方法来实现,比如:#define Pi 3.14 定义Pi的值为3.14,这样后边程序引用Pi的时候都会被3.14取代,这样的好处是可以方便调试。

Ø 变量的定义

变量根据作用域可以分为局域变量和全局变量,根据其有无static修饰可以分为静态变量和动态变量,因此这两种组合可以分成四个变量:动态全局变量,静态全局变量,动态局部变量,静态局部变量,其作用域以及生命周期的对比如下表格所示:

动态全局 变量 作用域为整个项目,即最终编译成可执行文件的所有文件中均可以使用动态全局变量。生命周期为从程序运行到程序退出,即贯穿整个运行时间。无显式初始化时默认初始化值为0。静态全局变量作用域为当前文件,从定义/声明位置到文件结尾。生命周期为从程序运行到程序退出,即贯穿整个运行时间。无显式初始化时默认初始化值为0动态局部变量作用域为当前函数,从定义位置,到其所在的{}的结束位置。生命周期为从函数调用到函数退出。无显式初始化时默认初始化值为随机值。静态局部变量作用域为当前函数,从定义位置,到其所在的{}的结束位置。生命周期为从程序运行到程序退出,即贯穿整个运行时间,当下次函数调用时,静态局部变量不会被再次初始化,而是沿用上次函数退出时的值。无显式初始化时默认初始化值为0。

对于初学者来说开始可能对这些变量的作用域和生命周期很难理解,是在理解不了就先记着,在我们介绍完函数后再回头看这些东西就有一个比较深刻的认识。

下面就主要举例介绍各个不同的变量以及应用,首先是对于单片机中常用的变量进行定义:

1. Bit定义位变量

bit位变量有点类似于高级语言中的boolean变量,其值只能取0和1。在单片机中经常用作标志位,比如:bit flag;定义一个flag标志位

2. sbit 定义可寻址变量

sbit是定义可寻址对象,定义特殊寄存器的某位。在实际中经常来定义端口的位的。其定义数据的方法:(1) sbit 位变量名=地址;(2) sbit 位变量名=特殊寄存器^位位置;(3) sbit 寄存器字节地址^位位置;比如:

sbit LED=P1^1//表示P1端口的第一位定义为LED;其中P1是特殊寄存器的名称(实际使用sfr关键字定义的)。sbit LED=0x91//0x91的位地址就是P1^1;sbit LED=0x90^1//0x90是P1寄存器的字节地址,0x90^1表示P1的第一位,上面的三个定义是一样的,只是表达方式不同,如果用地址来直接定义那么首先对单片机非常了解,而直接用寄存器的名称去定义的话就得包含单片机的头文件。

3. sfr定义特殊寄存器变量

sfr来定义8位寄存器变量,一般在头文件中经常看到这种定义方式,比如我们打开reg51.h的头文件,我看可以看到如下的定义方式:

sfr P0 = 0x80;

sfr P1 = 0x90;

sfr P2 = 0xA0;

sfr P3 = 0xB0;

其中P0、P1、P1、P3是寄存器变量的名称,而后面的一串数字是这个寄存器的地址,因此我们在编写程序的时候直接写P1、P2等,但是如果在编写程序的时候没有包含头文件直接写P1、P2等就会出现报错的信息,当然如果要将报错的信息解除的话要么包含头文件,要么自己去重新定义这些变量。当然有些时候也会看到sfr16,这个是用来定义长度为16位的寄存器变量的。

4. volatile修饰变量

volatile修饰的变量表示是随时可以发生变化的,因此当编译器用到这个值的时候会重新去调用其数值,而不是使用保存在寄存器的备份。Volatile一般来修饰寄存器变量,因为寄存器里面存数的数据会经常发生变化。Volatile只是一个修饰关键字因此可以用数据类型放在前边修饰比如:const volatile a;表示a是一个volatile类型的常量,说是常量是因为在程序运行中程序不能更改这个数值,但是它可能会意想不到的发生改变。在头文件中常来定义寄存器变量,比如:

#define PWMC (*(unsigned int volatile xdata *)0xfff0)

#define PWMCH (*(unsigned char volatile xdata *)0xfff0)

#define PWMCL (*(unsigned char volatile xdata *)0xfff1)

#define PWMCKS (*(unsigned char volatile xdata *)0xfff2)

通过指针来定义寄存器变量。

5. union定义变量

union定义联合体变量,他和后面要讲述的struct不一样的地方在于,union变量在内存的长度是由联合体内成员变量中长度最长的变量来决定的,他们共同占用一块内存。然而struct变量是struct中变量长度的总和。比如:union ADC{unsigned intADC_VAL;unsigned char AD[2]}其中DAC_VAL是两个自字节长度的变量,而AD[2]是单字节变量的数组,他们的排列如下图所示:

ADC_VALMSBLSBADAD[0]AD[1]

可以看到ADC_VAL的长度和AD[]数组的长度一样,因此可以认为ADC_VAL是用两个8位的数据合成的一个无符号的int数据,因此可以直接调用这个数据即可。

6. struct 定义的变量

Struct定义变量可以有很多的成员,而这些成员的地址是连续的,因此这在一些定义库函数的时候有个很大的方便只要得到结构体的首地址就可以得到一系列的寄存器的地址,这样方便管理。具体的示例在以后可以讲解(在STM32库函数中可以找到)。

在这主要介绍一些常用的变量,对于int,char,等变量的定义和初始化和标准的C语言都相同,最容易出错的是定义变量的类型和采集的数据之间存在不一致,这样调试的时候编译器不会报错,这就需要我们在编程的时候认真。

总结一下:

本次课程的主要内容是,介绍了C语言的关键字以及变量标识符的规则,同时着重介绍了单片机中C语言的变量和定义的问题,最后通过举例说明了具有典型意义的变量的定义。

相关问答

单片机局部变量 在栈里怎么访问?

在单片机中,局部变量一般是存储在栈中的。栈是一种后进先出的数据结构,每个函数都有自己的栈帧。当函数被调用时,栈指针会移动到下一个空闲位置,并将函数的局...

单片机变量 由什么组成?

单片机的变量主要由两个部分构成:一个是变量名,一个是变量值。每个变量都有一个变量名,AD7656BSTZ-1在内存中占有一定的内存单元(地址),并在该内存单元中...

单片机 delay()函数?

定义一个延时xms毫秒的延时函数voiddelay(unsignedintxms)//xms代表需要延时的毫秒数{unsignedintx,y;for(x=xms;x》0;x--)for(y=110;...

单片机 1k等于多少字节?

以大多数初学者接触的c51单片机为例,1k的内存相当于1024字节,即2的10方个字节。其实不止c51单片机,所有的控制板,它的内存进制都是这样的,那么,初学者想要...

单片机 C语言设置 变量 存储在什么地方?

变量在RAM。RAM是数据存储器,用来保存运行中的数据,掉电后数据消失。EEPROM跟FLASH都是程序存储器,保存程序代码跟不分配内存的常量,两者的区别在于写入方式...

c51char占几个字节?

对于该款单片机,int类型的变量占据2个字节,char类型的占据1个字节。不过一般大家为了节约空间,大部分会使用char类型,同时89C51是一款8位的单片机,所以推荐使...

单片机 中BIT是什么意思?

在单片机中,BIT是二进制位(BinaryDigit)的缩写,它是计算机数据存储的基本单位,表示了一个二进制数中的一位,只能存储0或1两种状态。在单片机编程中,BIT通...

c51 变量 定义的四个要素?

[存储种类]数据类型[存储类型]变量名其中:存储种类与标准C语言相同,包括:自动型(auto)、外部型(extern)、静态型(static)、寄存器型(register)。数据类...

单片机 多线程调度原理?

2.每个线程都需要分配一个独立的栈空间,以便保存线程的执行状态、局部变量等信息。3.线程执行时,总是从它的栈顶部开始执行。当一个线程的执行被中断时,它...

单片机 C语言数组最多能定义多少个元素?

但在单片机c语言程序中,实际上还是有限制的,原因在于单片机的存储空间是有限的,数组最多能有几个元素取决于单片机型号。你定义了一个全局的数组,unsigned...

猜你喜欢