• 总结嵌入式C语言难点 (1部分) 【结尾有资料】


    大家好!

    有人说C语言是非常简单的,也有人说学了十年还是没有学明白。事实上,编写优质嵌入式C程序并非易事,需要了解相关硬件特性和缺陷,还需要了解相应地编译原理。

    关键字

    几乎每一门语言中都有关键字,具有特殊功能,C语言也不例外,按照功能可分为:

    • 数据类型(常用char, short, int, long, unsigned, float, double)

    • 运算和表达式( =, +, -, *, while, do-while, if, goto, switch-case)

    • 数据存储(auto, static, extern,const, register,volatile,restricted)

    • 结构(struct, enum, union,typedef)

    • 位操作和逻辑运算(<<, >>, &, |, ~,^, &&)

    • 预处理(#define, #include, #error,#if...#elif...#else...#endif等)

    • 平台扩展关键字(__asm, __inline,__syscall)

    这些关键字共同构成了嵌入式平台的C语言语法,嵌入式的应用从逻辑上可以抽象为以下三个部分:

    • 数据的输入(如传感器,信号,接口输入)

    • 数据的处理(如协议的解码和封包,AD采样值的转换等)

    • 数据的输出(GUI的显示,输出的引脚状态,DA的输出控制电压,PWM波的占空比等)

        贯穿在整个嵌入式应用开发的过程中,对数据的管理包含以下几部分:

    • 数据类型

    • 存储空间

    • 位和逻辑操作

    • 数据结构

    为了应对嵌入式开发中受限的资源环境,C语言从语法上支撑上述功能的实现,并提供相应的优化机制。

    数据类型

    C语言支持常用的字符型,整型,浮点型变量,有些编译器如keil还扩展支持bit(位)和sfr(寄存器)等数据类型来满足特殊的地址操作。C语言只规定了每种基本数据类型的最小取值范围,因此在不同芯片平台上相同类型可能占用不同长度的存储空间,这就需要在代码实现时考虑后续移植的兼容性,而C语言提供的typedef就是用于处理这种情况的关键字,在大部分支持跨平台的软件项目中被采用,典型的如下:

    既然不同平台的基本数据宽度不同,那么如何确定当前平台的基础数据类型如int的宽度,这就需要C语言提供的接口sizeof,实现如下。

     这里还有重要的知识点,就是指针的宽度,如

    其实这就和芯片的可寻址宽度有关,如32位MCU的宽度就是4,64位MCU的宽度就是8,在有些时候这也是查看MCU位宽比较简单的方式。

    内存管理和存储架构

    C语言允许程序变量在定义时就确定内存地址,通过作用域,以及关键字extern,static,实现了精细的处理机制,按照在硬件的区域不同,内存分配有三种方式:

    • 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。

    • 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中 ,效率很高,但是分配的内存容量有限。

    • 从堆上分配,亦称动态内存分配。程序在运行的时候用 malloc 或 new 申请任意多少的内存,程序员自己负责在何时用 free 或 delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活,但同时遇到问题也最多。

    这里先看个简单的C语言实例。

    C语言的作用域不仅描述了标识符的可访问的区域,其实也规定了变量的存储区域,在文件作用域的变量st_val和ex_val被分配到静态存储区,其中static关键字主要限定变量能否被其它文件访问,而代码块作用域中的变量a, ptr和local_st_val则要根据类型的不同,分配到不同的区域,其中a是局部变量,被分配到栈中,ptr作为指针,由malloc分配空间,因此定义在堆中,而local_st_val则被关键字限定,表示分配到静态存储区,这里就涉及到重要知识点,static在文件作用域和代码块作用域的意义是不同的:在文件作用域用于限定函数和变量的外部链接性(能否被其它文件访问), 在代码块作用域则用于将变量分配到静态存储区。

    对于C语言,如果理解上述知识对于内存管理基本就足够。

    但对于嵌入式C编程来说,定义一个变量,它不一定在内存,也就是SRAM中,也有可能在FLASH空间,或直接由寄存器存储(register定义变量或者高优化等级下的部分局部变量),如定义为const的全局变量定义在FLASH中,定义为register的局部变量会被优化到直接放在通用寄存器中,在优化运行速度,或者存储受限时,理解这部分知识对于代码的维护就很有意义。

    此外,嵌入式C语言的编译器中会扩展内存管理机制,如支持分散加载机制和__attribute__((section("用户定义区域"))),允许指定变量存储在特殊的区域如(SDRAM, SQI FLASH), 这强化了对内存的管理,以适应复杂的应用环境场景和需求。

    采用这种方式,我们就可以将变量指定到需要的区域,这在某些情况下是必须的,如做GUI或者网页时因为要存储大量图片和文档,内部FLASH空间可能不足,这时就可以将变量声明到外部区域,另外内存中某些部分的数据比较重要,为了避免被其它内容覆盖,可能需要单独划分SRAM区域,避免被误修改导致致命性的错误,这些经验在实际的产品开发中是常用且重要,不过因为篇幅原因,这里只简略的提供例子,如果工作中遇到这种需求,建议详细去了解下。

    至于堆的使用,对于嵌入式Linux来说,使用起来和标准C语言一致,注意malloc后的检查,释放后记得置空,避免"野指针“,不过对于资源受限的单片机来说,使用malloc的场景一般较少,如果需要频繁申请内存块的场景,都会构建基于静态存储区和内存块分割的一套内存管理机制,一方面效率会更高(用固定大小的块提前分割,在使用时直接查找编号处理),另一方面对于内存块的使用可控,可以有效避免内存碎片的问题,常见的如RTOS和网络LWIP都是采用这种机制,我个人习惯也采用这种方式,所以关于堆的细节不在描述,如果希望了解,可以参考中关于存储相关的说明。

    指针和数组

    数组和指针往往是引起程序bug的主要原因,如数组越界,指针越界,非法地址访问,非对齐访问,这些问题背后往往都有指针和数组的影子,因此理解和掌握指针和数组,是成为合格C语言开发者的必经之路。

    数组是由相同类型元素构成,当它被声明时,编译器就根据内部元素的特性在内存中分配一段空间,另外C语言也提供多维数组,以应对特殊场景的需求,而指针则是提供使用地址的符号方法,只有指向具体的地址才有意义,C语言的指针具有最大的灵活性,在被访问前,可以指向任何地址,这大大方便了对硬件的操作,但同时也对开发者有了更高的要求。

    参考如下代码:

    对于数组来说,一般从0开始获取值,以length-1作为结束,通过[0, length)半开半闭区间访问,这一般不会出问题,但是某些时候,我们需要倒着读取数组时,有可能错误的将length作为起始点,从而导致访问越界,另外在操作数组时,有时为了节省空间,将访问的下标变量i定义为unsigned char类型,而C语言中unsigned char类型的范围是0~255,如果数组较大,会导致数组超过时无法截止,从而陷入死循环,这种在最初代码构建时很容易避免,但后期如果更改需求,在加大数组后,在使用数组的其它地方都会有隐患,需要特别注意。

    指针占有的空间与芯片的寻址宽度有关,32位平台为4字节,64位为8字节,而指针的加减运算中的长度又与它的类型相关,如char类型为1,int类型为4,如果你仔细观察上面的代码就会发现par的值增加了8,这是因为指向指针的指针,对应的变量是指针,也就是长度就是指针类型的长度,在64位平台下为8,如果在32位平台则为4,这些知识理解起来并不困难,但是这些特性在工程运用中稍有不慎,就会埋下不易察觉的问题。另外指针还支持强制转换,这在某些情况下相当有用,参考如下代码:

      基于指针的强制转换,在协议解析,数据存储管理中高效快捷的解决了数据解析的问题,但是在处理过程中涉及的数据对齐,大小端,是常见且十分易错的问题,如上面arr字符数组,通过__align(4)强制定义为4字节对齐是必要的,这里可以保证后续转换成int指针访问时,不会触发非对齐访问异常,如果没有强制定义,char默认是1字节对齐的,当然这并不就是一定触发异常(由整个内存的布局决定arr的地址,也与实际使用的空间是否支持非对齐访问有关,如部分SDRAM使用非对齐访问时,会触发异常), 这就导致可能增减其它变量,就可能触发这种异常,而出异常的地方往往和添加的变量毫无关系,而且代码在某些平台运行正常,切换平台后触发异常,这种隐蔽的现象是嵌入式中很难查找解决的问题。另外,C语言指针还有特殊的用法就是通过强制转换给特定的物理地址访问,通过函数指针实现回调,如下:

    这里说明下,volatile易变的,可变的,一般用于以下几种状况:

    • 并行设备的硬件寄存器(如:状态寄存器)

    • 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)

    • 多线程应用中被几个任务共享的变量

    volatile可以解决用户模式和异常中断访问同一个变量时,出现的不同步问题,另外在访问硬件地址时,volatile也阻止对地址访问的优化,从而确保访问的实际的地址,精通volatile的运用,在嵌入式底层中十分重要,也是嵌入式C从业者的基本要求之一。函数指针在一般嵌入式软件的开发中并不常见,但对许多重要的实现如异步回调,驱动模块,使用函数指针就可以利用简单的方式实现很多应用,当然我这里只能说是抛砖引玉,许多细节知识是值得详细去了解掌握的。

    【学习交流群875672513】

    【网盘免费资料包,大家需要的自行领取】:

    嵌入式物联网 22个STM32项目、大赛作品,【华清远见发放资料包】http://makerschool.mikecrm.com/f4wjYBB

     【下方分享一些嵌入式c语言资料,大家可以学习一下】:

    物联网嵌入式工程师网络基础

    学习物联网的那些坑

    STM32单片机之GPIO开发

    STM32-按键消抖

    手把手教你做一个语音控制智能灯

    物联网人脸识别考勤机

    嵌入式应用轻量级数据库。

    嵌入式开发也要懂的WEB技术

    IIC总线协议

     

  • 相关阅读:
    『无为则无心』Python面向对象 — 47、Python中的self详解
    赋能心理大模型,景联文科技推出高质量心理大模型数据库
    shell: 遍历目录下的文件并查看
    Hadoop之HDFS——【模块一】元数据架构
    智慧城市智慧灯杆IP网络广播可视紧急求助系统
    UVaLive 6693 Flow Game (计算几何,线段相交)
    2012年下半年 系统架构设计师 (案例分析)
    nodejs格式化输入
    【云原生】k8s新版本与Docker和Containerd的关联关系
    Django后台管理(二)
  • 原文地址:https://blog.csdn.net/an520_/article/details/126121738