项目介绍:本项目从零编写引导加载程序(Bootloader)实现IAP(本项目为BL初版程序,不涉及各种中间件的使用),使用四块区域进行----24c02负责存储版本号,需要更新的程序长度,更新标志位、w25q64负责存储多块更新程序,按需求存储和调用、内部FLASH划分为两块空间,B区存储BL程序,A区用来存放和更新主程序同时,通过map⽂件来观察flash存储中Bootloader区的内存占⽤情况,决定使⽤配置条件变量、printf覆写、CRC32模⼆除法三种⽅式,来优化Bootloader的内存占⽤⼤⼩。使用Xmodm通信协议,允许上位机通过UART与MCU进⾏通信,以执⾏固件升级、擦除、重启系统、选择存储外部FLASH位置、外部FLAHS调用程序等操作。定义了数据包状态机,⽤于逐步解析数据,获取当前状态。使⽤CRC32校验起始标志位来保证数据的安全传输。ps:(文件代码附有可以验证支持Xmodm协议的软件,BIN文件,和整个程序源码(keil下直接编译即可))

下图为流程图,只是简单的指出了一二指令,其他指令看源码

下图为程序现象,这个串口软件不支持GB解码出现乱码我会解释作用。👇

一.串口的中断触发

        一般我们都是使用TXE或者RXNE来触发中断,其实还有完整传输结束的TC标志位和接收完成的IDLE标志位

        这两个标志位有些不同,RXNE标志位只需要读取寄存器就会自行清除,但是这两个需要读取两个,拿IDLE举例子

这里需要这么写,才能清理标志位

二.关于DMA提前人为中断

        如果手动disable了DMA,那么TCIF标志位会立马挂起

但是如果不清除该标志位,DMA就无法启动

所以我们再配置好DMA后,启动前,要清理标志位,1,2两种清理方式,任意一种都行

三.3种关于定义print导向串口输出的方法

1.覆写库函数

基础概念

关于怎么覆写标准库里面的原函数_c语言编译,怎么覆盖库文件的函数-CSDN博客

我们首先需要点开keil中的MicroLIB库

什么是MicroLIB库,为什么需要点开

这是keil的精简C语言库,代码量少很多,他使用是了标准库的头文件接口,接口在编译阶段会被链接到选择的库(比如 MicroLIB)的 实现部分。

同时他的底层实现函数和标准库的不同

这里我们需要重新定义他的底层fputc()函数,一下定义即可

2.使用sprintf打印到字符数组,再用串口发送字符数组,此方法打印到字符数组

3.将sprintf函数封装起来,实现专用的printf,此方法就是把方法2封装起来

方法三涉及到可变参函数,这里介绍说明如何使用和原理

四.可变参函数

1.我们首先需要添加<stdarg.h>头文件,包括一下宏

首先类似void Serial_Printf(char *format, ...),这种带有....的函数就是,可变参函数,他允许你

👆这些操作都是合理的

2.实现原理
 

首先每个函数的调用就会构建一个属于自己的栈空间,存放自己的变量

va_list args;可以理解为生成了一个栈指针

va_start(arg, format);可以理解为将栈指针指向format后

所以使用va_arg(arg, int);取出数据的时候,会直接取走format后面的int类型数据,类似👇操作

 va_end(arg);就是清理掉之前的指针

这样上面的代码就很简单可以看懂了 ,下面的这个代码也能轻松看懂使用了

五.串口传递的底层原理

我们都知道串口传输的是二进制数据,那么

比如我使用sprintf(send,“%d,%c,%x”,0x21,0x21,0x12),然后使用串口发送send,for(uint8_t i = 0;i<strlen((char*)send);i++){usart_senddata(send[i]);while(等待发送完成)},来发送,结果会是什么呢?

"33,!,21"

是不是很奇怪?为什么结果是不一样的,明明传输的都是0x30,转化了不同的格式,底层得到二进制应该都是一样的才对啊

解答:%d%c%x 等格式符的本质是

它们只是告诉 sprintf()printf()

请把这个值转成“某种格式”的“字符表示形式”❞

而不是“按什么进制去储存数据”。

下面我们具体分析,首先我们看看ASCII码表

我们发现这些结果的底层二进制是一样的,串口显示不同的原因就在于他不是把33对应的ASCII码存入进send里面,而是将33分为字符'3''和'3'寻找对应的ASCII码对应的二进制,存入,所以串口输出的就是3 3两个字符

我们假设调整串口的输出是HEX模式输出,那么结果会是3333 21 3231,对照下面的ASCII码表就一目了然了

+

六.各种字符类型和存储器的底层原理

先说结论,内存不关心你里面放置的内容是什么类型,他只关心内存是否够用

我们看看这个例子,加上format和list里面的内容是char类型的

uint8_t string[50];

sprintf((char*)string, format, list);

上面这个代码里面将原本的char类型放入uint8类型的空间中,这里是不会报错的,因为大小都是一个字节。那么这个操作的目的是什么呢,首先char类型的范围是-127--127,uint8范围是0--255范围,我开始传入的都是char类型的字符,如果不转换,会丢失数据!!!

那么是不是我使用char*就改变了内存缓存空间里面的内存布局?

并不是,因为都是一个字节所以没有一点影响,至于你使用int8类型读取里面数据,丢失范围是你自己的事情,内存不关心。发现没,这和我们之前说的uint8和uint32指针取内容是一个道理,8就是取一个字节,32就是取四个字节,只要你可以处理好内部关系就ok,内存只管空间够不够。

printf 输出格式总结(C语言)和C 中的类型提升规则、默认整型提升-CSDN博客

这就是为什么你可以通过不同的数据类型来读取同一块内存,只要你确保数据的存储格式符合预期,并且在读取时能正确处理这些类型的内存表示。

七.硬件SPI配置

初始化后一定要把SS引脚设置为高电平,不然无法启动,真傻逼,这个没检查到......

八.为什么软件I2C需要在里面添加延时函数而SPI不需要

应为芯片捕获频率和I2C需要实现半双工同时经常总线仲裁,来回在主机从机之间切换,这些功能都不允许I2C使用推挽输出,而使用开漏输出会倒是上升沿缓慢👇

从而I2C是频率远低于SPI的频率,需要加入延时函数来人为降低频率(I2C:Khz,SPI:Mhz),SPI只需要简单的几次分频就可以满足使用要求。

八.关于EEPROM芯片(24C02C)I2C读写的神坑

情况:我手写软件读写I2C代码,发现无论如何都无法写入后读取数据,不论写入什么都是0XFF

我使用多个版本的I2C进行检查,发现都是一样结果,判断是硬件问题。

等了三天,新硬件到了依旧有这个问题,开始思考......

        测试代码如下,每个函数里面都包装了延时10us符合I2C时序,首先测试一区的ACK返回值,发现都有回应,结果都是00,但是写入结果依旧是0XFF。

        开始测试二区代码,发现一区二区代码一起打开,二区代码ACF返回都是01,说明二区代码无响应。

        注释掉一区代码,单独测试二区代码,发现响应正常都为00,说明一区二区代码都是正确

        开始思考是都是延迟的问题,开始在中间不断加大延时函数,发现加到5MS接收完全正常

下面是GPT回答 👇

总结:EEPROM芯片没有在手册中写出,写入操作芯片需要额外的5ms进行操作,但是默认都是需要的,神坑啊!!!

九.使用汇编指令配置SP指针

上面这串代码一一拆解

ps:汇编函数和普通的C函数在栈空间的管理和函数返回上是一样的,唯一的区别是函数的内部实现各种寄存器返回需要自己细节处理好。

一. 标题 __asm(ARMCC 特有)

二.MSR指令和输入参数对应寄存器

MSR(Move to Special Register):用于把值写入系统寄存器。

输入参数默认第一个对应R0,第二个对应R1以此类推

三.BX指令,B指令和BL指令的区别

指令 是否保存返回地址? 能否返回? 通常用途
B label ❌ 否 ❌ 否 死跳,无返回
BL label ✅ 是(保存到 LR ✅ 是(用 BX LR 函数调用
BX reg ❌ 否 ✅ 是(跳到你给的地址) 函数返回或跳转寄存器地址

简单来说就是B是直接跳转,BL是跳转后将返回地址保存到LR寄存器中,BX可以直接跳转到LR寄存器(B只能跳转固定地址,不能跳转到寄存器的地址)

 

所以我们使用跳转寄存器的时候通用为BX或者BL,或者组合使用。

四.对于R14寄存器MSP(主堆栈指针)和PSP (进程堆栈指针)

 简单来说

十.如何调用PC指针

 一,如何使用PC指针指向中断服务函数

 

 如上图所示,其中的load_a(),就能直接调用PC指针指向中断服务函数。

如何实现的?

 首先PC指针是无法使用汇编指令直接调用的,他只保存了下一句要执行的语句地址,这里直接将地址赋值给load_a,然后直接调用load_a()

这里说个细节,最底层就是函数指针和结构体差不多,你赋给函数指针的值是这个函数栈的起始位置,函数里面所有执行的语句都是根据顺序指针向下执行的。所有执行空的load_a(),开始就会跳转到load_a的起始位位置去,这个位置刚好是中断服务函数,加上这个函数里面是空,执行完中断服务函数就返回了

二.sp和pc的区别

sp指针需要直接取值给sp,比如0x08000000位置对应的是__initial_sp,直接取*(uint32_t *)0x08000000的值赋值给sp,直接取 __initial_sp 这个 sp 指针要创建栈空间的位置,判断这个栈空间的位置是否在 sram 内。__initial_sp保存的是栈顶的地址

PC 指向一条指令地址,CPU 每执行一条指令,就自动让 PC 往下移动到下一条指令,形成连续执行流。Reset_Handler保存的是函数的起始地址

十一. A区的地址偏移

stm32F4 IAP实现原理讲解以及中断向量表的偏移_iap原理-CSDN博客

​​​​​​​https://blog.csdn.net/weixin_50127894/article/details/157210510?sharetype=blogdetail&sharerId=157210510&sharerefer=PC&sharesource=weixin_50127894&spm=1011.2480.3001.8118

十二.解决中文乱码的问题

STM32的串口数据打印汉字乱码的解决_串口打印乱码-CSDN博客

十三.数据是如何保存在FLASH和RAM中的

STM32中不同类型所存放区域大全_stm32 全局变量存在哪里-CSDN博客

上面是一个参考帖子,下面是我个人的总结👇,对照.map文件(连接器生成用来调试)和.sct文件(用来指定好代码要以什么方式保存在什么位置)

总结:主程序是保存在flash的.text段,这里包括了启动文件生成的中断向量表和main的源文件结合一块,前面可以很明显看出启动文件的.o文件位置,后面就是源文件的.text文件。

下面我们可以看到在flash中的常量段,和ram中的.data段和.bss段,还有最后的tact顶,中断向量表中的 __initial_sp就是指向这个位置 

同时如果我不使用malloc()、calloc()、realloc()进行分配,那么ram中就不会分配heap段空间,使用就会分配如下👇

总的来说,虽然主程序里面有所有的变量,但是处于内存分配使用的考虑,常量会放置在flash的.rodata中,全局变量会放置在ram的.data段中,局部变量只有在执行到函数时候才会初始化,加入栈空间。

十四.如何设置栈大小

在启动文件中有栈大小的设置,这个大小是所有栈的总和

如何判断我们最大栈需要多少空间呢,可以通过.htm文件来判断。一般在mdk的objects文件下。

打开后里面有估计最大使用空间是多少,我们根据内容配置即可。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐