STM32 串口 ISP 攻略

1 STM32 串口 ISP 原理

STM32 单片机系统存储器中有一段 Bootloader 代码,他的主要作用为通过串口下载程序代码到单片机内部 Flash 中。该过程称为串口 ISP,也就是说串口与 Bootloader 进行通信,就可以实现代码下载功能了,下面详细讲解与 Bootloader 的通信规则。

2 Bootloader 通信规则

2.1 启动 bootloader

与 Bootloader 通信前,需要先启动他,也就是需要让单片机进入到 Bootloader 到代码中执行。通过 STM32 的两个引脚可以实现配置。如下图所示:

image.png

如上图所示,可以配置成三种模式,第二种为启动 Bootloader 方式,即 BOOT0=1,BOOT1=0。

2.2 Bootloader 编码序列

image.png

当 STM32F10xx 系列单片机进入 Bootloader 程序后,单片机开始检测 PA10(串口 1 接收引脚)是否收到串口数据 0x7F,并且要求串口数据格式为 1bit 起始 +8bit 数据 +1bit 偶校验 +1bit 停止位。

收到 0x7F 后,单片机将内部串口的波特率设置和发送数据方相同,并且给主机回复应答信号 0x79。为方便描述,我们这里将发送数据方称为主机,STM32 单片机方称为从机。回复完应答信号 0x79 后,表示从机已经准备好接收主机的指令了。

2.3 波特率选择

为保证数据传输的正确性,一般选择在 1200bps 至 115200bps 之间。

2.4 Bootloader 指令集

完整指令集如下图,但并不是单片机同时支持所有的指令,后续将逐一介绍。

2.4.1 Get 命令

该指令允许主机获取从机的 Bootloader 版本号以及支持哪些命令。当从机收到主机发来的 Get 命令后,从机将 Bootloader 版本及所支持的命令发送给主机。发送 Get 命令的主机端时序如下图所示。

image.png

代码实现如下:

char GetCMD(DeviceInfo_t *DInfo)
{
   char error,len;

   //step1 发送指令0x00,0xFF,并等待ACK
   SendByte(0x00);
   SendByte(0xFF);
   error = ACK();if(!error) return 0;

   //step2 接收数据
   len = GetByte();  DInfo->cmd_count = len;//第一个字节为命令数量N
   DInfo->bootloaderversion = GetByte();//第二个字节为版本号
   for(int i =0;i<len;i++) DInfo->cmd[i] = GetByte();//获取CMD
   error = ACK();if(!error) return 0;

    //step3 处理数据,并打印版本号,命令等
    //打印数据,并清空串口缓存
    return 1;
}
2.4.2 Get ID 命令

该命令用于获取单片机的的产品 ID,主机端时序如下图所示。

image.png
代码实现如下:

char GetID(DeviceInfo_t *DInfo)
{
    char len,error;

    //step1 发送序列0x02,0xfd,并等待ACK
    SendByte(0x02);
    SendByte(0xFD);
    error = ACK();if(!error) return 0;

    //step2 接收PID,并等待ACK
    len = GetByte();//第一个字节为ID长度N-1
    for(int i =0;i<=len;i++) DInfo->PID[i] = GetByte();//获取ID
    error = ACK();if(!error) return 0;

    return 1;
    //step3 清空接收缓存,并打印PID   
}
2.4.3 Read Memory 命令

读内存命令用来读取从机单片机内部 RAM,FLASH,信息块(包括系统存储区和选项字节)中的数据。主机端时序如下所示。

image.png

代码实现如下:

/////////////////////////////////////////////////
//
//读取数据
//与写数据步骤类似
//addr必须能被4整除,len发送数据长度-1,单次不能超过256B
/////////////////////////////////////////////////
char ReadMem(unsigned char *data, unsigned int addr, unsigned char len)
{
    unsigned char temp[4],error;      //保存addr的四个字节
    int i;
    temp[0] = ((addr>>24) & 0xFF);
    temp[1] = ((addr>>16) & 0xFF);
    temp[2] = ((addr>> 8) & 0xFF);
    temp[3] = ((addr    ) & 0xFF);
    //step1 发送序列0x11,0xEE,并等待ACK
    SendByte(0x11);
    SendByte(0xEE);
    error = ACK();if(!error) return 0;
    //step2 发送地址,先发高字节
    SendByte(temp[0]);
    SendByte(temp[1]);
    SendByte(temp[2]);
    SendByte(temp[3]);
    //step3 发送地址校验,并等待ACK
    SendByte(CheckSum(temp, 4));
    error = ACK();if(!error) return 0;
    //step4,发送len及校验,并等待ACK
    SendByte(len);
    SendByte(~len);
    error = ACK();if(!error) return 0;
    //step4 接收长度为len+1的数据
    for(i=0;i<=len;i++) data[i] = GetByte();
  
    return 1;
}
2.4.4 Write Memory 命令

写内存命令用来往从机单片机内部 RAM,FLASH,信息块(包括系统存储区和选项字节)中写入数据,主机端时序如下所示。

image.png

往选择字区写入数据时,开始地址必须为 0x1FFFF800。

代码实现如下:

/////////////////////////////////////////////////
//
//写入数据块,从*data处,往stm32的addr处,写入len+1字节数据
//
/////////////////////////////////////////////////
char WriteMem(unsigned char *data, unsigned int addr, unsigned char len)
   
{
    unsigned char temp[4],error;     //保存addr的四个字节
    int i;
    temp[0] = ((addr>>24) & 0xFF);
    temp[1] = ((addr>>16) & 0xFF);
    temp[2] = ((addr>>8 ) & 0xFF);
    temp[3] = ((addr    ) & 0xFF);

    //step1 发送序列0x31,0xCE,并等待ACK
    SendByte(0x31);
    SendByte(0xCE);
    error = ACK();if(!error) return 0;
    //step2 发送地址,先发高字节
    SendByte(temp[0]);
    SendByte(temp[1]);
    SendByte(temp[2]);
    SendByte(temp[3]);
    //step3 发送地址校验,并等待ACK
    SendByte(CheckSum(temp, 4));
    error = ACK();if(!error) return 0;
    //step4 发送len
    SendByte(len);
    //step5 连续发送数据,最后字节为校验,并等待ACK
    for(i=0;i<=len;i++)
    {
        SendByte(data[i]);
    }
    SendByte(len ^ CheckSum(data, len+1));
    //delay_msec(3000);
    error = ACK();if(!error) return 0;

   //清空串口缓存,并延时1s.
    return 1;
}
2.4.5 Erase Memory 命令

擦除从机单片机内部 FLASH 命令,仅单片机 Bootloader 版本 3.0 以下支持该命令。主机端时序如下所示。

image.png

全片擦除代码实现如下:

/////////////////////////////////////////////////
//
//全片擦除,bootloader V3.0以下有效
// step1 发送序列0x43,0xbc,并等待ACK
// step2 发送序列0xff,0x00,并等待ACK
// step3 清空串口接收缓冲
/////////////////////////////////////////////////
char EraseAll()
{
   char error;
    SendByte(0x43);//
    SendByte(0xBC);
    error = ACK();if(!error) return 0;
    SendByte(0xFF);
    SendByte(0x00);
    //不同的产品,全片擦除的时间长短不一,500ms的时间不一定够,
    //因此不能用现成的ACK函数,需重写如下:
    //error = ACK();if(!error) return 0;
    while(!MyComRevBUff.size())
    {
        delay_msec(1);//出让线程
    }
    if(0x79 == GetByte()) return 1;
    else                  return 0;
}
2.4.6 Extended Erase Memory 命令

仅单片机 Bootloader 版本 3.0 及以上支持该命令。主机端时序如下所示,以 Erase Memory 命令相比,该命令为双字节命令,即 Erase cmd 为双字节,例如 0xFFFF。

image.png

全片擦除代码实现如下:

/////////////////////////////////////////////////
//
//全片擦除,bootloader V3.0及以上有效
// step1 发送序列0x43,0xbc,并等待ACK
// step2 发送序列0xff,0x00,并等待ACK
// step3 清空串口接收缓冲
/////////////////////////////////////////////////
char ExtendedEraseAll()
{
   char error;
    SendByte(0x44);//
    SendByte(0xBB);
    error = ACK();if(!error) return 0;

    unsigned char EraseCMD[2];
    EraseCMD[0] = 0xFF;
    EraseCMD[1] = 0xFF;

    SendByte(0xFF);//写入地址0xFFFF
    SendByte(0xFF);

    SendByte(CheckSum(EraseCMD, 2));//双字节校验

    //不同的产品,全片擦除的时间长短不一,500ms的时间不一定够,
    //因此不能用现成的ACK函数,需重写如下:
    //error = ACK();if(!error) return 0;
    while(!MyComRevBUff.size())
    {
        delay_msec(1);//出让线程
    }
    if(0x79 == GetByte()) return 1;
    else                  return 0;
}
2.4.7 GO 命令

跳转到从机的指定地址开始执行程序。G0 命令主机时序如下。

image.png

代码实现如下:

/////////////////////////////////////////////////
//
//跳转执行指令,下载完成后,跳转到RAM或内部FLASH执行
//
//
/////////////////////////////////////////////////
char CMDGo(unsigned int addr)
{
    unsigned char temp[4],error;     //保存addr的四个字节
    int i;
    temp[0] = ((addr>>24) & 0xFF);
    temp[1] = ((addr>>16) & 0xFF);
    temp[2] = ((addr>>8 ) & 0xFF);
    temp[3] = ((addr    ) & 0xFF);

    //step1 发送序列0x21,0xDE,并等待ACK
    SendByte(0x21);
    SendByte(0xDE);
    error = ACK();if(!error) return 0;
    //step2 发送地址,先发高字节
    SendByte(temp[0]);
    SendByte(temp[1]);
    SendByte(temp[2]);
    SendByte(temp[3]);
    //step3 发送地址校验,并等待ACK
    SendByte(CheckSum(temp, 4));
    error = ACK();if(!error) return 0;

    return 1;
}

3 STM32 串口 ISP 操作参考步骤

  • step1 主机发送 0x7F,启动从机并等待接收主机命令;
  • step2 主机发送 Get 命令,获取从机 Bootloader 版本号及所支持的命令等;
  • step3 主机发送 Get ID 命令,获取从机产品 ID;
  • step4 主机发送擦除命令,擦除从机内部 FLASH,为写数据做准备;
  • step5 主机发送 Write Memory 命令,将代码写入从机器;
  • step6 主机发送 Read Memory 命令,读取从机代码并与写入的数据进行对比校验;
  • step7 主机发送 Go 命令,从机跳转至程序开始处执行代码,观察现象是否与程序一致。
  • step8 结束。