发帖数

50

原创数

50

关注者

12

阅读数

8965

点赞数

4

蔡琰

  • 程序的调试和宏使用的技巧

    在程序的开发过程中,调试语句是程序开发的一种主要的辅助手段。C语言主要的调试语句使用的是printf,它的定位是系统的标准输出。在嵌入式系统中,printf()的输出可能是屏幕、也可能是串口。


    #字符串转化操作符

    在编译系统中,可以使用#将当前的内容转换成字符串,例如:

    #define dprint(expr)  printf(<main>%s = %d n,#expr,expr)


    ①在程序中可以使用如下的方式调用:

    1.jpg

    在以上的例子中,使用#expr表示根据宏中的参数(即表达式的内容),生成一个字符串。因此,#expr代表将dprintexpr)括号中的内容生成一个字符串。一般来说,宏中的参数将作为一个变量被引用,而增加了#修饰之后的表达式,即代表了将宏中的参数名称直接转换成字符串。

    上述过程同样是由编译器产生的,编译器在编译源文件的时候,如果遇到了类似的宏(示例中的dprint)会自动根据程序中表达式的内容,生成一个字符串的宏(示例中的#expr)。这样宏同样可以在程序中表示一个字符串。


    ②进一步,在程序中可以按照如下的形式调用以上宏:

    2.jpg

    从运行结果的第一行可以看到,编译器的生成字符串的时候,不会照搬宏参数中的所有内容,注释类的内容是不会被放入字符串的宏,这也是因为去注释是编译器预处理阶段的内容,也就是说在实际的编译过程之前,程序中的注释已经被去掉。从运行结果的第二行看出,由于a不是整数,而是字符串的指针,因此打印出a的值实际是变量a的地址,而字符串的内容依然是a。从运行结果的第三行看出,对于直接写入程序的数值(立即数),编译器也可以将它的内容转换成字符串。


    这种方式的优点是可以用统一的方法打印表达式的内容,在程序的调试过程中可以方便直观地看到转换成字符串之后的表达式。具体的表达式的内容是什么,是由编译器“自动”写入程序中的,这样使用相同的宏打印所有表达式的字符串。


    由于#expr本质就是一个表示字符串的宏,因此在程序中也可以不使用%打印它的内容,而是可以将其直接和其他的字符串连接。上面的宏可以等价为以下的形式:

    #define dprint (expr)  printf(<main> #expr =%d n,expr)

    注意:#C语言预处理阶段的字符串转化操作符,可以将宏中的内容转换成字符串。


    ##:连接操作符

    在编译系统中,##C语言中的连接操作符,可以在编译的预处理阶段实现字符串连接的操作。

    以下的程序是一个使用##的示例:

    #define test(x) test ## x

    void test1(int a)      void test2(char *s)

    {         {

    printf(Test 1 interger: %d n,a);   printf(Test 2 String : %s n,s);

    }         }

    3.jpg

    在上面这个程序,test(x)宏被定义为test##x,它表示test字符串和x字符串的连接,因此test(1)将被预处理器为:test1,test(2)将被预处理器处理为:test2。预处理器仅仅是转换字符串而已,所以上面的test1test2刚好转换成两个函数的名称。


    条件编译调试语句


    在嵌入式系统的调试中,调试语句可以在程序运行的过程中输出程序的运行状态。然而调试语句的调用是有开销的,在最终发布版的程序中,调试语句都是应该去掉的。去掉调试语句最简单的方式将其注释掉,但是主要就需要维护两种源程序:一种是带有调试语句的调试版程序,另一种是不带有调试语句的发布版程序。这显然不是一种很好的方式,理想的方式是只有一套源程序,根据不同的条件编译选项,编译出不同的调试版和发布版的程序。


    在实现的过程中,可以使用一个调试宏来控制调试语句的开关,如下所示:

    #ifndef USE_DEBUG

    #define DEBUG_OUT(fmt,args...) printf(File:%s Function:%s Line:%dfmt,_FILE_,_FUNCTION_,_LINE_,

    ##args)

    #else

    #define DEBUG_OUT(fmt,args...)

    #endif

    在上面的程序中,宏USE_DEBUG用来控制调试语句的开关,当USE_DEBUG被定义的时候,将调试语句DEBUG_OUT定义成上面部分的形式,当没有定义的时候,宏定义为空,在这种情况下,即使程序中写很多DEBUG_OUT,编译器也会将其处理为没有任何语句。

    注意:一条语句太长换行需要在每行的结尾使用,表示下一行的内容是和上面的连续的。


    使用do...while的宏定义


    使用宏定义可以将一些较为短小的功能封装,方便使用。宏的形式和函数类似,但是可以节省函数跳转的开销。如何将一个语句封装成一个宏,在程序中常常使用do...while(0)的形式,例如,对一个简单打印的语句的宏封装如下所示:

    #define HELLO(str)  do{ printf(hello:%sn,str); }while(0)

    在上面这个语句中,将实际执行的功能封装在一个do...while(0)循环内。事实上,do...while(0)由于条件不成立,因此循环体之间的语句只会执行一次。然而,这样做的好处是就是可以让do...while(0)之中的语句像函数一样使用,而不必担心编译器发生错误。


    如果直接把后面语句放入宏使用,不用do...while,那么宏在一般顺序执行语句中使用没有问题,如果使用在if...else中,都会发生错误编译。事实上,一般的语句中多一个分号,只相当于多了一条空语句,没有影响。这里却是在if语句后面多出一个分号,它们代表if语句的结束。因此,后面的else就会被视为一条新的语句(相当于前面没有if只有else,这就会发生编译错误。


    而如果使用do...while(0)的形式就没有以上的问题,而且一般的C语言编译器都会对do...while(0)进行优化,使其和一般的一条函数等价,在其中可以含有任意条语句。

    收藏 0 回复 0 浏览 113
  • 常见的内存错误及对策

    对于用CC++除了考虑上层应用,还需要考虑底层的内存管理,或者说内存泄漏的问题。


    1、指针没有指向一块合法的内存

    定义了指针变量,但是没有为指针分配内存,即指针没有指向一块合法的内存。

    ①结构体成员指针未初始化

    定义一个结构体变量,但是结构体内部定义了指针成员,往往应用结构体变量的时候如果不给这 个成员指向一个合法的地址只是给成员分配了字节数,应用的时候对应内存的区域指针成员是无权 访问的,所以需要对结构体分配内存,结构体成员中指针变量也要分配内存,否则访问不到有效地 址。

    ②没有为结构体指针分配足够的内存

    分配内存的时候,分配的内存大小不合适,比如开辟内存空间时sizeof(struct stu)误写为sizeof(struct stu *),书写错误会导致开辟空间不正确。

    ③函数的入口校验

    不管什么时候,我们使用指针之前一定要确保指针是有效的。一般在函数入口处使用         assert NULL !=p)对参数进行校验。在非参数的地方使用if(NULL != p)来校验。但这都有一个要求,也就 p在定义的同时被初始化为NULL,如果没有被初始化为NULL,那么校验也起不了作用,没有被 初始化的指针变量,内部是一个非NULL的乱码

    assert是一个宏,而不是函数,包含在assert.h头文件中。如果其后面括号里的值为假,则程序终 止运行,并提示出错;如果后面括号里的值为真,则继续运行后面的代码。


    2、为指针分配的内存太小或内存访问越界

    为指针分配了内存,但是内存大小不够,导致出现越界错误。

    通常这种问题都会出现在我们容易忽略的字符串常量中,往往会忘记结束标志”,在开辟内存的时候sizeof计算中需要把结束标志加上,还有计算分配空间大小的时候最好用sizeof来操作,移植性也好。

    再有内存分配成功,且已经初始化,但是操作越过了内存的边界。

    这种错误经常是由于操作数组或者指针时出现“多1”或“少1”而出现的。比如:

    forint i = 0; i < = 10;i++

    一般for循环的循环变量一定要使用半开半闭的区间,而且如果不是特殊情况,循环变量尽量从0开始。


    3、内存泄漏

    内存泄漏几乎是很难避免的,不管是老手还是新手,都存在这个问题。甚至包括WindowsLinux这类软件,都或多或少有内存泄漏。也许对于一般的应用软件来说,这个问题似乎不是那么突出,重启一下也不会造成太大损失。但是如果你开发的是嵌入式系统软件,比如汽车制动系统、心跳起搏器等对安全要求非常高的系统,你总不能让心脏起搏器重启吧。

    会产生内存泄漏的内存就是堆上的内存,也就是由malloc系列函数或new操作符分配的内存。如果用完没有及时freedelete,这块内存就无法释放,直到整个程序终止。

    malloc是一个函数,专门用来从堆上分配内存,malloc函数的返回值是一个void类型的指针,参数是申请分配的内存大小,内存分配成功后,malloc函数返回这块内存的首地址,需要一个指针来接收这个地址,那么申请了就能成功吗?不一定的,如果所申请的内存块大于目前堆上剩余内存块,则内存分配失败,函数返回NULL。我们需要知道内存申请分配是连续的一块内存,如果剩余内存不够返回就是空,那么我们可以用校验的方式(if(NULL != p))来验证是否分配成功。当然也可以申请0字节内存,返回不是NULL,是一个正常的内存地址,但是你却无法使用这块大小为0的内存,这就好比尺子上的某个刻度,刻度本身并没有长度,只有某两个刻度一起才能量出长度。

    再来说下内存释放,既然有分配,就有释放,不然的话,有限的内存总会用光,而没有释放的内存却在空闲。与malloc对应的就是free函数了。free函数只有一个参数,就是所要释放的内存块的首地址,比如:free(p);这个函数主要是让指针变量和这块内存脱离关系,从此这个指针变量和分配过的这快内存没有关系了,那么指针变量p本身保存的地址并没有改变,但是它对这个地址处的那块内存却已经没有所有权了。释放后,那块内存里面保存的值并没有改变,只是再也没有办法使用了。

    需要记住一点mallocfree是匹配使用的,如果多写两次malloc或者free都会出错。

    再有内存释放之后需要给指针变量赋值为NULL,如果没有把指针置NULL,这个指针就成为了野指针,这是很危险的,也是经常出错的地方。


    4、内存已经被释放了,但是继续通过指针来使用

    一般会有以下三种情况:

    ①就是上面所说的,free(p)之后,继续通过p指针来访问内存,解决的办法就是给PNULL

    ②函数返回栈内存,这是初学者最容易犯的错误。比如在函数内部定义了一个数组,却用return 语句返回指向该数组的指针。解决的办法就是弄明白栈上变量的声明周期。

    ③内存使用太复杂,弄不清到底哪块内存被释放,哪块没有释放。解决的办法是重新设计程序,改善对象之间的调用关系。

    写程序就是要多练习,多调试代码,同时多总结经验,少走弯路。共勉。


    收藏 0 回复 0 浏览 100
  • MDK中Buil/ReBuild背后你不知道的故事

    在程序开发过程中,大家都会去点击编译按钮,直接开始仿真调试,基本上不怎么关注编译和链接的过程,因为我们使用的工具一般都是厂家做好的集成开发环境(IDE),比如MDK、IAR等。IDE通常将编译和链接合并到一起,虽然 IDE 提供的默认配置、编译和链接参数对于大部分应用程序来说已经足够使用了,但是作为学习,我们可以弄清楚从源代码生成可执行文件的原理。


        事实上,从源代码生成可执行文件可以分为四个步骤,分别是预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)和链接(Linking)。下图是生成可执行文件的过程:

    1.jpg     

    预处理(Preprocessing)过程主要是处理程序中以#开头的命令,比如 #include、#define等。预处理的规则一般如下:

    1.将程序中所有的#define宏定义进行替换。

    2.处理程序中所有条件编译命令,比如 #if、#ifdef、#elif、#else、#endif 等。

    3.处理#include命令,会将被包含的头文件的内容插入到该预处理命令所在的位置,需要注意的是,这个过程是递归进行的,也就是说被包含的头文件中还可能会包含其他的头文件。

    4.删除程序中的注释。

    5.添加行号和文件名标识,便于在调试和出错时给出具体的代码位置。

    6.保留程序中的#pragma命令,因为编译器需要使用它们。


         预处理的结果是生成.i文件。.i文件也包含C语言代码的源文件,只不过所有的宏已经被展开,所有包含的文件已经被插入到当前文件中。当你无法判断宏定义是否正确,或者文件包含是否有效时,可以查看.i文件来确定问题。


        编译(Compilation)就是把预处理完的文件进行一些列的词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件(.txt文件)。编译是整个程序构建的核心部分,也是最复杂的部分之一,涉及到的算法较多。


    在MDK中可以按照以下步骤生产预处理文件和编译文件。勾选之后,再重新Build或者ReBuild就可以了。


             2.jpg


    汇编(Assembly)的过程就是将上一步生成的汇编指令转换成可以执行的机器指令。汇编过程相对于编译来说比较简单,只是根据汇编语句和机器指令的对照表翻译就可以了。汇编的结果是产生目标文件,即为.o文件。


    目标文件已经是二进制文件,与可执行文件的形式类似,只是有些函数和全局变量都是分散的,地址还没有找到,程序不能执行。链接(Linking)的作用就是找到这些目标地址,将所有的目标文件组织成一个可以执行的二进制文件。


       以上就是你点击了MDK中的Buil/ReBuild之后,被雪藏的背后的过程,你清楚了吗?

    收藏 0 回复 0 浏览 70
  • 你知道I2C为什么要接上拉吗?

    实际工程项目中,有很多地方都会用到I2C总线通信,比如说24C02存储、传感器接口等,能使用I2C这项技能,就成为了我们工程师日常必备的武器,那么我们就来详细的说说I2C。


    1.jpg

                


    I2C是一种简单的双向二线制同步串行总线。只需要两根线即可在连接于总线上的器件之间传送信息,一根SCL时钟线,一根就是SDA数据需要注意的是SDA它是一个双向传输的线,主机向从机发送信号通过SDA把数据送出去,从机向主机发送信号也是从SDA线把数据送出来


        I2C也可以一个一,也可以一对多,每个连接到总线的外围设备都有一个独立的地址,主机可以通过该地址来访问不同设备。主机可通过SDA线发送设备地址查找从机。


        因为I2C 通信IO口输出结构都是配置为漏极开路或集电极开路输出。所以时钟线和数据线必须外部都接上拉电阻当一对多输出的时候,很多GPIO口会连接在同一根线上可能会存在某个GPIO输出高电平,另一个GPIO输出低电平的情况如果使用推挽输出你会发现这个GPIO的VCC和另一个GPIO的GND接在了一起也就是短路了如果换成开漏输出VCC和GND之间多了个电阻这样电路就是安全的


    上拉电阻一般取值在1K-10K之间上拉电阻的取值和通信速率会有一定的关系,如果上拉电阻的取值太大,那么信号的上升沿下降沿就会很缓会影响通信速率这个具体的看应用的通信速率去测试波形如果取的太小了,功耗很大取的太大了,信号的上升沿就会慢,影响通信速率(如下图的1 2表示)。

     2.jpg

    一般情况下I2C标准模式最高传输速率100Kbit/s,高速模式最高传输速率400Kbit当然也有用软件模拟I2C的时序来进行通信的,这个速率就需要自己去掌控测试了说白了就是我们对着时序图,一个同样的波形出来只要遵守同样的约定,按照时序图对应管脚拉高拉低就可以读写数据了


    在标准协议中区分启动、停止、应答、等动作,下面我们来一起学习一下I2C的标准协议


    3.jpg


       检测到SDA线上为下降沿,SLC线上为高电平,就表示收到了启动信号,检测到SDA线上为上升沿,SLC线上为高电平,就表示收到了停止信号。
            4.jpg
       

    收到启动信号之后就可以开始数据传输收到停止信号表示帧数据传输完毕。传输过程中,时钟线SCL为高电平的时候,数据线SDA必须保持稳定,不能改变电平状态,这样才能有效传输在时钟线SCL为低电平的时候,数据线SDA可以改变状态即:SCL为高电平的时候,数据线SDA如果为高,那么传送的就是1,如果SDA为低,那么传送的就是0SCL为低电平的时候,你可以改变数据可以把数据变为0/1图中红框交叉就表示数据的改变1变为0 从0变为1所以有了一个上升沿或者一个下降沿)。


    SCL的第9个时钟位,对应的SDA数据为应答数据应答数据是有方向的,可以是主设备应答,可以是从设备应答,应答不是固定的,可以是低电平为应答,也可以是高电平应答高应答或者低应答是由通信协议决定的,只要约定一个信号作为应答就可以了主机给从机发送数据,从机接到了应答主机,从机给主机发送数据,主机接到了,主机就要应答这样就形成了一个闭环

     

    以上就是一个标准的IIC通信标准规范,你清楚了吗?


    收藏 0 回复 0 浏览 257
  • 红外遥控原来这么简单!

    大家好!我是张飞实战电子蔡琰老师!今天给大家分享红外遥控接收。

    平时我们经常会用到遥控器,那么现在遥控器也分很多种类,有使用红外通信的,也有使用蓝牙,无线的等,今天我们来一起解码一下红外的工作原理。

    大家看现在图中的是2个红外对管,左边是发射端,右边是接收端,

    image.png

    遥控器上有一个红外发射二极管,发射红外数据信息,电视机上有一个红外接收管,接收红外信息,那么到底是怎么把数据从二极管中发送出去的呢?

    image.png

    如上图,遥控器发送之前要先进行编码调制,然后进行信号放大发射,接收设备需要先对这个信号进行解调,解调之后的信号送给单片机,单片机进行解码(分析是什么数据)。

    调制过程就是需要加上载波信号,中间加载了一个载波信号,发送的数据就是通过载波信号送出去的,对应的接收信号就需要对收到的载波信号进行解调处理了,即信号还原。

    image.png


    image.png

    一般情况下接收头,只能解调固定的一种载波频率信号,那遥控器的发送信号的载波频率要与接收头所用的频率一致,否则是没办法正确接收的。自然界中存在红外光,进行调制主要是为了避免一些干扰,以防止传输出错。下面我们一起来看看遥控器传输的协议编码规则。

    遥控器信号开始的地方有一段特殊长度的信号,这个我们叫它是引导码,引导码是9ms高电平+4.5ms的低电平,单片机只有结束到了正确的引导码,才可以开始接收后续的数据。

    image.png

    image.png

    image.png

    我们知道有效数据要么是0,要么是1,0或者1都是由一个固定的高电平+低电平组成,数据1: 0.56ms高电平+1.69ms低电平组成,数据0: 0.56ms高电平+0.56ms低电平组成,也就是说收到这样的一个高电平+低电平的数据就是认为收到有效数据了,再根据判断时间来区分是0还是1。通过分析出来0 1,再把这些0 1组合成一个有用的数据,然后进行处理执行动作,比如切换频道,关机、开机等。这样就是一个完成的遥控器发送,接收原理了。

    image.png

    上图是我们实测的一个遥控器解调后的波形中,你能分析出图中传输的数据吗?

    收藏 0 回复 0 浏览 144
×
蔡琰