【Qt调用jlinkARM.dll实现自制J-Flash烧录工具】

背景介绍

想必玩过STM32、GD32的同学都用过下面这个烧录工具吧,它就是J-Flash。通过它再配合我们购买的jlink、jlink-ob等烧录器,便能够非常方便的实现对cortext-M系列的单片机进行程序烧录。

但是在产品的生产和应用过程中,我们通常都会有一些定制化需求,比如读取MCU的芯片ID,根据芯片ID计算出秘钥,烧录程序的同时将秘钥烧录进Flash的某个区域,从而实现软件加密等功能。

原理分析

经过网上大佬们的分析得到一个内部消息,J-Flash烧录工具的大部分接口都封装在安装目录下的JLinkARM.dll动态库中。

知道这个消息后,我们可以使用depends工具对dll库的进行分析, 获取到dll库中所有函数符号,然后在网上查找这些函数的参数类型和个数,然后定义一个函数指针,将指针指向该函数在dll中的地址,从而实现调用。

调用方法

理论上,只要是windows的程序都可以调用dll。
通过Visual Studio( C#)调用JLinkARM.dll,实现程序下载已经有大佬写过文章了。
在此我就以Qt( C++)为例,调用JLinkARM.dll,实现程序下载功能和芯片ID读取功能。调用方式是采用QLibrary类显式调用的方式。

示例程序

下面展示部分示例程序供大家参考,包括函数指针的定义、使用方法、设备连接、读取、烧录的步骤等。

宏定义及函数指针类型定义
//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 (*JLINKARM_Open_Func_Ptr)(void);       // 定义导出函数类型
typedef void (*JLINKARM_Close_Func_Ptr)(void);
typedef DWORD (*JLINKARM_TIF_Select_Func_Ptr)(int);
typedef void (*JLINKARM_SetSpeed_Func_Ptr)(int);
typedef void (*JLINKARM_Reset_Func_Ptr)(void);
typedef void  (*JLINKARM_Go_Func_Ptr)(void);
typedef BOOL (*JLINKARM_IsOpen_Func_Ptr)(void);

typedef void  (*JLINKARM_SetLogFile_Func_Ptr)(char *file);
typedef DWORD (*JLINKARM_GetDLLVersion_Func_Ptr)(void);
typedef DWORD (*JLINKARM_GetHardwareVersion_Func_Ptr)(void);
typedef DWORD (*JLINKARM_GetFirmwareString_Func_Ptr)(char *buff, int count);
typedef DWORD (*JLINKARM_GetSN_Func_Ptr)(void);

typedef BOOL  (*JLINKARM_ExecCommand_Func_Ptr)(char* cmd, int a, int b);
typedef DWORD (*JLINKARM_TIF_Select_Func_Ptr)(int type);
typedef void  (*JLINKARM_SetSpeed_Func_Ptr)(int speed);
typedef DWORD (*JLINKARM_GetSpeed_Func_Ptr)(void);
typedef DWORD (*JLINKARM_GetId_Func_Ptr)(void);
typedef DWORD (*JLINKARM_GetDeviceFamily_Func_Ptr)(void);

typedef BOOL  (*JLINKARM_Open_Func_Ptr)(void);
typedef void  (*JLINKARM_Close_Func_Ptr)(void);
typedef BOOL  (*JLINKARM_IsOpen_Func_Ptr)(void);
typedef BOOL  (*JLINKARM_Connect_Func_Ptr)(void);
typedef BOOL  (*JLINKARM_IsConnected_Func_Ptr)(void);
typedef int   (*JLINKARM_Halt_Func_Ptr)(void);
typedef BOOL  (*JLINKARM_IsHalted_Func_Ptr)(void);
typedef void  (*JLINKARM_SetResetType_Func_Ptr)(int type);
typedef void  (*JLINKARM_Reset_Func_Ptr)(void);
typedef void  (*JLINKARM_Go_Func_Ptr)(void);
typedef void  (*JLINKARM_GoIntDis_Func_Ptr)(void);
typedef DWORD (*JLINKARM_ReadReg_Func_Ptr)(int index);
typedef int   (*JLINKARM_WriteReg_Func_Ptr)(int index, DWORD data);

typedef int   (*JLINKARM_ReadMem_Func_Ptr)(DWORD addr, int len, void *buf);
typedef int   (*JLINKARM_WriteMem_Func_Ptr)(DWORD addr, int len, void *buf);
typedef int   (*JLINKARM_WriteU8_Func_Ptr)(DWORD addr, BYTE data);
typedef int   (*JLINKARM_WriteU16_Func_Ptr)(DWORD addr, WORD data);
typedef int   (*JLINKARM_WriteU32_Func_Ptr)(DWORD addr, DWORD data);

typedef int   (*JLINK_EraseChip_Func_Ptr)(void);
typedef int   (*JLINKARM_DownloadFile_Func_Ptr)(LPCSTR file, DWORD addr);
typedef void  (*JLINKARM_BeginDownload_Func_Ptr)(int index);
typedef void  (*JLINKARM_EndDownload_Func_Ptr)(void);

函数指针定义
JLINKARM_GetDLLVersion_Func_Ptr JLINKARM_GetDLLVersion_Entry = NULL;   //获取DLL版本
JLINKARM_Open_Func_Ptr JLINKARM_Open_Entry = NULL;               //打开设备
JLINKARM_IsOpen_Func_Ptr JLINKARM_IsOpen_Entry = NULL;           //是否已经打开
JLINKARM_Close_Func_Ptr JLINKARM_Close_Entry = NULL;             //关闭设备
JLINKARM_TIF_Select_Func_Ptr JLINKARM_TIF_Select_Entry = NULL;   //选择设备
JLINKARM_SetSpeed_Func_Ptr JLINKARM_SetSpeed_Entry = NULL;       //设置JLINK接口速度 0为自动调整
JLINKARM_Reset_Func_Ptr JLINKARM_Reset_Entry = NULL;             //复位系统
JLINKARM_Halt_Func_Ptr JLINKARM_Halt_Entry = NULL;               //中断程序执行,进入停止状态
JLINKARM_Go_Func_Ptr JLINKARM_Go_Entry = NULL;                   //执行程序
JLINKARM_WriteMem_Func_Ptr JLINKARM_WriteMem_Entry = NULL;       //写内存
JLINKARM_ReadMem_Func_Ptr JLINKARM_ReadMem_Entry = NULL;         //读内存
函数入口加载
//构造函数初始化时,new QLibaray时传入使用的dll文件
QLibrary jlink_lib = new QLibrary("JLinkARM.dll");

void Widget::load_library_function()
{
    if(jlink_lib->load()){
        qDebug()<<"加载JLinkARM.dll成功, 开始解析函数";
        JLINKARM_Open_Entry = (JLINKARM_Open_Func_Ptr)jlink_lib->resolve("JLINKARM_Open");
        JLINKARM_IsOpen_Entry = (JLINKARM_IsOpen_Func_Ptr)jlink_lib->resolve("JLINKARM_IsOpen");
        JLINKARM_Close_Entry = (JLINKARM_Close_Func_Ptr)jlink_lib->resolve("JLINKARM_Close");
        JLINKARM_ExecCommand_Entry = (JLINKARM_ExecCommand_Func_Ptr)jlink_lib->resolve("JLINKARM_ExecCommand");
        JLINKARM_GetDLLVersion_Entry = (JLINKARM_GetDLLVersion_Func_Ptr)jlink_lib->resolve("JLINKARM_GetDLLVersion");
        JLINKARM_TIF_Select_Entry = (JLINKARM_TIF_Select_Func_Ptr)jlink_lib->resolve("JLINKARM_TIF_Select");
        JLINKARM_SetSpeed_Entry = (JLINKARM_SetSpeed_Func_Ptr)jlink_lib->resolve("JLINKARM_SetSpeed");
        JLINKARM_GetSpeed_Entry = (JLINKARM_GetSpeed_Func_Ptr)jlink_lib->resolve("JLINKARM_GetSpeed");
        JLINKARM_Connect_Entry = (JLINKARM_Connect_Func_Ptr)jlink_lib->resolve("JLINKARM_Connect");
        JLINKARM_IsConnected_Entry = (JLINKARM_IsConnected_Func_Ptr)jlink_lib->resolve("JLINKARM_IsConnected");
        JLINKARM_GetId_Entry = (JLINKARM_GetId_Func_Ptr)jlink_lib->resolve("JLINKARM_GetId");
        JLINKARM_GetSN_Entry = (JLINKARM_GetSN_Func_Ptr)jlink_lib->resolve("JLINKARM_GetSN");
        JLINKARM_Reset_Entry = (JLINKARM_Reset_Func_Ptr)jlink_lib->resolve("JLINKARM_Reset");
        JLINKARM_Halt_Entry = (JLINKARM_Halt_Func_Ptr)jlink_lib->resolve("JLINKARM_Halt");
        JLINKARM_WriteMem_Entry = (JLINKARM_WriteMem_Func_Ptr)jlink_lib->resolve("JLINKARM_WriteMem");
        JLINKARM_ReadMem_Entry = (JLINKARM_ReadMem_Func_Ptr)jlink_lib->resolve("JLINKARM_ReadMem");
        JLINK_EraseChip_Entry = (JLINK_EraseChip_Func_Ptr)jlink_lib->resolve("JLINK_EraseChip");
        qDebug()<<"解析函数完成";
    }
    else
    {
        qDebug()<<"加载JLinkARM.dll失败!!";
    }
}
连接设备

所有操作前必需确保设备已经连接,如下为设备连接的示例程序。

bool Widget::connect_device()
{
    if(JLINKARM_IsOpen())
    {
        qDebug()<<"JLINKARM was Opened!";
        return true;
    }
    qDebug()<<"Try Open JLINKARM...";
    JLINKARM_Open();
    if(JLINKARM_IsOpen())
    {
        qDebug()<<"JLINKARM Open success!";
        JLINKARM_ExecCommand("device = STM32F407IG", 0, 0);
        JLINKARM_TIF_Select(JLINKARM_TIF_SWD);
        JLINKARM_SetSpeed(4000); //设置下载速度
        JLINKARM_Connect();
        if(JLINKARM_IsConnected()){
            return true;
        }else
        {
            print_log("连接设备失败! 请检查设备连接..");
        }
    }
    else {
        qDebug()<<"JLINKARM Open fail!";
        print_log("连接设备失败! 请检查烧录器连接..");
    }
    return false;
}

读取芯片ID

连接设备后我们可以通过JLINKARM_ReadMem接口,读取芯片ID,也就是读取CPUID寄存器地址的值。

QString Widget::get_cpu_id()
{
    unsigned char cpuid[12]={0};
    char cpu_id_tmp[128]={0};
    JLINKARM_ReadMem(0x1FFF7A10, 12, cpuid);
    JLINKARM_ReadMem(0x1FFF7A10, 12, cpuid); //多读一次解决读取错误的问题
    sprintf(cpu_id_tmp, "%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]);
    for(int i=0; i<sizeof(cpuid); i++)
        qDebug("cpuid[%d]=%02X", i, cpuid[i]);
    qDebug("cpuid=%s",cpu_id_tmp);
    return QString(cpu_id_tmp);
}

void Widget::on_pushButton_cpuid_clicked()
{
    static int cpuid_reading_flag = 0;
    if(cpuid_reading_flag)
    {
        print_log(QString("获取CPUID中,请稍后..."));
        return;
    }
    if( connect_device()){
//        qDebug("JLink Info:");
//        qDebug("SN = %08u", JLINKARM_GetSN());
//        qDebug("ID = %08X", JLINKARM_GetId());
//        qDebug("VER = %u", JLINKARM_GetDLLVersion());
//        qDebug("Speed = %u", JLINKARM_GetSpeed());
        print_log(QString("获取CPUID中,请稍后..."));
        cpu_id_str = get_cpu_id();
        print_log(QString("获取CPUID成功: ")+cpu_id_str);
        disconnect_device();
    }
}

烧录程序

烧录程序考虑到会持续一段时间,可能会导致界面假死。所以使用了一个QTimer定时器来实现程序烧录过程,每次定时结束时烧录1KB数据,同时更新烧录进度条,直到烧录结束。但是在实际使用过程中,感觉并没有真正将程序烧录至MCU中去,只是传递至jlinkARM.dll接口的内存中去了。而在断开连接时,会自动触发jlinkARM.dll中烧录功能,会弹出一个JFLASH的小烧录窗口进行真正的烧录。

//构造函数初始化时,连接定时器超时信号与槽函数
connect(timer_burn, SIGNAL(timeout()),this, SLOT(on_timer_burn_timeout()));

void Widget::on_pushButton_burn_clicked()
{
    if(burnning_flag)
    {
        print_log("正在加速烧录中,请稍后...");
        return;
    }
    burn_bin_data.clear();
    total_file_size = 0;
    if(target_bin_path.isEmpty())
    {
        print_log("请选择要烧录的固件!");
        return;
    }

    if( connect_device() ){ //连接设备

        cpu_id_str = get_cpu_id();// 获取CPUID

        bool ok = false;        //检查起始地址信息
        write_start_addr = ui->lineEdit_start_addr->text().trimmed().toInt(&ok, 16);
        if(!ok)
        {
            print_log("烧录起始地址格式有误!");
            disconnect_device();
            return;
        }

        QFile burn_file;
        burn_file.setFileName(target_bin_path);
        burn_file.open(QIODevice::ReadOnly); //打开文件
        if(burn_file.isOpen())
        {
            burn_bin_data = burn_file.readAll();    //将要烧录的数据读取到内存中
            burn_file.close();  //关闭文件
            if(burn_bin_data.size() > 1024*1024)
            {
                print_log("文件大小不允许超过1MB!");
                burn_bin_data.clear();
                disconnect_device();
                return;
            }
            print_log("开始烧录固件, 请稍后...");
            burnning_flag = 1;  //正在烧录
            timer_burn->start(BURN_DELAY);
        }
        else
        {
            print_log("打开固件失败, 请检查文件是否存在!");
            disconnect_device();
        }
    }
}

void Widget::on_timer_burn_timeout()
{
    if(timer_burn)
    {
        timer_burn->stop();
        //烧写固件
        if(burn_bin_data.isEmpty())  //烧录完成
        {
            ui->progressBar->setValue(100);
            burnning_flag = 0;
            disconnect_device();
            print_log("烧录完成!");
            return;
        }
        else  //烧录的数据非空
        {
            if(burn_bin_data.size() > BURN_STEP_SIZE)   //大小超过1K
            {
                int ret = JLINKARM_WriteMem(write_start_addr, BURN_STEP_SIZE, burn_bin_data.data());
//                qDebug()<<"JLINKARM_WriteMem ret = "<<ret;
                write_start_addr += BURN_STEP_SIZE; //烧写地址递增
                burn_bin_data.remove(0, BURN_STEP_SIZE);
            }
            else    //大小不到1K
            {
                int ret = JLINKARM_WriteMem(write_start_addr, burn_bin_data.size(), burn_bin_data.data());
//                qDebug()<<"JLINKARM_WriteMem ret = "<<ret;
                write_start_addr += burn_bin_data.size(); //烧写地址递增
                burn_bin_data.clear();
            }

            unsigned int percent = 1.0*(total_file_size - burn_bin_data.size())/total_file_size*100;
//            qDebug()<<"Burn progress: "<<percent;
            ui->progressBar->setValue(percent);
            timer_burn->start(BURN_DELAY);//5ms后继续烧录
        }
    }
}

学习参考传送门

在开发过程中,踩了很多坑,在此整理几个有借鉴意义的文章。
https://www.amobbs.com/thread-5670237-1-1.html
https://www.amobbs.com/thread-5718918-1-1.html
https://blog.csdn.net/qq446252221/article/details/89878996

免责声明

segger官网有配套的SDK,包含完整的头文件、库文件及PDF文档,有条件的同学建议支持正版。
https://www.segger.com/products/debug-probes/j-link/technology/j-link-sdk

本文章仅供学习参考,不可用于商业用途,如侵权请联系删除。

物联沃分享整理
物联沃-IOTWORD物联网 » 【Qt调用jlinkARM.dll实现自制J-Flash烧录工具】

发表评论