(转)从一个结构更加开放自由的 12864模块的设计过程,学习各种结构体和函数指针的使用
对于我这种初学者而言,好多东西都是知道的似是而非
数组,结构体,结构体指针,函数指针
只是知道个大概,偶然发现论坛里有讲解12864利用结构体和各种指针来完成一个自由奔放的模块设计经验,
看后感觉受益匪浅,所以转过来大家共同学习下.
-------------------------------------------------------------------------------------------
写一个12864是一种很简单的事情,网上的例程一抓一大把。
但是,如果你要根据你的MCU请选择不同的例程。受写uLib(前身是uS)的影响,我在写12864的时候,尽可能使其独立于单片机。
随着不断整理,最后,我发现我习惯了一种以 结构体封装对象 的方式来实现各种各样的模块,不管是硬件模块还是纯粹软件模块都一样。
这种方式和c++里,设计类的方式很接近,只是比起类,结构体在有些地方没有那么方便,因此略有点不爽,但这无关要紧,最重要是 面向对象 这种封装方式,在某种程度上对程序,特别是结构的容易理解有很显著的改善。
有很多人,很简单地把 “面向对象” 和 类封装对象 这种已经很强大的编程思维 等同。我在一些面向对象的书里看到很多提醒,类封装虽然是面向对象的一个形式,但面向对象是一个很深邃的概念,并没有那么简单,随着面向对象编程的实践不断深入才能够加强理解。
这里不再往下说,回到正题,给我,给你们看源码。
下面展示一下这个过程,现在是直接看到结果,没有看到我一路改变封装的过程,不过鉴于以前uS的直播过程,这个过程也没太大意义,所以现在也不管顺序了,按照以下的顺序来叙述即可。代码也附在其中
既然是 面向对象,我们就要首先 了解对象—— 什么是对象,什么可以作为对象。对象包含什么。
1.什么是 对象
这里,先说明一下,这些概念并不是那么简单的,所以我只能说,这里的说法是我个人的体会和看法,可能还是很肤浅,但是我希望它有一定的意义,每一次对概念的深化,都可以对实际编程有一定程度的帮助,甚至是突破。
我对一个对象最简单的理解就是
“有数据,有动作”
一个很简单的例子:
如果串口是一个对象,那它必然有数据:收发用的缓冲,还有这个过程中需要记录的种种状态,数值
还得有动作:打开,关闭,接收,发送;
对象这种东西,它的一个重要转变(指的是从面向过程到面向对象的变化)
就在于我们思考的角度.
同样是一个串口,如果处在C的阶段,我们下意识很容易会这样想
我首先要设置好串口,我要打开串口,我要发送数据,我还要接收数据......
但是,如果我们从对象的角度看待串口,我们就会这样考虑
对于串口这个对象,我们需要怎么定义它呢——需要一组怎样的动作和数据来 模拟它的存在?
这个问题就好像,乔布斯这个人如何成其为乔布斯
他首先是个人,是个美国人,他干了什么事......
对象也是一样,不管是什么东西,比如一个串口,一个LCD12864屏,这是物理实在,还是一个虚拟存在的对象,如一个FIFO缓冲。
在编程的世界里,我们是用一组数据,一组动作,来表征它,就好比我们不敢说我们罗列了 乔布斯的成就和简历以后我们就说我们了解了一个真正的乔布斯一样
光有 打开 关闭 接收 发送 还有收发缓冲 以后,我们也不敢说,我们就完全 定义模拟了一个串口对象。
但是,对我们来说,这样一个 串口对象已经能满足我们的需求了,那就够了,到此为止。
以后还可以根据需要扩展.
感谢辛昕前辈的辛勤付出!
补充: 附上相关的源代码: 本站百度网盘里可找到,资源名称:source_LCM12864.rar
11 个回复
admin
赞同来自:
我们可以很容易的从任何一份手册或者例程,针对我们自己的MCU类型和io连接修改成我们需要的程序。
但是现在,让我们从对象的角度来分析一下 12864作为一个对象,具有什么特性和要做的事情。
(当然这一切是建立在我几次重复实现了12864驱动以后的结果)
12864的驱动方法,我是指带STR7465这类驱动的模块。
我们只需以一定的时序 写入数据,写入命令,就可以完成驱动。
那么——
现在我可以吸收经验,并且真正理解了以前看到的一句编程箴言:
12864的驱动有什么特征呢?
首先,我们知道,不管我们底层如何连接,也不管我们用什么方式读写(串行还是并行,80时序还是68时序)
最后,我们对12864的任何操作,都是通过 写入命令 和 写入数据 完成的。
所以,驱动的最高层 就是这两个动作
(虽然后来真正涉及操作以后,我们还会知道,我们实质上,还需要一个对RES脚的特定时序来复位12864,在上电后复位,相当于 开始时序。)
现阶段提供的代码,都是伪代码,你无法直接编译,它只是作为一种设计过程的中间产物。
即使是我已经写了几次的12864驱动,实质上,当我完成上述这个设计的时候,我是不可能那么天才的知道接下来我要怎么设计 形参,或者什么实现 这几个op操作和具体驱动之间的分离的.......
如果是,那我一定是扯淡,骗你的——至少我现阶段做不到这个地步。
而事实上,说实话,上面这个设计,是我最后一步才想到的。
现在我们回到12864底层的IO时序操作。
为了简单,我们先以其中一种读写方式为例子。
比如 并行80时序(我之所以选择这个,是因为出差前,我只带了一块硬件上配置成80并行的屏,而我在宾馆里显然没有电烙铁给我用。)
标准的12864模块,都有16个引脚,它们是标准的(我好像还见过一种20引脚的)——不过,如果你和我一样,走完了一次完整的实现,你会发现,不管它硬件上怎么连接,对我的驱动程序都是无关紧要的。
我就不再上引脚图和上时序图了。
采用80并行时序时,需要操作的IO有(不包括 背光控制脚)
DB[0:7]; 这里表示DB0~DB7
CS;
RES;
A0;
WR;
RD;
每个引脚都将连着一个IO.
对象,12864是对象,一个IO也是对象,处处都是对象。
我们来看这些引脚。它们非常简单的对应于某一个IO;
让我们把它们也看成一个对象。
这个对象非常简单,它们只不过是一个IO脚,只有高低电平变化。
而且,它们都是输出的,不需要输入,也就是说,我们对它们的操作只有两个:写高,写低。
当然,具体到IO操作,除了51以外,我们大多数时候还要设置它的IO方向以及IO配置(推挽,开漏之类的)。
对于上述,进一步分析,我们发现,IO方向和配置的设置只需要操作一次,而写高写低却随时随地都需要操作的。
我们已经很习惯了一种模式:
初始化 --> 操作。
那么初始化 就是 设置方向为输出,这里因为硬件上懒得接个上拉电阻,我们设置成强推挽。
而操作,自然就是 写高写低。考虑到IO对象非常简单,我们不需要再为它设计一个结构体,并且把写高写低合并成一个函数即可。
这样,整体来设计,我们把这一系列IO驱动 合并成一个结构体。
每次,我们要做的是
把这些功能引脚 和 通用IO脚 绑定在一起——注意的是,我们不能固定死这种绑定,应使设计满足随时改动。
绑定了后,在使用前,我们还要完成 设置成输出和推挽模式的初始化。
所以,我们需要这样一个 12864 IO驱动结构
admin
赞同来自:
这个地方是一个关键,正是它实现了 这个驱动结构 可以独立于单片机。
这个地方我不具体解释我是怎么从开始到最终演变成这种思路的过程了。
只简单解释这种操作方法。
下面直接以这个12864(80时序并行)的驱动代码为例子说明:
请注意,对于下面的这段代码,如果我改变了引脚位置,甚至改用了别的单片机,比如51,我的所有改动都只紧紧锁定在下列这几个函数里而已,此外,任何代码都无须改变。
80方式的12864 IO驱动结构
实现
这里解释了一下为什么我会选择用union 共同体来重定义 这两个成员。
这里,我并不是为了节省空间,我的主要目的是
利用 ”共同体“ 表达一个逻辑:
尽管12864本身有多种驱动方式,但对于一个特定的12864硬件连线方式,它事实上只可以选择其中一种,因为我用共同体表达这种结构逻辑。
至于模式,则是为了 枚举化选项。
虽然枚举并不是一个足够安全的范围检查类型。
但它可以达到一定的强化作用,让大家明确它的含义,并且只要我们坚持不用字面量去赋值,就可以保证它的行为永远在我们的预期里,不会出现 不确定行为。
admin
赞同来自:
在我们继续进入 设置屏,写屏以前,我们先来试试怎么使用 已经设计好的结构。
其实,真实的设计过程,我是一边设计,一边试着考虑我会怎么用,怎么用方便来不断改进这个设计的。
所以,我们不管设计什么实现,都要不断从调用者的角度考虑问题
我们应该怎么设计,才能让它要做的事情最简单,最少,要关心的细节最少?
在前面注册IO的时候,我们只要在main()里进行一个简单的调用,就可以把这个具体的IO连线
和一个特定的12864驱动,绑定到一起。 有了这个 screen
我们等于拥有了 直接对我们的12864的 引脚,如RD 如DB0 DB1等的 IO写高写低。
当然了,我们也就自然的想到
写命令,写数据 的过程,实质上就是这些 引脚 之间的时序的组合而成。
因此,我们现在可以定义 这两个函数。
这两个函数的意义重大,因为,透过它们,我们可以完成所有的12864应用操作,这个接口
也是12864的IO驱动和应用操作的 接口点。 这个其实是设计好的接口结构体。
首先补充一下
poweron这个操作对应 12864上电后,RES脚要有一个 先低后高 的 负脉冲启动时序。
所以加上 写命令写数据,实质上是三个操作。
至于driver,它就是我们前面实现的那个 LCD12864.
这里有一个问题是
我为什么会把driver定义为void *指针,而不是直接定义为 LCD12864。
这里有一个”历史原因“。
我最初写这个12864驱动的时候,我只写了串行方式,我曾答应给 卖给我这个12864屏的朋友一套我自己写的例程。
所以我打算发给她的时候,她问我,是否还有并行的驱动,我说还没,我写完就给你吧。
然后我发现,由于我这种封装驱动的方式,导致我需要重新写一次 诸如 在屏上画 国际象棋棋盘 这样的画屏函数
而原因仅仅只是因为我的驱动结构体不一样!
这一点点的区别,就让我需要重新写一次,这恰好暴露了这个驱动的一个缺陷。还是不够灵活。
我思考的直接结果就是 我上面最开始定义的那套 用void *取代特定驱动结构体 的 那个 Op12864结构体定义。
我的设计思路是
对于具体的12864操作(所有12864的操作都由单纯的3个操作组合而成,PowerOn,writecommand,writedata),
而不管是哪一个操作,都需要特定的引脚时序组合而成,它们永远是逃不开 具体的 驱动的。
但是如果我要实现它们和 具体的驱动隔离,我就必须设计一个可以适应不同驱动结构体的 操作组合结构体。
于是我想到了 有通用指针之称 的 void *
这个时候还有一个小难题。
那就是,即使我可以用void *通用驱动 去写 12864应用操作,我最终还是要还原到真实驱动结构体里去。
我该怎么做呢?
简单的说就是
Op12864并不知道 真正的 LCD12864结构体信息,它却要去调用LCD12864.
但随即我就想到这个很容易实现。
还是类似之前 RD引脚自己也不知道自己可能会被连到GPIOA PIN3上还是 GPIOC PIN4 或者PA4 甚至P12上去一样。
我只要用一个函数指针来传递。
就可以实现 实现和调用的先后顺序颠倒。
具体来说,看这个地方。
admin
赞同来自:
现在我们需要沟通它和 应用操作 Op12864的连接(或者说是注册)
这个函数,不是实现在 定义Op12864和写 具体的画屏这些 应用函数 的地方
它是实现在 设计和实现 LCD12864 驱动结构体 的地方。
这其实合情合理。
一方面:除了这个地方,没有地方应该知道或者说,需要去了解具体的 驱动结构设计;
另一方面:
我们期待 应用操作,也就是使用Op12864的那些 画屏函数 可以以一个更开放的姿态接纳不一样的 12864 IO驱动结构设计。
就好比,它应该可以对我一开始的 串行方式 的IO结构
或者我后来希望新增的 80并行方式的IO结构 一样有效。
所以它采用void *通用指针去接纳 驱动。
自然也就不应该去关心具体的实现和内部结构。
END
上面罗罗嗦嗦说了一通。
基本上的设计思路也写完了。
忽然想起这个帖子,有些补充。哥们别嫌烦。
回想起来,这确实不算代理模式。
它是那本书中提到的 硬件抽象。原文abstract,估计和你说的那个是类似的。
至于是不是面向对象,可能确实差的有点远。
主要是受语言限制,如果大费周章去模拟反而得不偿失。
另外就是关于你说的抽象到什么层次。
关于这个问题我是这样看的。
比如说12864是点阵型液晶屏,所以我个人觉得它的最基本绘图元素应该是画点,而非其他更加复杂的字符,图形。
举例说,假如你抽象的最底层直接到了显示字符'那么,如果要换成其他字体呢?
而如果是对于1602或者七段数码管这种,因为它能绘制的最基本就是字符,当然,其实数码管某种程度上也可以看作八个点的点阵。
但大概很少那样干。
对他们而言,抽象到显示字体我就觉得合适了。
当然了,如你所说,这只是个人对抽象的理解,无所谓对错,欢迎和你继续深入讨论,或者是一起分享你的其他宝贵经验。
程序本该写的更加严谨和优雅,特别是嵌入式,单片机,因为就观察所得,似乎这两快的程序代码普遍更让人遗憾。
有时候不知如何下手。
这类书大多是非嵌入式领域。
比如说嵌入式领域要解决的一个很大问题就是 硬件抽象(我个人认为),也许 这个名词用的不对,但我这里说的这个很大的问题指的是 较高层对底层硬件的 抽象,使其可以无差别地用一种高度统一的方式操作它们,类似于 linux中任何设备都是文件 这个概念一样。
嵌入式讨论这一类的书,呵呵,可能是我少见多怪,但见到的书真心不算多。
其中一本就是我前面提到的一本 C嵌入式设计模式。
然后就是,说到嵌入式本身的特点。
一个是 资源有限,比如空间,但其实我个人觉得这个问题其实好解决。
毕竟现在已经看到,存储器是越来越便宜。
其实更多时候我更关心的是时间,因为嵌入式设备,特别是我实际接触的单片机。
它的主频了不起上到几十M,了不起上百M,绝大多数时候十几二十M。
即便现在单时钟周期,如果完全照搬在PC上的方法,我觉得对时间的消耗是玩不起的。
所以,时间换空间,或者空间换时间,我觉得是一个在不断发生变化的事情,视具体情况而定。
然后......扯了一通,主要还是谢谢哥们写了这么长的回复。
仔细看了几次,发现有些东西,还是没啥概念(我指的是,猪跑都是都见过了,但是没吃过猪,也不知道这些猪该怎么下手吃)
比如前面说的
OO绝对不只是OO语言,更多的是对OO的理解,以及OOA。
OOA这个概念,我在 设计模式 这本书里有看到他提过,只是,说实话,真心分不太清楚这些概念。
眼下的情形是,除了在折腾 stm32f030加GPRS,就是之前手机DIY那个DIY——其实主要是在不断实践各种编程的方法呀,结构呀,抽象呀.......理论看不太懂只好靠实践了。
另外就是买了一本 潘加宇翻译的 Martin Follwer的 UML精粹,打算好好学学UML。
UML这东西以前折磨过,太复杂,一直不知如何下手。
后来偶然发现Martin Follwer这本书,对他印象很深刻,因他的思想非常务实和简练——
即使不是很完全很专业的去应用一门技术,他却总能找到其中最有价值的部分,使其产生效益。
而他的这本UML精粹看了一部分,确实也看懂了不少东西。
嘿嘿一直没说。
个人一直在单片机C上写程序,有好几年了,基本上语法什么的已经不关心,虽然经常忘记具体写法,但大概有什么心里都有数。
一两年来主要关心的是 如何在结构啊,思想啊什么的上面取得一些更加有助于开发和写出更好的代码。
而最近几年主要干的事情,无非是
1.不断倒腾,琢磨 硬件抽象;
2.开始用一些很基本的数据结构,如队列,链表等 优化一些实现方案;
不一而足。
希望和你更多讨论,虽然我不知道我了解的东西和实践的程度是否具有和你对等的讨论基础......
晚上折腾别的东西,这回坐下来打开论坛已经是半夜了,看到哥们写了这么多辛苦了,看了一次,有好些话恰好很符合我的实际体验,就不敢说什么“仔细看了两三次才回复的了”。
首先是,哥们开始说的,最核心的应该是。
所谓模式,所谓分析 等等诸如此类(在我看来,都只是抽象,升华到不同层次,产生的概念)
而最实质的还是因为遇到的问题足够了,只要稍微用点心的,都会自然而然被触动,被启发,接而产生各种各样看起来高大上的东西。
所以这也正验证了我自己这几年编程的一些切身体会,这也是我常对那些问我怎么学写代码的朋友说的一些话。
“你现在啥都别想,给我好好写代码,好好debug,那些书你看不懂,也看不出感觉”——不是忽悠人或者故弄玄虚,只因为我自己曾经也如此。
别的不说,一本很基础的 计算机科学基础,学,用51前后 看了两次,感觉就完全不同。
最通俗的就是,实践产生问题,问题才是一切的本源,概念和理论反而是其次。
另外,我非常同意你最后说的那句话。
单片机,嵌入式本身并没有太多特别的地方,仅仅只是和PC有一些差异。
它们在开发等各方面不该有太多的差别。
当然,因为各自发展的阶段不同,以及从业人员的专业背景不同,所以造成各种遗憾什么的。
我本人是机械专业出身,当然我一天都没干过机械,我是典型的跑江湖的,全靠自学,自知非主流非专业。
但反而是因为这样,我从一开始就
“试图从传统PC领域,学到一些成熟的,经典的方法,思想,好用到嵌入式,单片机上,以改善开发和代码质量”。
然而不知是因为我个人过于懒散,或者资质,经历什么的,尽管我囫囵吞枣翻阅过不少书(有好多都没翻完),了解到不少看起来很高大上的名词,概念。
然而,在实际工作,代码中能用到的却少得可怜。
甚至我的一些朋友(都是圈内),甚至觉得,在单片机,嵌入式(在大家眼里,似乎都觉得我们和PC有天差地别一样),太讲究强调这些不是什么好事——虽然我自己一直继续坚持,只是不断权衡。
在这个板块里,我发了很多帖子,虽然内容可能不成熟,其实也挺丢人现眼的,但我却一直希望抛砖引玉,引来更多的人一起讨论,互相学习,真正了解在具体行业里,我们自己的行业,环境里,让这些高大上的理论,概念 更进一步贴近地气。
然而一直都没用。
成了我一个人的一言堂,经常想起都觉得像个笑话,甚至小丑。
后来想想主要还是自己的问题,不管是表述,还是自己的能力,自己没这个能力,又何来这个号召力,何堪抛砖引玉的重任?
所以有很长一段时间我基本不发帖,甚至不写博客了,想充实自己再说,所以在这个时候看到你的回复,让我很激动。
从你的回复里,能感受到你的功力和能力。
很想和你畅谈,可来往之中又深感自己,能力上和你不对等,而我体会过这种,不管是我比那些新人能力,经验高出一截,或者遇到比自己高一截的,我都知道,在不对等的情况下,多讨论也是多的。
何况,越来越的,我也觉得自己是应该把这些没用的扯淡时间用来充实自己的能力和知识,更重要。
呵呵,不好意思,我也说的很乱了,就此收住。
我会持续努力(我一直对自己寄望,至少写够十年代码,让自己对得起自己说过的那些话,提过的那些名词)。
虽然现在还不能和你继续深度讨论,还是希望与你同行。
admin
赞同来自: 晓蓝
简单的说,任何外设驱动,说到底无非就是io时序而已。
纯c代码实现,以满足绝大多数单片机直接使用;但是它使用结构体和函数指针实现了一个仿面向对象 类的封装;
概括地说,它抽象了两个互相隔离的,平行的结构体,一个是实现mcu gpio和 12864引脚的绑定,是为 驱动结构体。
另一个是 通过写命令,写数据 等操作12864的 操作结构体。
首先简单说一下封装的几个主要目的:
要在程序结构(层次结构,数据结构)上最大程度表达事物对象本身的逻辑结构;这样才可以“面向对象”地去看待,使程序的可读性和易理解程度得到加强;
它要实现类的 继承性,子层次结构。
为了防止复杂度,最大可能性减少不必要的层次嵌套;
以下是程序中使用的相关结构体——理解了它们,就可以理解整个结构,无须太在乎实现代码细节。能写12864的人都可以自己实现。
Driver.
Driver是这样一个东西。它把每个IO的写高写低(一个函数)绑定在一个 函数指针里,然后把所有这些函数指针封装成一个结构体,也就是最开始的两个
Serial_12864 和 Parallel_80_12864 驱动(因为12864的读写方式有一种串行,两种并行,这里只做了其中一种。但就此结构设计,要新增一种是非常容易的事情,而且对原有改动很小。)
我们对12864的任何操作都是以操作这些时序完成的,因此,我们必须有一个承载这些IO写状态的结构;
接下来,我们需要另一套结构,它是为了在一个更高的层次上抽象12864的所有操作。
这个地方我非常不爽,因为结构体,我一直实在想不出有什么办法,可以让它像类一样,各成员之间直接互相访问。
比如说,让writecommand可以直接访问 driver,那就好了~~
不过不要紧,让梦想打八折。
多带一个参数也不是太要命的事情~~~
这里解释一下为什么用void*
这是为了让这个结构体能以更开放的姿态承载各种各样不一样定义的 driver结构体——比如前面我介绍的那一种就是我实现的一种,你要是喜欢你也可以自己定义一种。
最后剩下一个问题
如何沟通这两个结构体——专业或者说装逼点的说法是,怎么连接 驱动结构体 和 操作结构体(Op12864)
驱动结构体 和 操作结构体 是两个独立平行的结构。这是为了最大层度减少 驱动 和 操作 这两个层面之间的耦合关系。也就是说,你爱怎么改是你的事,反正我这边照用不误。
因此,另一个角度来说,它们也就是 兵不识将,将不识兵。所以这个地方,一个核心的地方是使用了void*这个通用指针。
所以在连接 两个结构体的时候,必须让驱动结构的实现方,根据自己的结构体,来实现 这个连接函数,或者叫做 注册函数。
这里抽取这个函数作为例子 至于剩下的,大家伙就自己看程序吧。程序还很简单,只是一个画国际象棋的demo函数
xuezhimeng2010
赞同来自:
这么好的帖子,真正有内涵,赞一个
卿枫~凌
赞同来自:
看了此篇,表示惭愧啊,还需要不断向大神学习
电
赞同来自:
我之前都是直接复制粘贴这样来写代码。所以导致现在也不会写驱动。很是蛋疼。
大东 - 大二生
赞同来自:
收藏起来以后慢慢看,现在只是感觉起来好像直接用寄存器驱动12864
xiaomao760
赞同来自:
不错,值得学习,讲解的很不错,容易理解驱动的含义。
jesse_qiao
赞同来自:
感谢大神,好有用哦