开发环境:Window 7
开发工具:Keil uVision4
硬件:stm32f103c8t6
在研发调试的时候我们一般用烧录器下载代码,对于stm32f103c8t6来说,还可以用串口下载,步骤如下:
1.PC端下载一个上位机Flash Loader Demo
2.芯片的串口引脚Tx、Rx(PA.9、PA.10)通过USB>TTL连接到电脑上
3.将芯片的boot0引脚接高电平、boot1引脚接低电平。这是为了让芯片上电的时候从系统存储区启动,原厂的isp程序保存在那里,地址是0x1FFF 000 ~ 0x1FFF 77FF。系统存储区是用户不能访问的区域,它在芯片出厂时已经固化了启动程序,它负责实现串口、USB以及CAN等isp烧录功能。
4.打开上位机,配置如图,波特率可多选,这是因为上位机在发送握手0x7F时,芯片接收到0x7F后,就能将波特率算出来,然后给自身串口初始化,跟上位机设置一样的波特率。接着给芯片上电,上位机选择bin文件,下载到芯片里面后,将boot0和boot1引脚接低,重新上电,就能运行刚下载的程序了。
这样就可以在没有烧录器的情况下下载程序了,当然如果要进行调试的话,还是需要烧录器。
stc32f103c8t6内部有一个64k的flash存储器,用于储存代码,在电脑上编译好的程序,通过烧录器把它烧录到内部flash中。Flash里面的内容掉电不会丢失,烧录完,芯片重新上电,就可以从内部flash中加载代码(起始地址一般是0x0800 0000)。
内部falsh除了用烧录器读写外,还可以在芯片运行时,对自身的内部flash进行读写。如果flash储存了程序后还有剩余的空间,那么可以把它用来保存程序运行时产生需要掉电保存的数据;也可以在芯片运行时将另一个编译后的二进制程序文件写到剩余的flash,然后进行跳转到新的程序上面运行。这也是iap的实现原理。
所有flash操作相关的函数接口在stm32f10x_flash.h里面。读flash里面的数据直接根据地址读出来就行了。往写flash里面写数据,需要解锁,擦除,写入数据,上锁;擦除后存储单元都变成1,因为储存单元不能由0变1,所以在写入之前一定要先擦除,不然会写入失败。
操作代码如下:
#define address 0x08006000 //写入的flash地址
#define value 0x55aa55aa //将要写的数据
void flash_test(void)
{
uint32_t *pdata=address;
Printf(“data=%d”,*pdata); //先将原来的数据打印出来
FLASH_Unlock(); //解锁
FLASH_ErasePage(address);
//擦除,擦除只能按页擦,擦除address地址所在的页,不同的芯片一页的大小不一样,对于stc32f103c8t6来说,一页就是1024字节,也就是1k。
FLASH_ProgramWord(address,value);
/*将data写入address地址里面,除了写入uint32_t类型,还可以写uint16_t类型的数据,对于一份很长的代码来说,只能这样一个个的写进去flash,对应接口如下:
FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data);
FLASH_Status FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data);*/
FLASH_Lock(); //上锁,保护数据
Status=FLASH_WaitForLastOperation(0xFFFFFF);//等待烧写结束,参数是等待的超时时间
if(Status==FLASH_COMPLETE){
//写入成功
}
Printf(“data=%d”,*pdata);//打印确认是否写入成功
}
要注意,address的地址不能指向自身的代码区域,因为修改了自身的程序会造成不可预测的效果,所以要指向自身程序后面的空余区域,一般来说从0x08000000+加上程序的大小,后面的就是空余区域。下图是对于芯片stc32f103c8t6的工程配置:
对于升级的方式,可以选择以下几种,如USART,IIC,CAN,USB,以太网接口甚至是无线射频通道,将程序文件发送到iap。
存储区划分:
Bootloader工程:0x800 0000-0x800 2BFF (11k)
升级标志:0x800 2C00-0x800 2FFF (1K)
App工程:0x800 3000 -0x801 0000 (52k)
芯片上电首先是进入bootloader工程的,所以把bootloader放在前面。升级标志可能会有人问为啥要1k这么大,一个字节不行了吗?首先,bootloader和app都可能会对升级标志的值进行修改或读取,所以不能保存在RAM,只能保存在ROM,那么对ROM的数据进行修改就是flash读写操作,上面提了要先擦除,而且擦除是按页擦,一页就是1k,所以就算标志位不需要1k这么大,只用其一个字节,那剩下的也不能用到其他地方,因为它随时会被擦除。
下面开始说明这两部分的代码实现,其中的一些配置也要细心注意。
Bootloader程序开机引导app程序,在运行app程序中,若收到升级信号,则从app跳转到bootloader里,然后boorloader通过串口接收新的程序文件,对app进行升级。所以,我们还需要一个上位机将程序文件通过串口发送给bootloader,为了方便,我没自己做上位机,直接用Flash Loader Demo,这个可以网上下载。那么上位机有了,还要了解它的通讯协议, 到底数据是怎么从上位机发送过来了,bootloader该怎么接收数据。
其实我们要做的bootloader工程就是要实现原厂isp的功能,跟上位机同步,接受上位机数据。我们无法得到人家的isp代码,但是可以上st的官网下载它的isp协议。了解了它的协议就能自己写单片机端的代码了。协议下载链接。这里不对这个协议进行细说,直接说明实现的代码,下图是原厂isp所支持的命令。
建立bootloader工程,打开一个新的带stm32标准库的keil工程,对工程进行如下配置。
第一步,初始化USART1外设,这里不做波特率自适应,把波特率固定为115200,那么上位机配置就要跟其保持一致。
创建
#ifndef __USART1_INIT_H__
#define __USART1_INIT_H__
#include "stm32f10x.h"
#include
void USART1_Configuration(void);//打印输出串口初始化
void sengdata(unsigned char data);
unsigned char waitdata(void);
#endif
创建
#include "USART1.h"
#include "Queue.h"
void USART1_Configuration(void)//打印输出串口初始化
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
//配置串口1 (USART1) 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
//配置串口1接收终端的优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
//配置串口1 发送引脚(PA.09)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//配置串口1 接收引脚 (PA.10)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//串口1工作模式(USART1 mode)配置
USART_InitStructure.USART_BaudRate = 115200;//设置波特率;
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //数据位为8个字节
USART_InitStructure.USART_StopBits = USART_StopBits_1; //一位停止位
USART_InitStructure.USART_Parity = USART_Parity_No ; //无校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //不需要流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //接收发送模式
USART_Init(USART1, &USART_InitStructure);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启中断
USART_Cmd(USART1, ENABLE);//使能串口
}
void sengdata(unsigned char data)
{
USART_SendData(USART1, (unsigned char) data);
while( USART_GetFlagStatus(USART1,USART_FLAG_TC)!= SET);
}
extern QueueT RxQueueEntity;
unsigned char waitdata(void) //阻塞等待一个数据到来
{
while(1){
if(getDataCount(&RxQueueEntity)!=0){
return outQueue(&RxQueueEntity);
}
}
}
第二步,创建接收队列,因为上位机发送过来的数据很多,芯片不能及时处理,那么就要先把数据放进队列,然后逐个拿出来处理。这样就不会丢失数据。直接复制下面代码就行,可以先不用理解。
创建
#ifndef __QUEUE__H__
#define __QUEUE__H__
#include "core_cm3.h"
typedef struct
{
u16 in;
u16 out;
u16 cntMax;
u8* pBuf;
}QueueT;
/*队列的特点:先进先出,若队列满了,不能再放数据。可循环使用队列的位置*/
void QueueCreate(QueueT* thiz,u8* BufAddress,u16 BufSize); //创建一个队列,初始化结构体里面的成员
u16 getDataCount(QueueT* thiz); //获取队列里面有效的数据的大小
u16 getEmptyCount(QueueT* thiz); //获取队列里面还剩余多少空的位置
u8 inQueue(QueueT* thiz,u8 data); //将一个数据放进队列
u8 outQueue(QueueT* thiz); //从队列里面拿一个数据出来
#endif //__QUEUE__H__
创建
#include "Queue.h"
void QueueCreate(QueueT* thiz,u8* BufAddress,u16 BufSize)
{
thiz->in=0;
thiz->out=0;
thiz->cntMax=BufSize;
thiz->pBuf=BufAddress;
}
u16 getDataCount(QueueT* thiz)
{
if (thiz->in >= thiz->out){
return (thiz->in - thiz->out);
}else{
return (thiz->in + thiz->cntMax - thiz->out);
}
}
u16 getEmptyCount(QueueT* thiz)
{
u16 dataCnt;
if (thiz->in >= thiz->out){
dataCnt=(thiz->in - thiz->out);
}else{
dataCnt=(thiz->in + thiz->cntMax - thiz->out);
}
if ((dataCnt+1u) >= thiz->cntMax){
return 0; //fifo full
}
return (thiz->cntMax-dataCnt-1u);
}
u8 inQueue(QueueT* thiz,u8 data)
{
u16 in;
in = thiz->in + 1u;
if (in >= thiz->cntMax){
in = 0;
}
if (in == thiz->out){ //full fifo
return 0;
}
thiz->pBuf[thiz->in] = data;
thiz->in = in;
return 1;
}
u8 outQueue(QueueT* thiz)
{
u8 data;
u16 out;
if (thiz->in == thiz->out){ //empty fifo
return 0;
}
out = thiz->out;
data = thiz->pBuf[out];
out++;
if (out >= thiz->cntMax){
out = 0;
}
thiz->out = out;
return data;
}
第三步,建立与上位机的通讯协议,接收上位机的命令并作出相应的应答。下面代码是根据协议AN2606来写给上位机的应答的。
创建
#ifndef __IAP_H__
#define __IAP_H__
#include "core_cm3.h"
#define updata_flagaddr 0x8002C00 //升级标志
#define verify_flagaddr 0x800FF10 //在app里面
#define ACK 0x79 //肯定应答
#define NACK 0x1F //否定应答
typedef struct{
unsigned char cmd;
void (*pfunction)(void);
}CommandHandleStruct;
typedef void (*iapfun)(void);
void jump_to_app(u32 appxaddr);
extern const CommandHandleStruct CmdHdlStr[11];
#endif //__IAP_H__
创建
#include "iap.h"
#include "USART1.h"
static unsigned char bootloaderversion=0x22;
static unsigned char cmd_count=11;
static unsigned char cmd[11]={0x00,0x01,0x02,0x11,0x21,0x31,0x43,0x63,0x73,0x82,0x92};
void jump_to_app(u32 appaddr) //跳转函数
{
iapfun jump2app;
if(((*(vu32*)appaddr)&0x2FFE0000)==0x20000000)
{
__set_PRIMASK(1);
__set_MSP(*(vu32*)appaddr);
jump2app=(iapfun)*(vu32*)(appaddr+4);
jump2app();
}
}
unsigned char checksum(unsigned char *data, int len) //计算p开始len个字节的checksum,也就是计算异或
{
int i;
unsigned char cs;
cs = 0;
for ( i=0; i=0x08000000)||(addr<0x08010000)){
sengdata(ACK);
}else{
sengdata(NACK);
return;
}
len=waitdata();
recchecksum=waitdata();
tempsum=~len;
if(recchecksum==tempsum){
sengdata(ACK);
}else{
sengdata(NACK);
return;
}
for(i=0;i=0x08000000)&&(addr<0x08010000)){
sengdata(ACK);
}else{
sengdata(NACK);
return;
}
//清除升级标志
FLASH_Unlock(); //解锁
FLASH_ErasePage(updata_flagaddr);
FLASH_ProgramWord(updata_flagaddr,0x00);
FLASH_Lock(); //上锁
FLASH_WaitForLastOperation(0xFFFFFF);//等待擦除结束
jump_to_app(addr);
}
void WriteMemorycommand(void) //AN2606 page20
{
unsigned int Status;
unsigned int addr,recdata_32bit,i;
unsigned char temp[4],recchecksum=0,len,recdata[0xFF];
temp[0]= waitdata();
temp[1]= waitdata();
temp[2]= waitdata();
temp[3]= waitdata();
addr=(temp[0]<<24)|(temp[1]<<16)|(temp[2]<<8)|temp[3];
recchecksum = waitdata();
if(recchecksum==checksum(temp,4)&&(addr>=0x08000000)&&(addr<0x08010000)){
sengdata(ACK);
}else{
sengdata(NACK);
return;
}
len=waitdata();
for(i=0;i
第四步,创建main.c
#include "stm32f10x.h"
#include "USART1.h"
#include "iap.h"
#include "Queue.h"
void GPIO_Configuration(void);
#define RxbufSize 500
QueueT RxQueueEntity; //包含了队列内信息的结构体
u8 rxbuf[RxbufSize];//队列缓存
int main(void)
{
u8 ch=0,checksum,recchecksum,i;
RCC_DeInit();
SystemInit();
__set_PRIMASK(0);
USART1_Configuration();//串口初始化
GPIO_Configuration(); //将接到PC.13的lcd点亮
QueueCreate(&RxQueueEntity,&rxbuf[0],RxbufSize); //创建串口1的接收队列
if(*(uint16_t*)updata_flagaddr==0){ //不需要升级
if((*(unsigned int*)verify_flagaddr)==0xaabbccdd){
jump_to_app(0x8003000);//跳转到app
}
}
//等待升级
while(1){
if(getDataCount(&RxQueueEntity)!=0){
ch=outQueue(&RxQueueEntity);
if(ch==0x7f){ //接收到上位机发送的同步信号7f
sengdata(ACK);
}else{ //接收到上位机的命令
for(i=0;i<11;i++){
if(CmdHdlStr[i].cmd==ch){
checksum=~ch;
recchecksum=waitdata();//接收校验位
if(recchecksum==checksum){
sengdata(ACK);
CmdHdlStr[i].pfunction(); //调用相应的应答函数
break;
}
}
}
}
}
}
}
void GPIO_Configuration(void) //在运行bootloader时,设置pin13的led常亮
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOC , ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOC, &GPIO_InitStructure);
GPIO_ResetBits(GPIOC,GPIO_Pin_13);
}
最后,找到stm32f10x_it.c,加入:
将串口接收到的数据放进队列。
#include "Queue.h"
extern QueueT RxQueueEntity;
void USART1_IRQHandler(void)
{
u16 code,Status;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
code=USART_ReceiveData(USART1);
Status = USART_GetFlagStatus(USART1, USART_FLAG_NE|USART_FLAG_PE|USART_FLAG_NE);
if(Status!=RESET){//如果发生错误接忽略接收的数据
USART_ClearFlag(USART1,Status);//把错误标志清楚
return;
}
if(getEmptyCount(&RxQueueEntity)!=0){ //判断队列是否有空的位置,若满了就丢弃
inQueue(&RxQueueEntity,code); //将接受到的数据放进队列
}
//printf("%c",code); //将接受到的数据直接返回打印
}
}
第一步,初始化USART1外设,把波特率固定为115200。
创建USART1.h
#ifndef __USART1_INIT_H__
#define __USART1_INIT_H__
#include "stm32f10x.h"
#include
int fputc(int ch, FILE *f);
void USART1_Configuration(void);//打印输出串口初始化
#endif
创建USART1.c
#include "USART1.h"
void USART1_Configuration(void)//打印输出串口初始化
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
//配置串口1 (USART1) 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
//配置串口1接收终端的优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
//配置串口1 发送引脚(PA.09)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//配置串口1 接收引脚 (PA.10)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//串口1工作模式(USART1 mode)配置
USART_InitStructure.USART_BaudRate = 115200;//配置波特率;
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //数据位为8个字节
USART_InitStructure.USART_StopBits = USART_StopBits_1; //一位停止位
USART_InitStructure.USART_Parity = USART_Parity_No ; //无校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //不需要流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //接收发送模式
USART_Init(USART1, &USART_InitStructure);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启中断
USART_Cmd(USART1, ENABLE);//使能串口
}
int fputc(int ch, FILE *f) //重定向c库里面的fputc到串口,那么使用printf时就能将打印的信息从串口发送出去,在PC上同串口助手接收信息
{
//将Printf内容发往串口
USART_SendData(USART1, (unsigned char) ch);
while( USART_GetFlagStatus(USART1,USART_FLAG_TC)!= SET);
return (ch);
}
第二步,找到stm32f10x_it.c,加入:
int updatareq=0;
void USART1_IRQHandler(void)
{
u16 code,Status;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
code=USART_ReceiveData(USART1);
Status = USART_GetFlagStatus(USART1, USART_FLAG_NE|USART_FLAG_PE|USART_FLAG_NE);
if(Status!=RESET){//如果发生错误接忽略接收的数据
USART_ClearFlag(USART1,Status);//把错误标志清楚
return;
}
if(code==0x7F){ //如果接受到0x7F,那么跳转到bootloader,等待升级
updatareq=1;
}
}
}
第三步,找到system_stm32f10x.h,修改中断列表的偏移地址:
#define VECT_TAB_OFFSET 0x3000
最后加入main.c:
#include "stm32f10x.h"
#include "GPIOLIKE51.h"
#include "USART1.h"
void GPIO_Configuration(void);
void Delay(uint32_t nCount);
static const unsigned int verifyflag __attribute__((at(0x800FF10)))= 0xaabbccdd;
#define updata_flagaddr 0x8002C00 //升级标志
#define verify_flagaddr 0x800FF10 //在app里面
typedef void (*iapfun)(void);
extern int updatareq;
void jump_to_boot(u32 appaddr)
{
iapfun jump2boot;
if(((*(vu32*)appaddr)&0x2FFE0000)==0x20000000)
{
printf("jump_to_boot\n");
__set_PRIMASK(1);
__set_MSP(*(vu32*)appaddr);
jump2boot=(iapfun)*(vu32*)(appaddr+4);
jump2boot();
}
}
int main(void)
{
RCC_DeInit();
SystemInit();
__set_PRIMASK(0);
GPIO_Configuration(); //运行app时,将接到PC.13引脚的lcd闪烁
USART1_Configuration();
while (1){
GPIO_SetBits(GPIOC,GPIO_Pin_13);
Delay(0xfffff);
Delay(0xfffff);
GPIO_ResetBits(GPIOC,GPIO_Pin_13);
Delay(0xfffff);
Delay(0xfffff);
printf("app runing \n");
if(updatareq){
FLASH_Unlock(); //解锁
FLASH_ErasePage(updata_flagaddr);
FLASH_ProgramWord(updata_flagaddr,0x01);
FLASH_Lock(); //上锁
FLASH_WaitForLastOperation(0xFFFFFF);//等待擦除结束
jump_to_boot(0x8000000);
}
}
}
void GPIO_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOC , ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOC, &GPIO_InitStructure);
}
void Delay(uint32_t nCount)
{
for(; nCount != 0; nCount--);
}
最后,设置输出文件的保存路径,不更改也可以,保持默认,那么等一下就到这个文件夹下找到输出文件:
生成app的bin文件:
fromelf --bin --output “$Ltest.bin” “#L”
前面已经将boot下载到芯片里面去;现在,把usb>ttl 接口接到电脑上,打开上位机,上位机就能检测到连上串口的设备,设置波特率115200,无校验位。配置完了之后,先给芯片上电(boot0=0,boot1=0正常启动),正常来说,接到PC.13的led会常亮,因为上电运行的是boot。那么点击上位机的Next进行同步,若同步成功就会跳转到下一个界面,若是同步不成功,就会卡着,鼠标光标转圈;同步失败的话检查线路是否连接正确,COM口选择是否正确。
设置flash大小,然后点next继续:
这里选择刚才编译出来的app工程bin文件,注意红圈的配置,点击Next继续:
开始app下载,下载完成后会自动跳转到app程序,这时候可以看到led闪烁。
如果成功下载了之后,芯片重新上电,首先是进入boot,检测app完整存在,那么就会自动跳转到app。
再把芯片串口接到电脑上,打开串口助手,检测通讯正常,给芯片发送7F,如图,app收到7F就会跳转到boot,等待升级;然后把串口助手关闭,打开Flash Loader Demo上位机,给芯片升级app。其实也可以不用串口助手发送7F,下载上位机同步的时候就会给芯片发送7F,只不过在操作的时候,要点击两次Next,点击第一次的时候忽略提示就行,再点击一次。
到最后,终于写完了,这个iap还有很多要完善的地方,比如加入超时机制等。希望以上能起到提供一些参考的作用。有错误或者疑问都可以提出来,谢谢。