物联网创客指南:EFM32 MCU设计的最佳实践和除错技巧(四)

Lee_的头像
Lee_ 发布于:周一, 03/27/2017 - 17:09 ,关键词:

来源:SiliconLabs微信公众号

在上一节中,您了解了一些软件开发的基本实践和提示。我们在本节继续讨论这些主题,包括内联函数,读取和写入闪存(包括用户页面闪存),以及如何避免缓冲区溢出。我们将讨论类型转换的潜在问题,以及如何在出现问题时用配置锁解决问题。

根据下列重点步骤写好代码,一切都会更好!――续
(本系列1到8部分请参加往期文章:http://community.silabs.com/t5/Official-Blog-of-Silicon-Labs/Essential-B...

九 内联函数的影响与限制

如果您在函数的前面定义关键字“inline”,则告诉编译器您希望将该函数复制到代码中,就像是直接输入代码而不是作为函数。作为程序员,你希望这可以加快代码执行速度。每当在C中调用函数时,输入参数的内容,在函数完成时,代码应该与返回地址一起被放置到栈上,然后代码跳转以执行函数。通过内联函数,您打算绕过该将数据移入和移出堆栈的时间。然而,C编译器或多或少地会用到这一技巧,因为小的函数将由编译器自动内联。编译器将忽略对大型函数的这种内联建议,编译器已经确定这些函数不会从加速中获益。
内联函数不能包含本地静态变量。由于函数内容要在代码中替换,因此对函数作用域私有的静态变量没有意义。内联函数没有作用域。

十 读写闪存的区别

在EFM32中从闪存的任何区域读取数据非常简单。可以使用指向内存地址的指针访问闪存,如:
volatile uint8_t* address = 0x1000;
uint8_t my_value =*address;

通过在地址变量前面放置星号,您可以解除引用指针,这将提供系统地址0x1000的字节大小的内容。从闪存检索的字节数将根据my_value的变量类型而有所不同。例如,定义为uint64_t的my_value将从地址0x1000到0x1007同时获取8个字节。因此,执行my_value ++作为64位无符号整数指向0x1008。

写入闪存是一个更复杂的业务。所有闪存将存储器组织成“页面”,其将可写入单元组合在一起,成为只能作为一组擦除的chuck。因此,如果要写入单个字节的非易失性存储器,则必须先擦除整个页面。对于程序员来说,这意味着你必须首先读取整个页面的内存,将它存储在RAM中,擦除页面,然后将修改的字节写回整个页面,你在操作的中间不能失去电源,否则你将永远失去了那个页面的数据。通常也有时间含义,因为擦除和写入页面比读取值需要更多的时间。当一页闪存被擦除时,该页的所有内容被复位为1,或在字节大小的存储器查看器中为0xFF。因此,当你写一个字节到内存,你只是清除那些不是1的位。
要写入EFM32闪存,另一个要求是,写入闪存的功能必须驻留在RAM中,而不是在正常执行代码所在的闪存中,在那里相同的代码正被执行。这要求任何意图写入闪存的函数也将被执行的RAMFUNC声明,就像在ramfunc.h库实用程序文件中定义的那样。

十一 将持久数据存储在Flash用户页面闪存中

主闪存从地址0开始,并根据EFM32模型有着不同的容量上限。有关特定芯片的主闪存结束的位置,请查看参考手册。除了内置闪存,还有一个内存区域,用于外部闪存扩展,其位于内部主存储器之外,最高可达24 MB。上一节中所述的任何读取或写入主闪存的操作都可用于访问主闪存。但是,每次对芯片编程时,或者如果器件被擦除,存储在程序使用的区域中的数据将被新的代码覆盖。因此,当执行闪存更新过程时,例如,在引导加载器中,通常使用对主闪存的读取或写入。您当然可以随意使用位于程序大小以上的闪存,只要您意识到如果芯片通过器件擦除操作或JTAG编程序列擦除,它将被擦除。

EFM32提供了一个单独的闪存存储器页,它不会被器件擦除操作或JTAG编程操作擦除。闪存的这个区域称为用户页,并从地址0x0FE00000开始。可用的内存量是单页,并且闪存页面的大小根据EFM32芯片的型号而变化。有关闪存页面的大小,请查看数据手册或参考手册。您可以将此页面用于从设备擦除操作(例如设备序列号,设备校准数据或每个设备特定的任何数据)开始,从引导到引导的设备上保留的任何内容。

十二 避免缓冲区溢出和可能造成的破坏

当使用计算机开发代码时,您很幸运能在具有操作系统,内置文件系统和显示屏幕的系统上开发。当你的软件遇到麻烦,操作系统开始采取行动,帮助你,并通知你,你试图访问超出程序范围内的内存,或者你造成了一些故障。在开发嵌入式代码时,你没有这些帮助。没有操作系统来监视事情,以确保你的程序保持在一个很好定义的边界。如果你的代码决定写数据到地址零或一百万,MCU将尝试做它被告知的任何事情。它不能告诉你,你不知道你在做什么,因为没有限制你的程序可以做什么。你种情况有好坏两个方面。

嵌入式开发人员面临的一个大问题是缓冲区溢出。在嵌入式中工作意味着对存储器地址的大量直接操作。如果你的程序开始表现不规律,或者你看到一个看起来改变其值而不设置的变量,缓冲区溢出可能是罪魁祸首。

首先要注意的事是显而易见的。如果定义一个长x个字节的数组,请不要写超出x-1。如果在名为foo的数组中有x个项目,则只能将foo [0]映射到foo [x-1]。坏事是,如果你寻址foo [x],foo [x + 1],等等,你的代码仍然有效。 MCU将满意地写入任何其他变量恰好超出foo [x-1]的位置。然后你的项目开始出现问题。

当你调整指针,而将指针转换为不同的类型时,这也可以失去控制。例如:

void some_function()
{
// Array of just 4 bytes
uint8_t my_array[4];

// Pointer to group of 4 bytes
uint32_t * my_ptr =(uint32_t *) my_array;
// foo is assigned all 4 bytes of my_array,
int foo =*my_ptr;

// then my_ptr is incremented by one
// which to my_ptr's type means 4 bytes
// So my_ptr is now sitting out of bounds
my_ptr++;
}

以上演示了为什么使用在创建时是单向键入的结构,但稍后由不同类型的指针使用需要小心的原因。你可以做类型转换,但并不意味着你应该做。

另一种可以遇到缓冲区溢出问题的方法是忘记局部范围的限制。在C中,函数完成的大部分工作都是在传递给函数的指针上执行的。C函数的返回值通常被限制为“状态”字节,因为简单的值很容易从堆栈中的C函数返回,而其他结构很难返回。例如,一个简单的数组是局部的一个函数:

// This function will generate a compiler warning:
// warning: function returns address of local variable
int* some_function()
{
int my_array[4];
return my_array;
}
// This function has no warning!
int* some_other_function()
{
int my_array[4];
int* foo = my_array;
return foo;
}

一旦函数返回,分配给my_array的内存将被系统回收并分配给需要内存的下一个东西。如果从C函数返回一个指向本地数组的指针,编译器不会警告你,MCU会做你所要做的事情。如果在函数返回之后没有新的内存分配,代码有时会工作。你的解决方案将是间歇性的!

// This is a better way, pass pre-allocated pointers intothe function
// “int my_array[]” and “int * my_array” are identicalfor function parameters
void some_new_function(int my_array[])
{
my_array[0]= 1;
}

十三 精通类型转换

当你在嵌入式开发中“触及核心”时,你会经常使用小量的信息,所以你最终经常使用有限的可变大小(例如8位寄存器)寄存器,从一种类型转换到另一种类型。本节的一些例子展示了在uint8_t类型和uint32_t类型上增加指针时,你必须注意对地址的影响。此外,还必须了解C编译器如何解释和转换类型。

将类型从一种类型转换到另一种类型实际上不执行任何转换。它不是一个函数,而只是一种告诉编译器如何解释数据的方法。当你分配不同类型的变量,C编译器应该警告你,但编译器并不总是警告你这一点。例如,考虑将有符号整数转换为无符号整数的情况,如下所示:
int8_t a = 0; // Range is -128 to +127
uint8_t b = 0; // Range is 0 to 255
long c = 0; // Range is huge, both positive and negative
a =-64; // a can be negative
b = 64; // b can only be positive
c = a + b; // c is 0
a = b; // Converting a signed to unsigned is OK, in this case
c = a - b; // c is again 0
b = 128; // b is now bigger than a can represent
a = b; // a is now converted to -128
c = a - b; // c = -128 - 128 = -256!

这个例子显示了当你的变量变大时会遇到的麻烦。事情工作正常一段时间,但然后当你的变量超过范围,他们开始失败。确保在转换类型时知道您想要什么。大小为8的uint不能容纳超过0到255范围的任何内容,8位的int不能容纳超出-128到+127的任何内容。简单地把一个大的uint8_t作为一个int8_t不能将你的int8_t变为一个更大的int。它仍然限制在+127。如果你想增加它的范围,你必须使用更大的类型,如int16_t。

十四 使用配置锁来缓解故障代码的问题

有时候,当嵌入式工程师对指针进行数学运算时,我们会使用我们的软件来影响硬件。一种保护你的代码不因缓冲区溢出和其他糟糕的指针算法而造成严重错误的方法是使用配置锁。许多EFM32外设上可用的配置锁需要一个特殊值写入配置锁寄存器,以允许更改外设配置。这防止错误行为代码改变设备的整体配置。

编码成功的关键

从现有示例开始,然后使用命名良好的变量和大量注释记录您的更改。进一步了解C语言,并了解在何处以及如何声明变量和函数。不要尝试写入尚未擦除的闪存或超出变量可访问范围的内存。

在下一节中,我们将学习如何在SimplicityStudio IDE中更好地控制软件构建。

围观 316