STM32远程烧录程序

目录

简介

不同的程序下载方式

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) 从地址 0x00000000 处取出栈指针 MSP 的初始值,该值就是栈顶的地址。
(2) 从地址 0x00000004 处取出程序指针 PC 的初始值,该值指向复位后应执行的第一条指令。

这两个地址处存储的值通常是在嵌入式系统或者某些操作系统启动过程中使用的关键参数,具体作用如下:

1.栈指针 MSP 的初始值:
2.栈指针 MSP(Main Stack Pointer)是处理器用来管理栈空间的指针,它指向当前栈顶的地址。
3.在许多嵌入式系统中,系统启动时会从地址 0x00000000 处读取初始的栈指针值。这个值告诉处理器当前的栈顶在哪里,即初始时可用的栈空间范围。
4.栈指针的初始值是系统启动时的重要参数,确保函数调用和中断处理等操作都能正常进行。
5.程序指针 PC 的初始值:
6.程序计数器 PC(Program Counter)是一个寄存器,存储当前正在执行的指令的地址。
7.在系统复位后,处理器需要知道从哪里开始执行代码。地址 0x00000004 处通常存储了复位后应该执行的第一条指令的地址。
8.这个值告诉处理器从哪里开始执行代码,确保系统复位后可以顺利进入正常的程序执行流程,而不是随机执行内存中的数据。

总结起来,这些地址处存储的初始值对于系统启动和复位后的正常运行至关重要。栈指针 MSP 确保了栈空间的正常分配和管理,而程序指针 PC 则确保了系统复位后可以从正确的位置开始执行程序。

 

虽然内核是固定访问 0x00000000 0x00000004 地址的,但实际上这两个地址可以被重映射到其 它地址空间。以 STM32F103 为例,根据芯片引出的 BOOT0 BOOT1 引脚的电平情况 , 这两个 地址可以被映射到内部 FLASH、内部 SRAM 以及系统存储器中 ,不同的映射配置见表 BOOT 引脚的不同设置对 0 地址的映射

存储器组织

这里以STM32F103C8T6系列来进行举例。

程序存储器、数据存储器、寄存器和输入输出端口被组织在同一个4GB的线性地址空间内。
数据字节以小端格式存放在存储器中。一个字里的最低地址字节被认为是该字的最低有效字节,而最高地址字节是最高有效字节。

存储器映像

寄存器映像和位段不影响我们本篇使用BootLoader,所以这里就不去列举了。我们这里重点讲解嵌入式闪存(FLASH)和嵌入式(SRAM)。

嵌入式SRAM
STM32F10xxx 内置 64K 字节的静态 SRAM 。它可以以字节、半字 (16 ) 或全字 (32 ) 访问。
SRAM 的起始地址是 0x2000 0000

告诉编译器RAM的起始地址和大小。 

嵌入式FLASH

我们这里介绍一下各个STM32芯片叫法背后的区别:

 内部FLASH的构成

STM32 的内部 FLASH 包含 主存储器、系统存储器以及选项字节区域 ,它们的地址分布及大小见表STM32 大容量产品内部 FLASH 的构成 (在《 STM32 参考手册》中没有关于其内部 FLASH
说明,需要了解这些内容时,要查阅《 STM32F10x 闪存编程参考手册》)。

 各个存储区域的说明如下:

主存储:
一般我们说 STM32 内部 FLASH 的时候,都是指这个主存储器区域,它是存储用户应用程序空间,芯片型号说明中的 256K FLASH 512K FLASH 都是指这个区域的大小。
主存储器分为 256 页,每页大小为 2KB ,共 512KB 。这个分页的概念,实质就是 FLASH 存储器的扇区,与其它 FLASH 一样,在写入数据前,要先按页(扇区)擦除。
注意上表中的主存储器是本实验板使用的 STM32ZET6 型号芯片的参数,即 STM32F1 大容量产品。若使用超大容量、中容量或小容量产品,它们主存储器的页数量、页大小均有不同,使用的时候要注意区分。
系统存储区
系统存储区是用户不能访问的区域,它在芯片出厂时已经固化了启动代码 ,它负责实现串口和 USB以及CAN ISP 烧录功能。
选项字节
选项字节用于 配置 FLASH 的读写保护、待机/停机复位、软件/硬件看门狗等功能 ,这部分共 16字节。可以通过修改 FLASH 的选项控制寄存器修改。

IAP 简介

前面是进行远程烧录的基础知识讲解,我们本篇的重点是IAP。
IAP In Application Programming )即在应用编程, IAP 是用户自己的程序在运行过程中对User Flash 的部分区域进行烧写,目的是为了在产品发布后可以方便地通过预留的通信口对产品中的固件程序进行更新升级。 通常实现 IAP 功能时,即用户程序运行中作自身的更新操作,需要在设计固件程序时编写两个项目代码,第一个项目程序不执行正常的功能操作,而只是通过某种通信方式(如 USB、USART)接收程序或数据,执行对第二部分代码的更新;第二个项目代码才是真正的功能代码。 这两部分项目代码都同时烧录在 User Flash 中,当芯片上电后,首先是第一个项目代码开始运行,它作如下操作:
1)检查是否需要对第二部分代码进行更新
2)如果不需要更新则转到 4)
3)执行更新操作
4)跳转到第二部分代码执行
        第一部分代码必须通过其它手段,如 JTAG ISP 烧入;第二部分代码可以使用第一部分代码 IAP 功能烧入,也可以和第一部分代码一起烧入,以后需要程序更新时再通过第一部分 IAP代码更新。
        我们将第一个项目代码称之为Bootloader程序,第二个项目代码称之为APP程序 ,他们存放在 STM32 FLASH 的不同地址范围,一般从最低地址区开始存放 Bootloader ,紧跟其后的就是APP 程序(注意,如果 FLASH 容量足够,是可以设计很多 APP 程序的,本章我们只讨论一个APP 程序的情况)。这样我们就是要实现 2 个程序: Bootloader APP
STM32 的 APP 程序不仅可以放到 FLASH 里面运行,也可以放到 SRAM 里面运行,本章, 我们将制作两个 APP ,一个用于 FLASH 运行,一个用于 SRAM 运行。
 

STM32启动流程

我们先来看看 STM32 正常的程序运行流程(FLASH启动):

 STM32 的内部闪存(FLASH)地址起始于 0x08000000,一般情况下,程序文件就从此地址开始写入。此外 STM32 是基于 Cortex-M3 内核的微控制器,其内部通过一张“中断向量表” 来响应中断,程序启动后,将首先从“中断向量表”取出复位中断向量执行复位中断程序完成启动,而这张“中断向量表”的起始地址是 0x08000004,当中断来临,STM32的内部硬件机制亦会自动将PC指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断服务程序。

        在图中, STM32 在复位后,先从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,如图标号①所示;在复位中断服务程序执行完之后,会跳转到我们的main 函数,如图标号②所示;而我们的 main 函数一般都是一个死循环,在 main 函数执行过程中,如果收到中断请求(发生重中断),此时 STM32 强制将 PC 指针指回中断向量表处,如图标号③所示;然后,根据中断源进入相应的中断服务程序,如图标号④所示;在执行完中断服务程序以后,程序再次返回 main 函数执行,如图标号⑤所示。
当加入 IAP 程序之后,程序运行流程如图所示:

         在图所示流程中,STM32 复位后,还是从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到 IAP main 函数,如图标号①所示,此部分同图 52.1.1 一样;在执行完IAP以后(即将新的APP代码写入STM32的 FLASH,灰底部分。新程序的复位中断向量起始地址为 0X08000004+N+M),跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的 main 函数,如图标号②和③所示,同样 main 函数为一个死循环,并且注意到此时  STM32  的FLASH在不同位置上,共有两个中断向量表。

         在 main 函数执行过程中,如果 CPU 得到一个中断请求,PC 指针仍强制跳转到地址为 0X08000004 中断向量表处,而不是新程序的中断向量表 ,如图标号④所示; 程序再根据我们设 置的中断向量表偏移量,跳转到对应中断源新的中断服务程序中 ,如图标号⑤所示;在执行完中断服务程序后,程序返回 main 函数继续运行,如图标号⑥所示。
通过以上两个过程的分析,我们知道 IAP 程序必须满足两个要求:
1) 新程序必须在 IAP 程序之后的某个偏移量为 x 的地址开始;
2) 必须将新程序的中断向量表相应的移动,移动的偏移量为 x;
本篇,我们有 2 APP 程序,一个为 FLASH APP ,程序在 FLASH 中运行,另外一个SRAM APP ,程序运行在 SRAM 中,图 虽然是针对 FLASH APP 来说的,但是在SRAM 里面运行的过程和 FLASH 基本一致,只是需要设置向量表的地址为 SRAM 的地址。

APP程序

APP 程序起始地址设置方法

FLASH APP 的起始地址设置:
随便打开一个之前的实例工程,点击 Options for Target Target 选项卡,如图 所示:

        默认的条件下, 图中 IROM1 的起始地址(Start)一般为 0X08000000 ,大小( Size) 0X80000 ,即从 0X08000000 开始的 1024 K 空间为我们的程序存储(因为我们 STM32F407ZGT6的 FLASH大小是 1024K )。而图中,我们设置起始地址( Start )为 0X08010000 ,即偏移量为 0X10000 (64K字节),因而,留给 APP 用的 FLASH 空间只有 0X100000-0X10000=0XF0000(960K 字节)。设置好 Start 和 Szie,就完成 APP 程序的起始地址设置。
         这里的 64K 字节,需要大家根据 Bootloader 程序大小进行选择 ,比如我们本章的 Bootloader程序为 20K 左右,理论上我们只需要确保 APP 起始地址在 Bootloader 之后,并且 偏移量为0X200的倍数 即可(相关知识,请参考:http://www.openedv.com/posts/list/392.htm )。这里我们选择 64K(0X10000 )字节,留了一些余量,方便 Bootloader 以后的升级修改。

SRAM APP,那么起始地址设置如图所示:
这里我们将 IROM1 的起始地址(Start)定义为:0X20001000,大小为 0XC000(48K 字节),
即从地址 0X20000000 偏移 0X1000 开始,存放 APP 代码。因为整个 STM32F103ZET6 SRAM 大 小 为 64K 字 节 , 所 以 IRAM1 SRAM ) 的 起 始 地 址 变 为 0X2000D000( 0x20001000+0xC000=0X2000D000 ), 大 小 只 有 0X3000 12K 字 节 )。 这 样,,整 个STM32F103ZET6 的 SRAM 分配情况为:最开始的 4K Bootloader 程序使用,随后的 48K 存放APP 程序,最后 12K ,用作 APP 程序的内存。这个分配关系大家可以根据自己的实际情况修改,不一定和我们这里的设置一模一样,不过也需要注意,保证偏移量为 0X200 的倍数(我们这里为 0X1000 )。
后续我的代码会只有FLASH的APP,因为在实际应用当中,由于FLASH的容量大于SRAM,APP程序通常还是存在FLASH中,所以我们不去讨论SRAM,当然,机制都是一样的,学会FLASH的APP,自然而然的也能弄出存在SRAM的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() 函数。

systemInit 函数不仅初始化时钟系统,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存储器中。通过这种方式,可以在不同的嵌入式系统配置中灵活地设置向量表的位置,以适应特定的硬件设计和需求。

从代码可以理解 , VTOR 寄 存 器 存 放 的 是 中 断 向 量 表 的 起 始 地 址 。VECT_TAB_SRAM是没有定义 , 所以执行 SCB->VTOR = FLASH_BASE|VECT_TAB_OFFSET; 对于 FLASH APP ,我们设置为 FLASH_BASE+ 偏移量 0x10000, 所以我们可以在 FLASH APP的 main函数最开头处添加如下代码实现中断向量表的起始地址的重设:
SCB->VTOR = FLASH_BASE | 0x10000;

以上是 FLASH APP 的情况,当使用SRAM APP的时候,我们设置起始地址为:

SRAM_bASE+0x1000, 同样的方法,我们在 SRAM APP main 函数最开始处,添加下面代码:
SCB->VTOR = SRAM_BASE | 0x1000;

这样,我们就完成了中断向量表偏移量的设置。

        通过以上两个步骤的设置,我们就可以生成 APP 程序了,只要 APP 程序的 FLASH SRAM大小不超过我们的设置即可。

不过 MDK 默认生成的文件是.hex 文件,并不方便我们用作IAP更新,我们希望生成的文件是.bin 文件,这样可以方便进行 IAP 升级。bin文件和hex文件最大的区别就是hex文件会附带下载的地址,而bin文件则没有。

         这里我们通过 MDK 自带的格式转换工具 fromelf.exe,来实现.axf 文件到.bin 文件的转换。该工具在 MDK 的安装目录\ARM\BIN40 文件夹里面。

本章,我们通过在 MDK 点击 Options for TargetUser选项卡,在After Build/Rebuild栏, 勾选 Run #1,并写入:D:\tools\mdk5.14\ARM\ARMCC\bin\fromelf.exe --bin -o ..\OBJ\RTC.bin ..\OBJ\RTC.axf。如图所示:

通过这一步设置,我们就可以在 MDK 编译成功之后,调用 fromelf.exe,在得到.bin 文件之后,我们只需要将这个 bin 文件传送给单片机,即可执行 IAP 升级。
我们这里看一下APP程序的现象:

BootLader程序 

功能简介:

我们先用电脑作为上位机,通过串口来对STM32进行APP程序的下载。

本章实验( Bootloader 部分)功能简介:开机的时候先显示提示信息,然后等待串口输入接收APP 程序(无校验,一次性接收),在串口接收到 APP 程序之后,即可执行 IAP。我们通过KEY0,当key0按下之后,将串口接收到的 APP 程序存放到 STM32 FLASH,之后在按下KEY1既可以执行这个 FLASH APP 程序,通过 KEY2 按键,可以手动清除串口接收到的 APP程序, DS0 用于指示程序运行状态。

本实验用到的资源如下:
1、3个按键。
2、串口。
3、TFTLCD模块。
4、 指示灯 DS0。
这些用到的硬件,我们会在程序中用函数封装好,这里就不再介绍了。

软件设计

我自己写好了程序,如果读者需要的话,可以在评论区或者私聊我,我看到之后,会把工程分享出去。

由于我们要擦除和写入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, &sectorError); // 擦除操作

                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 组,在组下面添加了 iap.c 文件以及其头文件isp.h。

 打开 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) &amp; 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,它检查给定的应用程序起始地址的栈顶地址是否有效,然后设置函数指针和堆栈指针,并最终通过函数指针调用实现了应用程序的跳转和执行。这种方式通常用于实现固件升级或者在运行时加载并执行新的应用程序。
 

该文件总共只有2 个函数,其中, iap_write_appbin 函数用于将存放在串口接收 buf 里面的APP 程序写入到 FLASH iap_load_app 函数,则用于跳转到 APP 程序运行,其参数 appxaddr为 APP 程序的起始地址,程序先判断栈顶地址是否合法,在得到合法的栈顶地址后,通过MSR_MSP 函数(该函数在 sys.c 文件)设置栈顶地址,最后通过一个虚拟的函数( jump2app) 跳转到 APP 程序执行代码,实现 IAP 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
这部分代码比较简单。本章,我们是通过串口接收 APP 程序的,我们将 usart1.c usart1.h做了稍微修改,在 usart.h 中,我们定义 USART_REC_LEN 55K 字节,也就是串口最大一次可以接收55K 字节的数据,这也是本 Bootloader 程序所能接收的最大 APP 程序大小。然后新增一个USART_RX_CNT 的变量,用于记录接收到的文件大小,而 USART_RX_STA 不再使用。
打开 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); 
} 
可以看到我们修改 HAL_UART_RxCpltCallback部分代码如下:

 这里,我们指定 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,但是遇到一个问题就去解决一个问题,我们就能不断的进步。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/769677.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【JVM系列】内存泄漏

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

Python数据分析-股票数据分析(GARCH模型)

一、研究背景 随着金融市场的不断发展和全球经济的日益复杂&#xff0c;市场波动性和风险管理成为投资者和金融机构关注的焦点。波动率是衡量市场风险的重要指标&#xff0c;准确预测和评估波动率对于资产定价、风险控制和投资决策具有重要意义。在金融时间序列分析中&#xf…

唐山养老院哪家好---老了怎么过?到这里,享受生活的每一刻!

随着时间的流逝&#xff0c;我们每个人都将迎来老年时光&#xff0c;而"老了&#xff0c;怎么过&#xff1f;"这个问题&#xff0c;虽然简单&#xff0c;却深深触动了无数人的心。 面对老年生活&#xff0c;每个人都有不同的选择和追求。有的人选择顺其自然&#xf…

单目相机减速带检测以及测距

单目相机减速带检测以及测距项目是一个计算机视觉领域的应用&#xff0c;旨在使用一个摄像头&#xff08;单目相机&#xff09;来识别道路上的减速带&#xff0c;并进一步估计车辆与减速带之间的距离。这样的系统对于智能驾驶辅助系统&#xff08;ADAS&#xff09;特别有用&…

新章节:全设备通用调度算法-通讯重构

新章节&#xff1a;全设备通用调度算法-通讯重构 文章目录 新章节&#xff1a;全设备通用调度算法-通讯重构前言一、重构了TCP和UDP通讯二、优化了OPC和OPCUA三、升级了监控客户端四、升级了通讯的图形化其他升级 前言 现在真的很懒也很少写代码了&#xff0c;写代码和更新进度…

Android 15 应用适配默认全屏的行为变更(Android V的新特性)

简介 Android V 上默认会使用全面屏兼容方式&#xff0c;影响应用显示&#xff0c;导致应用内跟导航标题重合&#xff0c;无法点击上移的内容。 默认情况下&#xff0c;如果应用以 Android 15&#xff08;API 级别 35&#xff09;为目标平台&#xff0c;在搭载 Android 15 的设…

【网络安全的神秘世界】SQL注入(下)

&#x1f31d;博客主页&#xff1a;泥菩萨 &#x1f496;专栏&#xff1a;Linux探索之旅 | 网络安全的神秘世界 | 专接本 | 每天学会一个渗透测试工具 3.7 二次注入 不好挖这个漏洞&#xff0c;需要搞懂业务逻辑关系 二次注入通常是指在存入数据库时做了过滤&#xff0c;但是取…

vue中自定义设置多语言,并且运行js脚本自动生成多语言文件

在项目中需要进行多个国家语言的切换时&#xff0c;可以用到下面方法其中一个 一、自定义设置多语言 方法一: 可以自己编写一个设置多语言文件 在项目新建js文件&#xff0c;命名为&#xff1a;language.js&#xff0c;代码如下 // language.js 文档 let languagePage {CN…

Rocky Linux yum/dnf repo/mirrors 国内镜像列表及更换方法

Rocky Linux yum/dnf repo/mirrors 国内镜像列表及更换方法 Rocky Linux Mirrors 请访问原文链接&#xff1a;https://sysin.org/blog/rocky-linux-mirrors/&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。 作者主页&#xff1a;sysin.org 高校镜像推荐 mirr…

聚观早报 | 微信升级视频功能;一加Ace 3 Pro开启销售

聚观早报每日整理最值得关注的行业重点事件&#xff0c;帮助大家及时了解最新行业动态&#xff0c;每日读报&#xff0c;就读聚观365资讯简报。 整理丨Cutie 7月4日消息 微信升级视频功能 一加Ace 3 Pro开启销售 享界S9将亮相门店 澎湃OS出行助手将陆续升级 特斯拉公布二…

DDOS攻击不懂?看完让你秒懂!

什么是DDOS攻击&#xff1f; DDoS攻击&#xff0c;全称分布式拒绝服务攻击&#xff08;Distributed Denial of Service attack&#xff09;&#xff0c;是一种常见的网络安全攻击方式。以下是对DDoS攻击的详细解释: DDoS攻击是指攻击者利用大量被控制的计算机或设备&#xff…

如何查看JDK使用的垃圾收集器

在Java开发中&#xff0c;垃圾收集器&#xff08;Garbage Collector, GC&#xff09;是管理内存的关键组件&#xff0c;不同的垃圾收集器对应用性能有着显著影响。了解当前JDK使用的垃圾收集器对于性能调优至关重要。本文将介绍如何查看JDK使用的垃圾收集器。 JDK中的垃圾收集…

提升Android Studio开发体验:使用Kelp插件实现颜色和图标预览

提升Android Studio开发体验&#xff1a;使用Kelp插件实现颜色和图标预览 在Android开发中&#xff0c;自动补全功能对于提高开发效率至关重要。然而&#xff0c;默认的Android Studio并不能预览颜色和图标&#xff0c;这使得开发者在选择资源时常常感到困惑。本文将介绍如何使…

Linux中cat命令的英文含义

我之前一直在想cat不是猫的意思吗&#xff0c;但是cat命令在Linux中并不是指"猫"这个动物&#xff0c;而是来源于它的功能&#xff1a;concatenate&#xff08;连接&#xff09;和typeset&#xff08;打印&#xff09;。这个命令的名称是这两个功能的首字母缩写。尽管…

「电子数据」龙信助力鞍山市公安局电子数据取证竞赛

文章关键词&#xff1a;电子数据取证、服务支持、电子数据取证竞赛服务、取证人才培养 为进一步推动电子数据取证分析专业人才队伍建设&#xff0c;不断提高电子数据取证分析能力&#xff0c;7月1日&#xff0c;鞍山市公安局网安支队举办了电子数据取证竞赛&#xff0c;并取得…

【SOLID原则前端中的应用】里氏替换原则(Liskov Substitution Principle,LSP)- vue3示例

里氏替换原则&#xff08;Liskov Substitution Principle&#xff0c;LSP&#xff09;规定&#xff0c;子类对象必须能够替换父类对象&#xff0c;并且程序的行为保持不变。 在Vue 3中&#xff0c;这意味着我们在创建可替换的组件时&#xff0c;应该确保子组件能够完全替代父组…

杭州叉车A38老款国二 车辆维修与保养

发动机参数 更换缸垫 故障表现特征&#xff1a; 1. 发动机发动风扇端有异响&#xff0c;喷气 2. 水箱加水后外冒有水气泡 车型&#xff1a; 新柴490B 预估市场平均价格&#xff1a; 25一个 发动机-气门间隙调整 气门间隙大小&#xff1a; 经询问相关师傅&#xff0c;此次调整…

物联网综合实验平台-物联网实验实训教学平台-物联网实验箱

物联网&#xff08;IoT&#xff09;是一种连接性技术&#xff0c;将各种设备和物品连接到互联网&#xff0c;通过数据的收集和分析实现更智能的决策和操作。在《“十四五”数字经济发展规划》中&#xff0c;物联网被明确提出&#xff0c;被视为数字经济时代的基础设施。当前&am…

【JavaWeb】利用IntelliJ IDEA 2024.1.4 +Tomcat10 搭建Java Web项目开发环境(图文超详细)

1、启动IntelliJ idea 2024.1.4 在欢迎页面&#xff0c;请确认好版本。因为不同的版本&#xff0c;搭建项目过程不太一样。 点击&#xff0c;新建项目。如图&#xff1a; 2、新建项目 在新建项目界面&#xff0c;选择java&#xff0c;在右侧信息模块内&#xff0c;根据个人情…

C语言中函数的声明和创建

C语言的函数创建和java函数有有一定的区别&#xff1a;如下示例&#xff1a; fun.h文件 fun.c文件 main.c文件 或这简单一点可以这样写&#xff0c; 声明和定义的差异&#xff1a; 函数分类