目录
简介
不同的程序下载方式
ICP:In-Circuit Programming
ISP:In-System Programing
IAP:In-Application Programming
BootLoader
Bootloader 是什么?
STM32的启动方式
存储器组织
存储器映像
嵌入式SRAM
嵌入式FLASH
IAP 简介
STM32启动流程
APP程序
APP 程序起始地址设置方法
中断向量表的偏移量设置方法
BootLader程序
功能简介:
软件设计
打开stmflash.h 代码如下:
打开stmflash.c 代码如下:
打开 iap.c, 代码如下:
打开 iap.h 代码如下:
打开 usart1.c:
最后我们看看 main 函数如下:
下载验证
第一步,烧录BootLoader程序到单片机里面
观察现象:
第二步:先用电脑作为上位机,通过串口发送APP程序给单片机:
第三步:按下按键1将固件写入到FLASH中
第四步:按下按键2,看看程序是否从BootLoader程序跳到APP程序
总结
ESP8266
简介
AT指令
烧录固件
传输原理
实现流程
串口
ESP8266代码编写
现象观察
总结
简介
该篇将会从零基础开始像读者讲解怎么使用远程的方法来烧录STM32程序。我这里用的是ESP8266和STM32F407ZGT6,当然,使用其他32的芯片也是可以的,核心都是一样的。
不同的程序下载方式
目前,单片机的程序烧录方式可以分为三种:ICP,ISP,IAP。
ICP:In-Circuit Programming
在电路中编程。使用厂家配套的软件或仿真器进行程序烧录,目前主流的有JTAG接口和SWD接口,常用的烧录工具为J-Link、ST-Link等。
在程序开发阶段,通常在连接下载器的情况下直接使用编程软件进行程序下载调试。
在MDK软件中可以选择不同的下载器。
ISP:In-System Programing
在系统中编程。以STM32为例,其内置了一段Bootloader程序,可以通过更改BOOT引脚电平来运行这段程序,再通过ISP编程工具将程序下载进去。下载完毕之后,再更改BOOT至正常状态,使得MCU运行所下载的程序。
正点原子的STM32开发板中专门设计了一个单片机自动复位及设置Boot引脚电平状态的电路,便于程序下载。
IAP:In-Application Programming
在应用中编程。IAP可以使用微控制器支持的任一种通信接口(如I/O端口、USB、CAN、UART、I2C、SPI等)下载程序或数据到FLASH中。IAP允许用户在程序运行时重新烧写FLASH中的内容。但需要注意,IAP要求至少有一部分程序(Bootloader)已经使用ICP或ISP烧到FLASH中。
无论是ICP技术还是ISP技术,都需要连接下载线,设置跳线帽等操作。一般来说,产品的电路板都会密封在外壳中,在这时若要使用ICP或ISP的方式对程序进行更新,则必然要拆装外壳,如果产品的数量比较多,将花费很多不必要的时间。
采用IAP编程技术,可以在一定程度上避免上述的情况。一般情况下,产品的外壳都会留有通信接口,若能通过这种通信方式对程序进行升级,则可以省去拆装的麻烦。在此基础上,若引入远距离或无线数据传输方案,更可以实现远程编程或无线编程
BootLoader
要学会远程烧录,首先要知道BootLoader。
Bootloader 是什么?
Bootloader
是在应用程序开始前运行的一个小程序,里面可以进行一些初始化操作,升级引用程序等,在嵌入式设备中很常见。
STM32的启动方式
这两个地址处存储的值通常是在嵌入式系统或者某些操作系统启动过程中使用的关键参数,具体作用如下:
1.栈指针 MSP 的初始值:
2.栈指针 MSP(Main Stack Pointer)是处理器用来管理栈空间的指针,它指向当前栈顶的地址。
3.在许多嵌入式系统中,系统启动时会从地址 0x00000000 处读取初始的栈指针值。这个值告诉处理器当前的栈顶在哪里,即初始时可用的栈空间范围。
4.栈指针的初始值是系统启动时的重要参数,确保函数调用和中断处理等操作都能正常进行。
5.程序指针 PC 的初始值:
6.程序计数器 PC(Program Counter)是一个寄存器,存储当前正在执行的指令的地址。
7.在系统复位后,处理器需要知道从哪里开始执行代码。地址 0x00000004 处通常存储了复位后应该执行的第一条指令的地址。
8.这个值告诉处理器从哪里开始执行代码,确保系统复位后可以顺利进入正常的程序执行流程,而不是随机执行内存中的数据。
总结起来,这些地址处存储的初始值对于系统启动和复位后的正常运行至关重要。栈指针 MSP 确保了栈空间的正常分配和管理,而程序指针 PC 则确保了系统复位后可以从正确的位置开始执行程序。
存储器组织
这里以STM32F103C8T6系列来进行举例。
存储器映像
寄存器映像和位段不影响我们本篇使用BootLoader,所以这里就不去列举了。我们这里重点讲解嵌入式闪存(FLASH)和嵌入式(SRAM)。
嵌入式SRAM
告诉编译器RAM的起始地址和大小。
嵌入式FLASH
我们这里介绍一下各个STM32芯片叫法背后的区别:
内部FLASH的构成
各个存储区域的说明如下:
IAP 简介
STM32启动流程
STM32 的内部闪存(FLASH)地址起始于 0x08000000,一般情况下,程序文件就从此地址开始写入。此外 STM32 是基于 Cortex-M3 内核的微控制器,其内部通过一张“中断向量表” 来响应中断,程序启动后,将首先从“中断向量表”取出复位中断向量执行复位中断程序完成启动,而这张“中断向量表”的起始地址是 0x08000004,当中断来临,STM32的内部硬件机制亦会自动将PC指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断服务程序。
在图所示流程中,STM32 复位后,还是从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到 IAP 的 main 函数,如图标号①所示,此部分同图 52.1.1 一样;在执行完IAP以后(即将新的APP代码写入STM32的 FLASH,灰底部分。新程序的复位中断向量起始地址为 0X08000004+N+M),跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的 main 函数,如图标号②和③所示,同样 main 函数为一个死循环,并且注意到此时 STM32 的FLASH,在不同位置上,共有两个中断向量表。
APP程序
APP 程序起始地址设置方法
中断向量表的偏移量设置方法
在STM32微控制器中,启动时的初始化顺序通常如下:
1.复位向量:
2.当STM32微控制器上电或者复位时,会首先跳转到复位向量所指向的地址。复位向量通常指向微控制器内部的复位处理程序(Reset Handler)。
3.复位处理程序(Reset Handler):
4.复位处理程序是一个特殊的中断服务程序(ISR),它负责执行一系列的系统初始化操作,包括:
5.初始化堆栈指针(Stack Pointer)。
6.初始化数据段(Data Segment)和BSS段(未初始化数据段)。
7.调用 SystemInit() 函数进行系统级初始化。
8.SystemInit() 函数:
9.SystemInit() 函数是在复位处理程序中被调用的,用于执行系统级的初始化。这个函数通常包括设置系统时钟、初始化外设等操作。它的目的是在启动时将系统配置到一个合适的状态,以便后续的应用程序执行。
总结:
在STM32微控制器启动过程中,首先会跳转到复位向量指向的复位处理程序(Reset Handler)。复位处理程序会在其内部调用 SystemInit() 函数来进行系统级的初始化。因此,先调用的是复位处理程序,然后在复位处理程序内部调用 SystemInit() 函数。
#ifdef VECT_TAB_SRAM
SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET;
/* Vector Table Relocation in Internal SRAM. */
#else
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;
/* Vector Table Relocation in Internal FLASH. */
#endif
SRAM_BASE是STM32中SRAM的基地址,FLASH_BASE是STM32中FLASH的基地址。这段代码的作用是根据宏 VECT_TAB_SRAM
的定义,动态地选择将向量表重定位到内部的SRAM或FLASH存储器中。通过这种方式,可以在不同的嵌入式系统配置中灵活地设置向量表的位置,以适应特定的硬件设计和需求。
以上是 FLASH APP 的情况,当使用SRAM APP的时候,我们设置起始地址为:
通过以上两个步骤的设置,我们就可以生成 APP 程序了,只要 APP 程序的 FLASH 和 SRAM大小不超过我们的设置即可。
不过 MDK 默认生成的文件是.hex 文件,并不方便我们用作IAP更新,我们希望生成的文件是.bin 文件,这样可以方便进行 IAP 升级。bin文件和hex文件最大的区别就是hex文件会附带下载的地址,而bin文件则没有。
本章,我们通过在 MDK 点击 Options for Target→User选项卡,在After Build/Rebuild栏, 勾选 Run #1,并写入:D:\tools\mdk5.14\ARM\ARMCC\bin\fromelf.exe --bin -o ..\OBJ\RTC.bin ..\OBJ\RTC.axf。如图所示:
BootLader程序
功能简介:
我们先用电脑作为上位机,通过串口来对STM32进行APP程序的下载。
软件设计
我自己写好了程序,如果读者需要的话,可以在评论区或者私聊我,我看到之后,会把工程分享出去。
由于我们要擦除和写入FLASH,我们要封装好写入FLASH的函数,我们IAP组下,添加了stmflash.c以及头文件stmflash.h。
打开stmflash.h 代码如下:
#ifndef __STMFLASH_H__
#define __STMFLASH_H__
#include "main.h"
#include "common.h"
//FLASH扇区的地址
#define FLASH_Sector_0 0
#define FLASH_Sector_1 1
#define FLASH_Sector_2 2
#define FLASH_Sector_3 3
#define FLASH_Sector_4 4
#define FLASH_Sector_5 5
#define FLASH_Sector_6 6
#define FLASH_Sector_7 7
#define FLASH_Sector_8 8
#define FLASH_Sector_9 9
#define FLASH_Sector_10 10
#define FLASH_Sector_11 11
//FLASH起始地址
#define STM32_FLASH_BASE 0x08000000 //STM32 FLASH的起始地址
//FLASH 扇区的起始地址
#define ADDR_FLASH_SECTOR_0 ((u32)0x08000000) //扇区0起始地址, 16 Kbytes
#define ADDR_FLASH_SECTOR_1 ((u32)0x08004000) //扇区1起始地址, 16 Kbytes
#define ADDR_FLASH_SECTOR_2 ((u32)0x08008000) //扇区2起始地址, 16 Kbytes
#define ADDR_FLASH_SECTOR_3 ((u32)0x0800C000) //扇区3起始地址, 16 Kbytes
#define ADDR_FLASH_SECTOR_4 ((u32)0x08010000) //扇区4起始地址, 64 Kbytes
#define ADDR_FLASH_SECTOR_5 ((u32)0x08020000) //扇区5起始地址, 128 Kbytes
#define ADDR_FLASH_SECTOR_6 ((u32)0x08040000) //扇区6起始地址, 128 Kbytes
#define ADDR_FLASH_SECTOR_7 ((u32)0x08060000) //扇区7起始地址, 128 Kbytes
#define ADDR_FLASH_SECTOR_8 ((u32)0x08080000) //扇区8起始地址, 128 Kbytes
#define ADDR_FLASH_SECTOR_9 ((u32)0x080A0000) //扇区9起始地址, 128 Kbytes
#define ADDR_FLASH_SECTOR_10 ((u32)0x080C0000) //扇区10起始地址,128 Kbytes
#define ADDR_FLASH_SECTOR_11 ((u32)0x080E0000) //扇区11起始地址,128 Kbytes
u32 STMFLASH_ReadWord(u32 faddr); //读出字
void STMFLASH_Write(u32 WriteAddr,u32 *pBuffer,u32 NumToWrite); //从指定地址开始写入指定长度的数据
void STMFLASH_Read(u32 ReadAddr,u32 *pBuffer,u32 NumToRead); //从指定地址开始读出指定长度的数据
#endif
打开stmflash.c 代码如下:
#include "stmflash.h"
#include "common.h"
#include "usart1.h"
#include "stm32f4xx_hal_flash_ex.h"
#include "stm32f4xx_hal.h"
uint32_t STM32_FLASH_GetSector(uint32_t Address)
{
uint32_t sector = 0;
if ((Address < ADDR_FLASH_SECTOR_1) && (Address >= ADDR_FLASH_SECTOR_0))
{
sector = FLASH_SECTOR_0;
}
else if ((Address < ADDR_FLASH_SECTOR_2) && (Address >= ADDR_FLASH_SECTOR_1))
{
sector = FLASH_SECTOR_1;
}
else if ((Address < ADDR_FLASH_SECTOR_3) && (Address >= ADDR_FLASH_SECTOR_2))
{
sector = FLASH_SECTOR_2;
}
else if ((Address < ADDR_FLASH_SECTOR_4) && (Address >= ADDR_FLASH_SECTOR_3))
{
sector = FLASH_SECTOR_3;
}
else if ((Address < ADDR_FLASH_SECTOR_5) && (Address >= ADDR_FLASH_SECTOR_4))
{
sector = FLASH_SECTOR_4;
}
else if ((Address < ADDR_FLASH_SECTOR_6) && (Address >= ADDR_FLASH_SECTOR_5))
{
sector = FLASH_SECTOR_5;
}
else if ((Address < ADDR_FLASH_SECTOR_7) && (Address >= ADDR_FLASH_SECTOR_6))
{
sector = FLASH_SECTOR_6;
}
else if ((Address < ADDR_FLASH_SECTOR_8) && (Address >= ADDR_FLASH_SECTOR_7))
{
sector = FLASH_SECTOR_7;
}
else if ((Address < ADDR_FLASH_SECTOR_9) && (Address >= ADDR_FLASH_SECTOR_8))
{
sector = FLASH_SECTOR_8;
}
else if ((Address < ADDR_FLASH_SECTOR_10) && (Address >= ADDR_FLASH_SECTOR_9))
{
sector = FLASH_SECTOR_9;
}
else if ((Address < ADDR_FLASH_SECTOR_11) && (Address >= ADDR_FLASH_SECTOR_10))
{
sector = FLASH_SECTOR_10;
}
else
{
sector = FLASH_SECTOR_11;
}
return sector;
}
//读取指定地址的半字(16位数据)
//faddr:读地址
//返回值:对应数据.
u32 STMFLASH_ReadWord(u32 faddr)
{
return *(vu32*)faddr;
}
//获取某个地址所在的flash扇区
//addr:flash地址
//返回值:0~11,即addr所在的扇区
uint16_t STMFLASH_GetFlashSector(u32 addr)
{
if(addr<ADDR_FLASH_SECTOR_1)return FLASH_Sector_0;
else if(addr<ADDR_FLASH_SECTOR_2)return FLASH_Sector_1;
else if(addr<ADDR_FLASH_SECTOR_3)return FLASH_Sector_2;
else if(addr<ADDR_FLASH_SECTOR_4)return FLASH_Sector_3;
else if(addr<ADDR_FLASH_SECTOR_5)return FLASH_Sector_4;
else if(addr<ADDR_FLASH_SECTOR_6)return FLASH_Sector_5;
else if(addr<ADDR_FLASH_SECTOR_7)return FLASH_Sector_6;
else if(addr<ADDR_FLASH_SECTOR_8)return FLASH_Sector_7;
else if(addr<ADDR_FLASH_SECTOR_9)return FLASH_Sector_8;
else if(addr<ADDR_FLASH_SECTOR_10)return FLASH_Sector_9;
else if(addr<ADDR_FLASH_SECTOR_11)return FLASH_Sector_10;
return FLASH_Sector_11;
}
//从指定地址开始写入指定长度的数据
//特别注意:因为STM32F4的扇区实在太大,没办法本地保存扇区数据,所以本函数
// 写地址如果非0XFF,那么会先擦除整个扇区且不保存扇区数据.所以
// 写非0XFF的地址,将导致整个扇区数据丢失.建议写之前确保扇区里
// 没有重要数据,最好是整个扇区先擦除了,然后慢慢往后写.
//该函数对OTP区域也有效!可以用来写OTP区!
//OTP区域地址范围:0X1FFF7800~0X1FFF7A0F
//WriteAddr:起始地址(此地址必须为4的倍数!!)
//pBuffer:数据指针
//NumToWrite:字(32位)数(就是要写入的32位数据的个数.)
void STMFLASH_Write(uint32_t WriteAddr, uint32_t *pBuffer, uint32_t NumToWrite)
{
HAL_StatusTypeDef status = HAL_OK;
uint32_t addrx = 0;
uint32_t endaddr = 0;
if (WriteAddr < FLASH_BASE || WriteAddr % 4 != 0) return; // 非法地址
HAL_FLASH_Unlock(); // 解锁闪存
__HAL_FLASH_DATA_CACHE_DISABLE(); // 闪存擦除期间必须禁止数据缓存
addrx = WriteAddr; // 写入的起始地址
endaddr = WriteAddr + NumToWrite * 4; // 写入的结束地址
if (addrx < 0x1FFF0000) // 只有主存储区,才需要执行擦除操作
{
while (addrx < endaddr) // 扫清一切障碍.(对非FFFFFFFF的地方,先擦除)
{
if (*(volatile uint32_t*)addrx != 0xFFFFFFFF) // 有非0xFFFFFFFF的地方,要擦除这个扇区
{
uint32_t sectorError = 0;
FLASH_EraseInitTypeDef eraseInitStruct;
eraseInitStruct.TypeErase = FLASH_TYPEERASE_SECTORS;
eraseInitStruct.VoltageRange = FLASH_VOLTAGE_RANGE_3;
eraseInitStruct.Sector = STM32_FLASH_GetSector(addrx);
eraseInitStruct.NbSectors = 1;
status = HAL_FLASHEx_Erase(&eraseInitStruct, §orError); // 擦除操作
if (status != HAL_OK) break; // 发生错误了
}
else
{
addrx += 4;
}
}
}
if (status == HAL_OK)
{
while (WriteAddr < endaddr) // 写数据
{
status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, WriteAddr, *pBuffer); // 写入数据
if (status != HAL_OK)
{
break; // 写入异常
}
WriteAddr += 4;
pBuffer++;
}
}
__HAL_FLASH_DATA_CACHE_ENABLE(); // 闪存擦除结束,开启数据缓存
HAL_FLASH_Lock(); // 上锁闪存
}
//从指定地址开始读出指定长度的数据
//ReadAddr:起始地址
//pBuffer:数据指针
//NumToRead:字(4位)数
void STMFLASH_Read(u32 ReadAddr,u32 *pBuffer,u32 NumToRead)
{
u32 i;
for(i=0;i<NumToRead;i++)
{
pBuffer[i]=STMFLASH_ReadWord(ReadAddr);//读取4个字节.
ReadAddr+=4;//偏移4个字节.
}
}
打开 iap.c, 代码如下:
#include "usart1.h"
#include "stmflash.h"
#include "iap.h"
#include "main.h"
iapfun jump2app;
u32 iapbuf[512];
//appxaddr:应用程序的起始地址
//appbuf:应用程序CODE.
//appsize:应用程序大小(字节).
void iap_write_appbin(u32 appxaddr,u8 *appbuf,u32 appsize)
{
u16 t;
u16 i=0;
u32 temp;
u32 fwaddr=appxaddr;//当前写入的地址
u8 *dfu=appbuf;
for(t=0;t<appsize;t+=4)
{
temp=(u32)dfu[3]<<24;
temp+=(u32)dfu[2]<<16;
temp+=(u32)dfu[1]<<8;
temp+=(u32)dfu[0];
dfu+=4;//偏移4个字节
iapbuf[i++]=temp;
if(i==512)
{
i=0;
STMFLASH_Write(fwaddr,iapbuf,512);
fwaddr+=2048;//偏移2048 16=2*8.所以要乘以2.
}
}
if(i)STMFLASH_Write(fwaddr,iapbuf,i);//将最后的一些内容字节写进去.
}
//跳转到应用程序段
//appxaddr:用户代码起始地址.
void iap_load_app(u32 appxaddr)
{
if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000) //检查栈顶地址是否合法.
{
jump2app=(iapfun)*(vu32*)(appxaddr+4); //用户代码区第二个字为程序开始地址(复位地址)
MSR_MSP(*(vu32*)appxaddr); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
jump2app(); //跳转到APP.
}
}
总体代码还是非常简单的,这里需要注意的是:
void iap_write_appbin(u32 appxaddr,u8 *appbuf,u32 appsize) ;
当 iapbuf
数组填满时,即存储了1024个16位数据时,执行以下操作:
- 调用
STMFLASH_Write
函数,将iapbuf
中的512个32位数据写入Flash存储器中,从fwaddr
开始的位置。 fwaddr
递增2048,因为每次写入512个32位数据,占用了2048字节的Flash空间。
这段代码实现了一个简单的IAP函数,用于将应用程序的二进制数据写入STM32的Flash存储器中。它通过循环遍历应用程序数据,并将其暂存在 iapbuf
数组中,每次达到512个数据时就将其写入Flash。最后,处理剩余的数据并确保所有数据都被写入到目标Flash地址中,以实现固件更新或应用程序下载的功能。
void iap_load_app(u32 appxaddr) ;
这段代码的作用是从指定的应用程序起始地址 appxaddr 处跳转执行应用程序代码。让我们逐行分析:
函数定义和参数
//跳转到应用程序段
//appxaddr:用户代码起始地址.
void iap_load_app(u32 appxaddr)
{
1.iap_load_app 函数用于加载并执行位于 appxaddr 处的应用程序。
2.appxaddr 是用户代码的起始,即应用程序的入口点。
检查栈顶地址是否合法
if(((*(vu32*)appxaddr) & 0x2FFE0000) == 0x20000000)
3.这里通过 (*(vu32*)appxaddr) 来读取 appxaddr 地址处的数据,然后检查其高位地址是否符合特定的合法栈顶地址的格式。
4.0x2FFE0000 是一个标准的栈顶地址的检查模式,用来确保 appxaddr 指向的位置是有效的。
设置函数指针和堆栈指针
{
jump2app = (iapfun) * (vu32 *)(appxaddr + 4);
MSR_MSP(*(vu32 *)appxaddr);
}
5.如果栈顶地址合法,则执行以下操作:
6.jump2app = (iapfun) * (vu32 *)(appxaddr + 4);:从 appxaddr + 4 处读取一个 vu32 类型的值,这个值是应用程序的入口地址(复位地址)。然后将其转换为函数指针类型 iapfun,即 jump2app 现在指向应用程序的入口函数。
7.MSR_MSP(*(vu32 *)appxaddr);:用 appxaddr 处的值来初始化堆栈指针。在 ARM Cortex-M 微控制器中,初始化堆栈指针是通过 MSR_MSP 指令来实现的,它将 appxaddr 地址处的值作为新的主堆栈指针值。
执行应用程序跳转
jump2app(); // 跳转到应用程序入口点
}
}
8.最后,调用 jump2app() 函数指针,实际上是跳转到应用程序的入口点,开始执行应用程序代码。
总结
这段代码实现了一个简单的应用程序加载函数 iap_load_app,它检查给定的应用程序起始地址的栈顶地址是否有效,然后设置函数指针和堆栈指针,并最终通过函数指针调用实现了应用程序的跳转和执行。这种方式通常用于实现固件升级或者在运行时加载并执行新的应用程序。
打开 iap.h 代码如下:
#ifndef __IAP_H__
#define __IAP_H__
#include "common.h"
typedef void (*iapfun)(void); //定义一个函数类型的参数.
#define FLASH_APP1_ADDR 0x08010000 //第一个应用程序起始地址(存放在FLASH)
//保留0X08000000~0X0800FFFF的空间为IAP使用
void iap_load_app(u32 appxaddr); //执行flash里面的app程序
void iap_write_appbin(u32 appxaddr,u8 *appbuf,u32 applen); //在指定地址开始,写入bin
#endif
打开 usart1.c:
//iap
//串口1中断服务程序
//注意,读取USARTx->SR能避免莫名其妙的错误
u8 USART_RX_BUF[USART_REC_LEN] __attribute__ ((at(0X20001000)));//接收缓冲,最大USART_REC_LEN个字节,起始地址为0X20001000.
//接收状态
//bit15, 接收完成标志
//bit14, 接收到0x0d
//bit13~0, 接收到的有效字节数目
u16 USART_RX_STA=0; //接收状态标记
u16 USART_RX_CNT=0; //接收的字节数
u8 aRxBuffer[RXBUFFERSIZE]; //HAL库使用的串口接收缓冲
UART_HandleTypeDef UART1_Handler; //UART句柄
/****************************************************************************
* 名 称: void HAL_UART_MspInit(UART_HandleTypeDef *huart)
* 功 能:UART底层初始化,时钟使能,引脚配置,中断配置
* 入口参数:huart:串口句柄
* 返回参数:无
* 说 明:此函数会被HAL_UART_Init()调用
****************************************************************************/
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
//GPIO端口设置
GPIO_InitTypeDef GPIO_Initure;
if(huart->Instance==USART1)//如果是串口1,进行串口1 MSP初始化
{
__HAL_RCC_GPIOA_CLK_ENABLE(); //使能GPIOA时钟
__HAL_RCC_USART1_CLK_ENABLE(); //使能USART1时钟
GPIO_Initure.Pin=GPIO_PIN_9; //PA9
GPIO_Initure.Mode=GPIO_MODE_AF_PP; //复用推挽输出
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_FAST; //高速
GPIO_Initure.Alternate=GPIO_AF7_USART1; //复用为USART1
HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化PA9
GPIO_Initure.Pin=GPIO_PIN_10; //PA10
HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化PA10
HAL_NVIC_EnableIRQ(USART1_IRQn); //使能USART1中断通道
HAL_NVIC_SetPriority(USART1_IRQn,3,3); //抢占优先级3,子优先级3
}
}
/****************************************************************************
* 名 称: void uart1_init(u32 bound)
* 功 能:USART1初始化
* 入口参数:bound:波特率
* 返回参数:无
* 说 明:
****************************************************************************/
void uart1_init(u32 bound)
{
//UART 初始化设置
UART1_Handler.Instance=USART1; //USART1
UART1_Handler.Init.BaudRate=bound; //波特率
UART1_Handler.Init.WordLength=UART_WORDLENGTH_8B; //字长为8位数据格式
UART1_Handler.Init.StopBits=UART_STOPBITS_1; //一个停止位
UART1_Handler.Init.Parity=UART_PARITY_NONE; //无奇偶校验位
UART1_Handler.Init.HwFlowCtl=UART_HWCONTROL_NONE; //无硬件流控
UART1_Handler.Init.Mode=UART_MODE_TX_RX; //收发模式
HAL_UART_Init(&UART1_Handler); //HAL_UART_Init()会使能UART1
HAL_UART_Receive_IT(&UART1_Handler, (u8 *)aRxBuffer, RXBUFFERSIZE);//该函数会开启接收中断:标志位UART_IT_RXNE,并且设置接收缓冲以及接收缓冲接收最大数据量
}
/*因printf()之类的函数,使用了半主机模式。使用标准库会导致程序无法
运行,以下是解决方法:使用微库,因为使用微库的话,不会使用半主机模式.
请在工程属性的“Target“-》”Code Generation“中勾选”Use MicroLIB“这
样以后就可以使用printf,sprintf函数了*/
//重定义fputc函数
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0);//循环发送,直到发送完毕
USART1->DR = (u8) ch;
return ch;
}
//串口1发送一个字符
void uart1SendChar(u8 ch)
{
while((USART1->SR&0x40)==0);
USART1->DR = (u8) ch;
}
/****************************************************************************
* 名 称: void uart1SendChars(u8 *str, u16 strlen)
* 功 能:串口1发送一字符串
* 入口参数:*str:发送的字符串
strlen:字符串长度
* 返回参数:无
* 说 明:
****************************************************************************/
void uart1SendChars(u8 *str, u16 strlen)
{
u16 k= 0 ;
do { uart1SendChar(*(str + k)); k++; } //循环发送,直到发送完毕
while (k < strlen);
}
这里,我们指定 USART_RX_BUF 的地址是从 0X20001000 开始,这里的0x20001000这个地址其实十分的巧妙,细心发现,其实可以当SRAM的启动地址,这样子,只需直接跳转,甚至不需要在调用写FLASH函数来对FLASH进行擦写。然后在 USART1_IRQHandler 函数里面,将串口发送过来的数据,全部接收到 USART_RX_BUF,并通过 USART_RX_CNT 计数。代码比较简单,我们就不多说了。
最后我们看看 main 函数如下:
#include "main.h"
#include "gpio.h"
#include "common.h"
#include "lcd.h"
#include "key.h"
#include "led.h"
#include "usart1.h"
#include "iap.h"
#include "stmflash.h"
void SystemClock_Config(void);
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
u8 t;
u16 oldcount=0; //老的串口接收数据值
u16 applenth=0; //接收到的app代码长度
u8 clearflag=0;
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);//设置NVIC中断分组2:2位抢占优先级,2位响应优先级
delay_init(); //延时函数初始化
uart1_init(9600); //串口初始化波特率为9600
LED_Init(); //LED初始化
KEY_Init(); //按键初始化
LCD_Init(); //初始化LCD FSMC接口和显示驱动
BRUSH_COLOR=RED; //设置画笔颜色为红色
LCD_DisplayString(10,10,16,"KEY_UP:Copy APP2FLASH");
LCD_DisplayString(10,80,16,"KEY2:Erase SRAM APP");
LCD_DisplayString(10,150,16,"KEY1:Run FLASH APP");
LCD_DisplayString(10,220,16,"KEY0:Run SRAM APP");
while (1)
{
if(USART_RX_CNT)
{
HAL_Delay(1);
if(oldcount==USART_RX_CNT)//新周期内,没有收到任何数据,认为本次数据接收完成.
{
applenth=USART_RX_CNT;
oldcount=0;
USART_RX_CNT=0;
printf("用户程序接收完成!\r\n");
printf("代码长度:%dBytes\r\n",applenth);
}else oldcount=USART_RX_CNT;
}
t++;
delay_ms(10);
if(t==30)
{
LED0=!LED0;
t=0;
if(clearflag)
{
clearflag--;
if(clearflag==0)
{
}//LCD_Fill(30,210,240,210+16,WHITE);//清除显示
}
}
key_scan(0);
if(keydown_data==KEY0_DATA)
{
if(applenth)
{
printf("开始更新固件...\r\n");
//LCD_ShowString(30,210,200,16,16,"Copying APP2FLASH...");
if(((*(vu32*)(0X20001000+4))&0xFF000000)==0x08000000)//判断是否为0X08XXXXXX.
{
iap_write_appbin(FLASH_APP1_ADDR,USART_RX_BUF,applenth);//更新FLASH代码
//LCD_ShowString(30,210,200,16,16,"Copy APP Successed!!");
printf("固件更新完成!\r\n");
}else
{
//LCD_ShowString(30,210,200,16,16,"Illegal FLASH APP! ");
printf("非FLASH应用程序!\r\n");
}
}else
{
printf("没有可以更新的固件!\r\n");
//LCD_ShowString(30,210,200,16,16,"No APP!");
}
clearflag=7;//标志更新了显示,并且设置7*300ms后清除显示
}
if(keydown_data==KEY2_DATA)
{
if(applenth)
{
printf("固件清除完成!\r\n");
//LCD_ShowString(30,210,200,16,16,"APP Erase Successed!");
applenth=0;
}else
{
printf("没有可以清除的固件!\r\n");
//LCD_ShowString(30,210,200,16,16,"No APP!");
}
clearflag=7;//标志更新了显示,并且设置7*300ms后清除显示
}
if(keydown_data==KEY1_DATA)
{
printf("开始执行FLASH用户代码!!\r\n");
if(((*(vu32*)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000)//判断是否为0X08XXXXXX.
{
iap_load_app(FLASH_APP1_ADDR);//执行FLASH APP代码
}else
{
printf("非FLASH应用程序,无法执行!\r\n");
//LCD_ShowString(30,210,200,16,16,"Illegal FLASH APP!");
}
clearflag=7;//标志更新了显示,并且设置7*300ms后清除显示
}
if(keydown_data==KEY3_DATA)
}
}
}
该段代码,实现了串口数据处理,以及 IAP 更新和跳转等各项操作。Bootloader 程序就设计完成了,但是一般要求 bootloader 程序越小越好(给 APP 省空间),所以,本章我们把一些不需要用到的.c 文件全部去掉。
下载验证
第一步,烧录BootLoader程序到单片机里面
观察现象:
第二步:先用电脑作为上位机,通过串口发送APP程序给单片机:
第三步:按下按键1将固件写入到FLASH中
第四步:按下按键2,看看程序是否从BootLoader程序跳到APP程序
总结
可以看到,现象成功,证明代码没有问题,成功将串口接收到的bin文件烧录到FLASH里面,并且成功从BootLoader程序跳到APP程序。
ESP8266
简介
ESP8266 是一款低成本、高性能的Wi-Fi模块,由乐鑫(Espressif Systems)开发。它集成了处理器和Wi-Fi模块,广泛应用于物联网设备、智能家居、传感器网络等领域。主要特点包括:
1.处理器: ESP8266集成了Tensilica L106 32位处理器,时钟频率为80MHz或者160MHz。
2.Wi-Fi功能: 支持802.11 b/g/n协议,可以作为Wi-Fi客户端或者热点模式运行,支持TCP/IP协议栈,可以连接到互联网或者本地网络。
3.低功耗: 在待机模式下,功耗非常低,适合电池供电的应用场景。
4.丰富的GPIO: ESP8266具有多个通用IO口,可以连接外部设备,如传感器、执行器等。
5.易于开发: 提供了丰富的开发工具和资源,支持多种编程语言和开发环境,如Arduino IDE、MicroPython等。
6.ESP8266和STM32通常在物联网应用中合作,ESP8266负责Wi-Fi连接和数据传输,而STM32负责处理和控制设备的各种功能。这种分工使得系统既能保持低功耗和高效率,又能满足复杂的物联网应用需求。
前面我们用电脑作为上位机,通过串口将APP程序传给STM32,这里,我们通过ESP8266,通过远程传输,将APP程序传到STM32里面。
AT指令
我们烧录ESP8266的官方固件,就可以通过AT指令去控制ESP8266。
烧录固件
我们去下载官方的烧录软件
选择合适的固件(去找你买ESP8266的厂家要)
选中合适的COM号,然后点击START,就可以开始烧录官方的固件。
传输原理
我们简单看一下使用说明,可以发现有很多种传输模式,我们这里用TCP传输举例子。
单连接TCP Client
这里有一点需要注意,当esp8266接收到服务器的信息之后,他会通过串口将收到的信息发送回上位机,我们要注意的是,他会回多一个:/r/n+IPD,n: 这不是我们需要的,我们要的是APP1程序的bin文件,所以,我们要把这个进行代码上的移位,不把他擦写到flash里面。
这样子,FLASH的0x08010000开始存放的,都是APP1的程序。
实现流程
前面用串口模拟远程烧录只是为了证明我们BOOTLOADER程序可以成功实现,现在我们需要在原来的基础上,用8266远程烧录,现在我们所说的一切,都是基于STM32角度的,我们编写的代码全是STM32.
串口
我们这里需要用两个串口,一个串口(usart2)负责STM32和ESP8266通信,一个串口负责和电脑通信(usart1)。
因为这里是用串口2和8266通信,所以意味着,APP1的程序文件,是用串口二接收的,所以要改到前面的串口代码,把串口2收到的bin文件存放在[u8 USART_RX_BUF[USART_REC_LEN] __attribute__ ((at(0X20001000)));//接收缓冲,最大USART_REC_LEN个字节,起始地址为0X20001000.]里面,具体代码,交给读者自行编写,因为不是很难。
串口二和串口一的基本逻辑是一样的,写法几乎一致,只是句柄、缓存数组和一些定义会有所区别,不过注意,我们这里的printf用的是串口一,因为我们要打印消息给电脑,来知道程序运行状况。
ESP8266代码编写
我们要编写STM32发送AT指令给ESP8266。
#include "ESP8266.h"
//发送命令给ESP8266
void ESP8266_SendCommand(const char* command)
{
HAL_UART_Transmit(&UART2_Handler, (uint8_t *)command, strlen(command), 10000);
}
//连接wifi
void ESP8266_ConnectWiFi(const char* ssid, const char* pass)
{
char cmd[100];
// 发送命令:AT+CWJAP="YourWiFiSSID","YourWiFiPassword"
sprintf(cmd, "AT+CWJAP=\"%s\",\"%s\"\r\n", ssid, pass);
ESP8266_SendCommand(cmd);
}
//连接TCP服务器
void ESP8266_ConnectTCPServer(const char* ServerIP, int ServerPort)
{
// 定义一个缓冲区用于存储命令字符串
char cmd[100];
// 发送连接到TCP服务器的命令
// 例如:AT+CIPSTART="TCP","192.168.1.100",80
sprintf(cmd, "AT+CIPSTART=\"TCP\",\"%s\",%d\r\n", ServerIP, ServerPort);
ESP8266_SendCommand(cmd);
}
//发送数据到TCP服务器
void ESP8266_SendToTCPServer(const char* txData)
{
// 发送命令:AT+CIPSEND=<length>
char cmd[100];
sprintf(cmd, "AT+CIPSEND=%d\r\n", strlen(txData));
ESP8266_SendCommand(cmd);
HAL_Delay(100);
// 发送数据
ESP8266_SendCommand(txData);
}
//关闭TCP服务
void ESP8266_CLOSETCP(void)
{
ESP8266_SendCommand("AT+CIPCLOSE\r\n");
}
void ESP9266_Init(void)
{
//延迟十秒,因为复位的时候,esp8266会发一点没用的东西,我们选择忽视
//切记,要把串口二收到的垃圾清空
HAL_Delay(10000);
//设置工作模式
ESP8266_SendCommand("AT+CWMODE=3\r\n");
HAL_Delay(10000);
//连接wifi
ESP8266_ConnectWiFi("chenjiajun2","12345678");
HAL_Delay(20000);
//连接TCP服务器
ESP8266_ConnectTCPServer("10.201.150.216",8080);
HAL_Delay(10000);
//清掉串口2收到的东西
memset(receive2_str, 0, sizeof(receive2_str));//清空接收到的数据;
uart2_byte_count=0;
}
我写的代码其实很不好!!!因为我用了延迟来无视8266发回给我的信息,正确的是,应该去判断8266回我的信息,然后再根据这个,去决定之后怎么发送指令。
现象观察
我们接好线,8266的串口接到STM32的串口2,STM32的串口一和电脑连接(ch340)。
我们复位STM32,通过串口观察串口一和串口二。可以发现,ESP8266成功连接到TCP服务器。
我们将APP1的bin文件,通过服务器发送给ESP8266。
然后串口一打印,接收到程序文件。
我们摁下按键,将程序擦写到FLASH里面,然后我们在摁下按键,跳转到APP程序里面看一下。
实验成功,成功实现远程烧录。
总结
至此,STM32成功实现了远程烧录,这其中主要知识点就是STM32的启动机制、BootLoader程序、ESP8266基本的AT指令使用,和部分外设(串口、按键、FSMC驱动TFT LCD)等......,我自己去研究这个远程烧录的时候,也学了很多东西,因为遇到了不少bug,但是遇到一个问题就去解决一个问题,我们就能不断的进步。