转自 | 瑞萨嵌入式小百科
在C语言编程过程中,由于计算需要,会使用各种各样的变量,用于给需要访问的地址取个名称,方便编程中使用,代码维护者也容易理解。
这里先给大家分享一个案例,让大家意识到变量赋初始值的重要性。
案例:
某用户在基于瑞萨的MCU:RA6T2做开发时,发现一个问题,MCU发出的CAN数据帧总是莫名其妙的出错。
比如应用中明明只使用了CAN的扩展帧,但是使用捕捉工具总是能捕捉到远程帧,出现远程帧的情况毫无规律可言,有时添加一个定时器中断,该现象就不会出现了,有时修改了代码里某处跟CAN没有任何关系的代码,该问题又会出现,过了两周时间调试无果。
在介入Debug时发现,他使用的是CAN扩展帧,扩展帧使用29位ID标识符,而且对ID区数据定义了一个如下结构体:
z0i3c45gg0f64025259010.png
他在需要发送CAN帧时,申请一个如上结构体的临时变量can_id,在把can_id.id赋值后,再把该变量的地址传递给CAN的发送函数,在发送函数里使用如下语句把id的数据写入CAN的发送消息缓冲寄存器:
23ryrxoxhne64025259110.png
如下图,其中第30位的0表示数据帧,并不是远程帧,31位的1表示扩展帧。
u5v0z3tnp3s64025259210.png
用户是把can_id的所有数据赋值给了CFDTMID0寄存器,假如can_id.dummy中第二个位是1,会有什么后果呢?CFDTMID0.TMRTR=1,即CAN会发送远程帧。
用户又问:我没有给dummy赋值啊,为什么dummy的第二个位会变成1呢?这就是问题所在了,就是因为他没有给can_id.dummy赋值,所以can_id.dummy有可能为任意的值。下面详细分析一下,为什么这个局部变量的值会随意变化。”
大家知道,变量根据存储类型和用途,一般可以分成:全局变量和局部变量。全局变量,就是指分配了固定地址的变量,全局变量可以在整个代码范围内使用。我们在申请全局变量时,有时对它赋一个初始值,也时也不会赋初始值,在代码上可能看不出有什么区别,但是编译器在编译程序时,是区别对待他们的。
对于有初始化的变量,编译器还需要在Code Flash里(代码存储区)分配一段空间,把变量的初始值全部存储在该区域里,并且在MCU的启动代码里插入一段程序,把这些Code Flash区的初始值拷贝到变量对应的RAM地址中。假如上面的can_id是全局变量,并且申明变量的同时并按下图赋初始值:
3jtpdm4xhec64025259310.jpg
这时can_id.dummy=0,如果代码中用户没有再赋值,它的值也不会变化,这样就不会发生用户的那个远程帧的问题了。对于没有赋初始值的全局变量,编译器只是分配RAM的地址,并不会修改RAM地址里的数据,那么这个变量的值就会依赖于MCU启动时RAM里的值了。为了避免未赋值的全局变量出现上述的问题,我们一般会在MCU启动代码里插入未赋初始值全局变量的清零操作,相当于做了一个未赋初始值的全局变量的初始化赋值操作。
像上面的案例,can_id申请的是局部变量,这又是什么情况呢?
因为MCU的RAM资源有限,为了最大限度的利用RAM,MCU会提前分配一块RAM区域,叫堆栈区,这块区域大家共用,对于只需要在某个函数内使用的变量,引入了局部变量概念。在开始执行该函数时,才从堆栈里分配地址给局部变量使用,函数执行结束后,该变量占用的RAM区域被堆栈回收,当下次再调用该函数,再重新分配RAM。因此对于局部变量,每次申请到的地址是不同的,该地址很可能是其它函数使用过并改写数据了的,因此每次函数调用时can_id.dummy的数据是不确定的。因为堆栈区里的数据是被反复利用的,即使MCU的初始化代码对堆栈区域做清零处理,也是没有意义的。
由此看来,局部变量在申请的时候赋一个初始值,是非常有必要的。虽然有时候赋初始值没有用,但是出现问题时常常是致命的,而且也是非常难以定位的,你可能觉得我的代码里后面肯定会赋值的,但是后面维护该项目的其他工程师并不一定意识到这一点。像类似上面的案例,我在其他用户当中也是经常见到的。因此软件工程师在编程的时候,一定要养成局部变量赋初始值的习惯。
------------ END ------------
335xr01nqrd64025259410.jpg
MCU代码自检和诊断的常规方法
y3iq0oulfuh64025259510.jpg
一年一度MCU/MPU工业技术研讨会亮点来了!
3krcnupr5ar64025259610.jpg
瑞萨MCU这几年的崛起之路 |