欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

动手写一个STM8的轻量级bootloader

程序员文章站 2022-07-04 19:30:17
...

STM8凭借其低廉的成本、超高的性价比获得了许多公司的青睐。而在产品中由于方便、安全等需求,往往要使用到IAP下载的方式对已经拿到产品客户进行软件升级(如果产品在批量生产后发现你的程序有问题,而不能IAP更新,那售后维护成本就高了)。
而STM8S003等只有8K Flash的型号是不自带Bootloader的,且ST官方的Bootloader足足会占领4KB Flash空间,且还要考虑固件的保密性等因素,所以编写一个自己的轻量级的Bootloader尤为重要。

下面向大家分享一下本人在一个项目中写的一个Bootloader,最少只需占用0.5KB Flash空间!

【串口收发】

对于单片机而言,Bootloader最重要的功能就是把从串口发送过来的程序数据保存到MCU 的Flash上(即IAP下载),并跳转到所下载程序的起始地址并运行。所以串口功能必不可少。下面是我们用到的关于串口收发的函数(头文件添加stm8s.h,直接操作寄存器以节省Flash空间),MCU型号为STM8S003。

void UART1_SendByte(u8 data) //串口发一个字节
{ 
    UART1->DR=data;
    while (!(UART1->SR & 0x80));//等待发送完成
}
void UART_Init(void) //串口初始化函数
{     
    UART1->CR2 = 0;                       
    UART1->CR3 = 0;// b5,b4 = 00,1个停止位
    UART1->BRR2=0x00;
    UART1->BRR1=0x1a;
    //BRR2和BRR1设置串口波特率,这里设置的在2MHz默频下是4800的波特率
    UART1->CR2 = 0x2C; // b3 = 1,允许发送
    UART1->CR1 |= (0<<5);
}
void UART1_SendStr(u8* data)//发送字符串函数
{
    while (*data)
        UART1_SendByte(*data++);
}
u8 UART1_RcvB(void)//串口接收函数,以扫描(等待)方式
{
     while(!(UART1->SR & (u8)UART1_FLAG_RXNE));//等待接收到数据
         return ((uint8_t)UART1->DR);
}

有小伙伴就会问了,串口接收为何不用中断?因为STM8没有STM32那样的向量地址的偏移,因此你的APP(正式的程序)和Bootloader中只能有一个使用中断。当然也有同时都可以用中断的方法,但是本人愚见,觉得实现基本的下载升级功能用不着中断,添加中断还会增加Bootloader的代码量不是?不过如果真的需要,CSDN论坛有一篇《集合帖:STM8之支持中断方式的IAP技术实现》的帖子大家可以搜来看看。

【Flash的写入】

查阅Datasheet可以知道STM8S003 FLASH的起始地址是0x8000,到0x9fff结束共8KB,每64个字节在一个Block(块)。而我这里的Bootloader放在开头,即从0x8000开始,这样上电默认就进入bootloader。那么APP程序放在哪里呢,有空的地方就可以放~,比如你想把bootloader的存放空间预留1KB,那么你的APP就从0x8400开始存放,0x8400-0x9fff共7KB空间可供你编程。如果你IAP功能比较精简,没有数据加密、校验什么的,0.5KB就够,那么甚至可以把APP起始地址提到0x8200。
下面是个写一个块的Flash的函数,因为写Flash时程序不能在Flash中执行,故函数在RAM中执行。

//写Flash函数,addr地址必须是每个Block的开头(0x40的倍数)
IN_RAM(void FLASH_ProgBlock(uint8_t *addr, uint8_t *Buffer))
{
    u8 i;    

    FLASH->NCR2 &= (uint8_t)(~FLASH_NCR2_NPRG);
    FLASH->CR2 |= FLASH_CR2_PRG;
    for (i = 0; i < 64; i++)//一个块共写64个字节
    {
        *((PointerAttr uint8_t*) (uint16_t)addr + i) = ((uint8_t)(Buffer[i]));
    }
}

在写Flash之前别忘了对Flash解锁

#define FLASH_RASS_KEY1 ((uint8_t)0x56)
#define FLASH_RASS_KEY2 ((uint8_t)0xAE)//宏定义添加

    FLASH->PUKR = FLASH_RASS_KEY1;//代码添加,解锁Flash程序区
    FLASH->PUKR = FLASH_RASS_KEY2;

最常见的IAP方式是在刚上电时等待一定时间检测有没有串口命令,有就进入下载模式,没有就跳到APP,以下是最基本的IAP实现:

int main(void)
{           
    u16 j = 0;
    u8 ch, high, low;
    u8 buf[64]; //接收缓冲区

    asm("sim"); //关闭总中断使能
    UART_Init();
    UART1_SendStr("START\r\n");//发送开机提示

    while (j < 50000)//等待循环这么长时间
    {
        if(UART1->SR & (u8)0x20)    //如果串口收到数据
        {
            ch = (uint8_t)UART1->DR;
            if(ch == 0xa5) break; //收到了0xa5,则进入下载模式
        }
        j++;
    }

    if (j == 50000)//循环是超时退出的
    {
        asm("JPF $8400");//跳到0x8400这个地址去执行APP。
    }      
    //unlock flash,解锁flash
    FLASH->PUKR = FLASH_RASS_KEY1;
    FLASH->PUKR = FLASH_RASS_KEY2;
    UART1_SendStr("pro\r\n");
//发送开始编程提示
    while(1)
    {
        high = UART1_RcvB();
        low = UART1_RcvB();
        addr = (u8 *)((high << 8) | low);//先接收到本次数据16位的起始地址
        if (addr == 0)//如果收到的是0,就是下载结束了
        {
            UART1_SendStr("OK!\r\n");//发送下载完成提示给上位机
            FLASH->IAPSR &= FLASH_MEMTYPE_PROG; //锁住flash
            asm("JPF $8400");//跳转到0x8400执行刚下载完的APP
        }
        for (i = 0; i < 64; i++)
            buf[i] = UART1_RcvB();//接收64字节数据
        FLASH_ProgBlock(addr, buf);//将收到的数据写到相应地址的Flash
    }
}

上述程序使用IAR编译,仅用不到0.5KBFlash就实现了最基本的IAP功能,当然,下载程序需要使用协议与之匹配的上位机。

【中断向量重映射】

使用上述bootloader程序下载app程序后你会发现中断无法使用,因为STM8的默认中断向量地址是在0x8000-0x8080,stm8编译出的二进制文件会把向量表放在前0x80个字节。即如果在app中使用了中断,那么程序就会跳回bootloader程序中去了,解决这个问题的方是进行中断向量重映射。

在bootloader程序main.c文件的函数之外添加以下代码:

__root const long reintvec[]@".intvec"= 
{
0x82008080,0x82008404,0x82008408,0x8200840c, 
0x82008410,0x82008414,0x82008418,0x8200841c, 
0x82008420,0x82008424,0x82008428,0x8200842c, 
0x82008430,0x82008434,0x82008438,0x8200843c, 
0x82008440,0x82008444,0x82008448,0x8200844c, 
0x82008450,0x82008454,0x82008458,0x8200845c, 
0x82008460,0x82008464,0x82008468,0x8200846c, 
0x82008470,0x82008474,0x82008478,0x8200847c, 
};//当应用程序地址不是0x8400时则要相应改掉除第一个0x82008080以外的数值 

便完成了中断向量的重映射,这里映射到了0x8400-0x8480,即APP程序的中断向量表存放区。
但是这时候你的IAR编译器会报错:
动手写一个STM8的轻量级bootloader
中断向量空间不够?当然,因为加入了重定向数组。

接下来进入IAR设置下,Linker的Config下,可以看到使用了inkstm8s003f3.icf 这个配置文件:
动手写一个STM8的轻量级bootloader

我们用文本编辑器打开:
动手写一个STM8的轻量级bootloader
这不就是报错的INTVEC size吗,把它从0x80改成0x100,并保存,再回IAR编译就完美通过了!

【APP程序的设置】

现在有bootloader了,APP存放的地址改变了,那么APP中一些关于地址的量也需要跟着改变,换句话说就是编译器编译出的地址也要变。所以你需要将上面改INTVEC size大小的那张图改成下面这样再对APP进行编译:
动手写一个STM8的轻量级bootloader
嗯,所有的0x8000都改成了0x8400(APP程序的起始地址),中断向量表占用空间为0x80。在这样设置、保存编译后,APP通过bootloader下载进去就可以用了。

【数据的校验】

前面我们的bootloader程序是收到字节便马上写入Flash,写完后直接运行,可是万一出现串口多收/漏收/错收或者是Flash写入失败的情况怎么办?那就…

void get_flash_verify(u8 *add)//发送校验码的函数
{
  u16 i;
  u8 ch;
  u8 verify = 0;

  for (i = 0; i < 64; i++)
  {     
      ch = *((PointerAttr uint8_t*) (uint16_t)add + i);//读取flash并累加
      verify += ch;
  }
  UART1_SendByte(verify);
}

此函数读取了add地址开始的64字节Flash的值,并将他们累加,相加完的和取低8位得到一个校验码,并将该码通过串口发送出去。此函数在单片机每次写完一个块的Flash后调用,上位机将这个码与自己的数据算出的值进行比对便实现了校验,如果校验不正确,上位机便可重新发送命令写该快。


未完待续