FreeRTOS学习笔记02

FreeRTOS

FreeRTOS是一个迷你的实时操作系统内核。作为一个轻量级的操作系统,功能包括:任务管理、时间管理、信号量、消息队列、内存管理、记录功能、软件定时器、协程等,可基本满足较小系统的需要。

FreeRTOS是为小型嵌入式系统设计的可裁剪实时内核。其主要特点有:

  • 调度器支持抢占式调度,协助式调度,或者两者混合。时间片可选
  • 占用空间小,简单,易用

FreeRTOS的源码结构

在FreeRTOS v9.0版本中,FreeRTOS的代码主要包含两个文件夹:FreeRTOS_CORE和FreeRTOS_PORTABLE。这两个文件夹下包含多个.C文件。

  • port.c: 针对不同硬件平台的接口,定义与硬件接口相关的代码
  • heap_4.c: 内存管理相关
  • croutine.c:协程相关
  • event_groups.c:事件标志组相关
  • list.c:管理系统实际会应用到list,是FreeRTOS的一种基础数据结构
  • tasks.c:任务创建、挂起、恢复、调度相关
  • timers.c:软件定时器相关
  • queue.c:管理tasks之间的通信(message queue的概念)
  • FreeRTOSConfig.h:宏定义,配置RTOS所需要的资源

FreeRTOS的任务间通信机制

裸机编程中,一个复杂的功能通常需要多个子函数来实现,不同的子函数之间的通常采用一些全局变量来实现联系。在RTOS中,我们不仅可以使用全局变量,还可以采用系统自带的任务间通信机制。这种机制更加受推荐。其原因是:

  1. 阻塞等待机制比轮询等待更加高效:全局变量当用作某种事件的标志是,获取该标志的任务需要轮询检测标志位的状态是否变化。这样会产生大量的无效判断。如果使用任务间通信阻塞等待的机制,CPU可以转而处理其他事情,当标志变化时解除阻塞。又可以及时执行后续的处理。
  2. 全局变量会产生不可重入函数导致逻辑混乱:RTOS运行时,CPU需要调用不同的函数,如果全局变量使用不恰当,会导致原本设计的逻辑产生混乱。比如某个低优先级任务正在访问某个公共函数,并对函数中的全局变量进行了修改。还未退出函数时,更高优先级的任务抢占了CPU的使用权,并对该函数的全局变量进行了修改。此时低优先级任务若认为自己修改变量成功,执行后续逻辑时,就会发生错误。

FreeRTOS任务间的通信方式:

  • 信号量(Semaphore): 用于任务间的同步,一个任务以阻塞方式灯带另一个任务释放信号量。
  • 互斥量(Mutex):用于任务间共享资源的互斥访问,使用前获取锁,使用后释放锁。
  • 事件标志组(EventGroup): 用于任务间的同步,相比信号量,事件标志组可以等待多个事件的发生。
  • 消息队列(Queue): 类比全局数据,它可以一次发送多个数据(一般将数据定义成结构体发送),每次数据的大小固定不变。
  • 流缓冲区(SteamBuffer):在队列的基础上,优化的一种更加适合的数据结构,可以一次写入任意数量的字节,并且可以一次读取任意数量的字节。
  • 消息缓冲区(MessageBuffer):在流式缓冲区的基础上实现的,可以对消息进行设计改进。每一条消息的写入增加了一个字节用来表示该消息的长度,读取时需要至少一次性读出一条消息,否则会返回0.
  • 任务通知(Notify): 不同于上面的任务通信方式(使用某种通信对象,通信对象是独立于任务的实体,有单独的存储空间,可以实现数据的传递和较复杂的同步,互斥功能), 通知是发向一个指定的任务的,直接改变该任务 TCB的某些变量。

FreeRTOS队列

在实际应用中,一个任务或中断服务函数经常需要和另一个任务进行消息传递。裸机情况下通常通过全局变量实现。但是操作系统使用全局变量的方式会涉及“资源管理”问题。FreeRTOS中采用队列机制完成任务与任务,和任务与中断之间的消息传递。 队列可以存储有限的,大小固定的数据项目。

内存管理

FreeRTOS创建任务、队列、信号量有两种方法:

第一种是由用户自行定义所需的RAM,这种方法也叫静态的方法。静态方法的函数一般由Static结尾,比如任务创建 xTaskCreateStatic()。 使用此函数创建任务的时候需要用户定义任务堆栈。

另一种是动态的申请所需的RAM,使用动态内存管理时,FreeRTOS内核在创建任务、队列、信号量的时候会动态的申请RAM。C语言标准库的malloc()和free()也可以实现动态内存管理,但这种方法在小型嵌入式系统中效率不高,会占用很多代码空间,并且他们的线程不是安全的,程序执行的时间也是不确定的,此外还会导致内存碎片。因此在FreeRTOS中,内核采用 pvPortMalloc()代替 malloc() 申请内存,采用 vPortFree()函数释放内存。关于内存分配,FreeRTOS提供了5这种内存分配的方法: 也就是在5个.c文件,heap_1.c, heap_2.c, heap_3.c, heap_4.c, heap_5.c。

内存碎片:
内存分配与管理的方法中需要解决的问题之一就是内存碎片,其产生过程如下图所示,一个新的内存堆被系统按照应用需求分成多个大小不同的内存块。应用在使用完内存后就会进行释放,同时新的应用产生也需要分配新的可用该内存。经过多次申请和释放后,内存块被不断地分割,导致内存中存在大量的很小的内存块。这些内存块太小导致大多数应用无法使用,因此就形成了内存碎片。这些内存碎片的不断增加会导致实际可用内存越来越少。最终应用程序因为分配不到合适的内存而崩溃。而FreeRTOS的heap_4.c就提供了一个解决内存碎片的方法,即将内存碎片进行合并组成一个新的可用的大内存块。

heap_1.c简介
动态内存分配需要一个内存堆,在FreeRTOS中的内存堆为ucHeap[],大小为configTOTAL_HEAP_SIZE.

heap_1特性如下:

  1. 使用一旦创建好任务,信号量和队列就再也不会删除的应用,实际上大多数的FreeRTOS的应用都是这样的。
  2. 具有可确定性(执行所花费的时间大多数都是一样的),而且不会导致内存碎片。
  3. 代码实现和内存分配的过程都非常简单,内存是从一个静态的数组中分配的,也就是适合与那些不需要动态分配内存的应用。

heap_2.c简介

heap_3.c简介

heap_4.c简介
heap_4提供了一个最优的内存分配方法,不像heap_2, heap_4会将内存中的碎片合并成一个大的可用内存块,他提供了内存合并算法。内存堆为ucHeap[], 大小同样为configTOTAL_HEAP_SIZE。可以通过函数xPortGetFreeHeapSize()获取剩余内存大小。

heap_4特性如下:

  1. 可以用在那些需要重复创建和删除任务、队列、信号量和互斥信号量等应用中。
  2. 不会像heap_2那样产生严重的内存分配碎片,即使分配的内存大小是随机的。
  3. 具有不确定性,但是远比C标准库的malloc()和free()效率高。

heap_5简介
heap_5使用了heap_4相同的合并算法,内存管理实现基本相同,但是heap_5允许内存堆跨越多个不连续的内存段。如果使用heap_5需要调用函数xPortDefineHeapRegions()来对内存堆做初始化处理,该函数执行完之前禁止调用任何会调用pvPortMalloc()的函数。

参考资料:
[1] FreeRTOS源码探析之——任务调度相关

FreeRTOS学习笔记01

基本概念

  • 操作系统:操作系统是一个用以提供基础计算机功能的计算机程序,它可以向其他程序提供服务,应用来实现用户想要实现的功能。操作系统对应用程序的支持使得开发者在在开发应用程序是更加快捷,简单,易维护。

  • RTOS:大部分操作系统允许多个应用程序同时执行,这种成为多任务。但实际上,在任何一个时间点上只有一个进程在独立执行。由于应用程序切换足够快,好像所有的程序同时执行。操作系统中有一个调度器(Scheduler)的部分负责调度应用程序,决定什么时候执行哪个应用程序,调度器在每个程序之间的切换需要足够快速。实时操作系统费的调度器设计成可以提供确定的执行模式。实时性意味着嵌入式系统对某个具体事件的响应必须严格控制在一个预定的deadline内。实时操作系统会按照排序运行、管理系统资源,并为开发应用程序提供一致的基础。实时操作系统与一般的操作系统相比,最大的特色就是“实时性”,如果有一个任务需要执行,实时操作系统会马上(在较短时间内)执行该任务,不会有较长的延时。这种特性保证了各个任务的及时执行。

  • 线程:线程是操作系统能够进行运算调度的最小单位,包含在进程中,是进程的实际运作单位。一条线程是进程中的一个单一控制流。线程有四枣红基本状态:产生,阻塞,非阻塞,结束。

  • 进程:指计算机已运行的程序,是分时系统的基本运作单位。进程是程序的真正运行实例。进程有五种状态:新生,运行,等待,就绪,结束。在单CPU系统中,任何时间可能有多个进程在等待,但必定仅有一个进程在运行。

  • 线程和进程区别:进程是资源分配的最小单位,线程是CPU调度的最小单位。进程和线程都是一个时间段的描述,是CPU工作时间段的描述,是运行中程序指令的描述。

实时操作系统的设计原则:

  • 实时的消息、事件处理机制:常规的操作系统,消息队列是按照FIFO的方式进行调度。实时操作系统会提供基于优先级的处理方式。
  • 提供内核级的优先级翻转处理方式:实时操作系统调度器最精彩遇到的问题是优先级翻转,因此对于类似信号量一类的API,都能提供抑制优先级翻转的机制,防止操作系统死锁。
  • 减少粗粒度的所和长期关中断的使用:这里的所主要是指自旋锁(Spinlock)一类会影响中断的锁,也包括任何关中断的操作,在Windows和Linux的驱动中,为了同步的需要,可能会长期关闭中断,这里的长期可能是毫秒到百微秒级。但实时操作系统通常不允许长期关中断。对于非实时操作系统来说,如果收到外部中断,系统在处理中断的整个过程中可能会一直关中断。但实时操作系统的通常做法是吧中断作为一个事件通告给另外一个任务,interrupt handler在处理完关键数据以后,立即打开中断,驱动的中断处理程序以一个高优先级任务的方式继续执行。
  • 系统级的服务也要保证实时性:对于一些系统级的服务,如文件系统操作,非实时系统过会缓存用户请求,并不直接把数据写入设备,或者建立一系列线程池,分发文件系统的请求。但是实时系统允许高优先级的任务有限写入数据。这种设计会牺牲性能,但是会保证系统的实时性。
  • 避免提供实时性不确定的API:多数实时操作系统都不支持虚拟内存(page file / swap area),主要原因是缺页中断(page fault)会导致任务调度的不确定性增加。多数实时操作系统过都支持分页,但很少会使用虚拟内存,因为一次缺页中断的开销十分巨大,通常都是毫秒级。会导致用户程序执行的不确定性增加。
  • 提供针对实时系统调度的专用API:
  • 降低系统抖动:由于关中断的原因,通常情况下,操作系统的调度器不会太精确的产生周期性的调度。一个设计优秀的实时操作系统能把抖动降低到微妙甚至是百纳秒级别。
  • 针对实时性设计的SMP和虚拟化技术:

实时性,硬实时,软实时

实时性: 实时性也叫实时计算(real-time computing), 实时约束指的是从事件发生到系统回应之间的最长时间限制。实时程序必须保证在严格时间限制内响应。 换句话说就是,任务(Task)必须在给定的时间(Deadline)内完成。比如汽车安全气囊响应,在汽车检测到撞击后,汽车ECU以及执行器需要在40ms内完全打开气囊,否则就会对乘客安全造成威胁。这个时候就要求汽车ECU的程序运行满足实时性标准。

硬实时: The firm real-time definition allows for infrequently missed deadlines. In these applications the system can survive task failures so long as they are adequately spaced, however the value of the task’s completion drops to zero or becomes impossible.

软实时: The soft real-time definition allows for frequently missed deadlines, and as long as tasks are timely executed their results continue to have value. Completed tasks may have increasing value up to the deadline and decreasing value past it.

区别: 硬实时操作系统必须使任务在确定的时间内完成;软实时操作系统能使绝大多数的任务在确定时间内完成。因此,硬实时和软实时的差别是,软实时只能提供统计意义上的实时。只要任务及时执行就会具有价值,如果任务超出Deadline,只会导致价值的稍微降低。如计算机的声音系统就是软实时的任务。 而硬实时任务只要超时,任务的价值就会降低到零。

任务调度

调度同来确定多任务环境下任务执行的顺序和获得CPU资源后能执行的时间长度。操作系统通过一个调度程序来实现调度功能。调度程序以函数的形式存在,用来实现操作系统的调度算法。调度程序本身并不是一个任务,是一个函数调用,可在内核的各个部分进行调用。调用调度程序的具体位置成为一个调度点(Scheduling point), 调度点通常处于一下位置:**(i)** 中断服务程序的结束位置;**(ii)** 任务因等待资源而处于等待状态;**(iii)** 任务处于就绪状态时。

在操作系统中,一个任务有三种典型状态:

  • 正在运行(Running):正在CPU中执行
  • 待命(Ready):等待执行
  • 阻塞(Blocked):任务暂停,等待一个事件的发生,例如接受一组数据

由于CPU在某个事件只能执行一个任务,因此大部分任务在多数事件处于阻塞或待命状态。可能大量的项目在待命列表里等待执行。这取决于系统所需的任务数量和调度器类型。通常情况下,简单的时间触发式调度器,待命任务列表的数据结构要尽可能缩短最坏情况下,程序在调度器关键部分的执行时间,防止其他任务一直在待命列表中无法及时执行。在这种调度器中,应该尽量避免抢占式任务,甚至应该关闭调度器之外的所有中断。并且待命列表的数据结构应该根据系统所需要最大任务数量进行优化。如果列表任务较多,双向链表是一个合适的结构。在任务列表的排序上,应该按照优先级对任务进行排序。这样可以保证高优先级任务的及时执行。

调度算法:
实时操作系统需要采用各种算法和策略保证系统行为的可预测性,并且调用一切可利用的资源完成实时控制任务。 其实时调度算法分为三种类别:基于优先级的调度算法(Priority-driven scheduling-PD),基于CPU使用比例的共享式调度算法(Share-drivescheduling-SD),基于时间进程的调度算法(Time—driven schedulinq-TD)。

基于优先级的调度算法给每个进程分配一个优先级,在每次进程调度的时候,最高优先级任务首先被执行。算法的类型分为两种:

静态调度:静态调度在系统开始运行前进行调度,严格的静态调度在系统运行时无法对任务重新调度。静态调度的目标是把任务分配给各个处理机,并对每一处立即给出所要运行的静态运行顺序。静态调度算法实现简单

死锁: 死锁是指一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用的不会释放的资源而处于的一种永久等待的状态。

死锁的四个条件:

  • 互斥条件(Mutual exclusion):资源不能被共享,只能由一个进程使用
  • 请求与保持条件(Hold and wait):已经得到资源的进程可以再次申请新的资源
  • 非剥夺条件(No pre-emption):已经分配的资源不能从相应的进程中被强制的剥夺
  • 循环等待条件(Circular wait):系统中的若干进程组成的环路,该环路中每个进程都在灯带相邻进程正占用的资源

互斥锁: 互斥锁是一种独占锁,当线程A加锁成功后,此时的互斥锁已经被线程A独占了,只要A没有释放受众的锁,线程B加锁就会失败,于是就是会释放CPU让给其他线程,既然B释放了CPU,也就意味着线程B的加锁代码会被阻塞。对于互斥锁加锁失败而阻塞的现象,是由操作系统内核实现的。

自旋锁: 自旋锁是一种特殊的互斥锁,当资源被加锁后,其他线程想要再次加锁,此时该线程不会被阻塞睡眠而是陷入循环等待状态(CPU不能做其他事情),循环检查资源持有者是否已经释放了资源,这样做的好处是减少了线程从睡眠到唤醒的资源消耗,但是会一直占用CPU资源。适用于资源的锁被持有时间段,而又不希望在线程的唤醒上花费太多资源的情况。

  1. 自旋锁为什么不能睡眠?
    因为需要循环检测lock变量。

内存管理

  1. 介绍一下FreeRTOS的内存管理?
  • heap_1:只申请不释放,适用于一旦创建好任务,信号量和队列就再也不会删除的应用。
  • heap_2:最佳匹配,即申请又释放,适用于可能会重复的删除的任务,队列,信号量等应用中,需要注意内存碎片的产生问题。
  • heap_3:简单封装malloc,free,对其进行线程保护。使用时需要编译器提供一个内存堆,编辑器库需要提供malloc()和free()函数。
  • heap_4:最佳匹配+合并相邻内存,具有不确定性,但不会产生严重的内存碎片
  • heap_5:最佳匹配+合并不连续的内存区。
  1. 简述RTOS中,栈空间的最大使用率和栈溢出的检测方法?
  • 方法一:在任务切换时检测任务指针是否越界,如果越界就会在任务切换时触发栈溢出的钩子函数
  • 方法二:在任务创建的时候将任务战所有数据初始化为0Xa5,任务切换进行任务栈检测时检测末尾的16个字节是否都是0xa5,通过这种方式检测栈是否溢出。

参考文章

[1] https://blog.csdn.net/u012993936/article/details/41145863
[2] https://zhuanlan.zhihu.com/p/86861756

嵌入式面试常见问题整理

基本概念

单工,半双工,全双工

  • 单工:简单的说就是一方只能发信息,另一方则只能收信息,通信是单向的。
  • 半双工:比单工先进一点,就是双方都能发信息,但同一时间则只能一方发信息。
  • -全双工:比半双工再先进一点,就是双方不仅都能发信息,而且能够同时发送。

FLASH 和 EEPROM区别

基本概念:

  • ROM: Read-Only Memory
  • RAM: Random Access Memory
  • FLASH: Flash 存储器(FLASH EEPROM)又称闪存,快闪。它是EEPROM的一种。它结合了ROM和RAM的长处。不仅具备电子可擦除可编辑(EEPROM)的性能,还不会断电丢失数据同时可以快速读取数据
  • EEPROM: Electrically-Erasable Programmable Read-Only Memory
  • SRAM:静态RAM,读写速度非常快,比较昂贵。一般用于CPU的一级缓冲,二级缓冲。
  • DRAM:动态RAM,DRAM保留数据的时间很短,速度比SRAM慢,但是比任何ROM快。计算机的内存是DRAM。

FLASH和EEPROM的最大区别是FLASH按扇区操作,EEPROM则按字节操作,二者寻址方法不同,存储单元的结构也不同,FLASH的电路结构较简单,同样容量占芯片面积较小,成本自然比EEPROM低,因而适合用作程序存储器,EEPROM则更多的用作非易失的数据存储器。

目前的单片机,RAM主要是做运行时数据存储器,FLASH主要是程序存储器,EEPROM主要是用以在程序运行保存一些需要掉电不丢失的数据.

  1. Nano Flash和NOR Flash的区别:
    目前Flash主要有NANO和NOR两种
  • NOR Flash:该种Flash的读写和创建的SDRAM读写一样,用户可以直接运行装载在NOR Flash里面的代码,这样可以减少SRAM的容量从而节约成本。
  • NANO Flash:NAND Flash没有采取内存的随即读取技术,它的读取是一次读取一块的形式来进行的,通常是一次读取512个字节,采用这种技术的Flash比较廉价,用户无法直接运行上面的代码。

总线协议

CAN总线协议(Controller Area Network)

CAN总线协议是由博世开发的一种基于消息广播模式的串行通信总线,该协议非常适合于现场控制领域,主要用于实现汽车ECU之间的可靠通信。该通讯协议最高速率可达到1Mbps, 容错能力强。CAN控制器包含强大的检错和处理机制。另外CAN的节点之间不会传输大数据块,一帧CAN消息最多传输8字节用户数据。

总线特点

  • 多主控制,总线空闲时所有单元都可以发送消息,最先访问总线的单元获得发送权,多个单元同时发送时,发送优先级高的可以发送。发送的消息保温不包含原地址和目标地址,只通过标识符表示消息的功能和优先级。
  • 总线为事件触发型,只有消息要发送时,节点才向总线上广播消息; 同时每个节点也可以通过发送远程帧请求其他节点发送数据。
  • 总线上可同时连接多个节点,可连接节点总数在理论上是没有限制的,实际可连接的节点数受总线上的时间延迟和电气负载限制。
  • 符合OSI通信系统参考模型,属于物理层和数据链路层。两线式总线结构,电气信号为差分式,通信介质可以采用双绞线,同轴电缆和光导纤维,一般采用双绞线。
  • 总线电平:显性(Dominant): 0, 隐性(Recessive): 1, CAN总线的信号电平具有线与特性,即显性电平0总是会掩盖隐性电平1,如果不同的节点同时发送显性和隐性电平,总线总是表现出显性电平。只有所有节点发送隐性电平是,总线才表现为隐性。线与特性是CAN总线仲裁机制的电路基础
  • 高速CAN:总线通信速度最高1Mbp(40m条件下)。高速CAN时,CANH与CANL电压相同时为逻辑“1” (CANH=CANL=2.5V)。CANH和CANL电压相差2V时为逻辑“0” (CANH=3.5V, CANL=1.5V)。高速CAN收发器在共模电压范围内(-12V~12V),将CANH和CANL电压差大于0.9V成为显性状态,将CANH和CANL电压差小于0.5定义为隐性状态。

高速CAN信号电平( ISO 11898-2)

  • 低速CAN:定义CANH和CANL电压相差 5V (CANH = 0V, CANL = 5V)时为逻辑“1”,相差 2.2V (CANH = 3.6V, CANL = 1.4V)时为逻辑“0”。

低速CAN信号电平( ISO 11898-3)

CAN报文帧结构:在CAN总线上,报文以“帧”的形式发送,每个报文帧包含以下部分:

  • 帧起始:总线空闲时为隐性状态,帧起始由单个显性位构成,标志报文开始,在总线上起同步作用。
  • 仲裁段:仲裁段由报文的标识符完成,即ID,标准CAN的标识符为11位,扩展CAN中为29位。
  • 控制段:主要定义了数据域的字节长度,通过数据长度码,接收节点可以判断报文数据是否完整。
  • 数据域:主要包含0~8个字节数据。
  • CRC域:循环冗余码校验
  • 帧结束:由一串七个隐性位组成,表示报文帧的结束。

CAN报文帧种类

(1)数据帧:由发送节点发出,包含0 - 8个数据字节。

(2)远程帧:发送远程帧向网络节点请求发送某一标识符的数据帧。

(3)错误帧:总线节点发现错误时,以错误帧的方式通知网络上的其他节点。

(4)过载帧:发送过载帧,表示当前节点不能处理后续的报文(如帧延迟等)。

SPI协议

SPI(Serial Peripheral interface)串行外围设备接口,该接口主要用在EEPROM,Flash,实时时钟,AD转化器,数字信号处理器和数字信号解码器之间。

  • 该协议是一种高速的,全双工同步串行通信总线。
  • 没有速度限制,一般可达到10Mbps。
  • 主要有四根线:MOSI, MISO,SCLK,CS(片选,选择从设备)。数据在上升下降沿改变。
  • 优点:全双工通信,简单,传输速率快
  • 缺点:没有指定的流控制,没有应答机制确认是否收到数据,相对IIC可靠性不高。
  1. SPI的四种模式:
    SPI有四种工作模式,SPI为了和外设进行数据交换,根据外设工作要求,其输出串行同步时钟的极性(CPOL)和时钟相位(CPHA)可以进行配置。CPOL决定SPI空闲时,时钟信号的电平。CPHA决定SPI在SCLK的第几个边沿开始采样数据。
  • CPOL=0:串行同步时钟的空闲状态是低电平
  • CPOL=1:串行同步时钟的空闲状态是高电平
  • CPHA=0:SPI在串行同步时钟的第一个跳边沿开始采样
  • CPHA=1:SPI在串行同步时钟的第二个跳边沿开始采样

DMA

DMA(Direct Memory Access),即直接存储器访问,DMA的传输方式可以无需CPU直接进行控制传输,也没有中断处理方式那样的保护现场和恢复现场的过程。通过硬件为RAM与I/O设备开辟一条直接传输数据的通路,可以使CPU的效率大为提高。

  • 作用: 用于内存和内存之间或内存和外设之间的高速数据传输。
  1. DMA传输包括哪些操作?(STM32为例)
    每一次DMA传输包括三个操作
    • 通过DMA的寄存器寻址,从外设数据寄存器或存储器单元加载数据。
    • DMA计数器在数据存储结束后递减,该计算器中包含仍需执行的事务数目。

产生事件后,外设会向DMA控制器发送请求信号,DMA控制器根据通道优先级处理该请求,只要DMA控制器访问外设,DMA控制器就会向外设发送确认信号,外设获得确认信号后,便会立即释放请求。一旦外设使请求失效,DMA就会释放确认信号。如果有更多的请求,外设可以启动下一个事务。

C语言

Static关键字作用

Const关键字作用

Voilatile关键字作用

C语言基础-嵌入式面试

1. C和C++区别:

  • C语言:面向过程的语言,其核心关注于问题是如何被解决的,把实现一个软件功能的过程分为一个个过程。 例如汽车要去加油,其过程为:汽车启动->汽车行驶->汽车加油。在这里我们不关注物件本身(汽车这个对象),默认定义执行该过程的主题是汽车。
  • C++语言: 面向对象的语言,在计算机科学中的对象既可以表示客观世界的问题空间(namespace)中的具体的某个事物,又可以表示软件系统解空间的基本元素,如变量、数据结构、函数等。例如汽车要去加油,汽车<启动,开车,加油>, 我们关注物件(对象)本身,只需要考虑什么时间干什么事。启动,开车,加油属于这个物件的基本属性。

C语言基础部分:

  • 标识符: 一个标识符以字母或者下划线开始的后面可以跟多个字母、数字、下划线。C标识符中不允许出现标点字符,并且区分大小写。
  • 关键字: C中的保留字,这些保留字不能作为常量名,变量名和其他标识符的名称。常用的的关键字包括:while, do, for, break, continue, default, goto, char, double, int, long, short, float,
关键字 说明
auto 声明自动变量
const 定义常量,如果一个变量可以被const修饰,那么它的值就不能被在改变
enum 声明枚举类型
extern 声明变量或函数是在其他文件或本文件其他位置定义
register 声明寄存器变量
sizeof 计算数据类型或变量长度(即所占的字节数)
static 声明静态变量
typeof 给数据类型取别名
union 声明共用体类型
void 声明函数无返回值或者无参数,声明无类型指针
volatile 说明变量在程序执行中可以被隐含的改变
  • **数据类型: ** C语言有四种数据类型:
    • **基本数据类型: **他们是算术类型,包括整数类型和浮点类型,注意char是属于整数类型的。 基本数据类型占用的空间(64位机器):char-1字节,int-4字节, float-4字节,double-8字节。
    • 枚举类型: 他们也是算术类型,被用来定义在程序中,只能赋予其一定的离散整数值的变量
    • void类型: 类型说明符 void表明没有可用的值
    • 派生类型: 它们主要包括:指针,数组,结构,共用体,和函数类型
    • 强制类型转换: (类型说明符)(表达式)

变量:

变量是程序可操作的存储区名称。全局变量保存在内存的全局存储区中,占用静态的存储单元;局部变量保存在栈中,只有在所在函数被调用的时候才动态地为变量分配存储单元。

C语言经过编译后将内存分为一下几个区域:

  • 栈(stack): 由编译器进行管理,自动分配和释放,存放函数调用过程的各种参数,局部变量,返回值以及函数的返回地址。操作方式类似于数据中的栈。
  • 堆(heap): 用于程序动态申请分配和释放空间。C语言中的malloc和free,C++中的new和delete均是在堆中进行的。正常情况下,程序员申请的空间在使用结束后应该释放,若程序员没有释放空间,则在程序结束后由系统自动回收。注意:这里的对并不是数据结构中的堆。
  • 全局(静态)存储区: 分为DATA和BSS段,DATA段(全局初始化区)存放初始化的全局变量和静态变量;BSS段(全局未初始化分区) 存放未初始化的全局变量和静态变量。程序运行结束时自动释放,其中在BSS段在程序执行之前会被系统自动清零,所以未初始化的全局变量和静态变量在程序执行之前已经为0.
  • 文字常量区:存放常量字符串,程序结束由系统释放。
  • 程序代码区:存放程序的二进制代码。

因此,C语言的全局变量和局部变量在内存之中是有区别的,C语言的全局变量包括外部变量和静态变量,均是保存在全局存储区中,占用永久性的存储单元;局部变量,即自动变量,保存在栈中,只有在所在的函数被调用时才有系统动态在栈中分配临时性的存储单元。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <stdlib.h>
int k1 = 1;
int k2;
static int k3 = 2;
static int k4;
int main( )
{ staticint m1=2, m2;
inti=1;
char*p;
charstr[10] = "hello";
char*q = "hello";
p= (char *)malloc( 100 );
free(p);
printf("栈区-变量地址 i:%p\n", &i);
printf(" p:%p\n", &p);
printf(" str:%p\n", str);
printf(" q:%p\n", &q);
printf("堆区地址-动态申请:%p\n", p);
printf("全局外部有初值 k1:%p\n", &k1);
printf(" 外部无初值 k2:%p\n", &k2);
printf("静态外部有初值 k3:%p\n", &k3);
printf(" 外静无初值 k4:%p\n", &k4);
printf(" 内静态有初值 m1:%p\n", &m1);
printf(" 内静态无初值 m2:%p\n", &m2);
printf("文字常量地址 :%p, %s\n",q, q);
printf("程序区地址 :%p\n",&main);
return0;
}

常量

常量是固定值,在程序的执行期间不会改变。它可以是任意基本的数据类型,整数常量,浮点常量,字符常量,枚举常量等。

  • 定义常量: 定义常量有两种方法,使用#define或者使用const关键字,这两种方法从本质上是不同的。#define 是宏定义,它不能定义常量,但宏定义可以实现在字面意义上和其它定义常量相同的功能,本质的区别就在于 #define 不为宏名分配内存,而 const 也不为常量分配内存,怎么回事呢,其实 const 并不是去定义一个常量,而是去改变一个变量的存储类,把该变量所占的内存变为只读!

    示例
    #define length 10
    #define NEWLINE ‘\n’

    const int var=5;
  • 整数常量:整数常量可以是十进制,八进制,十六进制的常量,前缀指定基数,0x/0X表示十六进制,0表示八进制,不带前缀默认代表十进制。整数常量也可以带一个后缀,U代表无符号整数, L代表长整数,大小写和顺序任意。

C++语言的三大特性

  • 封装: C++语言支持数据封装,类是数据封装的工具,对象是数据封装的实现,在封装中,还提供一种对数据访问控制的机制,是的一些数据隐藏在封装体内,具有隐藏性。封装和外接的信息交换是通过操作接口进行的,这种访问的控制机制体现在类的共有成员(public),私有成员(private),和保护成员(protected)上。私有成员只有类内说明的一些函数才能访问;共有成员类外的函数也可以访问,保护成员只有该类的成员函数和派生类才能访问。
  • 继承: C++语言允许单继承和多继承。一个类可以根据需要生成它的派生类,派生类还可以再生成派生类。派生类可以继承基类的成员同时也可以定义自己的成员。继承是实现数据抽象和共享的一种机制。该机制可以避免不能重复利用程序导致的资源浪费。
  • 多态: 多态指的是对不同类发出的相同消息会有不同的实现。多态性也可以理解为,在一般类中定义的属性和服务被特殊类继承后,可以具有不同的数据类型或不同的实现。简单来说,多态性指的是发出同样的消息被不同的数据类型的对象接收后会导致不同的行为。C++支持多态性主要表现在:C++语言允许函数重载和运算符重载;C++语言通过定义虚函数来支持动态联编。多态特性的工作依赖虚函数的定义,在需要解决多态问题的重载成员函数前,加上virtual关键字,那么该成员函数就变成了虚函数,从上例代码运行的结果看,系统成功的分辨出了对象的真实类型,成功的调用了各自的重载成员函数。

Autosar学习

应用软件层AppL

应用软件层最重要的就是SWC, 而SWC之间的通信需要接口,每个SWC由runnable组成,所以应用软件层的组成主要分为三个部分:

  • 应用软件组件(SWC)
  • AutoSAR的接口(Ports)和连接器(Connector)
  • 可运行实体(Runnable)

例子:汽车顶灯控制
汽车的顶灯一般有三种模式:常闭,常开,随着车的开关而开关的模式。实现汽车顶灯的控制需要传感器,处理单元和执行器。假设左右两个车门,左右两个车灯,一个开关传感器。该车顶的的控制需要7个SWC实现。但是这些SWC并非由一个ECU完成。

SWC之间的通信是通过虚拟功能总线(VFB)实现,该总线是片内外通信的结合体:

  • 在片内通过RTE通信。每个SWC可以理解为一个.c文件,C文件之间的通信通过全局变量进行。
  • 在片外通过片外的总线进行通信。常用的就是CAN总线。

在实际的汽车中,上述的7个SWC将被分配到两个ECU中。车灯开关,调光控制器和左右顶灯由车身顶部的ECU控制;左右车门和车门开关逻辑单元由专用的车门ECU芯片控制。两个ECU即连个控制器,分别位于车身前部的车门控制器和车身顶部的顶灯控制器。ECU内部通信通过RTE进行管理,跨ECU通信通过外部CAN总线进行。

SWC的类型

SWC的类型总共有三种:原子级SWC(Atomic SWC),集合级SWC(Composition SWC),特殊的SWC。
原子级SWC(Atomic SWC): 最小单元,不可再拆分,每个原子级SWC对应一个.c文件。比其更小的单元是runnable,即.c文件中的函数。每个SWC的功能基本上都是用来实现特定的算法。

Atomic SWC

集合级SWC(Composition SWC):该级别的SWC将很多功能相近或者需要整合到一处的Atomic SWC整合起来,方便SWC归类。

集合级SWC类似于一个文件夹,用于存放相近功能的Atomic SWC.

特殊SWC:在实际的工程中,不止应用层需要设计SWC, 在基础软件层中,IO硬件抽象成层和复杂驱动(CDD)都需要手动添加代码。他们被看做一种特殊的SWC进行操作。

接口(Ports)的类型

Ports存在于SWC之间作为通信的通道。或者SWC通过RTE和BSW做接口通信使用。Ports共有五种类型,如下图所示:

S/R接口:用于传输数据。该数据传输过程通过RTE进行管理,避免数据出错。如同时调用同一数据可能出错。在数据传输时,一个接口可以包含多个数据,类似于通过结构体的传输。可以传输的数据类型包括基础的如int, float等,以及复杂数据类型如record, array等。

C/S接口:该接口的作用是提供操作,即Server提供函数供Client调用。调用的过程分为同步,异步两种。同步代表直接调用,相当于函数直接插入上下文运行;异步需要灯带,相当于函数在另一个线程中运行,不影响原线程运行。C/S接口的可以提供多个操作(函数),可在ECU内部或者跨ECU调用。

可运行实体(Runnable)

Runnable即SWC中的函数,在AutoSAR架构被DaVinci软件生成时,Runnable是空函数,需要手动添加代码实现相应功能。Runnable可以被触发,例如定时器触发,操作调用触发,以及接受数据触发等。

实时运行环境(RTE)

RTE是AutoSAR架构中介于应用层和基础软件层之间,是AutoSAR虚拟功能总线VFB的接口的实现。其目的是为应用软件的SWC之间的通信提供基础设施服务,并促进包括OS在内的基础软件组建的访问。

RTE在Vector的工具链中是自动生成的,它的作用包括:

  • 提供ECU内部或者跨ECU的通信管理,通过VFB, RTE就是VFB的具体实现。
  • 提供对Runnable的管理功能,包括触发,唤醒等。即RTE可以把Runnable配置到OS对应的Task中去,生成的Task代码通过RTE的时间触发runnables的运行。注意这里RTE抽象了OS,防止SWC直接访问OS和BSW。

下图是车顶的控制系统中SWC与RTE以及BSW的组件之间的关系。

RTE对Runnables的运行支撑

RTE作为运行环境的主要功能有:

  • 通过RTE给runnables提供触发时间
  • 通过RTE给runnables提供所需的资源
  • 将BSW和SWC做隔绝:即runnables的运行条件由RTE提供,不能由OS直接提供。

Runnables的触发条件
Runnables在设计时,需要考虑触发条件,负责无法运行。触发条件即一些特定的事件。AutoSAR中主要规定了以下触发事件:

  • 初始化事件:初始化自动触发
  • 定时器事件:给定一个周期性定时器,计时完成自动触发
  • 接受数据事件(S/R): Receiver Ports接受数据即可触发
  • 接受数据错误事件
  • 数据发送完成事件:Send Ports发送完成即可触发
  • 操作调用事件(C/S):
  • 模式切换事件
  • 模式切换应答事件
  • 异步服务返回事件:C/S可在异步下运行,即在异步调用Server函数时,被调用函数作为一个线程与当前程序并行运行,运行结束后返回时,将会触发异步服务返回事件。

RTE对Ports的支持

RTE可以作为SWCs和BSW之间的交流途径:

  • 作为VFB的具体实现
  • 作为S/R接口的通信实现
  • 作为C/S接口的通信实现
  • ECU内部通信/跨ECU通过COM口通信
  • 实现AR-COM的回调功能,具体实现是在SWC中完成的,RTE负责完成这个回调机制
    其他特征:
  • RTE提供了一种实现数据一致性的机制,即避免多个SWC同时操作一个数据出现问题。
  • RTE支持简单和复杂的数据类型
  • 对SWC的类型进行实例化

R/S接口的实现
在配置好DaVinci后,RTE会自动生成一些调用,在runnable上方,可以直接复制。下面是一个例子:

/**********************************************************************************************************************
 *
 * Runnable Entity Name: RAB_Core0_100us
 *
 *---------------------------------------------------------------------------------------------------------------------
 *
 * Executed if at least one of the following trigger conditions occurred:
 *   - triggered on TimingEvent every 100us
 *
 **********************************************************************************************************************
 *
 * Input Interfaces:
 * =================
 *   Explicit S/R API:
 *   -----------------
 *   Std_ReturnType Rte_Read_AppPI_Can_ReceiverCore0_DEP_Can_Receiver(Idt_Can_Receiver *data)
 *
 * Output Interfaces:
 * ==================
 *   Explicit S/R API:
 *   -----------------
 *   Std_ReturnType Rte_Write_AppPI_Can_SenderCore0_DEP_Can_Sender(Idt_Can_Sender data, Rte_TransformerError *transformerError)
 *
 * Service Calls:
 * ==============
 *   Service Invocation:
 *   -------------------
 *   Std_ReturnType Rte_Call_ComM_UserRequest_GetCurrentComMode(ComM_ModeType *ComMode)
 *     Synchronous Service Invocation. Timeout: None
 *     Returned Application Errors: RTE_E_ComM_UserRequest_E_NOT_OK
 *   Std_ReturnType Rte_Call_ComM_UserRequest_GetMaxComMode(ComM_ModeType *ComMode)
 *     Synchronous Service Invocation. Timeout: None
 *     Returned Application Errors: RTE_E_ComM_UserRequest_E_NOT_OK
 *   Std_ReturnType Rte_Call_ComM_UserRequest_GetRequestedComMode(ComM_ModeType *ComMode)
 *     Synchronous Service Invocation. Timeout: None
 *     Returned Application Errors: RTE_E_ComM_UserRequest_E_NOT_OK
 *   Std_ReturnType Rte_Call_ComM_UserRequest_RequestComMode(ComM_ModeType ComMode)
 *     Synchronous Service Invocation. Timeout: None
 *     Returned Application Errors: RTE_E_ComM_UserRequest_E_MODE_LIMITATION, RTE_E_ComM_UserRequest_E_NOT_OK
 *
 *********************************************************************************************************************/
/**********************************************************************************************************************
 * DO NOT CHANGE THIS COMMENT!           << Start of documentation area >>                  DO NOT CHANGE THIS COMMENT!
 * Symbol: RAB_Core0_100us_doc
 *********************************************************************************************************************/


​ /**********************************************************************************************************************
​ * DO NOT CHANGE THIS COMMENT! << End of documentation area >> DO NOT CHANGE THIS COMMENT!
/

FUNC(void, SWCCore0Basic_Type_CODE) RAB_Core0_100us(void) /* PRQA S 0850 / / MD_MSR_19.8 */
{
/
*
* DO NOT CHANGE THIS COMMENT! << Start of runnable implementation >> DO NOT CHANGE THIS COMMENT!
* Symbol: RAB_Core0_100us
*********************************************************************************************************************/


​ /**********************************************************************************************************************
​ * DO NOT CHANGE THIS COMMENT! << End of runnable implementation >> DO NOT CHANGE THIS COMMENT!
​ *********************************************************************************************************************/
​ }

直接调用: 相当于有一个全局变量,runnable可以直接读写这个变量。

其写法采用一下语法:(指的是全局变量的名字,data指的是局部变量。这些函数都是在runnable中使用的)

Std_ReturnType Rte_Read_<port>_<data> (<DataType> *data)
Std_ReturnType Rte_Write_<port>_<data> (<DataType> data)

缓存调用: 该调用方式相当于先将全局变量复制到一个runnable的局部变量中,然后对局部变量进行操作,最后把这个局部变量赋值到全局变量中。

使用方法:

<DataType> Rte_IRead_<r>_<port>_<data> (void)
void Rte_IWrite_<r>_<port>_<data> (<DataType> data)

队列调用:

RTE数据一致性的实现

RTE对Interface的支持

基础软件层(BSW)

C++:vector容器

二维向量的遍历方法

1
2
3
4
5
6
7
8
9
//二维向量的遍历,迭代器遍历
vector<vector<int> > array = {{1,2,8,9},{2,4,9,12},{4,7,10,13},{6,8,11,15}};
vector<vector<int >>::iterator iter;
for (iter = array.begin(); iter != array.end() ; ++iter) {
for (int i = 0; i < (*iter).size(); ++i) {
cout << (*iter)[i] << " " ;
}
cout << endl;
}
1
2
3
4
5
6
7
8
9
//二维向量的遍历,数组下表
vector<vector<int> > array = {{1,2,8,9},{2,4,9,12},{4,7,10,13},{6,8,11,15}};

for (int i = 0; i < array.size(); i++)
{
for(int j = 0; j < array[i].size(); j++)
cout << V[i][j] << " ";
cout << endl;
}

算法:判断质数

质数定义:指在大于1的自然数中,除了1和该数自身外,无法被其他自然数整除的数(也可定义为只有1与该数本身两个正因数的数)。

思路1:定义

根据定义,采用2到n-1中的每一个数去除n,如果不能被整除,则该数为质数。时间复杂度:O(n)。

1
2
3
4
5
6
7
8
9
10
11
12
bool is_prime(int n)
{
bool isprime = true;
for(int i=2; i < n; i++){
if(n % i==0){
isprime=false;
break;
}
}
return isprime;
}

思路2:质数筛选定理

质数筛选定理: 如果n不能够被不大于根号n的任何质数整除,则n是一个质数。
参考链接:

1
2
3
4
5
6
7
8
9
10
11
bool is_prime(int n){
bool isprime= true;
int max = static_cast<int>(sqrt(n)); \\隐式类型转换
for(int j=2; j<=max; j++){
if(n%j == 0){
isprime=false;
break;
}
}
return isprime;
}