MDK程序组成、存储与运行

MDK程序的编译过程


MDK程序从C/C++代码、汇编代码到二进制文件经历一下几个步骤:

MDK编译过程

经过编译器处理得到了.o对象代码,然后使用链接器链接得到了.axf映像文件,这时在映像文件中给变量分配好了地址。得到.axf文件后,只需要通过fromelf格式转换便可得到最终写入到flash中的程序了

MDK程序的存储与运行


单片机程序考虑加载(Loading)和执行(Executing)两种状态,下图为两种状态下数据存储情况

程序加载、运行视图

在这之前先介绍下MDK的数据区域类型:

CODE:代码域,存放在ROM

RO-Data:常量

RW-Data:初始值非零的全局变量

ZI-Data、堆栈空间:初始值为零的全局变量、局部变量、使用malloc动态分配的变量

程序加载阶段,数据存放在只读区域(RO section)、只写区域(RW section)两块区域,由上图可知,数据(Code、RO data和RW data)都存放在这两块区域中,都存放在ROM中

程序运行阶段,在RW section中的数据赋值到SRAM中,另外SRAM中还包括初始化为零的ZI data,RO section的数据仍然保留在ROM中

不同区域存放的数据类型

总结


理解MDK程序的编译、运行过程有助于理解程序运行的本质,是实现程序裁剪的基础,同时对后续Linux的学习也有一定的帮助。

freeRTOS中Task学习笔记

下文为学习韦东山老师《FreeRTOS入门与工程实践-基于STM32F103》的课程学习思考,仅纪录自己的阶段性学习成果。

创建任务


首先,假设我们现在要在裸机编程的基础上实现两个任务taskAtaskB并发运行,两个任务功能都是在lcd上显示一些数据,我们知道,为了让两块区域同时显示,调度器会周期性地分配给两个任务各自的运行时间(时间片轮转),那么势必会产生一个问题,切换的时机不是我们期望的时机(如在I2C数据传输过程中),这样会导致数据传输错误。为了解决这个问题,我们先考虑使用简单的全局变量互斥锁,从而实现当一个task跑完,全局变量完成翻转。然而当我们实验时,发现程序卡死在某一个任务,另一个任务一直不会运行,这是因为打印屏幕所需的时间长,任务切换的时间发概率就发生在这个阶段,也就是说全局变量翻转前,这个时候实现了任务切换,从而导致错误。知道这个原因后,我们可以添加一个延时函数,可以提高任务成功切换的概率。

创建任务

对于以上问题,我们提出两个问题:

问题一:怎么防止执行的任务被切换?

开启临界区

问题二:既然使用全局变量会带来这些问题,那么怎么实现任务间互斥的访问?

任务间互斥的访问,使用消息队列信号量

删除任务


当我们丢弃不使用的任务时,可以粗暴的删除任务,这会带来两个问题,

问题一:频繁的创建、删除产生的内存碎片如何解决?

针对这个问题,freeRTOS提供了专门的内存管理策略,一般使用heap.4策略

问题二:当任务删除后,堆栈的清理工作谁来负责完成?

当任务自我删除时,现场的清理工作由空闲函数来实现;当taskA由taskB删除时,那么taskB负责完成taskA的现场清理工作

任务状态


以下是任务状态切换的API函数

任务状态和调度

问题:任务阻塞和暂停的异同点?

首先任务阻塞和暂停都意味着放弃CPU资源,但是恢复方式不同,任务阻塞的恢复方式为被动恢复,需要等待其他事件(如信号量、消息队列等),而暂停是主动的,可由其他任务调用触发。

总结


以上内容比较零散,仅仅作为个人笔记记录,不适合分享交流。

单片机中常见的通信协议

在单片机应用中,通信协议是不可缺少的一部分,上位机与下位机、单片机与单片机、单片机与外设模块之间的通信都需要通信协议来实现信息交换和资源共享。由于设备之间的传输速率、电气特性、可靠性要求等不同,也产生了很多适用于不同情况的通信协议,并被广泛接受和使用。其实最经常使用的有以下几种:

USRT和USART


分别称为通用异步收发器和通用同步异步收发器。速度不快,可以全双工。结构一般由波特率速率生成器、UART/USART发送器,UART/USART接收器组成。硬件方面有两条线组成,一条挨着一条

UART

I2C


双向、双线、串行、多主接口标准。速度不快,半双工,同步接口,具有总线仲裁机制,非常适合设备间短距离频繁数据通讯,可实现设备联网。

总线仲裁:线与,谁发送0谁就仲裁成功

I2C

SPI


高速同步串口、高速、全双工、独立收发、同步接口,可实现多个SPI设备的互联,硬件3~4条线;也是所有基于SPI的设备所共有的,他们分别是SDI(数据输入)、SDO(数据输出)、SCK(时钟)、CS(片选)

SPI

CAN


采用两线传输,两根线分别作为CAN_H和CAN_L,接线端子处有120欧电阻。当CAN收发器接收到总线信号时,先将信号电平转换成逻辑状态,即CAN_H与CAN_L电平相减后,得到一个内插电平。各种扰动对两条线的影响是一样的,而相减后的内插电平可以滤除这些扰动。

CAN

RS-485


在要求通信距离为几十米到上千米时,广泛采用RS-485串行总线标准。RS-485采用平衡发送和差分接收,因此具有抑制共模干扰的能力。

RS232串口可以使用芯片转换电平来实现互转。

RS-485

总结


以上只是简单的列出了最常见的一些协议,具体的学习建议去了解官方文档

C语言中静态、动态属性

编程语言分为静态语言动态语言,静态语言在编译时进行类型检查,不允许在运行时修改变量类型,通常具有更严格的语法和较慢的开发速度,但在运行时具有更高的效率,常见的静态语言有C、C++、Java、Rust等;而动态语言的类型检查发生在运行时,因此具有较为宽松的语法规则和更快的开发速度,但这牺牲了程序的运行速度,常见的动态语言有Python、JavaScript、Ruby等。

那么C语言中有哪些静态/动态的术语呢?以下简述C语言的部分动态/静态属性:

静态依赖 vs. 动态依赖


在程序的世界里,依赖通常指的是一个软件组件(如库、框架、模块或软件包)依赖于另一个组件才能正常工作的情况,通过静态、动态两个属性又可以分为静态依赖、动态依赖。静态依赖 通常指程序在编译时用到的一些资源,比如头文件、接口定义等;而动态依赖是程序执行期间需要记载的依赖,通常是另一个组件的库、框架,一般用来提高软件的灵活性和适应性,如Win下的.dll和Linux下的.so文件。

静态调用 vs. 动态调用


正常的函数调用既是一种静态调用,相比于正常的函数调用,当一个函数作为参数去实现另一个函数的功能时(称为回调函数),此时函数指针可以动态地指向不同的函数,允许在程序运行时更改函数,因此这种调用称为动态调用

静态多态 vs. 动态多态


静态(编译时)多态

由编译器解释的多态为静态多态,这里介绍下重载解析模板解析。首先说一下重载解析的工作,在这个过程中,编译器会首先识别出函数名称相同但是参数列表不同的重载函数,接下来编译器会根据参数列表匹配最合适的函数,然后进行类型检查,最后所有工作完成后,来实现函数的调用。然后是函数的模板解析,当遇到模板调用(例如std::vector<int>)时,编译器首先创建该模板的一个具体实例,然后推断出模板参数的类型(例如intstd::vector<int>中),完成后模板的参数被替换为推导出的类型。

以上两种方式都实现了函数的静态多态

动态(运行时)多态

动态多态主要是虚函数的实现,对于每个派生类,编译器都会创建一个虚表(虚表是一个包含虚函数实现的指针的表,每个对象的第一个成员都是一个指向其虚表的指针),当调用虚函数时,编译器会使用对象的虚函数指针查找虚表中的正确函数指针,然后再调用该函数。

静态内存分配 vs.动态内存分配


静态内存分配在编译时,编译器便为变量分配好了大小和位置,其中初始值为零的全局变量和静态变量放在BSS段、初始值非零的全局变量和静态变量放在Data段,代码放在Code段,函数形参和局部变量放在栈区;动态内存分配发生在运行阶段,需要使用 malloccallocrealloc 等函数从堆中分配内存,其分配的内存大小在程序运行时才能够确定。分配情况如下图:

C语言变量内存分配

总结


C语言的静态、动态属性贯穿学习C语言的整个时期,从基本语法、面向对象一些特性到内存管理的底层机制,能把这个属性弄清楚是非常具有挑战性事情,希望以上内容能对你有所帮助