何为抽象
对于很多人来说,抽象这个概念一直很模糊,不妨来比较一下,哲学领域和科学领域对‘抽象’的解释。
哲学领域
抽象,是哲学的根本特点。它不能脱离具体而独自存在。抽象化主要是为了使复杂度降低,以得到论域中较简单的概念。
科学领域
单纯提取某一特性加以认识的思维活动,科学抽象的直接起点是经验事实,抽象的过程大体是这样的:从解答问题出发,通过对各种经验事实的比较,分析,排除那些无关紧要的因素,提取研究对象的重要特性(普遍规律与因果关系)加以认识,从而为解答问题提供某种科学定律或一般原理。
从哲学和科学领域大致提取出‘抽象’的关键词:复杂度降低 提取 排除 重要特性 提供
再看一下信息技术领域对抽象的理解:
软件工程过程中的每一步部可以看作是对软件解决方法的抽象层次的一次细化。在进行软件设计时,抽象与逐步求精、模块化密切相关,帮助我们定义软件结构中模块的实体,由抽象到具体地分析和构造出软件的层次结构,提高软件的可理解性。
背后的‘始作俑者’
如果是相关专业的读者,到这里脑海里肯定会浮现出四个大字:面向对象
抽象化与面向对象是密不可分的!
嵌入式开发
嵌入式中是否存在面向对象
很多人给嵌入式开发戴上了‘面向过程’的帽子,这是错误的。C语言至今仍稳居TIOBE榜单第二名(第一名Java),也是2019年年度编程语言!说明它仍然满足时代需求,姜还是老的辣。而面向对象和面向过程只是一种编程思维,而不受语言的束缚。可能有人问,我编写这么多C语言,怎么没有发现面向对象。可能大家最为熟悉的是其他语言中class这个关键词,纯C语言中是没有这个关键词的。但不能说C语言就不能实现面向对象。
可以参考C 语言实现面向对象编程
硬件抽象层的概念
硬件抽象层(Hardware Abstraction Layer)是软件层的例行程序包,用于模拟特定系统平台的细节使程序可以直接访问硬件的资源。将硬件方面的不同抽离操作系统的核心,核心模式的代码就不必因为硬件的不同而需要修改。因此硬件抽象层可加大软件的移植性。之所以有硬件抽象(Hardware abstraction)这个概念,是由于数字电脑具体的硬件(Hardware)操作相当繁杂,因此将具体的硬件操作抽象化简,避免由于直接以具体的机器代码(Machine code)撰写程序,而在将程序移植到不同硬件时,需要重写整个程序。其概念与目的,类似于数据结构(Data structure)中的抽象数据类型(Abstract data type),皆为保护程序免受变化的冲击。------WIKI
硬件抽象层技术是由Microsoft为了确保WindowsNT(往后Windows的基础)的稳定性和兼容性而提出的。Microsoft工程师们总结发现,早起Windows经常出现的系统死机或崩溃等现象是由于程序设计直接与硬件通信所造成的。于是在WindowsNT上取消了对硬件的直接访问,首先提出了硬件抽象层的概念,硬件抽象层就是:“将硬件差别与操作系统其他层相隔离的一薄层软件,它是通过采用使多种不同硬件在操作系统的其他部分看来是同一种虚拟机的做法来实现的。“后来,这种HAL设计思路被一些嵌入式操作系统参考,其系统内核被分成两层,上层称为“内核(Kernel)”,底层则称为“硬件抽象层”。在EOS中,HAL独立于EOS内核;对于操作系统和应用软件而言,HAL是对底层架构的抽象。综合分析HAL层的代码,可以发现这些代码与底层硬件设备是紧密相关的。因此,可以将硬件抽象层定义为所有依赖于底层硬件的软件。即使有些EOS的HAL在物理上是与系统内核紧密联系的,甚至相互交叉的,但是从功能上可以从分层技术的角度去分析它。------百度
拓展
可能会有人误认为只有硬件与操作系统之间才会存在HAL,并非如此,在一些嵌入式领域也早就运用HAL技术,并慢慢变成了一种思想。程序设计与硬件之间的直接联系可能会导致系统异常,这就意味着我们在编写嵌入式项目时,应规范地与硬件进行通信。接下来举一个HAL的例子。
STM32_HAL
STM32是当今主流MCU供应商之一,众多的MCU型号加上丰富的软件及代码支持,使得意法半导体的产品深受初学者喜爱。接下来我们详细地来看一下STM32 standard peripheral library之外的另一个驱动库STM32 cube library。
时钟树配置:
函数整体:
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};//定义一个关于晶振寄存器配置的自定义类型变量
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};//定义一个关于时钟寄存器配置的自定义类型变量
/*初始化所需要工作的晶振*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
/*将变量的值配置到具体寄存器中*/
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();//错误中断处理函数
}
/*初始化时钟树*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
/*将变量的值配置到具体的寄存器中*/
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();//错误中断处理函数
}
}
大家对这段代码再熟悉不过了,但是否又真正体会到这样编写代码的思维,接下来我们更加详细地来拆分这段代码。
首先来看一下这个自定义类型
typedef struct
{
uint32_t OscillatorType;
#if defined(STM32F105xC) || defined(STM32F107xC)
uint32_t Prediv1Source;
#endif /* STM32F105xC || STM32F107xC */
uint32_t HSEState;
uint32_t HSEPredivValue;
uint32_t LSEState;
uint32_t HSIState;
uint32_t HSICalibrationValue;
uint32_t LSIState;
RCC_PLLInitTypeDef PLL;
#if defined(STM32F105xC) || defined(STM32F107xC)
RCC_PLL2InitTypeDef PLL2;
} RCC_OscInitTypeDef;
对于硬件寄存器来说,为了方便寻址我们常用基地址加偏移量来进行读写操作,往往基地址都会使用宏定义,而结构体恰恰也能满足偏移的需求,所以我们常常看到MCU厂商都会用结构体来抽象地描述一个寄存器。但切记,这只是一个自定义结构体类型,而并非真正的寄存器。
HAL_StatusTypeDef HAL_RCC_OscConfig(RCC_OscInitTypeDef *RCC_OscInitStruct)
{
//此处省略几百行代码
__HAL_RCC_HSI_ENABLE();
//此处省略几百行代码
}
接下来我们不停地goto
#define __HAL_RCC_HSI_ENABLE() (*(__IO uint32_t *) RCC_CR_HSION_BB = ENABLE)
#define RCC_CR_HSION_BB ((uint32_t)(PERIPH_BB_BASE + (RCC_CR_OFFSET_BB * 32U) + (RCC_HSION_BIT_NUMBER * 4U)))
#define PERIPH_BB_BASE 0x42000000UL
终于我们找到了一个基地址!我们在主函数中调用SystemClock_Config时,其实并没有直接去配置寄存器,而是通过多层函数的调用来实现。ST在编写该库的时候,函数名已经标明该函数所在的层级,以HAL开头的相关函数均是STM32硬件抽象层向上的外置接口,它所进行的是向下配置硬件寄存器值,熟悉STM32 standard peripheral library的人会发现,其实就是将基础库中寄存器配置提取出来另加封装,其余的没有太多改变。可能在性能上无法直观地体会,但在稳定性层面上有了巨大提升。
总结
即使是在嵌入式程序开发中,我们也要时刻拥有面向对象的思维,不能想到哪儿写到哪儿,代码维护起来将是灭顶之灾。人们常挂在嘴边的封装、继承、多态,前提都是要在一个独立的层次上。当前层次只能调用小于等于该层次的阶层,不能调用高层次的阶层,并且也尽量避免跨界层的调用,以防出现程序缠绕!这是编写高内聚低耦合代码的有力保障,即使没有太多的代码优化,只要有鲜明的层级,代码的可读性、稳定性将大大提高。