最近在研究STM32的IIC接口,严格来说,是STM32做为IIC或者I2C通信的从机。STM32做主机是很简单,意法半导体的HAL库中也提供了很多很方便使用的函数。
但是STM32做IIC从机,这个之前确实用的少。但研究了一下,发现用法不是很难,现记录如下。
一、通用配置
本次使用的 STM32G030F6 单片机,开启SWD调试接口,时钟使用芯片内部的RC时钟,倍频后64MHZ。


二、IIC接口配置
STM32G030F6有两个IIC接口,这里使用的是I2C1接口,STM32CUBEMX中的配置如下。
2.1 I2C接口参数
相关参数如下图所示:

主要看3个地方:
- 1,速度模式,我选了标准模式,然后速率选的是100KHZ。
- 2、上升时间和下降时间,这里写的是100ns,应该是默认值。
- 3、从设备主地址,范围是0~127,一定要和主机的地址一致。
2.2 IO上拉电阻配置
通常来说,设计PCB的时候,会在板子上给IIC从设备加上上拉电阻。但随着单片机的配置升高,有的型号的IIC接口自带上拉电阻。所以,具体情况根据电路实际情况判断。我这里开启了上拉电阻:

原因很简单,调试过程中,我是让一个STM32F103C8T6的最小系统板做为IIC的主机,和从机通信。而这个最小系统板上的IIC接口没有外接上拉电阻,IIC模式下也没有上拉电阻(F103系列和G0系列的IIC的硬件差别),如下图:

所以这种情况下,我必须要开启STM32G030的上拉电阻。
2.3 开启中断
因为是从机,所以要开启IIC的中断:

还有这里:

确保数据收发及时。
三、驱动程序
接下来是IIC从机相关的程序,HAL库中准备了各种现成的函数。
3.1 开启中断
首先是开启侦听中断,在初始化部分:
HAL_I2C_EnableListen_IT(&hi2c1);

这个函数的功能主要有两个:
- 1、使能侦听模式,I2C 设备处于侦听模式时,它会持续监听 I2C 总线上的信号。对于从机而言,这意味着它时刻准备响应主机发送的地址和数据。
- 2、开启中断功能,开启中断后,当 I2C 总线上发生特定事件(如地址匹配、数据接收完成等)时,硬件会触发中断。在中断服务程序中,可以对这些事件进行相应处理。
3.2 侦听完成回调函数
IIC做为从机时,中断相应的回调函数较多。首先是侦听完成回调函数,即一次完整的IIC通信完成之后,会进入这个中断回调函数。
// 侦听完成回调函数
void HAL_I2C_ListenCpltCallback(I2C_HandleTypeDef *hi2c)
{
// 完成一次通信,清除状态
first_byte_state = 1;
offset = 0;
re_flag = 1;
addr_change_num = addr_change_num + get_ring_length();
HAL_I2C_EnableListen_IT(&hi2c1); // slave is ready again
}
first_byte_state定义时默认为1,用来判断是否是第一个收到的值,即寄存器地址。
Offset用来表示当前寄存器的地址。
其它的操作是我解析数据用的,不用过多关心。
3.3 地址匹配回调函数
即从机的硬件地址和主机发送的硬件地址一致的时候,会进入这个中断:
// I2C设备地址回调函数
// I2C设备地址回调函数
void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode)
{
if(TransferDirection == I2C_DIRECTION_TRANSMIT)
{// 主机发送,从机接收
if(first_byte_state)
{// 准备接收第1个字节数据
HAL_I2C_Slave_Seq_Receive_IT(&hi2c1, &offset, 1, I2C_NEXT_FRAME); // 每次第1个数据均为偏移地址
}
}
else
{// 主机接收,从机发送
HAL_I2C_Slave_Seq_Transmit_IT(&hi2c1, &ram[offset], 1, I2C_NEXT_FRAME); // 打开中断并把ram[]里面对应的数据发送给主机
}
}
中断中会判断数据传输方向,是写入还是读出,并以此执行后面的操作。
3.4 数据接收完成回调函数
字面意思:接收时,数据收完会进入这个中断回调函数。先看函数:
// I2C数据接收回调函数
void HAL_I2C_SlaveRxCpltCallback(I2C_HandleTypeDef *hi2c)
{
if(first_byte_state)
{// 收到的第1个字节数据(偏移地址)
first_byte_state = 0;
}
else
{// 收到的第N个字节数据
offset++; // 每收到一个数据,偏移+1
}
// 打开I2C中断接收,下一个收到的数据将存放到ram[offset]
//HAL_I2C_Slave_Seq_Receive_IT(&hi2c1, &ram[offset], sizeof(ram), I2C_NEXT_FRAME); // 接收数据存到ram[]里面对应的位置
HAL_I2C_Slave_Seq_Receive_IT(&hi2c1, &ram[offset], 1, I2C_NEXT_FRAME); // 接收数据存到ram[]里面对应的位置
}
程序中有这么一句:
HAL_I2C_Slave_Seq_Receive_IT(&hi2c1, &ram[offset], 1, I2C_NEXT_FRAME);
就是说,收到一个数据时,会进入该回调函数。
当然,也可以改成这样(被我注释掉的那句):
HAL_I2C_Slave_Seq_Receive_IT(&hi2c1, &ram[offset], sizeof(ram), I2C_NEXT_FRAME);
ram是一个128元素的数组,即收到128个数据才会进入该回调函数。
具体数量根据需求调整。
3.5 发送完成回调函数
即从机发送完指定的数据个数后,进入该回调函数:
// I2C数据发送回调函数
void HAL_I2C_SlaveTxCpltCallback(I2C_HandleTypeDef *hi2c)
{
offset++; // 每发送一个数据,偏移+1
HAL_I2C_Slave_Seq_Transmit_IT(&hi2c1, &ram[offset], sizeof(ram), I2C_NEXT_FRAME); // 打开中断并把ram[]里面对应的数据发送给主机
}
当主机要从从机中读取某些数据时,会用到这个回调函数。
3.6 故障回调函数
当I2C总线,无论主机还是从机,通信过程中发送故障的时候,会调用该函数。在这个函数中,可以做一些处理,例如记录故障信息,重启相应的总线接口。
void HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c)
{
// 检查是哪个 I2C 外设发生了错误
if (hi2c == &hi2c1)
{
// 记录错误信息
// 这里可以使用串口打印错误信息
printf("I2C1 communication error! Error code: %d\n", hi2c->ErrorCode);
// 重置 I2C 外设
HAL_I2C_DeInit(hi2c);
HAL_I2C_Init(hi2c);
}
else if (hi2c == &hi2c2)
{
// 处理 I2C2 的错误
printf("I2C2 communication error! Error code: %d\n", hi2c->ErrorCode);
HAL_I2C_DeInit(hi2c);
HAL_I2C_Init(hi2c);
}
}
四、注意事项
注意事项主要有两个,一个是上拉电阻的事情,前面已经提过了。另一个是硬件接线的问题,如果主机和从机在两个不同的板子上,接线的时候尽量要短,否则信号容易失真。
主要事项就这样,上电工作的图片或记录就不发了。程序肯定能用,实在有问题的可以留言,我再分享源码。
我是单片机爱好者-MCU起航,打完收工!