第3章:主机模式配置——时钟频率设置与主模式初始化代码实战
说实话,I2C 主机配置这块,很多人一开始就栽在时钟频率上。我记得刚带团队那会儿,有个同事调了两天,从机就是不响应。最后发现是时钟频率设错了,SCL 波形都变形了。嗯,今天我们就来把这块彻底讲透。
3.1 时钟频率的决定因素
I2C 的时钟频率,说白了就是 SCL 线上的高低电平切换速度。这个速度不是你想设多少就设多少的,它受几个硬性条件约束。
| 参数 | 标准模式 (100kHz) | 快速模式 (400kHz) | 快速+模式 (1MHz) |
|---|---|---|---|
| SCL 低电平时间 | ≥ 4.7μs | ≥ 1.3μs | ≥ 0.5μs |
| SCL 高电平时间 | ≥ 4.0μs | ≥ 0.6μs | ≥ 0.26μs |
| 上升时间 | ≤ 1000ns | ≤ 300ns | ≤ 120ns |
| 总线电容 | ≤ 400pF | ≤ 400pF | ≤ 550pF |
我在项目中遇到过最典型的问题:板子上挂了 5 个从机,总线长度 15cm,结果 400kHz 死活跑不起来。后来用示波器一看,上升沿都变成圆弧了。这就是总线电容太大,驱动能力不够。
⚠️ 我曾经踩过的坑: 别以为 STM32 的 I2C 外设标称支持 400kHz,你就直接配 400kHz。实际能跑多快,取决于你的 PCB 布局、上拉电阻大小、从机数量。我建议先按 100kHz 调通,再逐步提频。
3.2 STM32 I2C 时钟配置原理
STM32 的 I2C 时钟,是从 APB 总线时钟分频得来的。以 STM32F1 为例,它的 I2C 外设挂在 APB1 上,典型频率是 36MHz 或 72MHz(通过分频器)。
时钟配置的核心寄存器就两个:CCR(时钟控制寄存器)和 TRISE(上升时间寄存器)。
- CCR[11:0]:分频系数,决定 SCL 的频率
- CCR[15]:F/S 位,0=标准模式,1=快速模式
- CCR[14]:DUTY 位,快速模式下调整占空比
- TRISE[5:0]:最大上升时间,单位是 APB 时钟周期
计算公式其实很简单:
标准模式:SCL 频率 = APB1 时钟 / (2 × (CCR + 1))
快速模式(DUTY=0):SCL 频率 = APB1 时钟 / (3 × (CCR + 1))
快速模式(DUTY=1):SCL 频率 = APB1 时钟 / (25 × (CCR + 1))
举个例子,APB1=36MHz,想要 100kHz:
CCR = 36MHz / (2 × 100kHz) - 1 = 180 - 1 = 179
TRISE 的值呢?标准模式下就是 SCL 最大上升时间(1000ns)除以 APB1 周期。36MHz 的周期约 27.8ns,所以 TRISE = 1000 / 27.8 ≈ 36。快速模式同理,用 300ns 算。
💡 我的个人习惯: 配 TRISE 时,我一般会在计算值上加 1。因为 STM32 的 TRISE 寄存器是从 1 开始计数的,直接取整容易差一个周期。这个细节,数据手册里写得比较隐晦,我第一次用的时候就被坑了。
3.3 主模式初始化代码实战
好了,理论说完了,直接上代码。下面是我在实际项目里用的初始化函数,基于 STM32F103,HAL 库和标准库我都写了一份。
3.3.1 标准库版本
void I2C_Master_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
I2C_InitTypeDef I2C_InitStructure;
// 1. 使能时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// 2. 配置 GPIO:PB6=SCL, PB7=SDA,开漏输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 复用开漏
GPIO_Init(GPIOB, &GPIO_InitStructure);
// 3. 复位 I2C 外设(我习惯先复位再配置,避免残留状态)
I2C_DeInit(I2C1);
// 4. 配置 I2C 为主模式,100kHz
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; // 标准模式忽略此位
I2C_InitStructure.I2C_OwnAddress1 = 0x00; // 主机不需要地址
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_InitStructure.I2C_ClockSpeed = 100000; // 100kHz
I2C_Init(I2C1, &I2C_InitStructure);
// 5. 使能 I2C
I2C_Cmd(I2C1, ENABLE);
}
3.3.2 HAL 库版本
void I2C_Master_Init_HAL(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
I2C_HandleTypeDef hi2c1;
// 1. 使能时钟
__HAL_RCC_I2C1_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
// 2. GPIO 配置
GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF4_I2C1; // 注意 AF 号,不同系列不同
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// 3. I2C 配置
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 100000;
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.OwnAddress1 = 0;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.OwnAddress2 = 0;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
if (HAL_I2C_Init(&hi2c1) != HAL_OK)
{
// 初始化失败处理
Error_Handler();
}
}
🔑 关键点: 标准库和 HAL 库的配置逻辑是一样的,只是 API 封装不同。我个人更推荐用 HAL 库,因为它的超时机制和错误处理更完善。但如果你对实时性要求极高,标准库的寄存器操作更直接。
3.4 时钟频率配置的避坑指南
这部分内容,是我这些年调试 I2C 积累下来的血泪经验。你想想看,一个简单的时钟配置,能出多少幺蛾子?
- APB 时钟别搞错——STM32F1 的 I2C1 在 APB1 上,但 F4 系列有些 I2C 在 APB2 上。配之前一定查数据手册。
- GPIO 模式必须是开漏——推挽输出会导致总线冲突。我曾经见过有人配成推挽,结果 SDA 拉不低,从机直接罢工。
- 上拉电阻别忽略——STM32 内部有上拉,但驱动能力很弱。我建议外部加 4.7kΩ 上拉,总线电容大时换 2.2kΩ。
- 初始化顺序有讲究——先配 GPIO,再配 I2C,最后使能。顺序反了,寄存器可能锁死。
- 复位外设是个好习惯——系统刚上电时,I2C 外设可能处于未知状态。先 DeInit 再 Init,能避免很多奇怪的问题。
⚠️ 我曾经遇到的一个诡异问题: 板子冷启动时 I2C 通信正常,但热重启后就卡死。查了两天,发现是 I2C 外设没有完全复位,SCL 线被拉低,从机以为主机在传输数据。解决方案就是在初始化前加一段硬件复位代码,强制拉高 SCL 9 个时钟周期。
3.5 知识结构总览
下面这张图,把主机模式配置的整个知识体系串起来了。你可以把它当作调试时的检查清单。
3.6 快速验证方法
代码写完了,怎么确认配置对了?我一般用三个方法:
- 示波器测 SCL 频率——最直接。触发在 SCL 上,看周期对不对。100kHz 对应 10μs 周期,400kHz 对应 2.5μs。
- 逻辑分析仪抓起始条件——配置正确的话,主机发送 START 信号时,SDA 在 SCL 高电平时拉低。波形干净、无毛刺,基本就稳了。
- 寄存器回读——写进去的值读出来对不对?CCR 和 TRISE 有没有被意外修改?我见过 DMA 配置冲突把 CCR 覆盖掉的案例。
💡 一个小技巧: 如果你没有示波器,可以用 GPIO 翻转法。在 I2C 初始化完成后,让一个空闲 GPIO 输出与 SCL 同频的方波,用万用表测频率。虽然精度差点,但能快速判断数量级对不对。