目录
MDK硬件调试-Debug printf Viewer窗口显示打印信息
断点窗口
设置断点观察数据情况
我们知道常规的断点调试是在想观察哪里的问题时就在对应的代码地址设置断点,并且一旦运行到断点位置会让程序自动暂停运行,这种断点调试功能确实为开发者解决 bug 立下了汗马功劳,但是这种方式有很大的局限性,因为很多时候我们并不需要让程序停下来,而只想知道是否在这段代码运行过,或者说发生问题的位置根本不能停下来,否则就会让整个系统功能出现问题,比如中断处理函数的调试,程序一旦停下了也就失去了所有中断的后续响应;比如两个设备通信,一方采用常规断点的方式调试,肯定会打断正常的通信过程,而这可不是我们想要的,我们只想知道在收到或发送数据后得到环境快照,而并不想让程序停下来。可以先参考https://blog.csdn.net/guangod/article/details/99573665来实现。
MDK硬件调试-Debug printf Viewer窗口显示打印信息
平时大家在调试代码阶段都喜欢用printf函数输出打印信息,用来查看程序代码的执行情况。使用printf函数调试代码无非有两种情况:一种是使用目标板地硬件资源对外输出打印信息,但是这样会浪费硬件资源;另一种情况是硬件仿真可以利用J-Link等在线仿真工具实现打印信息的输出。由于需要重定向fputc函数,所以二者选其一,在资源步紧张的条件下,浪费硬件资源端口也没有什么问题,但是有些项目的硬件资源比较紧张时,这时就只能通过MDK输出一些打印信息。MDK实现硬件打印调试的方式有两种:ITM机制和semihosting(半主机)机制。
ITM机制
接下来总结一些ITM机制在MDK调试串口打印输出信息的步骤:
1、建立.c文件主要实现fputc重定向,并将其添加到工程目录中,具体内容如下:
#include <stdio.h>
#define ITM_Port8(n) (*((volatile unsigned char*)(0xE0000000 + 4*n)))
#define ITM_Port16(n) (*((volatile unsigned short*)(0xE0000000 + 4*n)))
#define ITM_Port32(n) (*((volatile unsigned long*)(0xE0000000 + 4*n)))
#define DEMCR (*((volatile unsigned long*)(0xE000EDFC)))
#define TRCENA 0x01000000
struct __FILE { int handle; /* Add whatever you need here */ };
FILE __stdout;
FILE __stdin;
int fputc(int ch, FILE *f)
{
if (DEMCR & TRCENA)
{
while (ITM_Port32(0) == 0);
ITM_Port8(0) = ch;
}
return(ch);
}
2、建立.ini调试文件,文件的具体内存如下:
/******************************************************************************/
/* STM32DBG.INI: STM32 Debugger Initialization File */
/******************************************************************************/
// <<< Use Configuration Wizard in Context Menu >>> //
/******************************************************************************/
/* This file is part of the uVision/ARM development tools. */
/* Copyright (c) 2005-2007 Keil Software. All rights reserved. */
/* This software may only be used under the terms of a valid, current, */
/* end user licence from KEIL for a compatible version of KEIL software */
/* development tools. Nothing else gives you the right to use this software. */
/******************************************************************************/
FUNC void DebugSetup (void) {
// <h> Debug MCU Configuration
// <o1.0> DBG_SLEEP <i> Debug Sleep Mode
// <o1.1> DBG_STOP <i> Debug Stop Mode
// <o1.2> DBG_STANDBY <i> Debug Standby Mode
// <o1.5> TRACE_IOEN <i> Trace I/O Enable
// <o1.6..7> TRACE_MODE <i> Trace Mode
// <0=> Asynchronous
// <1=> Synchronous: TRACEDATA Size 1
// <2=> Synchronous: TRACEDATA Size 2
// <3=> Synchronous: TRACEDATA Size 4
// <o1.8> DBG_IWDG_STOP <i> Independant Watchdog Stopped when Core is halted
// <o1.9> DBG_WWDG_STOP <i> Window Watchdog Stopped when Core is halted
// <o1.10> DBG_TIM1_STOP <i> Timer 1 Stopped when Core is halted
// <o1.11> DBG_TIM2_STOP <i> Timer 2 Stopped when Core is halted
// <o1.12> DBG_TIM3_STOP <i> Timer 3 Stopped when Core is halted
// <o1.13> DBG_TIM4_STOP <i> Timer 4 Stopped when Core is halted
// <o1.14> DBG_CAN_STOP <i> CAN Stopped when Core is halted
// </h>
_WDWORD(0xE0042004, 0x00000027); // DBGMCU_CR
_WDWORD(0xE000ED08, 0x20000000); // Setup Vector Table Offset Register
}
DebugSetup(); // Debugger Setup
3、在使用硬件调试时,仿真器Setting设置成SW模式调试,因为ITM机制需要使用该模式才能调试。
4、点击魔术棒,在Debug下添加刚才的.ini文件(下图):
5、 选择仿真器Port口为SW模式,在Trace下按照下图设置,使能Trace,由于我芯片系统时钟是120M,所以设置为120M,ITM Stimulus Ports口只保留0(如下图)。
6、编译成功之后进入调试模式,调用Debug printf Viewer创库,运行程序,这时窗口中就可以输出数据了。
警告:这种通过调试器直接输出会影响到对时间敏感的代码运行。
semihosting(半主机)机制
有关半主机的概念,参考RealView编译工具开发指南中的8.1.1解释了什么是半主机?
半主机是用于ARM目标的一种机制,可将来自应用程序代码的输入/输出请求传送至运行调试器的主机。使用此机制可以启用C库中的函数,如printf()和scanf()来使用主机的屏幕和键盘而不是在目标系统上配置屏幕和键盘。因为开发时使用的硬件通常没有最终系统的所有输入和输出设备。半主机可让半主机来提供这些设备。
半主机是通过一组定义好的软件指令来实现的,这些指令通过程序控制生成异常。应用程序调用相应地半主机调用,然后调试代理处理该异常。调试代理提供与主机之间的必需通信。半主机接口对 ARM 公司提供的所有调试代理都是通用的。 在无需移植的情况下使用 RealView ARMulator® ISS、指令集系统模型 (ISSM)、实时系统模型 (RTSM)、RealView ICE 或 RealMonitor 时,会执行半主机操作。标准库使用半主机模式,半主机是通过一组定义好的软件指令 (如 SVC)SVC 指令 (以前称为 SWI 指令)来实现的,这些指令通过程序控制生成异常。 应用程序调用相应的半主机调用,然后调试代理处理该异常。调试代理(这里的调试代理是仿真器)提供与主机之间的必需通信。也就是说使用半主机模式必须使用仿真器调试。ARMv7 之前的 ARM 处理器使用 SVC 指令 (以前称为 SWI 指令)进行半主机调用。 但是,如果要为 ARMv6-M 或 ARMv7-M (如 Cortex™-M1 或 Cortex-M3 处理器)进行编译,请使用 BKPT 指令来实现半主机。
简单的来说,半主机模式就是通过仿真器实现开发板在电脑上的输入和输出。和半主机模式功能相同的是ITM调试机制。
开发式一般单片机需要独立运行,开发者应去掉仿真器,把printf函数通过单片机的外设来实现,例如通过开发板的串口,lcd或者sd卡。
MDK中通常使用以下两种方法:
方法1.使用微库,因为使用微库的话,不会使用半主机模式.
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0);
USART1->DR = (u8) ch;
return ch;
}
方法2.仍然使用标准库,在主程序添加下面代码:
#pragma import(__use_no_semihosting)
struct __FILE
{
int handle;
};
FILE __stdout;
_sys_exit(int x)
{
x = x;
}
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0);//Ñ»··¢ËÍ,Ö±µ½·¢ËÍÍê±Ï
USART1->DR = (u8) ch;
return ch;
}
关于 microlib
microlib 是缺省 C 库的备选库。 它用于必须在极少量内存环境下运行的深层嵌入式应用程序。 这些应用程序不在操作系统中运行。 microlib 不会尝试成为符合标准的 ISO C 库。microlib 进行了高度优化以使代码变得很小。 它的功能比缺省 C 库少,并且根本不具备某些 ISO C 特性。某些库函数的运行速度也比较慢,例如, memcpy() 。上面给出了正确的方法,我在测试的过程中发现方法二中注释掉 #pragma import(__use_no_semihosting) 程序依然运行正确,这个很让人费解。也有使用 #pragma import(__use_no_semihosting_swi) 的。这几个的区别我没弄懂。网上的大部分都是禁用半主机模式,否则会出现如下错误:”Error: L6915E: Library reports error: __use_no_semihosting_swi was requested, but _ttywrch was referenced“。具体的解决方法:
//不使用半主机模式
#if 1 //如果没有这段,则需要在target选项中选择使用USE microLIB
#pragma import(__use_no_semihosting) //注释本行, 方法1
struct __FILE {
int handle;
};
FILE __stdout;
_sys_exit(int x)
{
x = x;
}
//__use_no_semihosting was requested, but _ttywrch was referenced, 增加如下函数, 方法2
_ttywrch(int ch)
{
ch = ch;
}
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0);//Ñ»··¢ËÍ,Ö±µ½·¢ËÍÍê±Ï
USART1->DR = (u8) ch;
return ch;
#endif
这样即可。
使用IO模拟串口
这种调试方法的使用条件是:在没有串口或串口已经被用作其他用途时如何输出调试信息?这时可以找一个空闲的普通的IO,模拟UART协议输出log到上位机的串口工具。常用的UART协议如下:
只要在确定的时间在IO上输出高低电平就可以模拟出波形,这个确定的时间就是串口波特率。为了得到精确延时,可以使用定时器产生1us的延时。警告:定时器不能重复复用。测试平台stm32f407,具体的实现过程如下:
uint32_t baud_delay = 0;
uint8_t ucSimuLogConfig(void *arg)
{
TIM_TimeBaseInitTypedef TIM_InitStruct;
uint32_t *baud = (uint32_t*)arg;
GPIO_InitTypeDef GPIO_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA, GPIO_Pin_2);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);
TIM_DeInit(TIM4);
TIM_InitStruct.TIM_Prescaler = 1; //2分频
TIM_InitStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_InitStruct.TIM_Period = 41;
TIM_InitStruct.TIM_ClockDevision = TIM_CKD_DIV1;
TIM_InitBaseInit(TIM4, &TIM_InitStruct);
TIM_ClearFlag(TIM4, TIM_FLAG_Update);
baud_delay = 1000000/(*bound); //根据波特率计算每个bit延时
}
//使用定时器的delay函数为:
void vSimuDelay(uint32_t us)
{
volatile uint32_t tmp_us = us;
TIM_SetCounter(TIM4, 0);
TIM_Cmd(TIM4, ENABLE);
while(tmp_us--)
{
while(TIM_GetFlagStatus(TIM4, TIM_FLAG_Update) == RESET);
TIM_ClearFlag(TIM4, TIM_FLAG_Update);
}
TIM_Cmd(TIM4, DISABLE);
}
//模拟输出函数,注意:输出前必须要关闭中断,一个byte输出完再打开,否则会出现乱码:
uint8_t ucSimu_Print_ch(uint8_t ch)
{
volatile uint8_t i = 8;
__asm("cpsid i");
//start bit
GPIO_ResetBits(GPIOA, GPIO_Pin_2);
vSimuDelay(baud_delay );
while(i--)
{
if(ch & 0x01)
GPIO_SetBits(GPIOA, GPIO_Pin_2);
else
GPIO_ResetBits(GPIOA, GPIO_Pin_2);
ch >>= 1;
simu_delay(baud_delay);
}
// Stop Bit
GPIO_SetBtis(GPIA, GPIO_Pin_2);
simu_delay(baud_delay);
simu_delay(baud_delay);
__asm("cpsie i");
return 0;
}
使用模拟IO可以达到与真实串口类似的效果,并且只需要一个普通的IO,在小封装芯片上比较使用。