Cortex-M7 D-Cache 与 DMA 缓存一致性说明
适用文件:Core/Src/usart.c、Core/Src/main.c
芯片架构:STM32H7 / Cortex-M7(含 32 字节 D-Cache 缓存行)
0. 当前工程 D-Cache 状态(重要)
结论:本工程目前未启用 D-Cache。
检查依据
| 检查项 | 文件 | 结果 |
|---|---|---|
SCB_EnableDCache() 调用 |
全工程 | 不存在 |
| MPU 缓存属性配置 | main.c:243 | IsCacheable = MPU_ACCESS_NOT_CACHEABLE(全局不可缓存) |
system_stm32h7xx.c 启动初始化 |
system_stm32h7xx.c | 无 Cache 使能 |
| 启动文件 | startup_stm32h743iitx.s | 无 Cache 使能 |
Cortex-M7 上电复位后 D-Cache 默认关闭 ,必须显式调用 SCB_EnableDCache() 才会激活。
当前状态下 Cache 维护函数的行为
SCB_InvalidateDCache_by_Addr 和 SCB_CleanDCache_by_Addr 在函数内部会检查 SCB->CCR 寄存器的 DC 位。若 D-Cache 未开启,这两个函数直接返回,不执行任何操作,不会出错。
因此当前程序可以正确运行,但 Cache 维护代码尚未发挥实际作用。
启用 D-Cache 的正确步骤
如果将来要开启 D-Cache 以提升性能,需要同时修改两处:
第一步:在 main.c 的 MPU_Config() 调用后、HAL_Init() 之前添加:
c
MPU_Config(); /* 已有 */
SCB_EnableICache(); /* 可选:使能指令缓存 */
SCB_EnableDCache(); /* 新增:使能数据缓存 */
HAL_Init(); /* 已有 */
第二步:在 MPU 中将 SRAM 区域配置为 Cacheable(否则 D-Cache 对内存无效):
c
/* 示例:将 SRAM(0x24000000,512KB)配置为 Write-back、可缓存 */
MPU_InitStruct.BaseAddress = 0x24000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_512KB;
MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1;
启用后,usart.c 中已有的 aligned(32) 对齐和 Clean/Invalidate 调用将自动生效,无需修改。
1. 背景:为什么会有缓存一致性问题
Cortex-M7 内核内置 D-Cache(数据缓存) ,缓存行(Cache Line)大小固定为 32 字节。
D-Cache 的工作方式:
- CPU 读内存 → 先查缓存,命中则直接返回,不访问 RAM(极快)
- CPU 写内存 → 写入缓存(Write-back 模式),不立即写回 RAM
问题根源:DMA 控制器直接读写 RAM,完全绕过 D-Cache。这导致两种竞争场景:
| 场景 | 现象 |
|---|---|
| DMA 写 RAM(RX),CPU 随后读 | CPU 从缓存拿到旧数据,看不到 DMA 写入的新内容 |
| CPU 写缓存(TX),DMA 随后读 RAM | DMA 从 RAM 拿到旧数据,CPU 的修改还没刷到 RAM |
解决手段:在 DMA 传输前后,对 DMA 缓冲区执行 Cache 维护操作(Invalidate / Clean)。
2. 32 字节对齐:__attribute__((aligned(32)))
c
/* usart.c, 第 29 行 */
uint8_t rs485_rx_buf[RS485_RX_BUF_SIZE] __attribute__((aligned(32)));
为什么必须对齐到 32 字节?
SCB_InvalidateDCache_by_Addr 和 SCB_CleanDCache_by_Addr 操作的最小单位是一整条缓存行(32 字节),不能操作某缓存行的一部分。
如果缓冲区地址不是 32 的倍数(未对齐):
RAM 布局示例(未对齐):
地址: 0x20000018
┌──────────────────────────────┐
│ other_var (属于缓存行 A) │ ← 0x20000000
│ ... │
│ rs485_rx_buf 从 0x18 开始 │ ← 0x20000018(不对齐)
└──────────────────────────────┘
此时对 rs485_rx_buf 执行 Invalidate,HAL 库会将操作范围向下取整到缓存行边界(0x20000000),导致 other_var 的缓存行也被作废 → 相邻变量数据损坏。
正确的对齐方式(aligned(32)):
RAM 布局示例(已对齐):
地址: 0x20000020
┌──────────────────────────────┐
│ rs485_rx_buf[0..31] │ ← 缓存行边界,独占完整缓存行
│ rs485_rx_buf[32..63] │
│ ... │
└──────────────────────────────┘
缓冲区从缓存行边界开始,Invalidate/Clean 操作精确覆盖且不影响其他变量。
注意:大小也应是 32 的整数倍
当前 RS485_RX_BUF_SIZE = 128(= 32 × 4),满足条件。
若大小不是 32 的倍数(如 100 字节),操作会覆盖到末尾那条缓存行的剩余字节,同样可能损坏相邻数据。
3. SCB_InvalidateDCache_by_Addr
函数原型
c
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
作用
将指定地址范围内的所有 D-Cache 缓存行标记为无效(Invalid) 。
CPU 下次访问这段内存时,强制从 RAM 重新加载,不再使用缓存中的旧数据。
注意:Invalidate 不会把缓存中的内容写回 RAM,直接丢弃缓存行。
使用位置 1:RS485_StartReceive()(接收启动前)
c
/* usart.c, 第 206-213 行 */
void RS485_StartReceive(void)
{
RS485_DE_LOW();
SCB_InvalidateDCache_by_Addr((uint32_t *)rs485_rx_buf, (int32_t)RS485_RX_BUF_SIZE);
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, rs485_rx_buf, RS485_RX_BUF_SIZE);
__HAL_DMA_DISABLE_IT(huart2.hdmarx, DMA_IT_HT);
}
时序分析:
CPU 上次处理数据时可能已将 rs485_rx_buf 的部分内容加载进缓存
↓
【Invalidate】:丢弃缓存中该区域的旧数据
↓
启动 DMA RX:DMA 将串口数据直接写入 RAM
↓
(等待接收完成)
目的:防止 CPU 在使用旧缓存时,DMA 的新写入被缓存掩盖,确保下次读取是真正的新数据。
使用位置 2:HAL_UARTEx_RxEventCallback()(接收完成后)
c
/* usart.c, 第 244-255 行 */
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart->Instance == USART2)
{
SCB_InvalidateDCache_by_Addr((uint32_t *)rs485_rx_buf, (int32_t)RS485_RX_BUF_SIZE);
rs485_rx_len = Size;
rs485_rx_flag = 1;
RS485_StartReceive();
}
}
时序分析:
DMA 将接收到的 RS485 数据写入 RAM(绕过缓存)
↓
UART IDLE 中断触发,进入回调
↓
【Invalidate】:使 rs485_rx_buf 对应的缓存行失效
↓
CPU 读取 rs485_rx_buf:强制从 RAM 加载 → 读到 DMA 写入的真实数据
目的:DMA 已把新数据写入 RAM,但 CPU 缓存中还是旧内容。Invalidate 后 CPU 才能看到新数据。
4. SCB_CleanDCache_by_Addr
函数原型
c
void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize);
作用
将 D-Cache 中指定地址范围内所有**脏缓存行(Dirty Cache Lines)**的内容写回到 RAM。
写回后缓存行变为 Clean 状态(仍然有效,下次 CPU 读取仍命中缓存)。
与 Invalidate 的区别:Clean = 写回 RAM 但不丢弃缓存;Invalidate = 丢弃缓存不写回 RAM。
使用位置:RS485_SendDMA()(发送启动前)
c
/* usart.c, 第 220-226 行 */
void RS485_SendDMA(const uint8_t *data, uint16_t len)
{
RS485_DE_HIGH();
SCB_CleanDCache_by_Addr((uint32_t *)data, (int32_t)len);
HAL_UART_Transmit_DMA(&huart2, (uint8_t *)data, len);
}
时序分析:
CPU 向发送缓冲区写数据(如 rs485_tx_buf)
→ Write-back 模式:数据写入 D-Cache,RAM 中仍是旧值
↓
【Clean】:将缓存中的新数据强制写回 RAM
↓
启动 DMA TX:DMA 从 RAM 读取数据 → 发送到 RS485 总线
目的:若没有 Clean,DMA 从 RAM 读到的是 CPU 写之前的旧数据,导致发送内容错误。
发送缓冲区同样需要 32 字节对齐
c
/* 调用方定义发送缓冲区时应对齐,例如: */
uint8_t rs485_tx_buf[32] __attribute__((aligned(32)));
原因与 RX 缓冲区完全相同:Clean 操作以 32 字节为单位,未对齐会影响相邻变量。
5. 操作对比速查表
| 操作 | 函数 | 缓存状态变化 | 使用时机 |
|---|---|---|---|
| Invalidate | SCB_InvalidateDCache_by_Addr |
缓存行 → Invalid(丢弃,不写回) | DMA 写 RAM 之前 和 之后(CPU 要读 DMA 结果) |
| Clean | SCB_CleanDCache_by_Addr |
Dirty 缓存行 → 写回 RAM,变为 Clean | DMA 读 RAM 之前(CPU 已写过缓冲区) |
| Clean+Invalidate | SCB_CleanInvalidateDCache_by_Addr |
写回 RAM 后丢弃 | 缓冲区既被 CPU 写过、又将被 DMA 写入时 |
6. 完整数据流图
RX 流程(DMA 写 → CPU 读)
RS485 总线
│
▼ 串行数据
USART2 外设
│
▼ DMA(直接写 RAM,绕过缓存)
rs485_rx_buf(RAM)
│
▼ IDLE 中断
SCB_InvalidateDCache_by_Addr ← 使缓存失效
│
▼ CPU 读取
rs485_rx_buf(从 RAM 重新加载) ← 得到真实接收数据
TX 流程(CPU 写 → DMA 读)
CPU 填充 rs485_tx_buf
│
▼(数据暂留在 D-Cache,RAM 可能未更新)
SCB_CleanDCache_by_Addr ← 强制将缓存写回 RAM
│
▼ DMA(从 RAM 读取,绕过缓存)
USART2 外设
│
▼
RS485 总线
7. 常见错误与后果
| 忘记操作 | 后果 |
|---|---|
| RX 前不 Invalidate | CPU 读到缓存中的旧帧数据(非 DMA 写入的新数据) |
| RX 后不 Invalidate | 同上;尤其在第二帧开始前仍读旧帧 |
| TX 前不 Clean | DMA 发送缓存更新前的旧数据,导致发送内容错误 |
| 缓冲区不对齐 | Invalidate/Clean 操作影响相邻变量,造成数据损坏,极难调试 |
生成日期:2026-03-30