良许Linux教程网 干货合集 嵌入式工程师必备的 8 个C语言技巧

嵌入式工程师必备的 8 个C语言技巧

作为一位从事单片机开发的工程师,我们很少有机会进行纯粹的上层软件开发,也不会完全投身于硬件电路设计。我们最常见的工作场景是这样的:左手夹着烟头,右手握着烙铁,双手搁在键盘上,不停地敲击着代码。

image-20240401214107499
image-20240401214107499

为了做一个好的设计,本身在软硬件的配合上就需要克服无数的困难和障碍,任何一名 MCU 爱好者都不希望遇到一些因为语言和工具而产生的困扰,我们在 MCU 这种资源受限的平台上进行 C语言的开发虽然被软件界看起来不怎么高大上,但是 MCU 的开发目前 C 语言还是主流,为了更好的操控和调试我们的硬件,我们还是需要竭力的避免一些 C语言编程的陷阱,避免被一些高大上的变成语言或者架构干扰产品整体的进度和可靠性。

这里就介绍几个 C语言中避坑的技巧

第1坑:不要使用“GOTO”语句

GOTO 语句最早源于汇编语言的跳转,在很多年前,计算机的变成还处于起步阶段,C语言开始也是寻着汇编的思路来设计的,因此就遗留下了这么一个 GOTO 语句,允许程序员自由的在代码间翱翔。使用GOTO语句的例子

#include 

int main() {
    int i = 0;

    // 使用goto语句的简单示例
    goto start;

loop:
    printf("Inside loop: %d\n", i);
    i++;

start:
    if (i printf("Loop finished.\n");

    return 0;
}

这种 goto 语句用起来简单,但是整体程序如果来回跳转,读起来会非常的困难,非常绕,并且 GOTO 语句还存在以下问题:

  1. 可读性差: 使用goto语句的代码通常会变得难以理解,因为它允许在程序中跳转到不同的标签位置。这使得代码流程变得不清晰,增加了理解代码的难度。
  2. 难以维护: 当代码包含大量goto语句时,很容易导致代码的维护困难。修改代码或添加新功能时,必须仔细考虑goto语句的影响,以防止引入错误。
  3. 错误的使用可能导致问题: 如果不小心使用了错误的标签,或者在不当的位置使用goto,可能导致程序的不正确行为。这种错误可能难以追踪和修复。
  4. 不利于结构化编程: 使用goto语句可能违背结构化编程的原则,使得代码难以按照清晰的结构组织。结构化编程强调使用顺序结构、选择结构和循环结构来构建清晰、可读、可维护的代码。
  5. 不利于调试: 调试时,跳转语句会使程序的执行路径变得复杂,增加了调试的难度。代码中的跳转可能使得代码不易于单步调试,阻碍了查找和修复错误的过程。

第2坑:使用完整的条件语句

在使用判断语句的时候,我们尤其要注意判断条件的完整性,我们许多工程师都熟悉简单的if else 语句,然而有一些工程师却没有注意到,不同的写法可能会浪费一些处理器的时间。比如:

if(value == 1U)
{

}

if(value == 0U)
{

}

if(value == 1U)
{

}
else 
{

}

在第一种写法中,处理器会去判断两次,然后根据判断结果进行分支运行,但是如果我们写成第二种写法,处理器只需要判断一次就可以了。尤其是这种判断在一个大循环内部,这将浪费我们很多处理器时间。

另外为了代码具备更清晰的可读性,我们应该让 if else 成对出现,并且都是用{}把程序分割开来,这样也避免我们在调试的时候复制粘贴出现一些错误,从而影响我们调试和解决问题的进度。

#include 

int main() {
    int choice;

    // 提示用户输入数字
    printf("Enter a number (1-3): ");
    scanf("%d", &choice);

    // 使用 switch 语句根据用户输入执行不同的操作
    switch (choice) {
        case 1:
            printf("You chose option 1.\n");
            // 执行操作1的代码
            break;

        case 2:
            printf("You chose option 2.\n");
            // 执行操作2的代码
            break;

        case 3:
            printf("You chose option 3.\n");
            // 执行操作3的代码
            break;

        default:
            printf("Invalid choice. Please enter a number between 1 and 3.\n");
            // 处理无效选择的代码
            break;
    }

    return 0;
}

如果判断分支比较多,一定是用 swich case 语句来代替 if else。道理是相同的,一定要完整且用{}将程序段分隔好。同时要注意,如果我们对分支的命中率有一定的前瞻性,那么我们最好把命中率比较高的分支放在前面。

对于 case 比较多的情况,有些编译器会主动优化,这时候就不必考虑命中率的问题了。

第3坑:使用FOR(;;)还是 While(1)?

MCU 的开发过程中,我们绝大部分情况下还是在使用前后台系统,当然即便我们跑了一些实时性的操作系统,也避免不了使用一些无限循环的处理。

那么处理无限循环的语句目前有两种写法,我常看到一些初级工程师会使用 while(1),而在一些操作系统源码中看到的更多的是 for(;;)。

如果在 C99 的版本下,我们使用 for 来写循环看起来更紧凑。

// while 循环的初始化
int i = 0;
while (i for 循环的初始化
for (int i = 0; i 

另外,我十几年前在赛普拉斯的单片机上开发,因为 flash 空间很小,需要极致优化代码来进行空间压缩,这里我选择了 for 循环的写法可以让空间多出一个字节来,不过现在的很多编译器都已经更新了很多年了,至少在主流的 arm 平台上他们的汇编代码都是一样的了。

第4坑:尽可能不用嵌入汇编语言

微处理器的自然语言为汇编语言指令。为低级别机器语言编程可能会为处理器提供更高效的代码。然而,人类并不是天生就会这种语言,并且经验表明,编写汇编语言会造成误解。误解会导致维护不当,更甚者,可能会使系统到处是bug,一般建议避免使用汇编语言。

image-20240401214202442
image-20240401214202442

我第一份工作是在地铁的广播系统,听说当时北京地铁一号线数字化改造就是用来单片机控制系统,当时的老工程师在天津,使用汇编写好的几页纸的汇编代码,要拿到北京进行编译,并且几乎是一次性编译通过。

这或许听起来很神话,但在我们现在在线调试工具如此丰富的今天,我们完全没必要去锻炼这种技能了。

实际上,现在大多数编译器都能编译出非常高效的代码。采用C语言或C++语言等高级语言的开发,能获得更有序的结构,便于理解和维护,使代码的整体效果更好。

第5坑:写千层饼式代码而非方便面式代码

在 MCU 开发的初级阶段,我们往往都是如初生牛犊般生猛,不管三七二一,上来就是一把梭,一气呵成的把代码写完,然后就会各种抱怨需求变更啦,增加个小功能麻烦了,等等。

image-20240401214205477
image-20240401214205477

因此,为了避免这些麻烦,我们需要养成架构的思维,先按照千层饼式的代码进行规划,然后一个模块一个模块的实现,前面慢一点,后面会走得更远。

而如果写的像泡面一样,形同乱麻会很容易出现乱码。纠缠到最后,你连重构的信心都没有了。

第6坑:要有模块化思维

MCU 的开发往往面对很多不同的平台,有早期的 8051 的,也有现在如火如荼的 ARM Cortex 系列的,但是不管哪种平台,我们本质上都是去操作他们的一些外设,那么我们针对外设的上层就会有很多可以抽象成模块的代码,比如串口的发送接受 FIFO。

再比如我们做一些数字信号处理时的一些算法,求最大最小值,一阶低通滤算法等等

我们都可以把这些小型算法抽象到一个模块中,以便于在各种不同的平台和项目中直接使用。

image-20240401214208744
image-20240401214208744

C语言编程使工程师能够将代码分成独立的功能模块,这简化了代码导航,同时还能够使工程师使用封装等面向对象技术。代码可以被组织成逻辑模块,这很有意义。虽然可能要先花点时间(几分钟),但从长远来看,这将能省掉很多漫长之夜,和很多调试之苦!

第7坑:一定要给自己定一套变量命名方式

打江山容易,守江山难,写代码时候如果一把梭爽了,后期维护代码就会很痛苦,很多时候我们真的连自己的注释都看不懂,因此让代码本身就透着意义是很重要的一种技能。

image-20240401214218454
image-20240401214218454

比如我们对于全局变量和局部变量的命名前面分别增加 g 和 m 开头。

unsinged char g_bValue = 0;

int main()
{
    short m_wCnt = 0;
}

比如在定义变量的时候根据变量的类型在变量前面增加标识:b(字节),w(字),dw(双字)

另外,对于变量的命名还需要注意其本身的意义,我们可以使用完整的英文来进行命名,如果在一个团队里面,大家可以约定有效,也可以使用一些简短的自创性的命名,当然前提是大家要有一个团队命名手册。

int Freq      //Frequency
int Btn       //Button
int MotorSta   //MotorState
int Spd       //  Speed

第8坑:少用#pragma语句

C语言中有一种特殊的#pragma语句。这些语句通常处理非标准的句法和特性,应尽可能避免使用这种语句,因为它们是非标准的,不能从一个处理器移植到另一个处理器。有些编译器可能要求用这类语句完成某项任务,例如定义一个中断服务程序,这时候我们应该将中断服务函数单独写出来,再让编译器要求的写法的函数去调用,把我们的程序和编译器特性需求给解耦合。

// 使用 typedef 定义关键字
typedef unsigned char U8;
typedef unsigned short U16;
typedef unsigned int U32;

另外,我们可以在自己的代码中自定义一些便于移植的数据类型,这样以后移植自己的代码的时候只需要#include 我们自己的配置文件就可以了。

以上就是良许教程网为各位朋友分享的Linu系统相关内容。想要了解更多Linux相关知识记得关注公众号“良许Linux”,或扫描下方二维码进行关注,更多干货等着你 !

137e00002230ad9f26e78-265x300
本文由 良许Linux教程网 发布,可自由转载、引用,但需署名作者且注明文章出处。如转载至微信公众号,请在文末添加作者公众号二维码。
良许

作者: 良许

良许,世界500强企业Linux开发工程师,公众号【良许Linux】的作者,全网拥有超30W粉丝。个人标签:创业者,CSDN学院讲师,副业达人,流量玩家,摄影爱好者。
上一篇
下一篇

发表评论

联系我们

联系我们

公众号:良许Linux

在线咨询: QQ交谈

邮箱: yychuyu@163.com

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部