Rice我叫加饭? · 2023年01月06日 · 北京市

QT编写一个JLINK烧录工具

概述

  • 作者一直有一个想法,就是写一个功能强大的桌面小工具,里面集成各种平时开发要用的工具。例如:串口助手,网络助手,下载工具等。那么如何也带来几个问题:
  1. 问题1:那么如何呈现在桌面上也是一个非常重要的问题 -- 桌面悬浮窗。
  2. 问题2:工具的名字 -- RTOOL(米饭工具)
  3. 问题3:是否贡献整个工具 -- 分为两个版本:开源版本和公司项目版本(已经发布了V1.0版本)。

image.png

  • 本篇文章介绍RTOOL的JLINK烧录小工具,那为什么要在RTOOL中集成JLINK的烧录工具呢?原因:
  1. 像MCU,我们如果使用GCC构建我们的程序后,没有IDE的支撑,就需要使用JFLASH这样的工具进行烧录,这个操作流程还是挺多步骤的。

image.png

  1. 方便我们对固件进行动手术,如对固件进行加密处理,对芯片ram,flash进行随心所欲的操作。

原理说明

  • 我们在使用JFlash烧录工具时,实际JFlash是通过调用JLinkARM.dll动态库提供的接口进行操作的。那么我们可以通过Dependency walker对JLinkARM.dll进行分析。获取到dll库中所有函数符号。

image.png

  • QT提供了QLibrary类可以动态加载dll,所以结合获取的函数符号,我们可以定义一些列函数指针指向对应的符号。

image.png

开发流程

  1. UI设计,实际可以很简单,目的也是简化JFlash的操作流程:

image.png

  1. 定义对接动态库JLinkARM.dll的一系列函数指针,头文件RJlinkARM.h:
#ifndef RJLINKARMH
#define RJLINKARMH

//JLINK TIF
#define JLINKARM_TIF_JTAG                0
#define JLINKARM_TIF_SWD                 1
#define JLINKARM_TIF_DBM3                2
#define JLINKARM_TIF_FINE                3
#define JLINKARM_TIF_2wire_JTAG_PIC32  4

//RESET TYPE
#define JLINKARM_RESET_TYPE_NORMAL       0
#define JLINKARM_RESET_TYPE_CORE         1
#define JLINKARM_RESET_TYPE_PIN          2

typedef bool  (*rjlinkOpenFunc)(void);
typedef void  (*rjlinkCloseFunc)(void);
typedef bool  (*rjlinkIsOpenFunc)(void);
typedef unsigned int (*rjlinkTIFSelectFunc)(int);
typedef void  (*rjlinkSetSpeedFunc)(int);
typedef unsigned int (*rjlinkGetSpeedFunc)(void);
typedef void  (*rjlinkResetFunc)(void);
typedef int   (*rjlinkHaltFunc)(void);
typedef void  (*rjlinkGoFunc)(void);

typedef int   (*rjlinkReadMemFunc)(unsigned int addr, int len, void *buf);
typedef int   (*rjlinkWriteMemFunc)(unsigned int addr, int len, void *buf);
typedef int   (*rjlinkWriteU8Func)(unsigned int addr, unsigned char data);
typedef int   (*rjlinkWriteU16Func)(unsigned int addr, unsigned short data);
typedef int   (*rjlinkWriteU32Func)(unsigned int addr, unsigned int data);

typedef int   (*rjlinkEraseChipFunc)(void);
typedef int   (*rjlinkDownloadFileFunc)(const char *file, unsigned int addr);
typedef void  (*rjlinkBeginDownloadFunc)(int index);
typedef void  (*rjlinkEndDownloadFunc)(void);
typedef bool  (*rjlinkExecCommandFunc)(const char* cmd, int a, int b);

typedef unsigned int (*rjlinkReadRegFunc)(int index);
typedef int   (*rjlinkWriteRegFunc)(int index, unsigned int data);

typedef void  (*rjlinkSetLogFileFunc)(char *file);
typedef unsigned int (*rjlinkGetDLLVersionFunc)(void);
typedef unsigned int (*rjlinkGetHardwareVersionFunc)(void);
typedef unsigned int (*rjlinkGetFirmwareStringFunc)(char *buff, int count);
typedef unsigned int (*rjlinkGetSNFunc)(void);
typedef unsigned int (*rjlinkGetIdFunc)(void);
typedef bool  (*rjlinkConnectFunc)(void);
typedef bool  (*rjlinkIsConnectedFunc)(void);

#endif // RJLINKARMH
  1. 通过QT提供了QLibrary类加载dll,然后函数指针指向对应的函数符号:
  • 通过头文件RJlinkARM.h定义的函数指针类型定义对应的变量:
private:
    rjlinkOpenFunc rjlinkOpenFuncPtr = NULL;
    rjlinkCloseFunc rjlinkCloseFuncPtr = NULL;
    rjlinkIsOpenFunc rjlinkIsOpenFuncPtr = NULL;
    rjlinkTIFSelectFunc rjlinkTIFSelectFuncPtr = NULL;
    rjlinkSetSpeedFunc rjlinkSetSpeedFuncPtr = NULL;
    rjlinkGetSpeedFunc rjlinkGetSpeedFuncPtr = NULL;
    rjlinkResetFunc rjlinkResetFuncPtr = NULL;
    rjlinkHaltFunc rjlinkHaltFuncPtr = NULL;
    rjlinkGoFunc rjlinkGoFuncPtr = NULL;
    rjlinkReadMemFunc rjlinkReadMemFuncPtr = NULL;
    rjlinkWriteMemFunc rjlinkWriteMemFuncPtr = NULL;
    rjlinkWriteU8Func rjlinkWriteU8FuncPtr = NULL;
    rjlinkWriteU16Func rjlinkWriteU16FuncPtr = NULL;
    rjlinkWriteU32Func rjlinkWriteU32FuncPtr = NULL;
    rjlinkEraseChipFunc rjlinkEraseChipFuncPtr = NULL;
    rjlinkDownloadFileFunc rjlinkDownloadFileFuncPtr = NULL;
    rjlinkBeginDownloadFunc rjlinkBeginDownloadFuncPtr = NULL;
    rjlinkEndDownloadFunc rjlinkEndDownloadFuncPtr = NULL;
    rjlinkExecCommandFunc rjlinkExecCommandFuncPtr = NULL;
    rjlinkReadRegFunc rjlinkReadRegFuncPtr = NULL;
    rjlinkWriteRegFunc rjlinkWriteRegFuncPtr = NULL;
    rjlinkSetLogFileFunc rjlinkSetLogFileFuncPtr = NULL;
    rjlinkGetDLLVersionFunc rjlinkGetDLLVersionFuncPtr = NULL;
    rjlinkGetHardwareVersionFunc rjlinkGetHardwareVersionFuncPtr = NULL;
    rjlinkGetFirmwareStringFunc rjlinkGetFirmwareStringFuncPtr = NULL;
    rjlinkGetSNFunc rjlinkGetSNFuncPtr = NULL;
    rjlinkGetIdFunc rjlinkGetIdFuncPtr = NULL;
    rjlinkConnectFunc rjlinkConnectFuncPtr = NULL;
    rjlinkIsConnectedFunc rjlinkIsConnectedFuncPtr = NULL;
  • 通过动态库(JLinkARM.dll)获取对应的函数指针
void RJLinkView::jlinkLibLoadHandle(void)
{
    jlinkLib = new QLibrary("JLinkARM.dll");
    if(jlinkLib->load())
    {
        rjlinkOpenFuncPtr = (rjlinkOpenFunc)jlinkLib->resolve("JLINKARM_Open");                             // 打开设备
        rjlinkCloseFuncPtr = (rjlinkCloseFunc)jlinkLib->resolve("JLINKARM_Close");                          // 关闭设备
        rjlinkIsOpenFuncPtr = (rjlinkIsOpenFunc)jlinkLib->resolve("JLINKARM_IsOpen");                       // 判断设备是否打开
        rjlinkTIFSelectFuncPtr = (rjlinkTIFSelectFunc)jlinkLib->resolve("JLINKARM_TIF_Select");             // 选择设备
        rjlinkSetSpeedFuncPtr = (rjlinkSetSpeedFunc)jlinkLib->resolve("JLINKARM_SetSpeed");                 // 设置烧录速度
        rjlinkGetSpeedFuncPtr = (rjlinkGetSpeedFunc)jlinkLib->resolve("JLINKARM_GetSpeed");                 // 获取烧录速度
        rjlinkResetFuncPtr = (rjlinkResetFunc)jlinkLib->resolve("JLINKARM_Reset");                          // 复位设备
        rjlinkHaltFuncPtr = (rjlinkHaltFunc)jlinkLib->resolve("JLINKARM_Halt");                             // 中断程序执行
        rjlinkReadMemFuncPtr = (rjlinkReadMemFunc)jlinkLib->resolve("JLINKARM_ReadMem");                    // 读取内存
        rjlinkWriteMemFuncPtr = (rjlinkWriteMemFunc)jlinkLib->resolve("JLINKARM_WriteMem");                 // 写入内存
        rjlinkEraseChipFuncPtr = (rjlinkEraseChipFunc)jlinkLib->resolve("JLINK_EraseChip");                 // 擦除芯片
        rjlinkExecCommandFuncPtr = (rjlinkExecCommandFunc)jlinkLib->resolve("JLINKARM_ExecCommand");        // 执行命令
        rjlinkGetDLLVersionFuncPtr = (rjlinkGetDLLVersionFunc)jlinkLib->resolve("JLINKARM_GetDLLVersion");  // 获取DLL版本号
        rjlinkGetSNFuncPtr = (rjlinkGetSNFunc)jlinkLib->resolve("JLINKARM_GetSN");                          // 获取sn号
        rjlinkGetIdFuncPtr = (rjlinkGetIdFunc)jlinkLib->resolve("JLINKARM_GetId");                          // 获取ID
        rjlinkConnectFuncPtr = (rjlinkConnectFunc)jlinkLib->resolve("JLINKARM_Connect");                    // 连接设备
        rjlinkIsConnectedFuncPtr = (rjlinkIsConnectedFunc)jlinkLib->resolve("JLINKARM_IsConnected");        // 判断是否连接设备
    }
}
  • 上述的函数指针,其实对访问操作一点也不友好,所以通过类方法重新对其封装,也避免的空指针的访问:
bool RJLinkView::jlinkOpen(void)
{
    if(rjlinkOpenFuncPtr){
        return rjlinkOpenFuncPtr();
    }
    return false;
}
void RJLinkView::jlinkClose(void)
{
    if(rjlinkCloseFuncPtr){
        rjlinkCloseFuncPtr();
    }
}
bool RJLinkView::jlinkIsOpen(void)
{
    if(rjlinkIsOpenFuncPtr){
        return rjlinkIsOpenFuncPtr();
    }
    return false;
}
unsigned int RJLinkView::jlinkTIFSelectFunc(int type)
{
    if(rjlinkTIFSelectFuncPtr){
        return rjlinkTIFSelectFuncPtr(type);
    }
    return false;
}
void RJLinkView::jlinkSetSpeedFunc(unsigned int speed)
{
    if(rjlinkSetSpeedFuncPtr){
        rjlinkSetSpeedFuncPtr(speed);
    }
}
unsigned int RJLinkView::jlinkGetSpeedFunc(void)
{
    if(rjlinkGetSpeedFuncPtr){
        return rjlinkGetSpeedFuncPtr();
    }
    return 0;
}
void RJLinkView::jlinkResetFunc(void)
{
    if(rjlinkResetFuncPtr){
        rjlinkResetFuncPtr();
    }
}
int RJLinkView::jlinkHaltFunc(void)
{
    if(rjlinkHaltFuncPtr){
        return rjlinkHaltFuncPtr();
    }
    return 0;
}
int RJLinkView::jlinkReadMemFunc(unsigned int addr, int len, void *buf)
{
    if(rjlinkReadMemFuncPtr){
        return rjlinkReadMemFuncPtr(addr, len, buf);
    }
    return 0;
}
int RJLinkView::jlinkWriteMemFunc(unsigned int addr, int len, void *buf)
{
    if(rjlinkWriteMemFuncPtr){
        return rjlinkWriteMemFuncPtr(addr, len, buf);
    }
    return 0;
}
int RJLinkView::jlinkEraseChipFunc(void)
{
    if(rjlinkEraseChipFuncPtr){
        return rjlinkEraseChipFuncPtr();
    }
    return 0;
}
bool RJLinkView::jlinkExecCommandFunc(const char *cmd, int a, int b)
{
    if(rjlinkExecCommandFuncPtr){
        return rjlinkExecCommandFuncPtr(cmd, a, b);
    }
    return false;
}
unsigned int RJLinkView::jlinkGetDLLVersionFunc(void)
{
    if(rjlinkGetDLLVersionFuncPtr){
        return rjlinkGetDLLVersionFuncPtr();
    }
    return 0;
}
unsigned int RJLinkView::jlinkGetSNFunc(void)
{
    if(rjlinkGetSNFuncPtr){
        return rjlinkGetSNFuncPtr();
    }
    return 0;
}
unsigned int RJLinkView::jlinkGetIdFunc(void)
{
    if(rjlinkGetIdFuncPtr){
        return rjlinkGetIdFuncPtr();
    }
    return 0;
}
bool RJLinkView::jlinkConnectFunc(void)
{
    if(rjlinkConnectFuncPtr){
        return rjlinkConnectFuncPtr();
    }
    return false;
}
bool RJLinkView::jlinkIsConnectedFunc(void)
{
    if(rjlinkIsConnectedFuncPtr){
        return rjlinkIsConnectedFuncPtr();
    }
    return false;
}
  • 下载信息窗体的显示格式设置,为了方便预览我们的操作步骤,所以规定一个显示格式,增加时间戳的显示:
void RJLinkView::infoShowHandle(QString info)
{
    QString timestamp = QDateTime::currentDateTime().toString("[hh:mm:ss.zzz]==> ");
    ui->burnInfoTextBrowser->append(timestamp + info);
}
  • 连接芯片设备,以下是按照STM32F407IG为例,下载方式采用SWD,下载速度未4000kHz:
bool RJLinkView::jlinkConnectHandle(void)
{
    if(jlinkIsOpen())
    {
        infoShowHandle(tr("设备连接成功"));
        return true;
    }
    jlinkOpen();
    if(jlinkIsOpen())
    {
        jlinkExecCommandFunc("device = STM32F407IG", 0, 0);
        jlinkTIFSelectFunc(JLINKARM_TIF_SWD);
        jlinkSetSpeedFunc(4000);
        jlinkConnectFunc();
        if(jlinkIsConnectedFunc()){
            infoShowHandle(tr("设备连接成功"));
            return true;
        }else
        {
            infoShowHandle(tr("连接设备失败! 请检查设备连接..."));
        }
    }
    else
    {
        infoShowHandle(tr("连接设备失败! 请检查烧录器连接..."));
    }
    return false;
}
  • 断开芯片设备:
void RJLinkView::jlinkdisconnectHandle(void)
{
    jlinkClose();
    if(!jlinkIsOpen())
    {
        infoShowHandle(tr("断开设备成功!"));
    }
    else {
        infoShowHandle(tr("断开设备失败..."));
    }
}
  • 获取CPU ID,原理其实很简单,通过读取对应UUID寄存器,就可以获取过去到芯片的UUID
QString RJLinkView::jlinkGetCpuIdHandle(void)
{
    unsigned char cpuId[12]={0};
    char cpuIdTemp[128]={0};
    jlinkReadMemFunc(0x1FFF7A10, 12, cpuId);
    sprintf(cpuIdTemp, "%02X%02X%02X%02X-%02X%02X%02X%02X-%02X%02X%02X%02X",
            cpuId[3], cpuId[2], cpuId[1], cpuId[0],
            cpuId[7], cpuId[6], cpuId[5], cpuId[4],
            cpuId[11], cpuId[10], cpuId[9], cpuId[8]);
    return QString(cpuIdTemp);
}
  • 选择固件按钮实现,这里指定了文件后缀为.bin格式:
void RJLinkView::on_fwFilePathSelectPushButton_clicked()
{
    QString fwFileName = QFileDialog::getOpenFileName(this, "Firmware file", QCoreApplication::applicationDirPath(), "fw file(*.bin)");
    ui->fwFilePathLineEdit->setText(fwFileName);
}
  • 清除信息显示窗体按钮实现:
void RJLinkView::on_cleanInfoPushButton_clicked()
{
    ui->burnInfoTextBrowser->clear();
}
  • 获取芯片ID按钮实现,需要先连接上设备,然后调用jlinkGetCpuIdHandle方案获取ID之后,断开连接:
void RJLinkView::on_getCpuIdPushButton_clicked()
{
    if(jlinkConnectHandle())
    {
        infoShowHandle(tr("获取CPU ID中,请稍后..."));
        infoShowHandle(tr("获取CPUID成功: ") + jlinkGetCpuIdHandle());
        jlinkdisconnectHandle();
    }
}
  • 同理,擦除flash也是要先连接上设备,然后通过调用jlinkEraseChipFunc方法进行擦除之后,断开连接:
void RJLinkView::on_clearFlashPushButton_clicked()
{
    if(jlinkConnectHandle())
    {
        infoShowHandle(tr("擦除flash中,请稍后..."));
        jlinkEraseChipFunc();
        infoShowHandle(tr("擦除flash成功!"));
        jlinkdisconnectHandle();
    }
}
  • 一键烧录按钮原理,获取固件内容存储到一个缓冲区中,然后启动一个定时器,然后将固件内容搬运到对应的Flash区域:
void RJLinkView::on_burnPushButton_clicked()
{
    bool burnAddrIsOk = false;
    QFile burnFile;

    burnFileSize = 0;
    burnFileContent.clear();

    if(ui->fwFilePathLineEdit->text() == tr(""))
    {
        infoShowHandle(tr("请选择要烧录的固件!"));
        return;
    }

    if(jlinkConnectHandle())
    {
        burnAddr = ui->burnAddrLineEdit->text().trimmed().toInt(&burnAddrIsOk, 16);
        if(!burnAddrIsOk)
        {
            infoShowHandle(tr("烧录起始地址格式有误,请正确输入地址..."));
            jlinkdisconnectHandle();
            return;
        }
        burnFile.setFileName(ui->fwFilePathLineEdit->text());
        burnFile.open(QIODevice::ReadOnly);
        if(burnFile.isOpen())
        {
            burnFileContent = burnFile.readAll();
            burnFileSize = burnFileContent.size();
            burnFile.close();

            infoShowHandle(tr("开始烧录固件, 请稍后..."));

            burnFileTimer->start(RJLINK_BURN_TIME_INTERVAL);
        }
        else
        {
            infoShowHandle(tr("烧录固件打开失败,请检查文件是否存在..."));
            jlinkdisconnectHandle();
        }
    }
}
  • 定时器处理函数,每次烧录从缓冲区提取1k数据进行烧录:
void RJLinkView::burnFileTimerHandle(void)
{
    int burnPercent = 0;
    if(burnFileTimer)
    {
        burnFileTimer->stop();

        if(burnFileContent.isEmpty())
        {
            ui->burnProgressBar->setValue(100);
            jlinkdisconnectHandle();
            infoShowHandle(tr("烧录完成!"));
            return;
        }
        else
        {
            if(burnFileContent.size() > RJLINK_BURN_CONTENT_SIZE)       // 每次搬运1K数据
            {
                jlinkWriteMemFunc(burnAddr, RJLINK_BURN_CONTENT_SIZE, burnFileContent.data());
                burnAddr += RJLINK_BURN_CONTENT_SIZE;
                burnFileContent.remove(0, RJLINK_BURN_CONTENT_SIZE);
            }
            else
            {
                jlinkWriteMemFunc(burnAddr, burnFileContent.size(), burnFileContent.data());
                burnAddr += burnFileContent.size();
                burnFileContent.clear();
            }

            burnPercent = (burnFileSize - burnFileContent.size()) * 100 / burnFileSize;
            ui->burnProgressBar->setValue(burnPercent);
            burnFileTimer->start(RJLINK_BURN_TIME_INTERVAL);
        }
    }
}

演示实例

  1. 获取CPU ID演示,点击"获取CPU ID"按钮,在显示窗体便可以看到对应的ID:

image.png

  1. 擦除flash演示,点击"擦除flash"按钮,会调用SEGGER应用,然后进行flash擦除:

image.png

  1. 烧录程序,点击"一键烧录"按钮,同样会调用SEGGER应用,然后进行烧录:

image.png
image.png

作者:Rice 嵌入式开发技术分享
文章来源:Rice 嵌入式开发技术分享

推荐阅读

更多嵌入式开发干货请关注 Rice 嵌入式开发技术分享 专栏。欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。
推荐阅读
关注数
1754
内容数
51
一个周末很无聊的嵌入式软件工程师,写写经验,写写总结。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息