最近在看stm32的HAL库程序,发现了一些非常重要的编写技巧,也充分感受到了HAL库作者功力深厚,所以不要因为STM32是单片机就小瞧了对应代码的含金量,代码暗含了非常多,也非常重要的一些编程理念,这里做一下简单记录:
(1)第一点:HAL库中每一个源文件对应一个模块,每一个源文件中都首先定义了一个非常复杂的句柄性质的数据类型,这个类型包含了大量数据,其中就包含Instance这个指向寄存器组的指针变量,当然还包含了state状态用于记录程序所在状态,parent成员变量用于重所在子变量找到对应父变量。源文件中的函数传参都包含这个自定义类型的参数变量,用于在函数内部去读写里面的成员变量。
例如:HAL库中的I2C模块对应源文件和头文件是stm32f1xx_hal_i2c.c以及stm32f1xx_hal_i2c.h这一对文件。自定义i2c这个模块类型的句柄变量类型如下所示:
typedef struct { I2C_TypeDef *Instance; /*!< I2C registers base address */ I2C_InitTypeDef Init; /*!< I2C communication parameters */ uint8_t *pBuffPtr; /*!< Pointer to I2C transfer buffer */ uint16_t XferSize; /*!< I2C transfer size */ __IO uint16_t XferCount; /*!< I2C transfer counter */ __IO uint32_t XferOptions; /*!< I2C transfer options */ __IO uint32_t PreviousState; /*!< I2C communication Previous state and mode context for internal usage */ DMA_HandleTypeDef *hdmatx; /*!< I2C Tx DMA handle parameters */ DMA_HandleTypeDef *hdmarx; /*!< I2C Rx DMA handle parameters */ HAL_LockTypeDef Lock; /*!< I2C locking object */ __IO HAL_I2C_StateTypeDef State; /*!< I2C communication state */ __IO HAL_I2C_ModeTypeDef Mode; /*!< I2C communication mode */ __IO uint32_t ErrorCode; /*!< I2C Error code */ __IO uint32_t Devaddress; /*!< I2C Target device address */ __IO uint32_t Memaddress; /*!< I2C Target memory address */ __IO uint32_t MemaddSize; /*!< I2C Target memory address size */ __IO uint32_t EventCount; /*!< I2C Event counter */ }I2C_HandleTypeDef;这个类型变量内部成员包含了I2c这个模块所有特征,是所有特性的集合(是咱在I2c这个模块角度考虑的),成员变量比较多,但是涵盖了I2C这个模块所有特点以及I2C功能实现所需数据。
(2)第二点:充分理解句柄变量(含有Handle这个字段)的含义。
首先,我认为句柄的含义是“起始变量集”,起始的含义是无论函数操作那些数据都是从句柄变量开中查找读写获取的。变量集的意思是内部有非常多的变量。也就是说模块对应函数体内部不断的将数据的的值写入到从句柄变量开始找到的具体成员变量中,也不断的从句柄变量开始查找所需成员变量中读取数据。这样做的好处非常明显,大大降低了函数内部代码编写难度,只需要不断将有用数据写入到句柄变量内部成员变量中,不断从句柄变量成员中提取有用数据,这里暗含了一种比较简洁的算法逻辑。有点像数组排序中的“冒泡排序”的做法。如果是人类,因为人脑非常擅长总结抽象而不擅长计算,人脑可以非常快的将数从大到小排列好,排列的过程其实非常复杂。冒泡排序就不一样了,无论数组成员数值是大于还是小于比较的值,都从第一个元素开始比较。这样虽然表看做了非常多无用比较(因为有些比较不需要互换位置),但是由于计算机运行速度非常快,这些浪费的时间基本可以忽略不计,好处就是大大降低了计算机难度。
总之,充分利用句柄变量这种“记事本”的特点,将所有有关数据都记录到这个“记事本”中,我们需要的时候,不用从大量复习习题中查找,只需要到这个“记事本”中查找就能找到对应数据。就算有时候直接找对应习题是最快的办法,也不要使用这种做法,因为会增加算法复杂性。
(3)HAL库中的“状态机”思维。
首先,我们都知道,代码其实是一个while (1)的无条件死循环中不断运行,也就说每一个函数都可以不断的被调用到,在程序运行阶段,不断的被调用执行。
其次,句柄变量中定义了名字为state的变量用于记录代码运行到这里的时候的状态,在函数内部有大量代码判断执行当前状态,如果状态合适才执行有效代码,如果状态不合适直接返回,或者发生超时时将status写入超时标志。
从代码整体看,整个程序其实就是一个非常大,非常分散的状态机(一般的状态机都是使用一个switch-case-break阶梯判断),HAL库没有使用传统的状态机,而是将状态机打散了,在每一个函数中不断读取句柄变量的状态去指导后面代码执行,代码执行后如果状态发生变化了,更新状态。
HAL_StatusTypeDef HAL_I2C_DisableListen_IT(I2C_HandleTypeDef *hi2c) { /* Declaration of tmp to prevent undefined behavior of volatile usage */ uint32_t tmp; /* Disable Address listen mode only if a transfer is not ongoing */ if(hi2c->State == HAL_I2C_STATE_LISTEN) { tmp = (uint32_t)(hi2c->State) & I2C_STATE_MSK; hi2c->PreviousState = tmp | (uint32_t)(hi2c->Mode); hi2c->State = HAL_I2C_STATE_READY; hi2c->Mode = HAL_I2C_MODE_NONE; /* Disable Address Acknowledge */ hi2c->Instance->CR1 &= ~I2C_CR1_ACK; /* Disable EVT and ERR interrupt */ __HAL_I2C_DISABLE_IT(hi2c, I2C_IT_EVT | I2C_IT_ERR); return HAL_OK; } else { return HAL_BUSY; } }函数内部一进来就条件判断句柄变量hi2c中的State的值,如果是HAL_I2C_STATE_LISTEN状态才处理,否则直接返回,结束函数调用。如果是LISREN状态,做完必要工作后,将hi2c指向的Status进行更新成了HAL_I2C_STATE_READY,这样再次运行到函数内部的时候就直接返回了,也可以指导其他函数中如果满足hi2c->State == HAL_I2C_STATE_READY时运行对应代码。
(4)主程序是一个大死循环,所有函数都有无数次机会得到运行,冗余重复代码不要写。将这段冗余代码只放在某一个函数体内部形成一份,需要的时候,能够确保冗余代码所在函数被调用执行即可。
例如:
HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);该函数内部有一个while(hi2c->XferSize > 0U)的循环实现了真正数据从内存到寄存器中写入,也就是完成了真正干活代码,如下图所示:
hi2c->Instance->DR = (*hi2c->pBuffPtr++); hi2c->XferCount--; hi2c->XferSize--;在中断主发送函数中就没有这个大循环,而是只将有效数据写入到记事本i2c中。
HAL_StatusTypeDef HAL_I2C_Master_Transmit_IT(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size) /**中断就不需要超时处理了 */ { __IO uint32_t count = 0U; if(hi2c->State == HAL_I2C_STATE_READY) { /* Wait until BUSY flag is reset */ count = I2C_TIMEOUT_BUSY_FLAG * (SystemCoreClock /25U /1000U); do { if(count-- == 0U) { hi2c->PreviousState = I2C_STATE_NONE; hi2c->State= HAL_I2C_STATE_READY; /* Process Unlocked */ __HAL_UNLOCK(hi2c); return HAL_TIMEOUT; } } while(__HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_BUSY) != RESET); /* Process Locked */ __HAL_LOCK(hi2c); /* Check if the I2C is already enabled */ if((hi2c->Instance->CR1 & I2C_CR1_PE) != I2C_CR1_PE) { /* Enable I2C peripheral */ __HAL_I2C_ENABLE(hi2c); } /* Disable Pos */ hi2c->Instance->CR1 &= ~I2C_CR1_POS; hi2c->State = HAL_I2C_STATE_BUSY_TX; hi2c->Mode = HAL_I2C_MODE_MASTER; hi2c->ErrorCode = HAL_I2C_ERROR_NONE; /* Prepare transfer parameters */ hi2c->pBuffPtr = pData; hi2c->XferCount = Size; hi2c->XferOptions = I2C_NO_OPTION_FRAME; hi2c->XferSize = hi2c->XferCount; hi2c->Devaddress = DevAddress; /* Generate Start */ hi2c->Instance->CR1 |= I2C_CR1_START; /* Process Unlocked */ __HAL_UNLOCK(hi2c); /* Note : The I2C interrupts must be enabled after unlocking current process to avoid the risk of I2C interrupt handle execution before current process unlock */ /* Enable EVT, BUF and ERR interrupt */ __HAL_I2C_ENABLE_IT(hi2c, I2C_IT_EVT | I2C_IT_BUF | I2C_IT_ERR); return HAL_OK; } else { return HAL_BUSY; } }主要原因是在整个主程序其实是一个大循环,每一个函数都有无数次被循环运行到,这段冗余代码只需要写在一个函数“HAL_I2C_Master_Transmit”中,当发生中断的时候跳转到“HAL_I2C_Master_Transmit_IT”中,对句柄变量hi2c指向的变量写入有效数据,并及时跳出中断服务程序(这就是尽量只在中断服务程序中做必要的部分,让中断服务程序尽量短一些,这样有利于提高代码相应及时性),运行主程序的时候,就有机会运行HAL_I2C_Master_Transmit里面的大循环。