1 为什么我们需要Bootloader
bootloader需求其实可以千奇百怪,我今天要介绍的是,在汽车单片机领域经常碰到的一种情况。左图是在开发阶段的ECU,prototype上两种接口都可以拿来烧录程序,而且开发人员一般为了省事,都选择用JTAG烧录
但是一旦ECU进了生产线,整个外壳都封装好了,开发人员就再也没有JTAG可用了,这个时候就会出现一个问题,如何通过CAN(或者UART,USB)等接口把新的应用程序烧进去
这个时候大家需要注意,一般单片机除了JTAG或之类的ISP( In-System Programming)接口不需要额外的驱动,所有其他的普通接口,如CAN,UART,USB都需要有驱动程序,不然外面的设备无法与之相连。但如果你没有bootloader,而只有一个完整的application SW,这个时候你有且仅有一种选择,就是把整个程序从flash"上传"到RAM,然后通过RAM里的程序提供驱动,通过对外接口把新的程序重新烧录到flash,然后重启
这样做有两个很大的问题,一是一般ram比flash要小的多,几乎1/10都不到,如果应用程序太大,根本没法swap,即使可行,这个“上传”Flash 到RAM的驱动写起来也是相当麻烦
第二个问题更要命,一旦在新的程序烧录过程中出现断电,或者通讯中断之类物理异常,整个ECU就彻底报废了。
总之,我们除了正常的application程序,还需要一段引导程序,提供简单的驱动功能,让外界设备可以通过和bootloader对话,把新的application程序烧到指定的位置
2 硬件环境
首先我们要了解 Arduino Uno的硬件布局
通常我们有两种方式烧程序
1) 通过左面红色的USB接口
2) 通过右面黄色的ICSP接口
先说第二种,因为它相对简单,仔细看接口那几个pin, D13 D12 D11其实就是
SPI_SCK,
SPI_MISO
SPI_MOSI
用这个接口烧程序不需要bootloader。所以在我们学会写bootloader之前,我们要先明白,程序到底是怎么烧到单片机上的
3 烧录原理
翻开手册P. 223,这段文字提供了几个很重要的信息
1) 这个MCU具备自我实现download和upload代码机制,很多人认为这是一句废话,但是,如果我们仔细思考一下,如果没有这个机制,程序怎么可能通过SPI接口被“灌到”flash内存地址呢?
2) 程序(code)总是一页一页(page)被刷到芯片上去的,不能是一个bit或者一个byte这么刷。那么刷程序就要非常三个步骤
a) erase page
b) fill temporary page
c) Perform a page write
其中 b和 c的先后顺序不能颠倒, a)和c)的先后顺序不能颠倒
这里插播一个话题,利用这个接口烧程序的硬件,我用的是这个program flasher (AVRISP mkII)
这个蓝盒子里面其实是就是另一个单片机(还是AVR家族的),里面装载了USB接口和SPI的驱动程序,所以它可以把你电脑里面的hex文件通过USB转换成SPI信号然后通过ICSP接口download到Atmega328p
我前文反复强调这个“搬运”过程,这里就要看看我们Atmega328p的硬件架构是如何布局的
从上图看出,我们的代码从计算机被flash device通过SPI接口传输到芯片。但是注意,传输的第一步只能抵达data bus,就是那根黑粗线。我们要特别小心,flash memory根本就没有直接挂载在这跟data bus上面
根据架构的示意,芯片是通过运算器ALU发送特殊指令(SPM),由一个非常特殊的Z-register把data bus上的内容"搬到" Flash memory。
这个过程看上去有点复杂,其实这和我们平时把数据通过单片机下载到SD卡有点像。唯一的区别就是,SD卡我们需要SD的读写驱动(通常也是SPI),而这个地方,我们是用到一个叫做SPM的指令
在这个单片机有个单独的章节介绍这个SPM —— SPMCSR (SPM Control and Status Register)
这段文字如此之长,我自己都不建议大家详细去了解,总之有这么一个机制,能够通过一个所谓的Z-pointer,把data bus上的内容“搬到”Flash里面去。
现在我们看看这个芯片它到底怎么"搬"。大家要注意的是,以这种方式在烧录芯片的时候,芯片本身是没有任何program在跑的,既然是这样,这个SPMCSR寄存器又是怎么被读写的呢?这里面就要从芯片外部引入一个特殊的烧录指令
例如,当你要开始烧录的第一个指令,就是0xAC 0x53 0x00 0x00——即Programming enable,即SPMCSR里的SELFPRGEN被激活。这里有一点小吐槽,这款AVR芯片的架构图并没有表明,是哪个logic控制单元翻译了这个外部debugger传输信号的工作(我看过其他单片机都有特别说明)
我这里以 Read signature byte为例子,做了个小实验,下面是烧录机和芯片在烧录过程中的一段SPI通讯截图
我们可以看到,当烧录机(此处烧录机是Master,芯片是Slave)发送0x30指令请求(询问芯片签名),芯片在在三次询问中回答了自己的device signature
下面是芯片手册,我们看看结果对不对
同理,我们可以用其他指令分别做到烧录指令里面的其他事情,如擦写flash eeprom,fuse等。这里有个小地方要特别注意,有一种叫做lock bit的东西千万别乱动,这是给生产线预留的设置,一旦烧录死了,芯片的内容就再也无法变更
特此声明:截止2022-12-07,我并没有成功复现其他的擦写指令 (如0x4C)
我用logic analyzer一帧一帧反复核对protocol,只发现在烧录过程中的verify阶段,即
0x20 read flash memory low byte
0x28 read flash memory high byte
得到的结果是正确的,而0x4C(写flash memory)所对应的protocol,我始终没有找到,尚不知道什么原因
3 什么是hex文件
到目前位置,我们整个烧写过程是得到了IDE的帮助
IDE在烧程序之前,询问要downloading哪一个hex文件,点击烧录按键,hex文件就会被download到flash
我们可以用普通的notepad++暴力打开这个hex文件,如下
很多人有点摸不着头脑、以为这个文件就是一个普通的二进制,没错,但它不是普通的二进制文件,而是一种叫做intel 16进制编码 ——Hex-i
不懂的可自行wiki
https://en.wikipedia.org/wiki/Intel_HEX
也就是说,这个hex文件里面除了我们程序本身的内容,还包含了一项非常重要的信息,地址。为了更清楚明了的查看hex,我推荐大家用vector他们家的hexviewer来打开这个hex文件
在我们继续深挖这个hex内容之前,先数一数这个hex文件到底多大?
从build的结果来看,hex大小就是178byte
其中176byte在.text区,.data里面有2个byte,看来是某个全局变量
现在让我们来看hex文件具体内容
从上图可以看出,左边第一列就是程序所对应的地址,我们一排有16个8bit,每一个8bit代表,在此处的内存地址
第一排 第1个地址 对应的是 Flash 绝对地址0x0000 0000,它的内容是0x0C
(现在从左往右横着数)
第一排 第2个地址 对应的是 Flash绝对地址0x0000 0001,它的内容是0x94
...
第二排 第1个地址 对应的是 Flash 绝对地址0x0000 0010,它的内容是0x0C
...
...
...
最后一排 第1个地址 对应的 Flash绝对地址0x0000 00B0,它的内容是0xAB
...
好了,现在问题来了,这些0x0C,0x94,0x34,0x00 又是什么东西呢?他们真的毫无意义吗?
接下来让我么看看在我们编译(build)完成后,IDE为我们生成了那些有趣的文件。
首先让我们打开一个叫做.lss的文件,如下
你会发现,刚才我们在hex文件里面的东西,在这个文件里面都能找到踪迹
第一排包含了四个地址, 0x0000, 0x0001, 0x0002, 0x0003,里面的内容是0x0C 0x94 0x34 0x00
什么意思?原来就是汇编 jmp 0x68(跳转到Flash里面第0x68的地址)
我们现在看0x68,经过一系列骚操作,我们来到第0x8a这个地址,这里我们看见call 0x96
0x96又是什么? 原来这里就是传说中的main函数
看到这里,估计非常有经验的码农已经能从这段汇编代码猜出C-code长啥样了,如下
没错,我的c-code里面,除了一个main函数,几乎什么都没有,那个hex文件里最后两个地址0xB0和0xB1 里面的内容0xAB和0x00,就是这个全局变量被赋予的值,它也属于hex文件的一部分,但为什么我们在.lss文件里面找不到它呢?
原来它藏在这里
这个int var一共占有16bit(即两个byte),所以最后hex里面最后两个地址被程序直接拉到了SRAM(即.data区域),而我们SRAM的物理起始地址就是0x100, 所以0x100和0x101分别给了0xAB和0x00
这里有一个隐藏的知识点很不好理解,就是虚拟内存地址VMA (Virtual Memory Address)这个概念
__data_start的虚拟地址不是0x0100而是0x0080 0100,这个VMA被Atmel在关于linker script的官方文献里给了解释
这段话很绕,大意是说,在大多数情况下,局域变量的地址VMA(虚拟地址)和LMA(真实内存地址)是一个地址(即LMA——load memory address即真实物理SRAM地址)
但是,只有当全局变量的时候,ROM里面地址反而是LMA,而RAM映射的地址是VMA
4 编译原理
现在有趣的事情来了,我们这个看似啥也不干的c-code,为什么编译完了(build)以后,生成的hex就把 (0x0C, 0x94, 0x34, 0x00 即jmp 0x68 放到0x0000 0000 这个Flash里面的第一个地址呢?放别的地方不行吗?
回答这个问题之前,软件新人要彻底明白,IDE是什么,所谓的build到底是怎么回事?
网上有无数关于编译原理和过程的介绍,我挑一篇我觉得不错的放在这里
【嵌入式04】Linux GCC编译过程详解及ELF文件介绍
我们还是从们这个例子出发,看看编译4四部曲到底都干了什么
第一步 预处理pre-processing:
由于我们的例子里面既没有宏,也没有注释,所以这一步暂时可以跳过
第二步 编译compile:
从这个地方开始,很多人就开始犯糊涂,分不清compiler和assembler的区别。有的IDE根本就不提供这个中间状态,例如我这个Atmel Studio的IDE就不提供单纯的编译文件,你点击了build以后,也不会生成任何.s文件
现在我们要怎么执行第二步——编译呢?首先我们要了解一个叫做toolchain的东西
所谓工具链,就是从日常嵌入式开发工作需要的角度,我们把需要编写代码edit、编译compile、汇编assemble、linking、flashing、debugging等等整个过程所需要的所有工具像串萝卜一样串起来。我们的IDE就是把这些玩意儿打了个包,配上精美的GUI展现在你的面前。
现在很多软件小白开始接触的都是大型IDE,好处是你少操心,几分钟内就能从无到有,写点hello world之类的小玩意儿自娱自乐,但是只要进入工作领域,就发现大型IDE给你带来方便的同时,也给你上了一道无形的黄金枷锁,有时会带来无穷的烦恼,尤其是在非常底层的嵌入式领域。
这个时候就需要软件菜鸟大胆摆脱IDE这座温室,来到真实又残酷的丛林世界。要知道,上古时代,是没有什么IDE可以用的,人们编程需要严格按照上图里面的四个步骤一步一步进行
现在我们来看看,AVR给我们提供了什么样的工具链
单从这个工具链里面的文件名就能看出,这些工具和GCC的官方toolchain非常象。究竟哪一个才是compile(编译)工具呢?
特别注意:不要滥用avr-gcc.exe,这个文件其实不是一个单一工具,而是n种工具的集合体(类似一个batch file),AVR官方也给了说明,如下
现在我们在项目文件夹单独设立一个区域,看看手动编译的结果是什么
为了更清晰的认识到不同的compiler对程序编译的影响,我故意用普通GCC和AVR-GCC编译了main.c文件,从他们各自的.s文件来看,他们的编译文件内容非常不同
用普通GCC编译的.s文件里面描述的register都是x86_64架构的,而AVR-gcc针对性的描述了符合AVR架构的register(如__SP_H__, __SP_L__, r28, r29等只有AVR架构才有的特殊register)
所以我们在使用编译器的时候,一定要选择符合所编译芯片架构的甚至特定芯片类型的编译器——通常在编译器后面跟着的option里面体现,后文会提到
所以我们必须把目光聚焦在AVR-GCC编译的结果
通过汇编.s文件和反汇编文件我们能看出几点信息:
1) 纯编译.s文件完全没有地址这个概念
2)纯编译文件只有汇编语言,没有二进制(类似0x0C 0x94这种)
3) 反汇编里面一个片段和单个编译文件很像,我们会在第三步汇编阶段确认
3)反汇编文件内容比纯汇编文件要多
4)反汇编文件里面有地址这个概念
第三步 汇编assembling:
现在我们来看assemble后的结果是什么
这里面有两个步骤:
步骤一:汇编 通过avr-gcc.exe -c 这个指令编译刚才那个main.s,编译完后,会发现多了一个main.o文件。
特别提醒:此o非彼o,这里的main.o文件不是linux系统下面那个已经被linking好了的可执行文件,它只是一个单纯的汇编文件,是没有办法独立运行的
步骤二:通过反汇编指令 avr-objdump.exe -D 观察main.o里面的内容
我们发现,
1) 这个main.o文件里面首次出现了地址,但不是绝对地址
2) 二进制内容和.lss反编译文件的main片段大部分内容相同,但是还是有细微差别
现在我们需要静下来认真思考下面几个问题:
问题一:hex文件里面 绝对地址0x96前面的内容是谁写的?
问题二:是谁把main.o的二进制代码放到最终hex文件0x96这个地址的?不能放别的地址吗?
问题三:是谁在hex文件里的绝对地址0x80 call main函数?它怎么知道我们写的是main函数
问题四:为什么main函数里面的片段,关于全局变量的部分发生了细微变化
要回答上述问题,我们就要进入编译四部曲的最终章——linking(链接)
第四步:恐怖的大魔王——Linking(链接)
很多软件新人在这个地方就瘫痪了。因为在学这一章之前,必须要牢牢掌握几个基本知识点,不然学习曲线如此陡峭,难如上青天
1) makefile——不懂makefile的话会非常麻烦,首先IDE给你提供的makefile会看不懂,更不要说后面要自己写makefile
学makefile还有另外一个好处就是,发现一些被IDE隐藏起来的信息,尤其是一些标准库函数、依赖文件
我把官方GNU makefile manual和一个不错的中文介绍放在下面
2) Linker Script,这个对新手真的非常非常难,第一次看到linker script,简直就像天书。因为牵涉到linker script的任何改动都要求作者不光对linker script语法有掌握,还要对所处芯片架构,memory layout,还有软件的组织架构了如指掌
3) Memory Layout,在学写linker script之前,要搞清楚自己所处的MCU的memory layout,搞清楚自己想把什么程序安插在什么地方,大小、位置有没有冲突。
不管怎样,我们还是要硬着头皮往前冲,这里我们可以借助IDE提供的一些信息找到蛛丝马迹
首先我们用notepad++打开IDE自动生成的Makefile
里面莫名其妙内容很多,我摘录两个重要的地方放在下面
第一个,纯编译,即main.c -> main.o
上图字太小看不清对吧,这就对了,因为这个恶心的IDE就是这么猥琐,它完全不换行,把一个超过几百个字符的指令全部写在一排
所以我们要学的makefile第一堂课就是如何断句换行,下面是换行后的结果
上面这一段就是IDE自动生成的makefile,里面一大堆option传参,
例如,-x c就是表示告诉compiler,我们用的语言是C,不要搞成c++了。另外-c -std=gnu99 表示compile + assemble并用gnu99规范编译,-O0表示关闭compiler优化等等,还有一大堆,我时至今日都搞不清楚到底有什么用,例如 -funsigned-char、-funsigned-bitfields之类的
不过这里有一个地方值得我们特别注意,就是-I和-B后面的路径
那 -I 和-B分别是什么意思呢?
还是那句话,我对GCC这种对新人极其不友好的toolchain表达方式,非常不感冒,这个-I 和-B 属于完全不同的部门,结果都放在avr-gcc option里面,虽然我知道这是一种内部传参的表达形式,但太不直观
首先-I 不是compiler,而是assembler的参数
而-B 则是compiler的参数
这样,我们就跟着makefile里面提的信息去找这两个文件夹,看看里面放了什么东西
先看 -B,这个是给compiler准备的include文件夹,也就是说,这个目录下面一定都是各类.h 文件
其中在AVR5文件夹里面有两个非常重要的文件一个.o 另一个.a
然后在另一个文件夹device-specs里面有一个奇怪的没有任何尾缀的文本文件
该文件可以用notepad++打开
这里面提供了几个很重要的信息,一个是crtatmega328p.o将作为AVR标准库放在文件头,一个是data数据(SRAM)起始地址是(0x0080 0100,就是前面提到过的虚拟地址VMA)
到此为止,我们暂时不知道这几个文件对compile和assemble有没有任何影响,但是通过我做了几轮小实验,即使我把这个-B和-I的路径删掉,也不影响compile和assemble的结果
接下来,我们就要看IDE提供的makefile最关键的第二部分,也就是关于linking的内容了
还记得我们的原始程序什么样子吗?
为了非常直观的展现linker script对这段简单代码的影响,我废了很大心血制作了一张linker script全景图(如下)
这个overview从右往左看,分为四大区块
第一大区块——Linker Script(通过 avr-ld.exe --verbose得到,它是avr-ld提供的默认linker script)
第二大区块——AVR提供的标准库的反汇编 (relocation)
第三大区块——AVR提供的标准库的反汇编(source)
第四大区块——linking结束以后,最终可执行文件elf文件的反汇编代码
这里又要插播一个小知识
什么叫做查反汇编 source和查反汇编 relocation,这里就是涉及到assembler的一个知识点:
在我们编译四部曲的第二步里面提到,我们的c-code被compiler翻译成由汇编指令集组成的语言,如入栈push, 出栈pop,ldi等等,到了编译四部曲的第三步assemble,汇编语言进一步被翻译成01机器语言,这个时候,assembler就会引入相应指令集机器编码和relocation地址编码的概念
因为我们没有AVR标准库文件的源码(c-code),但我们可以通过 反汇编avr-objdump.exe -S 和avr-objdump.exe -r我们能够从二进制.o或.a文件里面,从中分析,库函数到底干了些什么事情
非常可惜的是,知乎对图片的分辨率支持实在太烂,我不得不把整个Overview分成左右两半,以提高清晰度
Link Script 全景图 —— 左半
Link Script 全景图 —— 右半
我在对整个linker script的工作原理是分析的时候,是一个逆向工程(overview从左往右看),先分析通过IDE build生成的elf文件有哪些代码,分别在什么位置,再逆推,看看AVR提供了哪一些隐藏的library(如crtatmega328p.o和libgcc.a)文件,再看linkerscript是如何作用于这些代码的
现在我们把思维顺序正过来(从右往左看)
首先最右的linker script的.text 区块决定了,它以下{}里面的内容都属于flash memory,自然而然,它的第一个区块就是flash memory的绝对地址 0x0000 0000
我现在把linker script里面 .text (program code in flash) 从上到下分了6个小的分区
第一个分区——.vectors中断向量表(由crtatmega328p.o提供)
它意味着MCU被reset以后,第一个执行的代码就是中断向量表的第一个位置,即reset复位中断,而reset中断执行的代码就是__init,那么它就会跳过下面全部的中断,达到下一个分区 .init,由于整个中断向量表的大小是0x68,所以,这就是为什么第一个中断向量跳转的地址就是0x68
第二个分区:.init2(由crtatmega328p.o提供)
这个分区的主要功能是初始化栈,这里不存在跳转(call),所以这个区块的程序执行完了以后,程序继续往下运行
这里有一个插曲,因为crtatmega328p.o文件里面不提供.init0和.init1区块,所以linker script找不到相应的区块,所以就把这个.init2区块紧挨着上一个区块(.vectors)放在flash里面——起始地址0x68
第三个分区:.init4(由libgcc.a提供)
这个分区掌管全局变量和bss的初始化,还记得我们的c-code里面有一个全局变量 var=0xAB吗,这个全局变量就是通过这个.init4里面的 __do_copy_data函数完成,它的任务就是把这个var从flash memory"搬到"sram去
所以大家千万不要小瞧这简简单单的一句 int var = 0xAB; 编译器替你干的活远比你想象的多
第四分区:.init9(由crtatmega328p.o提供)
这是一个非常非常重要分区,它掌管了接下来程序要往哪里跳转,还记得那个原始的问题吗,main函数一定要是"main"吗?
我在这里故意搞了一个恶作剧,通过编辑crtatmega328p.o这个二进制文件,我把main函数改成了fuck函数,所以我的c-code的主函数main就变成了fuck,如果这个时候你还坚持写main(void),编译的时候linkerscript就会报错,说找不到fuck函数 :-)
另外,进入主函数地址为什么要跳转 到0x96,那是因为 main(现在叫fuck)函数被安置在最靠近.init分区的第一个位置,就是0x96,如果我们c-code很大,全局变量很多,那就不一定时0x96了,很有可能main函数在flash位置还要往后挪
第五分区:.text 由crtatmega328p.o提供
大家不要被这个.text 迷惑了,这里的.text是 linker script .text里面的 一个小分区,只是名字刚好一样而已,它们完全不是一个东西。
2022-12-19特此声明:(以下内容纯属自己臆测,尚未查证)
这里的.text是 掌管一切非法定义的中断向量__bad_interrupt。通常一个MCU有很多中断向量,(例如这个MCU就有20多个),如果你定义了它们,如ADC中断向量 ISR(__ADC_vector),那它就会执行你定义的中断向量里面的代码,但如果你完全没有定义,它就会到这个未定义的中断向量里面来,这里crtatmega328p.o提供的未定义中断向量(__bad_interrupt)默认代码是跳转到绝对0x00地址——即跳转到reset中断向量
这也意味着,在MCU执行代码的过程中,因为任何“意外”执行了未定义的中断向量,程序就会重启。我看很多其他MCU为了避免种事情发生,对所有的未定义中断向量默认了一个while(1); 即进入死循环,这样有利于debug,它们称这种中断为NMI(Non maskable interrupt)
第六分区:.fini0 (由libgcc.a提供)
这个分区负责程序退出机制,看得出来,一旦到达_exit,程序就把所有中断向量关闭(cli),进入__stop_program后就立刻跳回到_exit,形成一个无限死循环再也出不来了,除非被硬件重新上电复位(reset)
到此,我们粗略的理解了linker script的整个工作原理,linker script就像一个管家,它负责把我们编译好的程序,严格按照linker script里面的描述,精准的把每一个function放到它应该去的位置(地址),所谓程序,其实就是.o或.a文件里的某些函数。而我们最终的可执行程序elf,就是严格按照我们linker script编写的代码顺序,进行各种不同的地址跳转,甚至是跨区跳转(例如从bootloader section跳到 application section)
有了上面的知识作为武器,我们大致可以想象,所谓写bootloader,就是写一段代码,然后通过linker script安插到一个特定的位置,等MCU上电复位以后(reset),首先执行我们bootloader的引导程序(主函数可以叫main,叫fuck,随便你定义),把等bootloader做完它的工作后,bootloader程序就会跳转到你后面的application程序地址。
其实,在真实工业应用中,很多主程序都有非常复杂reset机制,在特殊触发条件满足的情况下,主程序有时也能跳转回bootloader。所以我们的普通程序和bootloader没有什么本质不同
在我们进入实战写bootloader之前,我们还有最后一个知识点要补齐,就是这一款atmega328p芯片的特殊硬件配置寄存器 —— Fuse register
Fuse register是一种非常特殊的寄存器,它独立于所有的sram,flash,eeprom之外,它的物理特性有点像eeprom,可以byte wise擦写,掉电后也可永久储存。我们可以通过配置这些古怪的fuse bits,达到上电复位后实现MCU的一些特殊物理功能
如上图所示,这一款atmega328p提供一些奇奇怪怪的fuse bits,它们各自功能不同,例如BODLEVEL是掌管监控MCU电压,一旦MCU供电不足,就会提前进入reset状态
再例如LOW.SUT_CLSEL是掌管,MCU在上电复位之前,选择哪一种晶振,需要多长启动时间
还有其他的我就不一一赘述了,我们今天的重点要放在中间那两个fuse bits
1) BOOTSZ —— Boot Section Zone
这个选项非常重要,它决定了你写的bootloader会被安置到flash的哪一个地址,size有多大
atmega328p在它的32kByte flash memory里划出了一个非常特殊的区域专门给bootloader
在我们这个例子里面,我们把bootloader section定义在了 0x3F00 (word)这个地址, 也就是说,我们的bootloader大小不能超过256 words (即512 bytes),看上去不大,但是够用了
这里我要狂吐槽Microchip,怎么搞这么恶心的user manual,好好的byte单位不用,用什么狗屎word,害的大票人掉到这个坑里面了。我们一会儿为bootloader程序写linker script的时候还是要用以byte为单位的地址,起始地址为0x7F00(byte)
2) BOOTRST —— Boot Reset
大家要特别注意,上面的boot section zone(bootloader 扇区)不是一个普通flash地址,它是芯片厂商通过硬件配置(fuse bit - BOOTRST)对这个扇区赋予一些特别的功能
为了让bootloader顺利完成它的使命,我们必须强制MCU上电复位以后直接进入bootloader扇区,不然的话,如果芯片就会跑到0x0000(普通程序扇区了)
在本文中bootloader start address = 0x7F00 (byte!!!)
链接:https://zhuanlan.zhihu.com/p/589175743
万向节即万向接头,英文名称universal joint,是实现变角度动力传递的机件,用于需要改变传动轴线方向的位置
ESP32的DAC函数可以实现真正的模拟输出。
ESP32 没有Arduino输出 PWM 的 analogWrite(pin, value) 方法,取而代之的 ESP32 有一个 LEDC 来实现PWM功能。
本书由少年创学院联合创始人兼院长、知名创客程晨撰写,以Arduino作为硬件平台,介绍了使用米思齐(Mixly)软件进行程序开发的方法。
本文档作为UNO R4 WiFi的技术概览,您将找到一系列资源和指南链接,帮助您开始下一个项目。
Arduino OneButton库是一个用于简化按钮操作的库,它可以轻松地处理按钮的单击、双击和长按等操作。适用于Arduino开发板以及ESP32等其他基于Arduino的开发板。
许多硬件厂商都希望自己的开发板能被Arduino IDE集成开发环境所支持。这里就以小脚丫开发板所使用的开发包为例,介绍一下第三方开发包的制作方法。
ESP32 可以通过 SDMMC 和 SPI 两种方式读取SD/TF卡数据。