1. 程式人生 > >KEIL / MDK C++程式設計例項說明:發掘C++的嵌入式開發活力

KEIL / MDK C++程式設計例項說明:發掘C++的嵌入式開發活力

眾所周知,KEIL / MDK是支援C++程式設計方式的。但是就目前來說,使用C++開發嵌入式的程式設計師還是比較少,就我個人認為原因是一方面KEIL / MDK對C++程式的支援還不夠全面,另一方面則是C++程式的體量相較於C程式過於龐大,對於小型的應用來說沒有必要,而且嵌入式開發程式設計師通常C++功底並不高,C才是他們的拿手好戲。但我認為隨著MCU效能的逐漸提升,嵌入式C++的開發潛力將會越來越多的被髮掘。並且C++的標準還在更新,總體來說C++的活力是高於C的。當然,最重要的一點則是C++相容了C,有人甚至說只要C不死,C++就不會死!我認為這是很有道理的。

作為一個嵌入式和智慧終端都有一點接觸的程式設計師來說,我當然是更希望我的程式碼能夠更容易地移植。那麼使用C++程式設計的方式無疑就要合理一些。最近兩天剛剛忙完期末測評,突然興起就寫了這篇博文,那麼接下來我就以自己在KEIL / MDK上建的一個STM32F1的C++模板工程為例,聊一聊C++程式設計的相關事項,如果有錯誤或者不足請各位勿怪,也歡迎各位留言和我交流。

準備C++模板工程:

首先我們要新建一個KEIL / MDK工程,如果沒建過的話就花點時間百度教程吧,如果不想多費力氣那也OK,直接把一個C工程的main.c替換為main.cpp即可。工程的配置均保持原樣無需改變。但此時編譯的這個工程應該會出一些錯誤,當檔案字尾是C的時候IDE會使用C編譯器進行編譯,如果檔案字尾是CPP則IDE使用C++編譯器進行編譯,工程包含的標頭檔案是使用C++編譯器進行編譯的,不過標頭檔案宣告的還是C檔案的符號,所以IDE會無法正確編譯連結。此時我們應該將標頭檔案所有宣告C符號的部分用預編譯巨集加extern "C" { }的形式包含起來,告訴編譯器該段要使用C編譯器進行編譯。具體形式如下:

#ifdef __cplusplus
 extern "C" {
#endif

... your C declared part ...

#ifdef __cplusplus
	}
#endif
當使用STM32標準庫編寫程式的時候會發現它所有的標頭檔案都是這樣處理過的。也就是說庫函式完全相容C++。這也是我們開發C++程式的強大後盾。在準備了基本工程後就可以在main.cpp檔案中使用C++程式設計了,但還應該注意的是,C編譯器不能直接引進CPP檔案的符號,同理C++編譯器也不能直接引進C檔案的符號。當C檔案要extern 一個CPP檔案的符號時,這個CPP檔案的符號定義應該用extern "C" { }進行修飾,否則不能通過編譯連結。同樣地,當CPP檔案要引進一個C檔案的符號時也要在CPP檔案中使用extern "C" { }進行修飾。另外匯編啟動檔案的[WEAK]宣告僅對C檔案符號有效,所以我們編寫外設中斷服務方法時應該寫在C檔案中,或者在CPP檔案中使用exetrn "C" { }修飾符。

我建的C++模板工程檔案目錄如下:

其中serial.c和retarget.c都是在KEIL / MDK的安裝目錄下複製過來的,這兩個檔案提供了對C標準庫和C++標準庫的部分支援。新增上述檔案到工程後我們就能正常使用C++的std標準庫進行程式設計了。關於serial.c和retarget.c我們需要知道以下一些知識:

KEIL / MDK不支援semihosting,要正常使用std標準庫就需要先關閉semihosting,即使用預編譯指令如下:

#pragma import(__use_no_semihosting_swi)
關閉semihosting後需要對部分標準庫方法進行重定向編寫。而retarget.c裡面就實現了相關的方法。

在serial.c中定義了對串列埠的初始化以及傳送和接收的方法。為了讓串列埠能在執行系統入口方法前被初始化而不是在使用者main.c中初始化。檔案定義瞭如下方法進行對串列埠的提前初始化。

/*----------------------------------------------------------------------------
  Superclass to initialize the serial interface
 *----------------------------------------------------------------------------*/
/* 引進原始__rt_entry方法 */
extern void $Super$$__rt_entry(void);

/* 定義新__rt_entry方法 */
void $Sub$$__rt_entry(void)  {
  SER_Init();
/* 呼叫原始__rt_entry方法; */
  $Super$$__rt_entry();
}
通過$super$$和$sub$$兩個編譯器指令的結合使用,將串列埠初始化方法“填”進了系統的入口方法中。其中SER_Init()方法初始化了USART1。我們也無需再在main.c中初始化串列埠。另外標準庫需要使用者提供堆記憶體進行支援。STM32的啟動檔案中已經定義了使用者堆。所以我們無需再定義,但預設的堆容量對於標準庫來說還是太過於小,要手動修改堆的尺寸如下:
Heap_Size       EQU     0x00001000	; Extend for using C++ std lib
還有需要注意的是,KEIL / MDK 的C++編譯器預設失能異常捕獲機制,如果你的程式碼中用到了該機制,那麼就需要在工程配置的"Options for Target - C/C++ - Misc controls"選項中新增'--exceptions'選項。新增如下圖所示:



C++模板工程測試:

弄完上面的這些步驟之後,這個模板工程就可以隨心所欲地進行C++程式編寫了,下面我們編寫一點簡單程式碼進行測試標準庫,程式碼如下:

#include <string>
#include <iostream>
#include "ledx.h"
#include "tick.h"


int main(void) {
	
	LED_Init();		// 初始化LED
	Tick_Init();		// 初始化SYSTICK
	
	std::string str("the float number is ");
	float number = 0;
	
	while (1) {
		std::cout << str << number++ << std::endl;
		
		led1_on();
		led2_on();
		delay_ms(500);
		led1_off();
		led2_off();
		delay_ms(500);
	}
}

燒錄執行並輸出到PC串列埠助手的內容如下:


可以看到執行結果是正確的。但實際情況卻是幾乎沒人願意這麼用在嵌入式開發中,原因也很簡單。


這是上面那段測試程式編譯連結後的輸出視窗,勤儉持家的嵌入式程式設計師看到這個估計要炸鍋了吧!這點程式碼用純C寫最多佔6KB FLASH和極少的RAM資源,但引入C++標準庫後居然要佔用52KB FLASH和27KB RAM,這不太科學啊!可能習慣編寫終端的高富帥程式設計師看了會不以為然:這麼點都不捨得花,怎麼能過上有品質的生活啊!但我們的嵌入式程式設計師卻表示:有錢也不能這樣花啊,我們的每一分錢都要花得值當,再說我們的積蓄哪裡有這麼多!(這才是原因,哈哈尷尬!)

優化C++程式:

當然以上只是玩笑話,不過作為一個優秀的嵌入式開發程式設計師,程式碼優化永遠都是重中之重。那麼我們為了勤儉節約一把,自然是不能濫用標準庫的,不過得益於C++的優越性,我們可以輕鬆地編寫自己的功能類用於處理一般性的問題,就拿以上測試程式為例,不就是需要一個類似cout輸出形式的類嘛!這還不容易!然後我們就開始簡單地編寫一個類用於實現上述功能。

下面是這個類的標頭檔案部分:

#include "stdio.h"
#include "stdarg.h"
#include "string.h"

// NAME SPACE OUT DEFINE
namespace cchar {
	
	// CONST TYPE VARIABLE
	const char tint = 'd';
	const char tuint = 'u';
	const char tchar = 'c';
	const char tftp = 'f';
	
	// CONST ENTER VARIABLE 
	const char endl = '\n';
}
using namespace cchar;

// STRING OUTPUT CLASS
class ostring {
public:
	ostring& operator << (const char* str);
	ostring& operator << (int num);
	ostring& operator << (unsigned int num);
	ostring& operator << (short num);
	ostring& operator << (unsigned short num);
	ostring& operator << (char num);
	ostring& operator << (unsigned char num);
	ostring& operator << (float num);
	ostring& operator << (double num);
private:
	void put_string(const char* str, unsigned int cnt);
	void put_number(char type, ...);
};
我們聲明瞭一個ostring類,類裡面啥資料成員都木有,只過載不同資料型別的運算子"<<"即可,私有的兩個成員函式負責整合字串並向串列埠傳送。

下面是ostring類的實現檔案:

#include "ostr.hpp"


extern "C" { extern int put_char(int c);}
// OUT STRING CLASS BASE PRINT FUNCTION
void ostring::put_string(const char* str, unsigned int cnt) {
	for (unsigned int index = 0; index < cnt; index++) {
		put_char(str[index]);
	}
}

// STRING OUTPUT CLASS PUT NUMBER FUNCTION
void ostring::put_number(char type, ...) {
	va_list arg;
	char temp[20];
	unsigned char cnt = 0;
	
	va_start(arg, type);
	switch (type) {
		case tint:
			cnt = sprintf(temp, "%d", va_arg(arg, int));
			break;
		case tuint:
			cnt = sprintf(temp, "%d", va_arg(arg, unsigned int));
			break;
		case tchar:
			cnt = sprintf(temp, "%c", va_arg(arg, int));
			break;
		case tftp:
			cnt = sprintf(temp, "%f", va_arg(arg, double));
			break;
	}
	va_end(arg);
	put_string(temp, cnt);
}

// STRING OUTPUT CLASS OPERATOR FUNCTION
ostring& ostring::operator << (const char* str) {
	put_string(str, strlen(str));
	return *this;
}
ostring& ostring::operator << (int num) {
	put_number(tint, num);
	return *this;
}
ostring& ostring::operator << (unsigned int num) {
	put_number(tuint, num);
	return *this;
}
ostring& ostring::operator << (short num) {
	put_number(tint, num);
	return *this;
}
ostring& ostring::operator << (unsigned short num) {
	put_number(tuint, num);
	return *this;
}
ostring& ostring::operator << (char num) {
	put_number(tchar, num);
	return *this;
}
ostring& ostring::operator << (unsigned char num) {
	put_number(tuint, num);
	return *this;
}
ostring& ostring::operator << (float num) {
	put_number(tftp, num);
	return *this;
}
ostring& ostring::operator << (double num) {
	put_number(tftp, num);
	return *this;
}

這個CPP檔案引進了傳送單個字元到串列埠的方法put_char,put_string方法簡單地將一個字串依次傳送出去,而put_number方法則是使用C標準庫的可變引數對資料型別進行分類,再整合到字串中再依次發出。這裡為了節約開銷,我沒有用模板函式(試過的,開銷顯然比這個大)。把這兩個檔案新增進工程後再執行剛剛的測試程式,

首先將測試程式修改成如下:

#include "ledx.h"
#include "tick.h"
#include "ostr.hpp"


int main(void) {
	
	LED_Init();		// 初始化LED
	Tick_Init();		// 初始化SYSTICK

	ostring cout;
	float number = 0;
	
	while (1) {
		cout << "the float number is " << number++ << endl;
		
		led1_on();
		led2_on();
		delay_ms(500);
		led1_off();
		led2_off();
		delay_ms(500);
	}
}

哈哈!現在我們也能像標準庫那樣輸出了!再來看看這個程式的開銷如何



可以看出來吧?僅僅用了7KB不到的FLASH和6KB RAM資源,而且我們還沒有將劃分給標準庫的那部分RAM回收,所以RAM實際上消耗是很小的。最後執行一下這個程式,其輸出到串列埠助手的截圖如下:


結果依然是正確的,和標準輸出唯一不同的是我沒有控制輸出精度。

C++模板工程對作業系統的支援:

在寫這篇博文之前我也驗證了在C++模板工程上執行RTOS的可行性。結果當然是可行的,而且不會出現任何問題。這正是得益於C++對C的相容性。也就是說,C++具備在嵌入式開發的一切條件,真的是隻欠東風(程式設計師)!另外,我還在C++模板工程上成功運行了自己這學期用C寫的一個RTOS。我把它叫做REGINA,在上一篇博文裡簡短地介紹了一下,有興趣的朋友不妨去下載下來使用看看,它是一個免費的自由軟體,而它的體量已經被我精簡地非常不錯了。博文地址是http://blog.csdn.net/hlld__/article/details/78865260。不過如果有興趣的你看到那篇博文後可能會感覺很奇怪,原諒我是用較官方性的語言來寫的,這樣能營造一些正式的氣氛!哈哈!

通過這個簡單的例項,我們還模仿了用於標準輸出的類,並實現了相關的功能。在實際嵌入式開發中為了節約體量還會幹很多這樣的事情,但就像我前面說的那樣,C++給予了程式設計師這樣的便利,僅僅很小的工作量就能實現我們想要的功能。而且相較與C程式而言,C++程式的可移植性、程式碼的可擴充套件性顯然更高。更為重要的是C++處理大批次資料的能力及實現複雜邏輯的困難程度都要優於C程式,我相信隨著MCU的運算能力不斷增強,使用C++開發的程式設計師將會越來越多,而嵌入式C++的開發潛力也將更多地體現出來。