在之前的《STM32串口IAP》一文中,通过传输数据流来升级程序,但是这种"裸"数据的传输方式存在这许多的问题,比如它没有容错机制,不能保证数据的正确传输,还比如说它无法获知升级文件的信息,导致它在判断何时停止接收数据上“犹豫不决”。正式为了解决上面的问题,才引进了YModem协议。
![STM32串口IAP(YModem) - ziye334 - ziye334的博客](http://img1.ph.126.net/7lQhuTV9rapj0SD_SYgAwA==/108086391157343377.jpg)
/************************************************************* Function : IAP_Init Description: IAP初始化函数,初始化串口1 Input : none return : none *************************************************************/ void IAP_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(COM1_RCC, ENABLE);//使能 USART2 时钟 RCC_APB2PeriphClockCmd(COM1_GPIO_RCC, ENABLE);//使能串口2引脚时钟 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//配置 USART2 的Tx 引脚类型为推挽式的 GPIO_InitStructure.GPIO_Pin = COM1_TX_PIN; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(COM1_GPIO_PORT, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//配置 USART2 的Rx 为输入悬空 GPIO_InitStructure.GPIO_Pin = COM1_RX_PIN; GPIO_Init(COM1_GPIO_PORT, &GPIO_InitStructure); USART_InitStructure.USART_BaudRate = 115200;//设置波特率为115200 USART_InitStructure.USART_WordLength = USART_WordLength_8b;//设置数据位为8位 USART_InitStructure.USART_StopBits = USART_StopBits_1;//设置停止位为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(COM1,&USART_InitStructure);//串口2相关寄存器的配置 USART_Cmd(COM1,ENABLE);//使能串口2 }
可以看到串口接收函数 IAP_SerialGetByte()仅仅是查询下是否有数据接收到,但无论是否接收到数据,程序都不会在这个函数上阻塞。所以为了实现阻塞的效果,还需要稍微做下文章,下面就提前讲讲如何在 IAP_SerialGetByte()的基础上 修改成阻塞的功能,如下获取一个用户输入键值的函数:/************************************************************* Function : IAP_SerialSendByte Description: 串口发送字节 Input : c-要发送的字节 return : none *************************************************************/ void IAP_SerialSendByte(u8 c) { USART_SendData(COM1, c); while (USART_GetFlagStatus(COM1, USART_FLAG_TXE) == RESET) {} } /************************************************************* Function : IAP_SerialSendStr Description: 串口发送字符串 Input : none return : none *************************************************************/ void IAP_SerialSendStr(u8 *s) { while(*s != '\0') { IAP_SerialSendByte(*s); s++; } } /************************************************************* Function : IAP_SerialGetByte Description: 接收一个字节数据 Input : none return : 返回结果值,0-没有接收到数据;1-接收到数据 *************************************************************/ u8 IAP_SerialGetByte(u8 *c) { if(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) != RESET) { *c = USART_ReceiveData(USART1); return 1; } return 0; }
然后再设计Bootload的界面,更《STM32串口IAP》一文给出的界面基本上一样,如下:/************************************************************* Function : IAP_GetKey Description: 获取键入值 Input : none return : 返回键值 *************************************************************/ u8 IAP_GetKey(void) { u8 data; while(!IAP_SerialGetByte(&data)){ } return data; }
界面与之前相比,唯一的区别在于多了一个FWDWLOAD的选项,提供用户从STM32上下载升级文件的功能。接下去讲讲这些功能的实现。/************************************************************* Function : IAP_ShowMenu Description: 显示菜单界面 Input : none return : none *************************************************************/ void IAP_ShowMenu(void) { IAP_SerialSendStr("\r\n+================(C) COPYRIGHT 2014 Ziye334 ================+"); IAP_SerialSendStr("\r\n| In-Application Programing Application (Version 1.0) |"); IAP_SerialSendStr("\r\n+----command----+-----------------function------------------+"); IAP_SerialSendStr("\r\n| 1: FWUPDATA | Update the firmware to flash by YModem |"); IAP_SerialSendStr("\r\n| 2: FWDWLOAD | Download the firmware from Flash by YModem|"); IAP_SerialSendStr("\r\n| 3: FWERASE | Erase the current firmware |"); IAP_SerialSendStr("\r\n| 4: BOOT | Excute the current firmware |"); IAP_SerialSendStr("\r\n| 5:REBOOT | Reboot |"); IAP_SerialSendStr("\r\n| ?: HELP | Display this help |"); IAP_SerialSendStr("\r\n+===========================================================+"); IAP_SerialSendStr("\r\n\r\n"); IAP_SerialSendStr("STM32-IAP>>"); }
上面给出的三个函数,都是跟flash相关的。第一个函数IAP_DisableFlashWPR(),它的功能是关闭flash写保护。第二个函数IAP_UpdataParam(),它的功能是更新参数,这里的参数实际上值的是文件的大小,每次收到升级文件后,就可以获得升级文件的大小,然后将文件的大小转化成32为的十六进制数,保存在指定的FLASH参数区IAP_PARAM_ADDR,这个地址在IAP.h中定义,之所以要开出这个区域,为的是方便YModem的传输,当要从芯片上下载升级的程序,可读取这块区域获取当前升级程序的大小,然后再发送指定大小的代码,这样就不用当心不知道如何结束发送了。第三个函数IAP_UpdataProgram(),将接收到的数据固化到Flash中,跟《STM2串口IAP》一文中的这个函数相比,它支持了以字为单位的flash烧写,进一步提高了效率。/************************************************************* Function : IAP_DisableFlashWPR Description: 关闭flash的写保护 Input : none return : none *************************************************************/ void IAP_DisableFlashWPR(void) { u32 blockNum = 0, UserMemoryMask = 0; blockNum = (IAP_ADDR - FLASH_BASE_ADDR) >> 12; //计算flash块 UserMemoryMask = ((u32)(~((1 << blockNum) - 1)));//计算掩码 if((FLASH_GetWriteProtectionOptionByte() & UserMemoryMask) != UserMemoryMask)//查看块所在区域是否写保护 { FLASH_EraseOptionBytes ();//擦除选择位 } } s8 IAP_UpdataParam(s32 *param) { u32 i; u32 flashptr = IAP_PARAM_ADDR; FLASH_Unlock();//flash上锁 FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);//清除flash相关标志位 for(i = 0; i < IAP_PARAM_SIZE; i++) { FLASH_ProgramWord(flashptr + 4 * i, *param); if(*(u32 *)(flashptr + 4 * i) != *param) { return -1; } param++; } FLASH_Lock();//flash解锁 return 0; } /************************************************************* Function : IAP_UpdataProgram Description: 升级程序 Input : addr-烧写的地址 size-大小 return : 0-OK 1-error *************************************************************/ s8 IAP_UpdataProgram(u32 addr, u32 size) { u32 i; static u32 flashptr = IAP_ADDR; FLASH_Unlock();//flash上锁 FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);//清除flash相关标志位 for(i = 0; i < size; i += 4) { FLASH_ProgramWord(flashptr, *(u32 *)addr);//烧写1个字 if(*(u32 *)flashptr != *(u32 *)addr)//判断是否烧写成功 { return -1; } flashptr += 4; addr += 4; } FLASH_Lock();//flash解锁 return 0; }
可以看到,它与《STM32串口IAP》中擦除程序相比,多一个参数size,即允许擦写指定大小的区域。这样做的好处是,当知道了升级文件文件的大小后,可以擦除升级文件大小相对应的页数,而不像之前那样,擦除升级代码起始位置后面所有的flash空间数据,这样就显著提高了效率。/************************************************************* Function : IAP_FlashEease Description: 擦除Flash Input : size-擦除的大小 return : none *************************************************************/ void IAP_FlashEease(u32 size) { u16 eraseCounter = 0; u16 nbrOfPage = 0; FLASH_Status FLASHStatus = FLASH_COMPLETE; if(size % PAGE_SIZE != 0)//计算需要擦写的页数 { nbrOfPage = size / PAGE_SIZE + 1; } else { nbrOfPage = size / PAGE_SIZE; } FLASH_Unlock();//解除flash擦写锁定 FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);//清除flash相关标志位 for(eraseCounter = 0; (eraseCounter < nbrOfPage) && ((FLASHStatus == FLASH_COMPLETE)); eraseCounter++)//开始擦除 { FLASHStatus = FLASH_ErasePage(IAP_ADDR + (eraseCounter * PAGE_SIZE));//擦除 IAP_SerialSendStr(".");//打印'.'以显示进度 } FLASH_ErasePage(IAP_PARAM_ADDR);//擦除参数所在的flash页 FLASH_Lock();//flash擦写锁定 }
这段代码的工作原理在《STM32串口IAP》一文已经详细分析过了,这里就不在赘述了。typedef void (*pFunction)(void); pFunction Jump_To_Application;
/************************************************************* Function : IAP_JumpToApplication Description: 跳转到升级程序处 Input : none return : none *************************************************************/ void IAP_JumpToApplication(void) { u32 JumpAddress;//跳转地址 if(((*(__IO u32 *)IAP_ADDR) & 0x2FFE0000) == 0x20000000)//有升级代码,IAP_ADDR地址处理应指向主堆栈区,即0x20000000 { JumpAddress = *(__IO u32 *)(IAP_ADDR + 4);//获取复位地址 Jump_To_Application = (pFunction)JumpAddress;//函数指针指向复位地址 __set_MSP(*(__IO u32*)IAP_ADDR);//设置主堆栈指针MSP指向升级机制IAP_ADDR Jump_To_Application();//跳转到升级代码处 } }
IAP.c的最后一个函数是IAP_WiatForChoose(),实现一个与用户交互的一个过程,代码如下:/************************************************************* Function : ShwHelpInfo Description: 显示帮助信息 Input : none return : none *************************************************************/ static void ShwHelpInfo(void) { IAP_SerialSendStr("\r\nEnter '1' to updtate you apllication code!"); IAP_SerialSendStr("\r\nRnter '2' to download the firmware from interal flash!"); IAP_SerialSendStr("\r\nRnter '3' to erase the current application code!"); IAP_SerialSendStr("\r\nEnter '4' to go to excute the current application code!"); IAP_SerialSendStr("\r\nEnter '5' to restart the system!"); IAP_SerialSendStr("\r\nEnter '?' to show the help infomation!\r\n"); }
具体的实现代码在《STM32串口IAP》已经介绍过了,自己对比理解吧。但需要补充说明的是:上面的DownloadFirmware()和UploadFirmware()分别Download.c与Upload.c文件中实现,后面马上就会讲到。/************************************************************* Function : IAP_WiatForChoose Description: 功能选择 Input : none return : none *************************************************************/ void IAP_WiatForChoose(void) { u8 c = 0; while (1) { c = IAP_GetKey();//获取键值 IAP_SerialSendByte(c);//串口返回键值 switch(c) { case '1'://FWUPDATA固件升级 if(IAP_GetKey() == '\r')//检测回车键 { IAP_DisableFlashWPR();//关闭写保护 DownloadFirmware();//开始升级 Delay_ms(500); NVIC_SystemReset();//重启 } break; case '2'://FWDWLOAD上传当前固件 if(IAP_GetKey() == '\r') //获取键值 { UploadFirmware();//开始上传 return;//退出循环 } break; case '3'://FWERASE固件擦除 if(IAP_GetKey() == '\r')//检测回车键 { IAP_SerialSendStr("\r\nErasing..."); IAP_FlashEease(FLASH_SIZE + FLASH_BASE_ADDR - IAP_ADDR);//擦除Flash,升级处后面的flash空间 IAP_SerialSendStr("\r\nErase done!\r\n"); return;//退出循环 } break; case '4'://BOOT执行升级程序 if(IAP_GetKey() == '\r')//获取键值 { IAP_SerialSendStr("\r\nBotting...\r\n"); if(((*(__IO u32 *)IAP_ADDR) & 0x2FFE0000) != 0x20000000)//判断是否有应用程序 { IAP_SerialSendStr("No user program! Please download a firmware!\r\n"); } Delay_ms(500); NVIC_SystemReset(); return;//退出循环 } break; case '5'://REBOOT系统重启 if(IAP_GetKey() == '\r')//检测回车键 { IAP_SerialSendStr("\r\nRebooting...\r\n"); return;//退出循环 } break; case '?'://HELP帮助 if(IAP_GetKey() == '\r') { ShwHelpInfo();//显示帮助信息 return;//退出循环 } break; default: IAP_SerialSendStr("\r\nInvalid Number! The number should be either 1、2、3、4or5\r\n"); return;//退出循环 } } }
IAP_ADDR是目标升级地址;IAP_PARAM_ADDR是保存参数的地址,它的地址在FLash空间最后一页的起始地址处。#ifndef __IAP_H__ #define __IAP_H__ #include "stm32f10x.h" #define FLASH_BASE_ADDR 0x8000000 //Flash基地址 #define IAP_ADDR 0x8005000 //升级代码地址 #define IAP_PARAM_SIZE 1 #define IAP_PARAM_ADDR (FLASH_BASE_ADDR + FLASH_SIZE - PAGE_SIZE) //Flash空间最后1页地址开始处存放参数 #if defined (STM32F10X_MD) || defined (STM32F10X_MD_VL) #define PAGE_SIZE (0x400) //页的大小1K #define FLASH_SIZE (0x20000) //Flash空间128K #elif defined STM32F10X_CL #define PAGE_SIZE (0x800) //页的大小2K #define FLASH_SIZE (0x40000) //Flash空间256K #elif defined STM32F10X_HD || defined (STM32F10X_HD_VL) #define PAGE_SIZE (0x800) //页的大小2K #define FLASH_SIZE (0x80000) //Flash空间512K #elif defined STM32F10X_XL #define PAGE_SIZE (0x800) //页的大小2K #define FLASH_SIZE (0x100000) //Flash空间1M #else #error "Please select first the STM32 device to be used (in stm32f10x.h)" #endif void IAP_Init(void); void IAP_SerialSendByte(u8 c); void IAP_SerialSendStr(u8 *s); u8 IAP_SerialGetByte(u8 *c); u8 IAP_GetKey(void); s8 IAP_UpdataParam(s32 *param); s8 IAP_UpdataProgram(u32 addr, u32 size); void IAP_FlashEease(u32 size); void IAP_ShowMenu(void); void IAP_WiatForChoose(void); void IAP_JumpToApplication(void); #endif
首先需要定义一个大小为1024字节的数组tab_1024[1024]用来暂存接收到升级数据。接着在 DownloadFirmware()它调用 YModem_Receive()函数来完成对升级文件的接胡搜与固化工作,当讲到YModem.c的时候才会讲到它,这里暂不多讲。 DownloadFirmware()这个函数会根据 YModem_Receive()返回值判断是否正确接收,并根据结构打印相应的消息:返回值大于0,说明正确接收,则打印收到的升级文件的文件名与文件大小;如果返回值等于-1,则说明升级文件的大小太大,没有足够的flash空间可以容纳它;如果返回值等于-2,则说明在接收到数据,但没有烧写成功,如果出现这部分错误,就要检查flash操作相关的代码了;如果返回值为-3,则说明发送端中止了升级文件的传输;其他返回值就表示其他未知原因的错误了。上面的代码中的延时1s放在这里是有目的的,为的是等待YModem完全端来连接,在打印消息,如果没有延时,则串口软件端是不不会打印后面的消息的。extern u8 file_name[FILE_NAME_LENGTH]; u8 tab_1024[1024] = {0}; /************************************************************* Function : DownloadFirmware Description: 下载升级固件 Input : none return : none *************************************************************/ void DownloadFirmware(void) { u8 number[10]= " "; //文件的大小字符串 s32 size = 0; IAP_SerialSendStr("\r\nWaiting for the file to be send...(press 'a' or 'A' to abort)\r\n"); size = YModem_Receive(&tab_1024[0]);//开始接收升级程序 Delay_ms(1000);//延时1s,让secureCRT有足够时间关闭ymodem对话,而不影响下面的信息打印 if(size > 0) { IAP_SerialSendStr("+-----------------------------------+\r\n"); IAP_SerialSendStr("Proramming completed successfully!\r\nName: "); IAP_SerialSendStr(file_name);//显示文件名 YModem_Int2Str(number, size); IAP_SerialSendStr("\r\nSize:"); IAP_SerialSendStr(number);//显示文件大小 IAP_SerialSendStr("Bytes\r\n"); IAP_SerialSendStr("+-----------------------------------+\r\n"); } else if(size == -1)//固件的大小超出处理器的flash空间 { IAP_SerialSendStr("The image size is higher than the allowed space memory!\r\n"); } else if(size == -2)//程序烧写不成功 { IAP_SerialSendStr("Verification failed!\r\n"); } else if(size == -3)//用户终止 { IAP_SerialSendStr("Aborted by user!\r\n"); } else //其他错误 { IAP_SerialSendStr("Failed to receive the file!\r\n"); } }
#ifndef __DOWNLOAD_H__ #define __DOWNLOAD_H__ #include "Download.h" void DownloadFirmware(void); #endif
在代码的组开始出,给出了一个判断语句: if(IAP_GetKey() == CRC16),即判断等待接收到字符'C',在《YModem协议简介》中曾经提过,接收端会最先发送一个字符'C'想发送端寻求数据,所以当开始上传时,需要等到接收到这个字符'C',如果在这期间,收到用户的按键输入,则终止传输。 UploadFirmware()函数也是调用YModem.c文件中的 YModem_Transmit()函数发送数据,这个函数的参数包括固件的读取地址、固件的文件名、固件的大小。这里的固件读取地址就是我们升级地址处IAP_ADDR,文件名我统一定为Firmware.bin文件,固件的大小则需要到之前烧尽flash参数区IAP_PARAM_ADDR区域去读取了。根据YModem_Transmit()函数返回的参数可以判断是否传输成功,如果返回值等于0,则说明传输成功,否则传输错误。/************************************************************* Function : UploadFirmware Description: 向上位机上传固件 Input : none return : none *************************************************************/ void UploadFirmware(void) { u32 status = 0; u32 imageSize = 0; IAP_SerialSendStr("\r\nBeginning to receive file...(press any key to abort)\r\n"); if(IAP_GetKey() == CRC16)//收到字符'C',便是ymodem询问数据 { imageSize = *(u32 *)IAP_PARAM_ADDR;//向参数IAP_PARAM_ADDR地址处读取固件的大小 status = YModem_Transmit((u8 *)IAP_ADDR, (u8 *)"Firmware.bin", imageSize); if(status != 0) //接收错误 { IAP_SerialSendStr("\r\nError Occured while transmitting file!\r\n"); } else//接收正确 { IAP_SerialSendStr("\r\nFile Transmitted successfully!\r\n"); } } else//终止接收 { IAP_SerialSendStr("\r\nAbort by user!\r\n"); } }
#ifndef __UPLOAD_H__ #define __UPLOAD_H__ #include "stm32f10x.h" void UploadFirmware(void); #endif
这个函数功能本来应该是可以用C语言的库函数中itoa()来说实现的(包含stdlib.h头文件),但是对于编译器的这个stdlib头文件没有能实现这个函数,所以只能字节编写上面这个转换函数。/************************************************************* Function : Int2Str Description: 整型转化成字符串 Input : str-字符串指针 intnum-转换值 return : none *************************************************************/ void YModem_Int2Str(u8* str, s32 intnum) { u32 i, Div = 1000000000, j = 0, Status = 0; for (i = 0; i < 10; i++) { str[j++] = (intnum / Div) + '0';//数字转化成字符 intnum = intnum % Div; Div /= 10; if ((str[j-1] == '0') & (Status == 0))//忽略最前面的'0' { j = 0; } else { Status++; } } }
接下去就可以编写接收一个数据包的函数,代码如下:/************************************************************* Function : YModem_RecvByte Description: ymodem接收一个字节 Input : c-存放接收到的字节 timeout-超时时间 return : none *************************************************************/ static s8 YModem_RecvByte(u8 *c, u32 timeout) { while(timeout-- > 0) { if(IAP_SerialGetByte(c) == 1) { return 0; } } return -1; } /************************************************************* Function : YModem_SendByte Description: 发送一个字节 Input : c-要发送的字节 return : none *************************************************************/ static void YModem_SendByte(u8 c) { IAP_SerialSendByte(c); }
首先值得一看的是这个函数的参数:u8 *data, s32 *length,对于第一个是指针结构的data,当让是用来保存收到的数据的啦,但是这里的length也使用指针让人难以理解?实际上length设计成指针跟data的效果一样,是为了保存长度值,如果不使用指针,就相当一个局部变量,函数执行完后,就失去了数值,使用指针就是为了保存函数执行到最后的数值。在这个函数中,先接收一个字节,在判断这个字节的信息,这里的情况也分很多种:SOH、STX、EOT、CA、ABORT1、ABORT2。当接收到SOH信号,则表示后面接收到的是128字节的数据包;当收到STX信号,则表示后面接收到的1024字节的数据包;当收到CA,表示中止传输,则直接退出这个函数;当收到的/************************************************************* Function : YModem_RecvPacket Description: 接收一个数据包 Input : data-存放接收到的数据 length-数据包的长度 timeout-超时时间 return : 0 -正常接收 -1 -接收错误 *************************************************************/ s8 YModem_RecvPacket(u8 *data, s32 *length, u32 timeout) { u16 i, packet_size; u8 c; *length = 0; if(YModem_RecvByte(&c, timeout) != 0)//接收数据包的第一个字节 { return -1; } switch(c) { case SOH: //128字节数据包 packet_size = PACKET_SIZE; /记录数据包的长度 break; case STX: //1024字节数据包 packet_size = PACKET_1K_SIZE; //记录数据包的长度 break; case EOT: //数据接收结束字符 return 0; //接收结束 case CA: //接收中止标志 if((YModem_RecvByte(&c, timeout) == 0) && (c == CA))//等待接收中止字符 { *length = -1; //接收到中止字符 return 0; } else //接收超时 { return -1; } case ABORT1: //用户终止,用户按下'A' case ABORT2: //用户终止,用户按下'a' return 1; //接收终止 default: return -1; //接收错误 } *data = c; //保存第一个字节 for(i = 1; i < (packet_size + PACKET_OVERHEAD); i++)//接收数据 { if(YModem_RecvByte(data + i, timeout) != 0) { return -1; } } if(data[PACKET_SEQNO_INDEX] != ((data[PACKET_SEQNO_COMP_INDEX] ^ 0xff) & 0xff)) { return -1; //接收错误 } *length = packet_size; //保存接收到的数据长度 return 0; //正常接收 }
哎,由于网页的code阅读器的问题,导致上面的代码度起来很别扭,千万体谅,你可以将这段代码复制到自己电脑里的C编译器中阅读。这段代码的最开始就有两个死循环,外面一层死循环是一个数据传输的连接的循环,传输链路断开之前,会一直执行这个循环;内一层的死循环是数据接收的循环,数据接收完成之前,会一直执行这个循环。在内循环中,调用YModem_RecvPacket()接收一个数据包,在根据这个函数的返回值进行判断。具体不细讲,否者不知又要打多少字了,读者自己联系上文理解,我只粗略的讲述下。每次接收一个数据包,如果接收的是第一个数据包,即包含了升级文件的信息的那个数据帧,则升级文件的名字、文件大小数据部分拷贝出来,文件大小的数据段部分需要先调用atoi()函数将字符串转换成整型值,然后将这个表示升级文件大小的整型值固化到flash参数存储区IAP_PARAM_ADDR。接收完一帧数据帧后,则发送'C'字符据需所要剩下的数据。后面每收到一帧数据就发送ACK响应字符,然后将数据固化到STM32的flash升级去,同时计算剩余的数据。知道接收到EOT结束传输字符,才结束结束接收,然后分两次跳出内外循环。最后 返回 接收到的文件大小(整型值)。/************************************************************* Function : YModem_Receive Description: ymodem接收 Input : buf-存放接收到的数据 return : 0 -发送端传输中止 -1 -固件过大 -2 -flash烧写错误 -3 -用户终止 *************************************************************/ s32 YModem_Receive(u8 *buf) { u8 packet_data[PACKET_1K_SIZE + PACKET_OVERHEAD], file_size[FILE_SIZE_LENGTH]; u8 session_done, file_done, session_begin, packets_received, errors; u8 *file_ptr, *buf_ptr; s32 packet_length = 0, size = 0; u32 i = 0,RamSource = 0; for (session_done = 0, errors = 0, session_begin = 0; ;)//死循环,一个ymodem连接 { for (packets_received = 0, file_done = 0, buf_ptr = buf; ; )//死循环,不断接收数据 { switch(YModem_RecvPacket(packet_data, &packet_length, NAK_TIMEOUT))//接收数据包 { case 0: errors = 0; switch(packet_length) { case -1: //发送端中止传输 YModem_SendByte(ACK);//回复ACK return 0; case 0: //接收结束或接收错误 YModem_SendByte(ACK); file_done = 1;//接收完成 break; default: //接收数据中 if((packet_data[PACKET_SEQNO_INDEX] & 0xff) != (packets_received & 0xff)) { YModem_SendByte(NAK);//接收错误的数据,回复NAK } else//接收到正确的数据 { if(packets_received == 0)//接收第一帧数据 { if(packet_data[PACKET_HEADER] != 0)//包含文件信息:文件名,文件长度等 { for(i = 0, file_ptr = packet_data + PACKET_HEADER; (*file_ptr != 0) && (i < FILE_NAME_LENGTH); ) { file_name[i++] = *file_ptr++;//保存文件名 } file_name[i++] = '\0';//文件名以'\0'结束 for(i = 0, file_ptr++; (*file_ptr != ' ') && (i < FILE_SIZE_LENGTH); ) { file_size[i++] = *file_ptr++;//保存文件大小 } file_size[i++] = '\0';//文件大小以'\0'结束 size = atoi((const char *)file_size);//将文件大小字符串转换成整型 if(size > (FLASH_SIZE -1))//升级固件过大 { YModem_SendByte(CA); YModem_SendByte(CA);//连续发送2次中止符CA return -1;//返回 } IAP_FlashEease(size);//擦除相应的flash空间 IAP_UpdataParam(&size);//将size大小烧写进Flash中Parameter区 YModem_SendByte(ACK);//回复ACk YModem_SendByte(CRC16);//发送'C',询问数据 } else//文件名数据包为空,结束传输 { YModem_SendByte(ACK);//回复ACK file_done = 1;/停止接收 session_done = 1;//结束对话 break; } } else //收到数据包 { memcpy(buf_ptr, packet_data + PACKET_HEADER, packet_length);//拷贝数据 RamSource = (u32)buf;//8位强制转化成32为数据 if(IAP_UpdataProgram(RamSource, packet_length) != 0) //烧写升级数据 { YModem_SendByte(CA); YModem_SendByte(CA);//flash烧写错误,连续发送2次中止符CA return -2;//烧写错误 } YModem_SendByte(ACK);//flash烧写成功,回复ACK } packets_received++;//收到数据包的个数 session_begin = 1;//设置接收中标志 } } break; case 1: //用户中止 YModem_SendByte(CA); YModem_SendByte(CA); //连续发送2次中止符CA return -3; //烧写中止 default: if(session_begin > 0) //传输过程中发生错误 { errors++; } if(errors > MAX_ERRORS) //错误超过上限 { YModem_SendByte(CA); YModem_SendByte(CA);//连续发送2次中止符CA return 0; //传输过程发生过多错误 } YModem_SendByte(CRC16); //发送'C',继续接收 break; } if(file_done != 0)//文件接收完毕,退出循环 { break; } } if(session_done != 0)//对话结束,跳出循环 { break; } } return (s32)size;//返回接收到文件的大小 }
参照YModem协议的起始帧数据格式,首先首部为SOH,表示后面接着的是128字节的数据段,接下去的两个位是帧序以及帧序的取反,对于起始帧,帧序自然是0x00,它的取反则为0xFF。这之后,开始拷贝文件名的到数据段中,接着在将要传输文件的大小的数值转换成字符串,再拷贝进文件名后面的数据段中。其他剩余的数据段统一用0x00填充。这样就准备好了第一帧数据帧。/************************************************************* Function : YModem_PrepareFirstPacket Description: 准备第一个数据包,包括文件名与大小 Input : data-要发送的数据包 fileName-文件名 length-文件的大小 return : none *************************************************************/ void YModem_PrepareFirstPacket(u8 *data, const u8 *fileName, u32 *length) { u16 i, j; u8 file_size[10]; data[0] = SOH; //128字节数据包 data[1] = 0x00; //第一个数据包 data[2] = 0xFF; //data[2] = ~data[1] for(i = 0; (fileName[i] != '\0') && (i < FILE_NAME_LENGTH); i++) { data[i + PACKET_HEADER] = fileName[i];//拷贝文件名 } data[i + PACKET_HEADER] = '\0';//文件名以'\0'结束 YModem_Int2Str (file_size, *length);//将文件长度转化成字符串 for (j =0, i = i + PACKET_HEADER + 1; file_size[j] != '\0' ; ) { data[i++] = file_size[j++];//拷贝文件长度 } for(j = i; j < PACKET_SIZE + PACKET_HEADER; j++) { data[j] = 0; //0填充 } }
数据帧的数据结构则有两种形式,一种是以STX开头的后面跟着1024字节数据的数据帧,另外一种是以SOH开头的后面跟128字节数据的数据帧。如何判断判断使用何种数据帧?当然是根据剩余数据的长度了,但剩余数据的长度大于128字节的时候,则使用STX开头的数据帧,否者使用SOH开头的数据帧。数据的第二字节与第三字节仍然分别是数据帧的帧序以及帧序的取反。接下去在根据是SOH开头还是STX开头分别拷贝128字节数据和1024字节数据,如果数据无法填充整个数据段,则用0x1A来填充。/************************************************************* Function : YModem_PrepareDataPacket Description: 准备数据包 Input : sourceBuf-要发送的数据 data-要发送的数据包 ptkNo-数据包的序号 sizeBlk-要发送的数据长度 return : none *************************************************************/ void YModem_PrepareDataPacket(u8 *sourceBuf, u8 *data, u8 pktNo, u32 sizeBlk) { u16 i, size, packetSize; u8 *file_ptr; packetSize = sizeBlk >= PACKET_1K_SIZE ? PACKET_1K_SIZE : PACKET_SIZE;//决定发送128字节数据包还是发送1024字节数据包 size = sizeBlk < packetSize ? sizeBlk : packetSize; //计算数据包中数据的长度 if(packetSize == PACKET_1K_SIZE)//1K字节数据 { data[0] = STX; //设置数据包首部STX,1024字节数据包 } else //128字节数据 { data[0] = SOH; //设置数据包首部SOH,128字节数据 } data[1] = pktNo; //数据包序号 data[2] = (~pktNo); file_ptr = sourceBuf; //指向需要发送的数据 for(i = PACKET_HEADER; i < size + PACKET_HEADER; i++) { data[i] = *file_ptr++; //拷贝要发送的数据 } if(size <= packetSize) //数据长度小于128字节 { for(i = size + PACKET_HEADER; i < packetSize + PACKET_HEADER; i++) { data[i] = 0x1A; //数据不够,以0x1A填充 } } }
结束帧的数据的第一个字节仍然以SOH开头,规定它的帧序跟起始帧一样为0x00,帧序的取反当然是0xFF,但是它后面的128字节数据段不存放任何信息,以0x00填充。/************************************************************* Function : YModem_PrepareLastPacket Description: 准备最后一个数据包 Input : data-要发送搞得数据包 return : none *************************************************************/ void YModem_PrepareLastPacket(u8 *data) { u8 i = 0; data[0] = SOH; //128字节数据包 data[1] = 0; //序号 data[2] = 0xFF; for(i = PACKET_HEADER; i < (PACKET_SIZE + PACKET_HEADER); i++) { data[i] = 0x00;//数据以0填充,即空数据包 } }
接下去需要编写以发送数据包的函数YModem_SendPacket(),代码如下:/************************************************************* Function : UpdateCRC16 Description: 计算一个字节的CRC16校验码(CRC16-CCITT欧洲标准) Input : crcIn-上一次的CRC码 byte-一个字节 return : 返回crc码 *************************************************************/ u16 UpdateCRC16(uint16_t crcIn, uint8_t byte) { uint32_t crc = crcIn; uint32_t in = byte|0x100; do { crc <<= 1; in <<= 1; if (in&0x100) ++crc; //crc |= 0x01 if (crc&0x10000) crc ^= 0x1021; } while (!(in&0x10000)); return crc&0xffffu; } /************************************************************* Function : Cal_CRC16 Description: 计算数据的CRC码 Input : data-要计算的数据 size-数据的大小 return : 返回计算出的CRC码 *************************************************************/ u16 Cal_CRC16(const uint8_t* data, uint32_t size) { uint32_t crc = 0; const uint8_t* dataEnd = data+size; while (data<dataEnd) crc = UpdateCRC16(crc,*data++); crc = UpdateCRC16(crc,0); crc = UpdateCRC16(crc,0); return crc&0xffffu; }
有了发送数据包的函数,下面就可以开始编写上面准备的三种数据帧。/************************************************************* Function : YModem_SendPacket Description: 发送数据包 Input : data-要发送的数据 length-发送的数据长度 return : none *************************************************************/ void YModem_SendPacket(u8 *data, u16 length) { u16 i = 0; while(i < length) { YModem_SendByte(data[i]); i++; } }
要发送这一帧数据,需要先将文件名以文件大小通过调用YModem_PrepareFirstPacket()函数拷贝到之前准备好的起始帧的数据段中。然后就可以将这个准备好的起始帧发送出去了,接下去在计算CRC检验码,然后再以高字节在前低字节在后的顺序发送出去。这样就完成了一个完整的数据帧的发送。当发送完毕后,等待先后接收到接收端返回的ACK和C信号,如果这两个字节全部接收到,表示接受端接收成功。如果没有接收成功,errors就会自增,错误的次数大于10次时,则返回-1,提示错误。/************************************************************* Function : YModem_TransmitFirstPacket Description: 传输第一个数据包 Input : sendFileName-文件名 fileSize-文件的大小 return : 0-successed -1 -failed *************************************************************/ s8 YModem_TransmitFirstPacket(u8 *sendFileName, u32 fileSize) { u8 i, ackReceived = 0, errors = 0, receiveChar = 0; u8 firstPacket[PACKET_SIZE + PACKET_HEADER]; u8 fileName[FILE_NAME_LENGTH]; u16 tempCRC = 0; for(i = 0; i < (FILE_NAME_LENGTH - 1); i++) { fileName[i] = sendFileName[i]; //拷贝文件名 } YModem_PrepareFirstPacket(firstPacket, fileName, &fileSize);//准备第一个数据包 do { YModem_SendPacket(firstPacket, PACKET_SIZE + PACKET_HEADER);//发送第一个数据包 tempCRC = Cal_CRC16(&firstPacket[3], PACKET_SIZE);//计算校验码 YModem_SendByte(tempCRC >> 8); //发送CRC高位 YModem_SendByte(tempCRC & 0xFF); //发送CRC低位 if(((YModem_RecvByte(&receiveChar, NAK_TIMEOUT) == 0) && (receiveChar == ACK)) && ((YModem_RecvByte(&receiveChar, NAK_TIMEOUT) == 0) && (receiveChar == CRC16))) { ackReceived = 1; //先后收到ACK与C标志,才表示接收方接收成功 } else { errors++; } }while(!ackReceived && (errors < 0x0A)); if(errors >= 0x0A) { return -1; } return 0; }
这个函数的绝大部分被while(fileSize)包围了起来,它的作用是一直发送,直到数据发送完毕。先根据文件的剩余大小调用 YModem_PrepareDataPacket()函数准备数据帧的数据结构,然后开始发送,接着再发送CRC检验码,这样就完成了一个完整数据帧的发送。发送完毕后,等待接收到ACK回应信号,如果接收到ACK信号,则开始准备下一帧数据,否则errors自增,当错误达到上限,直接返回退出。/************************************************************* Function : YModem_TransmitDataPacket Description: 开始传输数据包 Input : buf-要传输的数据 fileSize-要发送文件的大小 return : 0-successed -1 -failed *************************************************************/ s8 YModem_TransmitDataPacket(u8 *buf, u32 size) { u8 *buf_ptr = buf; u8 blkNumber = 0x01; u8 ackReceived, errors, receiveChar; u8 packet_data[PACKET_1K_SIZE + PACKET_OVERHEAD]; u16 tempCRC = 0; u32 pktSize = 0; u32 fileSize = size; while(fileSize) //数据没有发送完 { ackReceived = 0; receiveChar = 0; errors = 0; YModem_PrepareDataPacket(buf_ptr, packet_data, blkNumber, fileSize);//准备数据包 do { if(fileSize >= PACKET_1K_SIZE) //1024字节数据包 { pktSize = PACKET_1K_SIZE; } else //128字节数据包 { pktSize = PACKET_SIZE; } YModem_SendPacket(packet_data, pktSize + PACKET_HEADER);//发送数据包 tempCRC = Cal_CRC16(&packet_data[3], pktSize);//计算校验码 YModem_SendByte(tempCRC >> 8); //发送CRC高位 YModem_SendByte(tempCRC & 0xFF); //发送CRC低位 if((YModem_RecvByte(&receiveChar, NAK_TIMEOUT) == 0) && (receiveChar == ACK)) { ackReceived = 1;//收到ACK if(fileSize > pktSize) { buf_ptr += pktSize;//偏移要发送数据的位置 fileSize -= pktSize;//计算剩余的数据 if(blkNumber == (size/1024 + 3))//数据包是否还需发送 { return -1; } else { blkNumber++; } } else { buf_ptr += pktSize; fileSize = 0; } } else { errors++; } }while (!ackReceived && (errors < 0x0A)); if(errors >= 0x0A) { return -1; } } return 0; }
结束帧的发送跟起始帧的过程差不多,这里就不赘述了。/************************************************************* Function : YModem_TransmitLastPacket Description: 传输最后一个数据包 Input : none return : 0-successed -1 -failed *************************************************************/ s8 YModem_TransmitLastPacket(void) { u8 ackReceived = 0, receiveChar = 0, errors = 0; u8 lastPacket[PACKET_SIZE + PACKET_OVERHEAD]; u16 tempCRC = 0; YModem_PrepareLastPacket(lastPacket); do { YModem_SendPacket(lastPacket, PACKET_SIZE + PACKET_HEADER);//发送数据包 tempCRC = Cal_CRC16(&lastPacket[3], PACKET_SIZE);//计算CRC检验位 YModem_SendByte(tempCRC >> 8); //发送CRC高位 YModem_SendByte(tempCRC & 0xFF);//发送CRC低位 if((YModem_RecvByte(&receiveChar, NAK_TIMEOUT) == 0) && (receiveChar == ACK)) { ackReceived = 1; //收到ACK } else { errors++; } }while(!ackReceived && (errors < 0x0A)); if(errors >= 0x0A) { return -1; } return 0; }
要注意的是第一个EOT信号发送后,需要等待接收NAK信号。如果接收到这个信号,则表示接收端已经明白了之后不再数据包了。/************************************************************* Function : YModem_TransmitFirstEOT Description: 发送第一个EOT结束标志 Input : none return : 0-successed -1 -failed *************************************************************/ s8 YModem_TransmitFirstEOT(void) { u8 ackReceived = 0, receiveChar = 0, errors = 0; do { YModem_SendByte(EOT);//发送结束标志 if((YModem_RecvByte(&receiveChar, NAK_TIMEOUT) == 0) && (receiveChar == NAK)) { ackReceived = 1;//收到ACK, 表示对方收到EOT标志 } else { errors++; } }while(!ackReceived && (errors < 0x0A)); if(errors >= 0x0A) { return -1; } return 0; }
第二个EOT发送出去后,如果正常的话应该会先后接收到ACK信号与C信号。ACK信号用来回应EOT信号,而C信号则向发送端询问结束帧数据。/************************************************************* Function : YModem_TransmitSecondEOT Description: 发送第二个EOT结束标志 Input : return : 0-successed -1 -failed *************************************************************/ s8 YModem_TransmitSecondEOT(void) { u8 ackReceived = 0, receiveChar = 0, errors = 0; do { YModem_SendByte(EOT); //发送结束标志 if(((YModem_RecvByte(&receiveChar, NAK_TIMEOUT) == 0) && (receiveChar == ACK)) && ((YModem_RecvByte(&receiveChar, NAK_TIMEOUT) == 0) && (receiveChar == CRC16))) { ackReceived = 1;//分别收到ACK与C标志,才表示对方同意结束 } else { errors++; } }while(!ackReceived && (errors < 0x0A)); if(errors >= 0x0A) { return -1; } return 0; }
这里千万要注意输出的顺序,尤其是发送两个EOT信号后,在发送结束帧数据。/************************************************************* Function : YModem_Transmit Description: ymodem传输文件 Input : buf-要传输的数据 sendFileName-传传输的文件名 filesize-传输的数据大小 return : 0-successed -1 -failed *************************************************************/ s8 YModem_Transmit(u8 *buf, u8 *sendFileName, u32 fileSize) { s8 result = 0; result = YModem_TransmitFirstPacket(sendFileName, fileSize);//发送一个数据包 if(result != 0) return result; result = YModem_TransmitDataPacket(buf, fileSize);//发送文件数据 if(result != 0) return result; result = YModem_TransmitFirstEOT();//发送第一个结束标志 if(result != 0) return result; result = YModem_TransmitSecondEOT();//发送第二个结束标志 if(result != 0) return result; result = YModem_TransmitLastPacket();//发送最后一个数据包 if(result != 0) return result; return 0;; }
#ifndef __YMODEM_H__ #define __YMODEM_H__ #include "stm32f10x.h" #define PACKET_SEQNO_INDEX (1) //数据包序号 #define PACKET_SEQNO_COMP_INDEX (2) //包序取反 #define PACKET_HEADER (3) //首部3位 #define PACKET_TRAILER (2) //CRC检验的2位 #define PACKET_OVERHEAD (PACKET_HEADER + PACKET_TRAILER)//3位首部+2位CRC #define PACKET_SIZE (128) //128字节 #define PACKET_1K_SIZE (1024) //1024字节 #define FILE_NAME_LENGTH (256) //文件最大长度 #define FILE_SIZE_LENGTH (16) //文件大小 #define SOH (0x01) //128字节数据包开始 #define STX (0x02) //1024字节的数据包开始 #define EOT (0x04) //结束传输 #define ACK (0x06) //回应 #define NAK (0x15) //没回应 #define CA (0x18) //这两个相继中止转移 #define CRC16 (0x43) //'C'即0x43, 需要 16-bit CRC #define ABORT1 (0x41) //'A'即0x41, 用户终止 #define ABORT2 (0x61) //'a'即0x61, 用户终止 #define NAK_TIMEOUT (0x100000)//最大超时时间 #define MAX_ERRORS (5) //错误上限 void YModem_Int2Str(uint8_t* str, int32_t intnum); s32 YModem_Receive(u8 *buf); s8 YModem_Transmit(u8 *buf, u8 *sendFileName, u32 fileSize); #endif
/************************************************************* Function : KeyInit Description: 初始化按键 Input : none return : none *************************************************************/ void KeyInit (void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA , &GPIO_InitStructure); } /************************************************************* Function : GetKey Description: 获取按键状态 Input : none return : none *************************************************************/ u8 GetKey (void) { return (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_8)); } /************************************************************* Function : main Description: main入口 Input : none return : none *************************************************************/ int main(void) { BSP_Init(); //板子初始化 KeyInit(); //初始化按键 if(!GetKey ()) //按键按下,进入升级界面 { set: IAP_Init(); //初始化串口 shw: IAP_ShowMenu(); //显示功能菜单 IAP_WiatForChoose(); //等待选择界面 goto shw; //重新显示界面 } else { IAP_JumpToApplication();//跳转到升级出代码执行 goto set;//没有升级程序或者升级程序错误才会执行到这句,然后天转到升级界面 } }
![STM32串口IAP(YModem) - ziye334 - ziye334的博客](http://img2.ph.126.net/mjCUvrKFQ9gf4gJud8HVAA==/6619491007769845934.jpg)
![STM32串口IAP(YModem) - ziye334 - ziye334的博客](http://img0.ph.126.net/D0aif2yFKKrcOrvF8R1EIQ==/6608726788934279706.jpg)
![STM32串口IAP(YModem) - ziye334 - ziye334的博客](http://img2.ph.126.net/JvR0rr1LR75Grm7-5itk2g==/6619485510211707095.jpg)
![STM32串口IAP(YModem) - ziye334 - ziye334的博客](http://img0.ph.126.net/PrV5kXe3lR40NGVvA6HMbQ==/6619266707397779607.jpg)
![STM32串口IAP(YModem) - ziye334 - ziye334的博客](http://img0.ph.126.net/RGkNZG9Es-9N17_LmtonqA==/6619094084072221641.jpg)
![STM32串口IAP(YModem) - ziye334 - ziye334的博客](http://img0.ph.126.net/Vzp-aA_TnHcbtJe30MkPlQ==/6619459121932641273.jpg)
![STM32串口IAP(YModem) - ziye334 - ziye334的博客](http://img2.ph.126.net/SVNJ8wyaTXcQEtqGK5Ejzg==/6619261209839640794.jpg)
![STM32串口IAP(YModem) - ziye334 - ziye334的博客](http://img2.ph.126.net/JwdaBuzHHRlSDReoH8nRwg==/6608910407376118082.jpg)
![STM32串口IAP(YModem) - ziye334 - ziye334的博客](http://img2.ph.126.net/6i60kk97Qrs9go-vyTvxXA==/6619472316072174593.jpg)
![STM32串口IAP(YModem) - ziye334 - ziye334的博客](http://img1.ph.126.net/JC17X_gEIDJxXF5DzvHpQQ==/6608727888445907611.jpg)
![STM32串口IAP(YModem) - ziye334 - ziye334的博客](http://img2.ph.126.net/12IBOmOol0-WnDchv8hxSQ==/6608719092352885424.jpg)
![STM32串口IAP(YModem) - ziye334 - ziye334的博客](http://img1.ph.126.net/_knSJFlanSASEGY42sHDnw==/6608835640585430575.jpg)
![STM32串口IAP(YModem) - ziye334 - ziye334的博客](http://img2.ph.126.net/Z9SmUoRpbjtoGHuTstImkg==/6608938994678445359.jpg)