观前提醒:
作为一篇问题随记大概率是又臭又长,而且过程中会有一大堆的弯路错路,对于看到的内容请读者仔细甄别。
问题随记不定时更新,直到问题解决完结(毕竟你不能要求笔者解决了问题再来重新写
问题提出
问题描述:STM32H750VBT6的Flash空间过小,代码稍微多一点就会将其塞爆。
笔者目的:将板载的一块W25Q64通过QSPI内存映射的方式作为InternalFlash的拓展。
实现平台:Clion
解决思路:
- 修改OpenOCD:根据有类似应用的Board脚本修改stm32h7xx.cfg文件
- 修改ld文件:根据所需的内存布局修改连接脚本文件
- 问题解决
问题探索
修改OpenOCD
所找到的参考文件为:...\openocd\scripts\board\stm32l476g-disco.cfg
内容也不过寥寥几十行:
# This is an STM32L476G discovery board with a single STM32L476VGT6 chip.
# https://www.st.com/en/evaluation-tools/32l476gdiscovery.html
# This is for using the onboard STLINK
source [find interface/stlink.cfg]
transport select hla_swd
# increase working area to 96KB
set WORKAREASIZE 0x18000
# enable stmqspi
set QUADSPI 1
source [find target/stm32l4x.cfg]
# QUADSPI initialization
proc qspi_init { } {
global a
mmw 0x4002104C 0x000001FF 0 ;# RCC_AHB2ENR |= GPIOAEN-GPIOIEN (enable clocks)
mmw 0x40021050 0x00000100 0 ;# RCC_AHB3ENR |= QSPIEN (enable clock)
sleep 1 ;# Wait for clock startup
# PE11: NCS, PE10: CLK, PE15: BK1_IO3, PE14: BK1_IO2, PE13: BK1_IO1, PE12: BK1_IO0
# PE15:AF10:V, PE14:AF10:V, PE13:AF10:V, PE12:AF10:V, PE11:AF10:V, PE10:AF10:V
# Port E: PE15:AF10:V, PE14:AF10:V, PE13:AF10:V, PE12:AF10:V, PE11:AF10:V, PE10:AF10:V
mmw 0x48001000 0xAAA00000 0x55500000 ;# MODER
mmw 0x48001008 0xFFF00000 0x00000000 ;# OSPEEDR
mmw 0x48001024 0xAAAAAA00 0x55555500 ;# AFRH
mww 0xA0001030 0x00001000 ;# QUADSPI_LPTR: deactivate CS after 4096 clocks when FIFO is full
mww 0xA0001000 0x01500008 ;# QUADSPI_CR: PRESCALER=1, APMS=1, FTHRES=0, FSEL=0, DFM=0, SSHIFT=0, TCEN=1
mww 0xA0001004 0x00170100 ;# QUADSPI_DCR: FSIZE=0x17, CSHT=0x01, CKMODE=0
mmw 0xA0001000 0x00000001 0 ;# QUADSPI_CR: EN=1
# memory-mapped read mode with 3-byte addresses
mww 0xA0001014 0x0D002503 ;# QUADSPI_CCR: FMODE=0x3, DMODE=0x1, DCYC=0x0, ADSIZE=0x2, ADMODE=0x1, IMODE=0x1, INSTR=READ
}
$_TARGETNAME configure -event reset-init {
mmw 0x40022000 0x00000004 0x00000003 ;# 4 WS for 72 MHz HCLK
sleep 1
mmw 0x40021000 0x00000100 0x00000000 ;# HSI on
mww 0x4002100C 0x01002432 ;# 72 MHz: PLLREN=1, PLLM=4, PLLN=36, PLLR=2, HSI
mww 0x40021008 0x00008001 ;# always HSI, APB1: /1, APB2: /1
mmw 0x40021000 0x01000000 0x00000000 ;# PLL on
sleep 1
mmw 0x40021008 0x00000003 0x00000000 ;# switch to PLL
sleep 1
adapter speed 4000
qspi_init
}
本着先看懂再模仿的原则,先对文件语言进行了解:
机译:
OpenOCD使用一个称为JimTcl的小型“Tcl解释器”。该编程语言提供了一个简单且可扩展的命令解释器。
本指南中提供的所有命令都是Jim Tcl的扩展。您可以将它们作为简单的命令使用,而无需了解有关Tcl的很多内容。或者,您可以使用它们编写Tcl程序。
你可以在Jim的网站上了解更多关于Jim的信息,https://jim.tcl.tk. 这里有一个积极响应的社区,如果您有任何问题,请加入邮件列表。Jim Tcl维护人员也潜伏在OpenOCD邮件列表中。
文件使用的是Jim-Tcl,无需过多了解细节,直接冲向Tcl Crash Course:
浏览了一下,内容只有6页,开始逐句啃......
手册译文
Tcl速成班
不是每个人都知道Tcl-这不是为了替代学习 Tcl,本章的目的是让您了解 Tcl 脚本的工作原理。
本章是为两类读者而写的:
(1) OpenOCD 用户需要更多地了解 Jim-Tcl 的工作原理,以便他们可以做一些有用的事情,以及 (2) 那些想要向 OpenOCD 添加新命令的用户。
Tcl规则#1
有一个著名的笑话,它是这样说的:
- 规则1:妻子总是对的。
- 规则2:如果你不这么认为,请参见规则1
Tcl的等效说法如下:
- 规则#1:一切都是一个字符串
- 规则2:如果你不这么认为,请参见规则1
正如著名的笑话中所说,规则1的后果是深远的。一旦你理解了规则1,你就会理解Tcl。
Tcl 规则#1b
还有第二对规则。
- 规则#1:控制流不存在。 只有命令
例如:经典的 FOR 循环或 IF 语句不是控制流项,它们是命令,Tcl 中没有控制流这样的东西。
- 规则#2:如果您不这么认为,请参阅规则#1
实际上发生的事情是这样的:按照惯例,有些命令的作用类似于其他语言中的控制流关键字。 其中一个命令是单词“for”,另一个命令是“if”。
每个规则#1 - 所有结果都是字符串
每个 Tcl 命令都会导致一个字符串。 “导致”这个词是故意使用的。 没有结果只是一个空字符串。 记住:规则#1 - 一切都是字符串
Tcl 引用操作符
在 Tcl 脚本的生命周期中,有两个重要的时间段,区别很细微。
- 解析时间
- 评估时间
这里的两个关键项目是“引用的东西”在 Tcl 中的工作方式。 Tcl 有三个主要的引用结构,[方括号]、{花括号}和“双引号”
现在您应该知道
$VARIABLES
总是以$
符号开头。顺便说一句:要设置变量,您实际上使用命令“set”,就像在“set VARNAME VALUE”中一样,很像古老的 BASIC 语言“let x = 1”语句,但没有等号。
- [方括号]
[方括号] 是命令替换。 它的操作很像 Unix Shell 的“反引号”。 [方括号] 操作的结果正好是 1 个字符串。 记住规则#1 - 一切都是一个字符串。 这两个语句大致相同:
# bash example X=‘date‘ echo "The Date is: $X" # Tcl example set X [date] puts "The Date is: $X"
- “双引号”
“双引号”只是简单的引用文本。 $VARIABLES 和 [squarebrackets] 扩展到位 - 然而结果正好是 1 个字符串。 记住规则#1 - 一切都是字符串
set x "Dinner" puts "It is now [date], $x is in 1 hour"
- {Curly-Braces}
{Curly-Braces} 很神奇:$VARIABLES 和[方括号] 会被解析,但不会被扩展或执行。 {Curly-Braces} 就像 BASH shell 脚本中的“单引号”运算符,增加了一个特性:{curly-braces} 可以嵌套,单引号不能。
注意:[date] 是一个不好的例子; 在撰写本文时,Jim/OpenOCD 没有日期命令。
规则1/2/3/4的后果
规则1的后果是深远的。
标记化和执行。
当然,空格、空行和#注释行是按正常方式处理的。在解析脚本时,脚本文件中的每一行(多行)都会根据引用规则进行标记。标记化后,该行立即执行。
多行语句以一个或多个“仍然打开的”{花括号}结尾,最终在几行之后关闭。
命令执行
请记住前面的内容:Tcl中没有“控制流”语句。相反,有一些命令的作用与控制流操作符类似。
命令的执行方式如下:
- 将下一行解析为(argc)和(argv[])。
- 在表中查找(argv[0]),并调用其函数。
- 重复此操作,直到文件结束。
它的工作原理是这样的:
for(;;){ ReadAndParse( &argc, &argv ); cmdPtr = LookupCommand( argv[0] ); (*cmdPtr->Execute)( argc, argv ); }
当解析命令“proc”(创建一个过程函数)时,它在命令行上获得3个参数。
1是 proc (function)的名称,2是参数列表,3是函数体。不是用词的问题: LIST 和 BODY。PROC 命令将这些项目存储在某个表中,以便“LookupCommand ()”可以找到它
FOR 命令
最有趣的命令是 FOR 命令。 在 Tcl 中,FOR 命令通常在 C 中实现。记住,FOR 是一个命令,就像任何其他命令一样。 当解析包含 FOR 命令的 ascii 文本时,解析器会生成 5 个参数字符串,(如果有疑问:请参阅规则 #1)它们是:
- ascii 文本 \'for\'
- 开始文本
- 测试表达式
- 下一个文本
- 正文文本
有点让你想起“main(int argc, char **argv)”,不是吗? 记住规则#1 - 一切都是一个字符串。 关键是:通常许多这些参数都在 {curly-braces} 中 - 因此里面的变量直到以后才被扩展或替换。
请记住,每个 Tcl 命令看起来都像 C 中经典的“main(argc, argv)”函数。在 JimTCL 中 - 它们实际上是这样的:
int MyCommand( Jim_Interp *interp, int *argc, Jim_Obj * const *argvs );
真正的 Tcl 几乎相同。 虽然较新的版本引入了字节码解析器和解释器,但在核心上,它仍然以相同的基本方式运行。
FOR 命令执行
要理解 Tcl,查看 FOR 命令可能是最有帮助的。记住,它是一个 COMMAND 而不是一个控制流结构。
在 Tcl 中有两个底层的 c 辅助函数。
记住规则 # 1-你是一个字符串。
第一个助手解析和执行在 ascii 字符串中找到的命令。命令可以用分号或换行符分隔。在解析时,通过引用规则扩展变量。
第二个助手将ascii字符串作为数值表达式进行求值,并返回一个值。
下面是如何实现FOR命令的示例。下面的伪代码不显示错误处理。
void Execute_AsciiString( void *interp, const char *string ); int Evaluate_AsciiExpression( void *interp, const char *string ); int MyForCommand( void *interp, int argc, char **argv ){ if( argc != 5 ){ SetResult( interp, "WRONG number of parameters"); return ERROR; } // argv[0] = the ascii string just like C // Execute the start statement. Execute_AsciiString( interp, argv[1] ); // Top of loop test for(;;){ i = Evaluate_AsciiExpression(interp, argv[2]); if( i == 0 ) break; // Execute the body Execute_AsciiString( interp, argv[3] ); // Execute the LOOP part Execute_AsciiString( interp, argv[4] ); } // Return no error SetResult( interp, "" ); return SUCCESS; }
所有其他命令 IF、 WHILE、 FORMAT、 PUTS、 EXPR 都以相同的基本方式工作。
OpenOCD Tcl的使用
23.6.1源和查找命令
哪里:在许多配置文件中
示例:source [find FILENAME]
记住解析规则
- find 命令在方括号中,执行时带参数FILENAME。 它应该找到并返回具有该名称的文件的完整路径; 它使用内部搜索路径。 RESULT 是一个字符串,它被替换到命令行中以代替括号中的 find 命令。 (不要尝试使用包含“#”字符的 FILENAME。该字符开始 Tcl 注释。)
- 使用生成的文件名执行source命令; 它读取文件并作为脚本执行。
格式化命令
哪里:一般发生在很多地方。
Tcl 没有像 printf() 这样的命令,而是有format,这实际上更像 sprintf()。
例子:
set x 6 set y 7 puts [format "The answer: %d" [expr $x * $y]]
- SET 命令创建 2 个变量,X 和 Y。
- double [nested] EXPR 命令执行数学运算
EXPR 命令产生字符串形式的数值结果。
参考规则#1
- 执行格式命令,产生单个字符串
参考规则#1。
- PUTS 命令输出文本。
正文或内联文本
哪里:各种 TARGET 脚本。
#1 Good proc someproc {} { ... multiple lines of stuff ... } $_TARGETNAME configure -event FOO someproc #2 Good - no variables $_TARGETNAME configure -event foo "this ; that;" #3 Good Curly Braces $_TARGETNAME configure -event FOO { puts "Time: [date]" } #4 DANGER DANGER DANGER $_TARGETNAME configure -event foo "puts Time: [date]"
$TARGETNAME
是一个OpenOCD变量约定。$TARGETNAME
表示上次创建的目标,每次值的修改都会创建一个新的目标。记住解析规则。 当解析 ascii 文本时,$TARGETNAME
变成一个简单的字符串,目标的名称恰好是一个 TARGET(对象)命令。- -event参数的第二个参数是
TCLBODY
。有4个示例:
- TCLBODY是一个简单的字符串,恰好是一个proc名称
- TCLBODY 是由分号分隔的几个简单命令
- TCLBODY是一个多行{curly brace}带引号的字符串
- TCLBODY是一个字符串,其中包含展开的变量。
最后,当目标事件 FOO 发生时,将计算 TCLBODY。方法 # 1和 # 2在功能上是相同的。方法 # 3和 # 4更有趣。
什么是 TCLBODY?
记住解析规则。在案例#3中,{花括号}表示
$VARS
和[方括号]在事件发生后展开,并对文本进行求值。在第4种情况下,它们在执行“目标对象命令”之前被替换。这在替换$TARGETNAME
的同时发生。万一发生这种情况,日期永远不会改变。{BTW:[date]是一个糟糕的例子;在本文中,Jim/OpenOCD没有date命令}全局变量
哪里: 在编写自己的 PROC 时,您可能会发现这一点
简而言之,在 PROC 中,如果您需要访问全局变量,您必须这样说。参见“ upvar”。例子:
proc myproc { } { set y 0 #Local variable Y global x #Global variable X puts [format "X=%d, Y=%d" $x $y] }
其他 Tcl 技巧
动态变量创建
# Dynamically create a bunch of variables. for { set x 0 } { $x < 32 } { set x [expr $x + 1]} { # Create var name set vn [format "BIT%d" $x] # Make it a global global $vn # Set it. set $vn [expr (1 << $x)] }
动态过程/命令创建
# One "X" function - 5 uart functions. foreach who {A B C D E} proc [format "show_uart%c" $who] { } "show_UARTx $who" }
参考代码解析
阅读过了Tcl速成章节,再结合已知的知识,参考代码的很多部分都可以看懂了。
#选择调试器接口脚本为stlink.cfg
source [find interface/stlink.cfg]
#选择接口为hla_swd
transport select hla_swd
#将QUADSPI标志位置位以便stm32l4x.cfg文件中使用
set QUADSPI 1
#选择目标芯片脚本为stm32l4x.cfg
source [find target/stm32l4x.cfg]
下面的代码定义了一个名为qspi_init的函数,虽然名字上面很明显的说明了是用于QSPI的初始化,不过要想搞懂里面干了什么还需要一点其他的内容配合:ST寄存器边界地址表、寄存器地图以及IO复用表
RCC:
GPIOE:
QUADSPI:
PORTE的IO复用表:
一些目标指令
mdd,mdw,mdh,mdb:
memory display doublewords(64bit)/words(32bit)/halfwords(16bit)/bytes(8bit)
mwd,mww,mwh,mwb:
memory write doublewords(64bit)/words(32bit)/halfwords(16bit)/bytes(8bit)
两类指令的格式都是:
mdd [phys] addr [count]
mrw,mmw:
memory read/modify word
指令格式:
mrw/mmw reg setbits clearbits
proc qspi_init { } {
#定义全局变量a(没看出来有啥用
global a
#将RCC_AHB2ENR的低9位置位
#使能GPIOA到GPIOI的时钟
mmw 0x4002104C 0x000001FF 0
#将RCC_AHB3ENR的QSPIEN位置位
#使能QSPI时钟
mmw 0x40021050 0x00000100 0
#等待时钟开启
sleep 1
#将GPIOE_MODER寄存器的高12位隔位置位
#将PIN10-15设置为模式10: Alternate function mode
mmw 0x48001000 0xAAA00000 0x55500000
#将GPIOE_OSPEED寄存器的高12位置位
#将PIN10-15设置为速度等级11: Very high speed
mmw 0x48001008 0xFFF00000 0x00000000
#将GPIOx_AFRH寄存器的高24位隔位置位
#将PIN10-15设置为AF10
mmw 0x48001024 0xAAAAAA00 0x55555500
#将QUADSPI_LPTR寄存器的第15位置位
#指示FIFO满后QUADSPI等待4096个时钟周期
mww 0xA0001030 0x00001000
#将QUADSPI_CR寄存器的PSC=1,APMS、TOIE、TCEN位置位
#Qspi时钟2分频、出现匹配项时自动轮询停止、使能超时中断、使能超时计数器
mww 0xA0001000 0x01500008
#设置QUADSPI_DCR寄存器的FSIZE位与CSHT位
#FLASH大小为16M,片选引脚拉高2个时钟周期,SPImode0
mww 0xA0001004 0x00170100
#将QUADSPI_CR寄存器的最低位置位
#使能QSPI
mmw 0xA0001000 0x00000001 0
#设置QUADSPI_CCR寄存器:FMODE0b11、DMODE0b01、ADSIZE0b10、
#ADMODE0b01、IMODE0b01、INSTRUCTION0b00000011
#内存映射模式、单行数据、24位地址、
#单行地址、单行指令、发送指令READ
mww 0xA0001014 0x0D002503
}
接下来再来看看最后的复位初始化干了什么:
$_TARGETNAME configure -event reset-init {
#设置FLASH_ACR的LATENCY为0b100
#HCLK与FLASH之间等待4个状态
mmw 0x40022000 0x00000004 0x00000003
#等待设置生效
sleep 1
#将RCC_CR的HSION位置位
#使能外部高速晶振
mmw 0x40021000 0x00000100 0x00000000
#设置RCC_PLLCFGR寄存器:PLLEN置位、PLLN0b0100100\
#PLLM0b011、PLLSRC0b10->72MHz
#PLLSAI3CLK输出使能、锁相倍频因子36、主PLL分频4、高速内部时钟源:PLL
mww 0x4002100C 0x01002432
#设置RCC_CFGR寄存器:STOPWUCK置位、SW0b01
#HSI16振荡器选择为从停止时钟和CSS备份时钟唤醒
#HSI16作为系统时钟
mww 0x40021008 0x00008001
#将RCC_CR的PLLON位置位
#使能主锁相环
mmw 0x40021000 0x01000000 0x00000000
#等待设置生效
sleep 1
#设置RCC_CFGR寄存器:SW0b11
#锁相环作为系统时钟
mmw 0x40021008 0x00000003 0x00000000
#等待设置生效
sleep 1
#设置仿真器频率
adapter speed 4000
#调用QSPI初始化函数
qspi_init
}
类比参考代码自行撰写
全部看懂了之后,笔者开始模仿这段程序来撰写QSPI初始化函数和复位初始化函数:
source [find interface/jlink.cfg]
transport select swd
# increase working area to 96KB
set WORKAREASIZE 0x18000
# enable stmqspi
set QUADSPI 1
source [find target/stm32h7x.cfg]
# QUADSPI initialization
proc qspi_init { } {
global a
#RCC_AHB4ENR GPIOA-GPIOK
mmw 0x580244E0 0x000007FF 0
#RCC_AHB3ENR QSPI
mmw 0x580244D4 0x00004000 0
#等待
sleep 1
#PB3-AF9 PB6-AF10 PD11-AF9 PD12-AF9 PD13-AF9
#PE2-AF9
#GPIO模式设置
mmw 0x58020400 0x00002080 0x00001040
mmw 0x58020C00 0x0A800000 0x05400000
mmw 0x58021000 0x00000020 0x00000010
#GPIO输出速度
mmw 0x58020408 0x000030C0 0x00000000
mmw 0x58020C08 0x0FC00000 0x00000000
mmw 0x58021000 0x00000030 0x00000000
#GPIO复用功能
mmw 0x58020420 0x0A009000 0x05006000
mmw 0x58020C24 0x00999000 0x00666000
mmw 0x58021020 0x00000900 0x00000600
#QSPI
mww 0x52005030 0x00001000
mww 0x52005000 0x01500008
mww 0x52005004 0x00170100
mmw 0x52005000 0x00000001 0
mww 0x52005014 0x0D002503
}
$_TARGETNAME configure -event reset-init {
mmw 0x40022000 0x00000004 0x00000003
sleep 1
#CR
mmw 0x58024400 0x00000001 0x00000000
#PLLCFGR
mww 0x5802442c 0x01FF0009
#CFGR
mww 0x58024410 0x00000019
#CR enPLLON
mmw 0x58024400 0x01000000 0x00000000
sleep 1
#CFGR
mmw 0x58024410 0x0000001B 0x00000000
sleep 1
adapter speed 2000
qspi_init
}
修改链接脚本
修改链接脚本的第一步是了解链接脚本,笔者拜读了链接脚本的官方手册,将其中的部分内容进行了翻译并且记录在了这篇文章中。
阅读之后大致了解了应该如何对链接脚本进行修改,于是做出了如下修改:
在MEMORY命令中添加外部FLASH:
/* Specify the memory areas */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 128k
ExFlash(rx) : ORIGIN = 0x90000000, LENGTH = 8M
DTCMRAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
RAM_D1 (xrw) : ORIGIN = 0x24000000, LENGTH = 512K
RAM_D2 (xrw) : ORIGIN = 0x30000000, LENGTH = 288K
RAM_D3 (xrw) : ORIGIN = 0x38000000, LENGTH = 64K
ITCMRAM (xrw) : ORIGIN = 0x00000000, LENGTH = 64K
}
然后在内存描述中加入外部FLASH的代码段:
.extext :
{
. = ALIGN(4);
extext_start = .;
/*files goes into external flash*/
extext_end = .;
. = ALIGN(4);
} >ExFlash
然后只需要将需要放在外部FLASH的代码放到extext段即可。
开心的编译然后烧录,编译是过了,但是烧录报错了。
报出的错误是“Bank is invalid”,猜测是由于QSPI的初始化有问题导致的。
关于猜测的过程:
首先是查阅了ST官方的一本手册:AN5188,大致了解了一下XiP和BootROM两种启动方式,萌生了移植ST官方例程的想法,不过后面想了想决定还是没有移植。
之后向一位有经验的学长请教了一下相关的问题,和学长聊了很多,于是对我现在遇到的问题以及很多之前的疑点都有了一些了解:
- OpenOCD里面开了QSPI的作用是什么?
OpenOCD烧录片外FLASH的过程是这样的:先向芯片中写入一份程序,作用大概是将单片机作为一个下位机转发数据通过QSPI写入FLASH,因此要开QSPI。
OpenOCD在整个烧录的过程中其实作为一个服务器,我们可以通过telnet或者gdb预期进行通信,因此我们可以开启windows的Telnet服务来连接OpenOCD的服务器对硬件进行命令行方式的调试。
- 来看看我在调试的时候发现的惊喜:
通过OpenOCD的探针对STM32H750的内部FLASH进行检测得到的FLASH info让然喜出望外:
就是说内部FLASH其实有整整2M的大小,而这2M的空间前1M是可以使用的,后面1M是被写保护的,换句话说:H7其实有1M的空间可以用。