Some time ago I have written SPL vs HAL: which one should you use where I have focused on differences between two main frameworks for STM32 — Standard Peripheral Library (SPL) and Hardware Abstraction Layer commonly known as HAL. Since the recent post only focuses on those two sets of libraries I have decided to write some examples which can tip the scale. What is more, at the end of previous article I have asked an important question for a developer — does the STM is going to introduce us to a brand new library. Answer to this and other questions are further in this post.
Different approach
HAL was written for developers who would like to make things quicker. However, the right reasons were not quite forged into code. One of the examples is I2C.
I2C start and stop functions
In some cases you would like to have a complete control over I2C communication. Generally, it might be a bad idea but sometimes there is no other way. In SPL if you would like to generate START signal on I2C bus you would use
void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);
and analogically for STOP
void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);
You can even control the process of sending ACK flag by using:
void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);
If you are using HAL you won’t find an easy way to accomplish the same thing. What is more, if you would like to send two packages of data where the first one is some address and the second one is a portion of data. This way of handling communication is very common for memory chips like EEPROM with I2C communication protocol. Firstly, you send address where you would like to write some followed by a portion of data. Here HAL comes really handy, but only to some point. You can use those functions to simply send some portion of data:
HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout); HAL_StatusTypeDef HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);
and following to send address and a portion of data:
HAL_StatusTypeDef HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout); HAL_StatusTypeDef HAL_I2C_Mem_Read(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout);
The idea is pretty compelling. You can divide the buffers and you save time and memory for concatenation of data. But what can you do if the address part is not really an address but some another portion of data. This is very unlikely but it can happen from time to time with custom implementation. In this situation you have to concatenate the data and after that you can transmit it as a single package in a single transition. HAL does not support this out of the box.
Documentation is not always everything
Everyone who used SPL at least ones knows that the lack of a good documentation is a serious problem. The main source of how to use the libraries can be found in examples. However, the examples provided along with the library itself is not enough. The examples indeed show how to use a single peripheral like ADC, even you can find more sophisticated examples when a few of peripherals are being used like a UART in DMA mode with ADC. If you want to do something more than that, which usually is the case, you are left alone. Also if you would like to configure, for example, ADC in slightly different mode you end up with not only going through the documentation of the MCU but you also have to go through library code. Basically, you do the same job twice! With HAL the situation is a bit better. Now, you have a nice PDF file which describes most of the functions. The documentation is divided into parts depending on which peripheral it concerns. Now, each chapter has some introduction which describes implemented modes of operation and how to configure them. Also the work flow, how to start, use and stop peripheral, is described. Still there are examples which are huge help but are limited to basic operation modes and peripherals.
The examples are divided into groups by the development board and then each is divided into peripheral groups. If you want to use some peripheral in specific mode it is very likely that you will have to jump through all the available board because the examples, the quantity of examples, are different depending the board. It would not be that strange if only the different MCUs would support different peripherals but as it turns out some development boards were treated more profusely.
Undocumented stuff
Unfortunately, if you would like to lean on documentation and examples you will find out that it is not enough. Let me give you an example based on my recent work. I would like to use UART in circular DMA mode. The task was to read data from UART in DMA mode to save some CPU cycles, analyse them and send them via second UART. The problem with circular mode is that you do not know where in a buffer DMA exactly is. Since it is circular it will go round and round. You can go through the HAL documentation in vain and you will not find how to solve the problem easily with out much of alteration of your concept. The last resort is always the documentation of MCU. You will find that there is a register in DMA that holds current number of bytes, depending on the configuration mode, that allows you to navigate exactly where you are in the buffer with some simple arithmetic. The name of the register is NDTR. You can simply reference to it and read how much bytes is left to be processed. But the moment you will do this you end up with code which has another thing to change when you want to move to a different MCU. I like to dig through the documentation of HAL, sometimes I look with approbate sometimes there are tears in my eyes. But that time I have found this:
/** * @brief Returns the number of remaining data units in the current DMAy Streamx transfer. * @param __HANDLE__: DMA handle * * @retval The number of remaining data units in the current DMA Stream transfer. */ #define __HAL_DMA_GET_COUNTER(__HANDLE__) ((__HANDLE__)->Instance->NDTR)
Well, this is perfect but you will not find it in documentation! With that you are just one step ahead to write a few neat lines of code to know exactly where DMA is.
position = SIZE_BUFFER - __HAL_DMA_GET_COUNTER(&hdma_usartX_rx); if (position != last_position) ...
And you are good to go!
Everything changes
Some time ago I was writing a custom bootloader for STM32F1. To my surprise there was no substitute for NVIC_SetVectorTable(). While this is present in SPL generation you will not find it inside HAL. So I ended up using
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;
This is an example for how the philosophy of libraries for MCUs is changing.
Portability
The biggest advantage of HAL is portability. I have mentioned it in previous paragraph. When you want to migrate a project to different MCU form the same family or even with different Cortex the HAL is the best choice. There is more than 90% of chance that the project will work without any alterations! Of course, it depends on how different chip you are going to use is. What is more if you have your project in STMCube you can migrate the project. You have to create a clean target project and select Import project and the wizard will walk you through.
The wizard will highlight the differences and will propose a solution. While migrating you have to keep attention to the details! Also the Import project icon can be greyed out. This is because the new project was altered. You can not play with the configuration. It has to be clean for importing the project.
(Not so bright) future of HAL
In my previous post I have wondered if STMicroelectronics is going to support HAL longer than it did SPL. For now STM is offering great support and is fixing bugs. But as it appears it came to a conclusion, under pressure of developers, that it will do something about it. Instead of reshaping the HAL and make it more lightweight it decided to introduce developers to something called Low Layer Library. You can already use it. And one of the features is that you can mix HAL code with LL. Here is the proof:
You can select how each peripheral will be handled — with HAL or LL. This is a complete change of concept of programming with STM32. STM claims that the LL is using less resources in comparison to HAL but nothing comes without price. The code written in LL is not as much portable as HAL and most of all it is not supported through out all STM MCU families.
The LL was divided into three levels of APIs:
- low level,
- middle level,
- high level.
The low level is for direct register operations while middle level is more like setting a flag in peripheral’s register such as ADC or timer. The high level of LL is the closest one to the HAL. It is responsible for configuration and initialization of peripherals.
Lately, I have attended a STM workshop and they have displayed following comparison of HAL and LL:
HAL vs LL:
- +++ / + portability,
- + / +++ optimization (memory & MIPS),
- ++ / + easy,
- +++ / ++ readiness,
- +++ / ++ hardware coverage.
Actual comparison I will leave to the reader. There is no doubt that LL is much more tailor-made in comparison to HAL but a whole new set of libraries may not be so warmly welcomed — time will show.
The most important thing about LL is that you can try to migrate to it by only changing the prefix from HAL_ to LL_. It, however, refers mostly to the high level API of LL. Still it will not work with every function!
It’s not a bug it’s a feature!
It is said that in every 1000 lines of code there is at least one bug. The whole HAL and LL have a lot more than that. But still, only when you do nothing you can avoid mistakes.
Each component of STM ecosystem has some “features”. The STMCubeMX, for example, consumes a whole CPU core when you switch to Clock Configuration tab, at least for version 4.17.0 and some previous.
Also, be very cautious when you are using three UARTs in DMA configuration because two of them might not work at all. But if you make a small workaround, like reading status and data register to warm it up, they will 😉