我使用STM32CubeMX生成初始化代码,使用LL库,这里只介绍跟i2c相关的部分,其他必要的初始化需要自己完成。芯片使用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的如下设置:
数据传输方向
通道极性
模式
外设地址模式
内存地址模式
外设数据大小
内存数据大小
我们需要自己添加:
传输数据个数
设置内存地址
设置外设地址
打开中断,根据需要选择传输完成、传输一半和传输错误
DMA的模式有两种:NORMAL和CIRCULAR。
CIRCULAR模式一旦开始传输,DMA控制器就会自动不停的从源地址拿数据发送到目的地址,不需要我们干预。由于是异步的,如果内存的数据多于1个,有可能出现内存数据一部分新一部分旧的情况,导致数据不同步,如果各个数据之间独立还好,如果是一个整体就会出问题,所以要根据实际需求决定是否使用这种方式。
NORMAL模式发送一次后就停止了,如果还要发送就需要我们先关闭DMA通道,设置传输的数据个数,再打开通道,循环往复。本例使用这种方式,在中断中处理三个过程,稍后介绍。
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);
地址必须是偶数,这里使用了双地址,地址2是0,这样0和5A都可以通信。
I2C在通信过程中会产生很多中断,比如地址匹配、NACK、STOP、错误、溢出等等,这里根据需要只开启地址匹配中断(ADDR),一旦检测到地址匹配,我们就开启DMA传输数据,其他的事情交给DMA处理。
最后两个分别是启用 DMA 接收请求和启用 DMA 发送请求,只有开启它们DMA和I2C才能关联上。
到此初始化基本完成。
2、中断处理程序
1、I2C中断处理程序
这里就判断是否地址匹配,如果匹配,判断是读还是写,这里读写以主机视角确定,如果是WRITE,说明从机此时要接收数据。(这里我发现不同的版本和系列定义的还不一样,使用的时候要注意。)
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 */
}
这里根据方向开启对应的DMA通道,清除ADDR标志,之后数据就自动通过DMA传输了。
2、DMA中断处理程序
这里由于通道2和3公用一个中断,所以要先判断是谁触发的中断,然后清除对应的中断标志。前面我们设置的是DMA传输完成中断,所以进入这里就表面数据传完了。由于我们使用的是NORMAL模式,所以我在这个回调里关闭通道并重设传输的数据个数。我之所以放到这里是考虑到传输玩数据后一般会有个间隔,这段时间没事干就处理一下这些必要的事情,等下次想要传输的时候直接打开就行。(前面在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 */
}
在接收中断中有一个回调函数
I2C_SlaveDMARxCpltCallback(),里边是用户自定义程序,你想收到数据干啥就可以在这里边处理。
剩下所要做的事情就是准备好要发送的数据和使用收到的数据就行了。
最近发现使用DMA真的很方便,尤其在发送或接收多个数据的时候,就不用for循环了,这样既能收发大量数据,还不会占用CPU时间,效率大大提高。