Below, we first explain several bit operators, and then explain the techniques for using bit operations. C language supports the following 6 mid-bit operations:
Six bit operations
Next, we would like to focus on explaining some practical skills of bit operations in microcontroller development.
1.1 Set the value of certain bits without changing the value of other bits
This scenario is often used in microcontroller development. The method is to first clear the bits that need to be set using the & operator, and then use the | operator to set the value.
For example, if I want to change the status of GPIOA, I can first clear the register value:
Then perform |OR operation with the value that needs to be set:
1.2 Shift operation improves code readability
Shift operations are very important in microcontroller development. The following is a line of code for the delay_init function:
SysTick->CTRL |= 1 << 1;
This operation is to set the first bit of the CTRL register (counting from 0) to 1. Why do we need to shift left instead of directly setting a fixed value?
In fact, this is to improve the readability and reusability of the code. This line of code can be very intuitive and clear, setting bit 1 to 1. If written:
SysTick->CTRL |= 0X0002;
Although this can achieve the same effect, it is slightly less readable and more troublesome to modify.
1.3 Tips for using bitwise inversion operation
Bitwise inversion is often used when setting registers, and is often used to clear one/several bits. The following is a line of code for the delay_us function:
SysTick->CTRL &= ~(1 << 0) ; /* Close SYSTICK */
This code can be interpreted as setting only bit 0 (the lowest bit) of the CTRL register to 0, leaving the values of other bits unchanged.
Similarly, we do not use bitwise inversion and write the code as:
SysTick->CTRL &= 0XFFFFFFFE; /* Close SYSTICK */
It can be seen that the readability and maintainability of the former are much better than that of the latter.
1.4 ^Tips on using bitwise XOR operation
This function is very suitable for controlling the flipping of a certain bit. A common application scenario is to control LED flashing, such as:
GPIOB->ODR ^= 1 << 5;
Executing this code once will flip the output state of PB5 once. If our LED is connected to PB5, we can see the LED flashing.
define macro definition
define is a preprocessing command in C language. It is used for macro definition (defining constants), which can improve the readability of source code and provide convenience for programming. Common formats:
"Identifier" is the defined macro name. "String" can be a constant, an expression, a format string, etc. For example:
The value of the defined identifier HSE_VALUE is 8000000, and the U after the number means unsigned.
As for other knowledge about define macro definitions, such as macro definitions with parameters, we will not explain much here.
ifdef conditional compilation
During the development of microcontroller programs, we often encounter a situation where one set of statements is compiled when a certain condition is met, and another set of statements is compiled when the condition is not met.
The most common form of conditional compilation commands is:
#ifdef identifier program section 1 #else program section 2 #endif
Its function is: when the identifier has been defined (usually defined with the #define command), program segment 1 is compiled, otherwise program segment 2 is compiled. The #else part does not need to be included, that is:
#ifdef program section 1 #endif
Conditional compilation is used a lot in the HAL library. You can often see statements like this in the stm32mp1xx_hal_conf.h header file:
#if !defined (HSE_VALUE) #define HSE_VALUE 24000000U #endif
If the HSE_VALUE macro is not defined, the HSE_VALUE macro is defined, and the value of HSE_VALUE is 24000000U. Conditional compilation is also a basic knowledge of C language.
Let me mention here that the U in 24000000U represents unsigned integer type. Commonly, UL represents unsigned long integer type and F represents floating point type.
After adding U here, the system will not perform type checking when compiling, and directly assign the value to a corresponding memory in the form of U. If it exceeds the scope of the defined variable, it will be intercepted.
extern variable declaration
In C language, extern can be placed before a variable or function to indicate that the definition of the variable or function is in another file, prompting the compiler to find its definition in other modules when it encounters this variable or function.
It should be noted here that extern variables can be declared multiple times, but they can be defined only once. In our code you will see statements like this:
extern uint16_t g_usart_rx_sta;
This statement declares that the g_usart_rx_sta variable has been defined in other files and will be used here.
So, you can definitely find a statement with a variable definition somewhere:
uint16_t g_usart_rx_sta;
br
The use of extern is relatively simple, but it is used frequently and needs to be mastered.
typedef type alias
Typedef is used to create a new name, or type alias, for an existing type to simplify the definition of variables. Typedef is most commonly used in HAL libraries to define type aliases and enumeration types of structures.
struct _GPIO { __IO uint32_t CRL; __IO uint32_t CRH; … };
A structure GPIO is defined, so the way we define the structure variables is:
struct _GPIO gpiox; /* Define structure variable gpiox */
But this is very cumbersome. There are many such structure variables in the HAL library that need to be defined. Here we can define an alias GPIO_TypeDef for the structure, so that we can define structure variables through the alias GPIO_TypeDef elsewhere. The method is as follows:
typedef struct { __IO uint32_t CRL; __IO uint32_t CRH; … } GPIO_TypeDef;
Typedef defines an alias GPIO_TypeDef for the structure, so that we can define structure variables through GPIO_TypeDef: GPIO_TypeDef gpiox;
The GPIO_TypeDef here has the same function as struct _GPIO, but GPIO_TypeDef is much more convenient to use.