[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的计算其实还是没有展示出来,本篇也只是给个思路,所以再往下的代码就不放出来了(纯粹是我不想写

posted @ 2022-04-18 14:29  Foriver  阅读(151)  评论(0编辑  收藏  举报