一位嵌入式工程师,硬核单片机编程思想_嵌入式高级工程师和单片机

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

对于一个显示器,我们必须要有显示要求,我们才会去定制它,如果连使用要求都提不出来,就不要去让人为你做显示器。所以我们首先要明确我们要的显示器必须要做什么。由于是单片机控制的显示器,我们不能想象成微机显示器那样,一个大的显存,可以显示多少页,显示多少色,满屏满屏的传递数据,如果这样想了,就是犯了盲目例比的错误,说明对问题没研究透。对于单片机控制的显示器,我们考虑能显示单个字符、单行显示,就基本足够了。所以我们可以定义下列两个对象功能:

dispShowAChar();//显示一个字符
dispShowALine();//显示一行字符

由于是单片机的裸系统,所以我们作为一个软件设计者,我们一定要清楚,我们所面对的显示器,经常是没有CPU的,所以我们一定要明白,我们这两个函数,实质上都做些什么。很显然,这两个函数是不能长期占有CPU的,否则我们的程序将什么都不能做,专去显示了,成了显示器的一个处理芯片,所以这两个函数运行完后是肯定要退出来的,而显示不能中断呀,所以必须要有一个代码段一直存在于活动代码中而且不能影响其他的功能。做过上位机程序的人应该能看出来,这段代码就是线程。裸编程中我们也用这个概念。

我们的显示器对象正需要一个一直活动的线程,来完成单片机系统对显示功能的解释和执行,因此dispShowAChar()dispShowALine()实质上是不能直接去做显示工作的,它俩最合适的工作,就是去按指定的格式去设置显示内容,这样我们在使用的时候就不必在这两个函数里设置复杂的代码和嵌套调用关系,因为那样一定会浪费很多的代码,调用多了也会让单片机运行效率降低,硬件资源消耗增加,严重的可能会造成堆栈溢出最后还不晓得为什么。让我们也为这个活动线程也先命个名吧:

dispMainThread();//按指定的要求执行显示功能
//指定的要求包括颜色信息、闪烁、游动等等

程序分析下去,引出的概念也就会越来越多,这里所说的多线程概念以后有机会再说,单片机里的多线程也是一个复杂罗嗦的处理问题,现在介绍还为时过早。只是我感觉一不小心又说长了,具体下文继续展开。

四、展开思想

对于对象能力的定义,我们一般可以从重要的入手,然后慢慢地展开,把所需要的其他能力逐渐归纳为函数,从而把面向对象的思想发展下去。上文我们提到了三个函数是怎么来的,还没有涉及到函数的任何实质,那么本帖就探讨一下这三个函数的实质性规划与设计。

有了功能要求,我们就要实现它,在裸程序中,实现它的一个首要任务,就是要进行数据传递方式的设计。很显然我们必须要有一个显示区域,来存放我们所要显示的内容,以及显示内容的显示属性,我们还要规划这个显示区域到底要显示多少多少字符或者是点阵。但是由于我们事先并不知道我们的显示设备一次会提供多少显示量,所以我们可以把显示区域的内存,也就是显存,定义得大一点,以至任何一款符合设计要求的显示器都能得到满足,这样的做法在裸编程中其实还是比较实用的,因为裸编程中我们很少去申请动态的空间,程序设计完,所有的变量位置皆已确定,行就行,不行编译就过不去,所以我们可以通常选择一些内存资源比较丰富的新款单片机。

但是这样的做法也有一个弊端,比如当我们预先估计不足而导致数据空间不够的时候,我们就得从头来改这个显存的大小,从而导致整个显示程序都要相应的产生一些变动,这还不是最糟糕的,最糟糕的是当一款新的显示器因为新的功能需求而导致数据结构需要发生变化的时候,我们就崩溃了,前期的工作可能改动就非常大,甚至于都要重新过一遍,也就是重写重调,这么痛苦的事情,我是最讨厌的了。

所以我们要尽量避免这类事情发生,这里对面向对象的思想,就颇为需求了。这个时候,我们就要引入一个新的概念,那就是对象的儿子,子对象。前面讨论的,其实都只是一个抽象的对象,没有任何具体的样子,而只是笼统的规划了所有的显示器必须具有什么能力,而对于每一个具体的显示器来说,还没有任何具体的设计,在这里,每一个具体的显示器,就是显示器对象的子对象,他们形态各异,但是都必须能完成规定的功能。以传统的OOP语言理论来说,这里就产生了一个继承的关系,但是在裸程序思想里,我并不赞成引入这个概念,因为传统的OOP语言里的继承,纯粹是一个语法上的逻辑关系,继承关系明确,而裸程序中的这个思想,并没有任何语法支持,继承关系就非常微弱了,还不如说是归类与概括。但无论是什么关系,我还是不想就这种一目了然的关系弄个新名词来,让看的人费解。

既然引入了子对象,我们能看出这种做法有什么实际意义吗?也许有经验的资深程序员能看出来。我们在做父对象数据设计的时候,我们并不规定具体的数据格式和显存大小,而是一股脑儿地全推给子对象自己去搞,父对象什么都不管。哈哈!这样做事情真是很简单吧?不是我的事情我不管,不要说我偷懒,因为站在父对象的角度讲,这是最明智的做法,因为管不了,所以不管。

到这里也许就会产生更多的疑问了,一个对象什么都不管,那作为调用者怎么使用这个对象呢?你想用它,它什么都不管,这怎么行呀?别着急,父对象不管的,是那些具体的事情,抽象的事情,还是管的,要不然它就没有理由存在了。你抱怨了,说明你在思考,既然思考了,就把思考的问题提出来,提出来的,就是我们设计父对象的依据。提问题,我想这比搞程序要简单得多,比如:显示器能显示多少乘多少的字符?颜色是多色还是单色?显示模式是否支持预定的方式(如移动、闪烁等)?工作模式是图像还是字符?等等,这里附加说明一下,对于显示模式,我们这里都以字符显示为例,既然是面向对象的思想,相信扩充出图像显示模式,还是很容易的事情。

有问题出来了,我们就继续为它添加代码好了。

dispGetMaxCol();//取一行最多有多少列
dispGetMaxRow();//取显示器一共有多少行
dispGetMaxColors();//取显示器最多有多少色
dispSetShowMode();//设置显示的方式,对于不支持的显示方式就自动转为正常显示
dispSetWorkMode();//设置工作模式,如果没有的模式就返回0,支持的就返回1 

对于这些函数的定义,各人可以根据自己的习惯来设置,我只是临时弄了这个例子,未必就是最好的,我的目的是重在说明思想。我也害怕把程序弄得庞大了,出本书都嫌厚。

似乎加了这些函数之后,我们根本就没看到显示数据的具体形式,和前面的函数一起,都并没有什么明确的说法。这种感觉很正确,我们确实没有对显存做任何定义,但是似乎功能却都已经定义了,其实也确实是定义了,而且将来我们就这样用,而且也不用怕,程序一定会写完的。

五、数据传递与程序逻辑是同等重要的

继续上面讨论的问题。前面我们提到,为了使用dispShowAChar()dispShowALine()dispMainThread()这三个函数,我们又引出五个新的函数来,这些新的函数最主要的目标,就是要实现调用者与被调用者之间数据的传递。

对于程序设计来讲,数据传递与程序逻辑有着同样重要的地位,前者经常在最后会形成一种协议,后者则经常表现为各种算法。

在裸程序中,我们的思想应该主要是表现为一种灵魂,而不能如C++那样,去追求语法的完美,所以对于参数的传递,我们不能去追求语法上的完美,而是不拘一格用传递。除了函数可以传递数据外,直接调用值也是一种很快捷的方式,但是调用不能随便说调就调,而是也要学习C++上语法的习惯,尽量不能让一些专用的变量名称,出现在与专用变量无关的程序体中。例如,我们的设计中规定,我们这套裸系统对显示器最多支持65536色,那么我们就会用一个16位的无符号整数来保存这个指标。为了简化以后的说明,我们先定义两个数据类型:

typedef unsigned int UINT;
typedef unsigned char UCH;

如果我们用函数来传递这项数据,我们可以用如下的方式:

#define Monitor01\_MaxColors 0xFFFF

对于颜色调用函数则定义如下:

UINTdispGetMaxColors()
{
return Monitor01_MaxColors;
}

很显然,如果另一个显示器是个单色显示器,则颜色调用函数只需要改为下列形式就可以了:

#define Monitor02\_MaxColors 0x0001
UINTdispGetMaxColors()
{
return Monitor02_MaxColors;
}

之前有人提到过,用数组,这可以解决很多问题。说得一点没错!上面的例子我们忽略了一个问题,那就是同一个函数名要去做很多不同函数所做的事情,而我们却没有在函数体内使用switch(),这显然是不对的。要真正实现不同显示器的共同属性MaxColors的传递,我们必须要添加switch()以区分不同的显示器类型。那么这里我们就需要引入一个新的父对象属性以指代它的第几号儿子:

UCHMonitorType = 0;//显示器类型,最多支持256种显示器

并在初始化的时候,为该属性初始化为0,以作为缺省类型显示器的代号。以下命名我们就说一个约定,以让代码更具有规范的模样:父对象的接口函数用小写的disp打头,变量用Monitor打头,宏数据用Monitor开头并且内部至少有一个下划线,宏函数则用全大写字母组成。那么不用数组的情况下,上面的代码将会变成如下形式:

#define Monitor\_00 0
#define Monitor\_01 1
#define Monitor\_02 2
UINTdispGetMaxColors()
{
//以下用多出口,但这并不会破坏什么,为节约代码,完全可以使用
switch(MonitorType)
{
	caseMonitor_01:returnMonitor01_MaxColors;
	caseMonitor_02:returnMonitor02_MaxColors;
}
	return	Monitor00_MaxColors;//缺省则返回默认显示器
}

这样的形式很显然是太冗长了,尽管非常结构化,但是一般在优化程序的时候我们还是可能会废弃它的,所以这里就提到了数组的使用。既然是数组,那么它自然不能属于某一个子对象,而是应该在父对象中定义的,尽管这样做我们每次在添加新显示器的时候我们比如在父对象中添加难以理解的新的数据,但是为了节省代码,我们还是能忍受这样的痛苦。如果改用数组,则上面的代码将改变为如下形式:

#define Max\_Monitor\_Types 3\*\*\*
#define Monitor00\_MaxColors 1
UINTcodeMonitorMaxColorsArray[Max_Monitor_Types]=
{
	Monitor00_MaxColors,//缺省为单色
	Monitor01_MaxColors,
	Monitor02_MaxColors,
};\*\*\*

打***的语句将是未来扩充时不断需要修改的句子。那么上面的函数就简单了

UINTdispGetMaxColors()
{
	return	MonitorMaxColorsArray[MonitorType];
}

甚至有人还可以用宏函数来节省运行时间,只要修改一下调用规则就可以了:

#define DISPGETMAXCOLORS (c) c = MonitorMaxColorsArray[MonitorType];

也许当我们写成如上代码的时候,我们的每一次改进,都会让我们欣喜,我们的代码又优化了。但是可惜的是,这种没有思想的优化会在不远的将来,给我们带来麻烦。我觉得我的记忆力很不好,也许一分钟前的事情我都会想不起来,这种在将来扩充中的上窜下跳地修改会让我觉得晕眩!

所以,在工程化的工作中,我们需要把父对象与子对象尽量隔离开来,减少关联性的修改量,这也是面向对象思想的重要意义之所在,对于这一改动,我将在下帖中阐述

六、父对象接口函数与子对象功能剥离

上文我们说到 dispGetMaxColors() 的一些设计思路,我们有很多很好的办法来实现它,但是我们有没有更好的管理办法来实现它,这才是我们要站在更高层次看问题的焦点,是更重要的。这也就是一个从传统思维到面向对象思维的一个重要的转折。

要想把这个函数转变为面向对象的逻辑结构,我们也要先做些准备工作。

第一说参数传递的思想。尽量减少参数传递,这是尊重C51系列8位单片机硬件现状的一项重要措施,记着,不要抱怨C51档次低,资源少,而是要在尊重和热爱C51的前提下,我们才有热情来发展我们的裸程序面向对象思想的,也就是说,无论我们面临的系统有多简陋,我们都有策略,来实现复杂的功能,而且从发展的眼光来看,产品的升级,并不是盲目的升级我的CPU,因为那样只会让产品设计者智商下降,所以我觉得C51的特色就是应该在简洁,越来越简洁,而不是越来越复杂。所以我希望我们把思想升级作为升级产品的一个发展方向。传递参数要减少指针、浮点等类型的数据传递,尽量以UCH与UINT为主,而且参数的数量不要太多,最理想的上限是2个,如果实在多了,就使用共享缓冲区,或者全局变量。最好是不要传递参数。

本函数就利用了MonitorType省略了一个参数传递。

第二是我们要让父对象的接口函数与具体的子对象的多种可能性的功能实现剥离,这里我们就需要使用函数指针。函数指针也许我们一般用得少,但是其实并不是很复杂。先看我们函数的形式:

UINTdispGetMaxColors(void);

为该函数定义一个指针类型,只需做如下定义,就可以了:

typedefUINT( \*dGMC)(void);

那么对于父对象中的dispGetMaxColors()函数,我们就只需要转换定义一个函数指针,在创建父对象的时候为它提供一个子对象对应功能调用的入口地址,就足够了。所以对于这个函数的实体将只在子对象中出现,而在父对象中只会出现一个变量的定义:

dGMCdispGetMaxColors;

为了给它赋初值,我们也可以定义一个空指针,作为一个未使用的判断标志:

# define NIL 0

那么初始化dispGetMaxColors的时候只需要写条如下语句就可以了:

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

5GW0Wgll-1715540888481)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 14
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值