如何将.a文件导入如何将keil中的程序导入到单片机中?

最近用团队的帐号发了一篇文章,初衷是但愿给你们传递一些可能没有接触过的知识,其中某些知识可能也超出了前端的范畴,本质是但愿给你们提供一些扫盲的思路。可是文章的评论使我意识到你们对于这个文章的抵触心情很是大。我有很认真的看你们的每一条评论,而后可能过多的解释也没有什么用。我本身也反思可能文章就不该该以面试为标题进行传播,由于面试的话它就意味着跟职位以及工做息息相关,更况且我仍是以团队的名义去发这个文章。在这里,先跟这些读完文章体验不是很好的同窗道个歉。javascript

之前写文章感受都很开心,写完发完感受都能给你们带来一些新的输入。可是这一次,我感受挺难受的,也确实反思了不少,感受本身以这样的方式传播可能有些问题,主要以下:html

  • 题目取的不对,不该该拿面试做为标题,题目就应该是“脚撕专有钉钉前端面试题”
  • 若是做为面试题,其中某些问题问的太大,范围太广,确实不适合面试者进行回答
  • 若是做为面试题,其中某些问题问的不够专业,甚至是有歧义
  • 给出了面试题,就应该给出面试题的答案,这样才是真正帮助到你们扫盲

这里再也不过多解释和纠结面试题的问题了,由于我感受无论在评论中作什么解释,不承认的同窗仍是会一如既往的怼上来(挺好的,若是怼完感受本身还能释放一些小压力,或许还能适当的给子弈增长一些苍白解释的动力)。固然我也很开心不少同窗在评论中求答案,接下来我会好好认真作一期答案,但愿可以给你们带来一些新的输入,固然答案不可能一会儿作完,也不必定全面或者让你们感受满意,或许你们此次的评论又能给我带来一些学习的机会。前端

舒适提示:这里尽可能多给出一些知识点,因此不会针对问题进行机械式的回答,可能更多的须要你们自行理解和抽象。其中大部分面试题可能会已文章连接的形式出现,或许是我本身之前写过的文章,或者是我以为别人写的不错的文章。java

基础知识主要包含如下几个方面:node

  • 基础:计算机原理、编译原理、数据结构、算法、设计模式、编程范式等基本知识了解
  • 工程:编译工具、格式工具、Git、NPM、单元测试、Nginx、PM二、CI / CD 了解和使用
  • 性能:编译性能、监控、白屏检测、SEO、Service Worker 等了解
  • 后端:Redis 缓存、数据库、Graphql、SSR、模板引擎等了解和使用

现代计算机以存储器为中心,主要由 CPU、I / O 设备以及主存储器三大部分组成。各个部分之间经过总线进行链接通讯,具体以下图所示: 上图是一种多总线结构的示意图,CPU、主存以及 I / O 设备之间的全部数据都是经过总线进行并行传输,使用局部总线是为了提升 CPU 的吞吐量(CPU 不须要直接跟 I / O 设备通讯),而使用高速总线(更贴近 CPU)和 DMA 总线则是为了提高高速 I / O 设备(外设存储器、局域网以及多媒体等)的执行效率。ios

主存包括随机存储器 RAM 和只读存储器 ROM,其中 ROM 又能够分为 MROM(一次性)、PROM、EPROM、EEPROM 。ROM 中存储的程序(例如启动程序、固化程序)和数据(例如常量数据)在断电后不会丢失。RAM 主要分为静态 RAM(SRAM) 和动态 RAM(DRAM) 两种类型(DRAM 种类不少,包括 SDRAM、RDRAM、CDRAM 等),断电后数据会丢失,主要用于存储临时程序或者临时变量数据。 DRAM 通常访问速度相对较慢。因为现代 CPU 读取速度要求相对较高,所以在 CPU 内核中都会设计 L一、L2 以及 L3 级别的多级高速缓存,这些缓存基本是由 SRAM 构成,通常访问速度较快。git

高级程序设计语言不能直接被计算机理解并执行,须要经过翻译程序将其转换成特定处理器上可执行的指令,计算机 CPU 的简单工做原理以下所示: CPU 主要由控制单元、运算单元和存储单元组成(注意忽略了中断系统),各自的做用以下:程序员

  • 控制单元:在节拍脉冲的做用下,将程序计数器(Program Counter,PC)指向的主存或者多级高速缓存中的指令地址送到地址总线,接着获取指令地址所对应的指令并放入指令寄存器 (Instruction Register,IR)中,而后经过指令译码器(Instruction Decoder,ID)分析指令须要进行的操做,最后经过操做控制器(Operation Controller,OC)向其余设备发出微操做控制信号。
  • 运算单元:若是控制单元发出的控制信号存在算术运算(加、减、乘、除、增 一、减 一、取反等)或者逻辑运算(与、或、非、异或),那么须要经过运算单元获取存储单元的计算数据进行处理。
  • 存储单元:包括片内缓存和寄存器组,是 CPU 中临时数据的存储地方。CPU 直接访问主存数据大概须要花费数百个机器周期,而访问寄存器或者片内缓存只须要若干个或者几十个机器周期,所以会使用内部寄存器或缓存来存储和获取临时数据(即将被运算或者运算以后的数据),从而提升 CPU 的运行效率。

除此以外,计算机系统执行程序指令时须要花费时间,其中取出一条指令并执行这条指令的时间叫指令周期。指令周期能够分为若干个阶段(取指周期、间址周期、执行周期和中断周期),每一个阶段主要完成一项基本操做,完成基本操做的时间叫机器周期。机器周期是时钟周期的分频,例如最经典的 8051 单片机的机器周期为 12 个时钟周期。时钟周期是 CPU 工做的基本时间单位,也能够称为节拍脉冲或 T 周期(CPU 主频的倒数) 。假设 CPU 的主频是 1 GHz(1 Hz 表示每秒运行 1 次),那么表示时钟周期为 1 / 109 s。理论上 CPU 的主频越高,程序指令执行的速度越快。es6

上图右侧主存中的指令是 CPU 能够支持的处理命令,通常包含算术指令(加和减)、逻辑指令(与、或和非)、数据指令(移动、输入、删除、加载和存储)、流程控制指令以及程序结束指令等,因为 CPU 只能识别二进制码,所以指令是由二进制码组成。除此以外,指令的集合称为指令集(例如汇编语言就是指令集的一种表现形式),常见的指令集有精简指令集(ARM)和复杂指令集(Inter X86)。通常指令集决定了 CPU 处理器的硬件架构,规定了处理器的相应操做。github

早期的计算机只有机器语言时,程序设计必须用二进制数(0 和 1)来编写程序,而且要求程序员对计算机硬件和指令集很是了解,编程的难度较大,操做极易出错。为了解决机器语言的编程问题,慢慢开始出现了符号式的汇编语言(采用 ADD、SUB、MUL、DIV 等符号表明加减乘除)。为了使得计算机能够识别汇编语言,须要将汇编语言翻译成机器可以识别的机器语言(处理器的指令集): 因为每一种机器的指令系统不一样,须要不一样的汇编语言程序与之匹配,所以程序员每每须要针对不一样的机器了解其硬件结构和指令系统。为了能够抹平不一样机器的指令系统,使得程序员能够更加关注程序设计自己,前后出现了各类面向问题的高级程序设计语言,例如 BASIC 和 C,具体过程以下图所示: 高级程序语言会先翻译成汇编语言或者其余中间语言,而后再根据不一样的机器翻译成机器语言进行执行。除此以外,汇编语言虚拟机和机器语言机器之间还存在一层操做系统虚拟机,主要用于控制和管理操做系统的所有硬件和软件资源(随着超大规模集成电路技术的不断发展,一些操做系统的软件功能逐步由硬件来替换,例如目前的操做系统已经实现了部分程序的固化,简称固件,将程序永久性的存储在 ROM 中)。机器语言机器还能够继续分解成微程序机器,将每一条机器指令翻译成一组微指令(微程序)进行执行。

上述虚拟机所提供的语言转换程序被称为编译器,主要做用是将某种语言编写的源程序转换成一个等价的机器语言程序,编译器的做用以下图所示: 例如 C 语言,能够先经过 gcc 编译器生成 Linux 和 Windows 下的目标 .o 和 .obj 文件(object 文件,即目标文件),而后将目标文件与底层系统库文件、应用程序库文件以及启动文件连接成可执行文件在目标机器上执行。

舒适提示:感兴趣的同窗能够了解一下 ARM 芯片的程序运行原理,包括使用 IDE 进行程序的编译(IDE 内置编译器,主流编译器包含 ARMCC、IAR 以及 GCC FOR ARM 等,其中一些编译器仅仅随着 IDE 进行捆绑发布,不提供独立使用的能力,而一些编译器则随着 IDE 进行发布的同时,还提供命令行接口的独立使用方式)、经过串口进行程序下载(下载到芯片的代码区初始启动地址映射的存储空间地址)、启动的存储空间地址映射(包括系统存储器、闪存 FLASH、内置 SRAM 等)、芯片的程序启动模式引脚 BOOT 的设置(例如调试代码时经常选择内置 SRAM、真正程序运行的时候选择闪存 FLASH)等。

若是某种高级语言或者应用语言(例如用于人工智能的计算机设计语言)转换的目标语言不是特定计算机的汇编语言,而是面向另外一种高级程序语言(不少研究性的编译器将 C 做为目标语言),那么还须要将目标高级程序语言再进行一次额外的编译才能获得最终的目标程序,这种编译器可称为源到源的转换器。

除此以外,有些程序设计语言将编译的过程和最终转换成目标程序进行执行的过程混合在一块儿,这种语言转换程序一般被称为解释器,主要做用是将某种语言编写的源程序做为输入,将该源程序执行的结果做为输出,解释器的做用以下图所示:

解释器和编译器有不少类似之处,都须要对源程序进行分析,并转换成目标机器可识别的机器语言进行执行。只是解释器是在转换源程序的同时立马执行对应的机器语言(转换和执行的过程不分离),而编译器得先把源程序所有转换成机器语言并产生目标文件,而后将目标文件写入相应的程序存储器进行执行(转换和执行的过程分离)。例如 Perl、Scheme、APL 使用解释器进行转换, C、C++ 则使用编译器进行转换,而 Java 和 JavaScript 的转换既包含了编译过程,也包含了解释过程。

JavaScript 中的数组存储大体须要分为两种状况:

  • 同种类型数据的数组分配连续的内存空间
  • 存在非同种类型数据的数组使用哈希映射分配内存空间

舒适提示:能够想象一下连续的内存空间只须要根据索引(指针)直接计算存储位置便可。若是是哈希映射那么首先须要计算索引值,而后若是索引值有冲突的场景下还须要进行二次查找(须要知道哈希的存储方式)。

编译器的设计是一个很是庞大和复杂的软件系统设计,在真正设计的时候须要解决两个相对重要的问题:

  • 如何分析不一样高级程序语言设计的源程序
  • 如何将源程序的功能等价映射到不一样指令系统的目标机器

为了解决上述两项问题,编译器的设计最终被分解成前端(注意这里所说的不是 Web 前端)和后端两个编译阶段,前端用于解决第一个问题,然后端用于解决第二个问题,具体以下图所示: 上图中的中间表示(Intermediate Representation,IR)是程序结构的一种表现方式,它会比 AST(后续讲解)更加接近汇编语言或者指令集,同时也会保留源程序中的一些高级信息,除此以外 ,它的种类不少,包括、以及基于栈的 IR 等,具体做用包括:

  • 靠近前端部分主要适配不一样的源程序,靠近后端部分主要适配不一样的指令集,更易于编译器的错误调试,容易识别是 IR 以前仍是以后出问题
  • 以下左图所示,若是没有 IR,那么源程序到指令集之间须要进行一一适配,而有了中间表示,则可使得编译器的职责更加分离,源程序的编译更多关注如何转换成 IR,而不是去适配不一样的指令集
  • IR 自己能够作到多趟迭代从而优化源程序,在每一趟迭代的过程当中能够研究代码并记录优化的细节,方便后续的迭代查找并利用这些优化信息,最终能够高效输出更优的目标程序

因为 IR 能够进行多趟迭代进行程序优化,所以在编译器中可插入一个新的优化阶段,以下图所示: 优化器能够对 IR 处理一遍或者多遍,从而生成更快执行速度(例如找到循环中不变的计算并对其进行优化从而减小运算次数)或者更小体积的目标程序,也可能用于产生更少异常或者更低功耗的目标程序。除此以外,前端和后端内部还能够细分为多个处理步骤,具体以下图所示: 优化器中的每一遍优化处理均可以使用一个或多个优化技术来改进代码,每一趟处理最终都是读写 IR 的操做,这样不只仅可使得优化能够更加高效,同时也能够下降优化的复杂度,还提升了优化的灵活性,可使得编译器配置不一样的优化选项,达到组合优化的效果。

观察者模式和发布/订阅模式

编程范式(Programming paradigm)是指计算机编程的基本风格或者典型模式,能够简单理解为编程学科中实践出来的具备哲学和理论依据的一些经典原型。常见的编程范式有:

阅读连接::若是你对于编程范式的定义相对模糊,能够继续阅读 了解更多。

不一样的语言能够支持多种不一样的编程范式,例如 C 语言支持 POP 范式,C++ 和 Java 语言支持 OOP 范式,Swift 语言则能够支持 FP 范式,而 Web 前端中的 JavaScript 能够支持上述列出的全部编程范式。

顾名思义,函数式编程是使用函数来进行高效处理数据或数据流的一种编程方式。在数学中,函数的三要素是定义域、值域和**对应关系。假设 A、B 是非空数集,对于集合 A 中的任意一个数 x,在集合 B 中都有惟一肯定的数 f(x) 和它对应,那么能够将 f 称为从 A 到 B 的一个函数,记做:y = f(x)。在函数式编程中函数的概念和数学函数的概念相似,主要是描述形参 x 和返回值 y 之间的对应关系,**以下图所示:

舒适提示:图片来自于。

在实际的编程中,能够将各类明确对应关系的函数进行传递、组合从而达处处理数据的最终目的。在此过程当中,咱们的关注点不在于如何去实现**对应关系,**而在于如何将各类已有的对应关系进行高效联动,从而可快速进行数据转换,达到最终的数据处理目的,提供开发效率。

尽管你对函数式编程的概念有所了解,可是你仍然不知道函数式编程到底有什么特色。这里咱们仍然拿 OOP 编程范式来举例,假设但愿经过 OOP 编程来解决数学的加减乘除问题:

以上程序计算的结果是 8,彻底符合预期。咱们知道了 addmultiply 两个函数的实际对应关系,经过将对应关系进行有效的组合和传递,达到了最终的计算结果。除此以外,这两个函数还能够根据数学定律得出更优雅的组合方式:

咱们彻底不须要追踪相似于 OOP 编程范式中可能存在的内部状态数据,事实上对于数学定律中的结合律、交换律、同一概以及分配律,上述的函数式编程代码足能够胜任。

经过上述简单的例子能够发现,要实现高可复用的函数**(对应关系)**,必定要遵循某些特定的原则,不然在使用的时候可能没法进行高效的传递和组合,例如

若是你以前常常进行无原则性的代码设计,那么在设计过程当中可能会出现各类出乎意料的问题(这是为何新手总是出现一些稀奇古怪问题的主要缘由)。函数式编程能够有效的经过一些原则性的约束使你设计出更加健壮和优雅的代码,而且在不断的实践过程当中进行经验式叠加,从而提升开发效率。

虽然咱们在使用函数的过程当中更多的再也不关注函数如何实现(对应关系),可是真正在使用和设计函数的时候须要注意如下一些特色:

咱们之前设计的代码一般是命令式编程方式,这种编程方式每每注重具体的实现的过程(对应关系),而函数式编程则采用声明式的编程方式,每每注重如何去组合已有的**对应关系。**简单举个例子:

 
命令式代码一步一步的告诉计算机须要执行哪些语句,须要关心变量的实例化状况、循环的具体过程以及跟踪变量状态的变化过程。声明式代码更多的再也不关心代码的具体执行过程,而是采用表达式的组合变换去处理问题,再也不强调怎么作,而是指明**作什么。**声明式编程方式能够将咱们设计代码的关注点完全从过程式解放出来,从而提升开发效率。

在 JavaScript 中,函数的使用很是灵活,例如能够对函数进行如下操做:
经过以上示例能够看出,函数继承至对象并拥有对象的特性。在 JavaScript 中能够对函数进行参数传递、变量赋值或数组操做等等,所以把函数称为一等公民。函数式编程的核心就是对函数进行组合或传递,JavaScript 中函数这种灵活的特性是知足函数式编程的重要条件。

纯函数是是指在相同的参数调用下,函数的返回值惟一不变。这跟数学中函数的映射关系相似,一样的 x 不可能映射多个不一样的 y。使用函数式编程会使得函数的调用很是稳定,从而下降 Bug 产生的机率。固然要实现纯函数的这种特性,须要函数不能包含如下一些反作用:
  • 可变数据(包括在函数内部改变输入参数)
 
从以上常见的一些反作用能够看出,纯函数的实现须要遵循最小意外原则,为了确保函数的稳定惟一的输入和输出,尽可能应该避免与函数外部的环境进行任何交互行为,从而防止外部环境对函数内部产生没法预料的影响。纯函数的实现应该自给自足,举几个例子:
 
 
纯函数的特性使得函数式编程具有如下特性:
 
可缓存性和可测试性基于纯函数输入输出惟一不变的特性,可移植性则主要基于纯函数不依赖外部环境的特性。这里举一个可缓存的例子:

在函数式编程的简单示例中已经能够清晰的感觉到函数式编程绝对不能依赖内部状态,而在纯函数中则说明了函数式编程不能依赖外部的环境或状态,由于一旦依赖的状态变化,不能保证函数根据对应关系所计算的返回值由于状态的变化仍然保持不变。
这里单独讲解一下数据不可变,在 JavaScript 中有不少数组操做的方法,举个例子:
这里的 slice 方法屡次调用都不会改变原有数组,且会产生相同的输出。而 splice 每次调用都在修改原数组,且产生的输出也不相同。 在函数式编程中,这种会改变原有数据的函数已经再也不是纯函数,应该尽可能避免使用。

阅读连接:若是想要了解更深刻的函数式编程知识点,能够额外阅读。

 
 
响应式编程是一种基于观察者(发布 / 订阅)模式而且面向异步(Asynchronous)数据流(Data Stream)和变化传播的声明式编程范式。响应式编程主要适用的场景包含:
  • 用户和系统发起的连续事件处理,例如鼠标的点击、键盘的按键或者通讯设备发起的信号等
  • 非可靠的网络或者通讯处理(例如 HTTP 网络的请求重试)
  • 连续的异步 IO 处理
  • 复杂的继发事务处理(例如一次事件涉及到多个继发的网络请求)
  • 高并发的消息处理(例如 IM 聊天)
 
 
 
 
 



这里列出一些我经常使用的 API:
 

在 JavaScript 中利用(Event Loop)能够在单线程中实现非阻塞式、异步的操做。例如
 
咱们重点来看一下经常使用的几种编程方式(Callback、Promise、Generator、Async)在语法糖上带来的优劣对比。

Callback(回调函数)是在 Web 前端开发中常常会使用的编程方式。这里举一个经常使用的定时器示例:
回调函数常常会由于调用环境的变化而致使 this 的指向性变化。除此以外,使用回调函数来处理多个继发的异步任务时容易致使回调地狱(Callback Hell):
回调函数不能经过 return 返回数据,好比咱们但愿调用带有回调参数的函数并返回异步执行的结果时,只能经过再次回调的方式进行参数传递:
 

上述示例讲述的都是 JavaScript 中标准的异步 API ,若是使用一些三方的异步 API 而且提供了回调能力时,这些 API 多是非受信的,在真正使用的时候会由于执行反转(回调函数的执行权在三方库中)致使如下一些问题:
  • 使用者的回调函数设计没有进行错误捕获,而偏偏三方库进行了错误捕获却没有抛出错误处理信息,此时使用者很难感知到本身设计的回调函数是否有错误
  • 使用者难以感知到三方库的回调时机和回调次数,这个回调函数执行的权利控制在三方库手中
  • 使用者没法更改三方库提供的回调参数,回调参数可能没法知足使用者的诉求
 


Callback 的异步操做形式除了会形成回调地狱,还会形成难以测试的问题。ES6 中的 Promise (基于 规范的异步编程解决方案)利用的原理来解决异步的处理问题,Promise 对象提供了统一的异步编程 API,它的特色以下:
  • Promise 对象的执行状态不受外界影响。Promise 对象的异步操做有三种状态: pending(进行中)、 fulfilled(已成功)和 rejected(已失败) ,只有 Promise 对象自己的异步操做结果能够决定当前的执行状态,任何其余的操做没法改变状态的结果
 

舒适提示:有限状态机提供了一种优雅的解决方式,异步的处理自己能够经过异步状态的变化来触发相应的操做,这会比回调函数在逻辑上的处理更加合理,也能够下降代码的复杂度。

 
Promise 对象的执行状态不可变示例以下:
假设要实现两个继发的 HTTP 请求,第一个请求接口返回的数据是第二个请求接口的参数,使用回调函数的实现方式以下所示(这里使用 setTimeout 来指代异步请求):
 
 
 

舒适提示:继发请求的依赖关系很是常见,例如人员基本信息管理系统的开发中,常常须要先展现组织树结构,并默认加载第一个组织下的人员列表信息。

 
若是采用 Promise 的处理方式则能够规避上述常见的回调地狱问题:

首先 Promise 能够注册多个 then(放在一个执行队列里),而且这些 then 会根据上一次返回值的结果依次执行。除此以外,各个 Promise 的 then 执行互不干扰。 咱们将示例进行简单的变换:
上述例子中的执行结果是由于 then 的执行会返回一个新的 Promise 对象,而且若是 then 执行后返回的仍然是 Promise 对象,那么下一个 then 的链式调用会等待该 Promise 对象的状态发生变化后才会调用(能获得这个 Promise 处理的结果)。接下来重点看下 Promise
从上述示例能够看出 Promise 的错误不会影响其余代码的执行,只会影响 Promise 内部的代码自己,由于Promise 会在内部对错误进行异常捕获,从而保证总体代码执行的稳定性。Promise 还提供了其余的一些 API 方便多任务的执行,包括
  • Promise.all:适合多个异步任务并发执行但不容许其中任何一个任务失败
  • Promise.race :适合多个异步任务抢占式执行
  • Promise.allSettled :适合多个异步任务并发执行但容许某些任务失败
 
Promise 相对于 Callback 对于异步的处理更加优雅,而且能力也更增强大, 可是也存在一些自身的缺点:
  • 状态单一,每次决断只能产生一种状态结果,须要不停的进行链式调用
 

舒适提示:手写 Promise 是面试官很是喜欢的一道笔试题,本质是但愿面试者可以经过底层的设计正确了解 Promise 的使用方式,若是你对 Promise 的设计原理不熟悉,能够深刻了解一下或者手动设计一个。

 

Promise 解决了 Callback 的回调地狱问题,但也形成了代码冗余,若是一些异步任务不支持 Promise 语法,就须要进行一层 Promise 封装。Generator 将 JavaScript 的异步编程带入了一个全新的阶段,它使得异步代码的设计和执行看起来和同步代码一致。Generator 使用的简单示例以下:
经过上述代码,能够看出 Generator 相对于 Promise 具备如下优点:
  • 丰富了状态类型,Generator 经过 next 能够产生不一样的状态信息,也能够经过 return 结束函数的执行状态,相对于 Promise 的 resolve 不可变状态更加丰富 
  • Generator 函数内部的异步代码执行看起来和同步代码执行一致,很是利于代码的维护
  • Generator 函数内部的执行逻辑和相应的状态变化逻辑解耦,下降了代码的复杂度
 
next 能够不停的改变状态使得 yield 得以继续执行的代码能够变得很是有规律,例如从上述的手动执行 Generator 函数能够看出,彻底能够将其封装成一个自动执行的执行器,具体以下所示:

舒适提示:设计了一个 Generator 自动执行器 ,使用 Co 的前提是 yield  命令后必须是 Promise 对象或者 Thunk 函数。Co 还能够支持并发的异步处理,具体可查看官方的 。

 
须要注意的是 Generator 函数的返回值是一个 Iterator 遍历器对象,具体以下所示:
Generator 函数的错误处理相对复杂一些,极端状况下须要对执行和 Generator 函数进行双重错误捕获,具体以下所示:
在使用 g.throw 的时候还须要注意如下一些事项:
  • 若是 Generator 函数自己没有捕获错误,那么 Generator 函数内部抛出的错误能够在执行处进行错误捕获
  • 若是 Generator 函数内部和执行处都没有进行错误捕获,则终止进程并抛出错误信息
  • 若是没有执行过 g.next,则 g.throw 不会在 Gererator 函数中被捕获(由于执行指针没有启动 Generator 函数的执行),此时能够在执行处进行执行错误捕获
 

  • 内置执行器:Generator 函数须要设计手动执行器或者通用执行器(例如 Co 执行器)进行执行,Async 语法则内置了自动执行器,设计代码时无须关心执行步骤
 

经过上述示例能够看出,async 函数的特性以下:
  • 调用 async 函数后返回的 Promise 对象必须等待内部全部 await 对应的 Promise 执行完(这使得 async 函数多是阻塞式执行)后才会发生状态变化,除非中途遇到了 return 语句
  • await 命令后若是是 Promise 对象,则返回 Promise 对象处理后的结果,若是是原始数据类型,则直接返回原始数据类型
 
上述代码是阻塞式执行,nextPromise 须要等待 firstPromise 执行完成后才能继续执行,若是但愿二者可以并发执行,则能够进行下述设计:
除了使用 Promise 自带的并发执行 API,也能够经过让全部的 Promise 提早并发执行来处理:
Async 的错误处理相对于 Generator 会更加简单,具体示例以下所示:
async 函数内部抛出的错误,会致使函数返回的 Promise 对象变为 rejected 状态,从而能够经过 catch 捕获, 上述代码只是一个粗粒度的容错处理,若是但愿 firstPromise 错误后能够继续执行
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
业务思考更多的是结合基础知识的广度和深度进行的具体业务实践,主要包含如下几个方面:
  • 通用性:脚手架、SDK、组件库等框架设计
  • 低代码:通用表单设计、通用布局设计、通用页面设计、JSON Schema 协议设计等
  • 测试:E2E 测试、单元测试、测试覆盖率、测试报告等
  • 业务:数据、体验、复杂度、监控
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
笔试更多的是考验应聘者的逻辑思惟能力和代码书写风格,主要包含如下几个方面:
 
 
 
 
 
 
 
 
 

为了顺利过渡到库开发,在STM32编程的开始,我们对照51点亮一个LED的方法,给大家演示一下STM32如何用操作寄存器的方法点亮一个LED,然后再慢慢讲解到底什么是库,让大家知道库跟寄存器的关系。

  在用STM32点亮一个LED之前,我们先来复习下用51如何点亮一个LED。

硬件上我们假设51单片机的P0口的第0位接了一个LED,负逻辑亮。如果我们要点亮这个LED,代码上我们会这么写:

这时候我们就把LED点亮了,如果要关掉LED ,则是:

这里面我们用的是总线操作的方法,即是对P0口的8个IO同时操作,但起作用的只是P0^0。

除了这种总线操作的方法,我们还学习过位操作,利用51编译器的关键字sbit,我们可以定义一个位变量:

为了让程序看起来见名知义,我们定义两个宏:

点亮和关闭LED的代码就变成了:

稍微整理一下代码,整体效果就是:

//假设51单片机的PO~0口接LED,负逻辑点亮

 

上面总线和位操作的的方法,学过51的朋友是非常熟悉的,也很容易理解。那么我们再说一下大家容易忽略的几个知识点。

在点亮 LED 的时候,我们都是用操作寄存器的方法来实现的,那大家是否想过,这个寄存器到底是什么?为什么我们可以直接操作P0口?

解答上面的问题之前,我们先简单介绍下51单片机的主要组成部分,这对我们学习其他单片机也有好处。

我们以国内的STC89C51为例,该单片机主要由51内核、外设IP、和总线这三大部分组成。内核是由 Intel 公司生产的,外设 IP 就是 STC 公司在内核的基础上添加的诸如定时器、串口、IO 口等这些东西,总线就是用来连接内核和外设的接口单元。Intel 在这里属于IP核设计公司,STC 属于 IC 设计公司。世界上能设计 IP 核的公司屈指可数。我们非常熟悉的ARM公司就属于IP核设计公司,ARM 给其他公司授权,其他IC公司就在ARM内核上设计出各具特色的MCU,我们后面要学习的STM32就是属于一中基于ARM内核的MCU。

寄存器则是内置于各个 IP 外设中,是一种用于配置外设功能的存储器,就是一种内存,并且有想对应的地址。学过C语言我们就知道,要操作这些内存就可以使用C语言中的指针,通过寻址的方式来操作这些具有特殊功能的内存一寄存器。比如 P0 口对应的地址是0X80,那么我们要修改 0X80 这个地址对应的内存的内容的话,按照常理可以这样操作:

可当我们编译的时候,编译器会报错,在51里面只能通过sfr和sbit这两个关键字来实现寄存器映象,不能直接操作寄存器对应的地址,这是51相较于STM32不同的地方。

51单片机的这些寄存器位于地址80H~FFH中,对应着128个地址,但不是每个地址都是有效的,51系列的单片机有21个,52系列的则有26个,其他的都是保留区。

  实际上我们在编程的时候并不是通过指针来操作寄存器的,而是直接给P0、P1这些端口寄存器赋值。那么这些外设资源是如何与地址建立一一对应的关系(寄存器映射定义),这得益与51特有的两个关键字:sfr和sbit,其他单片机没有,只能用其他的方式来实现寄存器映射。这两个关键字帮我们实现了所有寄存器的定义,所以我们才可以像操作普通变量一样来操作寄存器。其实我们一开始提到的点亮LED的代码,全貌应该是这样的:

为了方便起见,我们可以把寄存器映射全部写好封装在一个头文件里面,不用每用一个寄存器就定义一次。其实这方面的工作不用我们做,我们在编程的时候都会在开始的地方添加一个头文件:

这个头文件已经实现了全部寄存器的定义,该文件是keil自带,在安装目录:
  Keil\C51\INC下可以找到。这个文件实现了字节寄存器和位寄存器的定义。

  还有一个就是启动代码,这个也是很多初学者容易忽略的地方,对于这部分我们主要总结下它的功能,不详解讲解里面的代码。

单片机在上电复位后,首先执行的是启动文件一STARTUP.A51,而不是我们通常看到的main函数。我们新建51工程的时候会有一个提示:是否拷贝启动代码到当前的工程,我们一般选择是。

启动代码用汇编语言编写,主要实现了以下功能:

  清除内部数据存储器、清除外部数据存储器、清除外部页储存器、初始化small模式下的可重入栈和指针、初始化large模式下可重入栈和指针、初始化compact模式下的可重入栈和指针、初始化8051硬件栈指针、传递初始化全局变量的控制命令或者在没有初始化全局变量时给main函数传递命令。然后程序就跳转到main函数,来到我们熟知的C世界。

  用KEIL5新建一个工程,把工程放在一个事先建好的文件夹内,工程命名为REG后保存。然后在工程目录下添加启动文件:startup_stm32fl0x_hd.s,该文件可以从KEIL5安装目录找到,也可以从ST库里面找到,然后把启动文件添加到工程里面。

启动文件由汇编语言编写,具体功能跟51里面的启动文件:STARTUP.A51差不多。

STM32的启动文件主要实现了:

3、设置向量表入口地址,并初始化向量表。

5、跳转到标号_main,最终来到C的世界。

这里我们先去除繁枝细节,挑重点的讲,主要理解第4和第5点,在启动文件的147~155行,是复位处理函数,代码如下:

这里我们简单介绍下这10行代码。

第1行是程序注释,在汇编里面注释用的是“;”,跟C语言不一样。

第2行是定义了一个子程序:Reset_Handler。PROC是子程序定义伪指令。一般用法为:

  其中NEAR和FAR是属性词。

  NEAR属性(段内近调用):调用程序和子程序在同一代码段中,只能被相同代码段的其他程序调用。

  FAR属性(段间远调用):调用程序和子程序不在同一代码段中,可以被相同或不同代码段的程序调用。

第3行EXPORT表示Reset_Handler这个子程序可供其他模块调用。关键字[WEAK]表示弱定义,如果编译器发现在别处定义了同名的函数,则在链接时用别处的地址进行链接,如果其它地方没有定义,编译器也不报错,以此处地址进行链接。

第4行和第5行IMPORT说明Systemlnit和__main这两个标号在其他文件,在链接的时候需要到其他文件去寻找。

  Systemlnit在库文件system_stm32f10x.c实现,用来初始化STM32的一系列时钟,把系统时钟设置为72MHZ。STM32的时钟比51单片机复杂,需要经过一系列的配置才能达到稳定运行的状态。

  __main其实不是我们定义的,当编译器编译时,只要遇到这个标号就会定义这个函数,该函数的主要功能是:负责初始化栈、堆,配置系统环境,并在最后跳转到用户自定义的main函数,从此来到C的世界。

第7行程序跳转到R0中的地址执行程序,之后系统的时钟就被设置成72MHZ。

第8行把__main的地址加载到寄存器R0。

第9行程序跳转到R0中的地址执行程序,执行完毕之后就去到我们熟知的C世界。

第10行表示子程序的结束。

总结下就是,Reset_Handler这个函数执行了两个函数调用,一个是Systemlnit,把系统时钟设置成72M,另一个是__main,初始化好系统环境,最终调用C的main,从此去到C的世界。

__main函数由编译器生成,负责初始化栈、堆等,并在最后跳转到用户自定义的main()函数,来到C的世界。

所以,就题主给出的情况,在算完低4位B+6 = 1,产生了进位,这时再算高4位,就不能再是对A不操作了,而是A+6 = 0,即正确答案01H。

仔细看下指令集的说明则不难做出以上回答。

我要回帖

更多关于 如何将keil中的程序导入到单片机中 的文章

 

随机推荐