之前写了简单的Bootloader,只实现了程序跳转的功能。但是作为一个Bootloader,只能完成程序跳转感觉缺了点啥。那就继续添加可以加载固件的功能吧。
加载固件的途径有很多,常见的就有串口、可移动存储设备、网络等等。这里就从最简单的开始吧,先做串口的。
整个程序的思路如下:
- 上位机与单片机进行握手,并传输一些必要的信息。
- 上位机发送固件的一部分到单片机(这里描述为一个块数据)。
- 单片机接收到一块数据后,将其保存到RAM中。然后向上位机返回这块数据的校验和。
- 上位机检查单片机返回的校验和是否正确,正确则重复步骤2和步骤3,直到整个固件发送完成。不正确则结束传输。
要完成整个加载的过程,起码要有几个方面的工作要做:
- 串口接收程序。
- Flash写入程序。
- 解析二进制文件并发送到串口的上位机程序。
串口接收程序需要和上位机程序一起说明,留到后面再说。现在先看下如何操作Flash。
1 Flash写入程序
1.0 目标
将一块RAM中的数据写入到Flash。
1.1 准备硬件
依然是淘宝上随处可见的STM32F103C8T6核心板,以及烧录和调试需要用到的STLINK。
1.2 理论基础
Flash简介之类的东西就不写了,上网查一下都有。这里主要写这款硬件相关的吧。这里主要参考ST的PM0075文档《STM32F10xxx Flash memory microcontrollers》,感兴趣的可以去看原文。
F103C8属于STMF1系列中的中等容量单片机,Flash容量为64KB,占用地址为:0x08000000~0x0800FFFF。这块64KB的Flash被分为64个1KB的页。这就意味着擦除的最小单位为1KB。
了解完Flash的组织结构后,再来了解一下Flash的读写操作。
读操作相对简单,直接声明一个指向要读取数据的指针就可以直接读取数据了。
写操作相对繁琐,ST官方PM0075文档中有如下流程图:
对照着流程图,这里稍作一点解释。
- STM32F103内部有一组寄存器(FPEC)是实现Flash控制的。上电后,FPEC寄存器组处于锁定状态,不可操作。因此在操作Flash之前,需要读取锁定状态位,如果FPEC锁定了,则需要解锁。
- 解锁Flash的方法很简单,就是依次先后将KEY1 = 0x45670123、KEY2 = 0xCDEF89AB 写入FLASH_KEYR寄存器即可。
- 解锁后,将FLASH_CR寄存器的PG位置为1。然后向目标地址写入一个半字(16bit)。
- 检查FLASH_SR寄存器的BSY位。等待BSY位变为0,从目标地址中读取一个半字,与写入的数据进行比较,检查是否真正写入。
实际上,HAL库会提供操作Flash的API函数,上述步骤只是了解就行,实际使用起来还是直接使用HAL库。
还有一点需要注意的。Flash操作的带宽为16位(16bit)。这意味着我们每次写入的时候都只能是16位的倍数。但是HAL库会提供写8位的函数,实际上这个函数是把高八位设定为0而已,本质上还是写入16位。
1.3 创建工程
创建工程就比较简单了。由于HAL库的工程会带有Flash操作的函数库,所以我们正常创建一个LED闪烁的工程即可。如下图所示:
1.3.1 简单测试
首先简单测试一下上面提到的读写操作是否有问题。
读操作:
/*
*
* 从Flash的某个地址中读取一个半字(16bit)
*
*/
uint16_t FLASH_ReadHalfWord(uint32_t addr)
{
return *(volatile uint16_t *)addr;
}
由于写入是半字写入,那读取也写成一致的半字读取。需要字读取的可以在半字读取的基础上进行改写。
写操作函数不需要自己写,Hal库有提供。但是需要注意,执行的操作还是要按照上述流程图的顺序来执行的。
下面是Hal库提供的函数。
HAL_StatusTypeDef HAL_FLASH_Unlock(void); //解锁
HAL_StatusTypeDef HAL_FLASH_Program(uint32_t TypeProgram, uint32_t Address, uint64_t Data); //编程
HAL_StatusTypeDef HAL_FLASH_Lock(void); //上锁
使用上述的函数,可以完成写入半字的操作,如下:
/*
*
* 按照官方文档的写入序列,向Flash的某个地址写入半字(16bit)
*
*/
uint8_t Flash_WriteHalfWord(uint32_t addr, uint16_t data)
{
HAL_StatusTypeDef ret; //操作返回值
/* 1、解锁 */
ret = HAL_FLASH_Unlock();
if(ret != HAL_OK)
{
return 0;
}
/* 2、设置PG位为1 */
/* 3、写入半字 */
/* 4、等待写入完成 */
ret = HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, addr, data);
if(ret != HAL_OK)
{
return 0;
}
/* 5、读出,与写入的数据进行比较 */
uint16_t readBackData = Flash_ReadHalfWord(addr);
if(readBackData != data)
{
return 0;
}
/* 6、上锁 */
ret = HAL_FLASH_Lock();
/* 写入成功 */
return 1;
}
其中HAL库的HAL_FLASH_Program()函数完成的工作比较多,包括:
等待BSY位为0
设置PG位为1
往指定的地址中写入数据
等待BSY位为0
清除PG位为0
这样我们就得到了往Flash中写入半字的函数。但是这里还有一个问题,在写入操作之前,没有擦除目标地址所在的页。
Flash与EEPROM有个不同点,Flash是不可改写的。如果目标地址里面存在数据,那么再次写入数据时会失败。因此需要在写入数据之前,擦除目标地址所在的页。HAL为我们提供了擦除函数,如下:
/*
* description : flash擦除操作
* param - pEraseInit : 擦除初始结构体,用于设置擦除类型、地址、大小。
* param - PageError : 擦除失败时,保存擦除失败发生时的地址。
* return : 操作结果
*/
HAL_StatusTypeDef HAL_FLASHEx_Erase(FLASH_EraseInitTypeDef *pEraseInit, uint32_t *PageError);
至此可以编写一个简单的测试程序,主要代码如下。每秒中擦除0x08005000地址所在的块,然后写入一个数据。
现象:可以看到LED灯每秒闪烁一次,也可以在调试中查看0x08005000地址的值在变化。
FLASH_EraseInitTypeDef flashEraseStrc;
uint32_t pageErr;
while (1)
{
data += 100;
if(data > 65535)
data = 0;
flashEraseStrc.TypeErase = FLASH_TYPEERASE_PAGES;
flashEraseStrc.PageAddress = 0x08005000;
flashEraseStrc.NbPages = 1;
HAL_FLASHEx_Erase(&flashEraseStrc, &pageErr);
if(Flash_WriteHalfWord(0x08005000, data) >= 0)
{
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
HAL_Delay(1000);
}
1.3.2 复制函数
完成Flash的读写函数后,可以继续进行下去,编写一个从RAM复制数据到Flash的函数。如下:
/*
* description : 从RAM复制到Flash
* param - source : 源地址
* param - destination : 目的地址
* param - size : 复制字节数
* return : 0 - false; 1 - true;
* note : Flash的组织形式和Bin文件的组织形式都是小端,所以是低8位在前。
*/
uint8_t Flash_RamToFlash(uint8_t *source, uint32_t destination, uint32_t size)
{
uint16_t u16Temp = 0; //16位临时值
uint32_t i = 0; //循环计数值
uint8_t ret = 0; //操作结果值
for(i=0; i
{
if((i+1) >= size)
{
/* 最后一个单数字节,在其低8位补0xFF */
u16Temp = (0xFF << 8) + source
;
}
else
{
/* 正常能凑成双字节(半字)的数据 */
u16Temp = (source[i+1] << 8) + source;
}
ret = Flash_WriteHalfWord(destination + i, u16Temp);
if(ret == 0)
return 0;
}
return 1;
}
完成这个函数之后,其实就已经完成了向Flash写入程序了。因为上位机发送过来的块数据也是先存储到RAM再写入Flash的。
2 上位机及单片机串口接收程序
2.0 目标
上位机与单片机通过串口进行通讯,先握手,再传输一些必要的信息,最后完成Bin文件的传输。
2.1 创建工程
单片机工程不需要重建,沿用上面的就行。上位机比较简单,创建一个.NET Winform的C#工程即可。创建好工程后,摆放一下控件,最终效果如下所示:
2.2 上位机主要逻辑
这个上位机的功能比较简单,这里简要描述一下工作逻辑。
2.2.1加载一个Bin文件。
这里使用常见的文件路径+浏览按键的方式。单击浏览按键,弹出一个打开文件对话框(OpenFileDialog),OpenFileDialog的对象设置一下属性,使其过滤器只能识别Bin文件。选择好文件后将路径名显示在长条文本框内。同时创建一个BinaryReader对象,读取Bin文件的内容,并显示到大文本框内。核心代码如下:
////// 加载Bin文件/////////private void btnBrowse_Click(object sender, EventArgs e){ OpenFileDialog dialog = new OpenFileDialog(); dialog.Multiselect = false; dialog.Title = "请选择Bin文件"; dialog.Filter = "Bin文件(*.bin)|*.bin"; if (dialog.ShowDialog() == DialogResult.OK) { txtFilename.Text = dialog.FileName; FileStream fs = new FileStream(dialog.FileName, FileMode.Open); BinaryReader reader = new BinaryReader(fs); byte[] content = reader.ReadBytes((int)fs.Length); txtMessage.Clear(); int cnt = 0; string strContent = null; foreach (var item in content) { strContent += item.ToString("X2") + " "; cnt++; if (cnt >= 16) { cnt = 0; strContent += Environment.NewLine; } } txtMessage.AppendText(strContent + Environment.NewLine); txtMessage.AppendText("Total : " + fs.Length.ToString() + "bytes"); fs.Close(); }} 运行效果如下:
2.2.2 上位机串口程序
写串口通讯程序之前,要先要先确定通讯的协议。考虑到通讯不算复杂,就设计一个定长帧协议好了。(此处留空。协议表留在办公室忘记上传了)
串口控件的程序用的是以前写的串口封装类。这里稍微说一下工作的逻辑。由于串口接收事件有大约十几MS的延迟,因此想要实现单片机上接收一个字节处理一个字节的逻辑基本是做不到的。因此需要在每次发生串口接收事件时,将串口接收到的数据全部填充到一个缓冲区中。在需要读取串口数据时,再使用帧结构的规则来检索缓冲区,最后得到需要的数据。
2.2.3 上位机通讯过程
- 上位机发送请求指令:FA A5 5A 01 00 00 00 00 X
- 单片机接收到请求指令后,返回回应信号 FA 5A A5 01 00 00 00 00 X
- 上位机接收到单片机的01回应信号后,发送编程地址指令:FA A5 5A 02 四字节编程地址 X
- 单片机接收到编程地址指令后,返回回应信号 FA 5A A5 02 四字节编程地址 X
- 上位机接收到单片机的02回应信号后,发送块大小指令:FA A5 5A 03 四字节当前传输的块大小 X
- 单片机接收到块大小指令后,调用串口API函数,开始接收一个块的数据。
- 单片机接收完一个块的数据后,计算这个块数据的校验和,返回校验和给上位机:FA 5A A5 04 00 00 00 块校验和 X
- 上位机接收到单片机回应的校验和,与自己计算的校验和进行比较,若相等,就重复步骤5~步骤8
- 上位机传输完所有的块,且验证了校验和都没有问题,此时上位机发送结束指令:FA A5 5A FE 00 00 00 00 X
- 单片机接收到结束指令后,跳转到编程地址执行程序。
通讯过程如下所示:
3 总结
使用串口加载固件的重点其实不是怎么传输,而是怎么编程Flash。支持IAP(应用内编程)的芯片很多,操作方法也很多,一般来说,阅读官方的文档都能解决问题。
联系方式:489304195@qq.com