目录
最近在看《FreeRTOS 内核实现与应用开发实战》, 里面有这样一句代码:
#define portSTACK_TYPE uint32_t
typedef portSTACK_TYPE StackType_t;
StackType_t *pxTopOfStack;
uint32_t ulStackDepth;
/* 获取栈顶地址 */
pxTopOfStack = pxNewTCB->pxStack + (ulStackDepth - (uint32_t)1);
/* 向下做 8字节对齐 */
pxTopOfStack = (StackType_t *)(((uint32_t)pxTopOfStack) & ( ~((uint32_t)0x0007)));
/* 将栈顶指针向下做 8 字节对齐。
在 Cortex-M3(Cortex-M4 或 Cortex-M7)内核的单片机中,因为总线宽度是 32 位的,
通常只要栈保持 4 字节对齐就行, 可这样为啥要 8 字节?难道有哪些操作是64 位的?
确实有,那就是浮点运算,所以要8 字节对齐
(但是目前我们都还没有涉及到浮点运算,只是为了后续兼容浮点运行的考虑)。
如果栈顶指针是 8 字节对齐的,在进行向下 8 字节对齐的时候,指针不会移动,
如果不是 8 字节对齐的,在做向下 8 字节对齐的时候,就会空出几个字节,不会使用,
比如当 pxTopOfStack 是33,明显不能整除 8,进行向下8 字节对齐就是 32,
那么就会空出一个字节不使用。 */
代码分析
这行代码的作用是将栈顶指针 pxTopOfStack 向下对齐到 8 字节边界。具体步骤如下:
-
(uint32_t)pxTopOfStack:
将指针 pxTopOfStack 转换为 uint32_t 类型(32 位无符号整数),以便进行位操作。
-
~((uint32_t)0x0007):
0x0007 的二进制表示为 0000 0000 0000 0111。
-
取反操作
~
将其变为1111 1111 1111 1000
,即0xFFFFFFF8
。这个掩码的作用是将低 3 位(0x0007)清零。
具体什么是“低 3 位”呢?
在计算机中,内存地址通常用二进制表示。例如,一个 32 位的内存地址可以表示为:31 0 +--------------------------------+ | 0000 0000 0000 0000 0000 0000 0000 0111 | // 0x00000007 +--------------------------------+
这里的“低 3 位”指的是地址的最右边 3 位(即第 0 位到第 2 位)。在上面的例子中,低 3 位是 111(二进制)。
-
&
操作:将
pxTopOfStack
的地址与掩码0xFFFFFFF8
进行按位与操作。结果是将 pxTopOfStack 的低 3 位清零,从而实现向下对齐到 8 字节边界。
为什么清零低 3 位可以实现 8 字节对齐?
1)8 字节对齐的含义
8 字节对齐是指内存地址必须是 8 的倍数。
在二进制中,8 的倍数的地址的低 3 位一定是000
,因为 8 的二进制表示是1000
(即2^3
)。
2)清零低 3 位的作用
如果一个地址的低 3 位是000
,那么这个地址一定是 8 的倍数。
通过清零低 3 位,我们可以将任意地址向下舍入到最近的 8 字节对齐的地址。
3)举例说明
假设有一个地址0x20001007
,它的二进制表示为:0010 0000 0000 0000 0001 0000 0000 0111
低 3 位是
111
(即0x7
)。如果我们清零这低 3 位,地址变为:0010 0000 0000 0000 0001 0000 0000 0000
即
0x20001000
,这是一个 8 字节对齐的地址。地址: 0010 0000 0000 0000 0001 0000 0000 0111 (0x20001007) 掩码: 1111 1111 1111 1111 1111 1111 1111 1000 (0xFFFFFFF8) 结果: 0010 0000 0000 0000 0001 0000 0000 0000 (0x20001000)
-
强制类型转换:
最后,将结果转换回StackType_t*
类型,作为对齐后的栈顶指针。
为什么需要对齐?
在嵌入式系统和 RTOS 中,栈对齐通常是为了满足以下需求:
-
硬件要求
某些处理器架构(如 ARM Cortex-M)要求栈指针必须对齐到 8 字节(或更高)边界。
如果栈指针未对齐,可能会导致硬件异常或性能下降。 -
性能优化
对齐的内存访问通常比未对齐的内存访问更快。
现代处理器通常以对齐的方式加载和存储数据,未对齐的访问可能需要额外的时钟周期。 -
兼容性
对齐可以确保与某些库函数或硬件特性(如浮点运算、DMA 等)兼容。
例如,ARM Cortex-M 的浮点单元(FPU)要求栈对齐到 8 字节边界。 -
RTOS 的要求
在 RTOS 中,任务栈通常用于保存任务的上下文(如寄存器、局部变量等)。
对齐栈可以确保上下文切换时不会因为未对齐的栈指针而导致问题。 -
为什么是 8 字节对齐?
8 字节对齐是一种常见的选择,尤其是在 32 位系统中。
对于 64 位系统,可能需要 16 字节对齐。
对齐的大小通常由处理器架构和编译器决定。 -
代码的实际作用
这行代码的作用是确保pxTopOfStack
指向的地址是 8 字节对齐的。对齐的目的是满足硬件要求、优化性能、确保兼容性,并符合 RTOS 的设计规范。对齐操作是嵌入式系统和 RTOS 中常见的编程技巧,尤其是在处理栈和内存布局时。
例如:
如果pxTopOfStack
的原始值是0x20001007
,经过对齐操作后,值变为0x20001000
。
如果原始值是0x20001008
,则保持不变。
为什么是“向下”对齐?
清零低 3 位的操作会将地址向更小的方向舍入。
例如:
地址 0x20001007
对齐后变为 0x20001000
。
地址 0x20001008
对齐后保持不变(因为它的低 3 位已经是 000
)。
这种对齐方式称为“向下对齐”,因为地址总是被舍入到不大于原始地址的最接近的 8 字节对齐地址。
如果我们将栈顶指针向下对齐到 8 字节边界,是否会导致计算结果错误?
为什么不会导致计算结果错误?
-
对齐操作的目的是确保栈指针满足硬件或软件的要求
对齐操作是为了满足处理器或 RTOS 对栈指针的对齐要求(例如 8 字节对齐)。
如果栈指针未对齐,可能会导致硬件异常、性能下降或数据访问错误。
对齐操作本身不会改变栈的使用方式,只是确保栈指针的地址满足对齐条件。 -
对齐操作是向下对齐
对齐操作会将栈指针向下舍入到最近的 8 字节边界。
如果栈指针已经是 8 字节对齐的(如 0x20001008),对齐操作不会改变它的值。
如果栈指针未对齐(如 0x20001007),对齐操作会将其舍入到 0x20001000。 -
栈的使用方式
栈是从高地址向低地址生长的。
对齐操作只是调整栈顶指针的起始位置,不会影响栈的使用逻辑。
在任务初始化时,栈顶指针的对齐操作是为了确保栈的起始位置满足对齐要求,而不是为了改变栈的计算方式。
为什么对齐操作不会导致栈空间浪费或错误?
-
栈的分配是预先计算的
在任务创建时,栈的大小(ulStackDepth
)是预先定义的。
对齐操作只会调整栈顶指针的起始位置,不会改变栈的总大小。
例如,如果栈大小是 100 字节,对齐操作可能会将栈顶指针向下调整几个字节,但栈的总空间仍然是 100 字节。 -
对齐操作的影响是微小的
对齐操作最多只会将栈顶指针向下调整 7 个字节(因为 8 字节对齐的最大偏移是 7)。
这种调整对栈的使用影响非常小,通常可以忽略不计。 -
对齐操作是必要的
如果不对齐栈顶指针,可能会导致硬件异常或性能问题。
对齐操作是为了确保系统的稳定性和性能,而不是为了改变栈的计算逻辑。
Cortex-M4 中使用 DMA 传输数据的示例
#include <stdint.h>
#include <stddef.h>
#include <stdio.h>
// 假设的 DMA 寄存器地址
#define DMA_SRC_ADDR (volatile uint32_t *)0x40020000
#define DMA_DST_ADDR (volatile uint32_t *)0x40020004
#define DMA_SIZE (volatile uint32_t *)0x40020008
#define DMA_CTRL (volatile uint32_t *)0x4002000C
// DMA 控制寄存器标志
#define DMA_CTRL_START (1 << 0)
// 分配对齐的内存(4 字节对齐)
// 这种方式通过编译器属性直接声明变量的对齐方式。
// 仅适用于静态分配的内存(如全局变量或栈变量)。
uint32_t src_array[16] __attribute__((aligned(4))); // 源数组
uint32_t dst_array[16] __attribute__((aligned(4))); // 目标数组
// 对于动态分配的内存(如 malloc),需要使用其他方式(如 aligned_alloc)
// void *ptr = aligned_alloc(16, size);
// 这种形式会分配 16 字节对齐的内存。
/*
__attribute__((aligned)) 适用于静态分配的内存,代码简洁且可读性好。
手动对齐(掩码) 适用于动态计算对齐地址或对齐边界不固定的场景。
aligned_alloc 适用于动态分配对齐的内存。
*/
// 初始化数组
void init_arrays() {
for (int i = 0; i < 16; i++) {
src_array[i] = i + 1; // 初始化源数组
dst_array[i] = 0; // 清空目标数组
}
}
// 配置并启动 DMA 传输
void start_dma_transfer(uint32_t *src, uint32_t *dst, size_t size) {
// 检查地址是否对齐
if (((uintptr_t)src % 4 != 0) || ((uintptr_t)dst % 4 != 0)) {
printf("错误:源地址或目标地址未对齐!\n");
return;
}
// 配置 DMA 源地址
*DMA_SRC_ADDR = (uint32_t)src;
// 配置 DMA 目标地址
*DMA_DST_ADDR = (uint32_t)dst;
// 配置传输大小(以字节为单位)
*DMA_SIZE = size * sizeof(uint32_t);
// 启动 DMA 传输
*DMA_CTRL = DMA_CTRL_START;
printf("DMA 传输已启动...\n");
}
// 检查 DMA 传输是否完成
int is_dma_transfer_complete() {
// 假设 DMA 控制寄存器的第 1 位表示传输完成
return (*DMA_CTRL & (1 << 1)) != 0;
}
int main() {
// 初始化数组
init_arrays();
// 启动 DMA 传输
start_dma_transfer(src_array, dst_array, 16);
// 等待 DMA 传输完成
while (!is_dma_transfer_complete()) {
// 等待
}
printf("DMA 传输完成!\n");
// 打印目标数组内容
printf("目标数组内容: ");
for (int i = 0; i < 16; i++) {
printf("%d ", dst_array[i]);
}
printf("\n");
return 0;
}