动手写一个STM8的轻量级bootloader
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编译器会报错:
中断向量空间不够?当然,因为加入了重定向数组。
接下来进入IAR设置下,Linker的Config下,可以看到使用了inkstm8s003f3.icf 这个配置文件:
我们用文本编辑器打开:
这不就是报错的INTVEC size吗,把它从0x80改成0x100,并保存,再回IAR编译就完美通过了!
【APP程序的设置】
现在有bootloader了,APP存放的地址改变了,那么APP中一些关于地址的量也需要跟着改变,换句话说就是编译器编译出的地址也要变。所以你需要将上面改INTVEC size大小的那张图改成下面这样再对APP进行编译:
嗯,所有的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后调用,上位机将这个码与自己的数据算出的值进行比对便实现了校验,如果校验不正确,上位机便可重新发送命令写该快。
未完待续
上一篇: 搜狗输入法成国内第一大语音输入应用