最近,我把stm32的HAL库函数中的源码都大致看了一遍,对面向对象编程有了一些新的体会,于是我在心里想stm32的标准库函数是不是也是面向对象编程的呢?标准库相对比HAL有哪些不足之处,我最近看了几个标准库的源文件,现在将发现的标准库存在的不足之处进行存储。
(1)不足一:标准库函数没有将模块的模型抽象总结成句柄类型的变量,模型的特征是分散的,而不是几种打包成一个大的结构体类型句柄。
例如:
cpp
void ADC_AnalogWatchdogSingleChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel)
{
uint32_t tmpreg = 0;
/* Check the parameters */
assert_param(IS_ADC_ALL_PERIPH(ADCx));
assert_param(IS_ADC_CHANNEL(ADC_Channel));
/* Get the old register value */
tmpreg = ADCx->CR1;
/* Clear the Analog watchdog channel select bits */
tmpreg &= CR1_AWDCH_Reset;
/* Set the Analog watchdog channel */
tmpreg |= ADC_Channel;
/* Store the new register value */
ADCx->CR1 = tmpreg;
}
其中参数传参的第一个参数ADC_TypeDef是寄存器组类型的结构体类型变量,而没有更进一步打包成Handle句柄变量。
(2)不足二:函数操作寄存器比较随意,有的函数是通过参数传参的方式访问对应寄存器的,有些函数就直接通过宏定义找到对应寄存器直接访问,没有做到和HAL库一样都是从句柄变量为起始线索进行寻找的。
例如:
cpp
void ADC_TempSensorVrefintCmd(FunctionalState NewState)
{
/* Check the parameters */
assert_param(IS_FUNCTIONAL_STATE(NewState));
if (NewState != DISABLE)
{
/* Enable the temperature sensor and Vrefint channel*/
ADC1->CR2 |= CR2_TSVREFE_Set;
}
else
{
/* Disable the temperature sensor and Vrefint channel*/
ADC1->CR2 &= CR2_TSVREFE_Reset;
}
}
cpp
#define ADC1 ((ADC_TypeDef *) ADC1_BASE)
#define ADC1_BASE (APB2PERIPH_BASE + 0x2400)
函数参数中没有定义ADC_TypeDef *类型的指针变量,而是直接通过ADC1这个宏作为寄存器组的首地址找到对应CR2寄存器直接访问。这样做好处就是比较干脆直接访问,没通过参数传参方式实现,坏处比较明显,就是破坏了函数定义尤其是参数传参的统一性,降低了代码的可移植性。
(3)不足三:标准库定义的函数名有时候感觉比较随意,不知道需要做什么,函数命名有时候不符合"主谓宾"结构。
例如:
cpp
void ADC_ITConfig(ADC_TypeDef* ADCx, uint16_t ADC_IT, FunctionalState NewState)
{
uint8_t itmask = 0;
/* Check the parameters */
assert_param(IS_ADC_ALL_PERIPH(ADCx));
assert_param(IS_FUNCTIONAL_STATE(NewState));
assert_param(IS_ADC_IT(ADC_IT));
/* Get the ADC IT index */
itmask = (uint8_t)ADC_IT;
if (NewState != DISABLE)
{
/* Enable the selected ADC interrupts */
ADCx->CR1 |= itmask;
}
else
{
/* Disable the selected ADC interrupts */
ADCx->CR1 &= (~(uint32_t)itmask);
}
}
从函数名来看,是需要实现ADCx的某个中断配置,传参ADCx代表哪个ADC,ADC_IT代表哪个中断(吐槽:中断类型还是uint16_t,最好是使用枚举类型),第三个参数NewState新状态与中断配置在字面上感觉就不是很匹配,配置应该是操作比较多,NewState就只有Enable和Disable两种状态。
标准库中大部分函数的函数名中都有Config以及Cmd字样,Cmd的字样从含义上看是发送什么命令,但是在标准库中总是跟着Enable或者Disable两种情况。
还例如这个函数:
cpp
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal)
{
/* Check the parameters */
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
GPIOx->ODR = PortVal;
}
函数名为:GPIO_Write,GPIO写什么,没有宾语,不明白要写的是什么东西,而且也没有任何校验就直接写到ODR寄存器中。
cpp
uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx)
{
/* Check the parameters */
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
return ((uint16_t)GPIOx->IDR);
}
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
uint8_t bitstatus = 0x00;
/* Check the parameters */
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
assert_param(IS_GET_GPIO_PIN(GPIO_Pin));
if ((GPIOx->IDR & GPIO_Pin) != (uint32_t)Bit_RESET)
{
bitstatus = (uint8_t)Bit_SET;
}
else
{
bitstatus = (uint8_t)Bit_RESET;
}
return bitstatus;
}
函数GPIO_ReadInputData()和函数GPIO_ReadInputDataBit()如果不对比看的话,不明白GPIO_ReadInputData()函数的含义是读取所有GPIOx引脚上的Bit位而不是针对某一个引脚。我当时看到这个函数的时候就比较奇怪,潜意识里以为操作的是某一个引脚。为什么只传参了GPIO_TypeDef *GPIOx而没有uint16_t GPIO_Pin,看到函数内部才知道为什么,以及后面函数Bit字样才知道这样的函数是针对某一个引脚。
(4)位域操作比较随意,位域清零与置1不明显,有时候直接写数值,需要查询参考手册才知道。
cpp
void GPIO_EventOutputConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource)
{
uint32_t tmpreg = 0x00;
/* Check the parameters */
assert_param(IS_GPIO_EVENTOUT_PORT_SOURCE(GPIO_PortSource));
assert_param(IS_GPIO_PIN_SOURCE(GPIO_PinSource));
tmpreg = AFIO->EVCR;
/* Clear the PORT[6:4] and PIN[3:0] bits */
tmpreg &= EVCR_PORTPINCONFIG_MASK;
tmpreg |= (uint32_t)GPIO_PortSource << 0x04;
tmpreg |= GPIO_PinSource;
AFIO->EVCR = tmpreg;
}
#define EVCR_PORTPINCONFIG_MASK ((uint16_t)0xFF80)
tmpreg &= EVCR_PORTPINCONFIG_MASK;是为了对某些位域清零,正常应该使用
tmpreg &= ~(xxx_MASK),而不是直接将数值自己转换后直接操作。
cpp
uint32_t ADC_GetDualModeConversionValue(void)
{
/* Return the dual mode conversion value */
return (*(__IO uint32_t *) DR_ADDRESS);
}
#define DR_ADDRESS ((uint32_t)0x4001244C)
这段代码DR_ADDRESS的值直接是从参数手册中查找得到的0x4001244C,如果不查找手册,完全不知道是什么含义。
总之,stm32的标准库写的也非常优秀,stm32的标准库代码量相对较少,函数实现的功能基本都集中在了对寄存器的操作,函数内部的代码结构相对比较简单,分支或者循环内部嵌套级别也就最多2层或者3层。但是stm32标准库也存在一定不足以及偷懒的地方,这些与HAL库相对比就对代码的可移植性以及可读性就有了非常大的影响。
心得:变量如果从访问方式上分类可以分为两类:一种是可以变量内部参与位域访问的也就是参与位操作的位域变量,另外一种就是直接整体读写访问的普通变量。变量的意义在于变量名与写入的数据的统一,也就是说变量名能够通俗易懂的描述模块特性,写入有效数值才真正处于激活态,也就说写入有效数据后才决定了这个变量是上述两种变量的哪种,接下来才决定能够如果去读访问。