【嵌入式 - 关于地址对齐】

最近在看《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 字节边界。具体步骤如下:

  1. (uint32_t)pxTopOfStack:

    将指针 pxTopOfStack 转换为 uint32_t 类型(32 位无符号整数),以便进行位操作。

  2. ~((uint32_t)0x0007):

    0x0007 的二进制表示为 0000 0000 0000 0111。

  3. 取反操作 ~ 将其变为 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(二进制)。

  4. & 操作:

    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)
    
  5. 强制类型转换:
    最后,将结果转换回 StackType_t* 类型,作为对齐后的栈顶指针。

为什么需要对齐?

在嵌入式系统和 RTOS 中,栈对齐通常是为了满足以下需求:

  1. 硬件要求
    某些处理器架构(如 ARM Cortex-M)要求栈指针必须对齐到 8 字节(或更高)边界。
    如果栈指针未对齐,可能会导致硬件异常或性能下降。

  2. 性能优化
    对齐的内存访问通常比未对齐的内存访问更快。
    现代处理器通常以对齐的方式加载和存储数据,未对齐的访问可能需要额外的时钟周期。

  3. 兼容性
    对齐可以确保与某些库函数或硬件特性(如浮点运算、DMA 等)兼容。
    例如,ARM Cortex-M 的浮点单元(FPU)要求栈对齐到 8 字节边界。

  4. RTOS 的要求
    在 RTOS 中,任务栈通常用于保存任务的上下文(如寄存器、局部变量等)。
    对齐栈可以确保上下文切换时不会因为未对齐的栈指针而导致问题。

  5. 为什么是 8 字节对齐?
    8 字节对齐是一种常见的选择,尤其是在 32 位系统中。
    对于 64 位系统,可能需要 16 字节对齐。
    对齐的大小通常由处理器架构和编译器决定。

  6. 代码的实际作用
    这行代码的作用是确保 pxTopOfStack 指向的地址是 8 字节对齐的。对齐的目的是满足硬件要求、优化性能、确保兼容性,并符合 RTOS 的设计规范。对齐操作是嵌入式系统和 RTOS 中常见的编程技巧,尤其是在处理栈和内存布局时。
    例如:
    如果 pxTopOfStack 的原始值是 0x20001007,经过对齐操作后,值变为 0x20001000
    如果原始值是 0x20001008,则保持不变。

为什么是“向下”对齐?

清零低 3 位的操作会将地址向更小的方向舍入。

例如:

地址 0x20001007 对齐后变为 0x20001000

地址 0x20001008 对齐后保持不变(因为它的低 3 位已经是 000)。

这种对齐方式称为“向下对齐”,因为地址总是被舍入到不大于原始地址的最接近的 8 字节对齐地址。

如果我们将栈顶指针向下对齐到 8 字节边界,是否会导致计算结果错误?

为什么不会导致计算结果错误?

  1. 对齐操作的目的是确保栈指针满足硬件或软件的要求
    对齐操作是为了满足处理器或 RTOS 对栈指针的对齐要求(例如 8 字节对齐)。
    如果栈指针未对齐,可能会导致硬件异常、性能下降或数据访问错误。
    对齐操作本身不会改变栈的使用方式,只是确保栈指针的地址满足对齐条件。

  2. 对齐操作是向下对齐
    对齐操作会将栈指针向下舍入到最近的 8 字节边界。
    如果栈指针已经是 8 字节对齐的(如 0x20001008),对齐操作不会改变它的值。
    如果栈指针未对齐(如 0x20001007),对齐操作会将其舍入到 0x20001000。

  3. 栈的使用方式
    栈是从高地址向低地址生长的。
    对齐操作只是调整栈顶指针的起始位置,不会影响栈的使用逻辑。
    在任务初始化时,栈顶指针的对齐操作是为了确保栈的起始位置满足对齐要求,而不是为了改变栈的计算方式。

为什么对齐操作不会导致栈空间浪费或错误?

  1. 栈的分配是预先计算的
    在任务创建时,栈的大小(ulStackDepth)是预先定义的。
    对齐操作只会调整栈顶指针的起始位置,不会改变栈的总大小。
    例如,如果栈大小是 100 字节,对齐操作可能会将栈顶指针向下调整几个字节,但栈的总空间仍然是 100 字节。

  2. 对齐操作的影响是微小的
    对齐操作最多只会将栈顶指针向下调整 7 个字节(因为 8 字节对齐的最大偏移是 7)。
    这种调整对栈的使用影响非常小,通常可以忽略不计。

  3. 对齐操作是必要的
    如果不对齐栈顶指针,可能会导致硬件异常或性能问题。
    对齐操作是为了确保系统的稳定性和性能,而不是为了改变栈的计算逻辑。

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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

六月悉茗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值