我们学习单片机一般都是从51开始的,51单片机烧录程序通常是使用烧录软件如STC-ISP。这种方式,通过串口连接单片机,选择一个合适的波特率就可以烧录了。
后来学习STM32,编程时使用KEIL软件自带的下载按钮就能下载程序,方便了不少,但需要额外使用J-Link等下载器。
再后来,接触到产品研发,给已经发布出的产品升级,都是要靠远程无线升级的。
……
首先需要明确的是,单片机程序下载的本质就是将由0和1组成的hex文件写入到掉电数据不会消失的EEPROM(Electrically Erasable Programmable Read Only Memory,电可擦除可编程只读存储器)中。
最早使用的烧录程序的方式是使用单独的编程器,据说价格比较昂贵,而且每次编程时都需要把可编程芯片取下来放在编程器上,然后再写入程序。【似乎知道为什么现在普遍使用的51单片机最小系统板是用一个夹紧的绿色底座而不是接触更好的双列直插的芯片插座,估计就是历史遗留问题】
这里的编程器是电擦除的,据说更早还有紫外线擦除的,应该是匹配EPROM。
显然,这种烧录程序的方式一个是价格昂贵,意味着你每开发一款芯片,都需要先买一个可能比单片机还贵的编程器;另一方面就是这种编程方式意味着你每改动一次程序都需要拆装一次,不仅麻烦还会对电路板造成损伤,而且如果是成型的产品需要升级程序,还需要返厂或者让技术人员到现场解决,非常不便。于是后来就有了ISP。
ISP (In System Programming, 在系统编程)
所谓ISP,即In System Programming,有些人翻译成“在系统中编程”,确实也有道理,因为原来的编程方式需要将芯片取下,即离开系统,而ISP不需要编程器即可完成程序烧录,此时单片机芯片可以焊在电路板上,调试完即是成品。这里的“系统”应该理解成SoC,即板载系统或者片上系统,而不是我们常说的软件系统。
ISP基本是目前单片机烧录程序的主要方式。它的实现方式就是通过电脑端的上位机软件,通过某种数据传输协议,将程序编译产生的二进制文件烧录到单片机的EEPROM中。一般电路板上还需要添加少量的外围电路辅助程序的烧录。因此调试单片机程序时,只需要将相关的接口留出即可,而不需要来回取下芯片。
其中,因为对烧录速度和质量的要求,人们在 “某种数据传输协议” 上不断更新,因此也就有了各种不同的烧录方式。如STC的51单片机基于的就是串口协议,即程序通过串口写入到FLASH(EEPROM的一种)中;Atmel的AT89S51,AT89S52等系列单片机基于SPI协议将程序写入到FLASH中;STM32系列的芯片采用ST-Link和J-Link等设备来下载程序,其基于的协议为SWD和JTAG,当然,STM32也可以基于串口协议下载程序;Arduino单片机可以通过串口协议下载程序,也可以通过SPI协议下载(算是AVR单片机一个独有的特点吧)。
不过虽然大家都是ISP,但各自还是有区别的。
其中最为特殊的莫过于串口协议,因为其他的各种协议都是借助外界设备(如ST-Link、USBasp)来直接操作单片机的FLASH,而通过串口下载程序时,虽然也需要使用外部设备(一般是一个USB转TTL模块),但是其本质还是靠芯片内部已固化的一段程序来写入FALSH。这也就是为什么网上很多资料都把串口下载称之为ISP,而基于其他协议下载的都不叫ISP。
另外,对于某些单片机,支持ISP模式烧写程序的协议可能不止串口协议,或者说不止一个通讯接口。如STM32F4支持多个串口写入和CAN协议。用来写入FLASH的这部分程序是芯片出厂就已经固化到芯片当中的,称为引导程序,也叫自举程序(自己能举起自己嘛),英文名叫BootLoader。这部分程序是对用户保密的,也就是说用户无法知道这段程序到底是怎么写入FLASH的,只知道它能这么做。
因此,为实现这种功能,芯片内部ROM就可以分为两部分,一部分是系统存储区 (System Flash),一般在低地址,用来存放引导程序;另一部分是用户存储区(User Flash),有些也称为应用程序区(Application Flash),一般在高地址,用来存放用户编写的程序(主要执行的部分)。其示意图如下所示。
以STM32为例,由于它既支持SWD,JTAG这种直接操作FLASH的协议,也支持基于串口协议利用引导程序写入FLASH,因此它需要支持多种启动模式。如下图所示。
这里的“主闪存存储器”即上面提到的用户存储区,用来存放用户编写的程序。
所以,显而易见,如果手边有ST-Link或J-Link,就可以考虑从用户存储区开始运行,这样上位机就直接将数据写入到用户存储区的FLASH中;如果要采用串口下载,就需要从系统存储区开始启动,当芯片在这个部分开始执行程序时,会不断检测串口是否有写入FLASH的指令,如果没有,则开始执行后面的用户程序,如果有,则开始写入用户存储区的FLASH。
但是,一般串口下载要更慢,而且ST-Link或J-Link除了下载程序外,还支持硬件仿真,这也就是为什么用串口下载的比较少。
为了更好地了解这个过程,下面以STC单片机为例来展示这个过程。【图片来自官网手册】
这里值得注意的是,STC单片机对于从系统存储区启动有一个特别的要求,那就是冷启动,即单片机彻底没电(给2-3V供电也不行)。这个时候上电单片机才会执行ISP程序(一般按下RST单片机不会执行ISP程序,只是从头开始执行用户程序),检测是否有烧入程序的需求。
还有一种单独给STC单片机烧录的设备,不用冷启动就能烧录程序,我只知道它大概是通过软件复位到系统存储区去执行ISP程序(这个功能STC已提供,而且应该是独有的),但具体原理还不太明白。(为什么那个下载器能在芯片运行过程中修改芯片内部寄存器的数值呢?)
ICP
ICP全称是
In Circuit Programming
,即在电路中编程,这个我感觉应该是和ISP是一个意思,因为芯片所在的系统不就是电路系统吗?不需要离开电路就能对芯片进行编程,感觉和ISP是一个意思,有些型号的芯片之所以区分这两者估计是使用的协议不同。
IAP (In Application Programming,应用在线编程)
说了这么多ISP,感觉基本够用了,为什么还会有IAP呢?这个主要是用于一些特殊的情况,比如一个产品内程序的远程升级。
和上面的ISP一样,IAP也有翻译成“在应用中编程”,这个也有其合理性,但是个人感觉“应用在线编程”会更形象点,这个“线”就不是指系统了,而是指芯片正在执行应用程序,在这个过程中实现程序的自我更新,此即IAP的原理。也正是这种特殊操作,能够实现对一个已开发的产品进行远程的程序升级。
和ISP相比,IAP还需要一块类似于存放ISP程序的区域来存放IAP程序。
从上图可知,ISP程序引导加载IAP程序,IAP程序引导加载应用程序。在开发者开发产品时,IAP程序必须通过SWD、JTAG或者ISP(串口or其他协议)烧录,第二部分应用程序可以在第一部分烧录时一起烧录,也可以通过IAP程序烧录。
那在程序运行过程中,也就是正在执行应用程序时,是怎么跳转到IAP程序部分执行的呢?STM32中是采用中断的方式。一般中断向量都是存放在低地址,而IAP恰好在低地址段,所以IAP程序就相当于是一个中断服务程序,当相关中断被触发时,芯片就开始执行IAP程序,来进行自我更新。因此,在第二部分代码开始执行时,首先需要把CPU的中断向量表映像到自己的向量表,然后再执行其他的操作。
如果IAP程序被破坏,产品必须返厂才能重新烧写程序,这是很麻烦并且非常耗费时间和金钱的。针对这样的需求,STM32在对Flash区域实行读保护的同时,自动地对用户Flash区的开始4页设置为写保护,这样可以有效地保证IAP程序(第一部分代码)区域不会被意外地破坏。
程序编写
回想ISP方式,要么需要串口,要么需要JTAG或者SWD接口,都需要直连,是没有办法实现远程升级的。另一方面,ISP方式,不管是串口还是JTAG,都需要复位后才能运行新的程序代码,如果是远程,是没有办法再过去手动复位的。而且,有时候产品都装好了,你再给拆下来升级程序显然是不现实的。
那么,IAP就能解决这个问题吗?
IAP可以通过wifi、GPRS、4G等无线技术将程序发送给目标,然后让程序自己实现升级。
关键引导代码如下:
/* Includes ------------------------------------------------------------------*/ #include "MyApplication.h"/* Private define-------------------------------------------------------------*//* Private variables----------------------------------------------------------*/ static uint16_t ulPage_DataBuf[STM32_FlASH_Page_SIZE / 2]; //页面数据缓存/* Private function prototypes------------------------------------------------*/ static void IAP_Write_App_Bin(uint32_t, uint8_t *, uint32_t); //写入APP的bin文件 static void IAP_ExecuteApp(uint32_t); //跳转APP应用程序/* Public variables-----------------------------------------------------------*/ IAP_t IAP = {0,{0},0,IAP_Write_App_Bin,IAP_ExecuteApp }; /* Private function prototypes------------------------------------------------*/ /** @name IAP_ExecuteApp* @brief 通过IAP写入应用程序BIN文件* @param ulStartAddr :起始地址(起始地址必须与Page页面地址对齐)* pBin_DataBuf:数据指针* ulBufLength :应用程序长度(写入的16位数据的个数)* @retval None */ void IAP_Write_App_Bin (uint32_t ulStartAddr, uint8_t * pBin_DataBuf, uint32_t ulBufLength) {uint16_t usCnt = 0; //计数uint32_t ulIndex,ulAppWriteAddr = ulStartAddr; //索引,APP数据写入地址uint8_t* pBinData = pBin_DataBuf; //APP数据指针//起始地址与Page页面地址对齐校验if(((ulStartAddr - FLASH_BASE) % STM32_FlASH_Page_SIZE) != 0){printf("错误:FLASH写入初始地址没有与Page页面地址对齐!");System.Error_Handler();}//按页写入FLASHfor(ulIndex=0; ulIndex < ulBufLength; ulIndex += 2){ulPage_DataBuf[usCnt++] = (uint16_t)(*(pBinData+1) << 8) + (uint16_t)(*pBinData); //数据处理,按半字发送pBinData += 2; //指针偏移2个字节//数据达到1页if(usCnt == STM32_FlASH_Page_SIZE / 2){usCnt = 0; //计数清零STM32_FlASH.Write(ulAppWriteAddr,ulPage_DataBuf,STM32_FlASH_Page_SIZE / 2); //发送一页ulAppWriteAddr += STM32_FlASH_Page_SIZE; //写入地址偏移一页}}//写入最后不到一页的内容if(usCnt > 0){STM32_FlASH.Write(ulAppWriteAddr,ulPage_DataBuf,usCnt);} }__asm void MSR_MSP ( uint32_t ulAddr ) {MSR MSP, r0 //set Main Stack valueBX r14 }/** @name IAP_ExecuteApp* @brief 跳转到应用程序段* @param ulAddr_App: 应用程序段起始地址* @retval None */ void IAP_ExecuteApp(uint32_t ulAddr_App) {pIapFun_TypeDef pJump2App;//检查栈顶地址是否合法if(((*(__IO uint32_t*)ulAddr_App) & 0x2FFE0000) == 0x20000000) {printf("栈顶合法,运行APP\r\n");pJump2App = (pIapFun_TypeDef) *(__IO uint32_t *)(ulAddr_App + 4); //用户代码区第二个字为程序开始地址(复位地址)MSR_MSP(*(__IO uint32_t *)ulAddr_App ); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)pJump2App (); //跳转到APP}else{printf("错误:栈顶地址不合法!");HAL_ResumeTick();System.Error_Handler();} } /********************************************************End Of File ********************************************************/