这篇文章来源于我发现的一个不解:为什么访问一地址存16bits的存储芯片需要字节对齐?我STM32G474VET6访问SST39 flash需要执行:
cs
/* 将Flash字地址转换为STM32字节地址(字地址 × 2)*/
#define FLASH_WORD_TO_BYTE(addr_word) ((addr_word) << 1)
/* 将Flash字节地址转换为字地址(字节地址 ÷ 2)*/
#define FLASH_BYTE_TO_WORD(addr_byte) ((addr_byte) >> 1)
核心是因为你的32中是8位,FMC他也觉得自己是八位,所以STM32映射到BANK的地址一个32 FMC以为8位实际代表16位,具体执行:
| STM32 地址 | 计算过程 | 实际访问的 Flash 地址 | 访问的数据 |
|---|---|---|---|
| 0x60000000 | (0x60000000 - 0x60000000) >> 1 = 0x0000 | 第 0 个 16 位字 | 低字节 @0x60000000, 高字节 @0x60000001 |
| 0x60000001 | (0x60000001 - 0x60000000) >> 1 = 0x0000(取整) | 第 0 个 16 位字 | 只访问高字节(具体看字节使能信号) |
| 0x60000002 | (0x60000002 - 0x60000000) >> 1 = 0x0001 | 第 1 个 16 位字 | 低字节 @0x60000002, 高字节 @0x60000003 |
不是你不能访问奇数地址而是访问了也是错的,你访问
|----------------|
| 0x60000000 |
| 0x60000001 |
实际在fmc会输出访问同一个FLASH的地址但0x60000001的访问只访问高字节(具体看字节使能信号)访问了也没用所以执行:
cs
/* 将Flash字地址转换为STM32字节地址(字地址 × 2)*/
#define FLASH_WORD_TO_BYTE(addr_word) ((addr_word) << 1)
/* 将Flash字节地址转换为字地址(字节地址 ÷ 2)*/
#define FLASH_BYTE_TO_WORD(addr_byte) ((addr_byte) >> 1)
来消除奇数地址为偶地址达成以下效果:
| STM32 地址 | 计算过程 | 实际访问的 Flash 地址 | 访问的数据 |
| 0x60000000 | (0x60000000 - 0x60000000) >> 1 = 0x0000 | 第 0 个 16 位字 | 低字节 @0x60000000, 高字节 @0x60000001 |
| 0x60000001 | (0x60000001 - 0x60000000) >> 1 = 0x0000(取整) | 第 0 个 16 位字 | 只访问高字节(具体看字节使能信号) |
| 0x60000002 | (0x60000002 - 0x60000000) >> 1 = 0x0001 | 第 1 个 16 位字 | 低字节 @0x60000002, 高字节 @0x60000003 |
|---|
引言:一个看似"异常"的访问现象
在STM32开发中,当我们通过FMC接口连接16位宽度的外部Flash时,经常会遇到一个看似奇怪的现象:访问地址0x60000000和0x60000001实际上访问的是Flash的同一个16位存储单元 。这种"地址重叠"现象背后的原因,正是本文要深入探讨的字节对齐问题。
一、根本原因:数据宽度不匹配
1.1 两种不同的编址方式
-
STM32(CPU端) :按字节(Byte) 编址,每个地址对应1字节
-
外部Flash(设备端) :按字(Word,16位) 编址,每个地址对应2字节
这种差异导致了地址映射的"缩放"关系。
1.2 地址总线连接
在硬件连接上,STM32的地址线A[0]通常不连接到16位Flash,因为Flash的A[0]引脚用于选择16位字内的低/高字节。STM32使用字节使能信号(NBL0/NBL1)来替代这一功能。
二、地址转换原理:右移一位的数学
2.1 转换公式
text
Flash字地址 = (STM32字节地址 - 0x60000000) >> 1
-
>> 1(右移一位)等效于除以2 -
这是因为每个Flash地址包含2个字节
2.2 具体实例分析
| STM32地址 | 计算过程 | 实际Flash地址 | 访问的数据 |
|---|---|---|---|
| 0x60000000 | (0x60000000-0x60000000)>>1 = 0x0000 |
第0个16位字 | 低字节@0x60000000, 高字节@0x60000001 |
| 0x60000001 | (0x60000001-0x60000000)>>1 = 0x0000 |
第0个16位字 | 只访问高字节(字节使能NBL1=0, NBL0=1) |
| 0x60000002 | (0x60000002-0x60000000)>>1 = 0x0001 |
第1个16位字 | 低字节@0x60000002, 高字节@0x60000003 |
关键发现 :
0x60000000和0x60000001都映射到Flash的同一个16位字地址(0x0000)!差别仅在于访问的是该字的低字节还是高字节。
三、为什么需要字节对齐访问?
3.1 性能考量
非对齐访问的问题:
c
// 非对齐访问示例(潜在问题)
uint16_t *ptr = (uint16_t*)0x60000001; // 奇数地址!
uint16_t value = *ptr; // 可能触发硬件异常或性能下降
-
需要多个总线周期:非对齐的16位访问可能需要2次8位访问
-
增加延迟:额外的时间开销影响实时性
-
降低吞吐量:无法充分利用16位数据总线带宽
3.2 硬件限制
-
某些Flash芯片:要求必须按字(16位)写入
-
原子性保证:对齐访问确保操作的原子性
-
时序一致性:简化时序控制逻辑
3.3 编程简化
对齐访问使得代码更易于理解和维护:
c
// 正确的对齐访问
#define FLASH_BASE ((volatile uint16_t*)0x60000000)
// 按字访问,自然对齐
void write_flash(uint32_t word_index, uint16_t data) {
FLASH_BASE[word_index] = data; // word_index对应Flash字地址
}
// 按字节访问,明确处理高低字节
void write_flash_byte(uint32_t byte_addr, uint8_t data) {
uint32_t word_addr = byte_addr >> 1;
uint8_t byte_offset = byte_addr & 0x01;
if (byte_offset == 0) {
// 写低字节:保留高字节,更新低字节
uint16_t current = FLASH_BASE[word_addr];
current = (current & 0xFF00) | data;
FLASH_BASE[word_addr] = current;
} else {
// 写高字节:保留低字节,更新高字节
uint16_t current = FLASH_BASE[word_addr];
current = (current & 0x00FF) | (data << 8);
FLASH_BASE[word_addr] = current;
}
}
四、硬件实现细节:字节使能信号
FMC使用NBL[1:0](字节使能)信号来控制16位模式下的字节访问:
| NBL1 | NBL0 | 操作 | 访问位置 | 对应STM32地址 |
|---|---|---|---|---|
| 1 | 0 | 写低字节 | 字地址的低8位 | 偶地址(如0x60000000) |
| 0 | 1 | 写高字节 | 字地址的高8位 | 奇地址(如0x60000001) |
| 0 | 0 | 写整个字 | 整个16位字 | 偶地址(16位对齐) |
当访问0x60000001时:
-
FMC仍然向Flash发送地址
0x0000 -
但设置
NBL[1:0] = 01,表示"只操作高字节" -
Flash芯片根据此信号只更新对应字节
五、实际应用建议
5.1 地址转换宏的正确使用
c
/* Flash字地址 → STM32字节地址(字地址 × 2)*/
#define FLASH_WORD_TO_BYTE(addr_word) ((addr_word) << 1)
/* Flash字节地址 → 字地址(字节地址 ÷ 2)*/
#define FLASH_BYTE_TO_WORD(addr_byte) ((addr_byte) >> 1)
/* 实际使用示例 */
uint32_t flash_word_addr = 0x1000; // Flash的第0x1000个16位字
uint32_t stm32_byte_addr = 0x60000000 + FLASH_WORD_TO_BYTE(flash_word_addr);
// 结果:0x60000000 + 0x2000 = 0x60002000
5.2 数据类型选择
c
// 推荐:使用对齐的16位指针
volatile uint16_t *flash_ptr = (volatile uint16_t*)0x60000000;
// 不推荐:使用8位指针进行16位数据操作
volatile uint8_t *byte_ptr = (volatile uint8_t*)0x60000000;
// byte_ptr[0]和byte_ptr[1]实际上属于同一个Flash字
5.3 内存布局规划
c
// 在链接脚本中确保Flash数据区对齐
.section .extflash, "aw", %progbits
.align 2 // 2字节对齐(16位)
.global _extflash_base
_extflash_base:
.space 0x100000 // 预留1MB外部Flash空间
六、特殊情况处理
6.1 混合字节/字访问策略
c
// 灵活处理对齐和非对齐访问
uint16_t read_flash_any(uint32_t stm32_addr, uint8_t size) {
if (size == 2 && (stm32_addr & 0x01) == 0) {
// 16位对齐访问:最优性能
return *(volatile uint16_t*)stm32_addr;
} else {
// 非对齐或8位访问:分解操作
volatile uint8_t *p = (volatile uint8_t*)stm32_addr;
if (size == 1) return p[0];
else return (p[0] | (p[1] << 8)); // 注意:p[0]和p[1]可能跨字边界
}
}
6.2 DMA传输的对齐要求
c
// DMA通常要求源/地址对齐
// 对于16位Flash访问,DMA地址必须是2的倍数
void setup_dma_for_flash(uint32_t flash_word_addr, uint32_t count) {
uint32_t stm32_addr = 0x60000000 + (flash_word_addr << 1);
// 检查地址对齐
if ((stm32_addr & 0x01) != 0) {
// 处理非对齐:需要特殊处理或调整
handle_unaligned_dma();
} else {
// 标准DMA配置
DMA1->CPAR = stm32_addr; // 外设地址(Flash)
DMA1->CMAR = (uint32_t)buffer; // 内存地址
DMA1->CNDTR = count * 2; // 传输字节数(字数×2)
}
}