做相关的应用,很经常遇到的一个情况就是希望能够实时的观察单片机中的变量,从而更直观的判断数据或算法的正确性,例如无人机的姿态估计。

现有方案:

上面提到的方法都有各自的局限性,使用串口通用性很好但是串口传输数据会占用CPU资源,编写上位机绘制曲线往往也耗时耗力,而商业IDE和JLink严格意义上说并不是免费的。覆盖面不全的问题,也亟需解决。

LinkGDBserial上位机介绍

项目简介

本程序使用QT编写,用于硬件设备的调试,可直接驱动串口或各种调试器(基于OpenOCD支持)。Ref: LinkScope

开发环境

主要功能

连接方式

使用方法

  1. 若使用串口连接,或需使用日志功能,需先将对应下位机程序移植到目标芯片中

    请查看串口移植说明日志移植说明

    注:串口与日志功能不冲突,可以同时使用

  2. 下载发行版文件,解压后双击LinkGDBserial.exe运行程序

  3. 点击设置符号文件,然后设置要查看的变量

    • 添加变量

      • 在变量选择窗口添加(须先设置符号文件)

      • 在主窗口表格最后一行的变量名处手动填写

    • 删除变量

      • 在变量名上单击右键

      • 单击选中变量名后按Del键

    注:添加的变量名可以是任何合法的C语言表达式,可参考进阶使用说明;结构体等复合类型只能查看,不能修改和绘图

  4. 选择连接模式,连接芯片,连接成功后程序开始采样

    • 调试器模式下,在下拉框中选择调试器和芯片类型,点击连接目标;或勾选“外部进程”后直接连接到正在运行的OpenOCD进程

    • 串口模式下,点击刷新串口加载串口列表,选中所连接的串口,点击连接目标

  5. 编辑修改变量列可以修改变量值,双击图线颜色列可以选择绘图颜色

  6. 单击变量名列选中对应的变量,绘图窗口会加粗绘制波形,左下角会显示当前值和查看值(拖动鼠标进行查看)

  7. 绘图界面中滚轮配合CtrlShiftAlt可以实现画面的缩放和移动

示意图

操作演示

日志输出

主要菜单项说明

下载链接: 通过网盘分享的文件:LinkGDBserial 链接: https://pan.baidu.com/s/1wvWXbn7fzidBLCKYLZJdpg?pwd=2e57 提取码: 2e57


进阶使用说明


使用注意事项

其他说明

关于采样速度

关于实际支持的设备


日志下位机程序说明

相关文件

本下位机程序包含log.clog.h两个文件 log.c文件

#include "log.h"

#ifdef LOG_ENABLE
//日志缓冲区定义
LogQueue logQueue={LOG_MAX_QUEUE_SIZE};
#endif

log.h文件

#ifndef _LOG_H_
#define _LOG_H_

/**************↓配置区↓**************/

//标准库头文件
//若不希望使用对应函数可注释头文件后修改下方函数宏实现相应功能
#include <stdio.h> //使用sprintf
#include <string.h> //使用memcpy、strlen
//标准库函数移植宏
#define LOG_MEMCPY(dst,src,len) memcpy(dst,src,len) //内存拷贝
#define LOG_SPRINTF(buf,fmt,...) sprintf(buf,fmt,##__VA_ARGS__) //格式化字符串
#define LOG_STRLEN(str) strlen(str) //计算字符串长度

//日志缓冲区大小,总大小为下列两个值的乘积
#define LOG_MAX_LEN 100 //单条日志缓冲区大小,包含日志内容、时间戳、函数名等信息,建议不小于100
#define LOG_MAX_QUEUE_SIZE 10 //缓冲区可存日志条数

//获取时间戳接口(启动后经过的毫秒数)
#define LOG_GET_MS() HAL_GetTick() //本例使用STM32-HAL

//获取语句所在函数名的宏(一般由编译器提供)
#define LOG_GET_FUNC_NAME() (__FUNCTION__)

//是否开启日志输出(注释掉则所有输出语句会替换为空语句)
#define LOG_ENABLE

/**************↑配置区↑**************/

/**************↓日志输出接口↓**************/
//信息日志
#define LOG_INFO(tag,msg,...) LOG_ADD_FORMAT('i',(tag),(msg),##__VA_ARGS__)
//调试日志
#define LOG_DEBUG(tag,msg,...) LOG_ADD_FORMAT('d',(tag),(msg),##__VA_ARGS__)
//警告日志
#define LOG_WARN(tag,msg,...) LOG_ADD_FORMAT('w',(tag),(msg),##__VA_ARGS__)
//错误日志
#define LOG_ERROR(tag,msg,...) LOG_ADD_FORMAT('e',(tag),(msg),##__VA_ARGS__)
/**************↑日志输出接口↑**************/

//日志缓冲区类型,以队列方式存储
typedef struct{
	int maxSize; //队列最大长度,初始化时须赋值为LOG_MAX_QUEUE_SIZE
	char buf[LOG_MAX_QUEUE_SIZE][LOG_MAX_LEN]; //实际存储空间
	int lenBuf[LOG_MAX_QUEUE_SIZE]; //与buf一一对应,表示存储的日志长度
	int size,startPos;//当前队列长度和队头位置
}LogQueue;

#ifdef LOG_ENABLE
//日志输出集中处理,添加一条日志到缓冲区
#define LOG_ADD_FORMAT(attr,tag,msg,...) if(logQueue.size<LOG_MAX_QUEUE_SIZE){ \
	int index=(logQueue.startPos+logQueue.size+1)%LOG_MAX_QUEUE_SIZE; \
	char *bufStartAddr=logQueue.buf[index],*buf=bufStartAddr; int termLen=0; \
	*buf=attr; buf+=1; \
	termLen=LOG_STRLEN(tag)+1; LOG_MEMCPY(buf,tag,termLen); buf+=termLen; \
	LOG_SPRINTF(buf,msg,##__VA_ARGS__); termLen=LOG_STRLEN(buf)+1; buf+=termLen; \
	LOG_SPRINTF(buf,"%d",LOG_GET_MS()); termLen=LOG_STRLEN(buf)+1; buf+=termLen; \
	termLen=LOG_STRLEN(LOG_GET_FUNC_NAME())+1; LOG_MEMCPY(buf,LOG_GET_FUNC_NAME(),termLen); buf+=termLen; buf+=termLen; \
	logQueue.lenBuf[index]=buf-bufStartAddr; logQueue.size++; \
}
//声明缓冲区定义,实际定义可在任意一个源文件中
extern LogQueue logQueue;
#else
#define LOG_ADD_FORMAT(attr,tag,msg,...)
#endif

#endif

配置项

日志输出接口

移植说明

  1. log.hlog.c添加到工程目录中

    注:若不想新增log.c文件或编译器不支持多个源文件,可以将其中的内容放入任何一个源文件中

  2. 根据所用平台修改配置项

    • 一般情况下只需修改时间戳配置LOG_GET_MS即可,其余参数可以保持默认

    • 若下位机内存不够,或希望输出更长的日志,或希望一次性写入更多条日志,可修改缓冲区大小配置LOG_MAX_QUEUE_SIZELOG_MAX_LEN

    • 若编译器不支持__FUNCTION__宏,需要将LOG_GET_FUNC_NAME替换为空字符串

    • 若编译器不支持标准库函数,需要自行实现各函数并替换到对应宏定义中

  3. 在需要使用日志的文件中引用log.h,然后调用接口进行日志输出即可,上位机会定时查看并移除日志缓冲区中的数据

注意事项


串口下位机程序说明

相关文件

本下位机程序仅包含单个文件debug.c文件

// debug.c
#include <stdint.h>

#include "usart.h" //本例使用STM32,其他平台无需引用此头文件

/**************↓配置区↓**************/
//串口发送语句,需实现将buf指向的len个字节通过串口发出
#define DEBUG_SEND(buf,len) HAL_UART_Transmit_IT(&huart1,(buf),(len))
//复位指令,若设备不支持或无需此功能可不定义
#define DEBUG_RESET() { \
	__set_FAULTMASK(1); \
	NVIC_SystemReset(); \
}
//读地址限制条件,若请求的地址addr不符合条件则返回0x00,无需限制可不定义
#define DEBUG_READ_ADDR_RANGE(addr) (addr>=0x20000000 && addr<=0x20001000)
//写地址限制条件,若请求的地址addr不符合条件则不会写入,无需限制可不定义
#define DEBUG_WRITE_ADDR_RANGE(addr) (addr>=0x20000000 && addr<=0x20001000)
//读写偏移地址,无偏移可不定义
#define DEBUG_ADDR_OFFSET 0x00000000
/**************↑配置区↑**************/

//接收缓存区(循环队列)大小,不建议修改
#define DEBUG_RXBUF_SIZE 30
//发送缓存区大小,不建议修改
#define DEBUG_TXBUF_SIZE 30

//数据帧格式:帧头1B+帧长1B+命令码1B+数据
//固定帧头
#define DEBUG_FRAME_HEADER 0xDB
//命令码枚举,用于标记数据帧类型
typedef enum{
	SerialCMD_ReadMem, //读内存数据
	SerialCMD_WriteMem, //写内存数据
	SerialCMD_Reset //复位
}SerialCMD;

//串口发送缓存区
uint8_t debugTxBuf[DEBUG_TXBUF_SIZE];

//以下实现了一个循环队列用于串口接收,无溢出检查
struct{
	uint8_t buf[DEBUG_RXBUF_SIZE]; //缓存区
	uint16_t startPos,endPos; //队头队尾指针
}debugRxQueue={0};
//入队一个字符
#define DEBUG_QUEUE_PUSH(ch) { \
	debugRxQueue.buf[debugRxQueue.endPos++]=(ch); \
	if(debugRxQueue.endPos>=DEBUG_RXBUF_SIZE) \
		debugRxQueue.endPos-=DEBUG_RXBUF_SIZE; \
}
//出队一个字符
#define DEBUG_QUEUE_POP() { \
	debugRxQueue.startPos++; \
	if(debugRxQueue.startPos>=DEBUG_RXBUF_SIZE) \
		debugRxQueue.startPos-=DEBUG_RXBUF_SIZE; \
}
//获取队头字符
#define DEBUG_QUEUE_TOP() (debugRxQueue.buf[debugRxQueue.startPos])
//获取队列大小
#define DEBUG_QUEUE_SIZE() \
	(debugRxQueue.startPos<=debugRxQueue.endPos? \
	debugRxQueue.endPos-debugRxQueue.startPos: \
	debugRxQueue.endPos+DEBUG_RXBUF_SIZE-debugRxQueue.startPos)
//获取队列第pos个元素
#define DEBUG_QUEUE_AT(pos) \
	(debugRxQueue.startPos+(pos)<DEBUG_RXBUF_SIZE? \
	debugRxQueue.buf[debugRxQueue.startPos+(pos)]: \
	debugRxQueue.buf[debugRxQueue.startPos+(pos)-DEBUG_RXBUF_SIZE])

//函数声明
void Debug_SerialRecv(uint8_t *buf,uint16_t len);
void Debug_ParseBuffer(void);

//串口收到数据后传入本函数进行解析,需被外部调用
void Debug_SerialRecv(uint8_t *buf,uint16_t len)
{
	for(uint16_t i=0;i<len;i++) //将收到的数据依次入队
		DEBUG_QUEUE_PUSH(buf[i]);
	Debug_ParseBuffer(); //进入解析
}

//解析串口数据
void Debug_ParseBuffer()
{
	if(DEBUG_QUEUE_AT(0)==DEBUG_FRAME_HEADER) //第一个字节为帧头,可以继续解析
	{
		if(DEBUG_QUEUE_SIZE()>2 && DEBUG_QUEUE_SIZE()>=DEBUG_QUEUE_AT(1)) //帧长足够,可以解析
		{
			uint16_t frameLen=DEBUG_QUEUE_AT(1);//读出帧长
			uint8_t cmd=DEBUG_QUEUE_AT(2); //读出命令码
			if(cmd==SerialCMD_ReadMem) //若要读取内存数据
			{
				uint8_t byteNum=DEBUG_QUEUE_AT(3); //要读取的字节数
				if(byteNum>DEBUG_TXBUF_SIZE-3) //限制读取的字节数不能使帧长超过发送缓冲区大小
					byteNum=DEBUG_TXBUF_SIZE-3;
				uint32_t addr=0; //计算目标地址
				for(uint8_t i=0;i<4;i++)
					addr|=((uint32_t)DEBUG_QUEUE_AT(4+i))<<(i*8);
				#ifdef DEBUG_ADDR_OFFSET
					addr+=DEBUG_ADDR_OFFSET;
				#endif
				debugTxBuf[0]=DEBUG_FRAME_HEADER; //构建发送数据帧
				debugTxBuf[1]=byteNum+3;
				debugTxBuf[2]=SerialCMD_ReadMem;
				for(uint8_t i=0;i<byteNum;i++) //依次写入指定地址的数据
				{
					uint8_t byte=0;
					#ifdef DEBUG_READ_ADDR_RANGE
					if(DEBUG_READ_ADDR_RANGE((addr+i)))
					#endif
						byte=*(uint8_t*)(addr+i);
					debugTxBuf[i+3]=byte;
				}
				DEBUG_SEND(debugTxBuf,byteNum+3); //串口发送
				for(uint8_t i=0;i<frameLen;i++) //将本帧出队
					DEBUG_QUEUE_POP();
			}
			else if(cmd==SerialCMD_WriteMem) //若要写入内存数据
			{
				uint8_t byteNum=frameLen-7; //要写入的字节数
				uint32_t addr=0; //计算目标地址
				for(uint8_t i=0;i<4;i++)
					addr|=((uint32_t)DEBUG_QUEUE_AT(3+i))<<(i*8);
				#ifdef DEBUG_ADDR_OFFSET
					addr+=DEBUG_ADDR_OFFSET;
				#endif
				for(uint8_t i=0;i<byteNum;i++) //依次写入数据
				{
					#ifdef DEBUG_WRITE_ADDR_RANGE
					if(DEBUG_WRITE_ADDR_RANGE((addr+i)))
					#endif
						*(uint8_t*)(addr+i)=DEBUG_QUEUE_AT(7+i);
				}
				for(uint8_t i=0;i<frameLen;i++) //将本帧出队
					DEBUG_QUEUE_POP();
			}
			else if(cmd==SerialCMD_Reset) //若要复位
			{
				#ifdef DEBUG_RESET
				DEBUG_RESET();
				#endif
			}
			if(DEBUG_QUEUE_SIZE()>0) //若后面还有数据,进行递归解析
				Debug_ParseBuffer();
		}
	}
	else //数据帧错误
	{
		while(DEBUG_QUEUE_AT(0)!=DEBUG_FRAME_HEADER && DEBUG_QUEUE_SIZE()>0) //将错误数据出队
			DEBUG_QUEUE_POP();
		if(DEBUG_QUEUE_SIZE()>0) //若后面还有数据,继续解析
			Debug_ParseBuffer();
	}
}

配置项

注:带**的项为必需配置,其余项若不需要对应功能可不定义


函数接口

移植说明

  1. 开启一个串口,将下位机程序添加到项目工程中

  2. 修改配置项

  3. 在收到串口数据时调用Debug_SerialRecv函数

问题处理

上位机点击连接后显示读取超时,下位机还在正常运行

上位机点击连接后显示读取超时,下位机出现运行异常(进入硬件错误或发生复位现象等)

上位机正常连接,但显示的数值不正确


移植示例

STM32 & HAL & 中断收发


/****Debug.c****/
//...
//串口发送指令(使用串口1)
#define DEBUG_SEND(buf,len) HAL_UART_Transmit_IT(&huart1,(buf),(len))
//复位指令
#define DEBUG_RESET() { \
    __set_FAULTMASK(1); \
    NVIC_SystemReset(); \
}
//...

/****main.c****/
//...
int main(void)
{
    //...(需先进行串口初始化)
    __HAL_UART_ENABLE_IT(&huart1,UART_IT_RXNE); //使能串口RXNE中断
    //...
    while(1)
    {
        //...
    }
}
//...

/****stm32f1xx_it.c****/
//...
void USART1_IRQHandler(void) //串口中断服务函数
{
    if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_RXNE)!=RESET) //判定为RXNE中断
    {
        uint8_t ch=huart1.Instance->DR; //读出收到的字节
        Debug_SerialRecv(&ch,1); //进行解析
    }
}
//...

Arduino 串口轮询方式


/****main.ino****/

//...
#define DEBUG_SEND(buf,len) Serial.write((buf),(len))
//...(下位机程序其他部分)

void setup()
{
    //...
    Serial.begin(115200); //初始化串口波特率为115200
    //...
}

void loop()
{
    while(Serial.available())
    {
        uint8_t ch=Serial.read(); //读出串口数据
        Debug_SerialRecv(&ch,1); //进行解析
    }
    //...其余代码不能有阻塞情况,需尽快执行完毕
}