Here's the overview of what is to be followed from here on :-
We will write the device driver layer and the first towards is to write the MCU Specific Header File. Let's begin :-
It contains the details of the MCU such as :-
1. Base Addresses of the memories present in the MCU (such as FLASH, SRAM1, SRAM2).
2. Base Addresses of the bus domains such as AHB1, APB2, APB1.
3. Base Addresses of the various peripherals hanging the different buses.
4. Clock enable and disable of various macros.
5. IRQ Definitions.
6. Peripheral Registration Structure & register bit definitions.
7. Other useful MACROS.
Assigning them in header file.
Following is the memory map of the MCU
To find out, we will go the memory map of the MCU and then go to the TIM2 peripheral registers. And we find that at 0x4000000 which is the starting address of peripherals.
Now, let's see the offsets of different buses from the starting address 0x40000000.
For ex, we are developing drivers for GPIO, then it should be applicable to all the GPIO ports in the MCU.
SPI2 and SPI3 hang on APB1 while SP1 hangs on APB1.
6 USART peripherals.
EXTI is for external interrupt
Let's define now, the base address of each and every peripheral that we are going to use.
To do so, since we have already defined the base-address of individual buses, let's add the offset for the peripherals. For ex, GPIOA is the first peripheral on AHB1 bus. Thus, we simply add 0x0000 0000 to the address of AHB1 bus.
We are only interested in peripherals that we have listed before
NOTE:
1. Writing MACROS in CAPS is a good embedded programming style.
2. We can also use suffixes like DRV_I2C.. so that we know that which layer this MACRO belongs to.
Our next task is to understand how to add the addresses of peripheral register. We will derive the base-address of each & every register from the base address of the peripheral.
For example, to find the base-address of ModeReg, we will just add its offset from base address of GPIOA.
Note: Each hex address holds 8 bits that we can configure in any manner. Thus, a memory address given in hex, if we go to it, we will find 8 bits lying and we change them to represent different configuration. If we notice here, each register covers memory address from 0x00 to 0x04 say, means 4 chunks of memory in hex. Means four 8 bits or 32 bits.
Here, we will understand the need to make C-structure for each Peripheral.
Say, if i want to configure the GPIOA port, then one way to do is to assign MACROS to address of each and every register, that would make my task tedious. Instead, if i make a C-Structure for the peripheral, accessing all the register would become very easier. For instance,
After creating this structure, we create a pointer variable that stores the addresses of data type GPIO_RegDef_t. Say GPIO_RegDef_t *pGPIOA. This pointer is initialized to the base-address of GPIOA by type-casting the address 0x40020000 to a type GPIO_RegDef_t which is (*GPIO_RegDef_t)(0x40020000). Now, whenver we will write GPIOA, we are now already pointing to the address of GPIOA peripheral and since our pointer stores this address for a data-type GPIO_RegDef_t, now if we further derefrence this pointer, we can access the further register. For ex,
Now, instead of the statement of pointer declaration, i would do this. I will define a MACRO named GPIOA which is type-casted value of GPIOA_Base_Address.
So, i will one by one, from the memory map of registers, will copy and paste the register names.
Also, we should define these registers as volatile because some register may change very quick. Like IDR (Input data register) will change its value after every AHB1 clock cycle. So, we need to define these registers as volatile.
Clock is controlled by RCC Register, so let's start doing the register definition of RCC reg so that we can access it right away. After copying the name of registers from here and defining them in the structure. Let's give a MACRO name to the base-address of RCC which is actually hanging on the AHB1 bus. Now, to switch ON the clock for GOPIA, all we have to do is to set the bit 0 of 32-bit AHB1ENR of the RCC as it corresponds to GPIOA.
The user application is gonna use this driver layer. So it may happen that user application demands to use different ports or it may want to change pin number or modes, etc. So ultimately, we will be developing driver APIs that the user application will use. So the driver layer should give a configuration structure to the user application. So the user application will just initialize or fill that structure and then it will pop the driver api will then decode that structure.
And all these functions that our user application may call, i am defining them in the gpio_driver.h file. Like GPIO_Init(). DeInit(),etc. Afterwards, we copy them into the gpio_drivers.c file and carry along to code them.
Like GPIO_Init() function is required to set the pin mode, pullup_pulldown, alternate func mode, etc. So, we first define MACROS for such modes :-
#define GPIO_MODE_IN 0
#define GPIO_MODE_OUT 1
#define GPIO_MODE_ALTFN 2
#define GPIO_MODE_ANALOG 3
#define GPIO_MODE_IT_FT 4
#define GPIO_MODE_IT_RT 5
#define GPIO_MODE_IT_RFT 6
Similarly, if i were to write MACROS for Output type register then, i would see that 0 is for Push-Pull config while 1 is for Open-Drain, etc
Moreover, you must do the documentation well. For ex, GPIO_Pin_Number of GPIO_PinConfig_t is going to have arguments from the Mode macros we define.
uint8_t temp1, temp2;
temp1 = pGPIOHandle->GPIO_PinConfig.GPIO_PinNumber / 8;
temp2 = pGPIOHandle->GPIO_PinConfig.GPIO_PinNumber % 8;
pGPIOHandle->pGPIOx->AFR[temp1] &= ~(0xF << ( 4 * temp2 ) ); //clearing
pGPIOHandle->pGPIOx->AFR[temp1] |= (pGPIOHandle->GPIO_PinConfig.GPIO_PinAltFunMode << ( 4 * temp2 ) );
#define GPIOA_REG_RESET() do{ (RCC->AHB1RSTR |= (1 << 0)); (RCC->AHB1RSTR &= ~(1 << 0)); }while(0)
uint8_t value;
value = uint8_t (pGPIO->IDR >> PinNumber)*0x00000001;
return value;
if(Value == GPIO_PIN_RESET)
{
pGPIOx->ODR &= ~(1 << PinNumber);
}
else
{
pGPIOx->ODR |= (1 << PinNumber);
}