STM32L0/F0/F3 I2C : Part 3 RX TX via DMA
Most modern microcontroller come with a peripheral called DMA which allows for an even more hands-off approach. The Direct Memory Access controller will get a tutorial of it's own in the future. However, it is so simple to use that I can easily explain the required bits in this tutorial without feeling like I will overwhelm the reader. In this iteration of the I2C series I will cover how to TX and RX date on the peripheral in conjunction with the DMA controller.
First let me briefly explain in a high level what the DMA controller does. It allows you to transfer data in 3 ways:
- Peripheral to memory
- Memory to peripheral
- Memory to memory.
What does this mean? For example, when you receive data on I2C in your RXDR register, you can have the DMA controller move that data received into an array. At the end of the day your variables are just memory locations, so if you have declared an array it has an address associated with it, this is address is what the DMA considers "Memory". The DMA can move the received data from Peripheral(I2C) to Memory(variable/array). Likewise, if you have a set of data you want to transfer to the I2C. You can tell the DMA to move the data from your variable to a certain peripheral like I2C TXDR, in this case the DMA is moving data from Memory to Peripheral. Furthermore if you have some data in one location and you wish to transfer it to a different location or memory space, thr DMA can do this as well.
From that description you may already be asking yourself some questions like:
- How does the DMA know where my memory location is or what variable I want it to transfer?
Answer: There is a register where you enter the address of the memory.
- How does the DMA know what peripheral I am using or what register to read/write from/to?
Answer: There is a register for that, you simply enter the address of the peripheral register.
- How does the DMA know how many bytes I want to transfer or when to stop?
Answer: Yup you can figure this one out...........
- How does DMA know what cahnnel to use or how many channels I need?
Answer: There is a ........ you know
All in all the DMA peripheral has only 7 registers and they are easy peezy. I will not go over them here or every single bit because I will do that in its dedicated tutorial.
So on to the I2C related stuff. There are only two bits you need to modify in the I2C registers to enable the DMA RX and TX. These bits are in Control-Register-1 (CR1) the bits are TXDMAEN and RXDMAEN and they must be set in order to enable the DMA request. Since I have not gone over interrupts yet we are good to go, otherwise you would need to disable any I2C interrupts because they are not needed in DMA mode. That is the beauty of it. No interruptions, no ISR routines, no checking flags, no clearing flags.
Technically the flag for reception (RXNE) is set and the (TXIS) for transmission is also set when ever the peripheral is ready, however when the DMA bits are enabled these flags trigger the DMA to do its thing, thus we have no interaction with the flags. To make life easier we enable the auto end feature of the I2C and it takes care of closing the transmission and releasing the clock line.
There is one flag we have to be mindful of and that is a flag in the DMA and that is the (TCIF) This is the Transfre Complete Interrupt Flag and it tells use the the transfer is complete. Easy peezy lemon squeezy!
Bits we are concerned with: 0 , 1 , 4 , 7 all the other bits also are concerning but it happens that the values we need for those are the default 0 values so were good to go. Again I wont explain all those bits because I will make a tutorial for DMA later on.
Bit 0 : is used to enable the DMA and should be done absolutely last.
Bit 1 : is to enable the Transfer Complete flag which is useful to know.
Bit 4: is use to tell the DMA if it is reading from memory or peripheral, for TX we are reading from memory and sending it to peripheral. And for RX we are reading from peripheral and sending it to memory.
Bit 7: Tells the DMA to increment the memory address, this is important because are we receive data we want it to store it in the next available space, instead of over writing the same address over and over, that sort of behavior might be useful when reading an ADC value for example.
Technically the flag for reception (RXNE) is set and the (TXIS) for transmission is also set when ever the peripheral is ready, however when the DMA bits are enabled these flags trigger the DMA to do its thing, thus we have no interaction with the flags. To make life easier we enable the auto end feature of the I2C and it takes care of closing the transmission and releasing the clock line.
There is one flag we have to be mindful of and that is a flag in the DMA and that is the (TCIF) This is the Transfre Complete Interrupt Flag and it tells use the the transfer is complete. Easy peezy lemon squeezy!
DMA: Channel Configuration Register (DMA_CCR)
The most involved register we will deal with is the Channel Configuration Register (CCR) seen below.Bits we are concerned with: 0 , 1 , 4 , 7 all the other bits also are concerning but it happens that the values we need for those are the default 0 values so were good to go. Again I wont explain all those bits because I will make a tutorial for DMA later on.
Bit 0 : is used to enable the DMA and should be done absolutely last.
Bit 1 : is to enable the Transfer Complete flag which is useful to know.
Bit 4: is use to tell the DMA if it is reading from memory or peripheral, for TX we are reading from memory and sending it to peripheral. And for RX we are reading from peripheral and sending it to memory.
Bit 7: Tells the DMA to increment the memory address, this is important because are we receive data we want it to store it in the next available space, instead of over writing the same address over and over, that sort of behavior might be useful when reading an ADC value for example.
DMA : Channel Number Data Register (DMA_CNDTRx)
In this register we simply tell the DMA how many transfer to do.DMA: Channel Peripheral Address Register (DMA_CPARx)
Here you will enter the address of the peripheral register. For example TXDR or RXDR
DMA: Channel Memory Address Register (DMA_CMARx)
And you can guess that here is where you enter the address of where you want the DMA to move data to or from
DMA: Channel Selection Register (DMA_CSELR)
Here you tell the DMA which channel you are using and what function peripheral it will listen to.
Below the lines that differ from Part 2 are as follows:
It is important to note that we still have to set up the I2C with required things like memory address and how many bytes we want to send to the slave etc... this is not the purpose of DMA, the DMA is solely used to transfer data from memory to peripheral back and forth.
- Lines 45 to 71 is ALL new code related to DMA it is well commented so please read.
- Line 93 is where we wait for the DMA to be done doing its job.
- Lines 95 to 115 are commented out because they are no longer needed since the DMA handles it all for us.
- Line 145 we wait for the DMA to finish its job again.
It is important to note that we still have to set up the I2C with required things like memory address and how many bytes we want to send to the slave etc... this is not the purpose of DMA, the DMA is solely used to transfer data from memory to peripheral back and forth.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 | #include "Stm32l0xx.h" #include "stdint.h" #include "string.h" void setClockTo32Mhz(void); uint32_t msTICKS; void delayMs(uint32_t ms); void SysTick_Handler(void); int main(void) { setClockTo32Mhz(); SystemCoreClockUpdate(); SysTick_Config(SystemCoreClock / 1000); //some sample data buffers and counter variable char TX_data[] ={ 0, 1,2,3,4,5,6,7,8}; uint8_t num_bytes = 9;// strlen(TX_data) + 1; //get the length of the above data char RX_data[10] ={ 0,0,0,0,0,0,0,0,0,0}; // where we will store the received bytes uint8_t count = 0; // to keep track of bytes sent // i2c & GPIO clock enable RCC->APB1ENR |= RCC_APB1ENR_I2C1EN; // enable i2c1 RCC->IOPENR |= RCC_IOPENR_GPIOAEN; //enable GPIOA //gpio configure to alternate function open drian GPIOA->MODER |= GPIO_MODER_MODE9_1 | GPIO_MODER_MODE10_1; //alternate function mode GPIOA->MODER &= ~(GPIO_MODER_MODE9_0 | GPIO_MODER_MODE10_0);//alternate function mode GPIOA->OTYPER |= (1<<10) | (1<<9); // output open drain GPIOA->OSPEEDR |= GPIO_OSPEEDER_OSPEED10 | GPIO_OSPEEDER_OSPEED9; // high speed //enabling upll-ups-- but i dont need them so i have commented these lines //GPIOA->PUPDR |= GPIO_PUPDR_PUPD10_0 | GPIO_PUPDR_PUPD9_0; //GPIOA->PUPDR &= ~(GPIO_PUPDR_PUPD10_1 | GPIO_PUPDR_PUPD9_1); //chosing the alternate function 1 GPIOA->AFR[1] |= (1<<GPIO_AFRH_AFRH1_Pos) | (1<<GPIO_AFRH_AFRH2_Pos); // Alternate function 1 //---------------------------------------------------| dma |-------------------------- RCC->AHBENR |= RCC_AHBENR_DMA1EN; //enable DMA clock I2C1->CR1 |= I2C_CR1_TXDMAEN; //enable TX DMA in I2C I2C1->CR1 |= I2C_CR1_RXDMAEN; //enable RX DMA in I2C register //tx dma on channel 2 DMA1_CSELR->CSELR |= 6<<DMA_CSELR_C2S_Pos; //tell it to use channel 2 for I2C TX DMA1_Channel2->CMAR = (uint32_t)TX_data; // Give it memory address of my array TX_data DMA1_Channel2->CPAR = (uint32_t)&I2C1->TXDR; //give it address of the I2C data register DMA1_Channel2->CNDTR = 9;// tell it how many bytes i want to transfer DMA1_Channel2->CCR |= DMA_CCR_MINC; // tell it increment memory address after every transfer DMA1_Channel2->CCR |= DMA_CCR_DIR ; //direction = Read from memory DMA1_Channel2->CCR |= DMA_CCR_TCIE ; //enable transfer complete iterrupt on DMA DMA1_Channel2->CCR |= DMA_CCR_EN; //finally enable channel 2 //rx dma channel 3 DMA1_CSELR->CSELR |= 6<<DMA_CSELR_C3S_Pos; //tell it to use channel 3 for I2C RX DMA1_Channel3->CMAR = (uint32_t)RX_data; // give it address where it will store received data DMA1_Channel3->CPAR = (uint32_t)&I2C1->RXDR ; //address of I2C RXDR DMA1_Channel3->CNDTR =9; //how many bytes to read DMA1_Channel3->CCR |= DMA_CCR_MINC | DMA_CCR_TCIE ; //same as above but all in a single statement DMA1_Channel3->CCR |= DMA_CCR_EN; //enable channel 3 //---------------------------------------------------| dma |-------------------------- //timing register about 400Khz I2C1->TIMINGR = 0x0050101B; I2C1->CR2 |= 0xA0; //set slave address I2C1->CR2 &= ~(I2C_CR2_NBYTES); //clear nbytes I2C1->CR2 |= (num_bytes)<<I2C_CR2_NBYTES_Pos; // set num of bytes to send I2C1->CR2 |= (I2C_CR2_AUTOEND); //disable auto end, instead TC flag is set when NBYTES data are transferred I2C1->CR2 &= ~(I2C_CR2_ADD10); //7 bit addressing mode.. this is default just here for referance I2C1->CR2 &= ~(I2C_CR2_RD_WRN); //write operation.. this is default just here for referance I2C1->CR1 |= I2C_CR1_PE; //enable peripheral //first byte to sent to eeprom will be the memory address //where we want to store our data, in this case ill send //the data to address 0x00, with every byte I send the eeprom //increment its pointer and store the data I send in the next //sequential memory location. //first send the memory address where we want to start writing - dont confuse this with slave address I2C1->CR2 |= I2C_CR2_START; // send start condition while( ! (DMA1->ISR & DMA_ISR_TCIF2) ); // wait for DMA transfer to be complete //--------------------- This code where we check the flags and load data is no longer needed /* while(!((I2C1->ISR & I2C_ISR_TXE) == (I2C_ISR_TXE))); // wait for tx buffer to be empty/ready I2C1->TXDR = 0x00; // memory adress to start storing while(count < num_bytes -1) // N bytes transfered { //send bytes in TX_data while(!((I2C1->ISR & I2C_ISR_TXE) == (I2C_ISR_TXE))); // wait for tx buffer to be empty/ready temp[count] = TX_data[count]; I2C1->TXDR =TX_data[count++]; // send enxt byte } while(!((I2C1->ISR & I2C_ISR_TC) == (I2C_ISR_TC))); // N bytes transfered I2C1->CR2 |= I2C_CR2_STOP; */ delayMs(2); //-------- READING ---------------------------- count = 0; //----------------------------------------------------------------------------------------- // Here I am just doing a manual write operation of one byte to move the EEPROM pointer // to the address where I want it to start reading from I could have used DMA but its just 1 byte I2C1->CR1 &= ~I2C_CR1_PE; //disable peripheral I2C1->CR2 &= ~(I2C_CR2_NBYTES); //clear nbytes I2C1->CR2 |= 1<<I2C_CR2_NBYTES_Pos; //set nbytes to 1 because only need to send initial address to read from I2C1->CR2 &= ~(I2C_CR2_RD_WRN); //write operation I2C1->CR1 |= I2C_CR1_PE; //enable peripheral I2C1->CR2 &= ~(I2C_CR2_AUTOEND); //disable auto end, instead TC flag is set when NBYTES data are transferred I2C1->CR2 |= I2C_CR2_START; // send start condition while(!((I2C1->ISR & I2C_ISR_TXE) == (I2C_ISR_TXE))); // wait for tx buffer to be empty/ready I2C1->TXDR = 0x00; // send the address we will start from while(!((I2C1->ISR & I2C_ISR_TC) == (I2C_ISR_TC))); // wait for transfer complete //----------------------------------------------------------------------------------------- I2C1->CR2 &= ~(I2C_CR2_NBYTES); //clear nbytes I2C1->CR2 |= (num_bytes -1)<<I2C_CR2_NBYTES_Pos; //set nbytes I2C1->CR2 |= (I2C_CR2_RD_WRN); //read operation I2C1->CR2 |= I2C_CR2_AUTOEND; //enable auto end I2C1->CR2 |= I2C_CR2_START; // send start condition again while( ! (DMA1->ISR & DMA_ISR_TCIF2) ); // wait for DMA transfer to be complete //--------------------- This code where we check the flags and read data is no longer needed /* while(count < num_bytes) // keep track of how many bytes we have sent { while(!((I2C1->ISR & I2C_ISR_RXNE) == (I2C_ISR_RXNE))) { //handle timeout error here } // wait for tx buffer to be empty/ready RX_data[count++]= I2C1->RXDR; } */ //these 3 lines are not needed because we have enabled auto end feature //while(!((I2C1->ISR & I2C_ISR_TC) == (I2C_ISR_TC))); // wait for tx buffer to be empty/ready //I2C1->CR2 |= I2C_CR2_STOP; /* Go */ //I2C1->CR2 |= I2C_CR2_STOP; /* Go */ while(1) { } } void delayMs(uint32_t ms) { msTICKS = 0; while (msTICKS < ms) ; } void SysTick_Handler(void) { msTICKS++; } void setClockTo32Mhz(void) { //adjust flash latency FLASH->ACR |= FLASH_ACR_LATENCY; while ((FLASH->ACR & FLASH_ACR_LATENCY) == 0) ; //wait for latency set flag //set voltage scaling to range 1 PWR->CR |= PWR_CR_VOS_0; PWR->CR &= ~(PWR_CR_VOS_1); while (((PWR->CSR) & (PWR_CSR_VOSF)) == 1) ; //wait for voltage to settle //turn on HSE external, HSE bypass and security RCC->CR |= RCC_CR_CSSHSEON | RCC_CR_HSEBYP | RCC_CR_HSEON; while (((RCC->CR) & RCC_CR_HSERDY) == 0) ; //wait for the HSE to be ready //reset and configure pll mull and div settings, and PLL source RCC->CFGR = ((RCC->CFGR & ~(RCC_CFGR_PLLDIV | RCC_CFGR_PLLMUL)) | RCC_CFGR_PLLDIV2 | RCC_CFGR_PLLMUL8 | RCC_CFGR_PLLSRC_HSE); while ((RCC->CR & RCC_CR_PLLRDY) == 1) ; //turn on PLL , wait for ready RCC->CR |= RCC_CR_PLLON; while (((RCC->CR) & RCC_CR_PLLRDY) == 0) ; // wait for pll to ready //set PLL as system clock RCC->CFGR |= RCC_CFGR_SW_PLL; while (((RCC->CFGR) & (RCC_CFGR_SWS_PLL)) != RCC_CFGR_SWS_PLL) ; } |
Comments