发新帖我要提问
12
返回列表
[应用相关]

编写优质嵌入式C程序

[复制链接]
楼主: 八层楼
手机看帖
扫描二维码
随时随地手机跟帖
八层楼|  楼主 | 2018-9-15 07:58 | 显示全部楼层
5.2.1简单易用的调试函数

      1)       使用库函数printf。以MDK为例,方法如下:

             I>初始化串口

             II>重构fputc函数,printf函数会调用fputc函数执行底层串口的数据发送。



  • 1.        /**



  • 2.          * @brief  将C库中的printf函数重定向到指定的串口.



  • 3.          * @param  ch:要发送的字符



  • 4.          * @param  f :文件指针



  • 5.          */  



  • 6.        int fputc(int ch, FILE *f)  



  • 7.        {  



  • 8.          



  • 9.            /*这里是一个跟硬件相关函数,将一个字符写到UART */  



  • 10.            //举例:USART_SendData(UART_COM1, (uint8_t) ch);  



  • 11.             



  • 12.            return ch;  



  • 13.        }


           III> 在Options for Targer窗口,Targer标签栏下,勾选Use MicroLIB前的复选框以便避免使用半主机功能。(注:标准C库printf函数默认开启半主机功能,如果非要使用标准C库,请自行查阅资料)

      2)       构建自己的调试函数

      使用库函数比较方便,但也少了一些灵活性,不利于随心所欲的定制输出格式。自己编写类似printf函数则会更灵活一些,而且不依赖任何编译器。下面给出一个完整的类printf函数实现,该函数支持有限的格式参数,使用方法与库函数一致。同库函数类似,该也需要提供一个底层串口发送函数(原型为:int32_t UARTwrite(const uint8_t *pcBuf, uint32_t ulLen)),用来发送指定数目的字符,并返回最终发送的字符个数。



  • 1.        #include <stdarg.h>               /*支持函数接收不定量参数*/  



  • 2.          



  • 3.        const char * const g_pcHex = "0123456789abcdef";  



  • 4.          



  • 5.        /**



  • 6.        * 简介:   一个简单的printf函数,支持\%c, \%d, \%p, \%s, \%u,\%x, and \%X.



  • 7.        */  



  • 8.        void UARTprintf(const uint8_t *pcString, ...)  



  • 9.        {  



  • 10.            uint32_t ulIdx;  



  • 11.            uint32_t ulValue;       //保存从不定量参数堆栈中取出的数值型变量  



  • 12.            uint32_t ulPos, ulCount;  



  • 13.            uint32_t ulBase;        //保存进制基数,如十进制则为10,十六进制数则为16  



  • 14.            uint32_t ulNeg;         //为1表示从变量为负数  



  • 15.            uint8_t *pcStr;         //保存从不定量参数堆栈中取出的字符型变量  



  • 16.            uint8_t pcBuf[32];      //保存数值型变量字符化后的字符  



  • 17.            uint8_t cFill;          //'%08x'->不足8个字符用'0'填充,cFill='0';   



  • 18.                                    //'%8x '->不足8个字符用空格填充,cFill=' '  



  • 19.            va_list vaArgP;  



  • 20.          



  • 21.            va_start(vaArgP, pcString);  



  • 22.            while(*pcString)  



  • 23.            {  



  • 24.                // 首先搜寻非%核字符串结束字符  



  • 25.                for(ulIdx = 0; (pcString[ulIdx] != '%') && (pcString[ulIdx] != '\0'); ulIdx++)  



  • 26.                { }  



  • 27.                UARTwrite(pcString, ulIdx);  



  • 28.          



  • 29.                pcString += ulIdx;  



  • 30.                if(*pcString == '%')  



  • 31.                {  



  • 32.                    pcString++;  



  • 33.          



  • 34.                    ulCount = 0;  



  • 35.                    cFill = ' ';  



  • 36.        again:  



  • 37.                    switch(*pcString++)  



  • 38.                    {  



  • 39.                        case '0': case '1': case '2': case '3': case '4':  



  • 40.                        case '5': case '6': case '7': case '8': case '9':  



  • 41.                        {  



  • 42.                            // 如果第一个数字为0, 则使用0做填充,则用空格填充)  



  • 43.                            if((pcString[-1] == '0') && (ulCount == 0))  



  • 44.                            {  



  • 45.                                cFill = '0';  



  • 46.                            }  



  • 47.                            ulCount *= 10;  



  • 48.                            ulCount += pcString[-1] - '0';  



  • 49.                            goto again;  



  • 50.                        }  



  • 51.                        case 'c':         



  • 52.                        {  



  • 53.                            ulValue = va_arg(vaArgP, unsigned long);  



  • 54.                            UARTwrite((unsigned char *)&ulValue, 1);  



  • 55.                            break;  



  • 56.                        }  



  • 57.                        case 'd':     



  • 58.                        {  



  • 59.                            ulValue = va_arg(vaArgP, unsigned long);  



  • 60.                            ulPos = 0;  



  • 61.                              



  • 62.                            if((long)ulValue < 0)  



  • 63.                            {  



  • 64.                                ulValue = -(long)ulValue;  



  • 65.                                ulNeg = 1;  



  • 66.                            }  



  • 67.                            else  



  • 68.                            {  



  • 69.                                ulNeg = 0;  



  • 70.                            }  



  • 71.                            ulBase = 10;         



  • 72.                            goto convert;  



  • 73.                        }  



  • 74.                        case 's':  



  • 75.                        {  



  • 76.                            pcStr = va_arg(vaArgP, unsigned char *);  



  • 77.          



  • 78.                            for(ulIdx = 0; pcStr[ulIdx] != '\0'; ulIdx++)  



  • 79.                            {  



  • 80.                            }  



  • 81.                            UARTwrite(pcStr, ulIdx);  



  • 82.          



  • 83.                            if(ulCount > ulIdx)  



  • 84.                            {  



  • 85.                                ulCount -= ulIdx;  



  • 86.                                while(ulCount--)  



  • 87.                                {  



  • 88.                                    UARTwrite(" ", 1);  



  • 89.                                }  



  • 90.                            }  



  • 91.                            break;  



  • 92.                        }  



  • 93.                        case 'u':  



  • 94.                        {  



  • 95.                            ulValue = va_arg(vaArgP, unsigned long);  



  • 96.                            ulPos = 0;  



  • 97.                            ulBase = 10;  



  • 98.                            ulNeg = 0;  



  • 99.                            goto convert;  



  • 100.                        }  



  • 101.                        case 'x': case 'X': case 'p':  



  • 102.                        {  



  • 103.                            ulValue = va_arg(vaArgP, unsigned long);  



  • 104.                            ulPos = 0;  



  • 105.                            ulBase = 16;  



  • 106.                            ulNeg = 0;  



  • 107.                 convert:   //将数值转换成字符  



  • 108.                            for(ulIdx = 1; (((ulIdx * ulBase) <= ulValue) &&(((ulIdx * ulBase) / ulBase) == ulIdx)); ulIdx *= ulBase, ulCount--)      



  • 109.                            { }  



  • 110.                            if(ulNeg)  



  • 111.                            {  



  • 112.                                ulCount--;                        



  • 113.                            }  



  • 114.                            if(ulNeg && (cFill == '0'))  



  • 115.                            {  



  • 116.                                pcBuf[ulPos++] = '-';  



  • 117.                                ulNeg = 0;  



  • 118.                            }  



  • 119.                            if((ulCount > 1) && (ulCount < 16))  



  • 120.                            {  



  • 121.                                for(ulCount--; ulCount; ulCount--)  



  • 122.                                {  



  • 123.                                    pcBuf[ulPos++] = cFill;  



  • 124.                                }  



  • 125.                            }  



  • 126.          



  • 127.                            if(ulNeg)  



  • 128.                            {  



  • 129.                                pcBuf[ulPos++] = '-';  



  • 130.                            }  



  • 131.          



  • 132.                            for(; ulIdx; ulIdx /= ulBase)  



  • 133.                            {  



  • 134.                                pcBuf[ulPos++] = g_pcHex[(ulValue / ulIdx) % ulBase];



  • 135.                            }  



  • 136.                            UARTwrite(pcBuf, ulPos);  



  • 137.                            break;  



  • 138.                        }  



  • 139.                        case '%':  



  • 140.                        {  



  • 141.                            UARTwrite(pcString - 1, 1);                    



  • 142.                            break;  



  • 143.                        }  



  • 144.                        default:  



  • 145.                        {                     



  • 146.                            UARTwrite("ERROR", 5);                    



  • 147.                            break;  



  • 148.                        }  



  • 149.                    }  



  • 150.                }  



  • 151.            }  



  • 152.            //可变参数处理结束  



  • 153.            va_end(vaArgP);  



  • 154.        }



使用特权

评论回复
八层楼|  楼主 | 2018-9-15 07:58 | 显示全部楼层
5.2.2对调试函数进一步封装

      上文说到,我们增加的调试语句应能很方便的从最终发行版中去掉,因此我们不能直接调用printf或者自定义的UARTprintf函数,需要将这些调试函数做一层封装,以便随时从代码中去除这些调试语句。参考方法如下:



  • 1.        #ifdef MY_DEBUG  



  • 2.        #define MY_DEBUGF(message) do { \  



  • 3.                                          {UARTprintf message;} \  



  • 4.                                       } while(0)  



  • 5.        #else   



  • 6.        #define MY_DEBUGF(message)   



  • 7.        #endif /* PLC_DEBUG */


在我们编码测试期间,定义宏MY_DEBUG,并使用宏MY_DEBUGF(注意比前面那个宏多了一个‘F’)输出调试信息。经过预处理后,宏MY_DEBUGF(message)会被UARTprintf message代替,从而实现了调试信息的输出;当正式发布时,只需要将宏MY_DEBUG注释掉,经过预处理后,所有MY_DEBUGF(message)语句都会被空格代替,而从将调试信息从代码中去除掉。


使用特权

评论回复
八层楼|  楼主 | 2018-9-15 07:59 | 显示全部楼层
6.编程思想6.1编程风格

       《计算机程序结构与说明》一书在开篇写到:程序写出来是给人看的,附带能在机器上运行。

6.1.1 整洁的样式

      使用什么样的编码样式一直都颇具争议性的,比如缩进和大括号的位置。因为编码的样式也会影响程序的可读性,面对一个乱放括号、对齐都不一致的源码,我们很难提起阅读它的兴趣。我们总要看别人的程序,如果彼此编码样式相近,读起源码来会觉得比较舒适。但是编码风格的问题是主观的,永远不可能在编码风格上达成统一意见。因此只要你的编码样式整洁、结构清晰就足够了。除此之外,对编码样式再没有其它要求。

      提出匈牙利命名法的程序员、前微软首席架构师Charles Simonyi说:我觉得代码清单带给人的愉快同整洁的家差不多。你一眼就能分辨出家里是杂乱无章还是整洁如新。这也许意义不大。因为光是房子整洁说明不了什么,它仍可能藏污纳垢!但是第一印象很重要,它至少反映了程序的某些方面。我敢打赌,我在3米开外就能看出程序拙劣与否。我也许没法保证它很不错,但如果从3米外看起来就很糟,我敢保证这程序写得不用心。如果写得不用心,那它在逻辑上也许就不会优美。

6.1.2清晰的命名

      变量、函数、宏等等都需要命名,清晰的命名是优秀代码的特点之一。命名的要点之一是名称应能清晰的描述这个对象,以至于一个初级程序员也能不费力的读懂你的代码逻辑。我们写的代码主要给谁看是需要思考的:给自己、给编译器还是给别人看?我觉得代码最主要的是给别人看,其次是给自己看。如果没有一个清晰的命名,别人在维护你的程序时很难在整个全貌上看清代码,因为要记住十多个以上的糟糕命名的变量是件非常困难的事;而且一段时间之后你回过头来看自己的代码,很有可能不记得那些糟糕命名的变量是什么意思。

      为对象起一个清晰的名字并不是简单的事情。首先能认识到名称的重要性需要有一个过程,这也许跟谭式C程序教材被大学广泛使用有关:满书的a、b、c、x、y、z变量名是很难在关键的初学阶段给人传达优秀编程思想的;其次如何恰当的为对象命名也很有挑战性,要准确、无歧义、不罗嗦,要对英文有一定水平,所有这些都要满足时,就会变得很困难;此外,命名还需要考虑整体一致性,在同一个项目中要有统一的风格,**这种风格也并不容易。

       关于如何命名,Charles Simonyi说:面对一个具备某些属性的结构,不要随随便便地取个名字,然后让所有人去琢磨名字和属性之间有什么关联,你应该把属性本身,用作结构的名字。

6.1.3恰当的注释

       注释向来也是争议之一,不加注释和过多的注释我都是反对的。不加注释的代码显然是很糟糕的,但过多的注释也会妨碍程序的可读性,由于注释可能存在的歧义,有可能会误解程序真实意图,此外,过多的注释会增加程序员不必要的时间。如果你的编码样式整洁、命名又很清晰,那么,你的代码可读性不会差到哪去,而注释的本意就是为了便于理解程序。

       这里建议使用良好的编码样式和清晰的命名来减少注释,对模块、函数、变量、数据结构、算法和关键代码做注释,应重视注释的质量而不是数量。如果你需要一大段注释才能说清楚程序做什么,那么你应该注意了:是否是因为程序变量命名不够清晰,或者代码逻辑过于混乱,这个时候你应该考虑的可能就不是注释,而是如何精简这个程序了。


使用特权

评论回复
八层楼|  楼主 | 2018-9-15 07:59 | 显示全部楼层
6.2数据结构

      数据结构是程序设计的基础。在设计程序之前,应该先考虑好所需要的数据结构。

      前微软首席架构师Charles Simonyi:编程的第一步是想象。就是要在脑海中对来龙去脉有极为清晰的把握。在这个初始阶段,我会使用纸和铅笔。我只是信手涂鸦,并不写代码。我也许会画些方框或箭头,但基本上只是涂鸦,因为真正的想法在我脑海里。我喜欢想象那些有待维护的结构,那些结构代表着我想编码的真实世界。一旦这个结构考虑得相当严谨和明确,我便开始写代码。我会坐到终端前,或者换在以前的话,就会拿张白纸,开始写代码。这相当容易。我只要把头脑中的想法变换成代码写下来,我知道结果应该是什么样的。大部分代码会水到渠成,不过我维护的那些数据结构才是关键。我会先想好数据结构,并在整个编码过程中将它们牢记于心。

    开发过以太网和操作系统SDS 940的Butler Lampson:(程序员)最重要的素质是能够把问题的解决方案组织成容易操控的结构。

    开发CP/M操作系统的Gary.A:如果不能确认数据结构是正确的,我是决不会开始编码的。我会先画数据结构,然后花很长时间思考数据结构。在确定数据结构之后我就开始写一些小段的代码,并不断地改善和监测。在编码过程中进行测试可以确保所做的修改是局部的,并且如果有什么问题的话,能够马上发现。

    微软创始人比尔·盖茨:编写程序最重要的部分是设计数据结构。接下来重要的部分是分解各种代码块。

    编写世界上第一个电子表格软件的Dan Bricklin:在我看来,写程序最重要的部分是设计数据结构,此外,你还必须知道人机界面会是什么样的。

       我们举个例子来说明。在介绍防御性编程的时候,提到公司使用的LCD显示屏抗干扰能力一般,为了提高LCD的稳定性,需要定期读出LCD内部的关键寄存器值,然后跟存在Flash中的初始值相比较。需要读出的LCD寄存器有十多个,从每个寄存器读出的值也不尽相同,从1个到8个字节都有可能。如果不考虑数据结构,编写出的程序将会很冗长。



  • 1.        void lcd_redu(void)  



  • 2.        {  



  • 3.            读第一个寄存器值;  



  • 4.            if(第一个寄存器值==Flash存储值)  



  • 5.            {  



  • 6.                读第二个寄存器值;  



  • 7.                if(第二个寄存器值==Flash存储值)  



  • 8.                {  



  • 9.                    ...  



  • 10.                      



  • 11.                    读第十个寄存器值;  



  • 12.                    if(第十个寄存器值==Flash存储值)  



  • 13.                    {  



  • 14.                        返回;  



  • 15.                    }  



  • 16.                    else  



  • 17.                    {  



  • 18.                        重新初始化LCD;  



  • 19.                    }  



  • 20.                }  



  • 21.                else  



  • 22.                {  



  • 23.                    重新初始化LCD;  



  • 24.                }  



  • 25.            }  



  • 26.            else  



  • 27.            {  



  • 28.                重新初始化LCD;  



  • 29.            }  



  • 30.        }  


      我们分析这个过程,发现能提取出很多相同的元素,比如每次读LCD寄存器都需要该寄存器的命令号,都会经过读寄存器、判断值是否相同、处理异常情况这一过程。所以我们可以提取一些相同的元素,组织成数据结构,用统一的方法去处理这些数据,将数据与处理过程分开来。

      我们可以先提取相同的元素,将之组织成数据结构:


  • 1.        typedef struct {  



  • 2.            uint8_t  lcd_command;           //LCD寄存器  



  • 3.            uint8_t  lcd_get_value[8];      //初始化时写入寄存器的值  



  • 4.            uint8_t  lcd_value_num;         //初始化时写入寄存器值的数目  



  • 5.        }lcd_redu_list_struct;  


      这里lcd_command表示的是LCD寄存器命令号;lcd_get_value是一个数组,表示寄存器要初始化的值,这是因为对于一个LCD寄存器,可能要初始化多个字节,这是硬件特性决定的;lcd_value_num是指一个寄存器要多少个字节的初值,这是因为每一个寄存器的初值数目是不同的,我们用同一个方法处理数据时,是需要这个信息的。

      就本例而言,我们将要处理的数据都是事先固定的,所以定义好数据结构后,我们可以将这些数据组织成表格:



  • 1.        /*LCD部分寄存器设置值列表*/  



  • 2.        lcd_redu_list_struct const lcd_redu_list_str[]=  



  • 3.        {  



  • 4.          {SSD1963_Get_Address_Mode,{0x20}                                   ,1}, /*1*/



  • 5.          {SSD1963_Get_Pll_Mn      ,{0x3b,0x02,0x04}                         ,3}, /*2*/



  • 6.          {SSD1963_Get_Pll_Status  ,{0x04}                                   ,1}, /*3*  



  • 7.          {SSD1963_Get_Lcd_Mode    ,{0x24,0x20,0x01,0xdf,0x01,0x0f,0x00}     ,7}, /*4*/



  • 8.          {SSD1963_Get_Hori_Period ,{0x02,0x0c,0x00,0x2a,0x07,0x00,0x00,0x00},8}, /*5*/



  • 9.          {SSD1963_Get_Vert_Period ,{0x01,0x1d,0x00,0x0b,0x09,0x00,0x00}     ,7}, /*6*/



  • 10.          {SSD1963_Get_Power_Mode  ,{0x1c}                                   ,1}, /*7*/



  • 11.          {SSD1963_Get_Display_Mode,{0x03}                                   ,1}, /*8*/



  • 12.          {SSD1963_Get_Gpio_Conf   ,{0x0F,0x01}                              ,2}, /*9*/



  • 13.          {SSD1963_Get_Lshift_Freq ,{0x00,0xb8}                              ,2}, /*10*



  • 14.        };


      至此,我们就可以用一个处理过程来完成数十个LCD寄存器的读取、判断和异常处理了:


  • 1.        /**



  • 2.        * lcd 显示冗余



  • 3.        * 每隔一段时间调用该程序一次



  • 4.        */  



  • 5.        void lcd_redu(void)  



  • 6.        {  



  • 7.            uint8_t  tmp[8];  



  • 8.            uint32_t i,j;  



  • 9.            uint32_t lcd_init_flag;  



  • 10.             



  • 11.            lcd_init_flag =0;  



  • 12.            for(i=0;i<sizeof(lcd_redu_list_str)/sizeof(lcd_redu_list_str[0]);i++)  



  • 13.            {  



  • 14.                LCD_SendCommand(lcd_redu_list_str.lcd_command);  



  • 15.                uyDelay(10);  



  • 16.                for(j=0;j<lcd_redu_list_str.lcd_value_num;j++)  



  • 17.                {  



  • 18.                    tmp[j]=LCD_ReadData();  



  • 19.                    if(tmp[j]!=lcd_redu_list_str.lcd_get_value[j])  



  • 20.                    {  



  • 21.                        lcd_init_flag=0x55;  



  • 22.                        //一些调试语句,打印出错的具体信息



  • 23.                        goto handle_lcd_init;  



  • 24.                    }  



  • 25.                }  



  • 26.            }  



  • 27.             



  • 28.            handle_lcd_init:  



  • 29.            if(lcd_init_flag==0x55)  



  • 30.            {  



  • 31.                //重新初始化LCD  



  • 32.                //一些必要的恢复措施  



  • 33.            }     



  • 34.        }  


      通过合理的数据结构,我们可以将数据和处理过程分开,LCD冗余判断过程可以用很简洁的代码来实现。更重要的是,将数据和处理过程分开更有利于代码的维护。比如,通过实验发现,我们还需要增加一个LCD寄存器的值进行判断,这时候只需要将新增加的寄存器信息按照数据结构格式,放到LCD寄存器设置值列表中的任意位置即可,不用增加任何处理代码即可实现!这仅仅是数据结构的优势之一,使用数据结构还能简化编程,使复杂过程变的简单,这个只有实际编程后才会有更深的理解。


使用特权

评论回复
八层楼|  楼主 | 2018-9-15 07:59 | 显示全部楼层
7.总结和阅读书目

      本文介绍了编写优质嵌入式C程序涉及的多个方面。每年都有亿万计的C程序运行在单片机、ARM7、Cortex-M3这些微处理器上,但在这些处理器上如何编写优质高效的C程序,几乎没有书籍做专门介绍。本文试图在这方面做一些努力。编写优质嵌入式C程序需要大量的专业知识,本文虽尽力描述编写嵌入式C程序所需要的各种技能,但本文却无力将每一个方面都面面俱到的描述出来,所以本文最后会列举一些阅读书目,这些书大多都是真正大师的经验之谈。站在巨人的肩膀上,可以看的更远。

7.1关于语言特性
  • Stephen Prata 著 云巅工作室 译 《C Primer Plus(第五版)中文版》
  • Andrew Koenig 著 高巍 译 《C陷阱与缺陷》
  • Peter Van Der Linden 著 徐波 译 《C专家编程》
  • 陈正冲 编著 《C语言深度解剖》

7.2关于编译器
  • 杜春雷 编著 《ARM体系结构与编程》
  • Keil MDK 编译器帮助手册

7.3关于防御性编程
  • MISRA-C-:2004 Guidelines for the use of the C language in criticalsystems
  • Robert C.Seacord 著 徐波 译 《C安全编码标准》

7.4关于编程思想
  • Pete Goodliffe 著 韩江、陈玉 译 《编程匠艺---编写卓越的代码》
  • Susan Lammers 著 李琳骁、吴咏炜、张菁《编程大师访谈录》

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则