定义类型typedef与#define一样?
最近在开发中使用typedef的时候掉了一次坑,这里记录一下同时分享给大家~1typedef与#define大部分朋友在编写代码的时候都会把无符号类型、结构体等等进行"简化",要么用宏定义进行文本替换,要么就通过typedef来定义一个类型别名。参考代码: 1#include [removed]2#include [removed] 3 4#define uint16  unsigned int  5typedef unsigned int  uint16_t; 6 7int main(int argc, char *argv[]) { 8 9    uint16   Var1  = 1; 10    uint16_t Var2  = 10; 11 12    printf("var1:%d \r\nvar2:%d\n",Var1,Var2); 13 14    return 0; 15} 似乎在一般人眼里两种方式并没有什么区别,有些人习惯用宏,有些人习惯用typedef,甚至一套代码中还出现两种定义,不过使用typedef来兼容不同平台的类型更加的专业。两者的区别其实还是很明显的,只是大部分人在编码的过程中没有遇到两者差异的情况,那么这里就举个例子:参考代码: 1#include [removed]2#include [removed] 3 4#define ptrINT  int * 5typedef int* ptrint_t; 6 7int main(int argc, char *argv[]) { 8 9    int temp1 = 6; 10    int temp2 = 12; 11    const ptrINT   Var1  = &temp1; 12    const ptrint_t Var2  = &temp1; 13 14     Var1 = &temp2; 15     //*Var1 = 10;  //指针内容为可读,禁止访问 16 17     printf("*Var1 = %d\n",*Var1); 18 19     //Var2 = &temp2;  //error 20 21    return 0; 22} 如果typedef与#define没啥差异的话,那么测试代码中的Var1与Var2使用方法应该是一样的,但当我们使用var2进行赋值则编译不通过。编译结果出来你应该就明白了,typedef把整个int*作为了一个整体,与基本类型int,float等一样,此时const修饰的是Var2变量,与int * const等价,其指针所指向的地址无法改变,但内容*Var的内容可以进行修改。同样还有一个类似且非常值得注意的地方:参考代码: 1#include [removed]2#include [removed] 3 4#define ptrINT  int * 5typedef int* ptrint_t; 6 7int main(int argc, char *argv[]) { 8 9    ptrint_t pVar1,pVar2; 10    ptrINT pVar3,pVar4; 11 12    int Temp = 10; 13 14    pVar1 = &Temp; 15    pVar2 = &Temp; 16    pVar3 = &Temp; 17    pVar4 = &Temp; 18 19    return 0; 20} 问题就处在pVar4的定义上,此时pVar4仅仅只是一个int类型而非int*类型,这也是宏定义所带来的弊端。这样大家应该对typedef定义的整体类型与#define定义的宏有了一个较好的理解了吧。2数组类型是typdef专属?顺便问了一下同事:你觉得typedef还有与#define不同的地方吗?他想了想,回答道 : typedef可以直接定义数组类型,而#define不能。他的回答也对,但是也不完全对:参考代码:1#include [removed]2#include [removed] 3 4#define ARRAY_SIZE 5 5 6typedef int Array[ARRAY_SIZE] ; 7 8#define dArray(a) int a[ARRAY_SIZE]  9 10int main(int argc, char *argv[]) { 11 12    Array array1; 13    dArray(array2); 14 15    for(int i = 0;i < ARRAY_SIZE;i++) 16    { 17        array1[i] = i; 18    } 19 20    for(int i = 0;i < ARRAY_SIZE;i++) 21    { 22        printf("array1[%d]=%d\n",i,array1[i]); 23    } 24 25    printf("\r\n"); 26 27    for(int i = 0;i < ARRAY_SIZE;i++) 28    { 29        array2[i] = i + 1; 30    } 31 32    for(int i = 0;i [removed]
最后一个bug
0 4 嘉立创PCB
三种管理C程序中标志位的方法~
在嵌入式开发中难免会涉及到非常多的标志位处理,特别是玩单片机、裸机开发的朋友,比如跟一些模块配合联调会遇到各种信号是否到位、成功等等状态,而这些信号大多都是bool类型,1个bit即可进行标识。当然如果仅仅是几个标志,直接拿个uint8_t的整形来进行标识也不会影响什么,但如果特别多的话似乎就比较废RAM了。然而为了更好的管理这些标志位等,有个如下几种方式供大家更好的管理这些标志位 :1、位域直接标识采用位域是管理这些标志位比较直接且方便的方式,代码如下所示: 1typedef union _tag_SystemFlag 2{ 3    uint16_t all; 4    struct  5    { 6        uint16_t Run         :1; 7        uint16_t Alarm       :1; 8        uint16_t Online      :1; 9        uint16_t TimerOver   :1; 10        uint16_t Reserver    :12; 11    }bit; 12 13} uSystemFlag; 14 15uSystemFlag  unSystemFlag; 16 17int main(int argc, char *argv[]) { 18 19    unSystemFlag.all = 0x00; //系统标志清除 20 21    unSystemFlag.bit.Run       = 1; //置位 22    unSystemFlag.bit.Alarm     = 1; 23    unSystemFlag.bit.Online    = 1; 24    unSystemFlag.bit.TimerOver = 1; 25 26    unSystemFlag.bit.Run       = 0; //清零 27    unSystemFlag.bit.Alarm     = 0; 28    unSystemFlag.bit.Online    = 0; 29    unSystemFlag.bit.TimerOver = 0; 30 31    return 0; 32} 这些标志位的操作无非就是置位,清零、以及读取三种方式。但如代码中这样的操作方式在语句或语义表达上还是不够直观。bug菌经常谈到,代码可以不写注释,不过你的每个变量、函数名称等需要足够的直观,所以很多朋友习惯把这些标志封装起来。2、枚举+移位为了更好的表达一般会对标志位进行进一步的封装,如下代码所示: 1typedef enum _tag_Flag { 2cEmRun = 0, 3cEmAlarm, 4cEmOnline, 5cEmTimerOver 6}emSystemFlag; 7 8uint16_t SystemFlag ; 9//置位 10void SetFlag(emSystemFlag flag) 11{ 12    SystemFlag |=  ((uint16_t)0x01) << flag; 13} 14//清除 15void ClrFlag(emSystemFlag flag) 16{ 17    SystemFlag &=  ~(((uint16_t)0x01) << flag); 18} 19//获得状态 20uint8_t  GetFlag(emSystemFlag flag) 21{ 22    return (((SystemFlag & (((uint16_t)0x01) <[removed]
最后一个bug
7 12 嘉立创PCB
7个嵌入式C进阶技巧
1、void 与 void*void表示的是无类型,不可以采用这个类型声明变量或常量,但是可以把指针定义为void类型,如void* ptr。void指针可以指向任意类型的数据,可用任意数据类型的指针对void指针赋值,比如int *ptrInt;void *ptrVoid = ptrInt ;指针的赋值可以认为是地址的传递,而一般的32位系统指针都是占用4个字节,所以指针赋值仅仅只是这4个字节的赋值与类型没什么关系。1void * memcpy( void *dest, const void *src, size_t len ); 2void * memset( void * buffer, int c, size_t num); 2、volatile关键字volatile修饰表示变量是易变的,编译器中的优化器在用到这个变量时必须每次都小心地从内存中重新读取这个变量的值,而不是使用保存在寄存器里的备份,有效的防止编译器自动优化,从而与软件设计相符合。3、数据占用大小数据占用大小是指不同的数据类型在平台中所占用的字节个数,不同的平台不同类型占用的字节个数稍有不同,不过在对应的平台进行开发过程中,必须要对每个数据类型的占用大小了如指掌,否则各种数据溢出,数据越界等等接踵而来。下面是简单罗列的一些数据占用情况:(在一般32位PC中)char8bitshort16bitint32bitlong32bitfloat32bitdouble64bit4、const与指针const是恒定不变的意思,与指针的结合主要的问题是其const在指针中的位置导致该变量属性不同。主要的识别办法是去掉数据类型,看const修饰的是哪部分。const int *ptr --> const *ptr -->那么const修饰的就是*ptr,而*ptr表示的是指针所指向内容,所以其总体也叫"常量指针"表示值无法改变。int *const ptr --> *const ptr -->那么const修饰的就是ptr,而ptr表示的是指针变量,指针变量的值就是地址,所以总体也叫"指针常量"表示地址无法改变。5、结构体与共联体对于结构体和共联体在嵌入式领域是使用得非常频繁的,一些可编程芯片提供的寄存器库都是采用结构体和共联体结合的方式来提供给软件人员进行开发,同时在平时的编码过程中这两个数据类型的灵活应用也能够实现代码更好的封装与简化。  如下面的简单示例,就可以非常灵活的访问Val中的bit位。 1typedef union 2{ 3    BYTE Val; 4    struct __packed 5    { 6        BYTE b0:1; 7        BYTE b1:1; 8        BYTE b2:1; 9        BYTE b3:1; 10        BYTE b4:1; 11        BYTE b5:1; 12        BYTE b6:1; 13        BYTE b7:1; 14    } bits; 15}BYTE_VAL, BYTE_BITS; 6、预定义标识符一般编译器都支持预定义标识符,这些标识符结合printf等打印信息帮助程序员调试程序是非常有用的,一般编译器会自动根据用户指定完成替换和处理。如下是常用的标识:__FILE__ :表示进行编译的源文件字符串;__LINE__ :表示当前文件的行号;__DATE__:表示文件日期;__TIME__ :表示文件时间;使用范例:1printf("file:%s\n line:%d \n data:%s \n time: %s \n",__FILE__,__LINE__,__DATE__,__TIME__); 7、#与###:是一种运算符,用于带参宏的文本替换,将跟在后面的参数转成一个字符串常量。##:是一种运算符,是将两个运算对象连接在一起,也只能出现在带参宏定义的文本替换中。1#define STR(s) #s  2#define COMB(str1,str2) str1##str2 3int main() 4{ 5    int UART1= 57600; 6    printf("%d\n", COMB(UART, 1));     7    printf("%s\n", STR(3.1415));      8    return 0; 9}
最后一个bug
2 6 嘉立创PCB
回调函数、同步与异步调用
在嵌入式开发软件中回调函数是经常接触的一种软件设计方法,像我们的事件处理机制基本上都会使用到回调函数。那么就抽了点时间来聊聊他们:1、回调函数的理解在C语言中,回调函数其实与函数指针的调用在语法上并没有太大的差异,而为什么叫回调函数主要还是从功能上给它起的名字,即这个函数会被"返回来调用"。而这里所谓的“返回”就涉及到一个方向性问题,从哪里来到哪里去。而在软件中通常就是与“分层设计思想”挂钩的。在软件设计领域分层设计方式是非常广泛的,在嵌入式中最简单的分层就是两层"驱动层"和“应用层”。当函数功能上进行分层以后不应该直接在底层驱动中直接调用应用层函数等,比如应用程序通过调用驱动层接口获得物理量数据,我们常规的做法大部分都是不断的轮询相应的API接口返回数据,这样可能会导致不断的IO操作,效率相对比较低下。那么应用程序是否可以化主动为被动呢,一直舔狗实在是太累了?既然你现在不想搭理我,那等你准备好了,再来告诉我吧,到时候调用我给你的函数就可以了,这个函数已经放在了传给你的函数指针里了,那么这里应用程序所给的函数就是回调函数。比如我们经常会在应用程序中查询按键是否被按下,然后得编写一大堆的时序等等,还与应用逻辑耦合在一起。其实按键是是如何检测被按下的过程对于应用程序它并关心,底层程序查询确定好状态给应用程序一个是否按下的通知或者状态即可。此时底层按键检测程序要通知应用程序,就可以通过相应的回调函数来通知应用层并处理即可。如果还有点难理解,可以看看stm32使用hal库,你会发现在中断中有大量的回调函数指针被调用,其实回调函数的效果与中断服务函数的执行效果是类似的,hal库中使用回调函数的方式把中断的相关事件服务处理交给了用户自身来注册。把中断看成一种事件类型,那么回调函数的使用其实就类似于一种事件驱动机制。2、同步与异步调用首先要理清楚这两种方式需要理解什么是同步和异步。同步调用表示当调用一个底层接口,必须回调函数被执行完毕,不然该接口会一直处于堵塞状态没办法返回结果,且程序无法往下执行。异步调用表示当调用一个底层接口以后,不需要等待回调函数执行完毕,便可以直接返回继续做下面的事情,最终底层准备好以后便会执行回调函数处理应用层事务,所以我们也称这种回调函数为异步回调函数。而异步调用的好处在于调用函数不需要阻塞可以继续执行,从而大大提高程序运行效率,但由于异步回调函数在时间上是无序的,导致当我们需要异步调用函数能够顺序执行时便会存在难度,使得业务逻辑比较复杂,难以理解。为了保证回调的有序性,就需要以上一次回调的结果作为本次异步调用的条件,导致代码一层嵌套一层非常的冗长,类似于ifelse里面再嵌套ifelse之势,所以也很多人称这种方式为 Callback hell(回调地狱)。为了改善这种结构,通常会采用协程的概念去处理异步回调来规避该问题。对于异步调用常与多线程进行结合,在另外一个线程中执行异步操作,然后调用回调函数返回结果并继续处理。
最后一个bug
3 4 嘉立创PCB
C程序这函数参数太多了,头皮发麻~
本文主要是跟谈谈函数参数太多这个问题,其实应该早一点跟大家聊聊的,毕竟牵涉到软件代码的设计。主要还是因为平时很多话题写文章的时候又想不到,遇到了又不一定会想到要写到文章中去,所以迟迟没有涉及到。那么今天就针对函数参数数量问题做一个分析和编码建议:1、函数参数的传递既然本文谈到函数参数的个数问题,首先就需要了解函数参数到底是如何传递的,并且其影响着什么?函数的调用过程主要是依赖函数调用栈,对于栈的原理很简单-先进后出,而对于函数调用栈主要是在该栈中分配内存以供函数临时使用-(即压栈),而当函数返回便是一个栈回溯的过程-(即出栈)。所以函数的参数跟局部变量一样都分配到栈上,当然为了提高函数调用速度,相关变量会直接通过寄存器来传递,原理上也是类似的。在嵌入式开发中经常会有C语言与汇编语言相互调用的编码形式,这样就需要遵循一定的调用原则。对于使用甚广的ARM内核都遵循ARM过程调用标准APCS(ARM Procedure Call Standard)。该标准中规定了各种寄存器的使用限制、栈的使用规则、以及函数调用过程中的参数传递返回等,大家编程的时候可以参考相关ARM手册进行了解。对于函数调用优先会采用R0~R3传递参数,且不需要恢复,那么如果超过4个参数则只能通过栈来传递,相比寄存器传递需要耗费更多的时间压栈和出栈。所以这也是为什么有些人经常说函数参数最好不要超过4个或者过多的原因。2、参数太多引起的问题前面bug菌在谈及参数太多会导致函数调用更加的耗时,因为增加了参数入栈和出栈的时间,特别是一些循环语句中调用参数较多的子函数。而对性能上的影响,对于大部分朋友所做的项目可能并不是很在意,快点慢点也无所谓,一般用户也体会不到,毕竟现在的芯片性能都还不错,可以为低效的代码买单。但是参数太多对于编码风格却是大煞风景,更值得注意的是很容易让编码人员犯错。bug菌之前就遇到一种情况: 1void Function(int param1,int param2,int param3,int param4,int param5\ 2              int param6,int param7,int param8,int param9,int param10) 3{ 4 5    ...... 6} 7 8int main(void) 9{ 10    ...... 11 12    Function(Val1,Val2,Val3,Val4,Val5,Val6,Val7,Val8,Val9,Val10); 13    ...... 14} 如以上代码,由于参数太多,在传递参数的过程中,传递顺序不小心错乱了,编译仍然可以正常通过,但程序就引入了bug导致出错。3、如何处理函数参数那参数多少才算多?其实并没有严格的界定,因为最终大量的参数定义都会在栈上分配,只要不把栈撑爆了,都是允许的。对于C语言的参数传递,主要是两种方式,一种就是传值和传地址。如果一个函数内部不依赖于静态全局区参数或者函数外部存储区,数据均来源于函数参数,那么随着需求不断的变化,函数功能更加的丰富多变,参数也将随着变大。经常听有些朋友说直接把参数封装成结构体来进行传递,这样的处理虽然能够在一定程度上减少函数参数顺序错乱带来的风险,但与各参数分别传递并没有太大的改善。就类似于如下代码: 1void process(void) 2 3{ 4    sPara.m1 = 1; 5    sPara.m2 = 2; 6    sPara.m3 = 3; 7    sPara.m4 = 4; 8    ..... 9 10    process(sPara);     11} 同时我个人还是不建议简单地用来减少参数数量进行数据打包,结构体应当尽量表现业务上的关联性,当然如果你一定在数据之间强加一种关系,那我也没办法。同时一个函数一般只实现一种功能,不要把太多的功能放到一个函数中,一方面把函数写得特别长,另一方面就是会使得参数特别多,因为这些参数都和特定的功能相关。似乎单独值传递并不能直接改善参数太多带来的弊端。那么就必须借助函数以外的存储区来作为相关参数的存储位置,参数仅仅只是索引,更多的参数和信息还需要根据传递的地址或者是索引来获得,这样就降低了调用函数参数太多所带来的负担。以前也跟大家介绍过一些C语言面向对象程序的设计,所有的数据和方法都会封装到一个结构体对象中,这对于相关方法函数的调用只需要传递相应的对象地址给形参指针,一个指针多大?应该不用我多说了吧。当然也没有十全十美的事情,子函数引用函数以外存储区可能相对没有那么独立,因为外部存储区可能会发生参数变化,从而导致子程序运行结果发生变化,这一点是值得注意的,特别是对于多线程编程。
最后一个bug
1 7 嘉立创PCB
调试上位机也要好好管管~
最近工作上都无法用一个"忙"字来形容了,周天还在因工作上的一些事情加班,毕竟工作还是要放在首位,分享只能看成一种爱好。本篇文章主要是跟大家聊聊一个成熟项目中调试上位机的功能定位和职责划分:1、调试上位机的必要性 做嵌入式软件开发的朋友大部分时间都在跟底层驱动打交道,也基本上都是一些逻辑、算法或者策略等等。而一款与用户打交道的产品,为了更加直观和方便的给用户使用,基本上都离不开人机交互,像手持移动设备,往往都会有LCD等屏幕显示;工控行业的大型设备,一般都会与PC端桌面软件结合,也就是常说的上位机。然而这样的上位机涉及到桌面UI、数据库等等相关的技术知识,所以这样的项目至少也会有两拨人来共同开发,上位机和下位机。我们知道在嵌入式软件开发的前期都离不开各种仿真器,如果是嵌入式Linux会有比较成熟的终端或者调试手段那就不在这里讨论了。对于下位机部分基本都会采用仿真器来进行调试,相关驱动和环境部署好以后,就开始进行功能和算法的调试,在该过程中经常涉及到参数的整定、模式的切换、数据的采集获取、以及问题的定位,急需要一个更方便的动态方式来实现这些功能,那就借助调试上位机吧。调试上位机功能相对比较简单,主要就数据显示和参数下发,如果你想做得强大一点,可以做一些数据采集变换处理等等。然而大部分上位机的同事没有太多的精力帮忙开发这样一个调试上位机,又或者并不是太符合自己的调试需求,甚至还要经常修改。那就只能自给自足了,自己写上位机,所以项目实战经验多一点嵌入式工程师多多少少能够做一些桌面应用,比如用QT、C#等等。很多嵌入式工程师招聘的时候,能够懂一些PC端的桌面应用的开发,也是一项加分项,能够比较方便高效的进行下位机软件开发。2、问题所在与改善然而bug菌发现非常多项目的调试上位机千奇百怪,今天工程师A调试平台开发了一个上位机,然而过一段时间交接给工程师B来维护,又出现一个新的调试上位机,导致软件杂乱无章,能够写点桌面应用确实是一种能力的体现,但不对软件加以管束,只会对项目带来诸多的后遗症。下面谈谈一些看法:1、不要本末倒置之前看到一个同事对自己的调试上位机非常的在意,弄了各种UI美化等等,可是下位机软件写得一团糟。也能理解,每个人对赏心悦目的东西都会有一定的追求,但软件的本质都是一样的,更何况调试上位机的定位并不是给用户去使用,仅仅只是一种更加形象的表现嵌入式软件的方式。所以对于嵌入式工程师真的没有必要花太多的时间把专注点放在这个上面,而是应花更多的精力对下位机的逻辑、算法进行优化,增强其冗余性和稳定性。话说回来如果项目下位机的软件功能很简单,程序一烧录一把搞定,那真的就没啥可提升的了,倒可以根据自己所要发展得方向来学习。2、和用户上位机集成调试和用户上位机并没有什么区别,完全可以统一管理发布,只是非常多的软件项目负责人并没有权衡到系统的各个方面,当然也有做得不错的,比如非常多的软件都有两种管理模式,一种是用户模式,一种是开发者模式,可以通过设置密码来进行使用模式切换。专门的人做专门的事情,毕竟做上位机的同事对桌面应用的开发还是相对比较擅长的,只是对相关的调试需求并不是很熟悉,此时相应的嵌入式工程师应该把相应的需求整理设计清楚。整合成一个上位机,一方面能够进行较好的版本管控和功能的迭代。也不会经常出现到了现场解决问题忘记带调试上位机,又或者售后人员经常找你拿调试上位机等等麻烦。3、功能定位调试上位机,主要还是用于排查和定位问题。所以与调试上位机相关的交互和设计在下位机软件中不要杂糅在一起,以前同事负责一个项目,到客户现场经常出现问题,而在实验室却怎么也复现不了,后来对比发现在实验室他一直挂着调试上位机,因为调试上位机有定时发一些数据,导致现场不一致。所以一定要对调试部分功能上定位好,其主要功能就两部分,一方面是查看下位机运行的状态;另外一方面就是可以下发一些数据用于调试参数等等,也可以做一些更强的功能,比如波形分析等等,但都大同小异吧。在用户上位机正常运行的过程中,调试上位机依然能够正常获取状态和参数下发,所以不要有两个上位机只能有一个在线的互斥设计。这样黑箱子一样的下位机状态全部在调试界面暴露出来了,也基本没有什么bug能逃得出你的监视,相当于一个在线仿真器。
最后一个bug
1 5 嘉立创PCB
UDP与TCP在嵌入式项目中该怎么选?
在单片机应用程序开发中可能用得比较多有RS485,CAN通信等等相对简洁一点的总线,由于所选用的单片机性能和资源有限,以太网并没有在单片机应用中作为一种普遍存在的对外通信接口。但随着MCU工艺、性能的逐渐加强以及嵌入式Linux平台的推行,以太网通信也慢慢开始成为开发者们所考虑使用的一种可靠通信方式。经常有一些朋友问到,现在用不到技术和知识点是不是可以不用学?我只能回答:技术在不断的革新,并且总是朝着更加便利和通用化方向发展,现在看似非常复杂的技术,在以后的应用上都会得以简化,但即使再简化也还是需要有必备的一些基础和认识,所以总归还是要学的,至于什么时候学就看自己的时间精力了,当然越早学习,就越多一种选择。那么今天就浅谈一下TCP与UDP的区别与应用,并指引后续在系统通信设计上的设计考虑和选择。1、TCP VS UDP 要想在通信的两者之间合理的选择TCP还是UDP,首先需要理清楚两者的特点与区别,下面简单梳理一下:在学习这两种协议的时候你一定看到过这样中一句总结的话:"TCP是面向连接的可靠传输而UDP是无连接的不可靠传输。"其实这句话已经把这两种协议大部分特点都囊括在内了 :1、面向连接与无连接TCP在传输数据之前需要经过三次握手建立连接进行相互确认,当需要断开连接的时候需要进行四次挥手;而对于UDP就不需要这么繁琐的连接建立过程,直接传输即可。所以对于TCP仅仅只支持单播,只能点对点的在连接的两个端点中数据传输数据,不支持多播和广播;而对于UDP而言支持一对多、多对一和多对多的传输,这一点在通信架构设计中对这两种协议的选择非常重要,比如有些资源优先的MCU对socket连接有限等等问题。2、字节流与数据报UDP也称之为是用户数据报协议,而TCP为传输控制协议,所以UDP是一种面向应用报文的传输,有明显的传输边界,仅仅只是封包以后进行处理,不会进行合并和拆分,一次就传输一个报文。但是TCP是一种面向字节流的通信协议,没有明显的边界,其主要是保证数据正确且有序,TCP存在一个数据缓存区,如果数据量较大,其会进行分包发出,而当数据较少也会等待数据达到合适的数量后进行合并发送,所以会存在多个粘包的问题,这一点在设计中需要考虑。3、可靠传输TCP是一种可靠传输,确认重传、差错控制、流量控制和拥塞控制等等都是传输数据过程中实现的策略和算法,这就使得TCP能够达到数据上无差错、不丢失、不重复、有序。所以为了保证这些数据的可靠传输,TCP相比UDP的报文格式要复杂,且占用的资源也相对较多。而UDP则不同,它仅仅只是一种最大努力交付的协议,其主要是利用IP层的无连接传通信服务,可靠性方面它是无法保障的,有点类似于串口通信,它不需要连接,只管传输。如果你想让数据可靠,那么用户可以在应用层自己来增加可靠性传输策略和机制来进行实现,所以Udp传输数据是可能会丢失、无序。4、实时性UDP由于没有拥塞控制等等策略,协议上会轻量很多,其均以比较恒定的速度进行传输,不会出现发送速率降低的问题,所以在网络不好的情况下就丢包了。而对于TCP一旦有数据包丢失,就会进行重传等等一系列机制,传输速度大大降低。2、项目中该怎么选择?对于TCP和UDP的选择有些朋友在开发的时候比较纠结,因为有时候采用UDP和用户方面可靠的传输机制也能够达到TCP类似的效果,比如许多支持可靠通信的UDP库,所以具体怎么选择还是要根据具体的应用和设计。比如对于很多直播、游戏等等通过自己加入一些重传机制,可以最大可能的发挥UDP传输实时性的优点,使得呈现给用户更加流畅的画面体验。在嵌入式、物联网方面由于项目对实时性要求高,且资源有限,UDP相对比较轻量,也是较好的选择。但对于一些文字、文件的数据传输还是会优先使用TCP,毕竟TCP这块在保证数据的可靠性方面还是做得非常成熟,用户程序这块也可以减少一些可靠性处理。所以最终得选择,还是要结合TCP与UDP的特点和具体项目综合考虑。
最后一个bug
1 2 嘉立创PCB
C代码中看到"!!",捻了把汗~
1、!!操作看到交流群里有朋友抛出一段C语言操作:offset = len/64 + !!(len%64); 这两个连续的感叹号把有些人给整蒙圈了,还有些朋友调侃道: !!表示语气加重,事情非常紧急得尽快处理,可把我给整笑了。其实都是常规操作吧,只是这样的写法在正常的编码过程中并不多见。首先这两个!!并不是什么C语言新的关键字,而是!运算符的嵌套作用。!在C语言中叫逻辑非运算符,是一种条件运算符。语法形式 : !(条件) 其中的条件如果是false,则最终整个表达式为true;反之则为false,bug菌提醒一句:要与~进行区分,~是按位取反,很多初学者容易混淆。语法形式 : !!(条件) 所以如上表达式就是两个!的嵌套形式。当然如果你想项目代码中变得无可替代,可以继续如下操作:语法形式 : !!!......!!(条件) 前提是你要自己能看懂。那么回到!!,该操作所达到的效果便是条件逻辑与最终表达式的逻辑结果保持一致:!!(false) == false !!(非false) == true 而对于大部分编译器false对应的是0,而true对应的是1,所以很多同志拿着逻辑结果参与数值运算。offset = len/64 + !!(len%64); 该表达式的用处也非常清楚了,当len不能被64整除,则:offset = len/64 + 1; 获得正确的数据分组个数,这代码应该是来源于存储或者通信中。但bug菌觉得,尽量还是不要操作:逻辑和数值运算最好是分开,以增加代码的可读性和可移植性。2、还有个用处最早看到这种处理方式是在判断一个引脚的高底电平上:u8GPIOLevel = !!(GPIOA_Data &  PIN_2); !!在其中的作用跟之前是一样的,使得表达式的数据结果与条件中的逻辑保持一致,便可以直接获得结果。
最后一个bug
1 4 嘉立创PCB
谈谈数据分包以及相关小技巧
前些天跟大家解释了如下代码:offset = len/64 + !!(len%64); 并且跟大家详细聊了一下其中的!!操作,然而这段代码的主要功能还是为了进行分包处理,既然是分包自然而然就会想到一种常用的分包处理方法,这也是本文的重点。数据分包在嵌入式软件开发中算是一种非常常见的处理,其主要原因还是硬件上的各种限制,不得已而为之,特别是在通信协议的定制过程中尤为常见。1、传输限制 玩过各种通信协议的朋友都知道,像非常多的通信方式都是以数据帧的形式来进行传递,不同的通信方式因各方面的因素又存在一个最大传输字节数的限制,考虑到稳定性、容错性等等对单次发送的数据长度进行限制,又或者所接收的设备其内存资源有限,不足以接收、处理过长的数据包。像zigbee这样的物理层每帧最大只能传输127个字节,通过每层不断的封包到应用层后每包才100个字节。当上层用户协议的数据包过大,无法一次性传输,就只能分包或者分组下发,最终接收方组包后解析提取数据。2、分包设计的考虑 有些朋友该说了,我就不喜欢搞大包发送,使用短包,然后通过不同的标识进行不同数据位的定义,简单很多。当然长包与短包并没有本质上的区别,其目的都是传输数据,但在实践的过程中还是会遇到居多处理上的区别:数据的同步性方面:比如当通信的设备转速超了,同时报了一个故障码,如果采用短包上传,很可能故障码和转速位于不同的数据包中,当数据包丢包或许是乱序,就会导致当接收到故障码的时候,此时超标的转速值已经丢失或者延时等,有概率不能准确获得故障时的超标转速。而使用长包,只需要发送方能够保证打包的时候同步,那么接收方就可以同步获得相应的数据。通信协议设计自由度方面:在设计协议的时候,长包会更加的自由,大多数情况都不需要考虑大数据传输的占位问题,甚至在编码上直接copy结构体发送也是相当方便的。3、计算包数问题既然长包的设计相对比较方便。那分包处理是少不了的?分包还不简单?要发100个字节的数据,每次只能发15个,那发送7包就可以了,直接编码,代码如下:SendPack = SendNum / PackNum; if(SendPack % PackNum)SendPack++; 这算是常规操作,如果觉得有点难度,还要多敲敲代码。一般用C语言比较久的朋友都想去简化这种操作,毕竟实现一个简单的功能需要两行代码,强迫症,忍不了~就有了本文开头的!!处理方式,或者如下处理也是一样的:#include[removed]#define PackNum(total,single)  (total/single + ((total%single)?1:0)) int main(void) {     printf("packNum: %d\r\n",PackNum(100,15));     printf("packNum: %d\r\n",PackNum(150,15));     printf("packNum: %d\r\n",PackNum(200,15));     printf("packNum: %d\r\n",PackNum(5,15));     printf("hello bug ~\r\n");     return 0; } 仅仅只是秀了一下C语言的几个小技巧罢了,并没有实质性的改善。很明显,本文的重点并不是介绍如上两种办法,而是如下更加高效的代码:PackNum = (total + (singleNum - 1))/singleNum ; 对于一些以往没有使用的朋友或许有点懵,那bug菌这是唠叨几句:该表达式主要是利用了取整的特性来达到+1的目的。直接除单包个数,不能整除的情况,结果都会少1,比如10/6,应该是2包,而由于最终除法结果只能是1。所以通过补偿(singleNum - 1)后,结果就分两种情况:1、原本能够整除的数,补偿后无法整除,结果与之前一致;2、原本不能够整除的数,其余数必然在【1~(singleNum - 1)】之间,所以补偿以后,其余数范围在【singleNum ~(singleNum + singleNum - 2)】,则其结果为整除部分+1。与我们分包个数是一致的,相当巧妙。4、扩展这种方法不仅仅只是用于通信的分组中,把思维进一步泛化。只要是类似分组的处理都可以使用该算法。比如内存的分区,flash的设计上都是一个扇区一个扇区的分布。现在想分配整数个扇形区域用于存储某些数据,每一个扇区512个字节,存储2000个字节的数据,该分配几个扇区?我相信你已经有答案了~
最后一个bug
1 2 嘉立创PCB
引出AD信号后,问题修护了,是软件问题还是硬件问题?
最近升级了一款产品,与其说是升级,更多的还是降本和替代。 降本就不用多说了,每次产品升级都是考虑的重要指标,能省则省,甚至不放过一颗电容、一个电阻,一个字"抠"。 而替代也是近段时间的主题,很多芯片可以说基本买不到,即使买得到也是从40元到了400元,10倍的价格还不能保证交期,还能不能好好玩耍?更可怕的是同样的芯片一个供应商一个价,也不知道是真买不到还是假买不到。 既然这么多事情,又加上人手紧张,只能亲自操刀,当然也是轻车熟路。调试完设备就把固件给硬件和测试去做可靠性了,硬件组装了5台机器,其中有一台机器上位机采样数据死活偏差了100mv。 这部分采样采用多路复用,发现问题以后就一台机器有问题,那只能硬件先上去排查,硬件的小兄弟也是各种测试对比,从输入到AD口一级一级排查,晚上7、8点跟我讨论说,实在是找不到问题了,一起讨论一下还能有什么办法定位原因, 我问他信号有没有确认过,他说从输入到输出都测量过了,信号都是没有问题的。当时我就想不可能呀,这AD采样原始值就显示有所偏差,驱动部分也是应用在蛮多款产品上了。 好吧,那就一起看看吧~ 他跟我展示了一下自己的一些确认结果,通过不同机器对比运放输出信号都是没问题,难道从多路复选芯片到ADC引脚存在问题? 而ADC引脚是多路信号的不同时刻的选通,直接测量有点难分辨,此时我要他把其他信号都拉3.3V,那么此时ADC引脚的低电平必然就是所想要的信号了。按照这样的方法,引出了一些信号点到示波器上,所采集的问题信号确实与正常信号是一致的。 奇怪了,怎么会有问题呢?百思不得其解~ 然而当我再次瞄了一下上位机上的采样数据,正常了~ 难道是因为接了示波器探头的缘故?还原现场! 拆掉看看什么情况? 重新上电看看什么情况? 看来是不出现了,真的是玄学~ 会不会是因为虚焊?或者有锡渣?....... 硬件的小兄弟,一脸蒙圈,说不可能的,今天因为这个问题还重新焊接过。
最后一个bug
1 5 嘉立创PCB
C语言如何规避全局变量?不可能~
今天终于安安静静的坐下来分享一些东西,根据今年的一些规划和目标,应该是非常忙的一年,再加上疫情的反复、各大厂的裁员、还有目前国际大环境等等都多多少少影响着我们的生活,跟一些同事和朋友的交流当中我也隐隐约约感受到大家的一丝焦虑和担忧。其实大可不必这样,一味担心一些未知东西或许仅仅只是一种自耗吧,还是要调整好心态。好了,今天谈论的话题是C语言中为何没法规避全局变量?为什么突然谈论这个话题,主要是最近有几个粉丝朋友问到了类似的问题。"bug菌,我代码中全局变量太多了,有没有一种方法不使用全局变量来编写C语言程序?"等等当时并没有感觉特别惊讶,因为曾经的我也是在这条道路上探索着,只是在C语言编程这条路上走得多一点,相对想得更加明白一点罢了。我的回答是:"没有办法规避全局变量的~"1、耦合其实自认为"没有办法规避全局变量的",这个答案相对而言并不是特别专业,却能够让更多的人听懂。在最开始使用C语言进行项目开发的过程中,都会提示要你用模块化思想来编程,怀着"低耦合,高内聚"的编程理念。但是你应该没有听说过模块之间无耦合吧,所以耦合和内聚都是相对而言,当采用了一种不是最优的设计思想和方法,便编写出了一套耦合度较高的代码罢了。而所谓的耦合便是你所划分的各功能模块代码之间的数据共享了。bug菌一直相信代码中的所有设计都可以在这世间找到类比原型,同样这世间规律也完全可以用程序来进行表达,只是目前的科技水平还并没有达到那么水平,或许人工智能就是一个萌芽吧。而在C语言中全局变量的使用,便是一种数据共享的方式,同样也是模块化沟通的桥梁。有朋友该说了,我不进行数据共享便不会使用全局变量了。比如点了个灯,同时又写了串口通信程序,两者并没有什么联系,各自安好,那确实没有数据共享,也可以不使用全局变量,最多认为你在静态存储区定义了几个变量吧。但这样的程序又能复杂到什么程度,或实现什么功能呢?基本没有特别大的意义。2、联系这世间原本就是千丝万缕羁绊着而存在,或许从蝴蝶效应看来就会颇有感触,那么编程中也是一样的,相互之间的存在着某种联系才愈发精彩,而这种联系的表达则少不了数据的共享。程序员们要用C语言来描述这世间种种规律,使用全局变量还逃得掉吗?即便有一种语言可以达到不使用类似于全局变量的语法形式,但数据的共享终究逃不了。虽话说回来,无法规避,但并非没有章法;不加约束,必将带来不少麻烦。3、面向对象面向对象的程序设计思想就是目前约束全局变量使用的一味良药,面向对象与面向过程都是一种思维方式的存在,并非孰优孰劣。但就目前软件设计的发展来看,面向对象的程序设计思想在大型程序的设计和把控上有更多的优势。它能够把各个抽象的个体描绘得更加的直观,这样个体之间的相互联系也就顺理成章的表达和约束。
最后一个bug
1 3 嘉立创PCB
栈的增长方向怎么理解?用C语言如何判断?
在学校或者各种编程类书本上,基本上都会看到一句话:"函数是程序的基本组成单位",可以说理解函数对编程是非常重要的,与函数调用紧密结合的机制就是函数调用栈了,而栈有一个特别的属性就是栈的增长方向问题了,也发现一些多年编程经验的朋友对这一块都有点迷迷糊糊的。在阅读RTOS源码的时候也会经常看到栈的增长方向配置项目,那么今天bug菌就带大家了解一下栈的增长方向到底是咋回事。1、栈的增长方向首先我们要明确的是栈同样也是分布在我们的内存之中,而内存是通过地址来进行编排访问的,如下是堆栈的示意图:对于堆栈而言原本并没有方向一说,只有入栈和出栈一说,程序中执行push指令则栈顶向上移动,执行pop指令则栈顶向下移动,其仅仅只是一种先进后出的数据结构,增长方向都是从栈底向栈顶方向移动,即分配数据的过程。而我们平时所说的栈的增长方向又是怎么回事呢?为了在内存中分配一段内存给堆栈,我们必须要区分堆栈相对于内存的地址而言的方向性,通常栈顶增长的方向是从内存的低地址向高地址变化,我们则称为向上增长;反之则向下增长。所谓"水往高处流,即向上增长",这样应该就很好记忆了。2、有什么用?当了解处理器中栈指针的增长方向以后,我们在debug程序的时候才能真正的把控程序的运行过程。在移植RTOS的过程中我们都需要对每个任务的堆栈分配一个合适的连续内存区域来使用,此时初始状态堆栈指针指向什么位置就跟堆栈的增长方向密切相关,有过RTOS移植经验的朋友应该都有在RTOS配置项中关注过这块的选择。RTOS在任务初始化的时候,其堆栈指针应该指向其栈底位置,那么对于堆栈向上增长,任务初始化的时候我们需要把堆栈指针设置在所分配内存的低地址内存处,反之则设置到高地址处。设置好以后,其在堆栈分配的过程中才会朝着所分配的内存区域中,否则就会堆栈反向自爆,导致程序异常;如果你的堆栈分配不合理,同样了解堆栈变化方向后也变得有迹可循。同样在裸机程序中也需要了解一下处理器的堆栈变化方向,从而用来排查一些堆栈溢出所导致的程序异常问题。3、用C语言如何判断?要了解一个CPU的堆栈的变换方向,一方面就是查询相应的芯片参考手册,另外一方面就是实际测试了。毕竟堆栈也就是内存,自然就可以通过堆栈的分配过程取出所分配的内存地址来比较判断,而C语言可以方便的访问内存,也就比较容易判断当前处理器中堆栈指针的增长方向了。那还不简单,直接在函数内部先后定义两个局部变量,直接比较两个变量的地址大小不就搞定了吗?其实这种方式是依赖于编译器实现的,毕竟哪个变量先进行内存申请,并没有太大的影响。那么是否有一种方法不依赖于编译器实现呢?必须有的,那就是函数调用栈了,因为先调用的函数必然首先入栈。基于这样的思想,这里bug菌写一个判断堆栈增长方向的demo供大家参考: 1#include [removed]2#include [removed] 3 4#define STACK_UP (0)5#define STACK_DN (1) 6 7/*************************************** 8@ Function: find_stack_direction 9@ Author  : bug man  10@ Note    : (公众号:最后一个bug) 11****************************************/ 12int find_stack_direction(int* ptr) 13{ 14    int  Val = 0; 15 16    printf("Last stack Addr : %p\n",ptr); 17    printf("Now  stack Addr : %p\n",&Val); 18 19    if(ptr > &Val) 20    { 21        return STACK_DN; 22    } 23 24    return STACK_UP; 25}  26/*************************************** 27@ Function: main 28@ Author  : bug man  29@ Note    : (公众号:最后一个bug) 30****************************************/ 31int main(int argc, char *argv[]) { 32    int  Val = 0; 33 34    printf("stack direction : %d\n",find_stack_direction(&Val)); 35    return 0; 36} 可以拿去试一试,看看你的芯片堆栈咋变化的~
最后一个bug
1 6 嘉立创PCB
老工程师总结的10条经验,太受益了~
1、你永远不可能什么都知道 2、好的团队可以让你的能力呈指数增长,而非线性增长 3、编写的代码应该便于阅读 4、耐心是你最好的朋友 5、持续性是关键 6、总是有比你优秀的人你可以向他学习 7、社交网络很重要 8、经常休息可以让你更高效 9、如果你希望在自己的职业生涯中更进一步,就需要有很好的人缘 10、大多数公司都不是你希望的样子
最后一个bug
3 9 嘉立创PCB
大话SPI通信--基础巩固篇
SPI通信在嵌入式领域是非常常用的一种通信方式了,相比串口、IIC等等,SPI通信是一种高速、高效率的串行接口技术。然而最近在新项目开发中使用SPI通信却遇到了不少问题,过程中还是有挺多技术细节和知识点需要把握住的,那么今天bug菌就结合最近的使用情况和一直以来在SPI通信上踩过的坑进行总结和记录,一起避坑。本文为基础巩固篇:1、通信形式SPI是一种全双工、同步串行的通信方式,全双工、半双工等,表示某一时刻,数据的流动形式,全双工即可双向同时收发,而半双工则在一个时刻只能收或者发,通常全双工具有两条独立通信线路,而半双工则共用同一条线路。从SPI通信接线图可以看出,MOSI用于主机数据输出,从机数据输入,MISO引脚的数据流则刚好与之相反,所以数据流收发是走的两条独立的线路,从而可实现全双工通信模式,当然你也可以用于只用于单向数据传输,比如省略掉MISO仅主机传输从机数据等。SPI通信有一个SCLK时钟线作为通信的同步信号,用来标定数据在MOSI和MISO引脚上的传输情况,SCLK是由主机来控制提供,从机检测识别,从而同步完成数据传输。片选信号CS引脚,还记得有一次面试问片选信号CS英文是什么?英文直译即可--Chip Select。片选信号在大部分数字芯片都有存在,外界给该引脚相应的电平即可选中,相当于一个"总开关",大部分从设备都是低电平被选中(电路图中通常在CS上划线标识),如果一直被选中则直接接地即可;当然也有少部分高电平选中则可直接接到VCC,切记不可悬空,以免异常。这样看SPI的通信线路非常简单,没有繁杂的线路硬件也省了不少事,但很多朋友包括bug菌曾经因为把主机的MOSI接到从机的MISO而折腾得不轻,都是惯性思维惹的祸,一定要记住是对应引脚相连。2、通信线路SPI是一种主从通信方式,在SPI通信总线上通常只有一个主机,一切通信的开始都是以主机发起,那如果在一条总线上与多个从设备通信呢?下面以两个从机为例,更多的从机也是类似的。通过不同的片选引脚来选中不同的从设备,从而完成一对多的通信过程。通常多从机的情况都会采用多余的IO口连接从设备的片选引脚,以便分时控制从设备,达到一主多从机的主从控制方式。当然如果独立的IO口引脚有限,可以采用IO口扩展芯片进行选中,也是比较方便的。3、通信数据SPI通信是一种交换数据的形式,主机根据SCLK时钟把数据从MOSI引脚按bit位发送的同时,从机也以相同的速率把数据从MISO引脚传输,其传输数据形式如下:从SPI通信数据流图可以看出其发送与接收形成了一个封闭的环,所要传输的数据像水在一根管道内循环流动,所以其硬件线路上并没有像I2C那样有所谓的应答机制,通信效率上提高不少,但数据可靠性也会有一定的减弱。前面bug也谈到SPI是一种主从通信机制,那么使得“管子”内数据流涌动的源泉一方面需要SPI的SCLK时钟保持好节奏,另一方面就是需要主机来推动,所以如果master只是想获取slave的数据,也需要发送空数据来使得整个数据流动起来,从而获得从机的数据。细心的朋友应该注意到上图中MSB和LSB方向了,通常SPI通信都是以MSB来进行发送,但像stm32芯片这样的芯片可以设置是LSB先发送还是MSB先发送。所以在分析SPI通信数据的时候这些数据都是需要提前了解的。4、通信电平SPI比较麻烦点的就是时钟极性和时钟相位的确定了,但再怎么麻烦也就确定了4种模式。如下是4种模式的时序图,其中CPOL(Clock Polarity)表示时钟极性,CPHA(Clock Phase)表示时钟相位。从图中我们可以分析得到:1、CPOL和CPHA共同决定数据的采集方式。2、CPOL决定了SCLK默认状态,当CPOL=0,时钟空闲时是低电平;当CPOL=1,时钟空闲时为高电平。3、CPHA决定了数据在第几个跳变沿采集,当CPHA=0,在SCLK第一个跳变沿采集稳定数据;当CPHA=1,在SCLK第二个跳变沿采集稳定数据。每个bit的数据交换,都是在电平稳定的时候进行数据电平采集,在电平变化的时候进行数据发送,一般从设备像高精度ADC等等,出厂就已经是固定了某种模式,我们需要做的就是通过配置可编程的主机SPI外设在相同的一种频率和模式下通信,否则就是造成数据错乱。现在比较流行库开发,很多外设使用案例直接可以拿过来用,或者尝试着调整几个参数就可以了,似乎不需要懂太多的原理,但这样的学习终究只是把芯片玩起来了,要做到一通百通还是得从最原始的理论出发,只有把握住这些重点才能在项目开发的过程中直面问题并搞定它。
最后一个bug
2 5 嘉立创PCB
大话SPI通信--进阶避坑篇
相信大家对该通信的大部分基础理论知识已经了然如胸了,在开发过程中使用SPI通信问题应该不会太大,但SPI作为一种嵌入式工程师们如此青睐的通信方式,在开发中总会有人挖一些坑让你来填,当然这个人可能是自己。在定位这些问题的时候动不动就几天过去了,项目工程师又要来催了~那么今天bug菌根据以往的经验,将重点从通信的速度、容错性和结构这三个方面进一步谈谈SPI通信,也算是完结篇了。1、通信速度对于SPI通信并没有规范最高的通信速率,在我的开发经验中有见过达到50Mbit/s的应用场景,但通常比较常见的还是10Mbit/s左右。具体选用多快的通信速度,还得在实际项目中根据情况具体设计,比如:1、当前主从机的主频和项目的具体应用都与SPI数据的处理能力有着直接关系,一般SPI通信的时钟频率都是来源于主频分频,这就在一定程度上限制了其通信速率上限。即使能够达到较高速的速率,而处理器还需要处理更多的业务逻辑,再去处理SPI数据也是不够及时的,此时高速率并没有太大的意义了。2、SPI硬件PCB布线长度等等影响着线路阻抗,这也同样限制了通信速率,一般通信距离越长,通信速度越低,否则容易造成通信不稳定。特别是通信线路经过一些干扰源更是影响其稳定性,所以SPI作为一种相对高速的通信方式,一般都不会用于长距离通信中,而是大量用于微处理器与外部SPI接口的设备之间的通信,比如高速采样芯片ADC、处理器之间等等。3、前面说了即使SPI主机能够达到较高的通信速率,但从机主频或者数据处理能力不够,这样也是没有太大意义的,当你可以通过配置从机为接收队列或者DMA等方式进行优化,当然高速率在多机中能够减少同步延时。值得注意的是一些芯片标称的最高通信速率,是在比较好的外界条件下的测试值,超过了该标称值可能也能用,但并不会很稳定,容易导致通信异常。所以具体选用多快的通信速度,还需根据实际情况分析确认。2、容错性SPI不像IIC那样存在应答机制,也没有流控制机制,当从机配置较低,如果一个报文还没处理完,后一个报文又到来,导致传输错乱,其通信过程几乎都是靠硬件来保证数据的传输稳定性,是一种不可靠传输。当然如果是用于多机通信倒是可以通过制定可靠性校验协议来保证传输数据的稳定,但这也在一定程度上会降低通信的有效数据传输速度。像stm32的SPI外设发送和接收都存在独立的CRC校验功能,大致的原理就是使用CRC在每个位上进行串行计算,然后在最后一次数据传输结束时来传输CRC校验值,接受方接收到CRC以后自动拿着数据和CRC值进行比对,看是与否有数据故障,如果存在传输问题就会置位相应的CRC故障标志位告知。当然如果所选用的芯片SPI外设没有独立CRC模块可以模拟类似的操作进行处理,只是相对比较耗时,毕竟这个CRC得软件自己处理。stm32的SPI外设的灵活度远不止这些,比如配置成双线单向模式等,可以把MISO和MOSI都向一个方向传输,从而提高一倍的传输速度,感兴趣可以参考一下手册玩一下。3、通信模式SPI通信拓扑结构上的一大特色就是菊花链拓扑。上篇文章跟大家介绍的SPI一主多从的通信方式属于并行方式,要控制多个从机需要使用大量的GPIO信号来控制片选信号。似乎非常简约的SPI通信一下子变得不那么简单了,特别是布线方面是非常低效的,此时菊花链的通信方式便有了一席之地。菊花链模式下的SPI所有的片选和串联时钟信号都可以共用一条线,其数据以时钟节拍逐个bit的循环移动,从而完成数据传递,这样就大大减少了控制线路。当凡事总有其两面性,采用链式结构相对并联方式没有那么灵活,主机无法一次任意选中一个从机进行直接通信,而是得通过数据的循环传递。另外对于链式结构主从关系没有那么直接,相连接的从机之间存在着更多的依赖关系,一旦链路或者从机出现了问题,其影响是联锁的。菊花链的通信方式应用是非常多的,比如当我们在一块板上有多块控制芯片,但此时由于PCB大小的限制,可以选用菊花链的拓扑方式实现一个JTAG仿真器对多个JTAG芯片进行烧录和控制。
最后一个bug
0 1 嘉立创PCB
不要只成为PID调参工程师
最近跟一个同事聊了聊天,他说一直用的位置式PID,从来没在具体项目中用过增量式PID,感觉两者没啥区别呀?于是跟他讨论了一番,不由得让人深思~1、“万能”的PIDPID是一种非常经典的控制类算法,凭着它的简单易用在工程上得到了广泛的应用,并且影响力也是极高,那为什么说其简单易用呢?可以说只要你对PID的主要的参数对系统的影响理解得足够好,完全可以通过手动试凑的方式来获得一套合适的PID参考,所以对一些非控制类的工程师应用起来也是非常友好的。当然还有一个非常重要的原因,PID算法对大部分系统稳定性、鲁棒性以及可靠性都非常的高,有调试PID控制算法经验的朋友应该都有类似的感觉,即便根据经验随便给系统一套参数都可以到达一个不错的控制效果。所以在很多人眼里这算法就是"万能的",适应的场景也非常的广泛,然而自古有一种规律"熊掌和鱼不可以兼得",PID算法虽然适应性非常广泛,对于处理一些特定的应用场景,或许其并不会最优的解决办法。基于大家在实际应用中各种各样的需求,曾经一个发展涌现了非常的PID的变种,也就是对PID算法在特定的应用场景进行优化、或者与其他一些控制方法进行结合,以便达到相应控制场景下的不错控制效果,比如微分先行PID、PID与智能控制的算法的结合等等。当然如果PID算法实在无法满足需求了,那也不能勉强,就只能考虑寻找另外更为匹配的控制策略,比如现在非常流行的自抗扰控制技术等等。2、PID的理解PID算法主要就是三项,比例(P)-积分(I)-微分(D),在连续的时间域内的表达式如下:从上面的公式可以看出PID算法是一个非常纯粹的数学表达式,既然是数学表达式那必然可以通过数学的方式进行分析,拉式变换一下,获得传递函数,然后采用控制理论分析方法,结合被控对象分析其对动态系统的稳定性、准确性以及快速性进行分析,从而达到性能上的最优解,这个控制系统的设计过程不是本文重点,暂时就不进行展开了。还记得最开始了解这个算法的时候,有一句话非常相信的彰显其魅力所在:P-I-D分别代表着当前、过去和未来。P对当前误差的抑制作用;I对历史误差的累积控制,以便消除静态误差;D根据误差的变化率进行补偿,从表达式也非常容易从根本上理解这几句话。3、数字PID在前面跟大家简单描述了连续域内的PID算法表达式,而连续域中该算法需要通过相应的模拟电路来进行实现,特别是一些没有数字芯片的嵌入式系统是经常可见的。但随着数字系统的应用,特别是单片机系统,数字PID更加得到广泛的应用,被大部分人所熟知的两种数字PID,分别是位置式PID和增量式PID。大部分的教材都会描述着两种形式的数字PID,然而这些教材一上来就介绍位置式PID与历史状态相关,过去会对现在的控制输出产生影响,而增量式PID仅仅只与最近的几次误差数据相关。如下是位置式PID表达式:这样看来增量式PID只与最近的两次误差有关系,不会存在累积误差的问题,说来增量式PID必然是更好的,那为何还要介绍位置式PID呢?也有伙计在自己的程序中使用增量式PID的表达式,毕竟增量式PID其输出仅仅只是控制量的增量,最后还是需要把输出量进行累积,这与位置式根本没啥区别。于是很多初学者,甚至一些用PID多年的工程师对此都抱有一丝疑问。其实这两种数字PID的应用场景是有区别的,增量式PID控制输出的仅仅只是控制量的增量,其主要是应用在执行机构带有积分部件的被控对象,而位置式PID其输出的是实际的控制量,则用于不带积分环节的执行机构。当然如果在一些不带积分环节的执行结构系统中你执意要使用增量PID的形式也无伤大雅,只是最后用数字积分进行处理后输出,其与位置式PID式没有差异,增量式PID的优势并没有展现,因为真正单独使用增量式PID的执行机构的积分环节是连续的,这是数字离散的方式无法比拟的。
最后一个bug
6 8 嘉立创PCB
C语言X-MACRO宏使用技巧
1、#define与#undef今天分享一个C语言宏定义小技巧,从语法上来看比较简单,不过一旦真正领悟到其精妙之处不仅可以简化代码、还能提高代码的可扩展性。X-MACRO宏技术的核心在于灵活的应用#define与#undef,对于玩C语言的伙计#define是再熟悉不过了,但#undef却鲜有人在实际的开发过程中熟练使用,基本上都是#define走天下。那#define的作用域是怎样的呢?其作用范围都是从宏定义处到文件结束,不管函数内外均可以随意使用。那一不小心使用#define重复定义相同的宏又会怎样呢?对于大部分编译器会报重复定义警告,但也有小部分编译器采用最近的宏定义直接通过,所以稍不留神就把bug引入到了代码中。其实对于C语言编程素养良好的工程师们多会使用#undef来限制宏定义的作用范围,即取消宏定义,以免造成宏泛滥。 1#include [removed]2#include [removed] 3 4#define HELLO_BUG   100 5 6int main(int argc, char *argv[]) { 7 8 printf("hello bug %d\r\n",HELLO_BUG); 9 10#undef HELLO_BUG 11 12 printf("hello bug %d\r\n",HELLO_BUG); 13 return 0; 14} 如上代码所示,便会编译报错,提示第二条打印语句HELLO_BUG宏未定义。2、X-MACROX-MACRO平时我们也叫"X宏",其实在bug菌之前的文章[removed]有一个简单的提及,今天单独拧出来简化讲讲。1#define X_MACRO(a, b)   a 2//do something 3#undef X_MACRO45#define X_MACRO(a, b)   b 6//do something 7#undef X_MACRO 如上是X-MACRO的比较精华的几句,通过#define与#undef的配合,可以使用相同的宏名称选择性的替换出我们想要的结构,从而达到简化代码的目的。同时我们也非常清楚,由于宏主要是靠编译器来处理,所以X-MACRO技巧也主要是在编译阶段来维护代码。下面来一波操作看看效果吧: 1/*************消息定义**********/ 2#define MSG_TABLE                  \ 3    X_MACROS(USER_MSG1, MsgProc1)  \ 4    X_MACROS(USER_MSG2, MsgProc2)  \ 5    X_MACROS(USER_MSG3, MsgProc3)  \ 6 7/*************消息枚举定义**********/ 8typedef enum { 9    #define X_MACROS(a, b) a, 10    MSG_TABLE 11    #undef X_MACROS 12    MSG_MAX  13} MSG_TYPE; 14 15/*************消息处理定义**********/ 16const Proc Proc_table[] = { 17    #define X_MACROS(a, b) b, 18    MSG_TABLE 19    #undef X_MACROS  20}; 21 22/*************实际使用**********/ 23void sMessageProc(MSG_TYPE msgtype) 24{ 25    (Proc_table[msgtype])(); 26} 当然X-MACRO还可以扩展多个参数来供序列化替换,同时X-MACRO宏定义也可以更加的复杂。比如使用#defineX_MACROS(a,b) #a宏来处理为字符串等。
最后一个bug
4 15 嘉立创PCB
C代码中花括号写成这种风格竟被吐槽~
最近来了位新同事,闲暇时分聊了几句,其中有一点让我记忆特别深刻,说:"怎么我们这边代码中的花括号风格都独立另起一行,看代码的时候挺不适应的~",我笑着说:"习惯就好了~"。 其实对于C代码中花括号的书写风格,bug菌也不是第一次听到不一样的声音了,包括之前文章中所编写的一些代码示例,也是有朋友跟我提及此事。 其实对于C代码中的花括号的风格也绝非偶然,他们是有具体的命名和使用考虑的,目前使用比较广泛的是如下两种风格: 风格一:Kernighan & Ritchie风格,也就K&R Style void function() { if(bTrue){ //do someting...... } else{ //do someting...... } } 这种风格的花括号与使用它的语句同一行,其实这也是Linux内核中常采用的一种风格,相比风格二没有单独起一行,在一定程度上降低了代码长度,整体更加紧凑,但使用者经常会衍生出各种变体风格。 风格二:Allman风格,也叫BSD Style。 void function() { if(bTrue) { //do someting...... } else { //do someting...... } } 这种风格也是bug菌目前习惯的一种编码风格,该编码风格有一种对称美,可能也是早已习惯的原因吧,但缺点也很明显,比较浪费行,一些朋友常说:"我都没写几句代码,却占了一个显示器~"。 至于花括号使用哪种风格也是一直存在一些争议,也没有说哪种风格会更胜一筹,所以不管选择哪种风格,最重要的是保持统一,不用过分纠结。
最后一个bug
5 16 嘉立创PCB
唠叨一些嵌入式技术学习习惯~
最近一些朋友问道如何快速提升自己的技术能力,以前觉得提这样问题的人有点急功近利,所以一般也不太愿意去解答,或许我也不知道有什么方法能够快速提升吧。但闲暇时分也会常常偷偷的问自己,假如一切从头开始,我会以怎样的方式去提升技能呢?思来想去最后总结了4点内容,在这里分享一下:1、多交流、多分享。技术更新是很快的,可能嵌入式技术革新相对计算机要慢点,不过目前受龙"卷"风的影响,很多技术的更新周期缩短了很多。不断的学习是一方面,但也不能蒙头搞研究,还需要多跟同行交流优秀的技术和方案,这样在学习和开发中才会更加的高效,拥有更多的思路和灵感,也会少走很多弯路。2、多利用零碎的时间。时间是我们生命中非常重要的东西,年龄、头发的颜色和疏密层度就是时间的痕迹。相同的年纪,你知道了解的东西越多、掌握的技能越透彻,便可以更容易超越很多的人。前段时间听到一个空降26岁技术总监的故事,其实并没有感觉很意外,要么是皇亲国戚,要么就是有两把刷子,其他情况下都不会是长久之计。而对于大部分打工人而言,一个人的成长很大程度上取决于工作之外的时间利用。3、注重基础。万丈高楼平地起,自从bug菌工作以来深感基础的重要性,很多时候深入、透彻的研究一些复杂的技术,我总会先大致学习或复习与之相关的基础知识,把知识脉络理清楚,然后一层一层往上学,知识盲区自然会被各个击破。同样像我们平时进行软件编程,各种底层驱动、框架搭建好了,那么上层应用开发会省心不少,其实是一样的道理。4、多总结、多实践。人是会健忘的,同样很多技术知识,如果你长时间的不去温习,自然就会生疏,很少有人能够做到长时间不用的技术能够信手拈来,所以很多时候一些朋友面对自己的生疏会变得非常焦躁。以前明明这些技术我可是“玩得溜溜的”,可现在走两步就有种掉坑的感觉,甚至开始怀疑自己是不是老了,记忆力衰退了等等。这些都是正常现象,我们不可能保证自己所学习的东西能够一直想硬盘一样存储中你的脑海。一方面我们需要定期总结重点技术要点,梳理自己的技术知识体系,只有这样,当我们需要把尘封的知识重新解封的那一刻,那么这些技术知识总结能够帮助我们在最短的时间恢复到当初的最佳状态。另外一方面就是多实践,为的就是让自己印象更加深刻,减慢遗忘速度,同时在技术知识的实践过程中也能够有自己新的领悟和理解,就如同恋爱一样,为什么会那么的刻骨铭心?因为你亲身经历过,真真切切的感受过,而不仅仅只是在电视上或者书本中在看着别人在谈情说爱。
最后一个bug
4 5 嘉立创PCB
嵌入式编程别忽略了C语言的标准
最近做代码评审发现很多同事的编码都游走在风险的边沿,其中最显眼的就是局部变量定义位置比较随意。对于C语言编程老手而言,绝大部分都已经养成了"变量定义必放在语句块的开头"这一习惯,依稀还记得那时候学校老师在课堂上强调C语言的局部变量一定要放在函数的开头。刚开始学习C语言的时候,感觉也没啥好解释的,照着书上和老师的"规矩"来就可以了,后来又学习了C++,C++对这一块相对就比较灵活,注意一下作用域,随时定义随时用,不需要遵循变量定义必须放在函数开头这一规则。直到很久很久以前有一次看到别人的C程序也可以像C++这样灵活的定义变量,才认识到C语言还有不同的标准,也是从那以后开始了解C语言的标准。所谓"无规矩、不成方圆",不同的编译器会根据不同的标准引入相关的"规矩"来约束和指导编程人员进行程序设计。C语言作为嵌入式领域主力的编程语言,自然也应该不断的修正和优化,那得有专门的人来做这些事 -- C语言标准化委员会,目前编译器主要参考的有三个标准:当然啦,据了解目前的标准都已经更新到了C17,但具体哪些编译器对其进行了支持,那又是另外一个故事了。编译器为了满足不同开发者的需求,以及新旧代码工程的兼容,都会把C语言标准的相关选择放开到编译选项中,开发人员根据自身需求进行选择编译自己的工程源码,我们来看看,比如:IAR编译器中:GCC编译器中:GCC提供了一些C程序编译的扩展选项,使用-std来进行设置,比如编译的时候选择-std=gnu99,对应着GUN对C99的的一些扩展。特别在进行代码的移植等等方面,对于当前手头工具所遵循的C标准还是要心里有数的,不然一大堆编译错误,够你折腾的。前面bug菌聊到,虽然C语言标准是在不断的更新,然而编译器却不一定同步支持,纵观大部分嵌入式编译器绝大多数还停留在最高仅支持C99的标准,甚至还不一定全面支持该标准。个人觉得主要的原因还是对于相关的标准实现起来比较繁琐,同时在相关领域使用的频率实在是太低,所以没有得到全面支持,当然了,有一些编译器为了更好的客户体验,还会加入自身的一些特殊扩展。那么bug菌这里简单罗列了一下相对于C89标准,C99标准所新增的一些常用的标准项目:1、预处理,行注释"//"的支持;2、inline内联关键字,内联函数的支持;3、变量声明不必放在语句块的开头,这也是前面提到的,支持该特性以后for循环常用的一种方式是:for(int i=0;i[removed]
最后一个bug
5 10 嘉立创PCB
四种Bootloader程序安全机制设计
不管是玩单片机还是嵌入式linux,基本上都会接触到bootloader,所以bootloader程序也是一个关键的组件,进行硬件初始化,应用程序的合法性、完成性检测、升级功能等等都与其息息相关。像一些在ram运行的应用程序,或者是一些运行内存比较小的MCU等等,它需要保证从外部存储器中读取的应用程序是可信的,不能拿过来就执行,那往往是不安全的,特别是为了防止恶意程序的注入和攻击,在Bootloader程序中实现一定的安全机制就非常有必要了~那么bug菌今天就大致聊聊,一些常用的Bootloader安全机制的设计:1、进行image校验image校验我们也常叫镜像校验,Bootloader程序可以对从外部存储器中读取的应用程序镜像进行校验,以确保其完整性和正确性,常见的镜像校验方式包括CRC、哈希值等。当然你如果想简单一点使用求和检验也是可行的,将应用程序镜像中每个字节的值相加,得到一个校验和。Bootloader程序将计算得到的校验和与预先存储在应用程序镜像中的校验和进行比较,如果两者相等,说明应用程序镜像没有被篡改,校验和的优点是计算简单,缺点当然就是校验能力有限,更易受攻击。而相比求和校验,CRC等是一种更加可靠和安全的校验方式,它可以检测出更多的错误,并且不易受攻击。在STM32中,CRC校验可以使用硬件或软件实现。硬件CRC校验可以使用STM32的CRC模块,直接对应用程序镜像进行校验,速度快、精度高。软件CRC校验则需要Bootloader程序自行实现CRC算法,速度较慢,但也可以得到较好的校验效果。如果校验通过,则可以加载应用程序镜像;如果校验失败,则应该拒绝加载应用程序镜像,并给出相应的提示。镜像校验是可以保证从外部存储器中读取的应用程序是可信的,避免恶意程序的注入和攻击。如果镜像校验失败,Bootloader程序可以拒绝加载应用程序镜像,避免恶意程序的注入。2、签名认证签名认证并不是一个加密和解密的过程,它是一种数字签名技术。数字签名是一种用于确保电子文档的完整性和认证性的技术,它使用密钥对(公钥和私钥)来生成和验证签名。数字签名的过程如下:1、生成签名:文档的发送者使用私钥对文档进行哈希运算,然后使用私钥对哈希值进行加密,生成签名。2、验证签名:文档的接收者使用公钥对签名进行解密,得到哈希值,然后对文档进行哈希运算,得到另一个哈希值。接收者比较这两个哈希值,如果相等,则说明文档没有被篡改,并且签名是可信的。需要注意的是,数字签名技术依赖于密钥对的安全性,私钥需要妥善保管,防止泄露和丢失,公钥需要预置在Bootloader程序中,确保其可信和安全。签名认证需要使用一组密钥对,包括私钥和公钥。开发者使用私钥对应用程序进行签名,Bootloader程序使用公钥进行认证。如果签名认证失败,Bootloader程序可以拒绝加载应用程序镜像。3、内存保护在STM32的Bootloader中,内存保护是一种重要的安全机制,可以保护Bootloader程序和其他重要的代码和数据不被应用程序覆盖、修改或篡改。内存保护通常使用硬件或软件实现。在STM32中,硬件内存保护可以使用CPU的内存保护单元(MPU)实现,软件内存保护可以使用内存管理单元(MMU)或软件实现的保护机制,具体使用哪种需要看资源情况。硬件内存保护可以将MCU的内存空间划分为若干个区域,并为每个区域配置不同的访问权限。Bootloader程序可以将应用程序加载到特定的区域中,防止应用程序覆盖或修改Bootloader程序和其他重要的代码和数据。硬件内存保护的优点是速度快、可靠性高,缺点是配置复杂,需要占用一定的硬件资源。软件内存保护可以使用MMU或软件实现的保护机制,但一般有MMU单元的都属于微处理器了,而微控制器。MMU是一种硬件单元,用于将虚拟地址映射到物理地址,并可以配置不同的访问权限。软件实现的保护机制可以在Bootloader程序中实现一些保护代码,用于检测和防止应用程序的篡改和攻击。软件内存保护的优点是灵活性高,缺点是速度较慢,可靠性较低。内存保护可以保护Bootloader程序和其他重要的代码和数据不被应用程序覆盖、修改或篡改。在实现内存保护时,需要注意区域划分、加载位置、访问权限、保护代码等方面的问题。4、使用只读存储器只读存储器(ROM)通常是一种非易失性存储器,具有读取快、安全可靠、不易受攻击等优点。将Bootloader程序放在只读存储器中,可以保证Bootloader程序的完整性和安全性,防止恶意程序的注入和攻击。只读存储器也可以是MCU的Flash、ROM等。5、最后值得注意的是,Bootloader程序的安全机制应该根据实际的应用场景和需求进行设计和实现。安全机制的增加会增加系统的复杂性和开发成本,因此应该在安全性和可用性之间进行权衡。
最后一个bug
0 1 开源硬件平台
单片机程序开发把优化等级直接拉满吧~~
一谈到编译器的优化等级,很多人都会潜意识的认为:"一定不要开优化,否则会产生意想不到的问题~",于是就这样口口相传,使得很多后来的学习爱好者都会有意的去回避掉优化等级这个问题。优化其实是多个方面的,就像我们平时不同的人写一个功能模块的代码,有些人写出来的代码运行效率高、可是代码量大;而有些人写出来的代码效率一般,但是比较简洁有效等等。如何选择优化选项也是类似的,编译器会根据你的优化选项内容,按照一定的规则和策略来优化你的代码,比如代码中存在较多无效的逻辑语句,如果不进行优化,那么编译器几乎会根据你所写的语句一步一步进行“翻译”成汇编来执行,这样的运行效率是很慢的,好在现在大部分编译器即使你不开优化等级,也会对你的代码进行或多或少的优化处理。设置了对应的优化等级,编译器会根据你所选择的优化等级,对你的代码进行不同程度的分析,最终从不同的维度来进行优化处理(如代码大小、资源的占用、执行性能等)。那么很多掉过坑的朋友该说了,“有一次编译器开启了优化,不是按照我所编写的逻辑来运行的,于是从那以后我就习惯了不开优化”。其实通常编译器相对我们而言,更加的了解处理器,对于前面那位朋友的问题,更多的还是因为对所用的编译器不够熟悉,编写的代码让编译器"误解"了你的意思,不能完全赖在优化上。还是那句话:"存在即合理",编译器也仅仅只是一个工具,不要因为驾驭不了,就持否定的态度。反而你所编写的代码如果经不住各种优化等级的考验,是不是可以从侧面反映出当前代码的设计不合理?不够规范?考虑得不够全面呢?实际项目中让程序代码在所有优化等级下都可以正常运行来检查各种奇葩问题,也是一种检验代码健壮性的有效手段。GCC毕竟是嵌入式领域广泛应用的编译器,下面便以GCC编译器为例讲解一下相关优化选项:(其他编译器略有差异,注意区分)1、GCC的优化选项通常我们使用到-Ox选项,其中x有:0、1、2、3等,随着数值的增加,优化的程度也是逐步增加。-O0通常是不对代码进行优化,或者是最简单的一些优化,跟源码执行顺序基本是一致的,也比较方便单步调试,所以其不需要过多的分析代码,编译速度也是相当快的,debug阶段通常用该优化项,等代码稳定以后,再考虑其他的优化项。-O1主要是一些基础类的优化,比如删除一些无效的代码、展开内联函数等,能够在一定程度上提高运行效率。-O2在O1基础上进一步优化,比如循环优化、函数跳转优化等,编译相对较长,且代码的大小也会有一定增加。-O3在O2基础上更进一步优化,比如循环展开、向量化等。通常使用-O3优化较少,因为其优化程度较高,带来的逻辑风险也相应的提高,而-O2是一种比较折中的选项,基本满足要求,且比较安全可靠。除了上面常见的集中,当然你也可以使用其他更加精细化的选项,来自己控制优化的内容,具体就要参考GCC的相关手册来了解使用了。2、GCC部分代码设置编译选项有时候一套代码中有部分代码需要进行不一样的优化策略处理,那么通常编译器可以设置某些函数进行优化、或者某些文件进行优化的设置等。下是GCC部分代码使用固定的-O0进行优化#pragma GCC push_options #pragma GCC optimize ("O0") void foo(void) {     /* Do something, but don't optimize this function */ } #pragma GCC pop_options 当然,如果当你怀疑相关问题与优化等级的选择相关,可以采用这种方式,把部分可以代码进行非优化状态来运行,从而缩小问题的排查范围。3、优化等级越高越不好调试特别是玩单片机的同志们,习惯了使用调试器来进行单步调试,如果你想让代码编译以后易于调试,那么编译后的执行逻辑与源码所编写的执行逻辑基本上要吻合,此时代码就不能有优化处理了。所以如果你是为了获得更高的性能、更小的代码空间、甚至是编译时间,我们会选择进行优化,必然就会牺牲易于调试这一点。这也是很多朋友开了优化等级以后,单步调试断点到处乱跳的原因。所以通常我们在前期调试的时候不使用任务优化选项,而到了需要进一步优化代码大小、效率等以后再考虑开启优化等级。4、两个C关键字特别重要1. volatile 关键字volatile 关键字能够阻止编译器的过度优化,可以做到如下两件事情: · 阻止编译器为了提高速度将一个变量缓存到寄存器而不写回; · 阻止编译器调整操作 volatile 变量的指令顺序;2. register 关键字将代码放在寄存器的方式是使用 register 修饰变量,适用于频繁调用的变量。
最后一个bug
0 2 立创开发板
这个变量要不要用volatile修饰呢?
大家好,又见面了,我是bug菌~在嵌入式软件开发过程中,如果对volatile不熟,那可以你应该是个"假嵌入式程序员",因为一个变量需不需要使用volatile考虑的场景挺多的,如果在某些场景下乱用,会影响程序运行效率,有时候忘记加甚至会使得程序发生异常,那么bug菌今天就大家好好聊聊这个C语言关键字:1、传统定义volatile直译为“易变的”,也就是告诉编译器这个变量随时都可能发生变化,编译器你跟我“特殊照顾一下“。那么编译器通常会怎么去处理使用volatile修饰的变量呢?对于C变量都是代表着对应的内存,读取使用volatile修饰的变量,会直接从其所对应的内存地址中获取最新的数据,否则,编译器会对其访问进行优化,比如直接从缓存中读取副本、或者是从寄存器中读取副本。这样就可能会导致数据更新不一致的问题。2、最常用的地方从前面对volatile的功能描述,我们可以知道volatile最常用于那些与硬件外设寄存器打交道的地址,这样确保每次对寄存器的读取都是从内存中获取的最新值,比如:再比如下图所示,如果我们向地址0x812100地址连续改变其值:那么编译器通常会将其直接优化为第三条操作,并不会去执行前两条操作,这样会造成写寄存器时序上的问题。如果采用volatile去修饰,则三条命令便会依次执行,达到我们代码所示三次操作的目的。3、更复杂一点的,也是最重要的 其实对于volatile所解决的问题用更加专业的说法可以分为:可见性和有序性。1、可见性所谓可见性,通常是在多线程访问共享数据的情况,当一个线程对共享变量进行修改,而其他线程能否立即观察到这个修改的性质。在我们目前大部分单核一级缓存的CPU无需考虑这个问题,而对于现场多核多级缓存处理器,各个现场都会维护着自己的缓存,如果仅仅只是更新到了自己的缓存中那么其他线程是无法立马感受到这个修改的,最终导致结果不一致。2、有序性很多时候也叫作重排序,说白了就是对执行指令进行了执行顺序上的优化,以不改变指令运行的最终结果,而改变指令的执行顺序。编译器可以调整指令,同样处理器的多级流水线和乱序执行也同样可以改变指令执行顺序;甚至为了多级缓存的高效执行,也同样会对内存读写操作进行重排序。然而这样的重排序,却会对多线程并发访问共享数据的过程中产生问题,从而不符合我们编程源码的预期执行顺序。但对于volatile只能在一定程度上防止指令重排序,其只能保证单个变量访问的有序性,而不能保证整个程序的有序性,所以这一点是大家尤为要注意的。所以讲了这么多,相信以后大家再开发中也都会遇到。
最后一个bug
2 10 开源硬件平台
嵌入式系统如何尽可能避免存储数据丢失与损坏?
大家好,我是bug菌~对于一些需要动态存储数据的嵌入式系统往往我们需要考虑系统在各种状态的数据可靠性问题。当然也不仅仅这些数据敏感的协议,最常见的就是你向存储系统写入数据的过程中给断电了,系统下一次上电跑飞了~掉电过程是最为敏感的情景,也是一般在系统设计前期要重点考虑的,那么今天bug菌就跟大家重点聊聊一般的嵌入式系统如何尽可能的避免重要存储数据的丢失与损坏。1、掉电检测  前面也提到了,掉电过程是数据丢失和损坏比较高发的状态,一方面离不开硬件上掉电备电电源的相对稳定性和持久性,另一方面也需要软件部分最好掉电过程系统完整的收尾工作,最常见的问题就是正在掉电,你还在使劲的写文件或者其他改变存储介质的操作,运气好可能只是文件写少了;运气不好直接文件系统就崩溃了~那么快速的掉电检可以帮助系统在断电前尽早将这些数据进行保存,以确保系统重新上电后能够恢复到正常工作状态,而不会因为掉电导致数据丢失或损坏。2、存储器件的寿命与稳定性电子产品都有使用寿命,在嵌入式设备里面常用闪存存储器,即Flash,而闪存通常以擦除/写入循环次数(P/E cycles)来衡量其寿命。常见的闪存产品如NAND和NOR闪存都有固定的P/E周期数量,一般在几千到几十万次之间,所以如果频繁擦写就会导致损坏,最终也会使得数据丢失,另外,闪存的寿命还受到温度、电压以及擦除/写入操作的影响。所以为了减少存储介质上的数据丢失要么选择高品质且可靠的存储介质,要么根据介质的特点优化存储算法,延长使用寿命。那么通常在软件层面有如下几种软件处理方法和策略:磨损均衡在闪存中,频繁写入相同的块会导致这些块的寿命提前耗尽,从而降低整个存储器的寿命。磨损均衡算法旨在平衡闪存中不同块的使用次数,避免某些块过早失效。可以通过选择写入次数最少的块来进行新数据的写入,或者通过重新映射块来实现。垃圾回收当删除或更新数据时,闪存中会产生垃圾数据,占用空间而无法直接写入。垃圾回收算法会定期检查闪存中的垃圾数据,并将其重新组织以释放可用空间。有效的垃圾回收算法可以减少擦除操作的频率,从而延长闪存的寿命,当然如果你没有用文件系统,只是裸写,基本上都是按顺序去写了。坏块管理坏块管理指的是处理闪存存储器中出现的无法正常读取或写入数据的坏块的过程。通过坏块检测、标记和替换,系统可以有效地识别和处理坏块,确保数据的完整性和可靠性。坏块管理还包括维护坏块映射表,以记录坏块的位置和替代块的使用情况。有效的坏块管理可以延长闪存存储器的寿命,提高系统的可靠性,并确保数据安全。写入放大减少写入放大指的是实际写入闪存的数据量与应用程序请求的数据量之间的差异。减少写入放大可以减少对闪存的写入操作,从而延长其寿命。这可以通过合并小的写入请求、延迟写入、以及数据压缩等技术来实现。静态和动态数据分离将静态数据(很少修改的数据)与动态数据(频繁修改的数据)分开存储在不同的闪存块中。这样可以避免频繁写入对静态数据块的影响,延长其寿命。温度和电压管理通过一些辅助的采样。来调节读写负荷,维持在合适的工作温度和电压可以减少对闪存的损坏和老化,从而延长其寿命。3、数据备份对于数据动态存储非常严格的应用需求场合,保证嵌入式设备的实时数据存储稳定性是非常重要的,特别是对于需要高可靠性和实时性的应用场景。以下是一些办法来确保嵌入式设备的实时数据存储稳定性:实时数据备份实时将数据备份到多个分区或者其他位置,例如本地存储和远程服务器,即是一块区域物理上遭到破坏,也能从其他区域进行恢复,极大的降低了数据丢失或损坏的概率。使用事务性存储机制采用具有事务性支持的存储机制,确保数据的原子性操作,即要么全部写入成功,要么全部失败,以避免数据不一致性,以免存在第三种状态完成系统的混乱与破坏。实时监控和错误处理建立实时监控系统来检测存储设备的健康状况,及时发现并处理存储设备的故障或错误,以前bug菌就接手到一些项目,写数据出了问题,好几天系统也没有提示,客户也没有及时查看,等发现问题已经好几周的数据异常了。采用更加成熟的文件系统一些支持掉电保护的实务型文件系统基本都支持日志功能或者文件系统级的保护机制。数据完整性校验实施数据完整性校验机制,例如循环冗余校验(CRC)或者哈希校验,来检测存储数据的完整性,及时发现和纠正数据损坏。
最后一个bug
0 5 立创开发板
写大型C语言工程makefile构建
最开始学习linux应用开发编写的时候,估计大部分伙伴们都是在一个目录里面编译整个工程,主要是linux通常没有非常合适的集成开发环境。以前单目录的方式实在太过捡漏,在linux环境中进行C代码工程开发很多时候需要编写一个相对比较通用的makefile,一劳永逸,能自动查找并归类每个目录的文件进行编译。可能很多朋友会选择一些cmake,scons等自动化构建工具,但也有部分伙计编写makefile也完全够用,嵌入式平台迭代速度不快的话基本上可以成为传承级代码,那么今天大致梳理了一下makefile中构建大型一点的工程需要用到的一些编译语法与函数。1、常用的特殊变量这些符号是Makefile中的特殊变量,用于在规则中引用文件名和目标,$^ 用于表示所有的依赖文件列表,多个文件以空格分隔。在规则中,它可以用来引用所有依赖文件的列表。例如:target: dependency1 dependency2 dependency3     command $^ 在这个例子中,$^ 将会展开为 dependency1 dependency2 dependency3。$@/codespan style="color: rgb(123, 12, 0); font-size: 16px;"> 用于表示目标文件的名称。在规则中,它可以用来引用目标文件的名称。例如:target: dependency1 dependency2 dependency3     command $@ 在这个例子中,$@ 将会展开为 target。$<用于表示规则中的第一个依赖文件。通常在单目标多源文件的情况下使用。例如:target: dependency1 dependency2     command $< 在这个例子中,$< 将会展开为 dependency1。$? 用于表示所有比目标文件新的依赖文件列表。通常在需要重新生成目标文件的情况下使用。例如:target: dependency1 dependency2     command $? 在这个例子中,如果 dependency1 比 dependency2 新,则 $? 将会展开为 dependency1;如果 dependency2 比 dependency1 新,则 $? 将会展开为 dependency2;如果两者都是相同的时间戳,则 $? 将为空。$* 用于表示规则中目标文件的文件名部分(不包括扩展名)。通常在需要将目标文件名作为参数传递给命令时使用。例如:%.o: %.c     gcc -c $< -o $@     ./process $* 在这个例子中,如果目标文件是 example.o,则 $* 将会展开为 example。除了上面提到的 $^、$@、$<、$? 和 $*,Makefile 中还有一些其他常用的特殊变量。$(MAKE): 用于表示 make 命令的名称。这在递归调用 make 的时候非常有用。$(CC): 用于表示 C 编译器的名称,默认情况下是 cc。你可以在 Makefile 中使用这个变量来指定编译器。$(CFLAGS): 用于表示传递给编译器的参数。这个变量通常用于指定编译选项,比如警告选项、优化选项等。$(LDFLAGS): 用于表示传递给链接器的参数。这个变量通常用于指定链接选项,比如库路径、库文件等。$(RM): 用于表示删除文件的命令,默认情况下是 rm -f。你可以在 Makefile 中使用这个变量来指定删除文件的命令。比如$(CFLAGS) 是一个在 Makefile 中常用的特殊变量,用于指定传递给编译器的参数,可以使用它来设置编译选项,比如警告选项、优化选项等。通常是在 Makefile 中定义 $(CFLAGS) 变量,并在编译规则中使用。例如:CC = gcc CFLAGS = -Wall -O2 # 编译规则 %.o: %.c     $(CC) $(CFLAGS) -c $[removed]
最后一个bug
8 18 开源硬件平台
大部分公司的嵌入式软件只有debug版本
大部分开发工具或者IDE中都为你创建的工程配置为debug和release两个版本,字面上很好理解,一个用于调试debug,一个用于发布release,那么是不是很多伙计都是debug版本用到老?那么今天聊聊release的必要性吧。1、两个版本的通用解读在大型嵌入式应用软件开发中,如linux应用程序,这两个版本类型主要针对软件开发在不同阶段的构建过程的一些优化和设置,当然这些优化设置也是可以更改的。Debug版本主要是于开发和调试阶段,你所编写的软件通常被编译为未经优化的代码,以便于调试和排查。编译器会保留大量的符号信息、调试信息和原始代码结构,以便于在调试器中查看变量、函数调用栈等详细信息,有助于开发人员快速定位和解决问题。而对于release版本,经过了各种优化,以提高代码的执行效率和减小代码体积,比如去除未使用的代码、内联函数、循环展开、常量传播等,这样会使得程序具有较高的性能和较小的内存占用,适合于在实际运行环境中使用,但这也会大大削减一些调试信息和辅助检查。在大型OS上运行的应用程序所使用的两个版本均有如上特点,但对于单片机固件却有所差异,但也非常相似。2、单片机两种版本的特殊对于单片机软件系统中,这两个版本上的配置相对比较单纯点,主要是如下三个方面:1、优化级别:Debug版本通常使用较低的优化级别,例如-Og(优化以便调试),以便在调试过程中能够更好地观察变量的值和程序的执行流程。这种优化级别保留了较多的符号信息和原始代码结构,有利于开发人员进行实时调试和故障排除。Release版本则会使用更高的优化级别,例如-O2或者-O3(最大优化级别),以提升代码的运行效率和固件的性能。高优化级别会进行函数内联、循环展开、删除未使用的代码路径等操作,当然你也可以更细粒度的进行优化项目的设置,从而最大程度地优化代码的执行速度和资源利用率,但也会在一定程度上干扰开发软件对于程序与执行过程的对应分析。2、调试信息输出Debug版本通常会让程序中较多的debug_printf生效,这些打印调试信息不仅仅有第三方组件,还有一些自定义的调试信息,这样可以在一定程度上让开发人员更加直观的了解程序运行流程和结果,但也同时会导致程序执行负荷较高,体积变大。3、安全检查和错误处理:基本上也就是我们说的断言了,其实它就是在程序运行时检查程序中的某个条件是否满足预期,如果条件不满足,则程序会进入一个错误处理流程,通常是通过打印错误信息并可能进行一些清理工作后停止运行,如下代码示例:#include stdio.h#ifdef NDEBUG #define ASSERT(expr) ((void)0) #else#define ASSERT(expr) ((expr) ? (void)0 : assert_fail( __FILE__, __LINE__ )) #endif void assert_fail(const char* file, int line) {     printf("Assert failed at %s, line %d ", file, line);     while (1); // 可以添加清理代码,例如锁定资源,然后停止执行 } int main() {     int x = -1;     ASSERT(x > 0); // 这里的断言会失败,会调用assert_fail函数     return 0; } 所以当断言较多的时候,每个函数基本上都会对输入进行检查,执行效率可想而知。3、两个版本的必要性一般的公司基本上都只有一个debug版本,特别是单片机固件开发的项目更为突出。当然为了减少开发周期、又想保证产品功能的稳定性,处理器相对裕量较大,一直使用debug版本也不是不行,毕竟很多公司这些年就这么过来的,总比有些工程代码编译出来的release直接就飞了,还不知道怎么飞的好太多了,公司产品又急着上线,客户又要求快速解决,确实不容易;debug版本丢到现场,出了问题有日志,日复一日,终可修护稳定。不过从开发角度两个版本还是非常有必要的~release执行效率的提高、代码固件的压缩能够进一步降低核心主控的成本,同样对于执行效率敏感和内存资源受限的平台还是非常有用的,而且专业软件公司都会有单元测试与集成测试的开发与使用过程,release软件的稳定性能够得到较好的测试保障。同时debug版本通常比release版本更容易被反向工程,debug版本保留了大量的符号信息、调试信息以及原始代码结构,这些信息对于逆向工程者来说提供了更多的线索和可操作性。而且你打印了较多的调试信息,对于友商开发工程师而言也是很好的参考资料,相关功能也推敲个七七八八。当然话说回来,如果所开发的产品都是定制的话,并不需要去市场上争蛋糕,其实这块优势也会弱化。所以在软件稳定并且即将部署到目标设备时,尽量切换到Release版本,值得注意的是这里的release不是你测都没测,直接切换选项卡生成的版本,而是稳定发布版。正确选择和管理Debug版本和Release版本,能够有效地支持整个嵌入式软件开发生命周期的各个阶段。
最后一个bug
1 6 立创开发板
芯片的唯一标识符有什么作用?
今天主要是大家聊聊设备唯一标识符:1、聊聊唯一标识符早期大部分芯片都没有唯一设备标识符UID,英文叫Unique ID,现在去查查其实很多芯片现在也没有唯一标识,然而随着芯片成本降低,功能上大家基本都对齐了,新推出的芯片都会有一个唯一标识码,通常这个编码在芯片制造的过程中就生成了,用户通常读取固定地址或者调用相关API即可轻松获取。当然了这些UID通常不是随机的,都有一定的规律,比如标识制造商、批次、芯片型号等等,所以通常厂商会根据UID的部分字段做一些功能的区分。有一点一定要注意:对于相同型号的芯片,其UID通常是唯一的,但不同型号的芯片,尽管是同一家公司,其UID也有可能不是唯一的。因为之前遇到过这个问题,所以特意提示下,这一点一些人容易有惯性思维。2、UID有那些应用?那么唯一标识符到底有啥用处呢?字面上那肯定是为硬件设备提供唯一性,以便区分罢了,但具体涉及到哪些方面会要用到唯一标识区分呢?下面我总结了几个方面:1、产品唯一标识许多不同的设备可能会共享同一种硬件平台,每个设备都分配一个唯一标识符,系统可以确保每个设备在全球范围内都是唯一的,避免了设备间的冲突。像现在许多的IoT设备,其中的每个传感器或控制器可以通过唯一标识符进行识别和管理,这对于设备的监控、配置以及后续维护是非常重要的。2. 设备身份认证在一些需要身份验证的应用场景中,唯一标识符可以作为设备认证的一部分。在系统初始化或进行安全通信时,通过标识符验证设备的合法性,从而提高安全性。现在有很多的智能家居的产品,所有设备(如智能门锁、摄像头等)在联网时可以使用唯一标识符来进行身份认证,这些标识符提前录入了系统,确保只有经过授权的设备能够接入系统。3. 版本控制和固件更新通过设备的唯一标识符,厂商可以为特定设备提供定制化的固件更新或配置管理。这样一来,即使是相同型号的多个设备,也可以根据其唯一标识符来执行不同的操作或更新。4. 系统完整性绑定多个设备的唯一标识符可以进行捆绑,当检测到标识符不匹配可以进行报警提示,防止系统被拆解,从而带来的一系列混乱、不匹配问题。5. 软件许可和防盗唯一标识符可以作为本地软件运行的一种许可管理和防盗机制。设备唯一标识符可以与授权许可绑定,也就是相当于一种密钥,防止不法商家盗版软件的使用或设备被非法复制。通过唯一标识符与授权信息绑定,厂商可以确保只有授权的设备才能使用特定功能,防止盗版设备影响正常运行。
最后一个bug
0 1 立创开发板
UDP通信哪有那么不靠谱呀~
大家好,我是bug菌,又见面了~最近在做设计评审时,下面工程师在对用UDP和TCP中展开了激烈的讨论,而一个同事发表了这样的观点:"能用TCP绝不用UDP,UDP实在是太不靠谱了~"说实在的我也很理解,但是对于这样的言论也容易误导人,导致很多初学者在尝试使用UDP设计的时候,只要出现一些埋得比较深的问题就会一棍子把UDP给拍死,这是我觉得不应该的。其实UDP并没有那么不靠谱。1、UDP数据包首先我们看看UDP的数据包格式,数据内容本身的正确性是由UDP的**校验和(Checksum)**机制保证的:如果数据包在传输中发生比特错误(如电磁干扰),接收端会直接丢弃该包,不会将错误数据传递给应用层。然而UDP的“不可靠”主要是如下三点:不保证数据包到达(可能丢包)不保证顺序性(可能乱序)不主动控制发送速率(可能拥塞丢包)所以有些数据错乱问题的过,真不该让UDP来背,往往有时候就是自己设计的应用层逻辑设计不当。比如下面常见的一些场景:场景1:乱序未处理• 问题:接收端先收到后发送的包(如视频流的第2帧早于第1帧到达)。• 根因:应用层未实现乱序重组逻辑,直接按接收顺序处理数据。• 改进:在数据包头部添加序列号(Sequence Number),由应用层缓存并排序。场景2:分包/合包逻辑缺失• 问题:发送端传输了超过MTU的大数据包,IP层自动分片,但接收端未正确处理。• 根因:应用层未实现手动分包/合包逻辑,依赖IP分片(可靠性差)。• 改进:在应用层主动将大数据拆分为≤1472字节的块,并为每个块添加序号和偏移量。场景3:未区分丢包与延迟• 问题:误将延迟较高的包视为丢失,导致逻辑错误。• 根因:未设置合理的超时重传机制。• 改进:为关键数据添加ACK确认与重传,非关键数据允许丢弃(如实时音视频)。2、丢包原因其实UDP的丢包主要还是与网络环境有关,网络拥塞:路由器缓冲区溢出时,UDP包被优先丢弃(TCP会主动降速,UDP不会)。链路质量差:无线网络(如Wi-Fi、4G)易受干扰,物理层丢包率上升。IP分片丢失:若数据包超过MTU被分片,任一碎片丢失会导致整个UDP包不可用。虽然会丢包,但丢包 ≠ 数据错乱, 丢包只会导致部分数据缺失(如视频卡顿、语音中断),不会破坏已接收数据的正确性,数据内容错误,已被UDP校验和过滤。3、可靠的UDP协议设计从前面UDP的结构大家就知道,其实它挺简洁的,UDP既然不解决这些问题,那就应用层去弥补,说实在有更多的灵活度。UDP应用层需解决乱序、完整性、丢包三个问题就本上就比较靠谱了。比如传输协议中增加一些字段:| 序列号 (4B) | 时间戳 (4B) | 载荷长度 (2B) | 载荷数据 (N B) | 为了处理乱序、重复、丢包检测等问题。对于一些关键数据添加ACK确认与重传,一些实时数据丢包了也没关系,就不需要应答了~最好是避免IP分片,控制数据包大小≤1472字节(假设MTU=1500),大数据传输时,主动在应用层分包并添加序号。如果你加快异常时的收发效率,可以采用前向纠错(FEC),通过发送冗余数据包,允许接收端通过算法恢复部分丢失数据(如RTP协议中的FEC机制)。
最后一个bug
2 8 立创开发板
嵌入式C语言编程十诫
跟大家介绍一下C编程十诫,这10条建议是作者Henry Spencer提出来的,bug菌也是在不经意间看到了这10条建议的英文文档《The Ten Commandments for C Programmers》,今天翻译了一下,并做了批注,供大家学习参考:1、你应该经常运行 lint 并仔细研究它的声明,因为它的感知和判断确实经常超过你的判断力。(bug菌:尽量能够使用一些静态检测工具,这样通过自动化测试代码能够发现非常多潜在的错误,并且能极大地减轻测试人员的压力,减少软件项目的出错成本,重要的是这些工具的能力可能是我们平常达不到的。)2、你不能跟随 NULL 指针,因为混乱和疯狂在它的尽头等待着你。(bug菌:不要使用空指针NULL,他会使得比较混乱。)3、你应该将所有函数参数转换为预期的类型,即使它们已经不是那种类型,即使你确信这是不必要的,以免它们在你最不期望的时候对你进行残酷的报复。(bug菌:函数传参有时候一些编译器并不要求完全匹配仍然可以编译通过,不过最好是养成传递给函数的实参与形参保持一致,以免出现移植等问题。)4、如果你的头文件没有声明你的库函数的返回类型,你应该非常小心地自己声明它们,以免严重的伤害降临到你的程序上。(bug菌:注意函数声明的返回类型,以免引入bug。)5、你应该检查所有字符串(实际上是所有数组)的数组边界,因为肯定在你键入的地方,“foo”有一天有人会键入“supercalifragilisticexpialidocious”。(bug菌:数组越界一定要注意一下,C语言编程比较容易犯错,特别是初学者,加上一些编程中的防御性设计。)6、如果一个函数被声明为在遇到困难时返回错误代码,你应该检查那个代码,是的,即使检查是你的代码大小的三倍并且会让你的打字手指疼痛,因为如果你认为“这不可能发生在我身上”,众神一定会惩罚你的傲慢。(bug菌:函数异常返回需要认真对待,并且对于异常需要进行相应的处理,比如动态内存的释放等等。)7、你应该研究你的库,努力不无缘无故地重新发明它们,这样你的代码就可以简短易读,让你的日子愉快而富有成效。(bug菌:最好是自己整理一些可移植的库代码,以后直接拿来用提高效率。)8、即使你不喜欢,你也应该通过使用 One True, Brace Style 让你的同伴清楚你的程序的目的和结构,因为你的创造力更好地用于解决问题,而不是创造美丽的新障碍来理解。(bug菌:代码的结构和风格比较建议大家遵循 One True, Brace Style,通常也叫(1TBS),以免由于风格和代码结构混乱引入一些问题。)9、你的外部标识符在前六个字符中应该是独一无二的,尽管这种严厉的纪律会让一些人感到厌烦,而且它的必要性在你面前似乎永无止境地延伸,以免在你希望让你的程序在旧系统上运行的那个决定性的日子里撕破你的头发并发疯。(bug菌:代码的结构和风格,比较建议大家遵循 One True, Brace Style,通常也叫(1TBS),以免由于风格和代码结构带来一些混乱。)10、你应摒弃、放弃声称“整个世界都是虚无缥缈”的邪恶异端,并与坚持这种野蛮信仰的愚昧异教徒没有任何往来,你写程序的日子可能会很长,即使你当前机器的日子很短。
最后一个bug
2 10 立创开发板
嵌入式C代码调试利器---backtrace
1、backtrace基本原理backtrace英译为回溯的意思,这听起来有点专业了,其实大部分搞嵌入式的朋友都有听说过函数调用栈callstack。而backtrace说白了就是我们呈现函数调用关系的一项功能。所以backtrace调试功能的实现原理基于函数调用栈的概念。那什么是函数调用栈呢?函数调用栈是一个记录程序中函数调用关系的数据结构,它在程序运行时动态生成和维护。当程序执行函数调用时,它将当前函数的返回地址和一些其他信息压入堆栈中,并跳转到被调用的函数执行。当被调用函数执行完毕后,它将返回地址弹出堆栈,并跳回到调用函数继续执行。backtrace调试功能的实现原理就是利用函数调用栈中的信息来追踪程序执行的路径和调用关系。当程序出现错误或崩溃时,backtrace可以通过分析函数调用栈信息来确定出错的位置和原因。在Linux系统中,backtrace通常是通过使用调试器比如我们常用的gdb来实现的。调试器会在程序执行时,动态地获取函数调用栈信息,并将其保存在调试器的内部数据结构中。当程序出现错误或崩溃时,调试器就可以利用保存的函数调用栈信息来进行backtrace操作。2、backtrace功能而对于backtrace这个功能在不同的平台和开发环境中的使用是不同的.比如在我们平时的linux环境中:可以使用glibc提供的backtrace()函数实现backtrace功能。该函数通过解析函数调用栈信息获取函数名、参数和返回地址等信息,并将其打印到标准输出或指定的文件中。此外,还可以使用gdb或libunwind库来实现backtrace功能。gdb是一个强大的调试器,可以实时追踪程序的执行,获取程序的调用栈信息,并提供各种调试工具和命令。而其中的libunwind则是一个开源的C/C++库,也可以用于在运行时获取当前程序的调用栈信息,并且在不同的平台和架构上运行,并提供了简单易用的API接口,同样也是非常方便的。3、glibc下的backtrace功能使用glibc提供了backtrace函数,可以用来获取当前程序的调用栈信息,使用方法如下:包含头文件:#include [removed] 定义一个数组,用于存储回溯信息:#define BT_BUF_SIZE 100 void *bt_buffer[BT_BUF_SIZE]; 该数组用于存储backtrace信息,数组大小可以根据需要进行调整。3. 调用backtrace函数:int bt_size = backtrace(bt_buffer, BT_BUF_SIZE); 该函数会获取当前程序的调用栈信息,并将其存储在bt_buffer数组中。bt_size表示实际获取到的调用栈信息的条数,该值不会超过BT_BUF_SIZE。4. 使用backtrace_symbols函数将backtrace信息转换成字符串:char **bt_strings = backtrace_symbols(bt_buffer, bt_size); 该函数将backtrace信息转换成字符串数组,每个字符串表示一个调用栈信息。bt_strings指向字符串数组的首地址,需要在使用完毕后手动释放内存。5. 打印回溯信息:for (int i = 0; i < bt_size; i++) {     printf("%!s(MISSING)\n", bt_strings[i]); } 该代码会将回溯信息打印到标准输出中,可以根据需要进行调整。完整的使用示例代码如下:#include [removed]#include [removed] #include [removed]#define BT_BUF_SIZE 100 void print_backtrace() {     void *bt_buffer[BT_BUF_SIZE];     int bt_size = backtrace(bt_buffer, BT_BUF_SIZE);     char **bt_strings = backtrace_symbols(bt_buffer, bt_size);     printf("backtrace:\n");     for (int i = 0; i [removed]
最后一个bug
2 9 立创开发板