我使用STM32CubeMX生成初始化代碼,使用LL庫,這里只介紹跟i2c相關(guān)的部分,其他必要的初始化需要自己完成。芯片使用stm32f042。本文的代碼不能到手即用,只提供思路。
1、初始化
初始化部分包括GPIO、DMA、I2C等。
1、GPIO
這部分自動生成就OK,一般不需要作修改;
LL_GPIO_InitTypeDef GPIO_InitStruct = {0};
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOA);
/**I2C1 GPIO Configuration
PA9 ------> I2C1_SCL
PA10 ------> I2C1_SDA
*/
GPIO_InitStruct.Pin = LL_GPIO_PIN_9;
GPIO_InitStruct.Mode = LL_GPIO_MODE_ALTERNATE;
GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_OPENDRAIN;
GPIO_InitStruct.Pull = LL_GPIO_PULL_UP;
GPIO_InitStruct.Alternate = LL_GPIO_AF_4;
LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = LL_GPIO_PIN_10;
GPIO_InitStruct.Mode = LL_GPIO_MODE_ALTERNATE;
GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_OPENDRAIN;
GPIO_InitStruct.Pull = LL_GPIO_PULL_UP;
GPIO_InitStruct.Alternate = LL_GPIO_AF_4;
LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
2、DMA
DMA的初始化自動生成的程序會分為兩個部分:
第一個部分如下,會打開時鐘、初始化中斷:
void MX_DMA_Init(void)
{
/* Init with LL driver */
/* DMA controller clock enable */
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA1);
/* DMA interrupt init */
/* DMA1_Channel2_3_IRQn interrupt configuration */
NVIC_SetPriority(DMA1_Channel2_3_IRQn, 0);
NVIC_EnableIRQ(DMA1_Channel2_3_IRQn);
}
第二部分在I2C的初始化程序中
LL_DMA_SetDataTransferDirection(DMA1, LL_DMA_CHANNEL_3, LL_DMA_DIRECTION_PERIPH_TO_MEMORY);
LL_DMA_SetChannelPriorityLevel(DMA1, LL_DMA_CHANNEL_3, LL_DMA_PRIORITY_HIGH);
LL_DMA_SetMode(DMA1, LL_DMA_CHANNEL_3, LL_DMA_MODE_NORMAL);
LL_DMA_SetPeriphIncMode(DMA1, LL_DMA_CHANNEL_3, LL_DMA_PERIPH_NOINCREMENT);
LL_DMA_SetMemoryIncMode(DMA1, LL_DMA_CHANNEL_3, LL_DMA_MEMORY_INCREMENT);
LL_DMA_SetPeriphSize(DMA1, LL_DMA_CHANNEL_3, LL_DMA_PDATAALIGN_BYTE);
LL_DMA_SetMemorySize(DMA1, LL_DMA_CHANNEL_3, LL_DMA_MDATAALIGN_BYTE);
//上面是自動生成的,下面的部分需要自己添加
LL_DMA_SetDataLength(DMA1,LL_DMA_CHANNEL_3,5);
LL_DMA_SetMemoryAddress(DMA1,LL_DMA_CHANNEL_3,(uint32_t)i2cDataRx);
LL_DMA_SetPeriphAddress(DMA1,LL_DMA_CHANNEL_3,LL_I2C_DMA_GetRegAddr(I2C1,LL_I2C_DMA_REG_DATA_RECEIVE));
LL_DMA_EnableIT_TC(DMA1,LL_DMA_CHANNEL_3);
自動生成程序會完成DMA的如下設(shè)置:
- 數(shù)據(jù)傳輸方向
- 通道極性
- 模式
- 外設(shè)地址模式
- 內(nèi)存地址模式
- 外設(shè)數(shù)據(jù)大小
- 內(nèi)存數(shù)據(jù)大小
我們需要自己添加:
- 傳輸數(shù)據(jù)個數(shù)
- 設(shè)置內(nèi)存地址
- 設(shè)置外設(shè)地址
- 打開中斷,根據(jù)需要選擇傳輸完成、傳輸一半和傳輸錯誤
DMA的模式有兩種:NORMAL和CIRCULAR。
CIRCULAR模式一旦開始傳輸,DMA控制器就會自動不停的從源地址拿數(shù)據(jù)發(fā)送到目的地址,不需要我們干預(yù)。由于是異步的,如果內(nèi)存的數(shù)據(jù)多于1個,有可能出現(xiàn)內(nèi)存數(shù)據(jù)一部分新一部分舊的情況,導(dǎo)致數(shù)據(jù)不同步,如果各個數(shù)據(jù)之間獨(dú)立還好,如果是一個整體就會出問題,所以要根據(jù)實(shí)際需求決定是否使用這種方式。
NORMAL模式發(fā)送一次后就停止了,如果還要發(fā)送就需要我們先關(guān)閉DMA通道,設(shè)置傳輸?shù)臄?shù)據(jù)個數(shù),再打開通道,循環(huán)往復(fù)。本例使用這種方式,在中斷中處理三個過程,稍后介紹。
3、I2C
I2C包括時鐘、中斷、地址、時鐘、模式等等
/* Peripheral clock enable */
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_I2C1);
/* I2C1 interrupt Init */
NVIC_SetPriority(I2C1_IRQn, 0);
NVIC_EnableIRQ(I2C1_IRQn);
/* USER CODE BEGIN I2C1_Init 1 */
/* USER CODE END I2C1_Init 1 */
/** I2C Initialization
*/
LL_I2C_DisableGeneralCall(I2C1);
LL_I2C_EnableClockStretching(I2C1);
I2C_InitStruct.PeripheralMode = LL_I2C_MODE_I2C;
I2C_InitStruct.Timing = 0x2000090E;
I2C_InitStruct.AnalogFilter = LL_I2C_ANALOGFILTER_ENABLE;
I2C_InitStruct.DigitalFilter = 0;
I2C_InitStruct.OwnAddress1 = 0x5A;
I2C_InitStruct.TypeAcknowledge = LL_I2C_ACK;
I2C_InitStruct.OwnAddrSize = LL_I2C_OWNADDRESS1_7BIT;
LL_I2C_Init(I2C1, &I2C_InitStruct);
LL_I2C_EnableAutoEndMode(I2C1);
LL_I2C_SetOwnAddress2(I2C1, 0, LL_I2C_OWNADDRESS2_NOMASK);
LL_I2C_EnableOwnAddress2(I2C1);
//上面的部分是自動生成的,下面是自己添加的
/* USER CODE BEGIN I2C1_Init 2 */
LL_I2C_Enable(I2C1);
LL_I2C_EnableIT_ADDR(I2C1);
// LL_I2C_EnableIT_ERR(I2C1);
LL_I2C_EnableDMAReq_RX(I2C1);
LL_I2C_EnableDMAReq_TX(I2C1);
地址必須是偶數(shù),這里使用了雙地址,地址2是0,這樣0和5A都可以通信。
I2C在通信過程中會產(chǎn)生很多中斷,比如地址匹配、NACK、STOP、錯誤、溢出等等,這里根據(jù)需要只開啟地址匹配中斷(ADDR),一旦檢測到地址匹配,我們就開啟DMA傳輸數(shù)據(jù),其他的事情交給DMA處理。
最后兩個分別是啟用 DMA 接收請求和啟用 DMA 發(fā)送請求,只有開啟它們DMA和I2C才能關(guān)聯(lián)上。
到此初始化基本完成。
2、中斷處理程序
1、I2C中斷處理程序
這里就判斷是否地址匹配,如果匹配,判斷是讀還是寫,這里讀寫以主機(jī)視角確定,如果是WRITE,說明從機(jī)此時要接收數(shù)據(jù)。(這里我發(fā)現(xiàn)不同的版本和系列定義的還不一樣,使用的時候要注意。)
void I2C1_IRQHandler(void)
{
/* USER CODE BEGIN I2C1_IRQn 0 */
if(LL_I2C_IsActiveFlag_ADDR(I2C1))
{
if(LL_I2C_GetTransferDirection(I2C1) == LL_I2C_DIRECTION_WRITE)
{
LL_DMA_EnableChannel(DMA1,LL_DMA_CHANNEL_3);
}
else
{
LL_DMA_EnableChannel(DMA1,LL_DMA_CHANNEL_2);
}
/* Clear ADDR flag value in ISR register */
LL_I2C_ClearFlag_ADDR(I2C1);
}
/* USER CODE END I2C1_IRQn 0 */
/* USER CODE BEGIN I2C1_IRQn 1 */
/* USER CODE END I2C1_IRQn 1 */
}
這里根據(jù)方向開啟對應(yīng)的DMA通道,清除ADDR標(biāo)志,之后數(shù)據(jù)就自動通過DMA傳輸了。
2、DMA中斷處理程序
這里由于通道2和3公用一個中斷,所以要先判斷是誰觸發(fā)的中斷,然后清除對應(yīng)的中斷標(biāo)志。前面我們設(shè)置的是DMA傳輸完成中斷,所以進(jìn)入這里就表面數(shù)據(jù)傳完了。由于我們使用的是NORMAL模式,所以我在這個回調(diào)里關(guān)閉通道并重設(shè)傳輸?shù)臄?shù)據(jù)個數(shù)。我之所以放到這里是考慮到傳輸玩數(shù)據(jù)后一般會有個間隔,這段時間沒事干就處理一下這些必要的事情,等下次想要傳輸?shù)臅r候直接打開就行。(前面在i2c中斷程序里我們可以看到打開通道的代碼。)你要是無所謂都放到I2C的中斷里也可以的。
void DMA1_Channel2_3_IRQHandler(void)
{
/* USER CODE BEGIN DMA1_Channel2_3_IRQn 0 */
if(LL_DMA_IsActiveFlag_TC3(DMA1))
{
//LL_DMA_ClearFlag_GI3(DMA1);
LL_DMA_ClearFlag_TC3(DMA1);
I2C_SlaveDMARxCpltCallback();
LL_DMA_DisableChannel(DMA1,LL_DMA_CHANNEL_3);
LL_DMA_SetDataLength(DMA1,LL_DMA_CHANNEL_3,5);
}
else if (LL_DMA_IsActiveFlag_TC2(DMA1))
{
LL_DMA_ClearFlag_TC2(DMA1);
LL_DMA_DisableChannel(DMA1,LL_DMA_CHANNEL_2);
LL_DMA_SetDataLength(DMA1,LL_DMA_CHANNEL_2,5);
}
/* USER CODE END DMA1_Channel2_3_IRQn 0 */
/* USER CODE BEGIN DMA1_Channel2_3_IRQn 1 */
/* USER CODE END DMA1_Channel2_3_IRQn 1 */
}
在接收中斷中有一個回調(diào)函數(shù)
I2C_SlaveDMARxCpltCallback(),里邊是用戶自定義程序,你想收到數(shù)據(jù)干啥就可以在這里邊處理。
剩下所要做的事情就是準(zhǔn)備好要發(fā)送的數(shù)據(jù)和使用收到的數(shù)據(jù)就行了。
最近發(fā)現(xiàn)使用DMA真的很方便,尤其在發(fā)送或接收多個數(shù)據(jù)的時候,就不用for循環(huán)了,這樣既能收發(fā)大量數(shù)據(jù),還不會占用CPU時間,效率大大提高。
-
I2C
+關(guān)注
關(guān)注
28文章
1452瀏覽量
122248 -
dma
+關(guān)注
關(guān)注
3文章
552瀏覽量
99927 -
代碼
+關(guān)注
關(guān)注
30文章
4671瀏覽量
67765 -
GPIO
+關(guān)注
關(guān)注
16文章
1175瀏覽量
51513 -
stm32cubemx
+關(guān)注
關(guān)注
5文章
278瀏覽量
14619
發(fā)布評論請先 登錄
相關(guān)推薦
評論