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:
  1. Peripheral to memory
  2. Memory to peripheral
  3. 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!

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:

  • 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)
  ;
}


<< PREVIOUS

Comments

Share your comments with me

Archive

Contact Form

Send