由于Trace在ARM Cortex-M上,相比SWD/JTAG需要单独拉起Trace信号,本篇将着重篇幅介绍软硬件搭建环节。
1 连接ART-PI
事实上J-Trace包装盒内附赠了一块STM32F407的板子,用于下载官网Trace例程,学习Ozone调试流程之用。
这张板子太丐了,还卖$99,除了点灯就没别的了,所以在验证过J-Trace之后,我们直接上手去对接ART-PI。
1.1 硬件连接
首先确定ART-PI是否有Trace Pin的扩展能力,通过查看ART-PI硬件原理图,确定了如下管脚:
确定IO这一步的过程是比较让人激动的,毕竟说明书里没有注明这个扩展,可以认为,ART-PI设计初期的IO选定是花了心思的。
当然,知道IO之后我们就要去连接它,根据上篇的介绍我们知道,实现Stream Trace需要SWD+Trace共同实现,Segger也提供了1.27mm间距的2x10Pin接口。所以结合ART-PI背面预留的SWD焊盘,使用不同高度的【PogoPin】,这种测试接针,在接触面上有弹性,可作为烧录架使用,完美解决了调试部分,同时不会损失原有的扩展性。据此,可以简单画个孔位对齐的板子出来。
焊接好器件,使用M3孔径的螺柱就能锁紧开发板,可以看见PogoPin的连接非常稳健。
在硬件连通阶段建议结合逻辑分析仪抓一抓波形,能够很容易确定当前内核Trace的工作状态。
以上,就完成了ART-PI的外部调试器的引入,细心的同学可能会发现,在SWD部分,信号和ST-Link直连的,出现了两个调试器接入一个MCU的情况,这个的确不合理,但实测ST-Link只当串口情况下,几乎不会干扰SWD总线,两个调试器分时复用就好了。此外,这样也能用ST-Link下载,Jlink调试,属实NTR了。。
1.2 信号通路
在开展这一步之前,需要明确 Core -> ETM -> TPIU 这条信号通路(ATB Bus)上的初始条件。
1.2.1 STM32H7 Trace 信号通路
查阅STM32H7手册(rm0433),可以看见APB-D信号流需要从Core到ETM,处理后的trace信号送至CSTF合路器,最后送到TPIU并由Trace Pin输出到片外调试器。
**名词解释:
可以看出,Cortex-M7的CoreSight界面相比Cortex-M4有更多扩充,总线通路上要更灵活,在TPIU这一侧就可以支持:
ETM信号经合路器旁路到TPIU直出。
ETM信号经合路器送入ETF进行FIFO缓冲,再由TPIU输出。
ITM经Replicator分配到合路器,经由TPIU输出。
ETF/TPIU 均可由外部触发源刷新行为。
我们不需要一一实现,只需要实现最基础的Trace流输出。
更深入的,可以参考ARM官方CoreSight文档:
IHI 0031C (ID080813) - Arm® Debug Interface Architecture Specification ADIv5.0 to ADIv5.2, Issue C
DDI 0480F (ID100313) - Arm® CoreSight™ SoC-400 r3p2 Technical Reference Manual, Issue G
DDI 0461B (ID010111) - Arm® CoreSight™ Trace Memory Controller r0p1 Technical Reference Manual, Issue B
DDI 0314H - Arm® CoreSight™ Components Technical Reference Manual, Issue H
DDI 0403D (ID100710) - Arm®v7-M Architecture Reference Manual, Issue E.b
DDI 0494-2a (ID062813) - Arm® CoreSight™ ETM™-M7 r0p1 Technical Reference Manual, Issue D
1.2.2 时钟域与电源域
上述Trace电路原理上与SWD/JTAG的最大区别是,需要手动初始化它才可以使用。我们使能它之前需要再了解它的Power Domain和Clock Domain。
可以看见,D1 电源域覆盖了Trace全程,所以可以忽略这部分的初始化,除非有在shutdown模式时还要Trace的需求。
时钟域也是如此,但我们需要关注:rcc_c_ck时钟、TRACECLK。这里给个我浅显的理解:CPU作为指令流的生产者,设计原理上它不会停止,作为指令流的接收者,受限于【带宽】,同时需要结合Trace粒度需求,给出合适的时钟搭配。
例如,多数高速MCU平台下,如果想要每个指令都捕捉到,就需要给CPU降速运行,如果需要全速运行,Cortex-M7可以实现一定限度的Trigger-Buffered Stream,取决于厂商平台为这部分预留的SRAM大小。
Notice:【带宽】这里的制约条件:
J-Trace 的最高Stream Trace clock。
Trace IO 信号翻转能力,STM32H7 上限是133Mhz。
1.3 手动配置时钟
根据前面列举的条件,我们可以尝试计算出适合ART-PI能被Trace住的最高主频参数:
预定的要求:
跑满 Trace IO 翻转速率。
尽可能CPU频率更高。
使用 cubeMX 辅助调整时钟树:
- TraceCLK 分频到最高 133Mhz。
- CPU 主频为TraceCLK 3倍频,即400Mhz。
Notice: CPU时钟实际为TraceCLK的6倍,TraceCLK由于TPIU机制(DDR),输出66.6Mhz频率。
得出以下时钟树:
图中的时钟树配置,应当用在被调试的工程中去。
到这一步之前,我们都在讲实现原理,可能之前没有接触过的同学此时已经云里雾里了,这里做下本章的总结:使用Trace之前,需要初始化Trace通路上的一切{时钟、电源域、信号通路、IO}。
2 Ozone工程准备
2.1 创建具备Stream Trace功能的Ozone工程
可以根据上述的Demo,基于它作为模板,简单搭建Ozone工程。
2.1.1 创建Ozone工程
需要先理解Ozone是个什么类型的软件:
Ozone只需要编译产物就可以支持图形界面的调试,不依赖工具链,具备完善的调试交互体验。
选用Ozone的原因:
只要是Jlink就能使用。
去IDE化,不依赖IDE环境。
Ozone能够发挥J-Trace优势。
我们装好Ozone之后就直接打开使用,可以使用project wizard辅助创建一个工程,这步也可以跟着demo做。
添加了对应MCU的SVD文件,方便手动访问完整的外设寄存器,如果没有也可跳过。
添加Jlink连接方式,这步也可以之后做。
添加.axf、.elf等对应IDE产物文件,如果只有.bin,提供ROM偏移量,则可以基于disassemby的调试。
Notice: 如果顺利检索elf文件,则可以通过elf解析到的路径打开对应的源文件。
首次使用Ozone可以先不打开Trace,先熟悉各类调试窗口、按钮和工具。
由于Ozone支持启动前后各阶段的弱函数,可以在一些特殊启动场景,例如:debug on RAM / debug after bootloader 这些,可以实现对应阶段的弱函数,手动干预MCU的运行。
Notice: 有关Ozone的深入使用,请务必参考《Ozone_UM08025》.pdf
2.1.2 配置JlinkScript
之前谈到,Trace需要手动初始化。我们当然可以在芯片初始化阶段,将Trace相关的参数全部初始化完毕,Ozone在线调试时,只有在Trace模块初始化之后的指令流才能被抓住,这样做是没有问题的。
Segger推荐了第二种做法,即利用Jlink自动化脚本,在每次需要Trace时,例如每次Halt CPU之后,jlink会通过SWD直接访问外设寄存器,并写入对应配置。所以在ART-PI的jdebug工程中,通过实现BeforeTargetConnect()函数的方式,在每次连接MCU时配置好Trace功能:
void BeforeTargetConnect (void) {
//
// Trace pin init is done by J-Link script file as J-Link script files are IDE independent
//
Project.SetJLinkScript("$(ProjectDir)/Ozone/ST_STM32H750_400Mhz_Traceconfig.JLinkScript");
}
对于ART-PI,我们要在jlinkscript脚本中实现:
GPIO初始化为Trace Pin复用,并配置好合适的IO推力。
MCUDBG寄存器开启D1DBGCKEN/D3DBGCKEN时钟。
参考资源:
可以参考segger官方测试小板Tracing_on_ST_STM32F407,这个例程提供了jlinkscript脚本,如果要适配自己的平台,这部分的内容需要仔细揣摩。
也可以参考segger官方的STM32H743_Trace_Demo,但是这个例程对jlinkscript脚本进行了编译,变成了pex格式,反而没有太多参考价值。
这里也给出我自己按照上述流程思路,独立写好的jlinkscript脚本,实测比较稳定:
/*********************************************************************
File : ST_STM32H750_400Mhz_Traceconfig.JLinkScript
Author : stackryan (yuanjyjyj@outlook.com)
Purpose : Examplescript to modify TracePortWidth and Pin init
Literature:
[1] J-Link User Guide
Additional information:
For more information about public functions that can be implemented in order to customize J-Link actions, please refer to [1]
*/
/*********************************************************************
-
ConfigTargetSettings
- Function description
- Called before InitTarget(). Mainly used to set some global DLL variables to customize the
- normal connect procedure. For ARM CoreSight devices this may be specifying the base
- address of some CoreSight components (ETM, …) that cannot be auto-detected by J-Link
- due to erroneous ROM tables etc. May also be used to specify the device name in case
- debugger does not pass it to the DLL.
- Notes
- (1) May not, under absolutely NO circumstances, call any API functions that perform target communication.
- (2) Should only set some global DLL variables
- Return value
-
= 0 O.K.
-
< 0 Error
-
-1 Unspecified error
*/
int ConfigTargetSettings(void) {
//
// Enable erase for all flash banks (e.g. QSPI)
//
JLINK_SYS_Report("-ConfigSettings.-");
JLINK_ExecCommand("CORESIGHT_SetCSTFBaseAddr = 0xE00F3000 ForceUnlock = 1 APIndex = 2");
JLINK_ExecCommand("CORESIGHT_SetTPIUBaseAddr = 0xE00F5000 ForceUnlock = 1 APIndex = 2");
JLINK_ExecCommand("CORESIGHT_SetTMCBaseAddr = 0xE00F4000 ForceUnlock = 1 APIndex = 2");
return 0;
}
/*********************************************************************
-
OnTraceStart()
- Function description
- If present, called right before trace is started.
- Used to initialize MCU specific trace related things like configuring the trace pins for alternate function.
- Return value
-
= 0: O.K.
-
< 0: Error
- Notes
- (1) May use high-level API functions like JLINK_MEM_ etc.
- (2) Should not call JLINK_TARGET_Halt(). Can rely on target being halted when entering this function
*/
int OnTraceStart(void) {
U32 RCC_AHB4ENR_Addr;
U32 GPIOE_MODER_Addr;
U32 GPIOE_PUPDR_Addr;
U32 GPIOE_OSPEEDR_Addr;
U32 GPIOE_AFRL_Addr;
U32 GPIOG_MODER_Addr;
U32 GPIOG_PUPDR_Addr;
U32 GPIOG_OSPEEDR_Addr;
U32 GPIOG_AFRH_Addr;
U32 DBGMCU_CR_Addr;
U32 iTCLK;
U32 iTD0;
U32 iTD1;
U32 iTD2;
U32 iTD3;
U32 EdgeTCLK;
U32 EdgeTD;
U32 v;
U32 PortWidth;
//
// Adjust sampling point of trace pin (Optional: not needed for this cpu)
//
//JLINK_ExecCommand("TraceSampleAdjust TD=2000");
//
// Set Trace Portwidth(Optional): Default 4 Pin Trace, other possibilities: 1, 2, 4
//
//JLINK_TRACE_PortWidth = 4;
//
// RCC_AHB4ENR_Addr 0x580244E0 Periphal clock register
// GPIOE_MODER: 0x58021000 GPIO Port mode register
// GPIOE_PUPDR: 0x5802100C GPIO Pullup/Puldown register
// GPIOE_OSPEEDR: 0x58021008 GPIO output speed register
// GPIOE_AFRL: 0x58021020 GPIO Alternate function low register
// DBGMCU_CR: 0x5C001004 Debug MCU register
//
// PE2 => TCLK
// PG13 => TD0
// PG14 => TD1
// PE5 => TD2
// PE6 => TD3
//
//
// Init register addresses
//
JLINK_SYS_Report("Start: Initializing trace pins.");
RCC_AHB4ENR_Addr = 0x580244E0;
GPIOE_MODER_Addr = 0x58021000;
GPIOE_PUPDR_Addr = 0x5802100C;
GPIOE_OSPEEDR_Addr = 0x58021008;
GPIOE_AFRL_Addr = 0x58021020;
GPIOG_MODER_Addr = 0x58021800;
GPIOG_PUPDR_Addr = 0x5802180C;
GPIOG_OSPEEDR_Addr = 0x58021808;
GPIOG_AFRH_Addr = 0x58021824;
DBGMCU_CR_Addr = 0x5C001004;
iTCLK = 2;
iTD0 = 13;
iTD1 = 14;
iTD2 = 5;
iTD3 = 6;
PortWidth = JLINK_TRACE_PortWidth;
//
// Set drivestrength
// 0: Low speed
// 1: Medium speed
// 2: High speed
// 3: Very high speed
//
EdgeTCLK = 3;
EdgeTD = 3;
//
// Init pins
//
v = JLINK_MEM_ReadU32(RCC_AHB4ENR_Addr);
JLINK_MEM_WriteU32(RCC_AHB4ENR_Addr, v | 1 << 6); // Enable clock for GPIOG
v = JLINK_MEM_ReadU32(RCC_AHB4ENR_Addr);
JLINK_MEM_WriteU32(RCC_AHB4ENR_Addr, v | 1 << 4); // Enable clock for GPIOE
//
// TCLK init
//
v = JLINK_MEM_ReadU32(GPIOE_MODER_Addr);
v &= ~(3 << (2 * iTCLK)); // Mask Mode register
v |= (2 << (2 * iTCLK)); // Set alt function mode
JLINK_MEM_WriteU32(GPIOE_MODER_Addr, v);
v = JLINK_MEM_ReadU32(GPIOE_PUPDR_Addr);
v &= ~(3 << (2 * iTCLK)); // Mask PUP register
v |= (1 << (2 * iTCLK)); // Set PUP register (Pullup)
JLINK_MEM_WriteU32(GPIOE_PUPDR_Addr, v);
v = JLINK_MEM_ReadU32(GPIOE_OSPEEDR_Addr);
v &= ~(3 << (2 * iTCLK)); // Mask OSPEED register
v |= (EdgeTCLK << (2 * iTCLK)); // Set OSPEED register (very high speed)
JLINK_MEM_WriteU32(GPIOE_OSPEEDR_Addr, v);
v = JLINK_MEM_ReadU32(GPIOE_AFRL_Addr);
v &= ~(15 << (4 * iTCLK)); // Select alt func 0
JLINK_MEM_WriteU32(GPIOE_AFRL_Addr, v);
//
// TD0 init
//
v = JLINK_MEM_ReadU32(GPIOG_MODER_Addr);
v &= ~(3 << (2 * iTD0)); // Mask Mode register
v |= (2 << (2 * iTD0)); // Set alt function mode
JLINK_MEM_WriteU32(GPIOG_MODER_Addr, v);
v = JLINK_MEM_ReadU32(GPIOG_PUPDR_Addr);
v &= ~(3 << (2 * iTD0)); // Mask PUP register
v |= (1 << (2 * iTD0)); // Set PUP register (Pullup)
JLINK_MEM_WriteU32(GPIOG_PUPDR_Addr, v);
v = JLINK_MEM_ReadU32(GPIOG_OSPEEDR_Addr);
v &= ~(3 << (2 * iTD0)); // Mask OSPEED register
v |= (EdgeTD << (2 * iTD0)); // Set OSPEED register (very high speed)
JLINK_MEM_WriteU32(GPIOG_OSPEEDR_Addr, v);
v = JLINK_MEM_ReadU32(GPIOG_AFRH_Addr);
v &= ~(15 << (4 * (iTD0 - 8))); // Select alt func 0
JLINK_MEM_WriteU32(GPIOG_AFRH_Addr, v);
//
// TD1 init
//
if (PortWidth > 1) {
v = JLINK_MEM_ReadU32(GPIOG_MODER_Addr);
v &= ~(3 << (2 * iTD1)); // Mask Mode register
v |= (2 << (2 * iTD1)); // Set alt function mode
JLINK_MEM_WriteU32(GPIOG_MODER_Addr, v);
v = JLINK_MEM_ReadU32(GPIOG_PUPDR_Addr);
v &= ~(3 << (2 * iTD1)); // Mask PUP register
v |= (1 << (2 * iTD1)); // Set PUP register (Pullup)
JLINK_MEM_WriteU32(GPIOG_PUPDR_Addr, v);
v = JLINK_MEM_ReadU32(GPIOG_OSPEEDR_Addr);
v &= ~(3 << (2 * iTD1)); // Mask OSPEED register
v |= (EdgeTD << (2 * iTD1)); // Set OSPEED register (very high speed)
JLINK_MEM_WriteU32(GPIOG_OSPEEDR_Addr, v);
v = JLINK_MEM_ReadU32(GPIOG_AFRH_Addr);
v &= ~(15 << (4 * (iTD1 - 8))); // Select alt func 0
JLINK_MEM_WriteU32(GPIOG_AFRH_Addr, v);
}
//
// TD2 & TD3 init
//
if (PortWidth > 2) {
v = JLINK_MEM_ReadU32(GPIOE_MODER_Addr);
v &= ~(3 << (2 * iTD2));
v |= (2 << (2 * iTD2));
JLINK_MEM_WriteU32(GPIOE_MODER_Addr, v);
v = JLINK_MEM_ReadU32(GPIOE_PUPDR_Addr);
v &= ~(3 << (2 * iTD2));
v |= (1 << (2 * iTD2));
JLINK_MEM_WriteU32(GPIOE_PUPDR_Addr, v);
v = JLINK_MEM_ReadU32(GPIOE_OSPEEDR_Addr);
v &= ~(3 << (2 * iTD2));
v |= (EdgeTD << (2 * iTD2));
JLINK_MEM_WriteU32(GPIOE_OSPEEDR_Addr, v);
v = JLINK_MEM_ReadU32(GPIOE_AFRL_Addr);
v &= ~(15 << (4 * iTD2));
JLINK_MEM_WriteU32(GPIOE_AFRL_Addr, v);
v = JLINK_MEM_ReadU32(GPIOE_MODER_Addr);
v &= ~(3 << (2 * iTD3));
v |= (2 << (2 * iTD3));
JLINK_MEM_WriteU32(GPIOE_MODER_Addr, v);
v = JLINK_MEM_ReadU32(GPIOE_PUPDR_Addr);
v &= ~(3 << (2 * iTD3));
v |= (1 << (2 * iTD3));
JLINK_MEM_WriteU32(GPIOE_PUPDR_Addr, v);
v = JLINK_MEM_ReadU32(GPIOE_OSPEEDR_Addr);
v &= ~(3 << (2 * iTD3));
v |= (EdgeTD << (2 * iTD3));
JLINK_MEM_WriteU32(GPIOE_OSPEEDR_Addr, v);
v = JLINK_MEM_ReadU32(GPIOE_AFRL_Addr);
v &= ~(15 << (4 * iTD3));
JLINK_MEM_WriteU32(GPIOE_AFRL_Addr, v);
}
//
// Config DBUGMCU
//
v = JLINK_MEM_ReadU32(DBGMCU_CR_Addr); // Debug MCU enables traceclk (STM32H7 specific)
v &= ~(1 << 20); // Mask Traceclk Register
v |= (1 << 20); // Enable Traceclk
v |= (1 << 21); // D1DBGCKEN
v |= (1 << 22); // D3DBGCKEN
JLINK_MEM_WriteU32(DBGMCU_CR_Addr, v);
JLINK_SYS_Report("End: Initializing trace pins.");
return 0;
}
2.1.3 实现 Stream Trace
按照上述配置并刷新好工程之后,可以尝试打开Trace功能,并进入调试状态,在点击halt或者任意step按键后,Trace信号就会被正常开启,此时逻辑分析仪上可以抓到正常的Trace波形了。
这里逻辑分析仪选用了4通道数量,采样频率400Mhz,TD3并未采集,不影响预期效果。
此时,Ozone上可以打开Instruction Trace窗口,并且能够看见海量指令流的记录了。在非侵入式调试下,能够快速回溯历史指令流,找到死机的原因。
通过code profile窗口可以看见,对于只有点灯任务的ART-PI来说,系统资源主要都在idle线程上运行着。通过较长时间的挂测,可以统计各函数体的实时代码覆盖率和指令覆盖率。这项功能在需要优化的业务中会非常实用,能够轻松看出哪些资源需要被优化。
3 总结
使用Ozone配合J-Trace完整体验stream trace的流程,相比基于IDE的调试是复杂些的,但它提供了工程师以更全的视角,更有说服力的去解决剩下5%的问题,用好它,能够提供远超调试器本身的价值。
当然,相比更贵的调试系统Lauterbach,J-Trace还不够与其相提并论,这款产品更像是面向嵌入式开发者的、灵活的解决方案。如果根据自己的产品需求,能够设计配套的工具脚本,实现更简便的调试闭环,那么这把利器也会跟着越磨越快。
原作者:StackYuan