简介:这份STM32H745/755与STM32H747/757参考手册面向嵌入式应用开发者与工业控制方案工程师,系统讲解双核Cortex-M7/M4平台的存储器与片内外设使用方法。压缩包仅含1个PDF文件,约40.2MB,为ST官方RM0399 Rev3文档,便于离线查阅与团队共享。内容从文档约定、总线矩阵与TCM实时控制存储器展开,覆盖内存组织、ADC/DAC、定时器、SPI/I2C/UART、CAN、以太网MAC、USB OTG等外设,并涉及加密硬件、安全启动与TrustZone等安全特性。已有517人学习,适合用于底层驱动开发、外设配置核对和实时控制架构设计时快速定位寄存器与功能说明,亦可结合Cortex-M4/M7技术参考手册深入理解内核编程模型。
1. 从一份 3528 页的 PDF 说起:STM32H745 参考手册到底该怎么读
手上这份 stm32h745 reference manual.pdf,封面写着 RM0399 Rev 3、2020 年 2 月、3528 页。多数人拿到它的时机很一致:STM32 双核 CM7 CM4 工业控制项目立项了,单核跑不动“协议栈 + 运动控制环 + 上位机通信”这套组合,才回头翻 H745/755、H747/757 这条线的底细。手册开篇说得很克制,它面向应用开发者,讲内存和外设怎么用;真翻进去会发现它不负责教你,只负责在你要配置某一位的时候,那一位一定在。所以这篇不带你从头读到尾,而是把它当一张可以反复查的图纸:先拆内存映射和双核启动,再拆 RAMECC、FLASH 双 bank、Cache 与 DMA,每块给出能抄进工程的步骤,以及我踩过的那些点。
2. 双核启动与内存映射:先把第 2 章啃下来再写代码
2.1 总线矩阵、TCM 与 AXI SRAM 的分工
手册第 2 章开头那张系统架构图,值得打印出来贴在显示器边上。它画的是 D1/D2/D3 三个域、总线矩阵、以及每个主机端口挂在哪条总线上。读这张图只需要回答三个问题:谁访问谁、走几级总线、有没有 cache 参与。
Cortex-M7 侧有两个私有的紧耦合存储区:ITCM 64KB 位于 0x00000000,DTCM 128KB 位于 0x20000000。这两块 RAM 直连内核,不经总线矩阵,取指和数据访问都是零等待、确定性延迟。手册在 2.1.2 里专门列了 TCM 总线,意思很明确:对时间敏感的东西往这里放。
Cortex-M4 侧没有 TCM 端口。这一条在手册里写得不算显眼,但实际影响巨大——M4 去访问位于 0x20000000 的 DTCM,是要穿过 D2-to-D1 的总线桥的,延迟和总线争用都不可控。很多双核方案一开始把共享数据丢在 DTCM,然后抱怨 M4 侧控制环抖动,原因就在这里。
AXI SRAM 512KB 在 0x24000000,挂在 AXI 矩阵上,是 CM7 访问带宽最高的普通内存,但有 D-Cache 参与,做 DMA 缓冲要额外处理一致性。SRAM1/2/3 在 0x30000000 起的 D2 域,M4 就近访问快,适合放 M4 的代码、栈和双核共享队列。SRAM4 64KB 在 0x38000000 的 D3 域,是低功耗域的一部分,DMA 缓冲和待机保持数据一般放这里。
我一般的分配是:CM7 的电流环、PWM 计算、编码器解码这类硬实时函数放 ITCM,状态变量放 DTCM;大块采样缓冲放 AXI SRAM,配 MPU 设成 non-cacheable;M4 的全部代码和栈放 SRAM1/2;DMA 描述符和环形缓冲放 SRAM4。这套分配不是拍脑袋,是把手册 2.3.2 的内存映射表和 2.1.1 的总线矩阵对照出来的结果。
2.2 把手册的地址表翻译成链接脚本
手册 2.3.2 那张表,本质上就是一份链接脚本的素材。表里每一行给出起始地址、大小、所属域、默认访问属性,直接抄进 MEMORY 段即可。下面是 1MB Flash 版本的写法,地址和长度都取自该表,换具体料号时按数据手册的存储容量改 LENGTH 就行。
/* stm32h745_flash.ld 片段,MEMORY 各项地址取自手册 2.3.2 内存映射表 */ MEMORY { ITCMRAM (xrw) : ORIGIN = 0x00000000, LENGTH = 64K DTCMRAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K AXI_SRAM (xrw) : ORIGIN = 0x24000000, LENGTH = 512K SRAM12 (xrw) : ORIGIN = 0x30000000, LENGTH = 256K SRAM3 (xrw) : ORIGIN = 0x30040000, LENGTH = 32K SRAM4 (xrw) : ORIGIN = 0x38000000, LENGTH = 64K } SECTIONS { /* 硬实时代码段,启动时由 startup 拷进 ITCM */ .itcmram : ALIGN(32) { __sitcmram = .; *(.itcmram) *(.itcmram*) __eitcmram = .; } > ITCMRAM AT > FLASH /* DTCM 状态变量,不放任何供 DMA 访问的缓冲 */ .dtcmram (NOLOAD) : ALIGN(4) { __sdtcmram = .; *(.dtcmram) *(.dtcmram*) __edtcmram = .; } > DTCMRAM }MEMORY 里每个名字对应的就是手册表里的一个地址区间,LENGTH 写成 K 后缀便于核对。.itcmram段带AT > FLASH,意思是链接时按 Flash 里的地址固化为初值,运行前由启动代码搬到 ITCM 起点,这样复位后代码就已经在零等待区里。.dtcmram用(NOLOAD),因为它只存运行时状态,不需要初值,也避免链接器把几百 KB 的初值塞进 Flash 镜像。C 代码里用段属性把函数和变量钉到对应区域:
/* 段名必须与链接脚本中的 SECTIONS 名字逐字一致 */ __attribute__((section(".itcmram"), noinline)) void current_loop_isr(void) { /* 电流环:采样、Clarke/Park、PI、SVPWM,全部在 ITCM 内执行 */ } __attribute__((section(".dtcmram"), aligned(32))) static float pid_state[3][8]; /* DMA 缓冲放 SRAM4,并且单独一个段,方便 MPU 按段设 non-cacheable */ __attribute__((section(".sram4"), aligned(32))) uint8_t uart_dma_buf[512];第一个段属性决定函数的取指位置,第二个决定变量的存放位置,第三个是为后面 MPU 配置做准备。参数上唯一要盯的是对齐:放 SRAM4 的缓冲按 32 字节对齐,等于 D-Cache 的行大小,后续做 clean/invalidate 时不用处理跨行边界,这一点在第四章还会展开。另外,DTCM 不能被 DMA 访问,这一点手册在总线矩阵里画得很清楚,DMA 主机端口根本连不到 TCM,所以任何带__attribute__((section(".dtcmram")))的数组都不要出现在 DMA 初始化参数里。
2.3 从核启动与 HSEM 握手
双核项目第一个卡点通常不是算法,而是“M4 为什么不跑”。手册在 FLASH 章节的选项字节部分(4.4.7 节)定义了 CM4 的启动地址,同时在 RCC 章节给了 BOOT_C2 位。这两者的区别值得说清:选项字节里的 BOOT4 地址是掉电保持的配置,RCC 的 BOOT_C2 是本次上电临时生效的强制位。调试阶段我一般用 BOOT_C2,因为它改完复位就生效,写坏了大不了重新烧;量产再固化到选项字节。
/* CM7 侧:强制 CM4 从 0x08100000 取向量表,然后发事件唤醒 */ void cm4_boot_from_bank2(void) { /* HSEM 与 RCC 的使能位以手册对应章节的寄存器表为准, 这里用器件支持包里的宏,避免手抄位号出错 */ RCC->AHB4ENR |= RCC_AHB4ENR_HSEMEN; (void)RCC->AHB4ENR; /* 读回一次,等时钟稳定 */ RCC->GCR |= RCC_GCR_BOOT_C2; /* BOOT_C2=1:CM4 从 0x08100000 启动 */ __DSB(); __SEV(); /* CM4 若在 WFE,会被这条事件唤醒 */ }RCC->AHB4ENR |= ...这一句不能省门控时钟,否则后面访问 HSEM 会直接进 HardFault。读回操作是很多工程师的习惯动作,作用是确保写操作在总线上真正落地,再往下走。__DSB()保证前面的写操作完成,__SEV()发出事件,让处于 WFE 的从核醒来。CM4 侧的初始化顺序必须和这个地址对上:
/* CM4 侧:向量表偏移必须等于 BOOT_C2 强制的地址,否则中断全跑飞 */ void cm4_init(void) { SCB->VTOR = 0x08100000UL; __DSB(); __ISB(); /* 让 VTOR 的修改对后续取指立即生效 */ }SCB->VTOR写错是双核项目里最常见的一类翻车:程序能跑进 main,但任何一个中断进来都跳进 HardFault,因为向量表还在 0x08000000 而 CPU 认为自己在 0x08100000。这两个地址必须成对修改。
双核之间真正需要互斥的地方不多,但一旦有,就得用硬件信号量。HSEM 是手册里独立的一章,每个信号量有 32 位寄存器,写入时带上核 ID,回读能判断归属:
/* HSEM 寄存器按 0x0C 步进排列,RLR 区按 0x04 步进,偏移见 HSEM 章节的寄存器映射 */ #define HSEM_R(n) (*(volatile uint32_t *)(0x58026400UL + 0x0CUL * (n))) #define HSEM_RLR(n) (*(volatile uint32_t *)(0x58026480UL + 0x04UL * (n))) /* coreid:CM7 写 0x0C7,CM4 写 0x0C4,具体编码见手册 HSEM 章节 */ int hsem_lock(uint32_t sem, uint32_t coreid) { HSEM_R(sem) = (coreid << 8) | 1U; /* PROCID[15:8] + LOCK */ __DMB(); /* 保证锁生效后再读共享数据 */ return (((HSEM_R(sem) >> 8) & 0xFFU) == coreid) ? 0 : -1; } void hsem_unlock(uint32_t sem, uint32_t coreid) { HSEM_R(sem) = (coreid << 8); /* 只清 LOCK,PROCID 保留供排查 */ }参数里 sem 是信号量编号,0 号通常留给 FLASH 双 bank 并行操作这种系统级用途,业务层从 1 号开始用。 coreid 写错会导致“锁了但解锁不掉”,因为解锁时硬件比对的是写入的核 ID。__DMB()不能省,它保证拿到锁之后的读操作不会被编译器或流水线提到加锁之前,这类问题在调试器里看变量一切正常,只在优化等级切到 -O2 之后偶发,属于典型的玄学故障。
3. RAMECC 与 FLASH 双 bank:第 3、4 章落到寄存器
3.1 RAMECC 监控器:把 ECC 报错变成可定位信息
工业现场对 RAM 位翻转的容忍度很低,H7 这块料所有 SRAM 都带 ECC,手册第 3 章讲的就是怎么把 ECC 错误从“系统莫名重启”变成“我知道哪块 RAM 的哪个地址出过错”。
结构上,RAMECC 分成三组监控器,每组下挂若干通道,每个通道对应一块具体的 RAM。监控器里有配置寄存器负责使能,状态寄存器记录错误类型,故障地址寄存器保存出错地址,还有低/高 32 位数据寄存器和 ECC 校验码寄存器。这套寄存器组合起来,等于给每次 ECC 事件留了一份现场快照。
/* 三组监控器基址,内部各通道按固定偏移排列,偏移值见手册 3.4.8 寄存器映射 */ #define RAMECC1_BASE 0x58027000UL #define RAMECC2_BASE 0x58027400UL #define RAMECC3_BASE 0x58027800UL typedef struct { volatile uint32_t CR; /* 0x00 使能与中断控制 */ volatile uint32_t SR; /* 0x04 状态,写 1 清除 */ volatile uint32_t FAR; /* 0x08 出错地址 */ volatile uint32_t FDRL; /* 0x0C 出错数据低 32 位 */ volatile uint32_t FDRH; /* 0x10 出错数据高 32 位 */ volatile uint32_t FECR; /* 0x14 ECC 校验码 */ } RAMECC_Mon_t; /* 以 DTCM 那一路监控为例,所在组与组内偏移按手册 3.3.3 监控器映射表确认 */ static RAMECC_Mon_t *const mon_dtcm = (RAMECC_Mon_t *)(RAMECC1_BASE + 0x20UL); void ramecc_enable(void) { RCC->AHB1ENR |= RCC_AHB1ENR_RAMECC1EN; /* 位号以 RCC 章节寄存器表为准 */ (void)RCC->AHB1ENR; mon_dtcm->CR |= (1U << 0); /* ECE:打开该通道的 ECC 监控 */ mon_dtcm->CR |= (1U << 1); /* ECIE:可纠正与不可纠正错误都产生中断 */ } void RAMECC_IRQHandler(void) { uint32_t sr = mon_dtcm->SR; if (sr & (1U << 0)) { /* 单比特可纠正 */ log_warn("SED far=%08lx fecr=%08lx", (unsigned long)mon_dtcm->FAR, (unsigned long)mon_dtcm->FECR); } if (sr & (1U << 1)) { /* 双比特不可纠正 */ log_error("DED far=%08lx data=%08lx%08lx", (unsigned long)mon_dtcm->FAR, (unsigned long)mon_dtcm->FDRH, (unsigned long)mon_dtcm->FDRL); } mon_dtcm->SR = sr; /* 写 1 清除,不清会反复进中断 */ }使能顺序上,先开门控时钟再写 CR,这个顺序反了会进 HardFault。状态寄存器必须写 1 清除,这跟很多外设的“写 0 清除”相反,是踩过一次才能记住的细节。FAR 只保留第一次未被读取的错误地址:只要状态位还没清,后续新错误不会覆盖它,所以中断里第一时间把 FAR 和 FECR 打印出来,再清状态。想更省事一点,可以在初始化时把可纠正错误配成中断、不可纠正错误配成复位前记录,两种处理策略取决于你的产品是“必须连续运行”还是“必须立刻停机”。
3.2 FLASH 双 bank 擦写与并行操作
手册第 4 章把 FLASH 拆得很细,值得先记住三个数字:扇区 128KB、编程单位是 32 字节的 flash word、双 bank 各自有独立的 KEYR/CR/SR/CCR。最后这一条是双 bank 的价值所在——两个 bank 可以并行擦写,一边跑代码一边更新另一边的固件,这就是工业设备在线升级的基础。
编程有一个必须遵守的约束:一次 flash word 的 8 个 32 位字要连续写完,中间不能插别的写操作,写完之后等状态寄存器里的“写队列”标志清空,才算这一拍完成。这一点在手册的编程时序图里画得很清楚,但例程里往往被封装掉了,自己写裸寄存器时漏掉就会得到 PGSERR。
/* bank1 解锁;bank2 把 KEYR1 换成 KEYR2、CR1 换成 CR2,其余一致 */ void flash_unlock_bank1(void) { if (FLASH->CR1 & FLASH_CR1_LOCK) { FLASH->KEYR1 = 0x45670123UL; FLASH->KEYR1 = 0xCDEF89ABUL; } } /* addr 必须 32 字节对齐;data8 指向 8 个 32 位字,共 32 字节 */ int flash_program_flashword(uint32_t addr, const uint32_t *data8) { if (addr & 0x1FU) return -1; /* 未对齐直接拒绝,别指望硬件兜底 */ if (FLASH->SR1 & FLASH_SR1_QW) return -2; /* 上一拍还没写完,先等 */ FLASH->CR1 |= FLASH_CR1_PG; /* 进入编程模式 */ for (int i = 0; i < 8; i++) { *(volatile uint32_t *)(addr + i * 4U) = data8[i]; } while (FLASH->SR1 & FLASH_SR1_QW) { } /* 等 32 字节真正落盘 */ FLASH->CR1 &= ~FLASH_CR1_PG; if (FLASH->SR1 & (FLASH_SR1_WRPERR | FLASH_SR1_PGSERR | FLASH_SR1_INCERR)) { FLASH->CCR1 = FLASH_SR1_WRPERR | FLASH_SR1_PGSERR | FLASH_SR1_INCERR; return -3; } return 0; }参数上只有三个点要盯:地址对齐、8 个字连续、写完等 QW。返回值设计成负数区分失败原因,现场排查时一眼能看出是对齐问题还是时序问题。擦除的写法类似,区别是要先选扇区号再置 START:
/* 擦除 128KB 扇区,bank 选 1 或 2,snb 为 bank 内扇区号 */ int flash_erase_sector(uint32_t bank, uint32_t snb) { volatile uint32_t *CR = (bank == 1) ? &FLASH->CR1 : &FLASH->CR2; volatile uint32_t *SR = (bank == 1) ? &FLASH->SR1 : &FLASH->SR2; volatile uint32_t *CCR = (bank == 1) ? &FLASH->CCR1 : &FLASH->CCR2; while (*SR & FLASH_SR1_BSY) { } /* 忙的时候什么都别做 */ *CR |= FLASH_CR1_SER; /* 扇区擦除 */ *CR = (*CR & ~FLASH_CR1_SNB) | (snb << FLASH_CR1_SNB_Pos); *CR |= FLASH_CR1_START; while (*SR & FLASH_SR1_BSY) { } *CR &= ~FLASH_CR1_SER; if (*SR & FLASH_SR1_WRPERR) { /* 目标扇区被写保护 */ *CCR = FLASH_SR1_WRPERR; return -1; } return 0; }FLASH_CR1_SNB_Pos这类位偏移量在手册的寄存器表里都有单独一行,用宏而不是写死数字,换到同系列其他型号时只需要换头文件。擦除是按 bank 独立操作的,双 bank 并行时必须用 HSEM 的 0 号信号量把两边的操作串起来,否则会出现“CM7 正在擦 bank1,CM4 同时在写 bank1”的悲剧。手册在 4.3.11 的并行操作一节把这条约束讲得很直白,只是很多人读到时已经调了两天。
3.3 选项字节、RDP 与 PCROP 的边界
选项字节是整份手册里最需要谨慎对待的部分,因为它改的不是某次运行的行为,而是这颗芯片之后每一次上电的行为。
流程上是三步:用独立的 OPTKEYR 解锁(注意密钥和主 bank 不是同一套)、把新值写进“待编程”寄存器、置 OPTSTART 触发、等待忙标志清除并检查错误标志。生效还需要一次复位。
/* 只改掩码覆盖的位,其余位保持当前值,避免把不相干的保护位写成默认 */ int option_byte_set(uint32_t mask, uint32_t value) { FLASH->OPTKEYR = 0x08192A3BUL; FLASH->OPTKEYR = 0x4C5D6E7FUL; while (FLASH->OPTSR_CUR & FLASH_OPTSR_OPT_BUSY) { } FLASH->OPTSR_PRG = (FLASH->OPTSR_CUR & ~mask) | (value & mask); FLASH->OPTCR |= FLASH_OPTCR_OPTSTART; while (FLASH->OPTSR_CUR & FLASH_OPTSR_OPT_BUSY) { } if (FLASH->OPTSR_CUR & FLASH_OPTSR_OPTCHANGEERR) { FLASH->OPTCCR = FLASH_OPTCCR_CLR_OPTCHANGEERR; return -1; } return 0; /* 需要复位后生效 */ }& ~mask那一句是关键:选项字节寄存器里同时住着 RDP 等级、PCROP 起止地址、写保护扇区位、启动地址、看门狗配置,一次全写会把手里的配置清成默认。我见过有人只想改启动地址,结果顺手把 RDP 从 0 改成 1,调试口当场失效,只能靠整片擦除救回来。
RDP 从等级 1 回退到等级 0 会触发整片擦除,这是硬件设计上的取舍,手册里写在读保护那一节,白纸黑字。所以调试期间改 RDP 之前,先确认板子上有没有可用的备份镜像。PCROP 更严格,它保护的是“代码不能被读出也不能被改”,配置错了连自己都改不回来,只在量产阶段才动它。这类操作我的习惯是:先在空板上验证一遍流程,再到正式板子上跑,中间不做任何“就改一位没事”的侥幸判断。
4. 外设与 Cache 一致性:工业现场最容易忽略的一层
4.1 从寄存器表提取位域,别从例程提取
手册每个外设章节的最后,都有一张按偏移排列的寄存器表,每张表下面跟一段位域说明。这是整份手册里最该被利用的部分,因为例程会过时、封装会变化,但这张表不会。
我的做法是:把要用的寄存器位表摘成一组宏,紧贴着驱动代码写注释,注明“位号取自手册第几节”。以后换型号或发现行为不符,五分钟能确认是硬件位定义变了还是自己写错了。改寄存器时统一走“读—改—写”,不要直接赋值,避免把保留位写成非默认值。
/* 位号摘自手册对应外设章节的寄存器位表,改代码前先对一遍 */ #define TIM_CR1_CEN_Pos 0U #define TIM_CR1_UDIS_Pos 1U #define TIM_CR1_DIR_Pos 4U #define TIM_CR1_CMS_Pos 5U /* 只改自己关心的位,其余位按读回值保留 */ static inline void reg_set_bits(volatile uint32_t *reg, uint32_t mask) { *reg = (*reg & ~mask) | mask; } static inline void reg_clr_bits(volatile uint32_t *reg, uint32_t mask) { *reg = *reg & ~mask; }函数名已经说明行为,两个参数一个是要动的寄存器地址,一个是要置位或清除的掩码。这两个内联函数看着简单,但它解决的是“直接赋值把别的位清零”这个高频事故——尤其是当手册里标注某个位是“必须保持复位值”时,直接赋值等于给自己埋雷。
4.2 定时器加 ADC 的硬触发链
工业控制里 ADC 采样最忌讳由软件随机触发,采样点跟着任务调度漂。正确做法是让定时器的更新事件去驱动 ADC,采样时刻由硬件掐死,软件只负责读结果。
/* TIM1 产生 1kHz 更新事件,经 TRGO 送给 ADC 作为外部触发源 */ void tim1_pwm_init(void) { RCC->APB2ENR |= RCC_APB2ENR_TIM1EN; (void)RCC->APB2ENR; TIM1->PSC = 240U - 1U; /* 240MHz / 240 = 1MHz 计数频率 */ TIM1->ARR = 1000U - 1U; /* 1MHz / 1000 = 1kHz 更新频率 */ TIM1->CCR1 = 300U; /* 30% 占空比初值 */ TIM1->CCMR1 = (6U << 4); /* OC1M = 110:PWM 模式 1 */ TIM1->CCER |= TIM_CCER_CC1E; /* 通道 1 输出使能 */ TIM1->CR2 |= (2U << 4); /* MMS = 010:更新事件作为 TRGO */ TIM1->BDTR |= TIM_BDTR_MOE; /* 高级定时器必须开主输出,否则引脚不出来 */ TIM1->CR1 |= TIM_CR1_CEN; }PSC 和 ARR 这两个参数决定了控制环的节拍。1kHz 对应 1ms 的电流环周期,如果你的环路跑到 20kHz,把 ARR 改成 50 就行,换算关系写在注释里。MMS 那一位决定 TRGO 的信号源,选的不是更新事件,ADC 的触发相位就会偏,采样点对不上 PWM 中心,电流重构出来的波形会有一层毛刺。MOE 位是高级定时器专属,不开它,寄存器全配对了引脚也没波形,这是新手最常卡住的一步。
ADC 侧只需要把外部触发源选成 TIM1_TRGO,打开对应的规则组通道,再配好 DMA 搬运即可。手册在 ADC 章节的“外部触发”表里列出了每个触发源对应的编号,选错编号的现象是“定时器在跑,ADC 一次都不转换”。
4.3 DMA 与 D-Cache:一致性问题怎么落
Cortex-M7 有 D-Cache,DMA 不经过 Cache。这句话展开就是两个事故:CPU 改了缓冲,Cache 里是新值,内存里还是旧值,DMA 搬走的是旧数据;DMA 搬来新数据到内存,CPU 读的却是 Cache 里的旧行。两个方向都要处理。
方案一,把 DMA 缓冲放到 MPU
本文还有配套的精品资源,点击获取