嵌入式开发中的内存管理与预处理技术
1. UART 相关配置与初始化
在嵌入式开发中,UART(通用异步收发传输器)是常用的通信接口。以下代码展示了 UART 的 GPIO 初始化和反初始化操作:
// Alternate function -- that of UART
GPIO_InitStruct.Alternate = GPIO_AF1_USART2;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/**
* Magic function called by HAL layer to de-initialize the
* UART hardware. Something we never do, but we put this
* in here for the sake of completeness.
*
* @note: Only works for UART2, the one connected to the USB serial
* converter
*
* @param uart The UART information
*/
void HAL_UART_MspDeInit(UART_HandleTypeDef* uart)
{
if(uart->Instance == USART2)
{
/* Peripheral clock disable */
__HAL_RCC_USART2_CLK_DISABLE();
/*
* USART2 GPIO Configuration
* PA2 ------> USART2_TX
* PA3 ------> USART2_RX
*/
HAL_GPIO_DeInit(GPIOA, GPIO_PIN_2|GPIO_PIN_3);
}
}
这个代码片段中,
GPIO_InitStruct.Alternate
被设置为
GPIO_AF1_USART2
,用于配置 GPIO 的复用功能为 UART2。
HAL_UART_MspDeInit
函数用于反初始化 UART2 的硬件,包括禁用时钟和释放 GPIO 引脚。
2. 闪存中多配置项的管理
在嵌入式系统中,我们常常需要在闪存中存储多个配置变量。然而,闪存的特性决定了在写入单个字之前必须擦除整个页面。以下是管理多个配置变量的步骤:
1. 将所有配置变量保存到 RAM 中。
2. 在 RAM 中更新需要更改的值。
3. 擦除闪存中的所有配置变量(即擦除闪存页面)。
4. 将 RAM 中的版本复制回闪存。
以下是一个示例代码,展示了如何声明配置结构并更新其中的值:
struct config {
char name[16]; // Name of the unit
uint16_t sensors[10]; // The type of sensor connected to each input
uint32_t reportTime; // Seconds between reports
// ... Lots of other stuff
};
struct config theConfig __attribute__((section ".config"));
// The configuration
static void updateReportTime(const uint32_t newReportTime) {
// <Prepare flash>
struct config currentConfig = config;
currentConfig.reportTime = newReportTime;
// <Erase flash>
writeFlash(&config, ¤tConfig, sizeof(currentConfig));
// <Lock flash>
}
3. 闪存相关问题及解决方案
闪存存在一些问题,主要包括写入时需擦除整个页面、写入时间长以及可能因系统断电或复位导致写入不完整,还有内存磨损问题。
为了解决写入不完整的问题,可以使用双配置区(主配置区和备份配置区),每个配置区包含一个校验和。程序首先尝试读取主配置,如果校验和错误,则读取备份配置。
对于内存磨损问题,由于不同类型的闪存允许的擦写周期不同(通常在 100,000 到 1,000,000 次之间),因此对于频繁更改的配置,不建议使用闪存存储,可考虑添加外部内存芯片。
4. 现场定制示例
假设我们为一家生产报警器的公司工作,客户可能希望在报警器启动时显示自己的标志。我们可以为标志预留一段内存:
MEMORY
{
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 52K
LOGO (r) : ORIGIN = 0x8000000 + 52K, LENGTH = 8K
CONFIG (rw) : ORIGIN = 0x8000000 + 60K, LENGTH = 4K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 8K
}
为了让客户能够自己更新标志,我们可以提供电缆和软件,让他们通过串口将数据写入
LOGO
内存。
5. 固件升级
在运行软件的同时升级软件是一项具有挑战性的任务。一种简单的方法是将闪存分为三个部分:
- 引导加载程序
- 程序区 1
- 程序区 2
引导加载程序是一个小型程序,不会被升级。程序区包含完整的程序版本、版本号和校验和。引导加载程序的任务是根据校验和和版本号选择要运行的程序区:
- 如果程序区 1 校验和错误,程序区 2 校验和正确,则使用程序区 2。
- 如果程序区 1 校验和正确,程序区 2 校验和错误,则使用程序区 1。
- 如果两个程序区校验和都正确,则使用版本号最高的程序区。
- 如果两个程序区校验和都错误,则闪烁紧急指示灯,表示系统故障。
以下是一个简单的流程图,展示了引导加载程序的决策过程:
graph LR;
A[开始] --> B{程序区 1 校验和是否正确};
B -- 是 --> C{程序区 2 校验和是否正确};
B -- 否 --> D{程序区 2 校验和是否正确};
C -- 是 --> E{程序区 1 版本号是否大于程序区 2};
C -- 否 --> F[使用程序区 1];
D -- 是 --> G[使用程序区 2];
D -- 否 --> H[闪烁紧急指示灯];
E -- 是 --> I[使用程序区 1];
E -- 否 --> J[使用程序区 2];
6. 预处理器的作用
基本的 C 编译器有一些无法完成的任务,因此引入了预处理器。预处理器主要是一个宏处理器,它可以根据特定条件替换、包含或排除文本。例如:
#define SIZE 20 // Size of the array
int array[SIZE]; // The array
for (unsigned int i = 0; i < SIZE; ++i) {
当
SIZE
被定义为 20 时,预处理器会将代码中的
SIZE
替换为 20。
7. 简单宏
简单宏是一种模式替换机制,使用
#define
指令定义。例如:
#define SIZE 20
在代码中,所有的
SIZE
都会被替换为 20。但预处理器只是简单的文本替换,不理解 C 语法和算术运算。例如:
#define SIDE 10 + 2 // Size + margin
printf("Area %d\n", SIDE * SIDE);
经过预处理器处理后,会变成:
printf("Area %d\n", 10 + 2 * 10 + 2);
为了避免这种问题,当定义复杂常量时,应使用括号:
#define SIDE (10 + 2) // Size + margin
或者使用
const
关键字:
const unsigned int SIDE = 10 + 2; // This works.
8. 参数化宏
参数化宏允许我们为宏提供参数。例如:
#define DOUBLE(x) (2 * (x))
printf("Twice %d is %d\n", 32, DOUBLE(32));
但使用参数化宏时需要注意括号的使用,否则可能会导致意外的结果。例如:
#define DOUBLE_BAD(x) (2 * x)
value = DOUBLE_BAD(1 + 2); // 结果为 4,而不是 6
同时,应避免在参数化宏中使用
++
或
--
操作符,因为 C 语言的执行顺序规则在这种情况下可能会导致结果不确定。如果需要类似的功能,建议使用内联函数:
static inline int CUBE_INLINE(const int x) {
return (x * x * x);
}
9. 代码宏
我们可以使用
#define
定义代码宏。例如:
#define FOR_EACH_VALUE for (unsigned int i = 0; i < VALUE_SIZE; ++i)
int sum = 0;
FOR_EACH_VALUE
sum += value[i]
但这种代码宏存在一些问题,例如变量的来源不明确。更常见的是定义一个模拟短函数的宏,如
DIE
宏:
// Defined badly
#define DIE(why) \
printf("Die: %s\n", why); \
exit(99);
但这个宏在某些情况下会导致问题,例如在
if
语句中使用时。为了解决这个问题,可以使用
do/while
语句:
#define DIE(why)
do { \
printf("Die: %s\n", why); \
exit(99); \
} while (0)
10. 条件编译
条件编译允许我们在编译时改变代码内容。经典的应用场景是区分调试版本和生产版本的程序。例如:
#ifdef DEBUG
printf("Debug version\n");
#endif // DEBUG
如果定义了
DEBUG
符号,则会编译并执行
printf
语句;否则,该语句将被忽略。为了简化代码,可以使用宏定义:
#ifdef DEBUG
#define debug(msg) printf(msg)
#else // DEBUG
#define debug(msg) /* nothing */
#endif // DEBUG
int main()
{
debug("Debug version\n");
debug("Starting main loop\n");
while (1) {
debug("Before process file \n");
processFile();
debug("After process file \n");
总之,在嵌入式开发中,合理管理内存和使用预处理器可以提高代码的可维护性和灵活性。通过了解闪存的特性和预处理器的工作原理,我们可以更好地应对各种开发挑战。
嵌入式开发中的内存管理与预处理技术
11. 预处理器应用总结
预处理器在嵌入式开发中有着广泛的应用,以下是对前面介绍的预处理器相关内容的总结表格:
| 类型 | 示例 | 注意事项 |
| ---- | ---- | ---- |
| 简单宏 |
#define SIZE 20
| 预处理器仅做文本替换,定义复杂常量时用括号或
const
避免运算错误 |
| 参数化宏 |
#define DOUBLE(x) (2 * (x))
| 注意括号使用,避免
++
或
--
操作符,可考虑用内联函数替代 |
| 代码宏 |
#define FOR_EACH_VALUE for (unsigned int i = 0; i < VALUE_SIZE; ++i)
| 变量来源不明确问题,用
do/while
解决部分代码宏在特定语句中的问题 |
| 条件编译 |
#ifdef DEBUG ... #endif
| 可简化代码,区分调试和生产版本 |
12. 内存管理与预处理器结合的实际应用
在实际的嵌入式开发中,内存管理和预处理器常常结合使用。例如,在不同的硬件平台上,我们可能需要根据平台特性调整配置变量的存储方式和代码的编译内容。
假设我们有一个项目,需要支持两种不同的硬件平台 A 和 B。我们可以使用条件编译来根据不同的平台选择不同的配置:
#ifdef PLATFORM_A
#define CONFIG_SIZE 10
#else // PLATFORM_B
#define CONFIG_SIZE 20
#endif
struct config {
char name[16];
uint16_t sensors[CONFIG_SIZE];
uint32_t reportTime;
};
struct config theConfig __attribute__((section ".config"));
static void updateConfig() {
// 根据不同平台进行不同的配置更新操作
#ifdef PLATFORM_A
// 平台 A 的配置更新逻辑
#else // PLATFORM_B
// 平台 B 的配置更新逻辑
#endif
}
13. 编程问题及解决方案探讨
在开发过程中,我们可能会遇到一些编程问题,以下是一些常见问题及解决方案:
问题 1:修改配置程序使
CONFIG
段不始于页边界会怎样?
当
CONFIG
段不始于页边界时,可能会导致闪存擦除和写入操作变得复杂。因为闪存的擦除操作通常是以页为单位进行的,如果配置段跨越了页边界,那么在更新配置时,可能需要擦除多个页,并且在写入时需要处理页之间的衔接问题。
问题 2:如何修改配置程序以打印完整的复位编号而非一位数?
我们可以修改配置程序中的打印逻辑,使用合适的格式化字符串来打印完整的复位编号。例如:
// 假设复位编号存储在 resetNumber 变量中
printf("Reset number: %d\n", resetNumber);
问题 3:如何利用链接器脚本符号打印文本区域的大小?
链接器脚本会定义一些符号来表示内存区域的起始和结束位置。我们可以通过读取这些符号的值来计算文本区域的大小。以下是一个示例:
extern char __text_start;
extern char __text_end;
int main() {
size_t textSize = &__text_end - &__text_start;
printf("Text area size: %zu bytes\n", textSize);
return 0;
}
我们可以使用
arm-none-eabi-size
命令来验证计算结果。
问题 4:如何打印分配的栈空间大小和剩余栈空间?
打印分配的栈空间大小可以通过链接器脚本中的符号来实现,类似于计算文本区域大小的方法。而打印剩余栈空间则需要读取当前栈指针的值。以下是一个示例:
extern char __stack_start;
extern char __stack_end;
int main() {
size_t stackSize = &__stack_end - &__stack_start;
printf("Allocated stack space: %zu bytes\n", stackSize);
// 读取当前栈指针的值
char* currentStackPtr;
asm("mov %0, sp" : "=r" (currentStackPtr));
size_t remainingStack = &__stack_end - currentStackPtr;
printf("Remaining stack space: %zu bytes\n", remainingStack);
return 0;
}
14. GNU 工具链命令介绍
GNU 工具链提供了一些有用的命令来分析二进制文件,以下是一些常用命令的介绍:
-
objdump
:用于转储目标文件的信息,例如反汇编代码、符号表等。可以使用以下命令查看目标文件的反汇编代码:
objdump -d your_file.o
-
nm:列出文件中的符号。例如,查看目标文件中的符号列表:
nm your_file.o
-
ar:用于创建库文件或从库文件中提取信息和文件。例如,创建一个静态库:
ar rcs libyourlib.a your_file1.o your_file2.o
-
readelf:显示 ELF(可执行和可链接格式)文件的信息。例如,查看 ELF 文件的头信息:
readelf -h your_file.elf
15. 总结与展望
在嵌入式开发中,内存管理和预处理器是非常重要的技术。合理的内存管理可以提高系统的性能和稳定性,而预处理器则可以增强代码的可维护性和灵活性。
通过了解闪存的特性和管理方法,我们可以更好地处理配置变量的存储和更新。同时,掌握预处理器的各种用法,如简单宏、参数化宏、代码宏和条件编译,可以使我们的代码更加高效和易于扩展。
未来,随着嵌入式系统的不断发展,对内存管理和代码优化的要求也会越来越高。我们需要不断学习和掌握新的技术和方法,以应对日益复杂的开发挑战。例如,随着物联网的兴起,嵌入式设备需要处理更多的数据和通信任务,这就要求我们更加精细地管理内存资源,并优化代码以提高系统的响应速度和能源效率。
以下是一个简单的流程图,展示了嵌入式开发中内存管理和预处理器的主要流程:
graph LR;
A[开始开发] --> B[内存规划];
B --> C[配置变量存储];
C --> D[闪存操作];
D --> E[预处理器处理];
E --> F[代码编译];
F --> G[调试与优化];
G --> H[部署与维护];
总之,嵌入式开发是一个充满挑战和机遇的领域,我们需要不断探索和实践,以提高自己的开发水平。
超级会员免费看
1401

被折叠的 条评论
为什么被折叠?



