我自己的原文哦~?? ? ?https://blog.51cto.com/whaosoft/13870376
一、STM32怎么選型
什么是 STM32
????STM32,從字面上來理解,ST是意法半導體,M是Microelectronics的縮寫,32表示32位,合起來理解,STM32就是指ST公司開發的32位微控制器。在如今的32位控制器當中,STM32可以說是最璀璨的新星,它受寵若嬌,大受工程師和市場的青睞,無芯能出其右。
????STM32屬于一個微控制器,自帶了各種常用通信接口,比如USART、I2C、SPI等,可接非常多的傳感器,可以控制很多的設備。現實生活中,我們接觸到的很多電器產品都有STM32的身影,比如智能手環,微型四軸飛行器,平衡車、移動POST機,智能電飯鍋,3D打印機等等。
????現在無人機非常火熱,高端的無人機用STM32做不來,但是小型的四軸飛行器用STM32還是綽綽有余的。
STM32 分類
????STM32有很多系列,可以滿足市場的各種需求,從內核上分有Cortex-M0、M3、M4和M7這幾種,每個內核又大概分為主流、高性能和低功耗。具體如下表所示。
???單純從學習的角度出發,可以選擇F1和F4,F1代表了基礎型,基于Cortex-M3內核,主頻為72MHZ,F4代表了高性能,基于Cortex-M4內核,主頻180M。之于F1,F4(429系列以上)除了內核不同和主頻的提升外,升級的明顯特色就是帶了LCD控制器和攝像頭接口,支持SDRAM,這個區別在項目選型上會被優先考慮。但是從大學教學和用戶初學來說,還是首選F1系列,目前在市場上資料最多,產品占有量最多的就是F1系列的STM32。
更詳細的命名方法說明,見下圖。
選擇合適的 MCU
????了解了STM32的分類和命名方法之后,就可以根據項目的具體需求先大概選擇哪類內核的MCU,普通應用,不需要接大屏幕的一般選擇Cortex-M3內核的F1系列,如果要追求高性能,需要大量的數據運算,且需要外接RGB大屏幕的則選擇Cortex-M4內核的F429系列。明確了大方向之后,接下來就是細分選型,先確定引腳,引腳多的功能就多,價格也貴,具體得根據實際項目中需要使用到什么功能,夠用就好。確定好了引腳數目之后再選擇FLASH大小,相同引腳數的MCU會有不同的FLASH大小可供選擇,這個也是根據實際需要選擇,程序大的就選擇大點的FLASH,要是產品一量產,這些省下來的都是錢啊。有些月出貨量以KK(百萬數量級)為單位的產品,不僅是MCU,連電阻電容能少用就少用,更甚者連PCB的過孔的多少都有講究。項目中的元器件的選型有很多學問。
二、C語言內存泄漏問題及其檢視方法
???本文通過介紹內存泄漏問題原理及檢視方法,希望后續能夠從編碼檢視環節就杜絕此類問題發生。
????預防內存泄漏問題有多種方法,如加強代碼檢視、工具檢測和內存測試等,本文聚集于開發人員能力提升方面。
內存泄漏問題原理
1 堆內存在C代碼中的存儲方式
????內存泄漏問題只有在使用堆內存的時候才會出現,棧內存不存在內存泄漏問題,因為棧內存會自動分配和釋放。C語言代碼中堆內存的申請函數是malloc,常見的內存申請代碼如下:
由于malloc函數返回的實際上是一個內存地址,所以保存堆內存的變量一定是一個指針(除非代碼編寫極其不規范)。再重復一遍,保存堆內存的變量一定是一個指針,這對本文主旨的理解很重要。當然,這個指針可以是單指針,也可以是多重指針。
??? malloc函數有很多變種或封裝,如g_malloc、g_malloc0、VOS_Malloc等,這些函數最終都會調用malloc函數。
2 堆內存的獲取方法
????看到本小節標題,可能有些同學有疑惑,上一小節中的malloc函數,不就是堆內存的獲取方法嗎?的確是,通過malloc函數申請是最直接的獲取方法,如果只知道這種堆內存獲取方法,就容易掉到坑里了。
????一般的來講,堆內存有如下兩種獲取方法:
「方法一:將函數返回值直接賦給指針,一般表現形式如下:」
char *local_pointer_xx = NULL;
local_pointer_xx = (char*)function_xx(para_xx, …);
????該類涉及到內存申請的函數,返回值一般都指針類型,例如:
GSList* g_slist_append (GSList *list, gpointer data)
「方法二:將指針地址作為函數返回參數,通過返回參數保存堆內存地址,一般表現形式如下:」
int ret;
char *local_pointer_xx = NULL; /**轉換后的字符串**/
ret = (char*)function_xx(..., &local_pointer_xx, ...);
????該類涉及到內存申請的函數,一般都有一個入參是雙重指針,例如:
__STDIO_INLINE _IO_ssize_t
getline (char **__lineptr, size_t *__n, FILE *__stream)
????前面說通過malloc申請內存,就屬于方法一的一個具體表現形式。其實這兩類方法的本質是一樣的,都是函數內部間接申請了內存,只是傳遞內存的方法不一樣,方法一通過返回值傳遞內存指針,方法二通過參數傳遞內存指針。? ?
3 內存泄漏三要素
????最常見的內存泄漏問題,包含以下三個要素:
**要素一:**函數內有局部指針變量定義;
**要素二:**對該局部指針有通過上一小節中“兩種堆內存獲取方法”之一獲取內存;
**要素三:**在函數返回前(含正常分支和異常分支)未釋放該內存,也未保存到其它全局變量或返回給上一級函數。
4 內存釋放誤區
????稍微使用過C語言編寫代碼的人,都應該知道堆內存申請之后是需要釋放的。但為何還這么容易出現內存泄漏問題呢?一方面,是開發人員經驗不足、意識不到位或一時疏忽導致;另一方面,是內存釋放誤區導致。很多開發人員,認為要釋放的內存應該局限于以下兩種:
1)直接使用內存申請函數申請出來的內存,如malloc、g_malloc等;
2)該開發人員熟悉的接口中,存在內存申請的情況,如iBMC的兄弟,都應該知道調用如下接口需要釋放list指向的內存:
dfl_get_object_list(const char* class_name, GSList **list)
????按照以上思維編寫代碼,一旦遇到不熟悉的接口中需要釋放內存的問題,就完全沒有釋放內存的意識,內存泄漏問題就自然產生了。
內存泄漏問題檢視方法
????檢視內存泄漏問題,關鍵還是要養成良好的編碼檢視習慣。與內存泄漏三要素對應,需
????要做到如下三點:
(1)在函數中看到有局部指針,就要警惕內存泄漏問題,養成進一步排查的習慣
(2)分析對局部指針的賦值操作,是否屬于前面所說的“兩種堆內存獲取方法”之一,如果是,就要分析函數返回的指針到底指向啥?是全局數據、靜態數據還是堆內存?對于不熟悉的接口,要找到對應的接口文檔或源代碼分析;又或者看看代碼中其它地方對該接口的引用,是否進行了內存釋放;
(3)如果確認對局部指針存在內存申請操作,就需要分析該內存的去向,是會被保存在全局變量嗎?又或者會被作為函數返回值嗎?如果都不是,就需要排查函數所有有”return“的地方,保證內存被正確釋放。
三、.h文件與.c文件
.h文件與.c文件的關系
????參考高手的程序時,發現別人寫的嚴格的程序都帶有一個“KEY.H”,里面定義了.C文件里用到的自己寫的函數,如Keyhit()、Keyscan()等。.H文件就是頭文件,估計就是Head的意思吧,這是規范程序結構化設計的需要,既可以實現大型程序的模塊化,又可以實現根各模塊的連接調試。
.H文件介紹:
????在單片機嵌入式C程序設計中,項目一般按功能模塊化進行結構化設計。將一個項目劃分為多個功能,每個功能的相關程序放在一個C程序文檔中,稱之為一個模塊,對應的文件名即為模塊名。一個模塊通常由兩個文檔組成,一個為頭文件*.h,對模塊中的數據結構和函數原型進行描述;另一個則為C文件*.c ,對數據實例或對象定義,以及函數算法具體實現。
.H文件的作用
????作為項目設計,除了對項目總體功能進行詳細描述外,就是對每個模塊進行詳細定義,也就是給出所有模塊的頭文件。通常H頭文件要定義模塊中各函數的功能,以及輸入和輸出參數的要求。模塊的具體實現,由項目組成根據H文件進行設計、編程、調試完成。為了保密和安全,模塊實現后以可連接文件OBJ、或庫文件LIB的方式提供給項目其他成員使用。由于不用提供源程序文檔,一方面可以公開發行,保證開發人員的所有權;另一方面可以防止別人有意或無意修改產生非一致性,造成版本混亂。所以H頭文件是項目的詳細設計和團隊工作劃分的依據,也是對模塊進行測試的功能說明。要引用模塊內的數據或算法,只要用包含include指定模塊H頭文件即可。
.H文件的基本組成
/*如下為鍵盤驅動的頭文檔*/
#ifndef _KEY_H_ //防重復引用,如果沒有定義過_KEY_H_,則編譯下句
#define _KEY_H_ //此符號唯一, 表示只要引用過一次,即#i nclude,則定義符號_KEY_H_
/char keyhit( void ); //擊鍵否unsigned char Keyscan( void ); //取鍵值/
#endif
盡量使用宏定義#define
????開始看別人的程序時,發現程序開頭,在文件包含后面有很多#define語句,當時就想,搞這么多標示符替換來替換去的,麻不麻煩啊,完全沒有理解這種寫法的好處。原來,用一個標示符表示常數,有利于以后的修改和維護,修改時只要在程序開頭改一下,程序中所有用到的地方就全部修改,節省時間。
#define KEYNUM 65//按鍵數量,用于Keycode[KEYNUM]
#define LINENUM 8//鍵盤行數
#define ROWNUM 8//鍵盤列數
????注意的地方:
- 宏名一般用大寫
- 宏定義不是C語句,結尾不加分號
不要亂定義變量類型
????以前寫程序,當需要一個新的變量時,不管函數內還是函數外的,直接在程序開頭定義,雖然不是原則上的錯誤,但是很不可取的作法。下面說一下,C語言中變量類型的有關概念。從變量的作用范圍來分,分為局部變量和全局變量:
- 全局變量:是在函數外定義的變量,全局變量在程序全部執行過程中都占用資源,全局變量過多使程序的通用性變差,因為全局變量是模塊間耦合的原因之一。
- 局部變量:在函數內部定義的變量,只在函數內部有效。
????從變量的變量值存在的時間分為兩種:
- 靜態存儲變量:程序運行期間分配固定的存儲空間。
- 動態存儲變量:程序運行期間根據需要動態地分配存儲空間。
????具體又包括四種存儲方式:
- auto
- static
- register
- extern
????不加說明默認為auto型,即動態存儲,如果不賦初值,將是一個不確定的值。而將局部變量定義為static型的話,則它的值在函數內是不變的,且初值默認為0。編譯時分配為靜態存儲區,可以被本文件中的各個函數引用。如果是多個文件的話,如果在一個文件中引用另外文件中的變量,在此文件中要用extern說明。不過如果一個全局變量定義為static的話,就只能在此一個文件中使用。register定義寄存器變量,請求編譯器將這個變量保存在CPU的寄存器中,從而加快程序的運行。
特殊關鍵字const volatile的使用
const
??? const用于聲明一個只讀的變量。
const unsigned char a=1;//定義a=1,編譯器不允許修改a的值
????作用:保護不希望被修改的參數。
volatile
????一個定義為volatile的變量是說這變量可能會被意想不到地改變,這樣,編譯器就不會去假設這個變量的值了。精確地說就是,優化器在用到這個變量時必須每次都小心地重新讀取這個變量的值,而不是使用保存在寄存器里的備份。
static int i=0;
int main(void)
{
...
while (1)
{
if (i)
dosomething();
}
}
/* Interrupt service routine. */
void ISR_2(void)
{
i=1;
}
????程序的本意是希望ISR_2中斷產生時,在main當中調用dosomething函數,但是,由于編譯器判斷在main函數里面沒有修改過i,因此可能只執行一次對從i到某寄存器的讀操作,然后每次if判斷都只使用這個寄存器里面的“i副本”,導致dosomething永遠也不會被調用。如果將將變量加上volatile修飾,則編譯器保證對此變量的讀寫操作都不會被優化(肯定執行)。
????一般說來,volatile用在如下的幾個地方:
- 中斷服務程序中修改的供其它程序檢測的變量需要加volatile;
- 多任務環境下各任務間共享的標志應該加volatile;
- 存儲器映射的硬件寄存器通常也要加volatile說明,因為每次對它的讀寫都可能由不同意義。
四、嵌入式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就是用于處理這種情況的關鍵字,在大部分支持跨平臺的軟件項目中被采用,典型的如下:
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
......
typedef signed int int32_t;
????既然不同平臺的基本數據寬度不同,那么如何確定當前平臺的基礎數據類型如int的寬度,這就需要C語言提供的接口sizeof,實現如下。
printf("int size:%d, short size:%d, char size:%d\n", sizeof(int), sizeof(char), sizeof(short));
? ??這里還有重要的知識點,就是指針的寬度,如:
char *p;
printf("point p size:%d\n", sizeof(p));
????其實這就和芯片的可尋址寬度有關,如32位MCU的寬度就是4,64位MCU的寬度就是8,在有些時候這也是查看MCU位寬比較簡單的方式。
內存管理和存儲架構
??? C語言允許程序變量在定義時就確定內存地址,通過作用域,以及關鍵字extern,static,實現了精細的處理機制,按照在硬件的區域不同,內存分配有三種方式(節選自C++高質量編程):
- 從靜態存儲區域分配。內存在程序編譯的時候就已經分配好,這塊內存在程序的整個運行期間都存在。例如全局變量,static 變量。
- 在棧上創建。在執行函數時,函數內局部變量的存儲單元都可以在棧上創建,函數執行結束時這些存儲單元自動被釋放。棧內存分配運算內置于處理器的指令集中 ,效率很高,但是分配的內存容量有限。
- 從堆上分配,亦稱動態內存分配。程序在運行的時候用 malloc 或 new 申請任意多少的內存,程序員自己負責在何時用 free 或 delete 釋放內存。動態內存的生存期由程序員決定,使用非常靈活,但同時遇到問題也最多。
????這里先看個簡單的C語言實例。
//main.c#include <stdio.h>#include <stdlib.h>static int st_val; //靜態全局變量 -- 靜態存儲區
int ex_val; //全局變量 -- 靜態存儲區int main(void)
{int a = 0; //局部變量 -- 棧上申請int *ptr = NULL; //指針變量static int local_st_val = 0; //靜態變量local_st_val += 1;a = local_st_val;ptr = (int *)malloc(sizeof(int)); //從堆上申請空間if(ptr != NULL){ printf("*p value:%d", *ptr);free(ptr); ptr = NULL; //free后需要將ptr置空,否則會導致后續ptr的校驗失效,出現野指針 }
}
??? 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), 這強化了對內存的管理,以適應復雜的應用環境場景和需求。
LD_ROM 0x00800000 0x10000 { ;load region size_regionEX_ROM 0x00800000 0x10000 { ;load address = execution address*.o (RESET, +First)*(InRoot$$Sections).ANY (+RO)}EX_RAM 0x20000000 0xC000 { ;rw Data.ANY (+RW +ZI)}EX_RAM1 0x2000C000 0x2000 {.ANY(MySection)}EX_RAM2 0x40000000 0x20000{.ANY(Sdram)}
}int a[10] __attribute__((section("Mysection")));
int b[100] __attribute__((section("Sdram")));
? ? 采用這種方式,我們就可以將變量指定到需要的區域,這在某些情況下是必須的,如做GUI或者網頁時因為要存儲大量圖片和文檔,內部FLASH空間可能不足,這時就可以將變量聲明到外部區域,另外內存中某些部分的數據比較重要,為了避免被其它內容覆蓋,可能需要單獨劃分SRAM區域,避免被誤修改導致致命性的錯誤,這些經驗在實際的產品開發中是常用且重要,不過因為篇幅原因,這里只簡略的提供例子,如果工作中遇到這種需求,建議詳細去了解下。
????至于堆的使用,對于嵌入式Linux來說,使用起來和標準C語言一致,注意malloc后的檢查,釋放后記得置空,避免"野指針“,不過對于資源受限的單片機來說,使用malloc的場景一般較少,如果需要頻繁申請內存塊的場景,都會構建基于靜態存儲區和內存塊分割的一套內存管理機制,一方面效率會更高(用固定大小的塊提前分割,在使用時直接查找編號處理),另一方面對于內存塊的使用可控,可以有效避免內存碎片的問題,常見的如RTOS和網絡LWIP都是采用這種機制,我個人習慣也采用這種方式,所以關于堆的細節不在描述,如果希望了解,可以參考<C Primer Plus>中關于存儲相關的說明。
指針和數組
????數組和指針往往是引起程序bug的主要原因,如數組越界,指針越界,非法地址訪問,非對齊訪問,這些問題背后往往都有指針和數組的影子,因此理解和掌握指針和數組,是成為合格C語言開發者的必經之路。
????數組是由相同類型元素構成,當它被聲明時,編譯器就根據內部元素的特性在內存中分配一段空間,另外C語言也提供多維數組,以應對特殊場景的需求,而指針則是提供使用地址的符號方法,只有指向具體的地址才有意義,C語言的指針具有最大的靈活性,在被訪問前,可以指向任何地址,這大大方便了對硬件的操作,但同時也對開發者有了更高的要求。參考如下代碼。
int main(void)
{
char cval[] = "hello";
int i;
int ival[] = {1, 2, 3, 4};
int arr_val[][2] = {{1, 2}, {3, 4}};
const char *pconst = "hello";
char *p;
int *pi;
int *pa;
int **par;p = cval;p++; //addr增加1pi = ival;pi+=1; //addr增加4pa = arr_val[0];pa+=1; //addr增加4par = arr_val;par++; //addr增加8
for(i=0; i<sizeof(cval); i++){
printf("%d ", cval[i]);}
printf("\n");
printf("pconst:%s\n", pconst);
printf("addr:%d, %d\n", cval, p);
printf("addr:%d, %d\n", icval, pi);
printf("addr:%d, %d\n", arr_val, pa);
printf("addr:%d, %d\n", arr_val, par);
}/* PC端64位系統下運行結果
0x68 0x65 0x6c 0x6c 0x6f 0x0
pconst:hello
addr:6421994, 6421995
addr:6421968, 6421972
addr:6421936, 6421940
addr:6421936, 6421944 */
? ? 對于數組來說,一般從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,這些知識理解起來并不困難,但是這些特性在工程運用中稍有不慎,就會埋下不易察覺的問題。另外指針還支持強制轉換,這在某些情況下相當有用,參考如下代碼:
#include <stdio.h>typedef struct
{
int b;
int a;
}STRUCT_VAL;
static __align(4) char arr[8] = {0x12, 0x23, 0x34, 0x45, 0x56, 0x12, 0x24, 0x53};
int main(void)
{STRUCT_VAL *pval;
int *ptr;pval = (STRUCT_VAL *)arr;ptr = (int *)&arr[4];
printf("val:%d, %d", pval->a, pval->b);
printf("val:%d,", *ptr);
}
//0x45342312 0x53241256
//0x53241256
???基于指針的強制轉換,在協議解析,數據存儲管理中高效快捷的解決了數據解析的問題,但是在處理過程中涉及的數據對齊,大小端,是常見且十分易錯的問題,如上面arr字符數組,通過__align(4)強制定義為4字節對齊是必要的,這里可以保證后續轉換成int指針訪問時,不會觸發非對齊訪問異常,如果沒有強制定義,char默認是1字節對齊的,當然這并不就是一定觸發異常(由整個內存的布局決定arr的地址,也與實際使用的空間是否支持非對齊訪問有關,如部分SDRAM使用非對齊訪問時,會觸發異常), 這就導致可能增減其它變量,就可能觸發這種異常,而出異常的地方往往和添加的變量毫無關系,而且代碼在某些平臺運行正常,切換平臺后觸發異常,這種隱蔽的現象是嵌入式中很難查找解決的問題。另外,C語言指針還有特殊的用法就是通過強制轉換給特定的物理地址訪問,通過函數指針實現回調,如下:
?這里說明下,volatile易變的,可變的,一般用于以下幾種狀況:
- 并行設備的硬件寄存器,如:狀態寄存器)
- 一個中斷服務子程序中會訪問到的非自動變量(Non-automatic variables)
- 多線程應用中被幾個任務共享的變量
??? volatile可以解決用戶模式和異常中斷訪問同一個變量時,出現的不同步問題,另外在訪問硬件地址時,volatile也阻止對地址訪問的優化,從而確保訪問的實際的地址,精通volatile的運用,在嵌入式底層中十分重要,也是嵌入式C從業者的基本要求之一。函數指針在一般嵌入式軟件的開發中并不常見,但對許多重要的實現如異步回調,驅動模塊,使用函數指針就可以利用簡單的方式實現很多應用,當然我這里只能說是拋磚引玉,許多細節知識是值得詳細去了解掌握的。
結構類型和對齊
??? C語言提供自定義數據類型來描述一類具有相同特征點的事務,主要支持的有結構體,枚舉和聯合體。其中枚舉通過別名限制數據的訪問,可以讓數據更直觀,易讀,實現如下:
typedef enum {spring=1, summer, autumn, winter }season;
season s1 = summer;
? ??聯合體的是能在同一個存儲空間里存儲不同類型數據的數據類型,對于聯合體的占用空間,則是以其中占用空間最大的變量為準,如下:
? 聯合體的用途主要通過共享內存地址的方式,實現對數據內部段的訪問,這在解析某些變量時,提供了更為簡便的方式,此外測試芯片的大小端模式也是聯合體的常見應用,當然利用指針強制轉換,也能實現該目的,實現如下:
int data = 0x12345678;
short *pdata = (short *)&data;
if(*pdata = 0x5678) printf("%s\n", "小端模式");
else printf("%s\n", "大端模式");
? ? 可以看出使用聯合體在某些情況下可以避免對指針的濫用。結構體則是將具有共通特征的變量組成的集合,比起C++的類來說,它沒有安全訪問的限制,不支持直接內部帶函數,但通過自定義數據類型,函數指針,仍然能夠實現很多類似于類的操作,對于大部分嵌入式項目來說,結構化處理數據對于優化整體架構以及后期維護大有便利。
??? C語言的結構體支持指針和變量的方式訪問,通過轉換可以解析任意內存的數據,如我們之前提到的通過指針強制轉換解析協議。另外通過將數據和函數指針打包,在通過指針傳遞,是實現驅動層實接口切換的重要基礎,有著重要的實踐意義,另外基于位域,聯合體,結構體,可以實現另一種位操作,這對于封裝底層硬件寄存器具有重要意義。通過聯合體和位域操作,可以實現對數據內bit的訪問,這在寄存器以及內存受限的平臺,提供了簡便且直觀的處理方式,另外對于結構體的另一個重要知識點就是對齊了,通過對齊訪問,可以大幅度提高運行效率,但是因為對齊引入的存儲長度問題,也是容易出錯的問題,對于對齊的理解,可以分類為如下說明。
- 基礎數據類型:以默認的的長度對齊,如char以1字節對齊,short以2字節對齊等
- 數組 :按照基本數據類型對齊,第一個對齊了后面的自然也就對齊了。
- 聯合體 :按其包含的長度最大的數據類型對齊。
- 結構體:結構體中每個數據類型都要對齊,結構體本身以內部最大數據類型長度對齊
??其中union聯合體的大小與內部最大的變量int一致,為4字節,根據讀取的值,就知道實際內存布局和填充的位置是一致,事實上學會通過填充來理解C語言的對齊機制,是有效且快捷的方式。
預處理機制
??? C語言提供了豐富的預處理機制,方便了跨平臺的代碼的實現,此外C語言通過宏機制實現的數據和代碼塊替換,字符串格式化,代碼段切換,對于工程應用具有重要意義,下面按照功能需求,描述在C語言運用中的常用預處理機制。
????#include 包含文件命令,在C語言中,它執行的效果是將包含文件中的所有內容插入到當前位置,這不只包含頭文件,一些參數文件,配置文件,也可以使用該文件插入到當前代碼的指定位置。其中<>和""分別表示從標準庫路徑還是用戶自定義路徑開始檢索。
????#define宏定義,常見的用法包含定義常量或者代碼段別名,當然某些情況下配合##格式化字符串,可以實現接口的統一化處理,實例如下:
#define MAX_SIZE 10
#define MODULE_ON 1
#define ERROR_LOOP() do{\printf("error loop\n");\}while(0);
#define global(val) g_##val
int global(v) = 10;
int global(add)(int a, int b)
{
return a+b;
}
????#if..#elif...#else...#endif, #ifdef..#endif, #ifndef...#endif條件選擇判斷,條件選擇主要用于切換代碼塊,這種綜合性項目和跨平臺項目中為了滿足多種情況下的需求往往會被使用。
????#undef 取消定義的參數,避免重定義問題。
????#error,#warning用于用戶自定義的告警信息,配合#if,#ifdef使用,可以限制錯誤的預定義配置。
????#pragma 帶參數的預定義處理,常見的#pragma pack(1), 不過使用后會導致后續的整個文件都以設置的字節對齊,配合push和pop可以解決這種問題,代碼如下:
#pragma pack(push)
#pragma pack(1)
struct TestA
{
char i;
int b;
}A;
#pragma pack(pop); //注意要調用pop,否則會導致后續文件都以pack定義值對齊,執行不符合預期
//等同于
struct _TestB{
char i;
int b;}__attribute__((packed))A;
總結
????嵌入式C語言在處理硬件物理地址、位操作、內存訪問方面都給予開發者了充分的自由。通過數組,指針以及強制轉換的技巧,可以有效減少數據處理中的復制過程,這對于底層是必要的,也方便了整個架構的開發。對于任何嵌入式C語言開發的從業者,清晰的掌握這些基礎的知識是必要的。
六、嵌入式C語言知識點2
1 位操作
????位操作與位帶操作并不相同,位操作就是對一個變量的每一位做運算,而邏輯位操作是對這個變量整體進行運算。
????下面是六種常用的操作運算符:
按位取反
void test01()
{int num = 7;printf("~num = %d\n", ~num);//-8// 0111 按位取反 1000 機器中存放的都是補碼
//補碼轉換原碼需要分有符號數和無符號數兩種
}
按位與
void test02()
{int num = 128;
//換算為八位,1換算就是00000001, 這樣只要所給數字的二進制最后一位是1.那么就是奇數,否則就是偶數if ( (num & 1) == 0) {printf("num為偶數\n");}else{printf("num為奇數\n");}
}
按位異或
void test03()
{//按位異或的意思是,兩個數字相同為0,不同為1。我們可以利用按位異或實現兩個數的交換num01 = 1; // 0001num02 = 4; // 0100printf("num01 ^ num02 = %d", num01 ^ num02); // 5 兩個二進制按位異或之后是: 0101printf("交換前\n");printf("num01 = %d\n", num1);printf("num02 = %d\n", num2);num01 = num01 ^ num02;num02 = num01 ^ num02;num01 = num01 ^ num02;//不用臨時數字實現兩個變量交換printf("交換后\n");printf("num01 = %d\n", num1);printf("num02 = %d\n", num2);
}
按位或
????計算方法:
????參加運算的兩個數,換算為二進制(0、1)后,進行與運算。只有當?相應位上全部為1時取1,?存在0時為0。
??? printf是格式化輸出函數,它可以直接打印十進制,八進制,十六進制,輸出控制符分別為%d, %o, %x, 但是它不存在二進制,如果輸出二進制,可以手寫,但是也可以調用stdlib.h里面的itoa函數,他不是標準庫里面的函數,但是大多數編譯器里面都有這個函數。
#include <stdio.h>
#include <stdlib.h>int main()
{test04();
}int test04()
{int a = 6; //二進制0110int b = 3; //二進制0011int c = a | b; //a、b按位或,結果8,二進制111,賦值給cchar s[10];itoa(c, s, 2);printf("二進制 --> %s\n", s);//輸出:二進制 -->111
}
左移運算符
void test05()
{int num = 6;printf("%d\n", num << 3);//左移三位,就是0000
}
右移運算符
void test06()
{int num = 6; //0110printf("%d\n", num >> 1); //右移一位,就是0011,輸出3
}
????上面是用普通c代碼舉得栗子,下面我們看一下STM32中操作通常用的代碼:
????(1)比如我要改變 GPIOA-> BSRRL 的狀態,可以先對寄存器的值進行& 清零操作
GPIOA-> BSRRL &= 0xFF0F; //將第4位到第7位清零(注意編號是從0開始的)
????然后再與需要設置的值進行|或運算:
GPIOA-> BSRRL |= 0x0040; //將第4位到第7位設置為我們需要的數字
????(2)通過位移操作提高代碼的可讀性:
GPIOx->ODR = (((uint32_t)0x01) << pinpos);
????上面這行代碼的意思就是,先將"0x01"這個八位十六進制轉換為三十二位二進制,然后左移"pinpos"位,這個"pinpos"就是一個變量,其值就是要移動的位數。也就是將ODR寄存器的第pinpos位設置為1。
????(3)取反操作使用:
??? SR寄存器的每一位代表一個狀態,如果某個時刻我們想設置一個位的值為0,與此同時,其它位置都為1,簡單的作法是直接給寄存器設置一個值:
TIMx->SR=0xFFF7;
????這樣的作法設置第 3 位為 0,但是這樣的作法可讀性較差。看看庫函數代碼中怎樣使用的:
TIMx->SR = (uint16_t)~TIM_FLAG;
????而 TIM_FLAG 是通過宏定義定義的值:
#define TIM_FLAG_Update ((uint16_t)0x0001)
#define TIM_FLAG_CC1 ((uint16_t)0x0002)
2 define宏定義
??? define 是 C 語言中的預處理命令,它用于宏定義,可以提高源代碼的可讀性,為編程提供 方便。
????常見的格式:
#define 標識符 字符串
????標識符意思是所定義的宏名,字符串可以是常數、表達式或者格式串等,例如:
#define PLL_Q 7 //注意,這個定義語句的最后不需要加分號
3 ifdef條件編譯
????在程序開發的過程中,經常會用到這種條件編譯:
#ifdef PLL_Q程序段1
#else程序段2
#endif
????上面這段代碼作用就是當這個標識符已經被定義過,那么就進行程序程序段1,如果沒有則進行程序段2。當然,和我們設計普通的c代碼是一樣的,"#else"也可以沒有,就是上面的代碼減去"#else"和程序段2。
#ifndef PLL_Q //意思就是如果沒有定義這個標識符
4 extern變量申明
????C 語言中 extern 可以置于變量或者函數前,以表示變量或者函數的定義在別的文件中,提示編譯器遇到此變量和函數時在其他模塊中尋找其定義(一個變量只能定義一次,而extern可以申明很多次)使用例子如下:
extern u16 USART_RX_STA;
????上面例子意思就是申明 “USART_RX_STA” 這個變量在其他文件中已經定義了,"u16"的意思是16位的。
5 結構體
????定義一個結構體的一般形式為:
struct 結構名
{成員列表
};
????成員列表由若干個成員組成,每個成員都是該結構體的一個組成部分。對每個成員也必須作類型說明,其形式:
類型說明符 成員名;//比如:int num;
????結合上面的說明,我們可以構建一個簡單的結構體例子:
struct sutdent
{int num;char name[20]; //20個字節長的字符char sex;int age;float score;char addr[30]; //30個字節長的字符
}
????而如果我們想定義結構體變量,那么我們在定義這個結構體的時候直接定義,或者定義完結構體再另外定義結構體變量,比如:
struct sutdent
{int num;char name[20]; //20個字節長的字符char sex;int age;float score;char addr[30]; //30個字節長的字符
}student01,student02; //變量名表列(如果由結構體變量名,那么我們可以不寫結構體名稱)
????有時候我們可能需要用到結構體的嵌套,比如:
struct date
{int year, month,day;
};
struct sutdent
{int num;char name[20]; //20個字節長的字符char sex;struct date birthday; //這里就用到了結構體的嵌套int age;float score;char addr[30]; //30個字節長的字符
}student01,student02; //變量名表列(如果由結構體變量名,那么我們可以不寫結構體名稱)
????如果需要引用結構體里面的成員內容,可以使用下面的方式:
student01.name = 小李;
// 結構體變量名.成員名(注意這里用的是點),這里是對這個成員的賦值
????結構指針變量說明的一般形式為:
struct 結構名 *結構指針變量名
????假如說我們想定義一個指向結構體"student"的指針變量pstu,那么我們可以使用如下代碼:
struct student *pstu;
????如果我們要給一個結構體指針變量賦初值,那么我們可以使用如下的方式:
struct student
{char name[66];int num;char sex;
}stu;
pstu = &stu;
????注意上邊的賦值方式,我們如果要進行賦值,那必須使用結構體變量,而不能使用結構體名,像下邊這樣就是錯誤的。
struct student
{char name[66];int num;char sex;
}stu;pstu = &student;
???這是因為結構名和結構體變量是兩個不同的概念,結構名只能表示一個結構形式,編譯系統并不會給它分配內存空間(就是說不會給它分配地址),而結構體變量作為一個變量,編譯系統會給它分配一個內存空間來存儲。
訪問結構體成員的一般形式:
(*pstu).name; //(1)(*結構指針變量).成員名;pstu->name; //(2)結構指針變量->成員名
????結構體的知識就簡單說上邊這些。
6 typedef類型別名
??? typedef用來為現有類型創建一個新的名字,或者稱為類型別名,用來簡化變量的定義(上邊extern變量申明的例子中,"u16"就是對"uint16_t"類型名稱的簡化)。typedef在MDK中用得最多的就是定義結構體的類型別名和枚舉類型。
????我們定義一個結構體GPIO:
struct _GPIO
{_IO uint32_t MODER;_IO uint32_tOTYPER;...
};
????定義這樣一個結構體以后,如果我們想定義一個結構體變量比如"GPIOA",那么我們需要使用這樣的代碼:
struct _GPIO GPIOA;
????雖然也可以達到我們的目的,但是這樣會比較麻煩,而且在MDK中會有很多地方用到,所以,我們可以使用"typedef"為其定義一個別名,這樣直接通過這個別名就可以定義結構體變量,來達到我們的目的:
typedef struct
{_IO uint32_t MODER;_IO uint32_t OTYPER;
}GPIO_typedef;
????這樣定義完成之后,如果我們需要定義結構體變量,那么我們只需要這樣:
GPIO_typedef _GPIOA,_GPIOB;
七、嵌入式開發中的編譯器
如果你和一個優秀的程序員共事,你會發現他對他使用的工具非常熟悉,就像一個畫家了解他的畫具一樣。----比爾.蓋茨
1 不能簡單的認為是個工具
- 嵌入式程序開發跟硬件密切相關,需要使用C語言來讀寫底層寄存器、存取數據、控制硬件等,C語言和硬件之間由編譯器來聯系,一些C標準不支持的硬件特性操作,由編譯器提供。
- 匯編可以很輕易的讀寫指定RAM地址、可以將代碼段放入指定的Flash地址、可以精確的設置變量在RAM中分布等等,所有這些操作,在深入了解編譯器后,也可以使用C語言實現。
- C語言標準并非完美,有著數目繁多的未定義行為,這些未定義行為完全由編譯器自主決定,了解你所用的編譯器對這些未定義行為的處理,是必要的。
- 嵌入式編譯器對調試做了優化,會提供一些工具,可以分析代碼性能,查看外設組件等,了解編譯器的這些特性有助于提高在線調試的效率。
- 此外,堆棧操作、代碼優化、數據類型的范圍等等,都是要深入了解編譯器的理由。
- 如果之前你認為編譯器只是個工具,能夠編譯就好。那么,是時候改變這種思想了。
2 不能依賴編譯器的語義檢查
????編譯器的語義檢查很弱小,甚至還會“掩蓋”錯誤。現代的編譯器設計是件浩瀚的工程,為了讓編譯器設計簡單一些,目前幾乎所有編譯器的語義檢查都比較弱小。為了獲得更快的執行效率,C語言被設計的足夠靈活且幾乎不進行任何運行時檢查,比如數組越界、指針是否合法、運算結果是否溢出等等。這就造成了很多編譯正確但執行奇怪的程序。
??? C語言足夠靈活,對于一個數組test[30],它允許使用像test[-1]這樣的形式來快速獲取數組首元素所在地址前面的數據;允許將一個常數強制轉換為函數指針,使用代碼(((void()())0))()來調用位于0地址的函數。C語言給了程序員足夠的自由,但也由程序員承擔濫用自由帶來的責任。
2.1莫名的死機
????下面的兩個例子都是死循環,如果在不常用分支中出現類似代碼,將會造成看似莫名其妙的死機或者重啟。
unsigned char i; //例程1 for(i=0;i<256;i++){//其它代碼 }unsigned char i; //例程2 for(i=10;i>=0;i--){//其它代碼 }
????對于無符號char類型,表示的范圍為0~255,所以無符號char類型變量i永遠小于256(第一個for循環無限執行),永遠大于等于0(第二個for循環無限執行)。需要說明的是,賦值代碼i=256是被C語言允許的,即使這個初值已經超出了變量i可以表示的范圍。C語言會千方百計的為程序員創造出錯的機會,可見一斑。
2.2不起眼的改變
????假如你在if語句后誤加了一個分號,可能會完全改變了程序邏輯。編譯器也會很配合的幫忙掩蓋,甚至連警告都不提示。代碼如下:
if(a>b); //這里誤加了一個分號 a=b; //這句代碼一直被執行
????不但如此,編譯器還會忽略掉多余的空格符和換行符,就像下面的代碼也不會給出足夠提示:
??這段代碼的本意是n<3時程序直接返回,由于程序員的失誤,return少了一個結束分號。編譯器將它翻譯成返回表達式logrec.data=x[0]的結果,return后面即使是一個表達式也是C語言允許的。這樣當n>=3時,表達式logrec.data=x[0];就不會被執行,給程序埋下了隱患。
2.3 難查的數組越界
????上文曾提到數組常常是引起程序不穩定的重要因素,程序員往往不經意間就會寫數組越界。
????一位同事的代碼在硬件上運行,一段時間后就會發現LCD顯示屏上的一個數字不正常的被改變。經過一段時間的調試,問題被定位到下面的一段代碼中:
int SensorData[30];//其他代碼 for(i=30;i>0;i--){SensorData[i]=…;//其他代碼 }
????這里聲明了擁有30個元素的數組,不幸的是for循環代碼中誤用了本不存在的數組元素SensorData[30],但C語言卻默許這么使用,并欣然的按照代碼改變了數組元素SensorData[30]所在位置的值, SensorData[30]所在的位置原本是一個LCD顯示變量,這正是顯示屏上的那個值不正常被改變的原因。真慶幸這么輕而易舉的發現了這個Bug。
????其實很多編譯器會對上述代碼產生一個警告:賦值超出數組界限。但并非所有程序員都對編譯器警告保持足夠敏感,況且,編譯器也并不能檢查出數組越界的所有情況。比如下面的例子:
????你在模塊A中定義數組:
int SensorData[30];
????在模塊B中引用該數組,但由于你引用代碼并不規范,這里沒有顯示聲明數組大小,但編譯器也允許這么做:
extern int SensorData[];
????這次,編譯器不會給出警告信息,因為編譯器壓根就不知道數組的元素個數。所以,當一個數組聲明為具有外部鏈接,它的大小應該顯式聲明。
????再舉一個編譯器檢查不出數組越界的例子。函數func()的形參是一個數組形式,函數代碼簡化如下所示:
?這個給SensorData[30]賦初值的語句,編譯器也是不給任何警告的。實際上,編譯器是將數組名Sensor隱含的轉化為指向數組第一個元素的指針,函數體是使用指針的形式來訪問數組的,它當然也不會知道數組元素的個數了。造成這種局面的原因之一是C編譯器的作者們認為指針代替數組可以提高程序效率,而且,可以簡化編譯器的復雜度。
????指針和數組是容易給程序造成混亂的,我們有必要仔細的區分它們的不同。其實換一個角度想想,它們也是容易區分的:可以將數組名等同于指針的情況有且只有一處,就是上面例子提到的數組作為函數形參時。其它時候,數組名是數組名,指針是指針。
????下面的例子編譯器同樣檢查不出數組越界。
????我們常常用數組來緩存通訊中的一幀數據。在通訊中斷中將接收的數據保存到數組中,直到一幀數據完全接收后再進行處理。即使定義的數組長度足夠長,接收數據的過程中也可能發生數組越界,特別是干擾嚴重時。
????這是由于外界的干擾破壞了數據幀的某些位,對一幀的數據長度判斷錯誤,接收的數據超出數組范圍,多余的數據改寫與數組相鄰的變量,造成系統崩潰。由于中斷事件的異步性,這類數組越界編譯器無法檢查到。
????如果局部數組越界,可能引發ARM架構硬件異常。
????同事的一個設備用于接收無線傳感器的數據,一次軟件升級后,發現接收設備工作一段時間后會死機。調試表明ARM7處理器發生了硬件異常,異常處理代碼是一段死循環(死機的直接原因)。接收設備有一個硬件模塊用于接收無線傳感器的整包數據并存在自己的緩沖區中,當硬件模塊接收數據完成后,使用外部中斷通知設備取數據,外部中斷服務程序精簡后如下所示:?
__irq ExintHandler(void) {unsignedchar DataBuf[50];GetData(DataBug); //從硬件緩沖區取一幀數據 //其他代碼 }
????由于存在多個無線傳感器近乎同時發送數據的可能加之GetData()函數保護力度不夠,數組DataBuf在取數據過程中發生越界。由于數組DataBuf為局部變量,被分配在堆棧中,同在此堆棧中的還有中斷發生時的運行環境以及中斷返回地址。溢出的數據將這些數據破壞掉,中斷返回時PC指針可能變成一個不合法值,硬件異常由此產生。
????如果我們精心設計溢出部分的數據,化數據為指令,就可以利用數組越界來修改PC指針的值,使之指向我們希望執行的代碼。
??? 1988年,第一個網絡蠕蟲在一天之內感染了2000到6000臺計算機,這個蠕蟲程序利用的正是一個標準輸入庫函數的數組越界Bug。起因是一個標準輸入輸出庫函數gets(),原來設計為從數據流中獲取一段文本,遺憾的是,gets()函數沒有規定輸入文本的長度。
??? gets()函數內部定義了一個500字節的數組,攻擊者發送了大于500字節的數據,利用溢出的數據修改了堆棧中的PC指針,從而獲取了系統權限。目前,雖然有更好的庫函數來代替gets函數,但gets函數仍然存在著。
2.4神奇的volatile
????做嵌入式設備開發,如果不對volatile修飾符具有足夠了解,實在是說不過去。volatile是C語言32個關鍵字中的一個,屬于類型限定符,常用的const關鍵字也屬于類型限定符。
??? volatile限定符用來告訴編譯器,該對象的值無任何持久性,不要對它進行任何優化;它迫使編譯器每次需要該對象數據內容時都必須讀該對象,而不是只讀一次數據并將它放在寄存器中以便后續訪問之用(這樣的優化可以提高系統速度)。
????這個特性在嵌入式應用中很有用,比如你的IO口的數據不知道什么時候就會改變,這就要求編譯器每次都必須真正的讀取該IO端口。這里使用了詞語“真正的讀”,是因為由于編譯器的優化,你的邏輯反應到代碼上是對的,但是代碼經過編譯器翻譯后,有可能與你的邏輯不符。
????你的代碼邏輯可能是每次都會讀取IO端口數據,但實際上編譯器將代碼翻譯成匯編時,可能只是讀一次IO端口數據并保存到寄存器中,接下來的多次讀IO口都是使用寄存器中的值來進行處理。因為讀寫寄存器是最快的,這樣可以優化程序效率。與之類似的,中斷里的變量、多線程中的共享變量等都存在這樣的問題。
????不使用volatile,可能造成運行邏輯錯誤,但是不必要的使用volatile會造成代碼效率低下(編譯器不優化volatile限定的變量),因此清楚的知道何處該使用volatile限定符,是一個嵌入式程序員的必修內容。
????一個程序模塊通常由兩個文件組成,源文件和頭文件。如果你在源文件定義變量:
unsigned int test;
????并在頭文件中聲明該變量:
extern unsigned long test;
????編譯器會提示一個語法錯誤:變量’ test’聲明類型不一致。但如果你在源文件定義變量:
volatile unsigned int test;
????在頭文件中這樣聲明變量:
extern unsigned int test; /*缺少volatile限定符*/
????編譯器卻不會給出錯誤信息(有些編譯器僅給出一條警告)。當你在另外一個模塊(該模塊包含聲明變量test的頭文件)使用變量test時,它已經不再具有volatile限定,這樣很可能造成一些重大錯誤。比如下面的例子,注意該例子是為了說明volatile限定符而專門構造出的,因為現實中的volatile使用Bug大都隱含,并且難以理解。
????在模塊A的源文件中,定義變量:
volatile unsigned int TimerCount=0;
????該變量用來在一個定時器中斷服務程序中進行軟件計時:
TimerCount++;
????在模塊A的頭文件中,聲明變量:
extern unsigned int TimerCount; //這里漏掉了類型限定符volatile
????在模塊B中,要使用TimerCount變量進行精確的軟件延時:
#include “…A.h” //首先包含模塊A的頭文件 //其他代碼 TimerCount=0;while(TimerCount<=TIMER_VALUE); //延時一段時間(感謝網友chhfish指這里的邏輯錯誤) //其他代碼
????實際上,這是一個死循環。由于模塊A頭文件中聲明變量TimerCount時漏掉了volatile限定符,在模塊B中,變量TimerCount是被當作unsigned int類型變量。由于寄存器速度遠快于RAM,編譯器在使用非volatile限定變量時是先將變量從RAM中拷貝到寄存器中,如果同一個代碼塊再次用到該變量,就不再從RAM中拷貝數據而是直接使用之前寄存器備份值。
????代碼while(TimerCount<=TIMER_VALUE)中,變量TimerCount僅第一次執行時被使用,之后都是使用的寄存器備份值,而這個寄存器值一直為0,所以程序無限循環。下面的流程圖說明了程序使用限定符volatile和不使用volatile的執行過程。
?為了更容易的理解編譯器如何處理volatile限定符,這里給出未使用volatile限定符和使用volatile限定符程序的反匯編代碼:
- 沒有使用關鍵字volatile,在keil MDK V4.54下編譯,默認優化級別,如下所示(注意最后兩行):
122: unIdleCount=0;123:0x00002E10 E59F11D4 LDR R1,[PC,#0x01D4]0x00002E14 E3A05000 MOV R5,#key1(0x00000000)0x00002E18 E1A00005 MOV R0,R50x00002E1C E5815000 STR R5,[R1]124: while(unIdleCount!=200); //延時2S鐘 125:0x00002E20 E35000C8 CMP R0,#0x000000C8 0x00002E24 1AFFFFFD BNE 0x00002E20</span>
- 使用關鍵字volatile,在keil MDK V4.54下編譯,默認優化級別,如下所示(注意最后三行):
122: unIdleCount=0;123:0x00002E10 E59F01D4 LDR R0,[PC,#0x01D4]0x00002E14 E3A05000 MOV R5,#key1(0x00000000)0x00002E18 E5805000 STR R5,[R0]124: while(unIdleCount!=200); //延時2S鐘 125:0x00002E1C E5901000 LDR R1,[R0]0x00002E20 E35100C8 CMP R1,#0x000000C8 0x00002E24 1AFFFFFC BNE 0x00002E1C
????可以看到,如果沒有使用volatile關鍵字,程序一直比較R0內數據與0xC8是否相等,但R0中的數據是0,所以程序會一直在這里循環比較(死循環);再看使用了volatile關鍵字的反匯編代碼,程序會先從變量中讀出數據放到R1寄存器中,然后再讓R1內數據與0xC8相比較,這才是我們C代碼的正確邏輯!
2.5局部變量
??? ARM架構下的編譯器會頻繁的使用堆棧,堆棧用于存儲函數的返回值、AAPCS規定的必須保護的寄存器以及局部變量,包括局部數組、結構體、聯合體和C++的類。默認情況下,堆棧的位置、初始值都是由編譯器設置,因此需要對編譯器的堆棧有一定了解。
????從堆棧中分配的局部變量的初值是不確定的,因此需要運行時顯式初始化該變量。一旦離開局部變量的作用域,這個變量立即被釋放,其它代碼也就可以使用它,因此堆棧中的一個內存位置可能對應整個程序的多個變量。
????局部變量必須顯式初始化,除非你確定知道你要做什么。下面的代碼得到的溫度值跟預期會有很大差別,因為在使用局部變量sum時,并不能保證它的初值為0。編譯器會在第一次運行時清零堆棧區域,這加重了此類Bug的隱蔽性。
?由于一旦程序離開局部變量的作用域即被釋放,所以下面代碼返回指向局部變量的指針是沒有實際意義的,該指針指向的區域可能會被其它程序使用,其值會被改變。
char * GetData(void) {char buffer[100]; //局部數組 …return buffer;}
2.6使用外部工具
????由于編譯器的語義檢查比較弱,我們可以使用第三方代碼分析工具,使用這些工具來發現潛在的問題,這里介紹其中比較著名的是PC-Lint。
??? PC-Lint由Gimpel Software公司開發,可以檢查C代碼的語法和語義并給出潛在的BUG報告。PC-Lint可以顯著降低調試時間。
????目前公司ARM7和Cortex-M3內核多是使用Keil MDK編譯器來開發程序,通過簡單配置,PC-Lint可以被集成到MDK上,以便更方便的檢查代碼。MDK已經提供了PC-Lint的配置模板,所以整個配置過程十分簡單,Keil MDK開發套件并不包含PC-Lint程序,在此之前,需要預先安裝可用的PC-Lint程序,配置過程如下:
- 點擊菜單Tools---Set-up PC-Lint…
PC-Lint Include Folders:該列表路徑下的文件才會被PC-Lint檢查,此外,這些路徑下的文件內使用#include包含的文件也會被檢查;
??? Lint Executable:指定PC-Lint程序的路徑
??? Configuration File:指定配置文件的路徑,該配置文件由MDK編譯器提供。
- 菜單Tools---Lint 文件路徑.c/.h
????檢查當前文件。
- 菜單Tools---Lint All C-Source Files
????檢查所有C源文件。
??? PC-Lint的輸出信息顯示在MDK編譯器的Build Output窗口中,雙擊其中的一條信息可以跳轉到源文件所在位置。
????編譯器語義檢查的弱小在很大程度上助長了不可靠代碼的廣泛存在。隨著時代的進步,現在越來越多的編譯器開發商意識到了語義檢查的重要性,編譯器的語義檢查也越來越強大,比如公司使用的Keil MDK編譯器,雖然它的編輯器依然不盡人意,但在其V4.47及以上版本中增加了動態語法檢查并加強了語義檢查,可以友好的提示更多警告信息。建議經常關注編譯器官方網站并將編譯器升級到V4.47或以上版本,升級的另一個好處是這些版本的編輯器增加了標識符自動補全功能,可以大大節省編碼的時間。
3 你覺得有意義的代碼未必正確
??? C語言標準特別的規定某些行為是未定義的,編寫未定義行為的代碼,其輸出結果由編譯器決定!C標準委員會定義未定義行為的原因如下:
- 簡化標準,并給予實現一定的靈活性,比如不捕捉那些難以診斷的程序錯誤;
- 編譯器開發商可以通過未定義行為對語言進行擴展
C語言的未定義行為,使得C極度高效靈活并且給編譯器實現帶來了方便,但這并不利于優質嵌入式C程序的編寫。因為許多 C 語言中看起來有意義的東西都是未定義的,并且這也容易使你的代碼埋下隱患,并且不利于跨編譯器移植。Java程序會極力避免未定義行為,并用一系列手段進行運行時檢查,使用Java可以相對容易的寫出安全代碼,但體積龐大效率低下。作為嵌入式程序員,我們需要了解這些未定義行為,利用C語言的靈活性,寫出比Java更安全、效率更高的代碼來。
3.1常見的未定義行為
- 自增自減在表達式中連續出現并作用于同一變量或者自增自減在表達式中出現一次,但作用的變量多次出現
????自增(++)和自減(--)這一動作發生在表達式的哪個時刻是由編譯器決定的,比如:
r = 1 * a[i++] + 2 * a[i++] + 3 * a[i++];
????不同的編譯器可能有著不同的匯編代碼,可能是先執行i++再進行乘法和加法運行,也可能是先進行加法和乘法運算,再執行i++,因為這句代碼在一個表達式中出現了連續的自增并作用于同一變量。更加隱蔽的是自增自減在表達式中出現一次,但作用的變量多次出現,比如:
a[i] = i++; /* 未定義行為 */
????先執行i++再賦值,還是先賦值再執行i++是由編譯器決定的,而兩種不同的執行順序的結果差別是巨大的。
- 函數實參被求值的順序
????函數如果有多個實參,這些實參的求值順序是由編譯器決定的,比如:
printf("%d %d\n", ++n, power(2, n)); /* 未定義行為 */
????是先執行++n還是先執行power(2,n)是由編譯器決定的。
- 有符號整數溢出
????有符號整數溢出是未定義的行為,編譯器決定有符號整數溢出按照哪種方式取值。比如下面代碼:
int value1,value2,sum//其它操作 sum=value1+value; /*sum可能發生溢出*/
- 有符號數右移、移位的數量是負值或者大于操作數的位數
- 除數為零
- malloc()、calloc()或realloc()分配零字節內存
3.2如何避免C語言未定義行為
????代碼中引入未定義行為會為代碼埋下隱患,防止代碼中出現未定義行為是困難的,我們總能不經意間就會在代碼中引入未定義行為。但是還是有一些方法可以降低這種事件,總結如下:
- 了解C語言未定義行為
????標準C99附錄J.2“未定義行為”列舉了C99中的顯式未定義行為,通過查看該文檔,了解那些行為是未定義的,并在編碼中時刻保持警惕;
- 尋求工具幫助
????編譯器警告信息以及PC-Lint等靜態檢查工具能夠發現很多未定義行為并警告,要時刻關注這些工具反饋的信息;
- 總結并使用一些編碼標準
??? 1)避免構造復雜的自增或者自減表達式,實際上,應該避免構造所有復雜表達式;
比如a[i] = i++;語句可以改為a[i] = i; i++;這兩句代碼。
??? 2)只對無符號操作數使用位操作;
- 必要的運行時檢查
????檢查是否溢出、除數是否為零,申請的內存數量是否為零等等,比如上面的有符號整數溢出例子,可以按照如下方式編寫,以消除未定義特性:
int value1,value2,sum;//其它代碼 if((value1>0 && value2>0 && value1>(INT_MAX-value2))||(value1<0 && value2<0 && value1<(INT_MIN-value2))){//處理錯誤 }else {sum=value1+value2;}
????上面的代碼是通用的,不依賴于任何CPU架構,但是代碼效率很低。如果是有符號數使用補碼的CPU架構(目前常見CPU絕大多數都是使用補碼),還可以用下面的代碼來做溢出檢查:
int value1, value2, sum;
unsigned int usum = (unsigned int)value1 + value2;if((usum ^ value1) & (usum ^ value2) & INT_MIN)
{/*處理溢出情況*/
}
else
{sum = value1 + value2;
}
????使用的原理解釋一下,因為在加法運算中,操作數value1和value2只有符號相同時,才可能發生溢出,所以我們先將這兩個數轉換為無符號類型,兩個數的和保存在變量usum中。如果發生溢出,則value1、value2和usum的最高位(符號位)一定不同,表達式(usum ^ value1) & (usum ^ value2) 的最高位一定為1,這個表達式位與(&)上INT_MIN是為了將最高位之外的其它位設置為0。
- 了解你所用的編譯器對未定義行為的處理策略
????很多引入了未定義行為的程序也能運行良好,這要歸功于編譯器處理未定義行為的策略。不是你的代碼寫的正確,而是恰好編譯器處理策略跟你需要的邏輯相同。了解編譯器的未定義行為處理策略,可以讓你更清楚的認識到那些引入了未定義行為程序能夠運行良好是多么幸運的事,不然多換幾個編譯器試試!
????以Keil MDK為例,列舉常用的處理策略如下:
1) 有符號量的右移是算術移位,即移位時要保證符號位不改變。
2)對于int類的值:超過31位的左移結果為零;無符號值或正的有符號值超過31位的右移結果為零。負的有符號值移位結果為-1。
3)整型數除以零返回零
4 了解你的編譯器
????在嵌入式開發過程中,我們需要經常和編譯器打交道,只有深入了解編譯器,才能用好它,編寫更高效代碼,更靈活的操作硬件,實現一些高級功能。下面以公司最常用的Keil MDK為例,來描述一下編譯器的細節。
4.1編譯器的一些小知識
- 默認情況下,char類型的數據項是無符號的,所以它的取值范圍是0~255;
- 在所有的內部和外部標識符中,大寫和小寫字符不同;
- 通常局部變量保存在寄存器中,但當局部變量太多放到棧里的時候,它們總是字對齊的。
- 壓縮類型的自然對齊方式為1。使用關鍵字__packed來壓縮特定結構,將所有有效類型的對齊邊界設置為1;
- 整數以二進制補碼形式表示;浮點量按IEEE格式存儲;
- 整數除法的余數的符號于被除數相同,由ISO C90標準得出;
- 如果整型值被截斷為短的有符號整型,則通過放棄適當數目的最高有效位來得到結果。如果原始數是太大的正或負數,對于新的類型,無法保證結果的符號將于原始數相同。
- 整型數超界不引發異常;像unsigned char test; test=1000;這類是不會報錯的;
- 在嚴格C中,枚舉值必須被表示為整型。例如,必須在?2147483648 到+2147483647的范圍內。但MDK自動使用對象包含enum范圍的最小整型來實現(比如char類型),除非使用編譯器命令??enum_is_int 來強制將enum的基礎類型設為至少和整型一樣寬。超出范圍的枚舉值默認僅產生警告:#66:enumeration value is out of "int" range;
- 對于結構體填充,根據定義結構的方式,keil MDK編譯器用以下方式的一種來填充結構:
I> 定義為static或者extern的結構用零填充;
II> 棧或堆上的結構,例如,用malloc()或者auto定義的結構,使用先前存儲在那些存儲器位置的任何內容進行填充。不能使用memcmp()來比較以這種方式定義的填充結構!
- 編譯器不對聲明為volatile類型的數據進行優化;
- __nop():延時一個指令周期,編譯器絕不會優化它。如果硬件支持NOP指令,則該句被替換為NOP指令,如果硬件不支持NOP指令,編譯器將它替換為一個等效于NOP的指令,具體指令由編譯器自己決定;
- __align(n):指示編譯器在n 字節邊界上對齊變量。對于局部變量,n的值為1、2、4、8;
- attribute((at(address))):可以使用此變量屬性指定變量的絕對地址;
- __inline:提示編譯器在合理的情況下內聯編譯C或C++ 函數;
4.2初始化的全局變量和靜態變量的初始值被放到了哪里?
????我們程序中的一些全局變量和靜態變量在定義時進行了初始化,經過編譯器編譯后,這些初始值被存放在了代碼的哪里?我們舉個例子說明:
unsigned int g_unRunFlag=0xA5;static unsigned int s_unCountFlag=0x5A;
????我曾做過一個項目,項目中的一個設備需要在線編程,也就是通過協議,將上位機發給設備的數據通過在應用編程(IAP)技術寫入到設備的內部Flash中。我將內部Flash做了劃分,一小部分運行程序,大部分用來存儲上位機發來的數據。隨著程序量的增加,在一次更新程序后發現,在線編程之后,設備運行正常,但是重啟設備后,運行出現了故障!經過一系列排查,發現故障的原因是一個全局變量的初值被改變了。
????這是件很不可思議的事情,你在定義這個變量的時候指定了初始值,當你在第一次使用這個變量時卻發現這個初值已經被改掉了!這中間沒有對這個變量做任何賦值操作,其它變量也沒有任何溢出,并且多次在線調試表明,進入main函數的時候,該變量的初值已經被改為一個恒定值。
????要想知道為什么全局變量的初值被改變,就要了解這些初值編譯后被放到了二進制文件的哪里。在此之前,需要先了解一點鏈接原理。
??? ARM映象文件各組成部分在存儲系統中的地址有兩種:一種是映象文件位于存儲器時(通俗的說就是存儲在Flash中的二進制代碼)的地址,稱為加載地址;一種是映象文件運行時(通俗的說就是給板子上電,開始運行Flash中的程序了)的地址,稱為運行時地址。
????賦初值的全局變量和靜態變量在程序還沒運行的時候,初值是被放在Flash中的,這個時候他們的地址稱為加載地址,當程序運行后,這些初值會從Flash中拷貝到RAM中,這時候就是運行時地址了。
????原來,對于在程序中賦初值的全局變量和靜態變量,程序編譯后,MDK將這些初值放到Flash中,位于緊靠在可執行代碼的后面。在程序進入main函數前,會運行一段庫代碼,將這部分數據拷貝至相應RAM位置。
????由于我的設備程序量不斷增加,超過了為設備程序預留的Flash空間,在線編程時,將一部分存儲全局變量和靜態變量初值的Flash給重新編程了。在重啟設備前,初值已經被拷貝到RAM中,所以這個時候程序運行是正常的,但重新上電后,這部分初值實際上是在線編程的數據,自然與初值不同了。
4.3在C代碼中使用的變量,編譯器將他們分配到RAM的哪里?
????我們會在代碼中使用各種變量,比如全局變量、靜態變量、局部變量,并且這些變量時由編譯器統一管理的,有時候我們需要知道變量用掉了多少RAM,以及這些變量在RAM中的具體位置。
????這是一個經常會遇到的事情,舉一個例子,程序中的一個變量在運行時總是不正常的被改變,那么有理由懷疑它臨近的變量或數組溢出了,溢出的數據更改了這個變量值。要排查掉這個可能性,就必須知道該變量被分配到RAM的哪里、這個位置附近是什么變量,以便針對性的做跟蹤。
????其實MDK編譯器的輸出文件中有一個“工程名.map”文件,里面記錄了代碼、變量、堆棧的存儲位置,通過這個文件,可以查看使用的變量被分配到RAM的哪個位置。要生成這個文件,需要在Options for Targer窗口,Listing標簽欄下,勾選Linker Listing前的復選框,如下圖所示。
4.4默認情況下,棧被分配到RAM的哪個地方?
??? MDK中,我們只需要在配置文件中定義堆棧大小,編譯器會自動在RAM的空閑區域選擇一塊合適的地方來分配給我們定義的堆棧,這個地方位于RAM的那個地方呢?
????通過查看MAP文件,原來MDK將堆棧放到程序使用到的RAM空間的后面,比如你的RAM空間從0x4000 0000開始,你的程序用掉了0x200字節RAM,那么堆棧空間就從0x4000 0200處開始。
????使用了多少堆棧,是否溢出?
4.5 有多少RAM會被初始化?
????在進入main()函數之前,MDK會把未初始化的RAM給清零的,我們的RAM可能很大,只使用了其中一小部分,MDK會不會把所有RAM都初始化呢?
????答案是否定的,MDK只是把你的程序用到的RAM以及堆棧RAM給初始化,其它RAM的內容是不管的。如果你要使用絕對地址訪問MDK未初始化的RAM,那就要小心翼翼的了,因為這些RAM上電時的內容很可能是隨機的,每次上電都不同。
4.6 MDK編譯器如何設置非零初始化變量?
????對于控制類產品,當系統復位后(非上電復位),可能要求保持住復位前RAM中的數據,用來快速恢復現場,或者不至于因瞬間復位而重啟現場設備。而keil mdk在默認情況下,任何形式的復位都會將RAM區的非初始化變量數據清零。
??? MDK編譯程序生成的可執行文件中,每個輸出段都最多有三個屬性:RO屬性、RW屬性和ZI屬性。對于一個全局變量或靜態變量,用const修飾符修飾的變量最可能放在RO屬性區,初始化的變量會放在RW屬性區,那么剩下的變量就要放到ZI屬性區了。
????默認情況下,ZI屬性區的數據在每次復位后,程序執行main函數內的代碼之前,由編譯器“自作主張”的初始化為零。所以我們要在C代碼中設置一些變量在復位后不被零初始化,那一定不能任由編譯器“胡作非為”,我們要用一些規則,約束一下編譯器。
????分散加載文件對于連接器來說至關重要,在分散加載文件中,使用UNINIT來修飾一個執行節,可以避免編譯器對該區節的ZI數據進行零初始化。這是要解決非零初始化變量的關鍵。
????因此我們可以定義一個UNINIT修飾的數據節,然后將希望非零初始化的變量放入這個區域中。于是,就有了第一種方法:
- 修改分散加載文件,增加一個名為MYRAM的執行節,該執行節起始地址為0x1000A000,長度為0x2000字節(8KB),由UNINIT修飾:
LR_IROM1 0x00000000 0x00080000 { ; load region size_regionER_IROM1 0x00000000 0x00080000 { ; load address = execution address*.o (RESET, +First)*(InRoot$$Sections).ANY (+RO)}RW_IRAM1 0x10000000 0x0000A000 { ; RW data.ANY (+RW +ZI)}MYRAM 0x1000A000 UNINIT 0x00002000 {.ANY (NO_INIT)}}
????那么,如果在程序中有一個數組,你不想讓它復位后零初始化,就可以這樣來定義變量:
unsigned char plc_eu_backup[32] __attribute__((at(0x1000A000)));
????變量屬性修飾符__attribute__((at(adde)))用來將變量強制定位到adde所在地址處。由于地址0x1000A000開始的8KB區域ZI變量不會被零初始化,所以位于這一區域的數組plc_eu_backup也就不會被零初始化了。? ? ?
????這種方法的缺點是顯而易見的:要程序員手動分配變量的地址。如果非零初始化數據比較多,這將是件難以想象的大工程(以后的維護、增加、修改代碼等等)。所以要找到一種辦法,讓編譯器去自動分配這一區域的變量。
- 分散加載文件同方法1,如果還是定義一個數組,可以用下面方法:
unsigned char plc_eu_backup[32] __attribute__((section("NO_INIT"),zero_init));
????變量屬性修飾符__attribute__((section(“name”),zero_init))用于將變量強制定義到name屬性數據節中,zero_init表示將未初始化的變量放到ZI數據節中。因為“NO_INIT”這顯性命名的自定義節,具有UNINIT屬性。
- 將一個模塊內的非初始化變量都非零初始化
????假如該模塊名字為test.c,修改分散加載文件如下所示:
LR_IROM1 0x00000000 0x00080000 { ; load region size_regionER_IROM1 0x00000000 0x00080000 { ; load address = execution address*.o (RESET, +First)*(InRoot$$Sections)}RW_IRAM1 0x10000000 0x0000A000 { ; RW data.ANY (+RW +ZI)}RW_IRAM2 0x1000A000 UNINIT 0x00002000 {test.o (+ZI)}}
????在該模塊定義時變量時使用如下方法:
????這里,變量屬性修飾符__attribute__((zero_init))用于將未初始化的變量放到ZI數據節中變量,其實MDK默認情況下,未初始化的變量就是放在ZI數據區的。
八、嵌入式操作系統的內存管理算法
主要介紹內存的基本概念以及操作系統的內存管理算法。
1 內存的基本概念
內存是計算機系統中除了處理器以外最重要的資源,用于存儲當前正在執行的程序和數據。內存是相對于CPU來說的,CPU可以直接尋址的存儲空間叫做內存,CPU需要通過驅動才能訪問的叫做外存。
2 ROM&RAM&Flash
內存一般采用半導體存儲單元,分為只讀存儲器(ROM,Read Only Memory)、隨機存儲器(RAM,Random Access Memory)ROM一般只能讀取不能寫入,掉電后其中的數據也不會丟失。RAM既可以從中讀取也可以寫入,但是掉電后其中的數據會丟失。內存一般指的就是RAM。
ROM在嵌入式系統中一般用于存儲BootLoader以及操作系統或者程序代碼或者直接當硬盤使用。近年來閃存(Flash)已經全面代替了ROM在嵌入式系統中的地位,它結合了ROM和RAM的長處,不僅具備電子可擦除可編程的特性,而且斷電也不會丟失數據,同時可以快速讀取數據。
3 兩類內存管理方式
內存管理模塊管理系統的內存資源,它是操作系統的核心模塊之一。主要包括內存的初始化、分配以及釋放。
從分配內存是否連續,可以分為兩大類。
- 連續內存管理
為進程分配的內存空間是連續的,但這種分配方式容易形成內存碎片(碎片是難以利用的空閑內存,通常是小內存),降低內存利用率。連續內存管理主要分為單一連續內存管理和分區式內存管理兩種。
- 非連續內存管理
將進程分散到多個不連續的內存空間中,可以減少內存碎片,內存使用率更高。如果分配的基本單位是頁,則稱為分頁內存管理;如果基本單位是段,則稱為分段內存管理。
當前的操作系統,普遍采用非連續內存管理方式。不過因為分配粒度較大,對于內存較小的嵌入式系統,一般采用連續內存管理。本文主要對嵌入式系統中常用的連續內存管理的分區式內存管理進行介紹。
4 分區式內存管理
分區式內存管理分為固定分區和動態分區。
- 固定分區
事先就把內存劃分為若干個固定大小的區域。分區大小既可以相等也可以不等。固定分區易于實現,但是會造成分區內碎片浪費,而且分區總數固定,限制了可以并發執行的進程數量。 - 動態分區
根據進程的實際需要,動態地給進程分配所需內存。
5 動態分區內存管理
運作機制
動態分區管理一般采用空閑鏈表法,即基于一個雙向鏈表來保存空閑分區。對于初始狀態,整個內存塊都會被作為一個大的空閑分區加入到空閑鏈表中。當進程申請內存時,將會從這個空閑鏈表中找到一個大小滿足要求的空閑分區。如果分區大于所需內存,則從該分區中拆分出需求大小的內存交給進程,并將此拆分出的內存從空閑鏈表中移除,剩下的內存仍然是一個掛在空閑鏈表中的空閑分區。
數據結構
空閑鏈表法有多種數據結構實現,這里介紹一種較為簡單的數據結構。每個空閑分區的數據結構中包含分區的大小,以及指向前一個分區和后一個分區的指針,這樣就能將各個空閑分區鏈接成一個雙向鏈表。
內存分配算法
- First Fit(首次適應算法)
First Fit要求空閑分區鏈表以地址從小到大的順序鏈接。分配內存時,從鏈表的第一個空閑分區開始查找,將最先能夠滿足要求的空閑分區分配給進程。
- Next Fit(循環首次適應算法)
Next Fit由First Fit算法演變而來。分配內存時,從上一次剛分配過的空閑分區的下一個開始查找,直至找到能滿足要求的空閑分區。查找時會采用循環查找的方式,即如果直到鏈表最后一個空閑分區都不能滿足要求,則返回到第一個空閑分區開始查找。
- Best Fit(最佳適應算法)
從所有空閑分區中找出能滿足要求的、且大小最小的空閑分區。為了加快查找速度,Best Fit算法會把所有空閑分區按其容量從小到大的順序鏈接起來,這樣第一次找到的滿足大小要求的內存必然是最小的空閑分區。
- Worst Fit(最壞適應算法)
從所有空閑分區中找出能滿足要求的、且大小最大的空閑分區。Worst Fit算法按其容量從大到小的順序鏈接所有空閑分區。
- Two LevelSegregated Fit(TLSF)
使用兩層鏈表來管理空閑內存,將空閑分區大小進行分類,每一類用一個空閑鏈表表示,其中的空閑內存大小都在某個特定值或者某個范圍內。這樣存在多個空閑鏈表,所以又用一個索引鏈表來管理這些空閑鏈表,該表的每一項都對應一種空閑鏈表,并記錄該類空閑鏈表的表頭指針。
圖中,第一層鏈表將空閑內存塊的大小根據2的冪進行分類。第二層鏈表是具體的每一類空閑內存塊按照一定的范圍進行線性分段。比如25這一類,以23即8分為4個內存區間【25,25+8),【25+8,25+16),【25+16,25+24),【25+24,25+32);216這一類,以214分為4個小區間【216,216+214),【216+214,216+2*214),【216+2*214,216+3*214),【216+3*214,216+4*214)。同時為了快速檢索到空閑塊,每一層鏈表都有一個bitmap用于標記對應的鏈表中是否有空閑塊,比如第一層bitmap后3位010,表示25這一類內存區間有空閑塊。對應的第二層bitmap為0100表示【25+16,25+24)這個區間有空閑塊,即下面的52Byte。
- Buddysystems(伙伴算法)
Segregated Fit算法的變種,具有更好的內存拆分和回收合并效率。伙伴算法有很多種類,比如BinaryBuddies,Fibonacci Buddies等。Binary Buddies是最簡單也是最流行的一種,將所有空閑分區根據分區的大小進行分類,每一類都是具有相同大小的空閑分區的集合,使用一個空閑雙向鏈表表示。BinaryBuddies中所有的內存分區都是2的冪次方。
因為無論是已分配的或是空閑的分區,其大小均為 2 的冪次方,即使進程申請的內存小于分配給它的內存塊,多余的內存也不會再拆分出來給其他進程使用,這樣就容易造成內部碎片。
當進程申請一塊大小為n的內存時的分配步驟為:
1、計算一個i值,使得2i-1<n≤2i
2、在空閑分區大小為2i的空閑鏈表中查找
3、如果找到空閑塊,則分配給進程
4、如果2i的空閑分區已經耗盡,則在分區大小為2i+1的空閑鏈表中查找
5、如果存在2i+1的空閑分區,則將此空閑塊分為相等的兩個分區,這兩分區就是一對伙伴,其中一塊分配給進程,另一塊掛到分區大小為2i的空閑鏈表中
6、如果2i+1的空閑分區還是不存在,則繼續查找大小為2i+2的空閑分區。如果找到,需要進行兩次拆分。第一次拆分為兩塊大小為2i+1的分區,一塊分區掛到大小為2i+1的空閑鏈表中,另一塊分區繼續拆分為兩塊大小為2i的空閑分區,一塊分配給進程,另一塊掛到大小為2i的空閑鏈表中
7、如果2i+2的空閑分區也找不到,則繼續查找2i+3,以此類推
在內存回收時,如果待回收的內存塊與空閑鏈表中的一塊內存互為伙伴,則將它們合并為一塊更大的內存塊,如果合并后的內存塊在空閑鏈表中還有伙伴,則繼續合并到不能合并為止,并將合并后的內存塊掛到對應的空閑鏈表中。
下面的表格對上面6種算法的優缺點進行了比較:
九、詳解STM32單片機的堆棧
?學習STM32單片機的時候,總是能遇到“堆棧”這個概念。分享本文,希望對你理解堆棧有幫助。
????對于了解一點匯編編程的人,就可以知道,堆棧是內存中一段連續的存儲區域,用來保存一些臨時數據。堆棧操作由PUSH、POP兩條指令來完成。而程序內存可以分為幾個區:
- 棧區(stack)
- 堆區(Heap)
- 全局區(static)
- 文字常亮區程序代碼區
????程序編譯之后,全局變量,靜態變量已經分配好內存空間,在函數運行時,程序需要為局部變量分配棧空間,當中斷來時,也需要將函數指針入棧,保護現場,以便于中斷處理完之后再回到之前執行的函數。
????棧是從高到低分配,堆是從低到高分配。
普通單片機與STM32單片機中堆棧的區別
????普通單片機啟動時,不需要用bootloader將代碼從ROM搬移到RAM。
????但是STM32單片機需要。
????這里我們可以先看看單片機程序執行的過程,單片機執行分三個步驟:
- 取指令
- 分析指令
- 執行指令
????根據PC的值從程序存儲器讀出指令,送到指令寄存器。然后分析執行執行。這樣單片機就從內部程序存儲器去代碼指令,從RAM存取相關數據。
??? RAM取數的速度是遠高于ROM的,但是普通單片機因為本身運行頻率不高,所以從ROM取指令慢并不影響。
????而STM32的CPU運行的頻率高,遠大于從ROM讀寫的速度。所以需要用bootloader將代碼從ROM搬移到RAM。
????使用棧就象我們去飯館里吃飯,只管點菜(發出申請)、付錢、和吃(使用),吃飽了就走,不必理會切菜、洗菜等準備工作和洗碗、刷鍋等掃尾工作,他的好處是快捷,但是自由度小。使用堆就象是自己動手做喜歡吃的菜肴,比較麻煩,但是比較符合自己的口味,而且自由度大。
????其實堆棧就是單片機中的一些存儲單元,這些存儲單元被指定保存一些特殊信息,比如地址(保護斷點)和數據(保護現場)。
????如果非要給他加幾個特點的話那就是:
- 這些存儲單元中的內容都是程序執行過程中被中斷打斷時,事故現場的一些相關參數。如果不保存這些參數,單片機執行完中斷函數后就無法回到主程序繼續執行了。
- 這些存儲單元的地址被記在了一個叫做堆棧指針(SP)的地方。
結合STM32的開發講述堆棧
????從上面的描述可以看得出來,在代碼中是如何占用堆和棧的。可能很多人還是無法理解,這里再結合STM32的開發過程中與堆棧相關的內容來進行講述。
????如何設置STM32的堆棧大小?
????在基于MDK的啟動文件開始,有一段匯編代碼是分配堆棧大小的。
???這里重點知道堆棧數值大小就行。還有一段AREA(區域),表示分配一段堆棧數據段。數值大小可以自己修改,也可以使用STM32CubeMX數值大小配置,如下圖所示。
STM32F1默認設置值0x400,也就是1K大小。
Stack_Size EQU 0x400
????函數體內局部變量:
void Fun(void){ char i; int Tmp[256]; //...}
????局部變量總共占用了256*4 + 1字節的棧空間。所以,在函數內有較多局部變量時,就需要注意是否超過我們配置的堆棧大小。
????函數參數:
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init)
????這里要強調一點:傳遞指針只占4字節,如果傳遞的是結構體,就會占用結構大小空間。提示:在函數嵌套,遞歸時,系統仍會占用棧空間。
????堆(Heap)的默認設置0x200(512)字節。
Heap_Size EQU 0x200
????大部分人應該很少使用malloc來分配堆空間。雖然堆上的數據只要程序員不釋放空間就可以一直訪問,但是,如果忘記了釋放堆內存,那么將會造成內存泄漏,甚至致命的潛在錯誤。
MDK中RAM占用大小分析
????經常在線調試的人,可能會分析一些底層的內容。這里結合MDK-ARM來分析一下RAM占用大小的問題。在MDK編譯之后,會有一段RAM大小信息:
??這里4+6=1640,轉換成16進制就是0x668,在進行在調試時,會出現:
??這個MSP就是主堆棧指針,一般我們復位之后指向的位置,復位指向的其實是棧頂:
????而MSP指向地址0x20000668是0x20000000偏移0x668而得來。具體哪些地方占用了RAM,可以參看map文件中【Image Symbol Table】處的內容:
十、STM32F10x中,一些專業術語
GPIO(General Purpose Input Output)是通用輸入/輸出端口;每個GPIO端口可通過軟件分別配置成輸入或輸出;輸出又分為推挽式(Push-Pull)和開漏式(Open-Drain)。
??? USART(Universal Synchronous/Asynchronous Receiver/Transmitter)是通用同步/異步串行接收/發送器,支持全雙工操作;可設置波特率,數據位,停止位,校驗位等。
??? PWM(Pulse Width Modulation)是脈沖寬度調制,簡稱脈寬調制,是利用微處理器的數字輸出來對模擬電路進行控制的一種非常有效的技術。簡單一點,就是對脈沖寬度的控制。
??? OLED(Organic Light-Emitting Diode)即有機發光二極管;具備自發光,不需背光源、對比度高、厚度薄、視角廣、反應速度快、可用于撓曲性面板、使用溫度范圍廣、構造及制程較簡單等優異之特性,被認為是下一代的平面顯示器新興應用技術。LCD都需要背光,而OLED不需要,因為它是自發光的,因此,OLED效果要來得好一些。OLED的尺寸難以大型化,但是分辨率確可以做到很高。
??? TFT-LCD(Thin Film Transistor-Liquid Crystal Display)即薄膜晶體管液晶顯示器;它在液晶顯示屏的每一個象素上都設置有一個薄膜晶體管(TFT),可有效地克服非選通時的串擾,使顯示液晶屏的靜態特性與掃描線數無關,因此大大提高了圖像質量。TFT-LCD也被叫做真彩液晶顯示器。
??? RTC(Real Time Clock)即實時時鐘,是一個獨立的定時器。RTC模塊擁有一組連續計數的計數器,在相應軟件配置下,可提供時鐘日歷的功能。修改計數器的值可以重新設置系統當前的時間和日期。
??? ADC(Analog-to-Digital Converter)指模擬/數字轉換器。是指將連續變量的模擬信號轉換為離散的數字信號的器件。真實世界的模擬信號,例如溫度、壓力、聲音或者圖像等,需要轉換成更容易儲存、處理和發射的數字形式。模/數轉換器可以實現這個功能,在各種不同的產品中都可以找到它的身影
??? DMA(Direct Memory Access)即直接存儲器訪問。DMA傳輸方式無需 CPU直接控制傳輸,也沒有中斷處理方式那樣保留現場和恢復現場的過程,通過硬件為RAM與I/O設備開辟一條直接傳送數據的通路,能使 CPU的效率大為提高。
??? I2C(Inter-Integrated Circuit)即集成電路總線,它用于連接微控制器及其外圍設備。它是由數據線 SDA 和時鐘 SCL 構成的串行總線,可發送和接收數據。
??? SPI(Serial Peripheral Interface)是串行外圍設備接口。SPI接口主要應用在FLASH,EEPROM(Electrically Erasable Programmable Read-Only Memory),RTC(Real Time Clock),ADC(Analog to Digital Converter),還有數字信號處理器和數字信號解碼器之間。SPI,是一種高速的,全雙工,同步的通信總線,并且在芯片的管腳上只占用四根線,節約了芯片的管腳,同時為PCB(Printed Circuit Board)的布局上節省空間,提供方便,正是出于這種簡單易用的特性,現在越來越多的芯片集成了這種通信協議。
??? PS/2是電腦上常見的接口之一,用于鼠標、鍵盤等設備。,PS/2接口的鼠標為綠色,鍵盤為紫色。PS/2接口是輸入裝置接口,而不是傳輸接口。所以PS2口根本沒有傳輸速率的概念,只有掃描速率。在Windows環境下,ps/2鼠標的采樣率默認為60次/秒,USB鼠標的采樣率為120次/秒。較高的采樣率理論上可以提高鼠標的移動精度。
??? USB(Universal Serial BUS)即通用串行總線;它是一個外部總線標準,用于規范電腦與外部設備的連接和通訊。它是應用在PC領域的接口技術。USB接口支持設備的即插即用和熱插拔功能。
??? SD(Secure Digital Memory Card)即安全數碼存儲卡,是一種基于半導體快閃記憶器的新一代記憶設備,它被廣泛地于便攜式裝置上使用,例如數碼相機、多媒體播放器等。
十一、STM32啟動過程
1 概述
說明
????每一款芯片的啟動文件都值得去研究,因為它可是你的程序跑的最初一段路,不可以不知道。通過了解啟動文件,我們可以體會到處理器的架構、指令集、中斷向量安排等內容,是非常值得玩味的。
??? STM32作為一款高端 Cortex-M3系列單片機,有必要了解它的啟動文件。打好基礎,為以后優化程序,寫出高質量的代碼最準備。
????本文以一個實際測試代碼--START_TEST為例進行闡述。
整體過程
??? STM32整個啟動過程是指從上電開始,一直到運行到 main函數之間的這段過程,步驟為(以使用微庫為例):
①上電后硬件設置SP、PC
②設置系統時鐘
③軟件設置SP
④加載.data、.bss,并初始化棧區
⑤跳轉到C文件的main函數
代碼
????啟動過程涉及的文件不僅包含 startup_stm32f10x_hd.s,還涉及到了MDK自帶的連接庫文件 entry.o、entry2.o、entry5.o、entry7.o等(從生成的 map文件可以看出來)。
2 程序在Flash上的存儲結構
????在真正講解啟動過程之前,先要講解程序下載到 Flash上的結構和程序運行時(執行到main函數)時的SRAM數據結構。程序在用戶Flash上的結構如下圖所示。下圖是通過閱讀hex文件和在MDK下調試綜合提煉出來的。
??上圖中:
- MSP初始值由編譯器生成,是主堆棧的初始值。
- 初始化數據段是.data
- 未初始化數據段是.bss
????.data和.bss是在__main里進行初始化的,對于ARM Compiler,__main主要執行以下函數:
??其中__scatterload會對.data和.bss進行初始化。
加載數據段和初始化棧的參數
????加載數據段和初始化棧的參數分別有4個,這里只講解加載數據段的參數,至于初始化棧的參數類似。
0x0800033c Flash上的數據段(初始化數據段和未初始化數據段)起始地址
0x20000000 加載到SRAM上的目的地址
0x0000000c 數據段的總大小
0x080002f4 調用函數_scatterload_copy
????需要說明的是初始化棧的函數--?0x08000304與加載數據段的函數不一樣,為?_scatterload_zeroinit,它的目的就是將棧空間清零。
3 數據在SRAM上的結構
????程序運行時(執行到main函數)時的SRAM數據結構
4 詳細過程分析
????有了以上的基礎,現在詳細分析啟動過程
上電后硬件設置SP、PC
????剛上電復位后,硬件會自動根據向量表偏移地址找到向量表,向量表偏移地址的定義如下:
????調試現象如下:
????看看我們的向量表內容(通過J-Flash打開hex文件)
??硬件這時自動從0x0800 0000位置處讀取數據賦給棧指針SP,然后自動從0x0800 0004位置處讀取數據賦給PC,完成復位,結果為:
SP = 0x02000810
PC = 0x08000145
設置系統時鐘
????上一步中令 PC=0x08000145的地址沒有對齊,硬件自動對齊到?0x08000144,執行 SystemInit函數初始化系統時鐘。
軟件設置SP
LDR R0,=__mainBX R0
????執行上兩條之類,跳轉到?__main程序段運行,注意不是main函數,?___main的地址是0x0800 0130。
????可以看到指令LDR.W sp,[pc,#12],結果SP=0x2000 0810。
加載.data、.bss,并初始化棧區
BL.W __scatterload_rt2
????進入?__scatterload_rt2代碼段。
__scatterload_rt2:
0x080001684C06 LDR r4,[pc,#24] ; @0x08000184
0x0800016A4D07 LDR r5,[pc,#28] ; @0x08000188
0x0800016C E006 B 0x0800017C
0x0800016E68E0 LDR r0,[r4,#0x0C]
0x08000170 F0400301 ORR r3,r0,#0x01
0x08000174 E8940007 LDM r4,{r0-r2}
0x080001784798 BLX r3
0x0800017A3410 ADDS r4,r4,#0x10
0x0800017C42AC CMP r4,r5
0x0800017E D3F6 BCC 0x0800016E
0x08000180 F7FFFFDA BL.W _main_init (0x08000138)
????這段代碼是個循環?(BCC0x0800016e),實際運行時候循環了兩次。第一次運行的時候,讀取“加載數據段的函數?(_scatterload_copy)”的地址并跳轉到該函數處運行(注意加載已初始化數據段和未初始化數據段用的是同一個函數);第二次運行的時候,讀取“初始化棧的函數?(_scatterload_zeroinit)”的地址并跳轉到該函數處運行。相應的代碼如下:
0x0800016E68E0 LDR r0,[r4,#0x0C]
0x08000170 F0400301 ORR r3,r0,#0x01
0x08000174
0x080001784798 BLX r3
????當然執行這兩個函數的時候,還需要傳入參數。至于參數,我們在“加載數據段和初始化棧的參數”環節已經闡述過了。當這兩個函數都執行完后,結果就是“數據在SRAM上的結構”所展示的圖。最后,也把事實加載和初始化的兩個函數代碼奉上如下:
__scatterload_copy:
0x080002F4 E002 B 0x080002FC
0x080002F6 C808 LDM r0!,{r3}
0x080002F81F12 SUBS r2,r2,#4
0x080002FA C108 STM r1!,{r3}
0x080002FC2A00 CMP r2,#0x00
0x080002FE D1FA BNE 0x080002F6
0x080003004770 BX lr
__scatterload_null:
0x080003024770 BX lr
__scatterload_zeroinit:
0x080003042000 MOVS r0,#0x00
0x08000306 E001 B 0x0800030C
0x08000308 C101 STM r1!,{r0}
0x0800030A1F12 SUBS r2,r2,#4
0x0800030C2A00 CMP r2,#0x00
0x0800030E D1FB BNE 0x08000308
0x080003104770 BX lr
跳轉到C文件的main函數
_main_init:
0x080001384800 LDR r0,[pc,#0] ; @0x0800013C
0x0800013A4700 BX r0
5 異常向量與中斷向量表
; VectorTableMapped to Address0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler; ResetHandler
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler; HardFaultHandler
DCD MemManage_Handler; MPU FaultHandler
DCD BusFault_Handler; BusFaultHandler
DCD UsageFault_Handler; UsageFaultHandler
DCD 0; Reserved
DCD 0; Reserved
DCD 0; Reserved
DCD 0; Reserved
DCD SVC_Handler ; SVCallHandler
DCD DebugMon_Handler; DebugMonitorHandler
DCD 0; Reserved
DCD PendSV_Handler; PendSVHandler
DCD SysTick_Handler; SysTickHandler; ExternalInterrupts
DCD WWDG_IRQHandler ; WindowWatchdog
DCD PVD_IRQHandler ; PVD through EXTI Line detect
DCD TAMPER_IRQHandler ; Tamper
DCD RTC_IRQHandler ; RTC
DCD FLASH_IRQHandler ; Flash
DCD RCC_IRQHandler ; RCC
DCD EXTI0_IRQHandler ; EXTI Line0
DCD EXTI1_IRQHandler ; EXTI Line1
DCD EXTI2_IRQHandler ; EXTI Line2
DCD EXTI3_IRQHandler ; EXTI Line3
DCD EXTI4_IRQHandler ; EXTI Line4
DCD DMA1_Channel1_IRQHandler ; DMA1 Channel1
DCD DMA1_Channel2_IRQHandler ; DMA1 Channel2
DCD DMA1_Channel3_IRQHandler ; DMA1 Channel3
DCD DMA1_Channel4_IRQHandler ; DMA1 Channel4
DCD DMA1_Channel5_IRQHandler ; DMA1 Channel5
DCD DMA1_Channel6_IRQHandler ; DMA1 Channel6
DCD DMA1_Channel7_IRQHandler ; DMA1 Channel7
DCD ADC1_2_IRQHandler ; ADC1 & ADC2
DCD USB_HP_CAN1_TX_IRQHandler ; USB HighPriority or CAN1 TX
DCD USB_LP_CAN1_RX0_IRQHandler ; USB LowPriority or CAN1 RX0
DCD CAN1_RX1_IRQHandler ; CAN1 RX1
DCD CAN1_SCE_IRQHandler ; CAN1 SCE
DCD EXTI9_5_IRQHandler ; EXTI Line9..5
DCD TIM1_BRK_IRQHandler ; TIM1 Break
DCD TIM1_UP_IRQHandler ; TIM1 Update
DCD TIM1_TRG_COM_IRQHandler ; TIM1 Trigger and Commutation
DCD TIM1_CC_IRQHandler ; TIM1 CaptureCompare
DCD TIM2_IRQHandler ; TIM2
DCD TIM3_IRQHandler ; TIM3
DCD TIM4_IRQHandler ; TIM4
DCD I2C1_EV_IRQHandler ; I2C1 Event
DCD I2C1_ER_IRQHandler ; I2C1 Error
DCD I2C2_EV_IRQHandler ; I2C2 Event
DCD I2C2_ER_IRQHandler ; I2C2 Error
DCD SPI1_IRQHandler ; SPI1
DCD SPI2_IRQHandler ; SPI2
DCD USART1_IRQHandler ; USART1
DCD USART2_IRQHandler ; USART2
DCD USART3_IRQHandler ; USART3
DCD EXTI15_10_IRQHandler ; EXTI Line15..10
DCD RTCAlarm_IRQHandler; RTC Alarm through EXTI Line
DCD USBWakeUp_IRQHandler; USB Wakeup from suspend
DCD TIM8_BRK_IRQHandler ; TIM8 Break
DCD TIM8_UP_IRQHandler ; TIM8 Update
DCD TIM8_TRG_COM_IRQHandler ; TIM8 Trigger and Commutation
DCD TIM8_CC_IRQHandler ; TIM8 CaptureCompare
DCD ADC3_IRQHandler ; ADC3
DCD FSMC_IRQHandler ; FSMC
DCD SDIO_IRQHandler ; SDIO
DCD TIM5_IRQHandler ; TIM5
DCD SPI3_IRQHandler ; SPI3
DCD UART4_IRQHandler ; UART4
DCD UART5_IRQHandler ; UART5
DCD TIM6_IRQHandler ; TIM6
DCD TIM7_IRQHandler ; TIM7
DCD DMA2_Channel1_IRQHandler ; DMA2 Channel1
DCD DMA2_Channel2_IRQHandler ; DMA2 Channel2
DCD DMA2_Channel3_IRQHandler ; DMA2 Channel3
DCD DMA2_Channel4_5_IRQHandler ; DMA2 Channel4& Channel5
__Vectors_End
??這段代碼就是定義異常向量表,在之前有一個“J-Flash打開hex文件”的圖片跟這個表格是一一對應的。編譯器根據我們定義的函數 Reset_Handler、NMI_Handler等,在連接程序階段將這個向量表填入這些函數的地址。
??? startup_stm32f10x_hd.s內容:
NMI_Handler PROC
EXPORT NMI_Handler [WEAK]
B .
ENDP
? stm32f10x_it.c中內容:
void NMI_Handler(void)
{
}
??在啟動匯編文件中已經定義了函數 NMI_Handler,但是使用了“弱”,它允許我們再重新定義一個 NMI_Handler函數,程序在編譯的時候會將匯編文件中的弱函數“覆蓋掉”--兩個函數的代碼在連接后都存在,只是在中斷向量表中的地址填入的是我們重新定義函數的地址。
6 使用微庫與不使用微庫的區別
?使用微庫就意味著我們不想使用MDK提供的庫函數,而想用自己定義的庫函數,比如說printf函數。那么這一點是怎樣實現的呢?我們以printf函數為例進行說明。
不使用微庫而使用系統庫
????在連接程序時,肯定會把系統中包含printf函數的庫拿來調用參與連接,即代碼段有系統庫的參與。
????在啟動過程中,不使用微庫而使用系統庫在初始化棧的時候,還需要初始化堆(猜測系統庫需要用到堆),而使用微庫則是不需要的。
IF :DEF:__MICROLIBEXPORT __initial_sp
EXPORT __heap_base
EXPORT __heap_limitELSEIMPORT __use_two_region_memory
EXPORT __user_initial_stackheap__user_initial_stackheapLDR R0, = Heap_Mem
LDR R1, =(Stack_Mem+ Stack_Size)
LDR R2, = (Heap_Mem+ Heap_Size)
LDR R3, = Stack_Mem
BX LRALIGNENDIF
????另外,在執行?__main函數的過程中,不僅需要完成“使用微庫”情況下的所有工作,額外的工作還需要進行庫的初始化,才能使用系統庫(這一部分我還沒有深入探討)。附上?__main函數的內容:? ? ? ?
__main:
0x08000130 F000F802 BL.W __scatterload_rt2_thumb_only (0x08000138)
0x08000134 F000F83C BL.W __rt_entry_sh (0x080001B0)
__scatterload_rt2_thumb_only:
0x08000138 A00A ADR r0,{pc}+4; @0x08000164
0x0800013A E8900C00 LDM r0,{r10-r11}
0x0800013E4482 ADD r10,r10,r0
0x080001404483 ADD r11,r11,r0
0x08000142 F1AA0701 SUB r7,r10,#0x01
__scatterload_null:
0x0800014645DA CMP r10,r11
0x08000148 D101 BNE 0x0800014E
0x0800014A F000F831 BL.W __rt_entry_sh (0x080001B0)
0x0800014E F2AF0E09 ADR.W lr,{pc}-0x07; @0x08000147
0x08000152 E8BA000F LDM r10!,{r0-r3}
0x08000156 F0130F01 TST r3,#0x01
0x0800015A BF18 IT NE
0x0800015C1AFB SUBNE r3,r7,r3
0x0800015E F0430301 ORR r3,r3,#0x01
0x080001624718 BX r3
0x080001640298 LSLS r0,r3,#10
0x080001660000 MOVS r0,r0
0x0800016802B8 LSLS r0,r7,#10
0x0800016A0000 MOVS r0,r0
__scatterload_copy:
0x0800016C3A10 SUBS r2,r2,#0x10
0x0800016E BF24 ITT CS
0x08000170 C878 LDMCS r0!,{r3-r6}
0x08000172 C178 STMCS r1!,{r3-r6}
0x08000174 D8FA BHI __scatterload_copy (0x0800016C)
0x080001760752 LSLS r2,r2,#29
0x08000178 BF24 ITT CS
0x0800017A C830 LDMCS r0!,{r4-r5}
0x0800017C C130 STMCS r1!,{r4-r5}
0x0800017E BF44 ITT MI
0x080001806804 LDRMI r4,[r0,#0x00]
0x08000182600C STRMI r4,[r1,#0x00]
0x080001844770 BX lr
0x080001860000 MOVS r0,r0
__scatterload_zeroinit:
0x080001882300 MOVS r3,#0x00
0x0800018A2400 MOVS r4,#0x00
0x0800018C2500 MOVS r5,#0x00
0x0800018E2600 MOVS r6,#0x00
0x080001903A10 SUBS r2,r2,#0x10
0x08000192 BF28 IT CS
0x08000194 C178 STMCS r1!,{r3-r6}
0x08000196 D8FB BHI 0x08000190
0x080001980752 LSLS r2,r2,#29
0x0800019A BF28 IT CS
0x0800019C C130 STMCS r1!,{r4-r5}
0x0800019E BF48 IT MI
0x080001A0600B STRMI r3,[r1,#0x00]
0x080001A24770 BX lr
__rt_lib_init:
0x080001A4 B51F PUSH {r0-r4,lr}
0x080001A6 F3AF8000 NOP.W
__rt_lib_init_user_alloc_1:
0x080001AA BD1F POP {r0-r4,pc}
__rt_lib_shutdown:
0x080001AC B510 PUSH {r4,lr}
__rt_lib_shutdown_user_alloc_1:
0x080001AE BD10 POP {r4,pc}
__rt_entry_sh:
0x080001B0 F000F82F BL.W __user_setup_stackheap (0x08000212)
0x080001B44611 MOV r1,r2
__rt_entry_postsh_1:
0x080001B6 F7FFFFF5 BL.W __rt_lib_init (0x080001A4)
__rt_entry_postli_1:
0x080001BA F000F919 BL.W main (0x080003F0)
使用微庫而不使用系統庫
????在程序連接時,不會把包含printf函數的庫連接到終極目標文件中,而使用我們定義的庫。
????啟動時需要完成的工作就是之前論述的步驟1、2、3、4、5,相比使用系統庫,啟動過程步驟更少。
十二、如何中斷單片機的中斷?
如果外部中斷來的頻率足夠快,上一個中斷沒有處理完成,新來的中斷該如何處理?
????中斷一般是由硬件(例如外設、外部引腳)產生,當某種內部或外部事件發生時,MCU的中斷系統將迫使 CPU 暫停正在執行的程序,轉而去進行中斷事件的處理,中斷處理完畢后,又返回被中斷的程序處,繼續執行下去,所有的Cortex-M 內核系統都有一個用于中斷處理的組件NVIC,主要負責處理中斷,還處理其他需要服務的事件。嵌套向量式中斷控制器(NVIC: Nested Vectored Interrupt Controller)集成在Cortex-M0處理器里,它與處理器內核緊密相連,并且提供了中斷控制功能以及對系統異常的支持。
????處理器中的NVIC能夠處理多個可屏蔽中斷通道和可編程優先級,中斷輸入請求可以是電平觸發,也可以是最小的一個時鐘周期的脈沖信號。每一個外部中斷線都可以獨立的使能、清除或掛起,并且掛起狀態也可以手動地設置和清除。
????主程序正在執行,當遇到中斷請求(Interrupt Request)時,暫停主程序的執行轉而去執行中斷服務例程(Interrupt Service Routine,ISR),稱為響應,中斷服務例程執行完畢后返回到主程序斷點處并繼續執行主程序。多個中斷是可以進行嵌套的。正在執行的較低優先級中斷可以被較高優先級的中斷所打斷,在執行完高級中斷后返回到低級中斷里繼續執行,采用“咬尾中斷”機制。
?內核中斷(異常管理和休眠模式等),其中斷優先級則由SCB寄存器來管理,IRQ的中斷優先級是由NVIC來管理。
????NVIC的寄存器經過了存儲器映射,其寄存器的起始地址為0xE000E100,對其訪問必須是每次32bit。
????SCB寄存器的起始地址:0xE000ED00,也是每次32bit訪問,SCB寄存器主要包含SysTick操作、異常管理和休眠模式控制。
????NVIC具有以下特性:
- 靈活的中斷管理:使能\清除、優先級配置
- 硬件嵌套中斷支持
- 向量化的異常入口
- 中斷屏蔽
1 中斷使能和清除使能
????ARM將處理器的中斷使能設置和清除設置寄存器分在兩個不同的地址,這種設計主要有如下優勢:一方面這種方式減少了使能中斷所需要的步驟,使能一個中斷NVIC只需要訪問一次,同時也減少了程序代碼并且降低了執行時間,另一方面當多個應用程序進程同時訪問寄存器或者在讀寫操作寄存器時有操作其他的中斷使能位,這樣就有可能導致寄存器丟失,設置和清除分成兩個寄存器能夠有效防止控制信號丟失。
?因此我可以獨立的操作每一個中斷的使能和清除設置。
1.1 C代碼
*(volatile unsigned long) (0xE000E100) = 0x4 ; //使能#2中斷
*(volatile unsigned long) (0xE000E180) = 0x4 ; //清除#2中斷
1.2 匯編代碼
__asm void Interrupt_Enable()
{LDR R0, =0xE000E100 ; //ISER寄存器的地址MOVS R1, #04 ; //設置#2中斷STR R1, [R0] ; //使能中斷#2
}__asm void Interrupt_Disable()
{LDR R0, =0xE000E180 ; //ICER寄存器的地址MOVS R1, #04 ; //設置#2中斷STR R1, [R0] ; //使能中斷#2
}
1.3 CMSIS標準設備驅動函數
//使能中斷#IRQn
__STATIC_INLINE void __NVIC_EnableIRQ(IRQn_Type IRQn)
{if ((int32_t)(IRQn) >= 0) {NVIC->ISER[0U] = (uint32_t)(1UL << (((uint32_t)(int32_t)IRQn) & 0x1FUL));}
}
//清除中斷#IRQn
__STATIC_INLINE void __NVIC_DisableIRQ(IRQn_Type IRQn)
{if ((int32_t)(IRQn) >= 0) {NVIC->ICER[0U] = (uint32_t)(1UL << (((uint32_t)(int32_t)IRQn) & 0x1FUL));__DSB();__ISB();}
}
//讀取使能中斷#IRQn
__STATIC_INLINE uint32_t __NVIC_GetEnableIRQ(IRQn_Type IRQn)
{if ((int32_t)(IRQn) >= 0) {return((uint32_t)(((NVIC->ISER[0U] & (1UL << (((uint32_t)(int32_t)IRQn) & 0x1FUL))) != 0UL) ? 1UL : 0UL));}else {return(0U);}
}
2 中斷掛起和清除掛起
????如果一個中斷發生了,卻無法立即處理,這個中斷請求將會被掛起。掛起狀態保存在一個寄存器中,如果處理器的當前優先級還沒有降低到可以處理掛起的請求,并且沒有手動清除掛起狀態,該狀態將會一直保持。
????可以通過操作中斷設置掛起和中斷清除掛起兩個獨立的寄存器來訪問或者修改中斷掛起狀態,中斷掛起寄存器也是通過兩個地址來實現設置和清除相關位。這使得每一個位都可以獨立修改,并且無需擔心在兩個應用程序進程競爭訪問時出現的數據丟失。
??中斷掛起狀態寄存器允許使用軟件來觸發中斷。如果中斷已經使能并且沒有被屏蔽掉,當前還沒有更高優先級的中斷在運行,這時中斷的服務程序就會立即得以執行。
2.1 C代碼
*(volatile unsigned long)(0xE000E100) = 0x4 ; //使能中斷#2
*(volatile unsigned long)(0xE000E200) = 0x4 ; //掛起中斷#2
*(volatile unsigned long)(0xE000E280) = 0x4 ; //清除中斷#2的掛起狀態
2.2 匯編代碼
__asm void Interrupt_Set_Pending()
{LDR R0, =0xE000E100 ; //設置使能中斷寄存器地址MOVS R1, #0x4 ; //中斷#2STR R1, [R0] ; //使能#2中斷LDR R0, =0xE000E200 ; //設置掛起中斷寄存器地址MOVS R1, #0x4 ; //中斷#2STR R1, [R0] ; //掛起#2中斷
}__asm void Interrupt_Clear_Pending()
{LDR R0, =0xE000E100 ; //設置使能中斷寄存器地址MOVS R1, #0x4 ; //中斷#2STR R1, [R0] ; //使能#2中斷LDR R0, =0xE000E280 ; //設置清除中斷掛起寄存器地址MOVS R1, #0x4 ; //中斷#2STR R1, [R0] ; //清除#2的掛起狀態
}
2.3 CMSIS標準設備驅動函數
//設置一個中斷掛起
__STATIC_INLINE void __NVIC_SetPendingIRQ(IRQn_Type IRQn)
{if ((int32_t)(IRQn) >= 0) {NVIC->ISPR[0U] = (uint32_t)(1UL << (((uint32_t)(int32_t)IRQn) & 0x1FUL));}
}//清除中斷掛起
__STATIC_INLINE void __NVIC_ClearPendingIRQ(IRQn_Type IRQn)
{if ((int32_t)(IRQn) >= 0) {NVIC->ICPR[0U] = (uint32_t)(1UL << (((uint32_t)(int32_t)IRQn) & 0x1FUL));}
}//讀取中斷掛起狀態
__STATIC_INLINE uint32_t __NVIC_GetPendingIRQ(IRQn_Type IRQn)
{if ((int32_t)(IRQn) >= 0) {return((uint32_t)(((NVIC->ISPR[0U] & (1UL << (((uint32_t)(int32_t)IRQn) & 0x1FUL))) != 0UL) ? 1UL : 0UL));}else {return(0U);}
}
????NVIC屬于處理器內核部分,因此在MM32 MCU芯片的用戶手冊中只有簡單的提及,沒有重點講述,需要深入了解相關寄存器和功能需要參考《Cortex-M0技術參考手冊》。
十三、幾個實用的嵌入式C程序代碼塊
1 十六進制字符轉整型數字
功能:
????將16進制的字符串轉換為10進制的數字。我是沒有找到相應的庫函數,所以參考網上的代碼自己手動寫了個函數來實現。
????常用的函數有atoi,atol,他們都是將10進制的數字字符串轉換為int或是long類型,所以在有些情況下不適用。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>int c2i(char ch)
{ // 如果是數字,則用數字的ASCII碼減去48, 如果ch = '2' ,則 '2' - 48 = 2 if(isdigit(ch)) return ch - 48; // 如果是字母,但不是A~F,a~f則返回 if( ch < 'A' || (ch > 'F' && ch < 'a') || ch > 'z' ) return -1; // 如果是大寫字母,則用數字的ASCII碼減去55, 如果ch = 'A' ,則 'A' - 55 = 10 // 如果是小寫字母,則用數字的ASCII碼減去87, 如果ch = 'a' ,則 'a' - 87 = 10 if(isalpha(ch)) return isupper(ch) ? ch - 55 : ch - 87; return -1;
} int hex2dec(char *hex)
{ int len; int num = 0; int temp; int bits; int i; char str[64] = {0};if(NULL==hex){printf("input para error \n");return 0;}if(('0'==hex[0])&&(('X'==hex[1])||('x'==hex[1]))){strcpy(str,&hex[2]);}else{strcpy(str,hex);}printf("input num = %s \n",str);// 此例中 str = "1de" 長度為3, hex是main函數傳遞的 len = strlen(str); for (i=0, temp=0; i<len; i++, temp=0) { // 第一次:i=0, *(str + i) = *(str + 0) = '1', 即temp = 1 // 第二次:i=1, *(str + i) = *(str + 1) = 'd', 即temp = 13 // 第三次:i=2, *(str + i) = *(str + 2) = 'd', 即temp = 14 temp = c2i( *(str + i) ); // 總共3位,一個16進制位用 4 bit保存 // 第一次:'1'為最高位,所以temp左移 (len - i -1) * 4 = 2 * 4 = 8 位 // 第二次:'d'為次高位,所以temp左移 (len - i -1) * 4 = 1 * 4 = 4 位 // 第三次:'e'為最低位,所以temp左移 (len - i -1) * 4 = 0 * 4 = 0 位 bits = (len - i - 1) * 4; temp = temp << bits; // 此處也可以用 num += temp;進行累加 num = num | temp; } // 返回結果 return num;
} int main(int argc, char **argv)
{int l_s32Ret = 0;if(2!=argc){printf("=====ERROR!======\n");printf("usage: %s Num \n", argv[0]);printf("eg 1: %s 0x400\n", argv[0]);return 0;}l_s32Ret = hex2dec(argv[1]);printf("value hex = 0x%x \n",l_s32Ret);printf("value dec = %d \n",l_s32Ret);return 0;
}運行結果:
biao@ubuntu:~/test/flash$ ./a.out 0x400
input num = 400
value hex = 0x400
value dec = 1024
biao@ubuntu:~/test/flash$
2 字符串轉整型
????功能:
????將正常輸入的16進制或是10進制的字符串轉換為int數據類型。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>int String2int(char *strChar)
{int len=0;const char *pstrCmp1="0123456789ABCDEF";const char *pstrCmp2="0123456789abcdef";char *pstr=NULL;int uiValue=0;int j=0; unsigned int t=0;int i=0;if(NULL==strChar)return -1;if(0>=(len=strlen((const char *)strChar)))return -1;if(NULL!=(pstr=strstr(strChar,"0x"))||NULL!=(pstr=strstr(strChar,"0X"))){pstr=(char *)strChar+2;if(0>=(len=strlen((const char *)pstr)))return -1;for(i=(len-1);i>=0;i--){if(pstr[i]>'F'){for(t=0;t<strlen((const char *)pstrCmp2);t++){ if(pstrCmp2[t]==pstr[i])uiValue|=(t<<(j++*4));}}else{for(t=0;t<strlen((const char *)pstrCmp1);t++){ if(pstrCmp1[t]==pstr[i])uiValue|=(t<<(j++*4));}}}}else{uiValue=atoi((const char*)strChar);}return uiValue;
}int main(int argc, char **argv)
{int l_s32Ret = 0;if(2!=argc){printf("=====ERROR!======\n");printf("usage: %s Num \n", argv[0]);printf("eg 1: %s 0x400\n", argv[0]);return 0;}l_s32Ret = String2int(argv[1]);printf("value hex = 0x%x \n",l_s32Ret);printf("value dec = %d \n",l_s32Ret);return 0;
}
3 創建文件并填充固定數據
功能:
????創建固定大小的一個文件,并且把這個文件填充為固定的數據。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>//#define FILL_DATA_VALUE 0xff
#define FILL_DATA_VALUE 0x30 //char 0int c2i(char ch)
{ if(isdigit(ch)) return ch - 48; if( ch < 'A' || (ch > 'F' && ch < 'a') || ch > 'z' ) return -1; if(isalpha(ch)) return isupper(ch) ? ch - 55 : ch - 87; return -1;
} int hex2dec(char *hex)
{ int len; int num = 0; int temp; int bits; int i; char str[64] = {0};if(NULL==hex){printf("input para error \n");return 0;}if(('0'==hex[0])&&(('X'==hex[1])||('x'==hex[1]))){strcpy(str,&hex[2]);}else{strcpy(str,hex);}printf("input num = %s \n",str);len = strlen(str); for (i=0, temp=0; i<len; i++, temp=0) { temp = c2i( *(str + i) ); bits = (len - i - 1) * 4; temp = temp << bits; num = num | temp; } return num;
} int main(int argc, char **argv)
{FILE *l_pFile = NULL;int l_s32Rest = 0;unsigned int l_WriteLen = 0;unsigned int l_FileLen = 0;unsigned char TempData[1024] = {FILL_DATA_VALUE};if(3!=argc){printf("usage: %s FileName FileLen \n ", argv[0]);printf("eg: %s ./Outfile.bin 0x400 \n ", argv[0]);return 0;};const char *l_pFileName = argv[1];if(NULL==l_pFileName){printf("input file name is NULL \n");return -1;}if(('0'==argv[2][0])&&(('X'==argv[2][1])||('x'==argv[2][1]))){l_FileLen = hex2dec(argv[2]);}else{l_FileLen = atoi(argv[2]);}printf("Need To Write Data Len %d \n",l_FileLen);printf("Fill Data Vale = 0x%x \n",FILL_DATA_VALUE);for(int i=0;i<1024;i++){TempData[i] = FILL_DATA_VALUE;}l_pFile = fopen(l_pFileName,"w+");if(l_pFile==NULL){printf("open file %s error \n",l_pFileName);return -1;}while(l_WriteLen<l_FileLen){if(l_FileLen<1024){l_s32Rest = fwrite(TempData,1,l_FileLen,l_pFile);}else{l_s32Rest = fwrite(TempData,1,1024,l_pFile);}if(l_s32Rest <= 0){break;};l_WriteLen +=l_s32Rest; }if(NULL!=l_pFile){fclose(l_pFile);l_pFile = NULL;}return 0;}
????運行結果:
biao@ubuntu:~/test/flash$ gcc CreateFile.cpp
biao@ubuntu:~/test/flash$ ls
a.out CreateFile.cpp hex2dec.cpp main.cpp out.bin
biao@ubuntu:~/test/flash$ ./a.out ./out.bin 0x10
input num = 10
Need To Write Data Len 16
Fill Data Vale = 0x30
biao@ubuntu:~/test/flash$ ls
a.out CreateFile.cpp hex2dec.cpp main.cpp out.bin
biao@ubuntu:~/test/flash$ vim out.bin 1 0000000000000000
4 批量處理圖片
功能:
????批處理將圖片前面固定的字節數刪除。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>#define START_READ_POSITION 128
#define PHOTO_START_TIME 83641
//l_s32PhotoTime = 92809;int Cut_file(char * InputFile)
{FILE *l_pFileInput = NULL;FILE *l_pFileOutput = NULL;char l_ars8OutputName[128] = {0};unsigned char l_arru8TempData[1024] = {0};int l_s32Ret = 0;static unsigned int ls_u32Num = 0;if(NULL== InputFile) {goto ERROR;}//sprintf(l_ars8OutputName,"./outfile/_%s",&InputFile[8]);sprintf(l_ars8OutputName,"./outfile/00%d.jpg",ls_u32Num++);//printf("out file name %s \n",l_ars8OutputName);l_pFileInput = fopen(InputFile,"rb+");if(NULL==l_pFileInput){printf("input file open error\n");goto ERROR;}l_pFileOutput = fopen(l_ars8OutputName,"w+");if(NULL==l_pFileOutput){printf("out file open error\n");goto ERROR;}fseek(l_pFileInput,START_READ_POSITION,SEEK_SET);while(!feof(l_pFileInput)){l_s32Ret = fread(l_arru8TempData,1,1024,l_pFileInput);if(l_s32Ret<0){break;}l_s32Ret = fwrite(l_arru8TempData,1,l_s32Ret,l_pFileOutput);if(l_s32Ret<0){break;}}ERROR:if(NULL!=l_pFileOutput){fclose(l_pFileOutput);l_pFileOutput =NULL;};if(NULL !=l_pFileInput);{fclose(l_pFileInput);l_pFileInput =NULL;}
}int main(void)
{char l_arrs8InputName[128] = {0};char l_s8PhotoChannel = 0;int l_s32PhotoTime = 0;l_s8PhotoChannel = 3;l_s32PhotoTime = PHOTO_START_TIME;/**從第一通道開始**/for(int j=1;j<l_s8PhotoChannel;j++){for(int i=l_s32PhotoTime;i<235959;i++){memset(l_arrs8InputName,0,sizeof(l_arrs8InputName));sprintf(l_arrs8InputName,"./image/%dY%06d.jpg",j,i);if(0==access(l_arrs8InputName,F_OK)){printf("%s\n",l_arrs8InputName);Cut_file(l_arrs8InputName); }}}
}
?運行結果:
biao@ubuntu:~/test/photo$ gcc CutFile.cpp
biao@ubuntu:~/test/photo$ ls
a.out CutFile.cpp image outfile
biao@ubuntu:~/test/photo$ ./a.out
./image/1Y083642.jpg
./image/1Y083714.jpg
./image/1Y083747.jpg
./image/1Y083820.jpg
./image/1Y083853.jpg
./image/1Y083925.jpg
./image/1Y084157.jpg
./image/1Y084228.jpg
./image/1Y084301.jpg
./image/1Y084334.jpg
./image/1Y084406.jpg
./image/1Y084439.jpg
./image/1Y084711.jpg
./image/1Y084742.jpg
./image/1Y173524.jpg
./image/1Y173556.jpg
./image/1Y173629.jpg
./image/1Y173702.jpg
./image/1Y173933.jpg
./image/1Y174004.jpg
./image/1Y174244.jpg
./image/1Y174315.jpg
./image/1Y174348.jpg
./image/1Y174420.jpg
./image/1Y174454.jpg
./image/1Y174733.jpg
biao@ubuntu:~/test/photo$ tree
.
├── a.out
├── CutFile.cpp
├── image
│ ├── 1Y083642.jpg
│ ├── 1Y083714.jpg
│ ├── 1Y083747.jpg
│ ├── 1Y083820.jpg
│ ├── 1Y083853.jpg
│ ├── 1Y083925.jpg
│ ├── 1Y084157.jpg
│ ├── 1Y084228.jpg
│ ├── 1Y084301.jpg
│ ├── 1Y084334.jpg
│ ├── 1Y084406.jpg
│ ├── 1Y084439.jpg
│ ├── 1Y084711.jpg
│ ├── 1Y084742.jpg
│ ├── 1Y173524.jpg
│ ├── 1Y173556.jpg
│ ├── 1Y173629.jpg
│ ├── 1Y173702.jpg
│ ├── 1Y173933.jpg
│ ├── 1Y174004.jpg
│ ├── 1Y174244.jpg
│ ├── 1Y174315.jpg
│ ├── 1Y174348.jpg
│ ├── 1Y174420.jpg
│ ├── 1Y174454.jpg
│ └── 1Y174733.jpg
└── outfile├── 000.jpg├── 0010.jpg├── 0011.jpg├── 0012.jpg├── 0013.jpg├── 0014.jpg├── 0015.jpg├── 0016.jpg├── 0017.jpg├── 0018.jpg├── 0019.jpg├── 001.jpg├── 0020.jpg├── 0021.jpg├── 0022.jpg├── 0023.jpg├── 0024.jpg├── 0025.jpg├── 002.jpg├── 003.jpg├── 004.jpg├── 005.jpg├── 006.jpg├── 007.jpg├── 008.jpg└── 009.jpg2 directories, 54 files
biao@ubuntu:~/test/photo$
運行前需要創建兩個目錄,image用來存放需要處理的圖片,outfile用來存放處理過后的文件。這種處理文件批處理方式很暴力,偶爾用用還是可以的。
5 IO控制小程序
????嵌入式設備系統一般為了節省空間,一般都會對系統進行裁剪,所以很多有用的命令都會被刪除。在嵌入式設備中要調試代碼也是比較麻煩的,一般只能看串口打印。現在寫了個小程序,專門用來查看和控制海思Hi3520DV300芯片的IO電平狀態。
#include <stdio.h>
#include <stdlib.h>
#include "hstGpioAL.h"int PrintfInputTips(char *ps8Name)
{printf("=========== error!!! ========\n\n");printf("usage Write: %s GPIO bit value \n", ps8Name);printf("usage Read : %s GPIO bit \n", ps8Name);printf("eg Write 1 to GPIO1_bit02 : %s 1 2 1\n", ps8Name);printf("eg Read GPIO1_bit02 Value : %s 1 2 \n\n", ps8Name);printf("=============BT20==================\n")printf("USB HUB GPIO_0_2 1_UP; 0_Down \n");printf("RESET_HD GPIO_13_0 0_EN; 1_disEN\n");printf("Power_HD GPIO_13_3 1_UP; 0_Down \n");return 0;
}int main(int argc, char **argv)
{if((3!=argc)&&(4!=argc)){PrintfInputTips(argv[0]);return -1;}unsigned char l_u8GPIONum = 0;unsigned char l_u8GPIOBit = 0;unsigned char l_u8SetValue = 0;GPIO_GROUP_E l_eGpioGroup;GPIO_BIT_E l_eBit;GPIO_DATA_E l_eData;l_u8GPIONum = atoi(argv[1]);l_u8GPIOBit = atoi(argv[2]);if(l_u8GPIONum<14){l_eGpioGroup = (GPIO_GROUP_E)l_u8GPIONum;}else{printf("l_u8GPIONum error l_u8GPIONum = %d\n",l_u8GPIONum);return -1;};if(l_u8GPIOBit<8){l_eBit = (GPIO_BIT_E)l_u8GPIOBit;}else{printf("l_u8GPIOBit error l_u8GPIOBit = %d\n",l_u8GPIOBit);return -1;}if(NULL!=argv[3]){l_u8SetValue = atoi(argv[3]);if(0==l_u8SetValue){l_eData = (GPIO_DATA_E)l_u8SetValue;}else if(1==l_u8SetValue){l_eData = (GPIO_DATA_E)l_u8SetValue;}else{printf("l_u8SetValue error l_u8SetValue = %d\n",l_u8SetValue);}}if(3==argc) {/**read**/ printf("read GPIO%d Bit%d \n",l_u8GPIONum,l_u8GPIOBit); /**set input**/ HstGpio_Set_Direction(l_eGpioGroup, l_eBit, GPIO_INPUT); /**read **/ char l_s8bit_val = 0; HstGpio_Get_Value(l_eGpioGroup, l_eBit, &l_s8bit_val); printf("read Data = %d \n",l_s8bit_val); }else if(4==argc) {/**write**/ printf("Write GPIO %d; Bit %d; Value %d\n",l_u8GPIONum,l_u8GPIOBit,l_u8SetValue); /***set IO output*/ HstGpio_Set_Direction(l_eGpioGroup, l_eBit, GPIO_OUPUT); /**Write To IO**/ HstGpio_Set_Value(l_eGpioGroup,l_eBit,l_eData);}else { }return 0;}
6 文件固定位置插入數據
????在文件的固定位置插入固定的數據。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#define BASIC_FILE_NAME "./nandflash.bin"
#define UBOOT_FILE_NAME "./u-boot.bin"
#define KERNEL_FILE_NAME "./kernel.bin"
#define ROOTFS_FILE_NAME "./rootfs.bin"
#define APP_FILE_NAME "./app.bin"#define UBOOT_POSITION 0x00
#define KERNEL_POSITION 0x100000
#define ROOTFS_POSITION 0x500000
#define APP_POSITION 0x2700000int InsertData(FILE *pfBasic,FILE *psInsert,int s32Position)
{int l_S32Ret = 0;unsigned char l_arru8Temp[1024] = {0xff};fseek(pfBasic,s32Position,SEEK_SET);fseek(psInsert,0,SEEK_SET);while(1){l_S32Ret = fread(l_arru8Temp,1,1024,psInsert);if(l_S32Ret > 0){l_S32Ret = fwrite(l_arru8Temp,1,l_S32Ret,pfBasic);if(l_S32Ret<=0){printf("line %d error l_S32Ret = %d \n",__LINE__,l_S32Ret);return -1;}}else{break;}}return 0;
}int main(void)
{int l_s32Ret = 0;FILE *l_pfBasec = NULL;FILE *l_pfUboot = NULL;FILE *l_pfKernel = NULL;FILE *l_pfRootfs = NULL;FILE *l_pfApp = NULL;l_pfBasec = fopen(BASIC_FILE_NAME,"r+");if(NULL==l_pfBasec){printf("line %d error \n",__LINE__);goto ERROR;}l_pfUboot = fopen(UBOOT_FILE_NAME,"r");if(NULL==l_pfUboot){printf("line %d error \n",__LINE__);goto ERROR;}l_pfKernel = fopen(KERNEL_FILE_NAME,"r");if(NULL==l_pfKernel){printf("line %d error \n",__LINE__);goto ERROR;}l_pfRootfs = fopen(ROOTFS_FILE_NAME,"r");if(NULL==l_pfRootfs){printf("line %d error \n",__LINE__);goto ERROR;}l_pfApp = fopen(APP_FILE_NAME,"r");if(NULL==l_pfApp){printf("line %d error \n",__LINE__);goto ERROR;}if(0> InsertData(l_pfBasec,l_pfUboot,UBOOT_POSITION)){printf("line %d error \n",__LINE__);goto ERROR;}if(0> InsertData(l_pfBasec,l_pfKernel,KERNEL_POSITION)){printf("line %d error \n",__LINE__);goto ERROR;}if(0> InsertData(l_pfBasec,l_pfRootfs,ROOTFS_POSITION)){printf("line %d error \n",__LINE__);goto ERROR;}if(0> InsertData(l_pfBasec,l_pfApp,APP_POSITION)){printf("line %d error \n",__LINE__);goto ERROR;}ERROR:if(NULL!=l_pfBasec){fclose(l_pfBasec);l_pfBasec = NULL;}if(NULL!=l_pfUboot){fclose(l_pfUboot);l_pfUboot = NULL;}if(NULL!=l_pfKernel){fclose(l_pfKernel);l_pfKernel = NULL;}if(NULL!=l_pfRootfs){fclose(l_pfRootfs);l_pfRootfs = NULL;}if(NULL!=l_pfApp){fclose(l_pfApp);l_pfApp = NULL;}return 0;
}
7 獲取本地IP地址
????在linux設備中獲取本地IP地址可以使用下面的程序,支持最大主機有三個網口的設備,當然這個網卡數可以修改。
#include <stdio.h>
#include <ifaddrs.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>int get_local_ip(char *ps8IpList)
{struct ifaddrs *ifAddrStruct;char l_s8IpAddr[INET_ADDRSTRLEN];void *tmpAddrPtr;int l_s32IPCount = 0;getifaddrs(&ifAddrStruct);while (ifAddrStruct != NULL) {if (ifAddrStruct->ifa_addr->sa_family==AF_INET){tmpAddrPtr=&((struct sockaddr_in *)ifAddrStruct->ifa_addr)->sin_addr;inet_ntop(AF_INET, tmpAddrPtr, l_s8IpAddr, INET_ADDRSTRLEN);if (strcmp(l_s8IpAddr, "127.0.0.1") != 0) {if(l_s32IPCount == 0){memcpy(ps8IpList, l_s8IpAddr, INET_ADDRSTRLEN);} else {memcpy(ps8IpList+INET_ADDRSTRLEN, l_s8IpAddr, INET_ADDRSTRLEN);}l_s32IPCount++;}}ifAddrStruct=ifAddrStruct->ifa_next;}freeifaddrs(ifAddrStruct);return l_s32IPCount;
}int main()
{char l_arrs8IpAddrList[3][INET_ADDRSTRLEN];int l_s32AddrCount;memset(l_arrs8IpAddrList, 0, sizeof(l_arrs8IpAddrList));l_s32AddrCount = get_local_ip(*l_arrs8IpAddrList);for(l_s32AddrCount;l_s32AddrCount>0;l_s32AddrCount--){printf("Server Local IP%d: %s\n",l_s32AddrCount,l_arrs8IpAddrList[l_s32AddrCount-1]);}return 0;
}