1. 程式人生 > 其它 >[C語言低能兒]函式封裝的一些心得

[C語言低能兒]函式封裝的一些心得

本人也就兩年不到的碼齡,迄今為止主要接觸的程式語言也就C和python兩種,也就只是個菜雞,所以這篇文章也就只能算是心得分享而配不上叫什麼教程了。當然我還是要老著麵皮把這篇文章寫出來,不怕大佬的嘲笑,只是做一個記錄同時稍微給比我更菜的菜雞一些引導和啟示吧。

封裝的要點

一個好的底層封裝可以讓你在頂層呼叫的時候倍感舒適,同時也能很簡單的移植到其他工程
相反的,如果封裝的欠妥當的話不管是頂層呼叫還是可移植性上都會有很大的問題。

這裡是我總結下來的幾個要點:

  • 函式、型別命名可讀性強
  • 介面要簡單易用
  • 儘可能的降低與其他模組的耦合程度

精髓就是:多用結構體,多用static

結合實際來設計一個封裝

拿出一個栗子

就拿一輛小車的控制程式碼為例好了,從車的角度拆分來看,我們的小車上有些什麼呢:

  • 電機
  • 編碼器
  • 電機驅動
  • 螢幕、蜂鳴器等可能的會與外界互動的器件
  • 姿態感測器、紅外等感測器

封裝設計

抽絲剝繭

個人認為在封裝程式碼的時候最好是要從上而下去寫,也就是先寫頂層呼叫,把函式的使用方式寫出來,考慮可移植性和呼叫方便程度,之後再考慮底層實現。當然這也少不了會有多層的呼叫。

那麼,對於控制一輛車的運動,程式碼部分應該怎麼實現呢?

前進後退,轉向之類的也是必要的:

void CarMoveForward(void);
void CarTurnLeft(void);
void CarTurnRight(void);

這麼寫好像有些欠妥當?以多少速度前進?轉向是轉多少度?轉向完成是不是應該返回些什麼?
於是加入補充:

void CarMoveForward(float speed);
uint8_t CarTurnLeft(float angle);
uint8_t CarTurnRight(float angle);

但是這個函式怎麼去控制這倆車呢?函式怎麼知道這輛車左輪對應的是哪幾個控制IO口呢
當然會有人說這些東西放在函式內部就夠,但是從移植性上來講,下次搭的車可能就不是那個引腳了,這樣就得跑到封裝庫裡面去修改庫的內容,這樣是非常不好的。
因此,這裡我們需要引入一個結構體來描述這輛車的屬性。

struct CAR_s
{
	struct Wheel_s leftwheel;//對應左輪的結構體
	struct Wheel_s rightwheel;
	struct IMU_s imu;//對應姿態感測器的結構體
	struct PID_s anglepid;//轉向會用到的角度環
	//當然還不止這麼點東西
};
void CarMoveForward(struct CAR_s *car,float speed);
uint8_t CarTurn(struct CAR_s *car,float angle);//注意到angle可以有正負那麼就將兩個函式合併了

通過這樣改進我們呼叫的時候就可以將結構體引數傳入,函式內部訪問結構體的引數來控制小車,如果需要修改引腳之類的話則將結構體裡面的變數值修改即可。

當然,一輛車還是太大了,因此我在這裡將其分解成了好幾個部分:兩個驅動的輪子,一個姿態感測器,後續還可以繼續補充。

頂層的函式設計姑且算是完成了,那麼我們就可以著手進入內部了,我們來想該怎麼實現CarTurn這個函式:

void CarTurn(struct CAR_s *car,float angle)
{
	static uint8_t turnflag = 0;
	static float tarangle;
	float curangle = IMUgetyaw(car->imu);
	if(!turnflag)
	{
		tarangle = curangle + angle;//這裡其實是不能直接做加法的,因為yaw的範圍在-180~180,具體怎麼解決自己想去
		turnflag=1;
	}
	float speedout = PIDCalc(&car->anglepid,tarangle,curangle);

	/*通過簡單的兩輪反轉來實現原地轉向*/
	MotorCtrl(&car->leftwheel,speedout);
	MotorCtrl(&car->rightwheel,-speedout);

	if(tarangle-curangle<1&&tarangle-curangle>-1)//這裡的簡單加減也同樣會出問題
	{
		turnflag = 0;
		return 1;//達成了條件則算轉向完畢,同時返回1讓上層函式不要再繼續呼叫該函式
	}
	else
		return 0;
}

可以發現有了結構體之後會使得這個函式變得更加簡潔,從邏輯性和可讀性上來講也是非常好的。同時裡面也留了很多坑:比如再底層的控制電機以及pid,還有imu的讀取

那麼繼續唄,這裡以6612驅動為例

struct Wheel_s
{
	/* 兩個控制方向的引腳 */
	GPIO_TypeDef A0Port;
	uint16_t A0Pin;
	GPIO_TypeDef A1Port;
	uint16_t A1Pin;

	uint32_t *timccr;
	uint32_t *timarr;

	/*pid速度環*/
	struct PID_s speedpid;

	/*編碼器部分*/
	int32_t speedraw;//速度的原始資料
	int32_t posraw;//位置的原始資料
	uint32_t num;//編碼器線數
	struct Encoder_s encoder;
};
/**
 * @brief 控制電機速度
 * @param w 輪子的結構體
 * @param s 目標速度
 */
void MotorCtrl(Wheel_s *w,float s)
{
	w->speedraw = encoderGet(w->encoder);
	float curspeed = (float)(w->speedraw/w->num);
	float pidout = PIDCalc(w->speedpid,s,curspeed);
	if(pidout>=0)
	{
		HAL_GPIO_WritePin(w->A0Port,w->A0Pin,1);
		HAL_GPIO_WritePin(w->A1Port,w->A1Pin,0);
		*w->timccr = *w->timarr*pidout/100;
	}
	else
	{
		HAL_GPIO_WritePin(w->A0Port,w->A0Pin,0);
		HAL_GPIO_WritePin(w->A1Port,w->A1Pin,1);
		*w->timccr = *w->timarr*(-pidout)/100;
	}
}

到這裡終於觸及到控制的底層了,只不過關於pid的計算其實還是沒有展示出來,本篇也只是給個思路,所以再往下的程式碼就不放出來了(純粹是我不想寫